C++ 容器(一):顺序容器简介
C++提供了使用抽象进行高效编程的方式,标准库中定义了许多容器类以及一系列泛型函数,使程序员可以更加简洁、抽象和有效地编写程序,其中包括:顺序容器,关联容器和泛型算法。本文将简介顺序容器(vector
,list
和deque
)的相关内容。
1.顺序容器的概念
标准库vector
类型,就是一种最常见的顺序容器,它将单一类型元素聚集起来成为容器,然后根据位置来存储和访问这些元素,这就是顺序容器。顺序容器的元素排列顺序与其值无关,而仅仅由元素添加到容器里的次序决定。
标准库定义了三种顺序容器:vector
,list
和deque
。它们的区别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。如下表:
顺序容器 | 功能 |
---|---|
vector |
支持快速随机访问 |
list |
支持快速插入/删除 |
deque |
双端队列 |
(1) 头文件
为了定义一个容器类型的对象,必须先包含相关的头文件:
#include <vector> // vector
#include <list> // list
#include <deque> // deque
(2) 定义
所有容器都是类模版,要定义某种特殊的容器,必须在容器名后加一对尖括号,里面提供存放元素的类型:
vector<string> sVec; // empty vector that can hold strings
list<int> iList; // empty list that can hold ints
deque<float> fDeque; // empty deque that can holds floats
(3)初始化
容器的构造函数:
构造函数 | 含义 |
---|---|
C<T> c |
创建一个名为c 的空容器,C 是容器类型名,如vector ,T 是元素类型,如int ,string 。适用于所有容器 |
C c(c2) |
创建容器c2 的副本c ;c2 和c 必须具有相同的容器类型,并存放相同类型的元素。适用于所有容器 |
C c(n) |
创建有n 个初始化元素的容器c 。只适用顺序容器 |
C c(n, t) |
使用n 个为t 的元素创建容器c ,其中值t 必须是容器类型C 的元素类型的值,或者是可以转换为该类型的值。只适用顺序容器 |
C c(b, e) |
创建容器c ,其中元素是迭代器b 和e 标示的范围内元素的副本。适用于所有容器 |
注意: 所有的容器类型都定了默认构造函数,用于创建制定类型的空容器对象。默认构造函数不带参数。为了使程序更加清晰、简短,容器类型最常用的构造函数时默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行性能,并且使容器更容易使用。
- 将一个容器初始化为另一个容器的副本
vector<int> iVec;
vector<int> iVec2(iVec); // ok
vector<double> dVec(iVec); // error, iVec holds int not double
list<int> iList(iVec); // error, iVec is not list<int>
注意:讲一个容器复制给另一个容器时,必须类型匹配(容器的类型和元素的类型都必须相同)。
- 初始化为一段元素的副本
通过使用迭代器,间接实现将一种容器内的元素复制给另一种容器。使用迭代器时,不要求容器类型相同,容器内的元素类型也可以不相同,只要它们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。
vector<string> sVec;
// initialize sList with copy of each element of sVec
list<string> sList(sVec.begin(), sVec.end());
// calculate the midpoint in the vector
vector<string>::iterator mid = sVec.begin() + sVec.size() / 2;
// initialize front with first half of sVec: the elements up to but not including *mid
vector<string> front(sVec.begin(), mid);
// also can initialize with a pointer
char* words[] = {"first", "second", "third", "forth"};
int sizeOfWords = sizeof(words) / (sizeof(char*));
vector<string> word2(words, words + sizeOfWords);
// cout
for ( int idx=0; idx<sizeOfWords; idx ++ )
cout << word2[idx] << endl;
- 分配和初始化指定数目的元素
创建顺序容器时,可显式地指定容器大小和一个(可选的)元素初始化式。容器的大小可以是常量或者非常量表达式,元素初始化式必须是可用于初始化其元素类型对象的值:
const list<int>::size_type listSize = 64; // also can be: int listSize = 64
list<std::string> lstr(listSize, "str"); // 64 strings, each is str
vector<int> iVec(listSize); // 64 ints, each initialized to 0
(4)容器内元素的类型约束
C++语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足最基本的两个约束:
- 元素类型必须支持赋值运算;
- 元素类型的对象必须可以复制。
除此外,一些容器操作对元素类型还有特殊要求。如果元素类型不支持这些要求,则相关的容器操作就不能执行:我们可以定义该类型的容器,但不能使用某些特定的操作。
另外,旧版C++标准中,指定容器作为容器类型时,必须使用如下空格:
vector<vector<int> > myVec; // the space required between close >
而在新版标准中,并无要求:
vector<vector<int> > myVec; // ok
vector<vector<int>> myVec; // ok
2.顺序容器的操作
每种顺序容器都提供了一组有用的类型定义以及以下操作:
- 在容器中添加元素;
- 在容器中删除元素;
- 设置容器大小;
- (如果有的话)获取容器内的第一个和最后一个元素
(1)容器定义的类型别名
类型别名 | 含义 |
---|---|
size_type |
无符号整型,足以存储此容器类型的最大可能容器长度 |
iterator |
此容器类型的迭代器类型 |
const_iterator |
元素只读迭代器类型 |
reverse_iterator |
按逆序寻址元素的迭代器类型 |
const_reverse_iterator |
元素只读逆序迭代器类型 |
difference_type |
足够存储两个迭代器差值的有符号整型,可为负数 |
value_type |
元素类型 |
reference |
元素的左值类型,是value_type& 的同义词 |
const_value_type |
元素的常量左值类型,等效于const value_type& |
例如:
// iter is the iterator type defined by vector<string>
vector<string>::iterator iter;
// cnt is the difference_type type defined by vector<int>
vector<int>::difference_type cnt;
(2)容器内元素操作
begin
和end
成员
操作 | 功能 |
---|---|
c.begin() |
返回一个迭代器,指向容器c 的第一个元素 |
c.end() |
返回一个迭代去,指向容器c 的最后一个元素的下一个位置 |
c.rbegin() |
返回一个逆序迭代器,指向容器c 的最后一个元素** |
c.rend() |
返回一个逆序迭代器,指向容器c 的第一个元素前面的位置 |
注意:
(a) 迭代器范围是左闭右开区间,标准表达方式为:
// includes the first and each element up to but not including last
[first, lase)
(b) 容器元素都是副本。在容器中添加元素时,系统是将元素值复制到容器里,被复制的原始值与新容器中的元素互不相关,此后,容器内元素值发生变化时,被复制的原值不会收到影响,反之亦然。
(c) 不要存储end
操作返回的迭代器。添加或者删除vector
或deque
容器内的元素都会导致迭代器失效。
vector<int> v(42);
// cache begin and end iterator
vector<int>::iterator first = v.begin(), last = v.end();
while( first!= last ) // disaster: this loop is undefined
{
// insert new value and reassign first, which otherwise would be invalid
first = v.insert(++first, 2);
++ first; // advance first just past the element we added
}
- 添加元素
操作 | 功能 |
---|---|
c.push_back(t) |
在容器c 的尾部添加值为t 的元素,返回void 类型 |
c.push_front(t) |
在容器c 的前端添加值为t 的元素,返回void 类型(只适用于list 和deque 容器类型) |
c.insert(p, t) |
在迭代器p 所指向的元素前面插入值为t 的新元素,返回指向新添加元素的迭代器 |
c.insert(p, n, t) |
在迭代器p 所指向的元素前面添加插入n 个值为t 的新元素,返回void 类型 |
c.insert(p, b, e) |
在迭代器p 所指向元素前面插入由迭代器b 和c 标记范围的元素,返回void 类型 |
// add elements at the end of vector
vector<int> iVec;
for ( int idx=0; idx<4; ++ idx )
{
iVec.push_back( idx );
}
// insert an element
vector<string> sVec;
string str("Insert");
// warning: inserting anywhere but at the end of a vector is an expensive operation
sVec.insert(sVec.begin(), str);
// insert some elements
sVec.insert(sVec.begin(), 10, "Anna");
string array[4] = {"first", "second", "third", "forth"};
sVec.insert(sVec.end(), array, array+4);
- 容器大小的操作
操作 | 功能 |
---|---|
c.size() |
返回容器c 中元素个数,返回类型为c::size_type |
c.max_size() |
返回容器c 可容纳的最多元素个数,返回类型为c::size_type |
c.empty() |
返回标记容器大小是否为0的布尔值 |
c.resize(n) |
调整容器c 的长度大小,使其能容纳n 个元素。如果n<c.size() ,则删除多余的元素,否则,添加采用值初始化的新元素 |
c.resize(n, t) |
调整容器c 的大小,使其能包纳n 个元素,所有元素的值都为t |
vector<int> iVec(10, 1); // 10 ints, each has value 1
iVec.resize(15); // adds 5 elements of value 0 to back of iVec
iVec.resize(25, -1); // adds 10 elements of value -1 to back of iVec
iVec.resize(5); // erases 20 elements from the back of iVec
注意:resize
操作可能会使迭代器失效。在vector
或deque
容器上做resize
操作可能使其所有迭代器都失效。对于所有容器类型,如果resize
操作压缩了容器,则指向已删除的元素的迭代器失效。
- 访问元素
操作 | 功能 |
---|---|
c.back() |
返回容器c 的最后一个元素的引用,如果c 为空,则该操作未定义 |
c.front() |
返回容器c 的第一个元素的引用,如果c 为空,则该操作未定义 |
c[n] |
返回下标为n 的元素的引用,如果n<0 或n>c.size() ,则该操作未定义(只适用于vector 和deque 容器) |
c.at(n) |
返回下标为n 的元素的引用。如果下标越界,则该操作未定义(只适用于vector 和deque 容器) |
注意:使用越界的下标,或调用空容器的front
或back
函数,都会导致程序出现 严重的错误。
- 删除元素:与插入元素对应容器类型提供了删除容器内元素的操作。
操作 | 功能 |
---|---|
c.erase(p) |
删除迭代器p 所指向的元素,返回一个迭代器,它指向被删除元素后面的元素。如果p 指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置,如果p 本身就是指向超出末端的下一位置的迭代器,则该函数未定义 |
c.erase(b, e) |
删除迭代器b 和e 标记的范围内的所有元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e 本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器末端的下一位置 |
c.clear() |
删除容器c 内的所有元素,返回void |
c.pop_back() |
删除容器c 的最后一个元素,返回void 。如果c 为空容器,则该操作未定义 |
c.pop_font() |
删除容器c 的第一个元素,返回void 。如果c 为空容器,则该操作未定义 |
注意:
(a) pop_front
操作通常与front
操作配套使用,实现栈(先进先出)的方式处理:
while ( !vec.empty() )
{
// do something with the current top of vec
process(vec.front());
// remove first element
vec.pop_front();
}
(b)删除一个或一段元素更通用的方法是erase
操作。erase
操作不会检查它的参数,使用时必须确保用作参数的迭代器或迭代器范围是有效的。
(c) 寻找一个指定元素的最简单的方法是使用标准库的find
算法(编程时需要添加头文件#include <algorithm>
)。find
函数需要一对标记查找范围的迭代器以及一个在该范围内查找的值作为参数。查找完成后,返回一个迭代器,它指向具有指定值的第一个元素或超出末端的下一位置。
string searchValue("find");
vector<std::string> vec(1, "find");
vector<string>::iterator iter = std::find(vec.begin(), vec.end(), searchValue);
if ( iter!= vec.end() )
cout << *iter << endl;
(d) 删除所有元素,可以用clear
或将begin
和end
迭代器传递给erase
函数。
vec.clear(); // delete all the elements within the container
vec.erase(vec.begin(), vec.end()); // equivalent
- 赋值与
swap
赋值操作中,首先删除其左操作数容器内的所有元素,然后将右操作数容器中的所有容器插入到左边容器中:
vec1 = vec2; // replace contents of vec1 with a copy of elements in vec2
// equivalent operation using erase and insert
vec1.erase(vec1.begin(), vec1.end()); // delete all elements in vec1
vec1.insert(vec2.begin(), vec2.end()); // insert vec2
操作 | 功能 |
---|---|
c1=c2 |
删除容器c1 中所有的元素,然后将c2 的元素复制给c1 。c1 和c2 的类型(包括容器类型和元素类型)必须相同 |
c1.swap(c2) |
交换内容:调用完该函数后,c1 中存放的是c2 原来的元素,c2 中存放的是原来c1 的元素。c1 和c2 的类型必须相同。该函数的执行速度通常要比将c2 复制到c1 的操作快 |
c.assign(b, e) |
重新设置c 的元素,将迭代器b 和c 标记范围内的所有元素复制到c 中。b 和e 必须不是指向c 中元素的迭代器 |
c.assign(n, t) |
将c 重新设置为存储n 个值为t 的元素 |
注意:
(a) swap
操作不会删除或插入任何元素,而且保证在常量的时间内刷新交换。由于容器内没有移动任何元素,因此迭代器不会失效。
(b) 在这里补充一点,vector
容器大小有两个描述参数size
和capacity
。size
前面已经讲述过,指容器中当前已存储元素的数目,而capacity
存储的是容器所分配的存储空间可以存储的元素总数。一般来说capacity >= size
。在clear
, 赋值(c1 = c2
),assign
(不超过原容器大小)等操作中,并未改变容器的capacity
,也就是说,只是把已经分配好的内存上写入的元素数据清掉或者重新赋值,但并未对存储空间进行变动;但是swap
操作时,size
和capacity
都会改变。
vector<int> vec(100); // size: 100, capacity: 100;
vec.clear(); // size: 0, capacity: 100
vector<int> vec2(50);
vec = vec2; // size: 50, capacity: 100
vector<int> vec3(30);
vec.assign(vec3.begin(), vec.end()); // size: 30, capacity: 100
vec.swap(vec3); // error!
vector<int> v1(30), v2(50);
v1.swap(v2); // v1: size 50, capacity 50; v2: size 30, capacity 30
基于此原因,有些时候,当我们想删除一个容器的所有元素的同时,又想把容器占用的内存释放掉时,clear
并不能完全实现这一目的,但是可以通过swap
:
vector<int> vec(100); // size 100, capacity 100
vector<int>().swap( vec ); // size 0, capacity 0
(c) vector
容器中有reserve
操作,可以设定存储空间大小:
vector<int> vec(24); // size 24, capacity 24
vec.reserve(50); // size 24, capacity 50
cout<< "size" << vec.size() << endl <<
"capacity" << vec.capacity() << endl;
3.结束语
我们很喜欢使用容器,因为确实很便捷,相比于数组,它可以很随意的实现元素的添加、删除等。我们也无需担心内存分配的问题,因为标准库会帮我们都搞定。但是我们最好还是了解一下。
以vector
为例,为了支持快速的随机访问,vector
容器的元素以连续的方式存放,即每一个元素都挨着前一个元素存储。当我们向容器中添加元素时,想想会发生什么:如果容器中已经没有空间容纳新元素,由于容器必须连续存储以便索引访问,所以不能在内存中随便找个地方来存储新元素,而是必须重新分配存储空间,存放在旧存储空间的元素被复制到新存储空间里,接着插入新元素,最后撤销旧的存储空间。如果vector
容器在每次添加新元素时,都要这么分配和撤销内存空间,那么其性能将会非常慢!所以,标准库不会这么做,为了使vector
容器实现快速的内存分配,其实际分配的容量要比当前所需的空间大一些,例如分配旧存储空间n
倍(例如2倍)大小的新存储空间,这样的策略显著提高了其效率。
vector
容器的内存分配策略是以最小的代价连续存储元素,通过访问上的便利弥补其存储代价,虽然list
容器优于vector
容器,但是大部分情况下人们还是觉得vector
更好用。实际中vector
的增长效率比起list
和deque
通常会更高。
参考文献:
- 《C++ Primer中文版(第四版)》,Stanley B.Lippman et al. 著, 人民邮电出版社,2013。