C++ Vector, что происходит, когда он расширяется / перераспределяется в стеке?
Я новичок в C++, и я использую векторный класс в моем проекте. Я нашел это весьма полезным, потому что у меня может быть массив, который автоматически перераспределяет всякий раз, когда это необходимо (т. Е. Если я хочу push_back элемент, и вектор достиг своей максимальной емкости, он перераспределяет себя, запрашивая больше места в памяти ОС), поэтому доступ к элементу вектора очень быстрый (это не похоже на список, чтобы добраться до "n-го" элемента, я должен пройти через "n" первых элементов).
Я нашел этот вопрос очень полезным, потому что их ответы прекрасно объяснили, как работает "распределитель памяти", когда я хочу сохранить свой вектор в куче / стеке:
[1] vector<Type> vect;
[2] vector<Type> *vect = new vector<Type>;
[3] vector<Type*> vect;
Тем не менее, какое-то время меня беспокоит сомнение, и я не могу найти его ответ: всякий раз, когда я создаю вектор и начинаю помещать в него множество элементов, наступает момент, когда вектор будет заполнен, поэтому продолжать расти ему нужно будет перераспределить, скопировать себя в новое место и затем продолжить элементы pushing_back (очевидно, это перераспределение скрыто в реализации класса, поэтому оно полностью прозрачно для меня)
Хорошо, если я создал вектор в куче [2], у меня нет проблем с представлением, что может происходить: класс vector вызывает malloc, получает новое пространство, а затем копирует себя в новую память и, наконец, удаляет старую память, вызывая free.
Однако завеса скрывает то, что происходит, когда я создаю вектор в стеке [1]: что происходит, когда вектор должен перераспределяться? AFAIK, всякий раз, когда на C/C++ вы вводите новую функцию, компьютер просматривает объявление переменных, а затем расширяет стек, чтобы получить необходимое пространство для размещения этих переменных, но вы не можете выделить больше места в стеке, когда функция уже запущена. Как класс-вектор решает эту проблему?
6 ответов
Вы написали
[...] скопируйте себя в новое местоположение [...]
что не так, как вектор работает. Векторные данные копируются в новое местоположение, а не в сам вектор.
Мой ответ должен дать вам представление о том, как устроен вектор.
Общий std::vector layout*
Обратите внимание std::allocator
на самом деле, вероятно, будет пустой класс и std::vector
вероятно не будет содержать экземпляр этого класса. Это может быть не так для произвольного распределителя.
В большинстве реализаций он состоит из трех указателей, где
begin
указывает на начало памяти данных вектора в куче (всегда в куче, если нетnullptr
)end
указывает одну ячейку памяти за последним элементом векторных данных ->size() == end-begin
capacity
указывает на ячейку памяти за последним элементом векторной памяти ->capacity() == capacity-begin
Вектор в стеке
Мы объявляем переменную типа std::vector<T,A>
где T
любой тип и A
тип распределителя для T
(т.е. std::allocator<T>
).
std::vector<T, A> vect1;
Как это выглядит в памяти?
Как мы видим: в куче ничего не происходит, но переменная занимает память, необходимую для всех ее членов в стеке. Вот оно и останется там до vect1
выходит за рамки, так как vect1
это просто объект, как любой другой объект типа double
, int
или что угодно. Он будет сидеть на своем месте в стеке и ждать, пока его уничтожат, независимо от того, сколько памяти он обрабатывает в куче.
Указатели vect1
не указывайте нигде, так как вектор пуст.
Вектор в куче
Теперь нам нужен указатель на вектор и использовать динамическое выделение кучи для создания вектора.
std::vector<T, A> * vp = new std::vector<T, A>;
Давайте снова посмотрим на память.
У нас есть переменная vp в стеке, а вектор находится в куче. Опять же, сам вектор не будет перемещаться в куче, поскольку его размер постоянен. Только указатели (begin
, end
, capacity
) будет перемещаться, чтобы следовать позиции данных в памяти, если происходит перераспределение. Давайте посмотрим на это.
Вставка элементов в вектор
Теперь мы можем начать помещать элементы в вектор. Давайте посмотрим на vect1
,
T a;
vect1.push_back(a);
Переменная vect1
все еще там, где он был, но память в куче была выделена для содержания одного элемента T
,
Что произойдет, если мы добавим еще один элемент?
vect1.push_back(a);
- Пространства, выделенного в куче для элементов данных, будет недостаточно (поскольку это только одна позиция памяти, пока).
- Новый блок памяти будет выделен для двух элементов
- Первый элемент будет скопирован / перемещен в новое хранилище.
- Старая память будет освобождена.
Мы видим: новое место в памяти отличается.
Чтобы получить дополнительное понимание, давайте посмотрим на ситуацию, если мы уничтожим последний элемент.
vect1.pop_back();
Выделенная память не изменится, но деструктор будет вызываться у последнего элемента, а указатель конца перемещается на одну позицию вниз.
Как вы видете: capacity() == capacity-begin == 2
в то время как size() == end-begin == 1
Векторный объект может быть хорошо ориентирован в стеке, но данные в векторе будут в куче.
(Тривиальный класс class foo {int* data;};
имеет такую характеристику)
То, как вы строите свой вектор (стек или куча), для этого не имеет значения.
Смотрите документацию для std::vector
Внутренне, векторы используют динамически распределенный массив для хранения своих элементов. Этот массив может потребоваться перераспределить для увеличения размера при вставке новых элементов, что подразумевает выделение нового массива и перемещение всех элементов в него.
Когда вектор "растет", векторный объект не растет, изменяется только внутренний динамический массив.
Что касается его реализации, вы можете посмотреть на векторную реализацию GCC.
Для простоты он объявляет вектор как класс с одним защищенным членом типа _Vector_impl
,
Как видите, он объявлен как структура, которая содержит три указателя:
- Тот, который указывает на начало хранилища (и начало данных)
- Тот, который указывает на конец данных
- Один на конец хранения
Вы по существу спрашиваете о деталях реализации vector
, Стандарт C++ не определяет, как vector
должен быть реализован - он определяет только то, что vector
должны делать и какие операции должны быть выполнены. Никто не может сказать вам со 100% точностью, что происходит, когда vector
перераспределен, потому что каждый компилятор теоретически отличается.
При этом нетрудно понять, как vector
обычно реализуется. Сам вектор представляет собой просто структуру данных, которая имеет указатель на фактические данные, хранящиеся "в" vector
, Что-то вроде этого:
template <typename Val> class vector
{
public:
void push_back (const Val& val);
private:
Val* mData;
}
Выше, очевидно, псевдокод, но вы поняли идею. Когда vector
размещается в стеке (или в куче):
vector<int> v;
v.push_back (42);
Память может выглядеть так:
+=======+
| v |
+=======+ +=======+
| mData | ---> | 42 |
+=======+ +=======+
Когда ты push_back
для полного вектора данные будут перераспределены:
+=======+
| v |
+=======+ +=======+
| mData | ---> | 42 |
+=======+ +-------+
| 43 |
+-------+
| 44 |
+-------+
| 45 |
+=======+
... и указатель вектора на новые данные теперь будет указывать там.
Вы также можете зарезервировать зарезервированный размер,
vect.reserve(10000);
это зарезервирует 10000 объектных пространств используемого типа
Вектор не является массивом элементов или памятью, используемой для их хранения.
Вектор управляет массивом элементов, память для которых он выделяет, выделяет, сжимает и увеличивает по мере необходимости.
Ваш выбор способа распределения вектора сам по себе не связан с собственными решениями вектора о том, как и где распределять память, которой он управляет, для вас.
Я не хочу препятствовать вашему интересу к тому, как вектор работает внутри (это интересно и полезно), но... весь смысл написания классов и их документирования заключается в том, что вам нужно понимать только интерфейс, а не реализацию.
Если вы создадите какой-либо растущий вектор, от пустого до некоторого размера, будет использовать стратегию удвоения и большую часть времени перераспределение, в этом примере с использованием целого числа от 1 до 10000, и clang (std=2a -O3) получит это, это просто для развлечения, чтобы показать важность использования резерва. vector::begin() указывает на начало фактического массива, а vector:: capacity показывает фактическую емкость. С другой стороны, отображается недействительный итератор.
std::vector<int> my_vec;
auto it=my_vec.begin();
for (int i=0;i<10000;++i) {
auto cap=my_vec.capacity();
my_vec.push_back(i);
if(it!=my_vec.begin()) {
std::cout<<"it!=my_vec.begin() :";
it=my_vec.begin();
}
if(cap!=my_vec.capacity())std::cout<<my_vec.capacity()<<'\n';
}
Это дает следующий результат:
it!=my_vec.begin() :1
it!=my_vec.begin() :2
it!=my_vec.begin() :4
it!=my_vec.begin() :8
it!=my_vec.begin() :16
it!=my_vec.begin() :32
it!=my_vec.begin() :64
it!=my_vec.begin() :128
it!=my_vec.begin() :256
it!=my_vec.begin() :512
it!=my_vec.begin() :1024
it!=my_vec.begin() :2048
it!=my_vec.begin() :4096
it!=my_vec.begin() :8192
it!=my_vec.begin() :16384