关于vector
vector是一个基于使用动态堆空间的连续空间的STL容器,可以方便动态追加扩展,方便如同数组的按索引访问;
vector有两个重要概念,一个是容器的分配的空间的大小(capacity),一个是容器内当前元素的个数(size),前者对应的是实际在堆上为这个vector分配了多少堆空间,后者是当前vector内,实际有多少个元素,这和vector的迭代器(begin、end)相关;
capacity可以和size相同,也可以大于size。比如一个vector已经分配了100个元素的空间,但可能里边不存在元素,或者部分元素,或者最多100个元素;
vector通过push_back向后动态追加元素,通过pop_back弹出元素。当size已经与capacity相同时,push_back会触发新的内存分配,分配策略与编译器实现有关(如gcc的方式是当前capacity乘以2,VC的方式是当前capacity乘以1.5),如果size还小于capacity,push_back不会触发新的内存分配;pop_back则仅仅弹出元素,不会释放内存;合理的运用push_back可以减少不必要的新的内存分配导致的耗时;
vector的元素的push_back,本质上是将元素的左值引用(或者是右值引用,会快出很多),拷贝构造(或者是移动构造)到vector的内存空间中去;对于较为庞大的对象,c++11的vector支持使用emplace_back,免于先在外部构造对象,然后再通过拷贝构造(移动构造)到vector的内存空间中去,而是直接在vector的内存空间中,直接构造元素,对于较为庞大的对象能省出不少耗时;
元素可以通过pop_back、resize、clear、erase被全部清除或部分清除(即vector占据的内存空间并不被释放),vector的迭代器(begin、end)将会被调整;
可以通过erase方法清除自定义的不需要的元素,但会造成被清除元素后边全部元素的向前移动,如vector有1000 * 1000个元素,通过erase清除掉第一个元素,后边的999,999个元素都将需要移动(memmove);
vector可以通过reserve实现占据的内存空间的预分配,这样避免了后续的push_back的不必要的新的内存分配的耗时;还可以通过shrink_to_fit、swap将vector占据的内存空间调整至实际元素个数或者自定义的大小;
vector支持从一个vector1赋值给另一个vector2,实现vector2拥有和vector1相同的元素。对于vector的赋值,应该选择v2 = v1这样的赋值,而不是循环的将v1的元素拷贝构造到v2中,原因和reserve一样,避免了不必要的新的内存分配的耗时;
vector通过有时间'[]'访问位于连续空间中不同索引位置的元素,同样可以通过at()方法获取,at()方法的优点是当访问不可访问的元素时,不会引起进程崩溃,而是抛出一个异常,但at()方法的访问速度比通过'[]'访问要慢,所以不推荐使用;
下面仔细介绍每个部分
1、vector的初始化:
c++11之前vector无法自动初始化,必须通过push_back挨个插入元素,c++11后可以通过左值引用拷贝构造或赋值构造,也可以通过右值引用移动构造或赋值构造,如下:
TEST (test1, init) {
std::vector<int> v = {1,2,3,4,5};
std::vector<int> v2(v), v3(std::move(v)), v4 = v2, v5 = {6,7,8,9,0};
std::cout << v.size() << std::endl; //0
std::cout << v2.size() << std::endl;//5
std::cout << v3.size() << std::endl;//5
std::cout << v4.size() << std::endl;//5
std::cout << v5.size() << std::endl;//5
}
v通过初始化列表构造,实质上是{1,2,3,4,5}的右值引用赋值构造;
v2通过v的左值引用拷贝构造;
v3通过v的右值引用移动构造;
v4通过v2的左值引用赋值构造;
v5通过{6,7,8,9,0}的右值引用赋值构造;
2、vector按什么构造:
由1已知,vector可以按左值引用、右值引用进行拷贝构造、移动构造、赋值;
需要注意的是,vector的赋值或者按左值引用构造,是基于赋值者的实际元素个数进行的,如果赋值者没有元素,则被赋值的vector也是不会分配内存空间的;
但如果按右值引用构造,则相当于取得了原vector的右值引用,也就取得了原vector的内存空间的引用;
TEST (test1, assign) {
std::vector<int> v1, v2;
v1.reserve(100);
std::cout << v1.capacity() << std::endl;//100
std::cout << v2.capacity() << std::endl;//0
v2 = v1;
std::cout << v2.capacity() << std::endl;//0
std::vector<int> v3 = v1;
std::cout << v3.capacity() << std::endl;//0
std::vector<int> v4(v1);
std::cout << v4.capacity() << std::endl;//0
std::vector<int> v5(std::move(v1));
std::cout << v5.capacity() << std::endl;//100
}
上面的v1预分配了100个元素的内存空间,但是没有任何元素,所以按v1赋值、按v1左值构造,都不会给新的vector分配空间;
上面的v5是另外一回事,v5取得了v1的右值引用,它的capacity会是100;
3、vector的capacity、size与reserve、resize:
vector的capacity()方法返回vector到底占据了多少动态内存空间;
vector通过reserve(i)方法,首先析构掉已存在的(size()个)元素,然后释放掉已分配了的(capacity()大小的)空间,然后为vector重新分配i个对象大小的动态内存空间;
vector的size()方法返回vector内实际有多少个元素;
vector通过resize(i)方法,将vector的实际元素个数变为i个;
需要注意的是:
a1. 如vector当前的内存空间大小为len1,此时如果reserve(len2)且len2 > len1,那么会重新分配len2大小的动态内存空间,然后将vector现有元素在新的内存空间中重新构造插入,然后释放掉旧的len1长度的内存空间,vector持有新的内存空间;
a2. 如vector当前的内存空间大小为len1,此时如果reserve(len2)且len2 <= len1,则什么都不会发生;
b1. 如vector当前的元素个数为len1,此时如果resize(len2)且len2 > len1,则重新push_back(len2 - len1)个默认元素到vector,如果会超过vector的capacity,则扩充vector的capacity;迭代器的end()指向最后一个元素;
b2. 如vector当前的元素个数为len1,此时如果resize(len2)且len2 < len1,则相当于pop_back(len1 - len2)个元素,迭代器的end()指向最后一个元素;如果此时vector不存在任何一个元素,则迭代器的begin()和end()都指向空;
TEST (test1, reserve_resize) {
std::vector<int> v;
v.reserve(10);
std::cout << v.capacity() << std::endl;//10
v.reserve(5);
std::cout << v.capacity() << std::endl;//10
v.reserve(15);
std::cout << v.capacity() << std::endl;//15
for (int i = 0; i < 5; i++) {
v.push_back(i);
}
std::cout << v.size() << std::endl;//5
v.resize(10);
std::cout << v.size() << std::endl;//10
for (int i = 5; i < 10; i++) {
std::cout << v[i] << " ";//0 0 0 0 0 default constructed element
}
v.resize(100);
std::cout << std::endl << v.capacity() << std::endl;//100
std::cout << v.size() << std::endl;//100
v.resize(1);
std::cout << v.size() << std::endl;//1
}
首先预分配10个元素的空间,capacity为10;
然后调用reserve(5),不影响,capacity仍为10;
然后调用reserve(15),capacity扩为15;
push_back5个元素,size为5;
然后调用resize(10),插入5个默认元素,对于int就是0,size为10;
然后调用resize(100),大于vector当前的capacity,扩充capacity为100,size也为100;
然后调用resize(1),size变为1,capacity不变仍为100;
4、通过clear清楚全部元素、通过pop_back清楚尾部元素、通过erase清楚自定义元素:
首先明确一点,clear、pop_back、erase,都不会影响capacity,也就是都不会释放已经分配了的内存,只影响size,也就是影响迭代器指针;
clear()方法直接清楚全部元素;迭代器的begin()和end()指向空;
pop_back()方法清楚掉尾部元素,影响迭代器的end(),元素全被清空时begin()和end()都指向空;
erase()清除自定义的元素,它的操作是:将被清除元素后边的全部元素,整体赋值到被清除元素所在位置处,相当于memmove,并且erase()方法返回赋值后的起始元素的迭代器指针;值得注意的是,当被清除元素后边还有很多元素时,memmove产生的大量的赋值操作的时间耗时很大;
TEST (test1, clear_pop_back_erase) {
std::vector<int> v;
v.reserve(10);
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
std::cout << v.capacity() << std::endl; //10
std::cout << v.size() << std::endl; //10
v.clear();
std::cout << v.capacity() << std::endl; //10
std::cout << v.size() << std::endl; //0
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
v.pop_back();
std::cout << v.capacity() << std::endl; //10
std::cout << v.size() << std::endl; //9
std::vector<int>::iterator it = v.begin();
it = v.erase(it);
std::cout << *it << std::endl; //1
std::cout << v.capacity() << std::endl; //10
std::cout << v.size() << std::endl; //8
for (it = v.begin(); it != v.end(); it++) {
if (*it == 5) {
it = v.erase(it); //erase element 5
}
}
std::cout << v.capacity() << std::endl; //10
std::cout << v.size() << std::endl; //7
v.clear();
v.reserve(1000 * 1000 * 1000);
for (int i = 0; i < v.capacity(); i++) {
v.push_back(i);
}
it = v.begin();
common::TimeCostCalcer tc;
it = v.erase(it);
std::cout << tc.Calc_microsecond() << std::endl; //slow
it = v.end() - 1;
tc.restart();
it = v.erase(it);
std::cout << tc.Calc_microsecond() << std::endl; //fast
}
首先预分配10个元素的内存空间给vector;同时push_back10个元素到vector,依次为0-9;
调用clear()方法,size为0,capacity仍为10;
重新push_back入10个元素,依次为0-9,pop_back掉尾部的元素9,size为9,capacity仍为10;
调用erase()方法,清除掉首元素,erase返回新的首元素(值为1);size为8,capacity仍为10;
遍历vector,调用erase()方法清除掉值为5的元素;size为7,capacity仍为10;
调用clear()方法清除全部元素,并重新为vector分配1000 * 1000 * 1000个元素的空间;并push_back入1000 * 1000 * 1000个元素;
调用erase()方法,清除掉首元素,可以发现memmove的成本很大,因为需要做999,999,999个元素的memmove,很慢;
调用erase()方法,清除掉尾元素,可以发现很快;
5、通过swap、shrink_to_fit调整vector占据的内存大小:
如果当前的vector的capacity很大,希望能够减小,那么swap、shrink_to_fit两个方法是需要的;
swap:调整到自定义大小,本质是:根据自定义大小,创建临时的vector,并push_back入自定义(或默认)的初始值,然后原vector持有临时vector分配的内存空间,临时vector持有原vector分配的内存空间,然后临时vector被析构(首先析构掉里边(可能存在的)已有元素,再释放掉内存空间);
shrink_to_fit:典型针对capacity大于size时,重新分配size个元素大小的新的内存空间,并将原vector中已有的size个元素,push_back入新的内存空间,然后释放掉原vector分配的内存空间,原vector改为持有新的内存空间;
TEST (test1, swap_shrink_to_fit) {
std::vector<int> v1(10, 1), v2(20, 2);
v1.swap(v2);
std::cout << v1.capacity() << std::endl;//20
std::cout << v2.capacity() << std::endl;//10
std::vector<int>().swap(v1);
std::cout << v1.capacity() << std::endl;//0
std::vector<int>(30, 3).swap(v2);
std::cout << v2.capacity() << std::endl;//30
v1.reserve(10);
std::cout << v1.capacity() << std::endl;//10
v1.shrink_to_fit();
std::cout << v1.capacity() << std::endl;//0
v2.reserve(50);
std::cout << v2.capacity() << std::endl;//50
v2.shrink_to_fit();
std::cout << v2.capacity() << std::endl;//30
}
初始v1分配了10个元素的空间,v2分配了20个元素的空间;
v1和v2进行交换,v1的capacity变为了20,v2变为了10;
使用临时空vector对象与v1交换,实现将v1变为空vector;
使用临时的具有30个元素的vector与v2交换,实现v2的capacity变为30,且元素值均为3;
重新为v1预分配10个元素的空间,然后调用shrink_to_fit()方法,v1的capacity恢复为0;
重新为v2预分配50个元素的空间,然后调用shrink_to_fit()方法,v2的capacity恢复为30;
6、vector的元素的访问与插入:
首先说访问,vector可以通过中括号运算符'[]'访问连续空间中不同索引位置的元素,也可以通过at()方法访问,两者区别在于:
a. 如果访问了超出size()范围的索引处的元素,通过'[]'访问可能会导致进程崩溃,而通过at()访问则会抛出一个异常
b. 通过'[]'访问的速度比通过at()访问要快,应该小心翼翼的使用'[]'访问
值得注意的是,不要访问超出size()范围的索引处的元素;
然后是插入,也就是之前所说的"push_back入元素";vector里边的元素的插入,可以通过如下方式:
a. 另一个元素的左值引用;通过元素的拷贝构造函数,在vector内部拷贝构造;
b. 另一个元素的右值引用;通过元素的移动构造函数,在vector内部移动构造;
c. 直接在vector里构造;使用c++11的emplace_back实现在vector内部直接构造;
很明显方法c应该是最快的方法;
TEST (test1, emplace_back) {
std::vector<std::string> v;
v.reserve(1000 * 1000);
common::TimeCostCalcer tc;
for (int i = 0; i < v.capacity(); i++) {
std::string temp = "test";
v.push_back(temp);
}
std::cout << tc.Calc_microsecond() << std::endl;
v.clear();
tc.restart();
for (int i = 0; i < v.capacity(); i++) {
v.emplace_back("test");
}
std::cout << tc.Calc_microsecond() << std::endl;
v.clear();
tc.restart();
for (int i = 0; i < v.capacity(); i++) {
std::string temp = "test";
v.push_back(std::move(temp));
}
std::cout << tc.Calc_microsecond() << std::endl;
v.clear();
tc.restart();
for (int i = 0; i < v.capacity(); i++) {
v.push_back("test");
}
std::cout << tc.Calc_microsecond() << std::endl;
}
首先给vector预分配1000 * 1000个元素的空间;
用临时变量temp的左值引用,通过拷贝构造函数,在vector内依次拷贝构造新的元素;这是最慢的方法;
用emplace_back的方式,在vector内依次直接构造新的元素;这是最快的方法;
分别显式和隐式的用右值引用,通过移动构造函数,在vector里依次移动构造新的元素;这是比较快的方法;
当单个元素的占据空间很大时,方法c的速度优势将体现的极为明显:
struct big_item {
char c;
short int si;
int i;
float f;
double d;
std::string s;
char cr[100];
float fr[200];
double dr[500];
big_item(char _c, short int _si, int _i, float _f, double _d, std::string _s): c(_c), si(_si), i(_i), f(_f), d(_d), s(_s) {}
big_item(const big_item &) = default;
big_item(big_item &&) = default;
big_item &operator= (const big_item &) = default;
};
TEST (test1, emplace_back_bigitem) {
std::vector<big_item> v;
v.reserve(1000 * 1000);
common::TimeCostCalcer tc;
for (int i = 0; i < v.capacity(); i++) {
big_item bi('a', 1, 101, 1.1, 2.2, "abcde");
v.push_back(bi);
}
std::cout << "push_back cost: " << tc.Calc_microsecond() << std::endl;
v.clear();
tc.restart();
for (int i = 0; i < v.capacity(); i++) {
v.emplace_back('a', 1, 101, 1.1, 2.2, "abcde");
}
std::cout << "emplace_back cost: " << tc.Calc_microsecond() << std::endl;
v.clear();
tc.restart();
for (int i = 0; i < v.capacity(); i++) {
big_item bi('a', 1, 101, 1.1, 2.2, "abcde");
v.emplace_back(std::move(bi));
}
std::cout << "push_back && cost: " << tc.Calc_microsecond() << std::endl;
}
结构体big_item比较大,初始给vector分配1000 * 1000个元素的空间;
首先用拷贝构造的方式,push_back入1000 * 1000个元素的左值引用;会发现很慢;
然后用emplace_back,直接在vector内构造1000 * 1000个元素;会发现快许多;
然后用移动构造的方式,push_back如1000 * 1000个元素的右值引用;耗时介于上面两者之间;
结论:当单个对象的空间比较大时,宜使用emplace_back方式,push_back入vector;
7、vector要点易错点总结:
a. 当capacity和size相同时,push_back会触发新的内存分配;尽量不要在未预分配空间时不停的push_back,导致不停的内存空间分配;
b. 需要减少vector占据的内存空间时,用swap、shrink_to_fit,而不是clear、pop_back、erase、resize;
c. 用一个vector对另一个vector赋值时,用赋值运算符,而不是遍历+push_back,原因同a;
d. 避免使用at()方法,速度慢;
e. vector不支持在首部push_front元素,因为会导致后边大量的memmove,同理,也尽可能不要erase位于vector前列的元素,尤其在vector内元素很多的时候;
f. 当单个元素的空间很大时,用emplace_back直接在vector内部直接构造,而不是首先构造元素,然后用元素的左值引用,在vector内部拷贝构造;
g. 对vector的访问,永远要保证访问的索引在size()的范围内,否则可能引起进程崩溃;如vector仅仅分配了内存空间,但并没有通过push_back或赋值等方式实现插入元素,那么size依然为0,通过索引访问vector仍非法;
下一篇: php怎么删除最后一个字符