C++ STL vector容器

vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。

vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。

vector 容器以类模板 vector<T>( T 表示存储元素的类型)的形式定义在 <vector> 头文件中,并位于 std 命名空间中。

创建vector容器

创建 vector 容器的方式有很多,大致可分为以下几种。

1) 如下代码展示了如何创建存储 double 类型元素的一个 vector 容器:

 

std::vector<double> values;

 

注意,这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。

在创建好空容器的基础上,还可以像下面这样通过调用 reserve() 成员函数来增加容器的容量:

 

values.reserve(20);

 

这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果 vector 的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素,即 values 容器内此时仍然没有任何元素。

还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector<T> 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。

2) 除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数,比如:

 

std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};

 

这样就创建了一个含有 8 个素数的 vector 容器。

3) 在创建 vector 容器时,也可以指定元素个数:

 

std::vector<double> values(20);

 

如此,values 容器开始时就有 20 个元素,它们的默认初始值都为 0。

注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。

如果不想用 0 作为默认值,也可以指定一个其它值,例如:

 

std::vector<double> values(20, 1.0);

 

第二个参数指定了所有元素的初始值,因此这 20 个元素的值都是 1.0。

值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:

 

int num=20;
double value =1.0;
std::vector<double> values(num, value);

4) 通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:

std::vector<char>value1(5, 'c');
std::vector<char>value2(value1);

 

由此,value2 容器中也具有 5 个字符 'c'。在此基础上,如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:

 

int array[]={1,2,3};
std::vector<int>values(array, array+2);//values 将保存{1,2}
std::vector<int>value1{1,2,3,4,5};
std::vector<int>value2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}

 

注:std::begin()函数是C++11标准新加全局函数,含义和容器的成员函数begin()基本相同。

由此,value2 容器中就包含了 {1,2,3} 这 3 个元素。

 

Vector成员函数

函数成员函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
capacity() 返回当前容量。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
reserve() 增加容器的容量。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
operator[ ] 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
data() 返回指向容器中第一个元素的指针。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
pop_back() 移出序列尾部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移出一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_back() 在序列尾部生成一个元素。

 迭代器相关成员函数

 

 

 Vector容器容量capacity与大小size

vector 容器的容量(用 capacity 表示),指的是在不分配更多内存的情况下,容器可以保存的最多元素个数;而 vector 容器的大小(用 size 表示),指的是它实际所包含的元素个数;capacity()-size()则是剩余的可用空间大小。当size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。 

可以调用 reserve() 成员函数来增加容器的容量(但并不会改变存储元素的个数);而通过调用成员函数 resize() 可以改变容器的大小,并且该函数也可能会导致 vector 容器容量的增加。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int>value{ 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 };
    cout << "value 容量是:" << value.capacity() << endl;
    cout << "value 大小是:" << value.size() << endl;
    value.reserve(20);
    cout << "value 容量是(2):" << value.capacity() << endl;
    cout << "value 大小是(2):" << value.size() << endl;
    //将元素个数改变为 21 个,所以会增加 6 个默认初始化的元素
    value.resize(21);
    //将元素个数改变为 21 个,新增加的 6 个元素默认值为 99。
    //value.resize(21,99);
    //当需要减小容器的大小时,会移除多余的元素。
    //value.resize(20);
    cout << "value 容量是(3):" << value.capacity() << endl;
    cout << "value 大小是(3):" << value.size() << endl;
    return 0;
}

输出:

value 容量是:15
value 大小是:15
value 容量是(2):20
value 大小是(2):15
value 容量是(3):30
value 大小是(3):21

可以看到,仅通过 reserve() 成员函数增加 value 容器的容量,其大小并没有改变;但通过 resize() 成员函数改变 value 容器的大小,它的容量可能会发生改变。另外需要注意的是,通过 resize() 成员函数减少容器的大小(多余的元素会直接被删除),不会影响容器的容量。

 

下标访问

通过下标访问 vector 中的元素时不会做边界检查,即便下标越界。也就是说,下标与 first 迭代器相加的结果超过了 finish 迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。如果想在访问 vector 中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。 在越界时,at()函数会抛出一个异常。

 

