关于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仍非法;



  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值