欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于vector

程序员文章站 2022-03-22 20:57:35
...

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