vector底层实现原理

通过分析 vector 容器的源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的:

//_Alloc 表示内存分配器,此参数几乎不需要我们关心
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
    ...
protected:
    pointer _Myfirst;
    pointer _Mylast;
    pointer _Myend;
};

其中,_Myfirst 指向的是 vector 容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。

 

 

如图 1 所示,通过这 3 个迭代器,就可以表示出一个已容纳 2 个元素,容量为 5 的 vector 容器。

在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:

  • _Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间;
  • _Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间;
  • _Myfirst 和 _Myend 可以用表示 vector 容器的容量。

 

对于空的 vector 容器,由于没有任何元素的空间分配,因此 _Myfirst、_Mylast 和 _Myend 均为 null。

通过灵活运用这 3 个迭代器,vector 容器可以轻松的实现诸如首尾标识、大小、容器、空容器判断等几乎所有的功能,比如:

 

template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
public:
    iterator begin() {return _Myfirst;}
    iterator end() {return _Mylast;}
    size_type size() const {return size_type(end() - begin());}
    size_type capacity() const {return size_type(_Myend - begin());}
    bool empty() const {return begin() == end();}
    reference operator[] (size_type n) {return *(begin() + n);}
    reference front() { return *begin();}
    reference back() {return *(end()-1);}
    ...
};

 

注:vector内存是连续的空间,引用没有实际地址,即没有独立的内存空间,因此vector的元素类型不能是引用。

 

自动扩容

只要有新元素要添加到 vector 容器中而恰好此时 vector 容器的容量不足时,该容器就会自动扩容。

vector 容器扩容的整个过程,和 realloc() 函数的实现方法类似,大致分为以下 4 个步骤:

  1. 分配一块大小是当前 vector 容量几倍(根据编译器的不同,一般为1.5或2倍,如在VS 下是1.5 倍,在GCC 下是2倍)的新存储空间。
  2. 将 vector 容器存储的所有元素,依照原有次序从旧的存储空间复制到新的存储空间中;
  3. 析构掉旧存储空间中存储的所有元素;
  4. 释放旧的存储空间。

通过以上分析不难看出,vector 容器的扩容过程是非常耗时的,并且当容器进行扩容后,之前和该容器相关的所有指针、迭代器以及引用都会失效。因此在使用 vector 容器过程中,我们应尽量避免执行不必要的扩容操作。
因此,避免 vector 容器执行不必要的扩容操作的关键在于,在使用 vector 容器初期,就要将其容量设为足够大的值。换句话说,在 vector 容器刚刚构造出来的那一刻,就应该借助 reserve() 成员方法为其扩充足够大的容量。

以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间。

对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。

 

 

迭代器失效

由之前的知识可知,当发生自动扩容时,所有已存在的vector容器的迭代器均会失效。而由于vector的底层特性,当删除或插入一个中间元素时,该元素之后的其他迭代器亦会因错位导致失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,可以

it=vec.erase(it);

采用通用算法remove()来删除vector容器中的元素. remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。不同的是:采用remove一般情况下不会改变容器的大小,而pop_back()与erase()等成员函数会改变容器的大小。(注:不是成员函数remove())

 

push_back() & emplace_back()

emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

#include <vector> 
#include <iostream> 
using namespace std;
class testDemo
{
public:
    testDemo(int num):num(num){
        std::cout << "调用构造函数" << endl;
    }
    testDemo(const testDemo& other) :num(other.num) {
        std::cout << "调用拷贝构造函数" << endl;
    }
    testDemo(testDemo&& other) :num(other.num) {
        std::cout << "调用移动构造函数" << endl;
    }
private:
    int num;
};

int main()
{
    cout << "emplace_back:" << endl;
    std::vector<testDemo> demo1;
    demo1.emplace_back(2);  

    cout << "push_back:" << endl;
    std::vector<testDemo> demo2;
    demo2.push_back(2);
}

输出:

