矢量中的动态内存分配

Dynamic memory allocation in Vector

本文关键字:内存 分配 动态      更新时间:2023-10-16

我对向量(STL - C++)中的内存分配有疑问。据我所知,每当向量的大小等于其容量时,它的容量都会动态翻倍。如果是这样的话,为什么分配是连续的?它如何仍然允许像数组一样使用 [] 访问运算符进行 O(1) 访问?谁能解释这种行为? (列表也有动态内存分配,但我们无法使用 [] 访问运算符访问其元素,矢量怎么可能?

#include<iostream>
#include<vector>
using namespace std;
int main() {
// your code goes here
vector<int> v;
for(int i=0;i<10;i++){
v.push_back(i);
cout<<v.size()<<" "<<v.capacity()<<" "<<"n";
}
return 0;
}

输出:

11
2 2 3
4 4
4 5
8 6 8 7 8 8 8


8 9
16
10 16

据我所知,每当向量的大小等于其容量时,它的容量就会动态地加倍。

不需要像您的情况那样加倍,它是定义的实现。因此,如果您使用其他编译器,则可能会有所不同。

如果是这样的话,为什么分配是连续的?

如果向量可以分配的连续内存不再多,则向量必须将其数据移动到满足其大小要求的新连续内存块。旧块将被标记为自由,以便其他人可以使用它。

它如何仍然允许像数组一样使用 [] 访问运算符进行 O(1) 访问?

由于之前所说的事实,可以通过[] operatorpointer + offset访问。对数据的访问将是 O(1)。

List 也有动态内存分配,但我们无法使用 [] 访问运算符访问其元素,矢量怎么可能?

列表(例如 std::list)与 std::vector 完全不同。在 C++ std::list 的情况下,它保存节点和数据、指向下一个节点的指针和指向前一个节点的指针(双链表)。因此,您必须浏览列表才能获得所需的特定节点。 向量的工作方式如上所述。

向量必须将对象存储在一个连续的内存区域中。因此,当它需要增加其容量时,它必须分配一个新的(更大的)内存区域(或者扩展它已经拥有的内存区域,如果可能的话),并将对象从"旧的,小"的区域复制或移动到新分配的区域。

这可以通过使用带有带有具有一些副作用的复制/移动构造函数的类来变得明显(ideone 链接):

#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
#define V(p) static_cast<void const*>(p)
struct Thing {
Thing() {}
Thing(Thing const & t) {
cout << "Copy " << V(&t) << " to " << V(this) << endl;
}
Thing(Thing && t) /* noexcept */ {
cout << "Move " << V(&t) << " to " << V(this) << endl;
}
};
int main() {
vector<Thing> things;
for (int i = 0; i < 10; ++i) {
cout << "Have " << things.size() << " (capacity " << things.capacity()
<< "), adding another:n";
things.emplace_back();
}
}

这将导致输出类似于

[..]
Have 2 (capacity 2), adding another:
Move 0x2b652d9ccc50 to 0x2b652d9ccc30
Move 0x2b652d9ccc51 to 0x2b652d9ccc31
Have 3 (capacity 4), adding another:
Have 4 (capacity 4), adding another:
Move 0x2b652d9ccc30 to 0x2b652d9ccc50
Move 0x2b652d9ccc31 to 0x2b652d9ccc51
Move 0x2b652d9ccc32 to 0x2b652d9ccc52
Move 0x2b652d9ccc33 to 0x2b652d9ccc53
[..]

这表明,当向量添加第三个对象时,它已经包含的两个对象从一个连续区域(查看 1 (sizeof(Thing)) 递增地址移动到另一个连续区域。最后,在添加第五个对象时,您可以看到第三个对象确实直接放置在第二个对象之后。

它何时移动,何时复制?当移动构造函数标记为noexcept时,将考虑移动构造函数(或者编译器可以推断出它)。否则,如果允许抛出,向量最终可能会处于这样的状态:其对象的一部分位于新的内存区域中,但其余部分仍在旧内存区域中。

这个问题应该在两个不同的层面上考虑。

从标准的角度来看,需要提供一个连续存储,以允许程序员使用其第一个元素的地址作为数组第一个元素的地址。当您通过重新分配仍然保留以前的元素来添加新元素时,需要让它的容量增长 - 但它们的地址可能会改变。

从实现的角度来看,它可以尝试就地扩展分配的内存,如果不能,则分配一个全新的内存,并在新的分配内存区域中移动或复制构造现有元素。标准未指定大小增加,留给实现。但你是对的,每次将分配的大小加倍是常见的用法。