文章目录
- 顺序容器类型
- 确定使用哪种顺序容器
- 容器库概览
- 容器操作
- 迭代器
- 迭代器支持的所有操作
- 迭代器支持的所有运算
- 迭代器范围
- 对构成范围的迭代器的要求
- 标准库迭代器范围左闭右开的三种性质
- 容器定义和初始化
- 将一个新容器创建为另一个容器的拷贝
- 将array拷贝到vector中的代码
- 与顺序容器大小相关的构造函数
- 标准库array具有固定大小
- 赋值和swap
- 测试swap的代码
- 关系运算符
- 顺序容器的特有操作
- 向顺序容器添加元素
- 容器元素是拷贝
- 在容器中特定位置添加元素:insert
- emplace操作
- 访问元素
- 访问成员函数返回的是引用
- 删除元素
- 特殊的forward_list操作
- 改变容器大小
- 容器操作可能使迭代器失效
- vector对象是如何增长的
- capacity和size
- 额外的string操作
- 构造string的其他方法
- substr操作
- 从一个vector \
顺序容器类型
string和vector将元素保存在连续的内存空间中,因此支持快速随机访问,但是在这两种容器的中间位置添加或删除元素会非常耗时,因为需要移动插入/ 删除位置之后的所有元素,来保持连续存储。而且添加一个元素有时可能还需要分配额外的存储空间,这种情况下,每个元素都必须移动到新的存储空间中。
list和forward_list两个容器添加和删除操作快速,但不支持元素的随机访问,为了访问一个元素只能遍历整个容器,其存储的内存空间不连续。
deque支持快速随机访问,中间位置添加或删除元素非常耗时,但是在deque的两端添加或删除元素很快。
确定使用哪种顺序容器
通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。
容器库概览
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同,即deque定义在头文件deque中,list定义在头文件list中,以此类推。容器均定义为模板类,例如对vector,我们必须提供额外信息来生成特定的容器类型。对大多数,但不是所有容器,我们还需要额外提供元素类型信息:
list<Sales_data> 保存Sales_data对象的list
deque<double> 保存double的deque
顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器。这种容器的定义与任何其他容器类型完全一样:
vector<vector<string>> lines; 此处lines是一个vector,其元素类型是string的vector
容器操作
迭代器
迭代器支持的所有操作
forward_list迭代器不支持递减运算符
迭代器支持的所有运算
这些运算只适用于string、vector、deque和array的迭代器,我们不能将它们用于其他任何容器类型的迭代器。
例如,list的迭代器不支持<运算,只支持递增、递减、==以及!=运算,原因在于list是将元素以链表方式存储,在内存中不连续,两个指针的大小关系与它们指向的元素的前后关系并不一定是吻合的,实现<运算将会非常困难和低效。
迭代器范围
一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这两个迭代器通常被称为begin和end
对构成范围的迭代器的要求
如果满足如下条件,两个迭代器begin和end构成一个迭代器范围:
它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置,且我们可以通过反复递增begin来到达end。换句话说,end不在begin之前。
标准库迭代器范围左闭右开的三种性质
假定begin和end构成一个合法的迭代器范围,则
- 如果begin与end相等,则范围为空
- 如果begin与end不相等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
- 我们可以对begin递增若干次,使得begin==end
容器定义和初始化
每个容器类型都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
将一个新容器创建为另一个容器的拷贝
为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了,而且新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。
将array拷贝到vector中的代码
int ia[] = { 0,1,1,2,3,5,8,13,21,55,89 };
vector<int>vect;
vect.assign(ia,ia+11);
与顺序容器大小相关的构造函数
如果元素类型是内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。
只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小,例如array<int,42>
与其他容器不同,一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化。如果我们对array进行列表初始化,则初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化。在这两种情况下,如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行。
我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制:
赋值和swap
与内置数组不同,标准库array类型允许赋值,赋值后左右两边的运算对象需具有相同的类型:
array<int, 10>a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10>a2 = { 0 };
a1 = a2;//此时a1中的元素为10个0
除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
除string外, 指向容器的迭代器、引用和指针在swap操作之后都不会失效,它们仍指向swap操作之前所指向的那些元素,但是在swap之后,这些元素已经属于不同的容器了,详情可见对vector进行测试的代码。与其他容器不同,swap两个array会真正交换它们的元素,因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换,详情可见对array进行测试的代码。
测试swap的代码
对array进行测试的代码
int main() {array<int, 10>a1 = {1};array<int, 10>a2 = {0};auto beg1 = a1.begin();auto beg2 = a2.begin();cout << "swap之前:" << endl;cout << "a1元素:";for (auto a : a1) {cout << a << " ";}cout << endl;cout << "a2元素:";for (auto a : a2) {cout << a << " ";}cout << endl;cout << "a1.begin:"<< *beg1 << endl;cout << "a2.begin:" << *beg2 << endl;swap(a1,a2);cout << "swap之后:"<<endl;cout << "a1元素:";for (auto a : a1) {cout << a << " ";}cout << endl;cout << "a2元素:";for (auto a:a2) {cout << a << " ";}cout << endl;cout << "a1.begin:" << *beg1 << endl;cout << "a2.begin:" << *beg2 << endl;system("pause");return 0;
}
测试结果:
swap之前:
a1元素:1 0 0 0 0 0 0 0 0 0
a2元素:0 0 0 0 0 0 0 0 0 0
a1.begin:1
a2.begin:0
swap之后:
a1元素:0 0 0 0 0 0 0 0 0 0
a2元素:1 0 0 0 0 0 0 0 0 0
a1.begin:0
a2.begin:1
对vector进行测试的代码:
int main() {vector<int>a1 = {1};vector<int>a2 = {0};auto beg1 = a1.begin();auto beg2 = a2.begin();cout << "swap之前:" << endl;cout << "a1元素:";for (auto a : a1) {cout << a << " ";}cout << endl;cout << "a2元素:";for (auto a : a2) {cout << a << " ";}cout << endl;cout << "a1.begin:"<< *beg1 << endl;cout << "a2.begin:" << *beg2 << endl;swap(a1,a2);cout << "swap之后:"<<endl;cout << "a1元素:";for (auto a : a1) {cout << a << " ";}cout << endl;cout << "a2元素:";for (auto a:a2) {cout << a << " ";}cout << endl;cout << "a1.begin:" << *beg1 << endl;cout << "a2.begin:" << *beg2 << endl;system("pause");return 0;
}
测试结果:
swap之前:
a1元素:1
a2元素:0
a1.begin:1
a2.begin:0
swap之后:
a1元素:0
a2元素:1
a1.begin:1
a2.begin:0
关系运算符
每个容器都支持相等运算符(==和 !=),除了无序关联容器外的所有容器都支持关系运算符(>,>=,<,<=),关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
比较两个容器实际上是进行元素的逐对比较:
- 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器
- 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果
顺序容器的特有操作
向顺序容器添加元素
除array外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素来改变容器大小。
容器元素是拷贝
当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。容器中的元素与提供值的对象之间没有任何关联,随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
在容器中特定位置添加元素:insert
在新标准下,接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器,如果范围为空,不插入任何元素,insert操作会将第一个参数返回。
通过使用insert的返回值,可以在容器中一个特定位置反复插入元素。
s.iinsert(iter,"hello") 将"hello"添加到iter之前的位置
s.iinsert(iter,10,"hello") 将10个"hello"添加到iter之前的位置
s.insert(s.begin(),s2.begin(),s2.end()) 将指定范围中的元素插入到给定迭代器位置之前
s.insert(s.begin(),{"the","haha","heihei"}) 将初始化列表中的元素插入到给定位置之前
s.insert(s.begin(),s.begin(),s.end()) 此句错误,拷贝的范围不能指向与目的位置相同的容器
emplace操作
访问元素
访问成员函数返回的是引用
在容器中访问元素的成员函数(即,front、back、下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值:
删除元素
特殊的forward_list操作
改变容器大小
示例代码如下:
容器操作可能使迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能会引起与使用未初始化指针一样的问题。
如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器,必须在每次操作后重新调用end(),而不能在循环开始前保存它返回的迭代器。
vector对象是如何增长的
reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。
capacity和size
容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
额外的string操作
除了顺序容器共同的操作之外,string类型还提供了一些额外的操作,这些操作中的大部分要么是提供string类和c风格字符数组之间的相互转换,要么是增加了运行我们用下标代替迭代器的版本。
构造string的其他方法
代码示例:
substr操作
从一个vector <char> 初始化一个string
vector提供了一个data成员函数,返回其内存空间的首地址
vector<char>vi{ 'a','b' ,'c','d'};
string s(vi.begin(),vi.end());
string s2(vi.data(),vi.size());
改变string的其他方法
string的下标版本的insert和erase版本
string类型支持顺序容器的赋值运算符以及assign、insert和erase操作,除此之外,它还定义了下标版本的insert和erase版本:
string s="0123456789";s.insert(5, 3, 'A'); //此时s是01234AAA56789,即在s[5]之前插入3个‘A’string s1 = "0123456789";s1.erase(5, 3);//此时s1是0123489,即删除s[5]之后的3个元素,包括s[5]
string的c风格字符数组版本的insert和erase版本
const char *cp = "0123456789";string s;s.assign(cp,5); //s=="01234"s.insert(s.size(),cp+7);s=="01234789"
string s = "some string";string s2 = "some other string ";s.insert(0,s2);//s=="some other string some string"// 在s[0]之前插入s2中s2[0]开始的s2.size()个字符s.insert(0,s2,0,s2.size());//s=="some other string some other string some string"
append和replace函数
string s="some ";
s.append("things"); //s=="some things"
s.replace(5,3,"hhhh");//s=="some hhhhngs"即从s[5]开始将3个元素,替换为“hhhh”
string搜索操作
string搜索操作,每个操作都返回一个string::size_type值,表示匹配位置的下标。如果搜索失败,则返回一个名为string::npos的static成员。
string find
find查找参数指定的字符串,若找到,则返回第一个匹配位置的下标,否则返回npos
string name("AnnaBelleAnnaBelle");
auto pos1=name.find("Anna");//pos1==0
auto pos2 = name.find("Anna",1);//pos2==9 此处是从name[1]处开始查找"Anna",返回第一个匹配位置的下标
string find_first_of
string numbers("0123456789");string name("n1inin4n");auto pos1 = name.find_first_of(numbers,2);//pos1==6auto pos1 = name.find_first_of(numbers);//pos1==1
string compare函数
根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、正数或负数。
数值转换
int i = 42;string s = to_string(i);//将整数i转换为字符“42”double num = stod(s);// 将字符“42”转换为浮点数42
s = "00110011";int num = stoi(s,0,2);// 将s转换为二进制,num==51
容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。
适配器是标准库中的一个通用概念,容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
所有容器适配器都支持的操作和类型
栈适配器
stack定义在stack头文件中,先进后出
队列适配器
queue和priority_queue定义在queue头文件中。
queue,先进先出
priority_queue,允许我们为队列中的元素建立优先级,新加入的元素会排在所有优先级比它低的已有元素之前。默认情况下,标准库在元素类型上使用<运算符来确定相对优先级。