Я прочитал это std::vector
должен быть смежным. Насколько я понимаю, его элементы должны храниться вместе, а не распределяться по памяти. Я просто принял факт и использовал эти знания, когда, например, используя его data()
метод, чтобы получить основной непрерывный кусок памяти.
Однако я столкнулся с ситуацией, когда память вектора ведет себя странным образом:
std::vector<int> numbers;
std::vector<int*> ptr_numbers;
for (int i = 0; i < 8; i++) {
numbers.push_back(i);
ptr_numbers.push_back(&numbers.back());
}
Я ожидал, что это даст мне вектор некоторых чисел и вектор указателей на эти числа. Однако при перечислении содержимого ptr_numbers
указатели, есть разные и, казалось бы, случайные числа, как будто я обращаюсь к неправильным частям памяти.
Я пытался проверить содержимое каждого шага:
for (int i = 0; i < 8; i++) {
numbers.push_back(i);
ptr_numbers.push_back(&numbers.back());
for (auto ptr_number : ptr_numbers)
std::cout << *ptr_number << std::endl;
std::cout << std::endl;
}
Результат выглядит примерно так:
1
some random number
2
some random number
some random number
3
Так что, кажется, когда я push_back()
к numbers
вектор, его старые элементы меняют свое местоположение.
Так что же это значит, что std::vector
такое смежный контейнер и почему его элементы перемещаются? Может быть, он хранит их вместе, но перемещает их все вместе, когда требуется больше места?
Изменить: есть std::vector
смежный только с C ++ 17? (Просто чтобы комментарии к моей предыдущей заявке были актуальны для будущих читателей.)
Это выглядит примерно так (извините, мой шедевр MS Paint):
std::vector
Например, у вас в стеке есть небольшой объект, содержащий указатель на выделенный в куче буфер, а также некоторые дополнительные переменные для отслеживания размера и емкости вектора.
Так что, кажется, когда я
push_back()
кnumbers
вектор, его старые элементы меняют свое местоположение.
Буфер, выделенный для кучи, имеет фиксированную емкость. Когда вы достигнете конца буфера, новый буфер будет размещен где-то еще в куче, и все предыдущие элементы будут перемещены в новый. Поэтому их адреса изменятся.
Может быть, он хранит их вместе, но перемещает их все вместе, когда требуется больше места?
Примерно да. Итератор и адресная стабильность элементов гарантируются при std::vector
только если перераспределение не происходит.
Я знаю, что
std::vector
является смежным контейнером только с C ++ 17
Макет памяти std::vector
не изменился с момента его первого появления в стандарте. ContiguousContainer
это просто «концепция», которая была добавлена, чтобы отличать смежные контейнеры от других во время компиляции.
Это одно смежное хранилище (1d массив).
Каждый раз, когда он исчерпывает свою емкость, он перераспределяется, и сохраненные объекты перемещаются в новое более крупное место — вот почему вы наблюдаете изменение адресов хранимых объектов.
Так было всегда, а не с C++17
,
Склад растет геометрически обеспечить требование амортизируемого O(1)
push_back()
, Коэффициент роста составляет 2 (Крышкап + 1 = СарN + КрышкаN) в большинстве реализаций стандартной библиотеки C ++ (НКУ, лязг, STLPort) и 1,5 (Крышкап + 1 = СарN + КрышкаN / 2) в MSVC вариант.
Если вы предварительно выделите его с vector::reserve(N)
и достаточно большой N
тогда адреса сохраненных объектов не будут меняться при добавлении новых.
В большинстве практических приложений обычно стоит предварительно выделить его как минимум для 32 элементов, чтобы пропустить первые несколько перераспределений, следующих вскоре после друг друга (0 → 1 → 2 → 4 → 8 → 16).
Иногда также целесообразно замедлить его, переключиться на арифметическую политику роста (Крышкап + 1 = СарN + Const) или полностью остановитесь после некоторого достаточно большого размера, чтобы приложение не тратило или не увеличивало объем памяти.
Наконец, в некоторых практических приложениях, таких как хранилища объектов на основе столбцов, возможно, стоит полностью отказаться от идеи непрерывного хранения в пользу сегментированного (то же, что и std::deque
делает, но с гораздо большими кусками). Таким образом, данные могут храниться достаточно хорошо локализованными для запросов как по столбцам, так и по строкам (хотя для этого также может потребоваться некоторая помощь со стороны распределителя памяти).
std::vector
Быть непрерывным контейнером означает именно то, что вы думаете, что это значит.
Тем не менее, многие операции над вектором могут переместить всю эту часть памяти.
Один общий случай — когда вы добавляете элемент к нему, вектор должен расти, он может перераспределять и копировать все элементы в другой непрерывный фрагмент памяти.
Так что же это означает, что std :: vector является смежным контейнером и почему его элементы перемещаются? Может быть, он хранит их вместе, но перемещает их все вместе, когда требуется больше места?
Именно так это работает и почему добавление элементов действительно делает недействительными все итераторы, а также места в памяти, когда происходит перераспределениеation. Это верно не только с C ++ 17, но и с тех пор.
У этого подхода есть пара преимуществ:
data()
Этот метод можно использовать для передачи базовой необработанной памяти в API, которые работают с необработанными указателями.push_back
, reserve
или же resize
сводиться к постоянному времени, так как геометрический рост амортизируется с течением времени (каждый раз push_back
называется емкость удваивается в libc ++ и libstdc ++, и ок. рост в 1,5 раза в MSVC).Эти последствия можно считать недостатком такой схемы памяти:
push_front
(как std::list
или же std::deque
предоставить) не предоставляются (insert(vec.begin(), element)
работает, но возможно дорого¹), а также эффективное слияние / объединение нескольких векторных экземпляров.¹ Спасибо @ FrançoisAndrieux за указание на это.
С точки зрения фактической структуры, std::vector
выглядит примерно так в памяти:
struct vector { // Simple C struct as example (T is the type supplied by the template)
T *begin; // vector::begin() probably returns this value
T *end; // vector::end() probably returns this value
T *end_capacity; // First non-valid address
// Allocator state might be stored here (most allocators are stateless)
};
Соответствующий фрагмент кода из libc++
реализация, используемая LLVM
Печать необработанного содержимого памяти std::vector
:
(Не делайте этого, если вы не знаете, что делаете!)
#include <iostream>
#include <vector>
struct vector {
int *begin;
int *end;
int *end_capacity;
};
int main() {
union vecunion {
std::vector<int> stdvec;
vector myvec;
~vecunion() { /* do nothing */ }
} vec = { std::vector<int>() };
union veciterator {
std::vector<int>::iterator stditer;
int *myiter;
~veciterator() { /* do nothing */ }
};
vec.stdvec.push_back(1); // Add something so we don't have an empty vector
std::cout
<< "vec.begin = " << vec.myvec.begin << "\n"<< "vec.end = " << vec.myvec.end << "\n"<< "vec.end_capacity = " << vec.myvec.end_capacity << "\n"<< "vec's size = " << vec.myvec.end - vec.myvec.begin << "\n"<< "vec's capacity = " << vec.myvec.end_capacity - vec.myvec.begin << "\n"<< "vector::begin() = " << (veciterator { vec.stdvec.begin() }).myiter << "\n"<< "vector::end() = " << (veciterator { vec.stdvec.end() }).myiter << "\n"<< "vector::size() = " << vec.stdvec.size() << "\n"<< "vector::capacity() = " << vec.stdvec.capacity() << "\n";
}