由C++ STL的vector容器中存储的对象拷贝引起的对capacity属性 的理解

时间:2021-08-21 19:53:53

【起因】

在测试C++中通过基类引用做形参实现多态的时候,写过一段很挫的测试程序,如下:

Base b;
Derived1 d1;
Derived2 d2;
Derived3 d3;
vector<Base> base_vec;
base_vec.push_back(b);
base_vec.push_back(d1);
base_vec.push_back(d2);
base_vec.push_back(d3);

//通过基类引用做形参实现多态测试
for (int i = 0; i != base_vec.size(); ++i) {
  Polymorphic1(base_vec[i]);
}

         之所以说这段代码很挫,是因为本来利用for循环省事的,却忽略了把派生类存到vector的时候,派生类对象被自动转换为基类对象了,因为vector参数类型是基类的。所以vector里面保存的对象都是Base类型的,而不是派生类的了。再用这些对象进行多态测试,可想结果是怎么悲剧的了。

         我们知道在把对象放入容器中保存的是对象的副本,而不是对象本身,会调用类的拷贝构造函数构造一个新的对象存放在容器里。本想确认下,上述把派生类对象存放到基类的容器中时,是调用派生类的拷贝构造函数?还是调用基类的拷贝构造函数? 当然,现在已经明确了是调用的基类的拷贝构造函数,也就是说先将派生类自动转换为基类对象,然后调用了基类的拷贝构造函数构造了一个新的基类对象存储到容器中的。

【正题】

         然而,当时却有一个很不解的现象:当只想vector中插入一个基类对象或者一个派生类对象时,仅调用一次基类的拷贝构造函数,而当向vector中插入一个基类对象和一个派生类对象时,却调用了3次基类的拷贝构造函数,和一次基类的析构函数;为什么会多调用一次拷贝构造函数和一次析构函数呢?那么我们多向vector里面插入几个对象,是不是可以发现一些规律呢?

  Base b;
  vector<Base> base_vec;
  for(int i = 1; i != 8; ++i) {
    cout << "插入第" << i << "个对象:" << endl;
    base_vec.push_back(b);
    cout << endl;
  }


上述代码逐个向vector中插入7个对象,下面是插入过程中构造函数的调用情况:

由C++ STL的vector容器中存储的对象拷贝引起的对capacity属性 的理解

        我们看上面的结果:在插入第2个对象是,除了本该调用一次拷贝构造函数外,又额外调用了1次拷贝构造函数和1次析构函数;相同的,在插入第3个和第5个元素时候,也分别额外的调用了2次和4次。也就是在插入第n个(n=2,3,5……)对象时,除了本该调用的一次拷贝构造函数外,又额外调用了n-1次构造拷贝函数和n-1次析构函数。这相当于什么呢?相当于在插入第n个对象时,把前n-1个对象拷贝一遍,然后把原来的n-1个对象析构掉。是不是这个样子呢?又为什么需要这么做呢?

        原来,STL 中vector容器存储的对象需要存放在连续的存储空间上。当一片连续的空间被用完而不能插入新的元素时,需要找一块更大的连续空间,来存储vector中的元素。也就是说当vector中插入的元素个数超过一定限制时,就需要扩大其存储空间来存储新的元素,为了保证在元素存储的连续性,需要将之前vector中的元素拷贝到新的空间中,并把原来内存空间中的对象删除掉。

         vector就像一所房子,一开始是一所小房子,等房子的空间被装满了,如果想要再往里面装物品,就需要换一个更大点的房子,还要保证所有物品都房子同一个房子里,那就得把原来小房子里的物品都搬到大房子里来。每次换房子,如果大房子比小房子只能多放一个东西,那么每增加一个物品就得换一次更大的房子,显得很麻烦,老是搬来搬去的,效率低下;但要一次换一个超大的房子,一时间又没那么多物品要放,那么多的空间又显得太浪费了。那么究竟什么时候才换房子呢,换多大的房子合适呢?大概是研究发现:由1变2,满2变4,满4变8,如此递增,这样的成倍扩增空间既可以保证更换不太频繁,也可以保证空间浪费不太大,使两方面都可以平衡。

【vector.size()  &  vector.capacity()】

         vector中有两个属性,一个常用的size,表示当前vector中存储了多少个元素;另一个是capacity,表示当前vector的可用存储空间大小。当size==capacity时,就表明当前vector可用的连续空间已用完,需要更换个大的内存空间。一个空的vector,其size和capacity均为0,然后capacity 按1变2,2变4,满4变8这样的趋势自动扩充存储空间,这就是我们之前看到的结果的原因。

【参考代码】

class Base {
 public:
  Base () {
    cout << "Base()" << endl;
  }

 Base (const Base& b) {
    cout << "Base --> copy-constructor!" << endl;
  }

 ~Base() {
    cout << "~Base()" << endl;
 }
};

下面程序和结果将为我们展示向vector中逐个插入元素对象的过程中,其size 和capacity的变化情况,和之前我们讲的一样:

【参考代码】

int main(int argc, char *argv[])
{
  Base b;
  vector<Base> base_vec;
  cout << "capacity: " << base_vec.capacity() 
       << "  size: " << base_vec.size() << endl << endl;
  for(int i = 1; i != 8; ++i) {
    cout << "插入第" << i << "个对象:" << endl;
    base_vec.push_back(b);
    cout << "capacity: " << base_vec.capacity() 
         << "  size: " << base_vec.size() << endl << endl;
  }
 return 0;
}


【运行结果】

由C++ STL的vector容器中存储的对象拷贝引起的对capacity属性 的理解

         让vector自行扩增其存储空间,初期看起来还是有些更换频繁,如果一开始就知道要用多大的空间,能不能直接为vector申请相应大小的空间呢?答案是肯定的,vector有一成员函数reserve(int size)就可以直接为vector指定存储空间大小。如我们直接为vector分配6个元素对象的空间,那么vector在插入前6元素时候都不需要移动元素,更换空间,因此,每插入一个元素对象,仅做一次构造拷贝;当vector内元素数达6个时,若要插入第8个元素,则须要扩曾其存储空间,其capacity就会倍增为12.

【参考代码】

int main(int argc, char *argv[])
{
  Base b;
  vector<Base> base_vec;
  base_vec.reserve(6);
  cout << "capacity: " << base_vec.capacity() 
       << "  size: " << base_vec.size() << endl << endl;
  for(int i = 1; i != 10; ++i) {
    cout << "插入第" << i << "个对象:" << endl;
    base_vec.push_back(b);
    cout << "capacity: " << base_vec.capacity() 
         << "  size: " << base_vec.size()  << endl;
  }
    return 0;
}


【运行结果】

由C++ STL的vector容器中存储的对象拷贝引起的对capacity属性 的理解


             原创文章,转载请注明: 转载自  

IIcyZhao's Road

          本文链接地址: http://blog.csdn.net/iicy266/article/details/11906637