std::vector 在内存中是啥样的?
Posted
技术标签:
【中文标题】std::vector 在内存中是啥样的?【英文标题】:What does std::vector look like in memory?std::vector 在内存中是什么样的? 【发布时间】:2019-02-19 03:37:39 【问题描述】:我读到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 开始连续? (只是为了让我之前的声明中的 cmets 与未来的读者相关。)
【问题讨论】:
vector 必须遵守其保证元素连续的承诺,这意味着如果 vector 必须将元素移动到更大的空间,它会这样做。 -- 我知道,std::vector 仅在 C++17 之后才成为连续容器 -- 自 1998 年以来一直是连续的。 这是我在 cppreference 上读到的:std::vector(用于 T 而非 bool)满足 Container、AllocatorAwareContainer、SequenceContainer、ContiguousContainer(自 C++17 起)和 ReversibleContainer 的要求。跨度> @McSim 这些只是现在使用的官方名称。始终保证std::vector
将其项目存储在连续的内存中。非正式地它是连续的,因为标准中有一个缺陷没有说明它是连续的。然后(如果我记得的话),缺陷在 03 标准中得到纠正。
如果要观察连续内存的效果,请提前使用reserve
。
@PaulMcKenzie 感谢您的解释。
【参考方案1】:
如果您尝试以这种方式编码,您将看到值保持不变,并且向量中每个值的地址与其相邻元素的差异为 4(有趣)。
std::vector<int> numbers;
std::vector<int*> ptr_numbers;
// adding values 0 up to 8 in the vector called numbers
for (int i = 0; i < 8; i++)
numbers.push_back(i);
// printing out the values inside vector numbers
//and storing the address of each element of vector called numbers inside the ptr_numbers.
for (int i = 0; i != numbers.size(); i++)
cout << numbers[i] << endl;
ptr_numbers.push_back(&numbers[i]);
cout << "" << endl;
// printing out the values of each element of vector ptr_numbers
for (int y = 0; y != ptr_numbers.size(); y++)
cout << *ptr_numbers[y] << endl;
// printing out the address of each element of vector ptr_numbers
for (int y = 0; y != ptr_numbers.size(); y++)
cout << &ptr_numbers[y] << endl;
当您遍历两个向量时。它们将输出相同的值。
【讨论】:
相当误导的答案,有很多冗余【参考方案2】:答案
这是一个单一的连续存储(一维数组)。 每次容量用完时,它都会重新分配并将存储的对象移动到新的更大的地方——这就是为什么你会观察到存储对象的地址发生变化的原因。
从C++17
开始就不是这样了。
TL;博士
存储在几何上增长,以确保摊销O(1)
push_back()
的需求。生长因子为 2(Capn+1 = Capn + Capn)在 C++ 标准库(GCC、Clang、STLPort)和 1.5(Capn+1 = Capn + Capn / 2) 在MSVC 变体中。
如果您使用vector::reserve(N)
预先分配它并且N
足够大,那么当您添加新对象时,存储对象的地址不会改变。
在大多数实际应用中,通常值得将其预分配给至少 32 个元素,以跳过紧随其后的前几次重新分配(0→1→2→4→8→16)。
有时也可以放慢速度,切换到算术增长策略 (Capn+1 = Capn + Const),或者在相当大的大小后完全停止,以确保应用程序不会浪费或增长内存。
最后,在一些实际应用中,例如基于列的对象存储,可能值得完全放弃连续存储的想法,转而使用分段存储(与std::deque
所做的相同,但块更大)。通过这种方式,可以为每列和每行查询合理地本地化存储数据(尽管这也可能需要内存分配器的一些帮助)。
【讨论】:
吹毛求疵:增长因素取决于实施。我听说 MSVC 使用 1.5 增长因子由实现定义。 @bobah 实际上,在某些实现中它不是 2 倍。 事实上,增长因子 2 相当糟糕,而 1.5 倍实际上效果更好,尽管这可能看起来违反直觉。还有since this is well-known,我有点惊讶 stdlibc++ 和 libc++ 仍然使用 2x。 对于那些好奇且不愿意关注链接的人,增长因子为 2 会导致行走向量问题;为向量分配的先前缓冲区的总和永远不会完全适合其新分配。因此,在一个唯一有意义的分配是不断增长的 std::vector 的系统上,2x 增长因子意味着您消耗几乎 2 倍您期望的内存,一半的内存是返回的向量缓冲区处于闲置状态。使用 1.5,在 ... 4 之后?循环之前的缓冲区足够大,可以分配一个新的缓冲区。【参考方案3】:就实际结构而言,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)
;
Relevant code snippet from the libc++
implementation as used by 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"
;
【讨论】:
【参考方案4】:大致是这样的(请原谅我的 MS Paint 杰作):
堆栈上的std::vector
实例是一个小对象,其中包含一个指向堆分配缓冲区的指针,以及一些额外的变量来跟踪向量的大小和容量。
所以似乎当我将
push_back()
转移到numbers
向量时,它的旧元素改变了它们的位置。
堆分配的缓冲区具有固定容量。当你到达缓冲区的末尾时,一个新缓冲区将被分配到堆上的其他地方,所有之前的元素都将被移动到新的缓冲区中。他们的地址因此会改变。
它是否可能将它们存储在一起,但在需要更多空间时将它们一起移动?
大概,是的。 std::vector
保证元素的迭代器和地址稳定性仅当不发生重新分配。
我知道,
std::vector
是自 C++17 以来的连续容器
std::vector
的内存布局自其首次出现在标准中以来没有改变。 ContiguousContainer
只是一个“概念”,用于在编译时将连续容器与其他容器区分开来。
【讨论】:
为什么绘图中的箭头不完全垂直?在投票之前让我犹豫了。 @lubgr:我故意把它弄歪了,这样就不会被误认为operator->
。
"元素的迭代器和地址稳定性不保证" - 实际上是,只要不发生重新分配。如果你push_back()
和size < capacity
,地址稳定性有保证
@VittorioRomeo 这太荒谬了,operator->()
是水平的。另一方面,operator↑()
...
@lubgr“完全垂直”箭头需要硬件支持【参考方案5】:
那么,std::vector 是一个连续的容器究竟是什么意思,为什么它的元素会移动呢?它是否可能将它们存储在一起,但在需要更多空间时将它们一起移动?
这正是它的工作原理以及为什么在重新分配发生时附加元素确实会使所有迭代器以及内存位置无效¹。这不仅从 C++17 开始有效,从那以后一直如此。
这种方法有几个好处:
它对缓存非常友好,因此非常高效。data()
方法可用于将底层原始内存传递给使用原始指针的 API。
在push_back
、reserve
或resize
上分配新内存的成本归结为恒定时间,因为几何增长会随着时间的推移而摊销(每次push_back
被称为容量在 libc++ 和 libstdc++ 中翻倍,并且在 MSVC 中大约增长了 1.5 倍)。
它允许最受限制的迭代器类别,即随机访问迭代器,因为当数据连续存储时,经典指针算法效果很好。
从另一个向量实例移动构造非常便宜。
这些影响可以被认为是这种内存布局的缺点:
所有迭代器和指向元素的指针在修改意味着重新分配的向量时无效。这可能会导致微妙的错误,例如在遍历向量的元素时擦除元素。 不提供像push_front
(如std::list
或std::deque
提供)之类的操作(insert(vec.begin(), element)
有效,但可能很昂贵¹),以及多个向量实例的高效合并/拼接。
¹ 感谢@FrançoisAndrieux 指出这一点。
【讨论】:
我喜欢提到的优点和缺点,但我会暂时保留当前接受的答案,因为它更多的是回答问题而不是附加说明。很遗憾,虽然我不能接受两个答案:)【参考方案6】:std::vector
是一个连续的容器,这正是您认为的意思。
但是,向量上的许多操作可以重新定位整个内存。
一种常见的情况是,当您向其中添加元素时,向量必须增长,它可以重新分配所有元素并将其复制到另一块连续的内存中。
【讨论】:
以上是关于std::vector 在内存中是啥样的?的主要内容,如果未能解决你的问题,请参考以下文章