一、概述
vector(向量)是C++中的一种数据结构,确切的说是一个类,它相当于一个动态的数组,当程序员无法知道自己需要的数组的规模多大时,用其来解决问题可以达到最大节约空间的目的。
1. vector是表示可变大小数组的序列容器。
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组 一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空 间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当 一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同 的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾 插入一个元素的时候是在常数时间的复杂度完成的。
5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6. 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元 素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好。
二、用法
1、库文件
在程序开头处加上#include<vector>以包含所需要的类文件vector,还有一定要加上using namespace std。
2、变量声明
2.1 用vector定义一维数组
vector <int> a //声明了一个int数组a[],大小没有指定,可以动态的向里面添加删除
2.2 用vector定义二维数组
vector <int *>a //声明一个一维数组向,该数组的名字代表二维数组的首地址
. 同理,用vector向量代替三维数组vector <int**>a,以此类推。
2.3 举例说明
a) vector的声明及初始化
vector<int> vec; //声明一个int型向量
vector<int> vec(5); //声明一个初始大小为5的int向量
vector<int> vec(10, 1); //声明一个初始大小为10且值都是1的向量
vector<int> vec(tmp); //声明并用tmp向量初始化vec向量
vector<int> tmp(vec.begin(), vec.begin() + 3); //用向量vec的第0个到第2个值初始化tmp
int arr[5] = {1, 2, 3, 4, 5};
vector<int> vec(arr, arr + 5); //将arr数组的元素用于初始化vec向量
//说明:当然不包括arr[4]元素,末尾指针都是指结束元素的下一个元素,
//这个主要是为了和vec.end()指针统一。
vector<int> vec(&arr[1], &arr[4]); //将arr[1]~arr[4]范围内的元素作为vec的初始值
b) 遍历元素
vector<int>::iterator it;
for (it = vec.begin(); it != vec.end(); it++)
cout << *it << endl;
//或者
for (size_t i = 0; i < vec.size(); i++) {
cout << vec.at(i) << endl;
}
c)元素翻转
#include <algorithm>
reverse(vec.begin(), vec.end());
d) 元素排序
#include <algorithm>
sort(vec.begin(), vec.end()); //采用的是从小到大的排序
//如果想从大到小排序,可以采用上面反转函数,也可以采用下面方法:
bool Comp(const int& a, const int& b) {
return a > b;
}
sort(vec.begin(), vec.end(), Comp);
3、具体用法以及函数调用
3.1 函数调用
1.push_back 在数组的最后添加一个数据
2.pop_back 去掉数组的最后一个数据
3.at 得到编号位置的数据
4.begin 得到数组头的指针
5.end 得到数组的最后一个单元+1的指针
6.front 得到数组头的引用
7.back 得到数组的最后一个单元的引用
8.max_size 得到vector最大可以是多大
9.capacity 当前vector分配的大小
10.size 当前使用数据的大小
11.resize 改变当前使用数据的大小,如果它比当前使用的大,者填充默认值
12.reserve 改变当前vecotr所分配空间的大小
13.erase 删除指针指向的数据项
14.clear 清空当前的vector
15.rbegin 将vector反转后的开始指针返回(其实就是原来的end-1)
16.rend 将vector反转构的结束指针返回(其实就是原来的begin-1)
17.empty 判断vector是否为空
18.swap 与另一个vector交换数据
3.2 举例说明(定义 vector<int> c)
c.clear() 移除容器中所有数据。
c.empty() 判断容器是否为空。
c.erase(pos) 删除pos位置的数据
c.erase(beg,end) 删除[beg,end)区间的数据
c.front() 传回第一个数据。
c.insert(pos,elem) 在pos位置插入一个elem拷贝
c.pop_back() 删除最后一个数据
c.push_back(elem) 在尾部加入一个数据
c.resize(num) 重新设置该容器的大小
c.size() 返回容器中实际数据的个数
c.begin() 返回指向容器第一个元素的迭代器
c.end() 返回指向容器最后一个元素的迭代器
4. 内存管理与效率
4.1 使用reserve()函数提前设定容量大小,避免多次容量扩充操作导致效率低下
关于STL容器,最令人称赞的特性之一就是是只要不超过它们的最大大小,它们就可以自动增长到足以容纳你放进去的数据。(要知道这个最大值,只要调用名叫max_size的成员函数)。对于vector和string,如果需要更多空间,就以类似realloc的思想来增长大小。
vector容器支持随机访问,因此为了提高效率,它内部使用动态数组的方式实现的。在通过 reserve() 来申请特定大小的时候总是按指数边界来增大其内部缓冲区。当进行insert或push_back等增加元素的操作时,如果此时动态数组的内存不够用,就要动态的重新分配当前大小的1.5~2倍的新内存区,再把原数组的内容复制过去。所以,在一般情况下,其访问速度同一般数组,只有在重新分配发生时,其性能才会下降。而进行pop_back操作时,capacity并不会因为vector容器里的元素减少而有所下降,还会维持操作之前的大小。
对于vector容器来说,如果有大量的数据需要进行push_back,应当使用reserve()函数提前设定其容量大小,否则会出现许多次容量扩充操作,导致效率低下。
reserve成员函数允许你最小化必须进行的重新分配的次数,因而可以避免真分配的开销和迭代器/指针/引用失效。以下是有时候令人困惑的四个相关成员函数,在标准容器中,只有vector和string提供了所有这些函数:
(1) size()告诉你容器中有多少元素。
它没有告诉你容器为它容纳的元素分配了多少内存。
(2) capacity()告诉你容器在它已经分配的内存中可以容纳多少元素。
那是容器在那块内存中总共可以容纳多少元素,而不是 还可以容纳多少元素。如果你想知道一个vector或string中有多少 没有被占用的内存,你必须从capacity()中减去size()。如果size和capacity返回同样的值,容器中就没有剩余空间 了,而下一次插入(通过insert或push_back等)会引发上面的重新分配步骤。
(3) resize(Container::size_type n)强制把容器改为容纳n个元素。
调用resize之后,size将会返回n。如果n小于当前大小,容器尾部的元素会被销毁。如果n大于当前大小,新默认构造的元 素会添加到容器尾部。如果n大于当前容量,在元素加入之前会发生重新分配。
(4) reserve(Container::size_type n)强制容器把它的容量改为至少n,提供的n不小于当前大小。
这一般强迫进行一次重新分 配,因为容量需要增加。(如果n小于当前容量,vector忽略它,这个调用什么都不做,string 可能把它的容量减少为size()和n中大的数,但string的大小没有改变。在我的经验中,使用reserve来从一个string中修整多 余容量一般不如使用“交换技巧”,那是条款17的主题。)
这个简介表示了只要有元素需要插入而且容器的容量不足时就会发生重新分配(包括它们维护的原始内存分配和回收,对象的拷贝和析构和迭代器、指针和引用的失效)。所以,避免重新分配的关键是使用reserve尽快把容器的容量设置为足够大,最好在容器被构造之后立刻进行。
例如,假定你想建立一个容纳1-1000值的vector<int>,没有使用reserve,你可以像这样来做:
vector<int> v;
for (int i = 1; i <= 1000; ++i) v.push_back(i);
在大多数STL实现中,这段代码在循环过程中将会导致2到10次重新分配。(10这个数没什么奇怪的。记住vector在重新分配发生时一般把容量翻倍,而1000约等于210)。
把代码改为使用reserve:
vector<int> v;
v.reserve(1000);
for (int i = 1; i <= 1000; ++i) v.push_back(i);
这在循环中不会发生重新分配。
在大小和容量之间的关系让我们可以预言什么时候插入将引起vector或string执行重新分配,而且,可以预言什么时候插入会使指向容器中的迭代器、指针和引用失效。例如,给出这段代码:
string s;
...
if (s.size() < s.capacity()) {
s.push_back('x');
}
push_back的调用不会使指向这个string中的迭代器、指针或引用失效,因为string的容量保证大于它的大小。如果不是执行 push_back,代码在string的任意位置进行一个insert,我们仍然可以保证在插入期间没有发生重新分配,但是,与伴随string插入时迭代器失效的一般规则一致,所有从插入位置到string结尾的迭代器/指针/引用将失效。
回到本条款的主旨,通常有两情况使用reserve来避免不必要的重新分配。第一个可用的情况是当你确切或者大约知道有多少元素将最后出现在容器中。那样的话,就像上面的vector代码,你只是提前reserve适当数量的空间。第二种情况是保留你可能需要的最大的空间,然后,一旦你添加完全部数据,修整掉任何多余的容量。
4.2 使用“交换技巧”来修整vector过剩空间/内存
有一种方法来把它从曾经最大的容量减少到它现在需要的容量。这样减少容量的方法常常被称为“收缩到合适(shrink to fit)”。该方法只需一条语句:vector<int>(ivec).swap(ivec);
表达式vector<int>(ivec)建立一个临时vector,它是ivec的一份拷贝:vector的拷贝构造函数做了这个工作。但是,vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临时vector没有多余的容量。然后我们让临时vector和ivec交换数据,这时我们完成了,ivec只有临时变量的修整过的容量,而这个临时变量则持有了曾经在ivec中的没用到的过剩容量。在这里(这个语句结尾),临时vector被销毁,因此释放了以前ivec使用的内存,收缩到合适。
4.3 用swap方法强行释放STL Vector所占内存
template < class T> void ClearVector( vector<T>& v )
{
vector<T>vtTemp;
vtTemp.swap( v );
}
如
vector<int> v ;
nums.push_back(1);
nums.push_back(3);
nums.push_back(2);
nums.push_back(4);
vector<int>().swap(v);
/* 或者v.swap(vector<int>()); */
/* 或者{ std::vector<int> tmp = v; v.swap(tmp); }; //加大括号{ }是让tmp退出{ }时自动析构*/
5、Vector 内存管理成员函数的行为测试
C++ STL的vector使用非常广泛,但是对其内存的管理模型一直有多种猜测,下面用实例代码测试来了解其内存管理方式,测试代码如下:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> Vec;
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //1个元素, 容器容量为1
Vec.push_back(1);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //2个元素, 容器容量为2
Vec.push_back(2);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //3个元素, 容器容量为4
Vec.push_back(3);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //4个元素, 容器容量为4
Vec.push_back(4);
Vec.push_back(5);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //5个元素, 容器容量为8
Vec.push_back(6);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //6个元素, 容器容量为8
Vec.push_back(7);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //7个元素, 容器容量为8
Vec.push_back(8);
cout << "容器 大小为: " << Vec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //8个元素, 容器容量为8
Vec.push_back(9);
cout << "容器 大小为: " << iVec.size() << endl;
cout << "容器 容量为: " << Vec.capacity() << endl; //9个元素, 容器容量为16
/* vs2005/8 容量增长不是翻倍的,如
9个元素 容量9
10个元素 容量13 */
/* 测试effective stl中的特殊的交换 swap() */
cout << "当前vector 的大小为: " << Vec.size() << endl;
cout << "当前vector 的容量为: " << Vec.capacity() << endl;
vector<int>(Vec).swap(Vec);
cout << "临时的vector<int>对象 的大小为: " << (vector<int>(Vec)).size() << endl;
cout << "临时的vector<int>对象 的容量为: " << (vector<int>(Vec)).capacity() << endl;
cout << "交换后,当前vector 的大小为: " << Vec.size() << endl;
cout << "交换后,当前vector 的容量为: " << Vec.capacity() << endl;
return 0;
}
6、vector的其他成员函数
c.assign(beg,end):将[beg; end)区间中的数据赋值给c。
c.assign(n,elem):将n个elem的拷贝赋值给c。
c.at(idx):传回索引idx所指的数据,如果idx越界,抛出out_of_range。
c.back():传回最后一个数据,不检查这个数据是否存在。
c.front():传回地一个数据。
get_allocator:使用构造函数返回一个拷贝。
c.rbegin():传回一个逆向队列的第一个数据。
c.rend():传回一个逆向队列的最后一个数据的下一个位置。
c.~ vector <Elem>():销毁所有数据,释放内存。
7、备注:在用vector的过程中的一些问题
1)
vector <int > a;
int b = 5;
a.push_back(b);
此时若对b另外赋值时不会影响a[0]的值
2)
vector <int*> a;
int *b;
b= new int[4];
b[0]=0;
b[1]=1;
b[2]=2;
a.push_back(b);
delete b; //释放b的地址空间
for(int i=0 ; i <3 ; i++)
{
cout<<a[0][i]<<endl;
}
此时输出的值并不是一开始b数组初始化的值,而是一些无法预计的值.
分析:
根据1) 2)的结果,可以想到,在1)中, 往a向量中压入的是b的值,即a[0]=b,此时a[0]和b是存储在两个不同的地址中 的。因此改变b的值不会影响a[0];而在2)中,因为是把一个地址(指针)压入向量a,即a[0]=b,因此释放了b的地址 也就释放了a[0]的地址,因此a[0]数组中存放的数值也就不得而知了。
三、vector容器和数组的辨析
数组是c++中类似vector的数据结构,它们都可以对一种类型进行储存,既都是容器。虽说两者有相似之处,但也有显著的区别,c++ primer的作者说到,在实际的编程中,我们作为程序员应该避免用到低级数组和指针,而更应该多用高级的vector和迭代器。在程序强调速度的情况下,我们程序员可以在类类型的内部使用数组和指针。下面我对vector和数组进行了总结。两者的相同和区别如下:
数据结构 | Vector | 数组 |
相同 | 都是对同一种类型的数据进行储存 | |
都可以用下标操作进行处理 | ||
都可以用迭代器进行操作(在c++中每个容器都配有各自的迭代器) | ||
不同 | 可以用size获取vector的长度 | 不可以获取,在定义时就已经确定了长度 |
长度不固定,可以随时增加 | 长度固定,在定义是就不可以更改 | |
可以在末尾增加vector的元素(用push_back) | 不能增加在长度以外的长度 | |
可以确定长度,节约空 间 |
不能确定长度,必须在定义时定义一个很大的空间留给数组,造成内存的浪费 |