【问题标题】:Dynamic memory allocation in VectorVector中的动态内存分配
【发布时间】:2017-11-14 19:54:10
【问题描述】:

我对向量中的内存分配有疑问(STL - C++)。据我所知,每当向量的大小等于它的容量时,它的容量就会动态加倍。如果是这种情况,为什么分配是连续的?它如何仍然允许像数组一样使用 [] 访问运算符进行 O(1) 访问?谁能解释这种行为? (List 也有动态内存分配,但是我们不能使用 [] 访问操作符访问它的元素,怎么还能用 vector 呢?)

#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;
}

输出:

1 1
2 2
3 4
4 4
5 8
6 8
7 8
8 8
9 16
10 16

【问题讨论】:

  • 向量将其包含的元素移动到新缓冲区中。
  • 容量没有翻倍。向量如何增加其容量取决于实现。
  • @Someprogrammerdude 但它必须是摊销常量 push_back。
  • @manni66 - 你可以通过增加 50% 来获得它。它留给实现是有原因的。
  • 这将帮助您了解其工作原理:mortoray.com/2014/08/22/…

标签: c++ vector stl dynamic-memory-allocation


【解决方案1】:

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

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

如果是这样,怎么分配是连续的?

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

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

由于前面所说的事实,可以使用[] operatorpointer + offset 进行访问。对数据的访问将是 O(1)。

List 也有动态内存分配,但是我们不能使用 [] 访问操作符访问它的元素,怎么还能用 vector 呢?

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

【讨论】:

  • C++ 中的std::list 实现为双链表,如here: List containers are implemented as doubly-linked lists; Doubly linked lists can store each of the elements they contain in different and unrelated storage locations.
  • 你在写,我在想std::dequeue。我已经删除了我之前的(错误的)评论,稍后将删除这条评论...
  • 嗨@Abhinav1602,如果这个或任何答案解决了您的问题,请点击复选标记考虑accepting it。这向更广泛的社区表明您已经找到了解决方案,并为回答者和您自己提供了一些声誉。没有义务这样做。
【解决方案2】:

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

这可以通过使用带有复制/移动构造函数的类来实现,并具有一些副作用 (ideone link):

#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)) 递增地址到另一个连续区域。最后,当添加第五个对象,你可以看到第三个对象确实是直接放在第二个之后的。

它何时移动以及何时复制? 移动构造函数被视为when it is markednoexcept(或者编译器可以推断)。否则,如果它被允许抛出,向量可能会处于这样一种状态,即它的部分对象在新的内存区域中,但其余部分仍然在旧的内存区域中。

【讨论】:

    【解决方案3】:

    应该从 2 个不同的层面考虑这个问题。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-23
      • 2012-01-13
      相关资源
      最近更新 更多