预留足够空间
vector
实际上是能够动态增长的数组,因此,当vector
的容量 ( capacity ) 不足以容纳新元素的时候,vecvtor
就会重新分配较大的内存,将原来的元素移动到新的内存,并回收旧的内存。这个过程会导致几个问题:
- 每一次重新分配内存的操作都会带来不小的开销。
- 由于旧的内存被系统回收,因此,原来所有指向
vector
元素的指针、迭代器和引用都会失效。
每一次使用push_back()
往vector
中增加元素,可能会导致vector
重新分配内存:
从例子可以看到,往vector
中增加 1000 个元素,会导致vector
多次重新分配内存:
为了减少重新分配内存带来的开销,实际上我们可以使用v.reserve(n)
来预留足够的内存( v
是vector
的对象 ):
- 要是
n
小于或等于v.capacity()
,那么,v
将忽略这个操作。 - 要是
n
大于v.capacity()
,那么,v
将会重新分配内存,并保证新的容量至少为n
。
很明显,要是一开始vector
就预留足够的空间,那么,即使不断地使用push_back()
增加元素,vector
也不会重新分配内存,并且所有指向元素的指针、迭代器和引用仍然有效。需要明白一点,reserve()
使用空间置配器 ( allocator ) 来分配内存,也就是,没有存储元素的那部分内存是原生 ( raw ) 内存。
根据这个方法,可以减少重新分配内存的次数:
修正过剩容量
我们知道,vector
在增加元素时,当元素的个数超过容器的容量时,容器的容量会以倍数的形式增加。但是,当vector
在删除元素的时候,容量并不会随着减小:
可以看到,即使vector
只剩下 2 个元素,但它的容量仍然是 1000:
要是这时候我们不需要再往vector
中增加元素,那么,可以使vector
的容量较少到合适的程度,这样可以避免占用过多的内存。
我们可以使用交换技巧 ( swap trick ) 来解决这个问题:
std::vector<int>( vec ).swap( vec );
假定vec
原来的容量是 1000,而实际只有 2 个元素,那么:
- 我们可以使用
vec
来初始化一个临时的对象,很明显,这个临时的对象的容量应该是 2 ( 或者某个接近 2 的数值 )。 - 接着,我们将这个临时的对象与
vec
进行交换,那么,交换过后vec
的容量也是 2 ,在这条语句执行结束之后,临时的对象被销毁,这样vec
的容量就与元素个数相等了。
我们知道,迭代器、指针或引用都是指向元素的,而不是直接指向容器,同时,对于vector
来说,swap()
操作是非常高效的,这也就是说执行swap()
操作时,元素并不会被交换,所有元素在内存中的位置仍保持不变,只是元素所属的容器不同了。所以,我们可以说,在执行swap()
操作之后,迭代器、指针和引用仍然指向原来指向的元素。
实际上,使用交换技巧,vector
的容量将会收缩到尽量接近元素的个数,但这并不表示二者确切相等,这还得取决于平台的实现。
在 C++11 中,vector
增加了新的成员shrink_to_fit()
来使容器的容量收缩到尽量等于元素的个数,同样,这并不保证容器的容量与元素个数确切相等。我们可以使用shrink_to_fit()
来实现与交换技巧相同的功能:
修改元素个数
我们知道,reserve()
和shrink_to_fit()
成员函数会改变容器的容量,但不会改变容器的元素个数,也不会修改元素。但与此不同,使用resize()
则会修改容器的元素个数:
void resize (size_type n);
void resize (size_type n, const value_type& val);
resize()
的行为将取决于参数n
,例如:
- 当
n
小于元素的个数,那么,容器的前n
个元素将得到保留,其它的元素将被移除并销毁。 - 当
n
大于元素的个数,那么,新的元素将被默认初始化,并添加到容器末尾。要是指定了val
,那么,将使用val
来初始化新的元素。 - 要是
n
大于容器的容量,那么,在添加新元素之前,容器的内存将进行重新分配。
也就是说,使用resize(n)
会强制将容器的元素个数改成n
,在必要时会初始化元素或重新分配内存。
直接构造元素
我们定义一个Person
类:
在 C++98 里面,使用push_back()
往容器中添加元素,实际上容器中包含的都是元素的拷贝,例如:
但在 C++11 里面,由于 move 语义的作用,将临时对象添加到容器里面,发生的是移动操作而不是拷贝操作。我们知道,相对于拷贝操作来说,移动操作实际上是比较高效的:
实际上,C++11 提供了可变模板 ( variadic template ),例如,vector
提供了emplace_back()
成员函数:
template <class... Args>
void emplace_back (Args&&... args);
我们可以借助vector
的成员函数emplace_back()
来直接构造元素,这样就可以避免移动操作或是拷贝操作带来的开销,例如:
可以看到,我们可以将Person
构造函数的参数直接传递给emplace_back()
,这样,元素就能够直接在容器中构造,避免了不必要的移动操作或是拷贝操作。
初始化的语法
在 C++98中,创建vector
时,可以指定元素的数目和这些元素的初始值:
vector( size_type n, const value_type& val, const allocator_type &alloc = allocator_type() );
例如,我们可以创建一个包含 100 个元素的vector
,指定 0 为初始值:
在 C++11中,vector
增加了新的构造函数,允许使用初始化列表来初始化vector
:
vector( initializer_list<value_type> il, const allocator_type &alloc = allocator_type() );
在创建vector
时,可以使用多个元素来初始化vector
,例如:
注意,在使用数值类型进行初始化时,这两种初始化方式极易混淆,在使用初始化列表时,需要使用{}
来初始化。
参考资料
- Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library
- StackOverflow: Benefits of using reserve() in a vector - C++
- public member function std::vector::resize
- StackOverflow: push_back vs emplace_back
- Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14