【问题标题】:c++ Vector, what happens whenever it expands/reallocate on stack?c++ Vector,当它在堆栈上扩展/重新分配时会发生什么?
【发布时间】:2013-06-22 09:42:38
【问题描述】:

我是 C++ 新手,我在我的项目中使用矢量类。我发现它非常有用,因为我可以拥有一个在必要时自动重新分配的数组(即,如果我想 push_back 一个项目并且向量已经达到它的最大容量,它会重新分配自己,向操作系统请求更多内存空间),所以访问向量的元素非常快(它不像列表,要到达“n-th”元素,我必须经过“n”个第一个元素)。

我发现this question 非常有用,因为他们的回答完美地解释了当我想将向量存储在堆/堆栈上时 “内存分配器” 的工作原理:

[1] vector<Type> vect;
[2] vector<Type> *vect = new vector<Type>;
[3] vector<Type*> vect;

但是,一个疑问困扰了我一段时间,我找不到答案: 每当我构建一个向量并开始将 很多 项目推入时,它会达到向量已满的时刻,因此要继续增长,它需要重新分配,将自身复制到一个新位置并然后继续 push_back 项目(显然,这种重新分配隐藏在类的实现中,所以它对我来说是完全透明的

好吧,如果我在堆上创建了向量 [2],我可以想象会发生什么:类向量调用 malloc,获取新空间,然后将自身复制到新内存中,最后删除旧内存免费通话。

但是,当我在堆栈上构造一个向量时,面纱隐藏了正在发生的事情 [1]:当向量必须重新分配时会发生什么? AFAIK,每当您在 C/C++ 上输入一个新函数时,计算机都会查看变量的声明,然后 展开 堆栈以获得放置这些变量所需的空间,但您无法分配当函数已经运行时,堆栈上有更多空间。类向量是如何解决这个问题的?

【问题讨论】:

  • 显然,那个问题的答案根本没有完美地解释它,因为你的想法完全错误。
  • 向量将其数据分配到可以在运行时增长的地方。矢量对象本身的大小保持固定,因为它为动态分配的数据保留了一个固定大小的句柄。
  • 好的,有了下面的答案,我了解了向量对象的数据结构:就像下面的@juanchopanza 所说,它是一组指针,定义了位于堆上的数组的大小,其中存储了对象在上面。由于这个数组在堆上,如果需要更多空间,它可能会被重新分配。顺便说一句,对不起英语语法!我希望通过实践来改进它!
  • 这是一个常见的误解,认为仅仅因为你说 std::vector&lt;...&gt; myvect;std::vector&lt;...&gt; *myvect = new std::vector&lt;...&gt;; 你最终会 - 对于内容 (!) - 使用 堆栈分配前者但 堆分配 后者。不是这样;而对于new ... 的情况,堆几乎是有保证的,容器类型的内部实现决定了当你在本地实例化它时会发生什么。只有某些容器(即std::array)会嵌入它们的内容。 std::vector 没有。

标签: c++ vector stack allocator


【解决方案1】:

如果您制作一些不断增长的向量,从空到某个大小,将使用加倍策略,并且大部分时间重新分配,在本示例中使用从 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

【讨论】:

    【解决方案2】:

    你写的

    [...] 将自身复制到新位置 [...]

    这不是矢量的工作方式。矢量数据被复制到新位置,而不是矢量本身。

    我的回答应该让您了解矢量是如何设计的。

    常见的 std::vector 布局*

    注意:std::allocator 实际上很可能是一个空类,std::vector 可能不包含此类的实例。对于任意分配器,这可能不是真的。

    在大多数实现中,它由三个指针组成

    • begin 指向堆上向量的数据内存的开始(如果不是nullptr,则始终在堆上)
    • end 指向向量数据最后一个元素之后的一个内存位置 -> size() == end-begin
    • capacity 指向向量内存最后一个元素之后的内存位置 -> capacity() == capacity-begin

    堆栈上的向量

    我们声明了一个std::vector&lt;T,A&gt; 类型的变量,其中T 是任何类型,AT 的分配器类型(即std::allocator&lt;T&gt;)。

    std::vector<T, A> vect1;
    

    这在记忆中是什么样子的?

    如我们所见:堆上什么都没有发生,但变量占用了堆栈上所有成员所需的内存。 它就在那里,直到vect1 超出范围,因为vect1 只是一个对象,就像doubleint 或其他类型的任何其他对象一样。不管它在堆上处理多少内存,它都会坐在它的堆栈位置上等待被销毁。

    vect1 的指针不指向任何地方,因为向量是空的。

    堆上的向量

    现在我们需要一个指向向量的指针并使用一些动态堆分配来创建向量。

    std::vector<T, A> * vp = new std::vector<T, A>;
    

    让我们再看看内存。

    我们的 vp 变量在堆栈上,我们的向量现在在堆上。同样,向量本身不会在堆上移动,因为它的大小是恒定的。如果发生重新分配,只有指针(beginendcapacity)将移动到内存中的数据位置之后。让我们来看看。

    将元素推送到向量

    现在我们可以开始将元素推送到向量。我们来看看vect1

    T a;
    vect1.push_back(a);
    

    变量vect1 仍在原来的位置,但堆上的内存被分配为包含T 的一个元素。

    如果我们再添加一个元素会发生什么?

    vect1.push_back(a);
    

    • 在堆上为数据元素分配的空间将不够(因为它只有一个内存位置)。
    • 将为两个元素分配一个新的内存块
    • 第一个元素将被复制/移动到新存储中。
    • 旧内存将被释放。

    我们看到:新的内存位置不同了。

    为了更深入地了解,让我们看看我们销毁最后一个元素的情况。

    vect1.pop_back();
    

    分配的内存不会改变,但最后一个元素将调用其析构函数,并且结束指针向下移动一个位置。

    如您所见:capacity() == capacity-begin == 2size() == end-begin == 1

    【讨论】:

    • 嗯...我想我的第一个想法是我们对实现错误:由于向量的数据具有固定大小,因此无论向量有多大,它都可以存储在堆栈中而无需任何问题:allocator 会要求更多的堆大小并且指向数据的指针会改变。现在我明白了为什么使用 GDB 来查看向量上存储了哪些数据并不那么直观。谢谢@Pixelchemist!
    • @Karl:如果你想要一个堆栈分配的“向量”,请使用std::array。缺点是......它不能调整大小。正是因为 std:array(容器及其内容)驻留在堆栈中,并且不可能“在堆栈中混洗”。
    • 您确定“分配器对象”在堆栈上占用空间吗? sizeof(std::vector&lt;int&gt;) 分别为 32 位和 64 位架构返回 12 和 24。 (x86_64, Ubuntu 13.04, gcc 4.7.3)
    • @stanm:std::allocator 可能为空,但您可以拥有一个需要实例的用户定义分配器。 IE。在 MSVS 中,std::vector 包含一个非空分配器实例。
    • @Pixelchemist:哦,我明白了,std::vector 本质上是一个与 std::vector 不同的类。当然,您需要将该分配器保存在某个地方。谢谢。
    【解决方案3】:

    向量不是元素数组,也不是用于存储这些元素的内存。

    一个向量管理一个元素数组,它根据需要为其分配、取消分配、收缩和增长的内存。

    您对如何分配向量本身的选择与向量自己关于如何以及在何处分配它为您管理的内存的决定没有任何关系

    我不想阻止您对矢量内部如何工作的兴趣(它既有趣又有用),但是......编写类和记录它们的全部意义在于您只需要 了解接口,而不是实现。

    【讨论】:

    • 谢谢!您的回答补充了@juanchopanza 的评论。是的,我同意编写类和文档的全部意义在于关注接口。但是,我认为人们必须至少对如何实现一个类以能够编写好的代码并预测可能的错误有一个模糊的概念:没有妖精在做内存分配的肮脏工作!
    【解决方案4】:

    您实际上是在询问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   |
                    +=======+
    

    ...指向新数据的向量指针现在将指向那里。

    【讨论】:

      【解决方案5】:

      你也可以保留预定的大小,

      vect.reserve(10000);
      

      这将保留 10000 个所用类型的对象空间

      【讨论】:

        【解决方案6】:

        您构造向量(堆栈或堆)的方式与此无关。

        请参阅std::vector 的文档

        在内部,向量使用动态分配的数组来存储它们的元素。当插入新元素时,可能需要重新分配该数组以增加其大小,这意味着分配一个新数组并将所有元素移至该数组。

        当向量“增长”时,向量对象不会增长,只是内部动态数组发生变化。

        至于它的实现,可以看GCC的vector实现。

        为了简单起见,它将vector声明为with one protected member的类_Vector_impl

        如你所见,它被声明为一个包含三个指针的结构体:

        • 一个指向存储开始(和数据开始)的位置
        • 指向数据末尾的那个
        • 一个用于存储结束

        【讨论】:

        • 是的,我知道向量的 content 存储在堆上(在我提出问题的每个案例中),但是... 是什么 在第一种情况下,在堆栈上实例化的向量对象的内部数据?我认为它可能是一个“指针数组”(就像 C 上的经典数组),每当你 push_back 一个项目时,向量类都会在堆上分配新内存,然后添加一个指向这个指针数组的指针
        • @Karl 它可以像指向数据的指针一样简单,加上大小和容量的数据成员。
        • 我的意思是,如果你在堆上使用 ->std::vector *vector;编辑:当然@juanchopanza,我没想到你的例子......它有道理
        • “内容”(即支持向量的 actual 数组,又名std::vector::data())总是堆分配的。对于堆栈分配的类向量容器,您需要使用std::array - 它具有与std::vector 相同的迭代器和访问器sans 调整大小的能力,并且它是由大小模板化的。跨度>
        • @Karl:vector 的一个常见实现只有成员(非调试版本)三个指针T *begin,*end,*capacity;(如果无法优化,可能还有一个分配器)。该向量不包含指向所有元素的指针,而是指向第一个元素的单个指针和两个 sentries 以确定 valid 元素的停止位置 (end) 和容量结束的地方 (capacity)。添加更多元素将需要增长,这反过来会将三个指针重置为不同的值,但堆栈中的结构(即std::vector&lt;int&gt; v; 中的v)不会改变它的大小
        【解决方案7】:

        向量对象很可能在堆栈上实例化,但向量中的数据将在堆上。

        (平凡的类class foo {int* data;};有这个特点)

        【讨论】:

        • 事实上,如果在堆栈上构建了一个 foo 实例,那么您的普通类在堆栈上有一个指针。所有指针类型变量声明的共同特征。甚至int * data; 也显示出来了。
        • 是的,你是对的,但我要说的是数据点的内存(可能)是堆分配的。粗略地说,这就是 stl 向量的工作方式。
        猜你喜欢
        • 2014-12-21
        • 2010-11-22
        • 2022-11-16
        • 2013-10-07
        • 2016-01-20
        • 2021-11-29
        • 2021-03-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多