首页 > 代码库 > C++:vector的内部行为

C++:vector的内部行为

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。


C++:vector的内部行为