元素在顺序容器中的顺序与其加入容器时的位置相对应。关联容器中元素的位置由元素相关联的关键字值决定。
所有容器都共享公共的接口,不同容器按不同方式对其进行扩展。
一些顺序容器的优点和缺点简介:
string和vector由元素的下标来计算其地址时非常快速的,但是在中间位置添加或者删除元素就会非常耗时。
list和forward_list不支持随机元素的随机访问,但是其在任何位置添加或删除操作都很快。
deque支持快速访问,在两端添加或删除元素很快,在中间添加或删除元素的代价(可能)很高
名词解释:随机访问:一般是通过index下标访问。
确定使用那种顺序容器
除非由更好的理由选择其他容器,否则使用vector
如果程序由很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list
如果程序要求随机访问元素,应使用vector或deque
如果程序要求在容器的中间插入元素或删除元素,应使用list或forward_list
如果程序需要在头尾位置插入或删除元素,但是不会在中间位置进行插入或删除操作,则使用deque
如果程序在读取输入的时候才需要在容器中间位置插入元素,随后需要随机访问元素
首先,确定是否真的需要在容器的中间添加元素
如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list的内容拷贝到vector中。
如果不确定使用哪种容器,那么可以在程序中只使用vector和list公共的操作:使用迭代器,不使用下标操作。避免随机访问。这样在必要时选择使用vector或list都很方便。
容器类型上的操作形成了一种层次:
某些操作时所有容器类型都提供的。
另外一些操作仅针对顺序容器、关联容器或无序容器
还有一些操作只适用于一小部分容器。
一般来说每个容器都定义在一个头文件中,文件名和类型名相同。
对容器可以保存元素类型的限制
与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。
迭代器范围
迭代器范围的概念是标准库的基础
一个迭代器的方位是由一对迭代器表示,两个迭代器分别指向同一个容器中的的元素或者是尾元素之后的位置
元素范围:左闭合区间,[begin,end)
对构成范围迭代器的要求:
1.他们指向同一个容器中的元素,或者容器最后一个元素之后的位置。
2.end不在begin之前。
使用左闭合范围蕴含的编程假定:
1.如果begin与end相等,则范围为空。
2.如果begin与end不相等,则范围内至少有一个元素,且begin指向该范围中的第一个元素。
3.我们可以对begin递增若干次,使得begin=end
每个容器都定义了多个类型:
size_type,iterator和const_iterator
大多数容器还提供了反向迭代器
还有就是类型别名
类型别名
通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。
元素类型,可以使用value_type
元素类型的引用,可以使用reference或const_reference
begin和end操作
生成一个指向容器中 第一个元素和尾元素之后位置的迭代器。形成一个包含容器中所有元素的迭代器范围。
begin和end有多个版本,带r(rbegin,rend)返回反向迭代器,带c(cbegin,cend)返回const迭代器。
当auto与begin或end结合使用时,得到的迭代器类型依赖与容器类型。
当不需要写访问时,应使用cbegin和cend.
每个容器都定义了一个默认构造函数。除array外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
将一个容器初始化为另一个容器的拷贝
直接拷贝整个容器
拷贝由一个迭代器对指定的元素范围
当将一个容器初始化为另外一个容器的拷贝时,两个容器的类型和元素类型必须相同。
与顺序容器大小相关的构造函数
顺序容器提供一个构造函数,它接受一个容器大小和一个(可选)元素初始值。
如果我们不提供元素的初始值,则标准库会创建一个值初始化器。
如果元素类型是内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。
标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分,当定义一个array时,除了指定元素类型,还要指定容器大小。
使用assign (仅顺序容器)
assign允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
std::list names;
//std::vector oldstyle{"hello", "world", "C++", "primer", "5"};
std::vector oldstyle = {"hello", "world", "C++", "primer", "5"};
names.assign(oldstyle.cbegin() + 2, oldstyle.cend() - 1);
for (auto name : names)
{
std::cout << name << std::endl;
}
return 0;
}
//输出内容
//C++
//primer
使用swap
swap操作交换两个相同容器的内容。
除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成
元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效
int main(int argc, char const *argv[])
{
std::vector names{"hello6", "world6", "C++6", "primer6", "6"};
// std::vector oldstyle{"hello", "world", "C++", "primer", "5"};
std::vector oldstyle = {"hello", "world", "C++", "primer", "5", "XXXX", "XXXXXXXXX"};
auto it1 = names.begin() + 2;
auto it2 = oldstyle.end() - 2;
std::cout << "Before swap: *it1 = " << *it1 << ", *it2 = " << *it2 << std::endl;
std::swap(names, oldstyle);
std::cout << "After swap: *it1 = " << *it1 << ", *it2 = " << *it2 << std::endl;
return 0;
}
//输出
//Before swap: *it1 = C++6,*it2 = XXXX
//After swap: *it1 = C++6,*it2 = XXXX
对一个string调用swap会导致迭代器、引用和指针失效。
除forward_list外,每个容器类型都由三个与大小相关的操作。
成员函数size(),返回容器中元素的数目
成员函数max_size(),返回一个大于或等于该类型容器所能容纳的最大元素数的值。
成员函数empty(),当size=0时,返回true
forward_list支持max_size和empty,不支持size.
每个容器都支持相等运算符(==和!=)
除无序关联容器外的所有容器都支持关系运算符(>,>=,<, <=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保持相同类型的元素。
比较两个同期实际上比元素的逐对比较:
如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则不等。
如果两个容器的大小不同,但较小容器中的每个元素都等于较大容器中对应的元素,则较小容器小于较大容器。
如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
int main(int argc, char const *argv[])
{
std::vector v1 = {1, 3, 5, 7, 9, 12};
std::vector v2 = {1, 3, 9};
std::vector v3 = {1, 3, 5, 7};
std::vector v4 = {1, 3, 5, 7, 9, 12};
if (v1 < v2)
{
std::cout << "v1 < v2" << std::endl;
}
if (v1 < v3)
{
std::cout << "v1 < v3" << std::endl;
}
if (v1 == v2)
{
std::cout << "v1 == v2" << std::endl;
}
if (v1 == v4)
{
std::cout << "v1 == v4" << std::endl;
}
return 0;
}
//输出结果
//v1 < v2
//v1 == v4
只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符比较两个容器。
顺序容器和关联容器的不同之处在于两者组织元素的方式。
关联容器是通过键(key)存储和读取元素的,而顺序容器则通过元素在容器中的位置顺序存储和访问元素
除array外,所有标准容器都提供灵活的内存管理。可以动态添加或删除元素
当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。
使用push_back
push_back将一个元素追加到一个vector的尾部。
关键概念:容器元素是拷贝
当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。容器中的元素与提供值的对象之间没有任何关联。随后对容器中的任何元素的任何改变都不会影响到原始对象。
使用push_front
此操作将元素插入到容器头部。
注意:deque像vector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_front。
在容器中的特定位置添加元素
insert成员提供了跟一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。
vector,deque,list和string都支持insert成员。
每个insert函数都接受一个迭代器作为其第一个参数。
insert函数将元素插入到迭代器所指定的位置之前。
插入范围内元素
insert的一个版本支持接受一个元素数目和一个值。它将指定数量的元素添加到指定位置之前,这些元素都按给定值初始化。
接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入指定位置之前。
接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器。
使用emplace操作
emplace_front,emplace,emplace_back这些操作构造而不是拷贝元素。
当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。
emplace函数在容器中直接构造元素,传递给emplace函数的参数必须与元素类型的构造函数相匹配。
访问成员函数返回的是引用
在容器中访问元素的成员函数即(front,back,下标和at)返回的都是引用。
下标操作和安全的随机访问
提供快速随机访问的容器(string,vector,deque,array)也都提供下标运算符。
下标运算符接受一个下标参数,返回容器中该位置元素的引用。
保证下标的有效性是程序员的责任。
使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误。
删除元素的成员函数并不检查其参数。在删除元素之前,程序员 必须保证它(们)是存在的。
如果当前的大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新的大小,会将新元素添加到容器后部。
向容器中添加元素或从容器中删除元素的操作可能会使指向容器元素的指针,引用或迭代器失效。
在向容器添加元素后:
如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、引用和指针都会失效。如果存储空间未失效,那么插入之前的迭代器,引用和指针未失效,插入位置之后的失效。
对于deque,插入收尾外任何绘制都会导致迭代器,引用和指针失效。如果在首尾添加元素,迭代器会失效,但指向存在元素的引用和指针不会失效。
对于list和forward_list,指向容器的迭代器(包括尾后/首前迭代器)、指针和引用仍有效。
当我们删除一个元素后:
对于list和forward_list,指向容器其他位置的迭代器,引用和指针仍然有效。
对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外的其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不受影响。
对于vector和string指向被删元素之前的迭代器、引用和指针仍有效。
注意:当我们删除元素时,尾后迭代器总是会失效。
必须保证每次改变容器的操作之后都正确地重新定位迭代器。(对vector,string和deque尤为重要)
不要保存end返回的迭代器
为了支持快速随机访问,vector将元素连续存储。每一个元素紧挨着前一个元素存放。
当不得不获取性的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。
管理容量成员函数
调用sharink_to_fit也不保证一定退回内存空间。
每个vector实现都可以选择自己的内存分配策略。但是必须遵守一条原则时:只有当迫不得已时才可以分配新的内存空间。
出了顺序容器的宫的操作之外,string还提供了一些额外的操作。
大部分是string和C风格字符数组间的相互转换
允许我们用下标代替迭代器的版本
string类型支持顺序容器的赋值运算符以及assign,insert和erase,它还提供了额外的insert和erase
1.接受下标的版本的insert和erase
s.insert(s.size(), 5, '!');
s.erase(s.size()-5,5);
2.接受C风格字符串的版本的insert和assign
const char* cp = "Stately, plump Buck";
s.assign(cp,7);
s.insert(s.size(), cp+7);
append和replace
string提供了两个额外的成员函数append和replace
string类提供了6个不同的额搜索函数,每个函数有四个重载版本。每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜索失败返回一个string::nops的static成员。
注意事项:
string搜索函数返回的string::size_type是一个unsigned类型,用一个int或其他带符号类型来保存这些函数的返回值不是一个好主意。
compare有6个版本。根据我们比较的是两个string还是一个string一个字符数组,参数各不相同。
int main(int argc, char const *argv[])
{
std::string str("Hello world!!!!");
std::string str1 = "Hello world!!! a xiba";
const char *cstr = "Hello world!!";
str.compare(str1); //比较str和str1
str.compare(0, 2, str1); //将str中从0位置开始的2两个字符与str1作比较
str.compare(0, 2, str1, 2, 4); //将str从0开始的两个元素与str1从2开始的四个元素作比较
str.compare(cstr);//比较str和cstr
str.compare(0, 2, cstr);//将str从0开始的2个元素与C风格字符串cstr作比较
str.compare(0, 2, cstr, 3);//将str从0开始的两个元素与cstr从起始地址开始的3个元素作比较。(cstr字符数组的名字代表其首地址)
return 0;
}
字符串中通常含有表示数值的字符。
有些函数可以实现数值与标准库string之间的转换
适配器是标准库中的一个通用概念。容器,迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事务的行为看起来像另外一种事物一样。一个容器的适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
标准库定义了三种容器适配器:stack,queue,priority_queue
对于一个给定的适配器,可以使用哪些容器是有限制的:
所有适配器都要求容器具有添加和删除元素的能力。适配器不能构造在array上
适配器名称 | 要求 | 可使用的容器 |
---|---|---|
stack | 只要求容器有push_back,pop_back和back操作 | 除array和forward_list外所有 |
queue | 要求back、push_back、front、push_front | list或deque |
priority_queue | 要求front,push_back,pop_back和随机访问能力 | vector或deque |
栈适配器
队列适配器
标准库容器时模板类型,用来保存给定类型的对象。
所有容器(除array)都提供高效动态内存管理。
很大程度上,容器只定义了极少的操作。每个容器都定义了构造函数,添加和删除元素的操作,确定容器大小的操作以及范围指向特定元素的迭代器的操作。其他一些有用的操作是由标准库算法实现的。
当我们使用添加和删除元素的容器操作时,必须注意这些操作可能使指向容器中 元素的迭代器,指针和引用失效。