emplace_back:
调用构造函数
push_back:
调用构造函数
调用移动构造函数

如果去掉testDemo中的移动构造函数:

emplace_back:
调用构造函数
push_back:
调用构造函数
调用拷贝构造函数

由此可以看出,push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。
显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。因此,在实际使用时,建议大家优先选用 emplace_back()。

 

swap()函数妙用

vector 模板类中提供了 pop_back()、erase()、clear() 等成员方法,可以轻松实现删除容器中已存储的元素。但需要注意得是,借助这些成员方法只能删除指定的元素,容器的容量并不会因此而改变。Vector 模板类中提供有一个 shrink_to_fit() 成员方法,该方法的功能是将当前 vector 容器的容量缩减至和实际存储元素的个数相等,可以之前的方法配合使用起到减小容量的目的。

除此之外,vector 模板类中还提供有 swap() 成员方法,该方法的基础功能是交换 2 个相同类型的 vector 容器(交换容量和存储的所有元素),但其也能用于去除 vector 容器多余的容量。
如果想用 swap() 成员方法去除当前 vector 容器多余的容量时,可以套用如下的语法格式:

vector<T>(x).swap(x);

其中,x 指当前要操作的容器,T 为该容器存储元素的类型。
下面程序演示了此语法格式的 swap() 方法的用法和功能:

 

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int>myvector;
    //手动为 myvector 扩容
    myvector.reserve(1000);
    cout << "1、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl;
    //利用 myvector 容器存储 10 个元素
    for (int i = 1; i <= 10; i++) {
        myvector.push_back(i);
    }
    //将 myvector 容量缩减至 10
    vector<int>(myvector).swap(myvector);
    cout << "2、当前 myvector 拥有 " << myvector.size() << " 个元素,容量为 " << myvector.capacity() << endl;
    return 0;
}

 

输出:

1、当前 myvector 拥有 0 个元素,容量为 1000
2、当前 myvector 拥有 10 个元素,容量为 10

调用swap函数的那行代码的执行流程可细分为以下 3 步:

1) 先执行 vector<int>(myvector),此表达式会调用 vector 模板类中的拷贝构造函数,从而创建出一个临时的 vector 容器(后续称其为 tempvector)。

值得一提的是,tempvector 临时容器并不为空,因为我们将 myvector 作为参数传递给了拷贝构造函数,该函数会将 myvector 容器中的所有元素拷贝一份,并存储到 tempvector 临时容器中。

注意,vector 模板类中的拷贝构造函数只会为拷贝的元素分配存储空间。换句话说,tempvector 临时容器中没有空闲的存储空间,其容量等于存储元素的个数。

2) 然后借助 swap() 成员方法对 tempvector 临时容器和 myvector 容器进行调换,此过程不仅会交换 2 个容器存储的元素,还会交换它们的容量。换句话说经过 swap() 操作,myvetor 容器具有了 tempvector 临时容器存储的所有元素和容量,同时 tempvector 也具有了原 myvector 容器存储的所有元素和容量。
3) 当整条语句执行结束时,临时的 tempvector 容器会被销毁,其占据的存储空间都会被释放。注意,这里释放的其实是原 myvector 容器占用的存储空间。
经过以上 3 个步骤,就成功的将 myvector 容器的容量由 100 缩减至 10。

swap() 方法还可以用来清空 vector 容器

当 swap() 成员方法用于清空 vector 容器时,可以套用如下的语法格式:

vector<T>().swap(x);

其中,x 指当前要操作的容器,T 为该容器存储元素的类型。
注意,和上面语法格式唯一的不同之处在于,这里没有为 vector<T>() 表达式传递任何参数。这意味着,此表达式将调用 vector 模板类的默认构造函数,而不再是复制构造函数。也就是说,此格式会先生成一个空的 vector 容器,再借助 swap() 方法将空容器交换给 x,从而达到清空 x 的目的。

 

注:在使用 vector 容器时,要尽量避免使用该容器存储 bool 类型的元素,即避免使用 vector<bool>。

