C++系统学习之九:顺序容器
元素在顺序容器中的顺序与其加入容器时的位置相对应。关联容器中元素的位置由元素相关联的关键字值决定。所有容器类都共享公共的接口,不同容器按不同方式对其进行扩展。
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。
1. 顺序容器概述
容器的两种性能:
- 向容器中添加或删除元素的代价
- 非顺序访问容器中元素的代价
顺序容器类型:
如何确定使用哪种顺序容器
NOTE:通常情况下,使用vector是最好的选择。根据使用的需求对容器的哪种性能更加看重来选择合适的容器。
2. 容器库概览
容器类型上的操作有三个层次:
- 某些操作是所有容器类型都提供的
- 另外一些操作仅针对顺序容器、关联容器或无序容器
- 还有一些操作只适用于一小部分容器
每个容器都定义在一个头文件中,文件名与类型名相同。容器均定义为模板类。
以下容器操作是所有容器类型所共有的操作
2.1 迭代器
迭代器范围
一个迭代器范围由一对迭代器表示,begin指向容器的首元素,end指向容器的尾元素之后的位置。
迭代器范围是一个左闭合区间,[begin, end)。
begin可以和end指向同一个位置,但是end不能指向begin之前的位置。
2.2 容器类型成员
处理迭代器之外还有反向迭代器,对反向迭代器进行++操作,会得到上一个元素。
通过value_type得到元素类型,通过reference或const_reference得到元素类型的一个引用。
2.3 标准库array具有固定大小
与内置数组一样,array的大小也是类型的一部分。定义array时,除了指定元素类型,还要指定容器大小。
array<int, 42> arr;
其构造函数和数组差不多,列表初始化时,列表元素不要超过array的大小,可以少,array剩下的元素就进行值初始化。
array<int, 10> a={1,2,3};
剩下的7个元素值初始化为0;
此外还可以对array进行拷贝或对象赋值操作,但内置数组是不支持的
int a1[3]={0,1,2}; int a2[3]=a1; //错误 array<int, 3> a3={0,1,2}; array<int, 3> a4=a3; //正确
拷贝或赋值要保证类型相同。
2.4 赋值和swap
注意array在初始化的时候可以进行列表初始化,但是赋值的时候不能将一个花括号列表赋值给它。
NOTE:array赋值的时候,两边的运算对象必须具有相同的类型。
array<int, 3> a1 = { 0, 1, 2 }; array<int, 3> a2 = { 0 }; array<int, 3> a3 = { 2, 3, 4 }; a1 = a2; a3 = { 0 }; //错误
对a3赋值的时候,a3的类型是array<int,3>,而右边是先把{0}转换成临时变量array<int,1> tmp,然后再将tmp赋值给a3,这是tmp和a3类型不一致。
由于可能因为大小不一致导致类型不一致,array不支持容器的赋值运算assign。
NOTE:assign操作仅适合顺序容器
赋值运算符要求左右两边的运算对象具有相同的类型,将右边运算对象所有元素拷贝到左边运算对象中。
swap
swap交换两个相同类型容器的内容。swap操作并不会交换元素本身,知识交换两个容器的内部数据结构(除array外)。这就意味着,指向容器的迭代器、引用和指针在swap操作之后都不会失效,仍指向swap操作之前的那些元素,只是这些元素属于不同容器了。
swap两个array会真正交换他们的元素。
有两个版本的swap:成员函数版本以及非成员版本,统一使用非成员版本的swap。
2.5 容器大小操作
- size:forward_list不支持size
- empty
- max_size
2.6 关系运算符
每个容器类型都支持相等运算符(=和!=),除了无序容器外都支持关系运算符。关系运算符两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
关系比较规则:
对元素进行逐对比较。大小相同、对应元素相等则相等;元素相等,大小不等,小的小于大的;都不相等,则比较第一个不相等的元素。
NOTE:只有当容器的元素定义了相应的比较运算符操作时,才可以使用关系运算符。
3. 顺序容器操作
顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。接下来的操作都是顺序容器所独有的操作。
3.1 向顺序容器中添加元素
新标准引入了三个新成员:emplace_front 、emplace和emplace_back,这些操作构造而不是拷贝元素。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用emplace成员函数时,则是将参数传递给元素类型的构造函数。
empalce函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。
c.empalce("978-001",25,15.99); //用三个参数直接构造对象 c.push_back("978-001",25,15.99); //错误,没有接受三个参数的push_back版本 c.push_back(Sales_data("978-001",25,15.99)); //正确
NOTE:emplace函数在容器中直接构造元素,而push_back则是创建临时对象。
3.2 访问元素
访问成员函数返回的是引用
front、back、下标和at,其返回的都是引用。
如果我们使用auto变量来保存这些函数的返回值,并且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型。
auto &v=c.back(); //获得指向最后一个元素的引用 auto v=c.back(); //v不是一个引用,是c.back()的一个拷贝
下标操作必须保证在范围内操作
3.3 删除元素
3.4 特殊的forward_list操作
forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after和erase_after的操作。
3.5 改变容器大小
list<int> ls(10,45); //10个int,都是45 ls.resize(15); //将5个值为0的元素添加到ls的末尾 ls.resize(25,-1); //将10个值为-1的元素添加到ls的末尾 ls.resize(5); //从ls末尾删除20个元素
3.6 容器操作可能使迭代器失效
由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。
4. vector对象是如何增长的
每次都比实际所需空间多分配点空间,而不是每次都把所有元素移动到新空间。
管理容量的成员函数
capacity和size
size是指已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
NOTE:只有当迫不得已时才可以分配新的内存空间。
5. 额外的string操作
5.1 构造string的其他方法
string定义的这些额外操作要么是提供string类和C风格字符数组之间的相互转换,要么是增加了允许我们用下标代替迭代器的版本。
substr操作
substr返回一个string,它是原始string的一部分或全部的拷贝,可以传递给substr一个可选的开始位置和计数值。
5.2 改变string的其他方法
string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。除此之外,还定义了额外的insert和erase版本。
s.insert(s.size(),5,'!'); //在s末尾插入5个感叹号 s.erase(s.size()-5,5); //从s删除最后5个字符 const char *cp="Stately, plump Buck"; s.assign(cp,7); //s=="Stately" s.insrt(s.size(),cp+7); //s=="Stately, plump Buck" string s="some string", s2="some other string"; s.insert(0,s2); //在s中位置0之前插入s2的拷贝 s.insert(0,s2,0,s2.size());
5.3 string搜索操作
搜索是大小写敏感的
逆向搜索
rfind成员函数搜索最后一个匹配,即子字符串最靠右的出现位置。
5.4 compare函数
5.5 数值转换
6. 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。容器、迭代器和函数都有适配器。
本质上,一个适配器是一种机制,能使某种事物的行为看起来像另一种事物一样。
一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。