C++中的vector是一个非常灵活的数组,它可以自动扩充大小来容纳新的元素,也可以快速地索引存储的元素,然而,这种使用上的便捷也是有代价的,因为vector的底层数据结构确实是一个数组,只是封装了一些便利的操作,像push_back()、reserve()等,下面我们就通过例子来看一下这些简便操作背后的行为,为了说明问题,我们定义了一个类,并在其构造函数中输出一些信息,类定义如下:
class A { static int i; public: int id; A() { id = i++; cout<<"A: constructor"<<endl; } A(const A&) { id = i++; cout<<"A: copy constructor"<<endl; } ~A() { cout<<"A: destructor"<<endl; } }; int A::i = 0;
这个类有一个默认构造函数、一个拷贝构造函数和一个析构函数,同时我们试图给A的每个对象一个id,首先来看一下vector的最简单用法:定义一个vector:
vector<A> v1(2); cout<<"id of the 1st element: "<<v1[0].id<<endl; cout<<"id of the 1nd element: "<<v1[1].id<<endl;
定义v1的时候我们默认它将保存两个对象,程序的输出结果如下:
A: constructor
A: copy constructor
A: copy constructor
A: destructor
id of the 1st element: 1
id of the 2nd element: 2
A: destructor
A: destructor
可以看到,在定义v1的时候,总共调用了1次默认构造函数和2次拷贝构造函数,定义过后,v1中便已经存储了2个对象,且它们的id分别为1、2,这说明v1中的2个对象是通过拷贝构造函数得到的,那么拷贝的是谁呢?当然就是调用默认构造函数生成的那个临时对象,这个临时对象在定义完v1后就被析构了,所以在输出id之前会先打印一个析构函数被调用的消息。接下来换一种定义方式:
vector<A> v1; v1.reserve(2); cout<<"id of the 1st element: "<<v1[0].id<<endl; cout<<"id of the 1nd element: "<<v1[1].id<<endl;
这里通过reserve来为v1保留两个对象的存储空间,输出结果如下:
id of the 1st element: 0
id of the 2nd element: 0
可以看到,在这种情况下,v1中不会自动构造并保存2个对象,但是我们还能侥幸地对这两个对象进行操作,尽管这两个操作是毫无意义的。接下来看看reserve()是怎么预留存储空间的:
vector<A> v1(2); v1.reserve(3);
输出结果如下:
#1 A: constructor
#2 A: copy constructor
#3 A: copy constructor
#4 A: destructor
#5 A: copy constructor
#6 A: copy constructor
#7 A: destructor
#8 A: destructor
#9 A: destructor
#10 A: destructor
大家可以数数共调用了几次构造函数,前三次构造函数在我们的预期之中,但是随着reserve的调用,我们又调用了两次拷贝构造函数,这是因为reserve为我们弄了个更大的数组,并试图把原数组里面的东西放到新数组里,之后便会删掉原来的数组,这一点也可以从#7、#8两行的析构函数可以看出来(析构掉了原数组里的两个对象),所以改变v1的大小并不像使用起来那么简单,有可能引发大量的构造析构操作。下面再来看一个例子:
A a; vector<A> v1; v1.reserve(2); v1.push_back(a); v1.push_back(a); v1.push_back(a);
在这个例子中,开始给v1预留了2个对象的空间,但随后插入了3个对象,可以看看输出结果是什么:
A: constructor
A: copy constructor
A: copy constructor
A: copy constructor
A: copy constructor
A: copy constructor
A: destructor
A: destructor
A: destructor
A: destructor
A: destructor
A: destructor
可以看到,共调用了5个拷贝构造函数!前两个好理解,是伴随着前两个push_back而被调用的,但是第三个push_back的时候,因为v1中的空间不够,所以重新弄了个数组,首先把原来数组里的两个对象拷贝过来,然后才将第三个对象push进新数组里,所以第三个push_back引发了3个拷贝构造函数,当然同时也额外引发了两个析构函数,所以vector虽然好用,但是某些时候可能引发大量的内存分配及构造析构操作,如果应用程序对这个敏感,就要考虑其他的容器了,像list。