我觉得容器是面向对象里面的具有数据结构设计的数组,它拥有了数组的存储其它元素对象的能力,又提供着比数组要大得多得多的控制和操作。简而言之,容器是一种模版类,它可以容纳特定类型的对象集合,这个特定的类型可以由我们在实例化容器对象的时候指定。除了容器本身定义的操作之外,我们的泛型算法也为之定义了更多更强大的操作,被封装在算法库中,后续在泛型算法的笔记中会提到,这里主要是梳理一下容器本身自带的一些操作、vector容器自增长的内容。
一、容器的初始化操作
首先我们要注意的是,容器也是一种类,只是它是特殊的类——模版类。模板类要求我们在实例化的时候需要对某些类型进行自定义指定,因此,定义一个容器对象除了指定容器类型和对象名之外,我们还需要为其指定容器元素的类型。
1、使用默认构造函数进行初始化(C<T> cname)
#include <vector>
#include <list>
#include <deque>
vector<string> svec;
list<int> ilist;
deque<className> citems; //其中className为自定义的类类型名
上面定义的容器对象均使用了容器类的默认构造函数,用于创建一个空的容器,这也是容器类型最常用的构造函数。这里对于className的要求就是className类需包含默认构造函数。2、使用同类型容器对象进行拷贝(C<T> cname(cname2))
#include <vector>
vector<string> svec;
vector<string> svec2(svec);
这里需要注意的是:当使用一个容器对象对新建的容器对象进行初始化时,这两个容器对象的类型必须一致,包括容器类型和容器元素的类型。如下面的初始化就会出错:#include <vector>
vector<string> svec;
//vector<int> svec2(svec);
list<string> svec2(svec);
3、使用迭代器进行初始化(C<T> cname(iter1, iter2))#include <vector>
vector<string> svec;
//insert svec
vector<string> svec2(svec.begin(),svec.end()/2);
使用迭代器进行初始化可以选择拷贝对象的一部分或者全部。迭代器初始化还有另外一个十分有用的作用。在使用容器对象拷贝的初始化方式中,需要两种容器对象的容器类型和元素类型一致。但是使用迭代器,可以实现将一种容器内的元素复制到另一种容器内,它不要求容器类型和容器元素类型相同,但是它们必须互相兼容,即能够将拷贝容器对象的元素转化为新建容器的元素类型。如下:vector<string> svec;
list<string> slist(svec.begin(),svec.end());
vector<string>::iterator mid = svec.begin() + svec.size()/2;
deque<string> front(svec.begin(),mid);
deque<string> back(mid,svec.end());
使用迭代器进行初始化有两个好处:1.可以复制不能直接复制的容器;2.可以实现复制其它容器的一个子序列而不是整个容器。4.分配和初始化指定书目元素(C<T> cname(n,value) C<T> cname(n,value))
const list<int>::size_type list_size = 64;
list<string> slist(list_size,"ehc?");
二、容器的迭代器
从上面的初始化式就可以看出,容器的迭代器实际上就是指针。还记得学习数组的时候,我们总是除了使用下标对数组元素进行访问之外,还经常定义一个同类型的指针来访问数组元素。既然容器具有数组的特性,我们也可以用指针来访问容器元素,我们为其取个名字叫迭代器。如下,我们显式地用指针来代替迭代器:
//指针就是迭代器,允许通过使用内置数组中的一对指针初始化容器
char *words[] = {"stately","plump","buck","mulligan"};
size_t words_size = sizeof(words)/sizeof(char*);
list<string> words2(words,words+words_size);
常用的迭代器运算如下:*iter //对迭代器进直接访问,得到迭代器所指向的容器元素
iter->mem //对迭代器进行解引用,获取指定元素中名为mem的成员,即(*iter).mem
++iter(或iter++) //移动迭代器,使其指向容器里的下一个元素
--iter(或iter--) //移动迭代器,使其指向容器里的上一个元素
iter1 == iter2(或iter1 != iter2) //比较两个迭代器是否相等,即是否指向同一个元素
因为vector和deque容器是顺序存储的,支持随机访问,而list容器是断续存储的,不支持随机访问,因此,vector和deque容器的迭代器还提供额外的运算:iter + n(或iter - n) //前(后)第n个元素的迭代器,新生成的迭代器需指向容器中的元素或者容器末端下一位置
iter1 += iter2(iter1 -= iter2)
iter1 - iter2
>、>=、<、<= //详细解说请参考书本的第269页
迭代器的范围
迭代器范围这个概念是由C++标准库定义的。C++中常使用一对迭代器来标记迭代器范围,我们通常将它们命名为first(last)和beg(end),这两个迭代器通常分别指向同一个容器中的两个元素或者末端的下一个位置,即其范围是个左闭合区间[first,last)。
我们对形成迭代器范围的迭代器有两个要求:
1.它们指向同一个容器中的元素或者末端的下一个位置;
2.当它们不想等时,对first反复自增运算必须能够到达last,即last绝对不能位于first之前。
这样表示的编程意义有两个:
1.当两个迭代器相等时,迭代器范围为空,我们可以判断容器为空;
2.当两个迭代器不想等时,我们可以判断容器中至少有一个元素。
迭代器失效
当我们定义了一个迭代器iter,将它指向容器中的某个元素,但是当容器中这个元素被删除或者容器中的元素位置发生改变时,我们定义的这个迭代器iter将会失效,成为一个悬垂指针。我们应当避免使用失效的迭代器。即,当程序中出现可能会是迭代器失效的操作时,我们就要及时更新迭代器。任何无效的迭代器的使用都可能导致运行时失败。
void main()
{
vector<int> ivec;
ivec.push_back(1);
ivec.push_back(2);
ivec.push_back(3);
vector<int>::iterator iter = ivec.begin();
ivec.erase(iter);
iter ++;
cout<<*ivec<<endl; //iter已经无效,即使向后移动也不会指向容器中的元素了
}
容器还定义了两个函数来返回容器的两端的迭代器(后端指的是末端的下一个位置)。
begin() //返回容器的第一个元素迭代器
end() //返回容器的最后一个元素下一个位置的迭代器
rbegin() //逆序,最后一个元素的迭代器
rend() //逆序,为第一个元素前面一个位置的迭代器
三、容器中添加元素操作
首先介绍一些容器定义的类型别名(在学习类定义的时候,我们知道类成员有三种:数据、函数和类型别名)。
size_type //无符号整数,足以存储此容器类型的最大可能容器长度
iterator //此容器迭代器类型
const_iterator //元素的只读迭代器类型
reverse_iterator //按逆序寻址元素的迭代器
const_reverse_iterator //元素的只读逆序迭代器
difference_type //足够存储两个迭代器差值的有符号整型,可为负数
value_type //元素类型
reference //元素的左值类型,是value_type&同义词
const_reference //元素的常量左值类型,等效于const value_type&<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
在容器中添加元素的插入位置有两种:两端和中间。两端的位置都是已知的,而在中间插入的话需要指明插入的具体位置。
首先是所有容器都适用的插入操作:
void push_back(t) //在容器的尾部添加值为t的元素
iterator insert(iter,t) //在迭代器iter所指向的元素的前面插入值为t的新元素,返回指向新元素的迭代器
//插入一段元素
void insert(iter,n,t) //在迭代器iter所指向的元素的前面插入n个值为t的新元素,返回void类型
void insert(iter,iter1,iter2) //在迭代器iter所指向的元素前面插入由迭代器iter1和iter2标记的范围内的元素
这里需要注意的是:容器元素都是副本。在容器中添加元素,系统是将元素值复制到容器里,而不是引用,添加进去后,容器与元素来源没有任何牵连了。为什么这里插入的位置都是所指定的迭代器参数的前一位置:因为这样可以指向容器的任何位置,包括末端的下一位置。想想,如果是后一位置的话,将end迭代器作为参数则会导致错误插入。
添加元素会导致元素的迭代器失效。因此应当避免存储end操作返回的迭代器。
list和deque容器类型还提供了void push_front(value)函数来在容器前端添加值为value的元素。
四、容器大小操作
这里的容器大小是所有容器通用的,后面讲到vector自增长的时候出现的capacity和reserve是针对vector容器的。
size_type size() //返回容器对象的元素个数
size_type max_size() //返回容器对象最多可容纳的元素个数
bool empty() //判断容器大小是否为空
resize(n) //调整容器大小,使其能容纳n个元素,删除多出的元素。新添加的元素默认初始化
resize(n,value) //调整容器大小,使其能容纳n个元素,新增的元素初始化为t
五、访问容器元素操作
首先,vector和deque容器是可随机访问的容器,因此可以使用下标操作进行访问,而list容器则不允许。
c[n] //返回下标为n的元素的引用
c.at(n) //返回下标为n的元素的引用
其次是所有容器通用的返回前后两端元素的函数:c.back(); //返回最后一个元素
c.front(); //返回第一个元素
当然,我们都可以使用迭代器的间接使用来访问容器元素。六、容器中删除元素操作
删除元素操作和插入元素操作是一致的。首先是所有顺序容器通用的操作:
c.erase(iter); //删除iter迭代器所指向的元素,返回被删除元素后面的元素
c.erase(iter1,iter2); ////删除iter1和iter2所标记范围内的所有元素,返回被删除元素段后面的元素
c.clear(); //清空容器
c.pop_back(); //删除容器最后一个元素。
其次,还有一个只适用与list和deque容器的删除操作:c.pop_front(); //删除容器第一个元素
删除元素会使得指向被删除元素的所有迭代器失效。七、容器对象的赋值操作
赋值操作符=
容器的赋值操作符首先删除其左操作数容器中的所有元素,然后将右操作数的所有元素插入到左边容器中。因此,赋值后,赋值操作符两边的容器是完全相等的,并且等于右操作数。如下:
</pre><pre name="code" class="cpp">c1 = c2; //将c2赋值给c1
//等同于
c1.erase(c1.begin(), c1.endl());
c1.insert(c1.begin(), c2.begin(), c2.end());
assign()函数assign操作是首先删除容器中所有的元素,然后将其参数所指定的新元素插入到该容器中。
c.assign(iter1, iter2);
c.assign(n,value);
因此,就如同容器的初始化一样,当两个容器的类型相同时,可以使用赋值操作符来将一个容器赋值给另外一个容器(当然也可以使用assign)。如果两个容器类型不同的但是兼容的时候,其赋值运算符必须使用assign函数。
swap()函数
swap函数将交换两个容器。
c.swap(c2);
swap操作实现交换两个容器内所有的元素功能,要进行交换的两个容器类型必须匹配。swap操作不会使迭代器失效,在书上有一段话讲的很清楚:swap操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效。没有移动元素这个事实意味着迭代器不会失效。它们指向同一元素(跟着容器)。虽然,在swap运算后,这些元素已经被存储在不同的容器中了,例如,在做swap运算之前,有一个迭代器iter指向svec[3],swap后,iter则指向svec2[3],这是同一个元素,只是存储在不同的容器中了而已。