您的位置:首页 > 编程语言 > C语言/C++

C++ 容器(一):顺序容器简介

2015-08-18 21:01 302 查看
C++提供了使用抽象进行高效编程的方式,标准库中定义了许多容器类以及一系列泛型函数,使程序员可以更加简洁、抽象和有效地编写程序,其中包括:顺序容器,关联容器和泛型算法。本文将简介顺序容器(
vector
list
deque
)的相关内容。

1.顺序容器的概念

标准库
vector
类型,就是一种最常见的顺序容器,它将单一类型元素聚集起来成为容器,然后根据位置来存储和访问这些元素,这就是顺序容器。顺序容器的元素排列顺序与其值无关,而仅仅由元素添加到容器里的次序决定。

标准库定义了三种顺序容器:
vector
list
deque
。它们的区别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。如下表:

顺序容器功能
vector
支持快速随机访问
list
支持快速插入/删除
deque
双端队列
(1) 头文件

为了定义一个容器类型的对象,必须先包含相关的头文件:

[code]#include <vector>  // vector
#include <list>    // list
#include <deque>   // deque


(2) 定义

所有容器都是类模版,要定义某种特殊的容器,必须在容器名后加一对尖括号,里面提供存放元素的类型:

[code]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
标示的范围内元素的副本。适用于所有容器
注意: 所有的容器类型都定了默认构造函数,用于创建制定类型的空容器对象。默认构造函数不带参数。为了使程序更加清晰、简短,容器类型最常用的构造函数时默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行性能,并且使容器更容易使用。

将一个容器初始化为另一个容器的副本

[code]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>


注意:讲一个容器复制给另一个容器时,必须类型匹配(容器的类型和元素的类型都必须相同)。

初始化为一段元素的副本

通过使用迭代器,间接实现将一种容器内的元素复制给另一种容器。使用迭代器时,不要求容器类型相同,容器内的元素类型也可以不相同,只要它们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。

[code]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;


分配和初始化指定数目的元素

创建顺序容器时,可显式地指定容器大小和一个(可选的)元素初始化式。容器的大小可以是常量或者非常量表达式,元素初始化式必须是可用于初始化其元素类型对象的值:

[code]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++标准中,指定容器作为容器类型时,必须使用如下空格:

[code]vector<vector<int> > myVec;  // the space required between close >


而在新版标准中,并无要求:

[code]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&
例如:

[code]// 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) 迭代器范围是左闭右开区间,标准表达方式为:

[code]// includes the first and each element up to but not including last
[first, lase)


(b) 容器元素都是副本。在容器中添加元素时,系统是将元素值复制到容器里,被复制的原始值与新容器中的元素互不相关,此后,容器内元素值发生变化时,被复制的原值不会收到影响,反之亦然。

(c) 不要存储
end
操作返回的迭代器。添加或者删除
vector
deque
容器内的元素都会导致迭代器失效。

[code]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
类型
[code]// 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
[code]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<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
操作配套使用,实现栈(先进先出)的方式处理:

[code]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
函数需要一对标记查找范围的迭代器以及一个在该范围内查找的值作为参数。查找完成后,返回一个迭代器,它指向具有指定值的第一个元素或超出末端的下一位置。

[code]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
函数。

[code]vec.clear(); // delete all the elements within the container
vec.erase(vec.begin(), vec.end()); // equivalent


赋值与
swap


赋值操作中,首先删除其左操作数容器内的所有元素,然后将右操作数容器中的所有容器插入到左边容器中:

[code]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
都会改变。

[code]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
:

[code]vector<int> vec(100);      // size 100, capacity 100
vector<int>().swap( vec ); // size 0, capacity 0


(c)
vector
容器中有
reserve
操作,可以设定存储空间大小:

[code]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。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: