《C++Primer》第九章——顺序容器

本文详细介绍了C++中的顺序容器,包括vector、deque、list、forward_list、array和string的特点和操作。重点讨论了向容器添加元素、访问元素、删除元素的方法,以及如何改变容器大小。此外,还提到了容器适配器如stack、queue和priority_queue的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第九章:顺序容器

9.1 顺序容器概述

1.顺序容器类型
1)vector:可变大小数组

  • 支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢
    2)deque:双端队列
  • 支持快速随机访问,在头尾位置插入/删除速度很快
    3)list:双向链表
  • 只支持双向顺序访问,在 list 中任何位置进行插入/删除操作速度都很快
    4)forword_list:单向链表
  • 只支持单向顺序访问,在链表任何位置进行插入/删除操作速度都很快
  • 新C++标准增加的类型,设计目标是达到与最好的手写的单向链表数据结构相当的性能,因此没有 size 操作,因为保存或计算其大小就会比手写链表多出额外的开销
    5)array:固定大小数组
  • 支持快速随机访问,不能添加或删除元素
  • 新C++标准增加的类型,与内置数组相比,array 更安全、更易使用,因为大小固定,因此不支持添加和删除元素以改变容器大小的操作
    6)string:与 vector 相似的容器,但专门用于保存字符
  • 随机访问快,在尾部插入/删除速度快
    注:现代C++程序应该使用标准库容器,而不是更原始的数据结构比如说内置数组

9.2 容器库概述

1.迭代器
1)迭代器范围:由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置,这两个迭代器通常被称为 begin 和 end,第二个迭代器从来不会指向范围的最后一个元素,而是指向尾元素之后的位置
左闭合区间——[begin, end)
2)左闭合范围的三种性质

  • 若 begin 和 end 相等,则范围为空
  • 若 begin 和 end 不等,则范围至少包含一个元素,且 begin 指向该范围中的第一个元素
  • 我们可以对 begin 递增若干次,使得 begin == end
    2.容器类型成员:每个容器都定义了多个类型,包括 size_type、iterator 和 const_iterator 等等
//iter 是通过 list<string> 定义的一个迭代器类型
list<string>::iterator iter;
//count 是通过 vector<int> 定义的一个 difference_type 类型
vector<int>::difference_type count;

3.begin 和 end 成员:begin 和 end 操作生成指向容器中第一个元素和尾元素之后位置的迭代器

list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin();	//list<string>::iterator
auto it2 = a.rbegin();	//list<string>::reverse_iterator
auto it3 = a.cbegin();	//list<string>::const_iterator
auto it4 = a.crbegin();	//list<string>::const_reverse_iterator

注意,不是以 c 开头的函数都有重载,比如说 begin,实际上存在两个名为 begin 的成员函数,一个是 const 成员,返回容器的 const_iterator 类型;另一个是非常量成员,返回容器的 iterator 类型,而 rbegin、end、rend 情况类似;
当我们对一个非常量对象调用这些成员是,得到的是返回 iterator 的版本,只有对一个 const 对象调用这些函数时,才会得到一个 const 版本,与 const 指针和引用类似,可以将一个普通的 iterator 转换为对应的 const_iterator,但反之不行

//显式指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
//是 iterator 还是 const_iterator 依赖于 a 的类型
auto it7 = a.begin();	//仅当 a 是 const时,it7 是 const_iterator
auto it8 = a.cbegin();	//it8是 const_iterator

当不需要写访问时,应使用 cbegin 和 cend
4.容器定义和初始化
1)将一个容器初始化为另一个容器的拷贝

  • 方法1:直接拷贝整个容器,两个容器的类型及其元素类型必须匹配
list<string> authors = {"Milton", "Shakespare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

list<string> list2(authors);		//正确,类型匹配
deque<string> authList(authores);	//错误,容器类型不匹配
vector<string> words(articles);		//错误,容器类型必须匹配
  • 方法2:拷贝由一个迭代器对指定的元素范围,这种方法不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为初始化的容器的元素类型即可
//正确,可以将 const char* 元素转换为 string
forward_list<string> words(articles.begin(), articles.end());

2)列表初始化:在新标准中,可以对一个容器进行列表初始化,这样就显式的指定了容器中每个元素的值,对于除 array 之外的容器类型,初始化列表还隐含地指定了容器的大小,容器将包含与初始值一样多的元素

list<string> authors = {"Milton", "Shakespare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

3)与顺序容器大小相关的构造函数:顺序容器(array 除外)还提供另一个构造函数,接受一个容器大小和一个(可选的)元素初始值,若不提供元素初始值,则标准库会创建一个值初始化器

vector<int> ivec(10,-1);	//10个int元素,每个都初始化为-1
list<string> svec(10,"hi");	//10个strings,每个都初始化为"hi!"
forward_list<int> ivec(10);	//10个元素,每个都初始化为0
deque<string> svec(10);		//10个元素,每个都是空string

元素类型是内置类型或者具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数
元素类型没有默认构造函数,除了大小参数外,必须制定一个显式地元素初始值
注:只有顺序容器的构造函数才接受大小参数,关联容器并不支持
4)标准库 array 具有固定大小

  • 当定义一个 array 时,除了指定元素类型,还要指定容器大小
array<int, 42>	//类型为:保存42个int的数组
array<string, 10>	//类型为:保存10个string的数组
array<int, 10>::size_type i;	//数组类型包括元素类型和大小
array<int>::size_type i;		//错误:array<int>不是一个类型
  • 一个默认构造的 array 是非空的,它包含了与其大小一样多的元素,这些元素都被默认初始化
  • 若使用列表初始化,初始值的数目必须等于或小于 array 的大小,若初始值数目小于 array 的大小,则剩余元素会进行值初始化
  • 若元素类型是一个类类型,则必须有默认构造函数,以使值初始化能够进行
array<int, 10> ia1;		//10个默认初始化的int
array<int, 10> ia2 = {0,1,2,3,4,5,6,7,8,9};	//列表初始化
array<int, 10> ia3 = {42};	//ia3[0]为42,剩余元素为0
  • 虽然不能对内置数组类型进行拷贝或者对象赋值操作,但是 array 对此无限制
int digs[10] = {0,1,2,3,4,5,6,7,8,9};
int cpy[10] = digs;				//错误,内置数组不支持拷贝或赋值
array<int, 10> digits = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> copy = digits;	//正确,只要数组类型匹配即合法

5)赋值和 swap
- 赋值(p 302)
- 使用 swap:swap 操作交换两个相同容器的内容
除 array 外,swap 不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成
与其他容器不同,对一个 string 调用 swap 会导致迭代器、引用和指针失效
与其他容器不同,swap 两个 array 会真正交换它们的元素怒,因此,交换两个 array 所需的时间与 array 中元素的数目成正比
6)关系运算符
具体比较方式见 p 304
注:容器的关系运算符使用元素的关系运算符完成比较,因此只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器,若元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算

vector<Sales_data> storeA, storeB;
if(storeA < storeB)	//错误,Sales_data没有 < 运算符

9.3 顺序容器操作

顺序容器和关联容器的不同之处在于二者组织元素的方式,这直接关系元素的存储、访问、添加和删除
1.向顺序容器添加元素:除 array 外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素来改变容器大小
详表见 p 305
1)使用 push_back:将一个元素追加到一个 vector 的尾部,除了 array 和 forward_list,每个顺序容器都支持该操作

string word;
while(cin >> word)
	container.push_back(word);

由于 string 可以认为是一个字符容器,因此也可以用 push_back 在 string 末尾添加字符

void pluralize(size_t cnt, string &word)
{
	if(cnt > 1)
		word.push_back('s');	//等价于 word += ‘s’
}

注:用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的值是对象值得拷贝,而非对象本身,容器中的元素与提供值的对象没有任何关联,因此对容器中元素的任何改变都不会影响到原始对象,反之亦然
2)push_front:list 、forward_list 和 deque 容器还支持名为 push_front 的类似操作,此操作将元素插入到容器头部
注:deque 像 vector 一样提供了随机访问元素的能力,但它提供了 vector 所不支持的 push_front
3)在容器中的特定位置添加元素:vector、deque、list 和 string 都支持 insert 成员,forward_list 提供特殊版本的 insert 成员
注:虽然有些容器不支持 push_front 操作,但它们对于 insert 操作并无类似(插入开始位置)限制,因此可以将元素插入到容器的开始位置,而不必担心容器是否支持 push_front

vector<string> svec;
list<string> slist;

//等价于调用 slist.push_front("Hello");
slist.insert(slist.begin(), "Hello!");

//vector不支持调用push_front,但可以插入到begin()之前
svec.insert(svec.begin(),"Hello!");

将元素插入到vector、deque 和 string 中的任何位置都是合法的,但这样做可能很耗时
4)使用 insert 的返回值:insert 函数返回指向新添加的元素的迭代器
5)使用 emplace 操作:新标准引入了三个新成员——emplace_front、emplace 和 emplace_back,这些操作构造而不是拷贝元素
当调用 push 或 insert 成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中
而当调用一个 emplace 成员函数时,则是将参数传递给元素类型的构造函数,emplace 成员使用这些参数在容器管理的内存空间中直接构造元素

//在容器管理的内存空间中直接创建对象,使用三个参数的Sales_data的构造函数
c.emplace_back("978-0590353403",25,15.99);
//正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data("978-0590353403",25,15.99);

emplace 函数在容器中直接构造元素,传递给 emplace 函数的参数必须与元素类型的构造函数相匹配
2.访问元素
1)包括 array 在内的每个顺序容器都有一个 front 成员函数,而除了 forward_list 之外的所有顺序容器都有一个 back 成员函数
2)访问成员函数返回的是引用,如果容器是一个 const 对象,则返回值是 const 的引用,若容器不是 const,则返回值是普通引用

if(!c.empty()){
	c.front() = 42;			//将42赋予c中的第一个元素
	auto &v = c.back();		//获得指向最后一个元素的引用
	v = 1024;				//改变c中的元素
	auto v2 = c.back();		//v2不是一个引用,它是c.back()的一个拷贝
	v2 = 0;					//未改变c中的元素

3)下标操作和安全的随机访问
提供快速随机访问的容器(string,vector,deque 和 array)也都提供下标运算符,下标运算符接受一个下标参数,返回容器中该位置的元素的引用,给定下标必须在合法范围内,使用越界的下标是一种严重的程序设计错误,而编译器并不检查这种错误
若希望确保下标合法,可使用 at 成员函数,at 成员函数类似下标运算符,但如果下标越界,将会抛出 out_of_range 异常

vector<string> svec;//空vector
cout << svec[0];	//运行时错误:svec中没有元素
cout << svec.at(0);	//抛出一个 out_of_range 异常

3.删除元素(p 311)
1)删除元素的成员函数并不检查其参数,在删除元素之前,程序员必须确保它们是存在的
2)删除 deque 中出首尾位置之外的任何元素都会使所有迭代器、引用和指针失效;指向 vector 和 string 中删除点之后位置的迭代器、引用和指针都会失效
3) pop_front 和 pop_back 成员函数:分别删除首元素和尾元素,与 vector 和 string 不支持 push_front 一样,这些类型也不支持 pop_front,类似的,forward_list 不支持 pop_back
4)从容器内部删除一个元素:成员函数 erase 返回指向删除的(最后一个)元素之后位置的迭代器
5)删除多个元素
接受一对迭代器的 erase 版本允许我们删除一个范围内的元素,并返回最后一个被删元素之后位置的迭代器

elem1 = slist.erase(elem1, elem2);	//调用后,elem1 == elem2

其中,elem1 指向我们要删除的第一个元素,elem2 指向我们要删除的最后一个元素之后的位置
4.特殊的 forward_list 操作(p 313)
1)forward_list 特殊的原因:forward_list 是一个单向链表,而在一个单向链表中添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变,但是在单向链表中没有简单的方法获得一个元素的前驱,因此在一个 forward_list 中添加或删除元素的操作是通过改变给定元素之后的元素来完成的,这样就可以访问到被添加或删除操作所影响的元素
2)由于这些操作与其他容器上的操作的实现方式不同,forward_list 并未定义 insert、emplace 和 erase,而是定义了名为 insert_after、emplace_after 和 erase_after 的操作
3)由于 forward_list 总是操作给定元素之后的元素,因此当 forward_list 添加或删除元素时,我们必须关注两个迭代器,一个指向我们要处理的元素,另一个指向其前驱(通过要处理的元素判断是否需要进行添加或删除的操作,通过前驱元素执行我们需要的操作)
5.改变容器大小:可以通过使用 resize 来增大或缩小容器,array 不支持 resize
若当前大小大于所要求的的大小,容器后部的元素会被删除;
若当前大小小于新大小,会将新元素添加到容器后部,resize 接收一个可选的元素值参数,用来初始化添加到容器中的元素,若调用者没有提供此参数,新元素进行值初始化;若容器保存的是类类型元素,且 resize 向容器添加新元素,则必须提供初始值或者元素类型必须提供一个默认构造函数
6.容器操作可能使迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使容器元素的指针、引用或迭代器失效,失效的指针、引用或者迭代器将不再代表任何元素,使用失效的指针、引用或迭代器是一种严重的程序设计错误
1)向容器添加元素后

  • 若容器是 vector 或 string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效;若存储空间未重新分配,则指向插入位置之后元素的迭代器、指针和引用将会失效
  • 对于 deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效;若在首尾添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效
  • 对于 list 和 forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效
    2)从一个容器删除元素后
  • 对于 list 和 forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效
  • 对于 deque,若在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效;若删除 deque 的尾元素,则尾后迭代器会失效,但其他迭代器、引用和指针不收影响;若删除首元素,则不受影响
  • 对于 vector 和 string,指向被删元素之前元素的迭代器、引用和指针仍有效
    注意:当我们删除元素时,尾后迭代器总会失效
    补充:对于 deque 的解释,deque 不支持对内存重分配时机的控制,除了首尾两端, 在任何地方插入和删除元素都将导致内存重分配 ,重新分配内存, 意味着原来的内存地址失效了, 原来的迭代器指针和引用都将失效;但在首尾添加元素,可能会引发新缓冲区的配置,同时会导致迭代器start、finish失效,而begin()、end()函数返回的恰恰是start、finish,所以迭代器失效。
    3)编辑改变容器的循环程序:程序中必须保证每个循环步中都跟新迭代器、引用和指针,对于调用 insert 和 erase 的程序,更新迭代器很容易,因为这些操作的返回值为迭代器,因此可以用来更新
    4)不要保存 end 返回的迭代器:因为添加/删除 vector 或 string 的元素后,或在 deque 中首元素之外任何位置添加/删除元素后,之前end 返回的迭代器总会失效,因此,添加或删除元素的循环程序必须反复调用 end,而不能在循环之前保存 end 返回的迭代器

9.4 vector对象是如何增长的(p 317)

9.5 额外的string操作(p 321)

说明:标准库 string 定义了大量函数,因此本节以了解 string 支持哪些操作为目的,在需要使用特定操作时回过头来在进行仔细阅读

9.6 容器适配器

除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和 priority_queue
适配器是标准库中一个通用概念,容器、迭代器、函数都有适配器
适配器本质上是一种机制,能使某种事物的行为看起来像另外一种事物一样
一个适配器接受一种已有的容器类型,能使其看起来像一种不同的类型
eg: stack 适配器接受一种顺序容器(array 和 forward_list 除外),并使其操作起来像 stack 一样
1.定义一个适配器
1)默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是在 vector 之上实现的,但我们也可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型

//在 vector 上实现的空栈
stack<string, vector<string>> str_stk;
//str_stk2 在 vector 上实现,初始化时保存 svec 的拷贝

2)对于给定的适配器,可以使用哪些容器是有限制的

  • 所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在 array 上
  • 因为所有适配器都要有添加、删除以及访问尾元素的能力,因此 forward_list 无法用于构造适配器
  • stack 要求 push_back、pop_back 和 back 的操作,因此除 array、forward_list 之外任何容器类型都可用来构造 stack
  • queue 要求 back、push_back、front、pop_front,因此可以构造于 list 或 deque 之上,但不能基于 vector 构造
  • priority_queue 除了 front、push_back、pop_back 还应具有随机访问的能力,因此可以构造于 vector 和 deque 之上,而不能用 list 构造
    2.栈适配器
    1)栈默认基于 deque 实现,也可以在 list 或 vector 上实现
    2)每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作,我们只可以使用适配器的操作,而不能使用底层容器类型的操作
    3.队列适配器
    1)queue 和 priority_queue 适配器定义在 queue 头文件中
    2)queue 默认基于 deque 实现,也可以用 list 实现
    3)priority_queue 默认基于 vector 实现,也可以使用 deque 实现
    4)标准库 queue 使用一种先进先出的存储和访问策略
    5)priority_queue 允许为队列中的元素建立优先级,新加入的元素排在所有优先级它低的已有元素之前,默认情况下,标准库在元素类型上使用 < 运算符来确定相对优先级
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值