具体来讲,不推荐使用 vector<bool> 的原因有以下 2 个:

  1. 严格意义上讲,vector<bool> 并不是一个 STL 容器;
  2. vector<bool> 底层存储的并不是 bool 类型值。

对于是否为 STL 容器,C++ 标准库中有明确的判断条件,其中一个条件是:如果 cont 是包含对象 T 的 STL 容器,且该容器中重载了 [ ] 运算符(即支持 operator[]),则以下代码必须能够被编译:

 

T *p = &cont[0];

 

此行代码的含义是,借助 operator[ ] 获取一个 cont<T> 容器中存储的 T 对象,同时将这个对象的地址赋予给一个 T 类型的指针。
这就意味着,如果 vector<bool> 是一个 STL 容器,则下面这段代码是可以通过编译的:

 

//创建一个 vector<bool> 容器
vector<bool>cont{0,1};
//试图将指针 p 指向 cont 容器中第一个元素
bool *p = &cont[0];

 

但这段代码不能通过编译。原因在于 vector<bool> 底层采用了独特的存储机制。
实际上,为了节省空间,vector<bool> 底层在存储各个 bool 类型值时,每个 bool 值都只使用一个比特位(二进制位)来存储。也就是说在 vector<bool> 底层,一个字节可以存储 8 个 bool 类型值。在这种存储机制的影响下,operator[ ] 势必就需要返回一个指向单个比特位的引用,但显然这样的引用是不存在的。

C++ 标准中解决这个问题的方案是,令 operator[] 返回一个代理对象(proxy object)。

同样对于指针来说,其指向的最小单位是字节,无法另其指向单个比特位。综上所述可以得出一个结论,即上面第 2 行代码中,用 = 赋值号连接 bool *p 和 &cont[0] 是矛盾的。

由于 vector<bool> 并不完全满足 C++ 标准中对容器的要求,所以严格意义上来说它并不是一个 STL 容器。可能有读者会问,既然 vector<bool> 不完全是一个容器,为什么还会出现在 C++ 标准中呢?

这和一个雄心勃勃的试验有关,还要从前面提到的代理对象开始说起。由于代理对象在 C++ 软件开发中很受欢迎,引起了 C++ 标准委员会的注意,他们决定以开发 vector<bool> 作为一个样例,来演示 STL 中的容器如何通过代理对象来存取元素,这样当用户想自己实现一个基于代理对象的容器时,就会有一个现成的参考模板。

然而开发人员在实现 vector<bool> 的过程中发现,既要创建一个基于代理对象的容器,同时还要求该容器满足 C++ 标准中对容器的所有要求,是不可能的。由于种种原因,这个试验最终失败了,但是他们所做过的尝试(即开发失败的 vector<bool>)遗留在了 C++ 标准中。

至于将 vector<bool> 遗留到 C++ 标准中,是无心之作,还是有意为之,这都无关紧要,重要的是让读者明白,vector<bool> 不完全满足 C++ 标准中对容器的要求,尽量避免在实际场景中使用它!

 

如何避免使用vector<bool>

那么,如果在实际场景中需要使用 vector<bool> 这样的存储结构,该怎么办呢?很简单,可以选择使用 deque<bool> 或者 bitset 来替代 vector<bool>。
要知道,deque 容器几乎具有 vecotr 容器全部的功能(拥有的成员方法也仅差 reserve() 和 capacity()),而且更重要的是,deque 容器可以正常存储 bool 类型元素。

 

还可以考虑用 bitset 代替 vector<bool>,其本质是一个模板类,可以看做是一种类似数组的存储结构。和后者一样,bitset 只能用来存储 bool 类型值,且底层存储机制也采用的是用一个比特位来存储一个 bool 值。
和 vector 容器不同的是,bitset 的大小在一开始就确定了,因此不支持插入和删除元素;另外 bitset 不是容器,所以不支持使用迭代器。

 

参考C语言中文网-STL-Vector容器部分

 

 

 

 

posted @ 2021-09-02 19:20  默行于世  阅读(347)  评论(0编辑  收藏  举报