为什么连续 vector::push_back 导致不同数量的构造函数调用?

Why successive vector::push_back results into different number of constructor call?

class base
{
    private:
            int k;
    public:
            base(const base& b){ this->k = b.k; cout<<"  c-ctor "<<endl; }
            base(int a = 10){ k = a; }

            ~base(){cout << "destructor called\n";}
};

int main()
{
    base b, b1(2);
    vector<base> m;
    cout << "first pushback" <<endl;
    m.push_back(b);
    cout << "2nd pushback" <<endl;
    m.push_back(b1);
    cout << "3rd pushback" <<endl;
    m.push_back(b1);
    cout << "4th pushback" <<endl;
    m.push_back(b);
    cout << "5th pushback" <<endl;
    m.push_back(b);
    cout<<" =============================================== "<<endl;

    return 0;
}

输出:

first pushback
  c-ctor 
2nd pushback
  c-ctor 
  c-ctor 
destructor called
3rd pushback
  c-ctor 
  c-ctor 
  c-ctor 
destructor called
destructor called
4th pushback
  c-ctor    
5th pushback
  c-ctor 
  c-ctor 
  c-ctor 
  c-ctor 
  c-ctor 
destructor called
destructor called
destructor called
destructor called
 =============================================== 

destructor called
destructor called
destructor called
destructor called
destructor called
destructor called
destructor called

为什么ithpush_back导致i个拷贝构造函数来电?

这不是调整大小的效果(即再次复制原始向量)并且向向量中插入元素的效率很低吗?

为什么 4th push_back 的行为与 2th 不同, 3th abd 5thpush_back?

Demo

矢量需要扩展以容纳新元素。这需要将现有元素复制到新缓冲区中。

如果您的 class 是 noexcept 移动构造函数,它会生成对移动构造函数的调用。

并不是每个 push_back 都会导致矢量重新分配内存的原因,可能是由于您的标准库实现试图在调整大小后保留一些额外的内存,以避免不得不过于频繁地这样做。这样做是 vector 实现的自由,并且有几种策略可以确定要保留多少。

std::vector 可以看作是一个动态数组。因此它会在有需要时增长,这需要向量分配更多内存并且 copy 向量中的现有元素到新的更大的内存。

然后销毁旧内存中的现有对象。

如果您事先知道向量中需要多少个元素,则可以 reserve 为向量存储。通过这样做,矢量不需要重新分配和复制数据,直到您达到新的容量。

我真的建议你阅读更多关于 std::vector 的内容,尤其是它的 容量 大小 之间的区别。

没什么大不了的。每次 size 达到 capacity 时,向量都会重新分配。所有元素都从旧向量复制到新向量。

一般情况下,新的vector会分配原来容量的两倍。

  1. 在第一个push_back之前,容量是1,所以不需要重新分配。
  2. 对于第二个 push_back,容量需要加倍,因此调用了两次复制构造函数,第一次是将旧元素复制到新向量,第二次是 push_back。容量现在是 2.
  3. 第三个push_back再次需要重新分配向量,因为容量现在是2。重新分配后容量变为4。
  4. 现在没有重新分配发生,所以只需调用一次复制 ctor(对于 push_back)。容量还是4.
  5. 对于第 5 个 push_back,发生重新分配,4 个旧元素和 1 个新元素 (push_back) 被复制到新向量。现在容量为 8.

如果你继续往前看,你会发现重新分配将在 9 日 push_back

此外,在重新分配时需要调用析构函数,此时不再需要旧向量,因此应销毁其中的成员。

退一步讲,std::vector有一个关键要求:vector中的元素必须连续存储。这意味着您可以安全地使用指向向量中元素的指针作为指向该类型元素的原始指针,因此可以像您对固定大小数组所期望的那样使用数组语义和指针算法。

这也意味着通过其索引访问数组的任何元素都非常快,并且无论数组的大小以及您正在访问的特定元素的索引(这是称为 O(1) 或 "constant time" 操作)。

这个福利需要付出的代价是重新分配。创建向量时,您通常不知道将使用多少个元素,因此您分配一定数量的连续内存来存储一定数量的元素。如果你继续添加到向量中,你将超过这个初始分配。但是您不能只扩展向量使用的内存量,因为您的程序可能已将当前向量之后的内存立即分配给其他变量。确保矢量元素保持连续的唯一方法是分配一个足够大的新块以包含所有元素,包括附加元素,然后将所有元素复制到这个新位置。

正如其他人所指出的,每次超出容量时,通过分配原始数组大小的 1.5 或 2 倍,以指数方式增加数组大小是很常见的。这减少了整个数组在增长时需要重新分配的频率。

如果您想要一个元素集合,在其中添加元素总是相对较快(或者至少总是花费相同的时间),您可以使用链表 (std::list),但是你不能再通过索引快速访问任何元素 ("random access").

"rather inefficient way of inserting elements into a vector"

正如其他人所指出的,您可以提示 std::vector 在第一时间为您保留足够的内存,以允许将项目推到您的 vector 上达到此限制,而无需重新分配。如果您更改代码以将以下调用添加到 reserve:

vector<base> m;
m.reserve(5);
cout << "first pushback" <<endl;

使用 reserve 不会更改向量的 大小 ,但它会分配足够的内存来容纳该大小的连续向量,从而避免重新分配。在您的示例中,您会看到调用的复制构造函数和析构函数的数量大大减少。如果您知道(大致相同)向量的大小和类型的构造/破坏是昂贵的,那么这会对您的代码产生很大的影响。

请注意 reserveresize 非常不同 - 第一个在您将项目推到向量上时最有用(您的示例),第二个将创建一个完全填充的项目向量,运行拷贝构造函数N次;这在随后通过下标运算符访问和更改项目时很有用。