【C++ Primer(第5版)笔记】第9章:顺序容器

第9章 顺序容器

更新记录:
(1)2023.3.8
待更新:
(1)[p292]顺序容器特点的成因(细节)
(2)[p308]使用emplace操作
(3)[p312]9.3.4 特殊的forward_list操作(细节)
(4)[p316]编写改变容器的循环程序
(5)[p316]不要保存end返回的迭代器
(6)[p317]9.4 vector对象是如何增长的(细节)
(6)[p320]9.5 额外的string操作(细节)
(7)[p329]9.6 容器适配器

9.1 顺序容器概述

所有顺序容器都提供了快速顺序访问元素的能力,但因此都有不同的性能折中

(1)向容器添加或从容器中删除元素的代价;(2)非顺序访问容器中元素的代价。

9.1.1 常用顺序容器

新标准库容器性能几乎肯定与最精心优化过的同类数据结构一样好(通常会更好)。

9.1.2 确定使用哪种顺序容器

9.2 容器库概览

1、每个容器都定义在一个头文件中,文件名与类型名相同。

2、容器均定义为模板类。

3、顺序容器几乎可以保存任意类型的元素。可以定义一个容器,其元素类型是另一个元素,如:

vector<vector<string>> lines;

4、如果容器保存的类型没有默认构造函数,则必须提供一个元素初始化器。

// 假定noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10, init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); // 错误:必须提供一个元素初始化器

5、容器操作概览


9.2.1 迭代器

迭代器的操作与运算

1、迭代器支持的所有操作

一个例外:forward_list迭代器不支持递减运算符(--)。

2、迭代器支持的所有运算:

迭代器范围

1、迭代器范围为左闭右开区间,即[begin, end)

2、对构成范围的迭代器的要求:分别指向同一个容器中的元素或尾后元素且end不在begin之前。编译器不强制这些要求,需要程序员自己注意满足要求

3、一个合法的迭代器范围的特点:

(1)如果begin与end相等,则范围为空。

(2)如果begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素。

(3)可以对begin递增若干次,使得begin==end。

// 以上性质意味着,可以使用如下循环处理一个元素范围
while (begin != end) // 范围非空则执行循环体
{
    *begin = val; // 修改范围内元素的值
    ++begin; // 移动迭代器,获取下一个元素
}

9.2.2 容器类型成员

反向迭代器

反向迭代器是一种反向遍历容器的迭代器,其各种操作与正向迭代器相反。

例如,对一个反向迭代器执行++操作,会得到上一个元素。

类型别名
// iter是通过list<string>定义的一个迭代器类型
list<string>::iterator iter;
// count是通过vector<int>定义的一个difference_type类型
vector<int>::difference_type count;

9.2.3 begin和end成员

begin和end成员生成指向容器中第一个元素和尾后元素的迭代器,其常见用途是形成一个包含容器中所有元素的迭代器范围。

1、begin和end的多个版本:带r为反向迭代器,带c为const迭代器。

list<string> a = {"C++", "Java", "Python"};
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

2、不带c版本迭代器返回的具体类型由对象是否是常量决定

(1)对象是常量,返回const_iterator类型;

(2)对象不是常量,返回iterator类型。

vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator

3、带c版本迭代器

(1)不论对象是否为常量,总是返回const_iterator类型;

(2)如果不需要修改对象的值,建议使用const_iterator(cbegin、cend)。

vector<int> v;
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator

9.2.4 容器定义和初始化

将一个容器初始化为另一个容器的拷贝

两种拷贝方式:(1)直接拷贝整个容器;(2)(array除外)拷贝由一对迭代器指定的元素范围

1、直接拷贝整个容器:两个容器的容器类型元素类型都必须相同。

2、(array除外)拷贝由一对迭代器指定的元素范围:

(1)不要求容器类型相同

(2)元素类型不一定要相同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。

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

// 直接拷贝整个容器
list<string> list2(authors); // 正确:、容器类型、元素类型都匹配
deque<string> authList(authors); // 错误:容器类型不匹配
vector<string> words(articles); // 错误:元素类型不匹配

// 拷贝由一对迭代器指定的元素范围
forward_list<string> words(articles.begin(), articles.end()); // 正确:可以将const char*元素转换为string

// 假设it指向authors中的一个元素,则可以用如下代码拷贝子序列
deque<string> authList(authors.begin(), it); // 正确
列表初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

显式指定容器中每个元素的值。对于除了array外的容器类型,初始化列表还隐式指定了容器的大小:容器将包含与初始值一样多的元素。

与容器大小相关的构造函数

只有顺序容器的构造函数才接受大小参数,关联容器不支持。

vector<int> ivec(10, -1) // 10个-1
list<string> svec(10, "hi!"); // 10个"hi"
forward_list<int> ivec(10); // 10个0,不提供初始值则执行值初始化
deque<string> svec(10); // 10个空串,不提供初始值则执行值初始化

若元素类型是内置类型或具有默认构造函数的类类型,则可以只为构造函数提供一个容器大小参数。如果元素类型没有构造函数,则除了大小参数外,还必须指定一个显式的元素初始值。

标准库array具有固定大小

1、当定义一个array时,除了指定元素类型,还要指定容器大小

array<int, 42>
array<string, 10>
array<int, 10>::size_type i; // 正确
array<int>::size_type j; // 错误

2、一个默认构造的array非空,含有与其大小一样多的元素,这些元素都被默认初始化。

3、对array进行列表初始化,初始值的数目必须小于等于array的大小。若初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,剩余元素进行值初始化。

4、如果元素类型是类类型,那么该类必须有一个默认构造函数,使得值初始化可以进行。

5、不同于内置数组,标准库array可以进行拷贝和对象赋值操作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; // 正确:只要容器大小和元素类型都匹配即合法

9.2.5 赋值与swap

常规赋值

赋值运算符将左边容器中的全部元素替换为右边容器中元素的拷贝。

c1 = c2; // 将c1的内容替换为c2中元素的拷贝
c1 = {1, 2, 3}; // 赋值后,c1大小为3
标准库array赋值

1、赋值号左右两边的运算对象必须具有相同的类型。

2、由于左右运算对象大小可能不同,array不支持assign,也不允许用花括号包围的值列表进行赋值。

array<int, 10> a1 = { 0,1,2,3,4,5,6,7,8,9 };
array<int, 10> a2 = {0}; // 所有元素值均为0
a1 = a2; // 正确
a2 = {0}; // 错误:不能将一个花括号列表赋予array对象
使用assign(仅顺序容器)

1、assign成员赋值的要求

(1)不要求容器类型相同

(2)元素类型不一定要相同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。

2、assign成员可以从容器的一个子序列赋值。

3、assign的使用:

(1)assign(iterator, iterator)用指定范围的元素替换容器中的所有元素

list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误:容器类型不匹配
names.assign(oldstyle.cbegin(), oldstyle.cend()); // 正确:可以将const char*转换为string
// 不能自己拷贝自己:names.assign(names.cbegin(), names.cend());

(2)assign(int, 元素值)用指定数目且具有相同给定值的元素替换容器中的所有元素

list<string> slist1(1); // 1个元素,为空string
slist1.assign(10, "Hiya!"); // 替换后:10个元素,每个都是"Hiya!"
使用swap

swap操作交换两个相同类型容器的内容。

vector<string> svec1(10); // 10个元素的vector
vector<string> svec2(24); // 24个元素的vector
swap(svec1, svec2);

1、除array外,交换两个容器内容的操作都很快。元素本身并未交换,swap只是交换了两个容器的内部数据结构。

2、除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。

3、除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。

例如,假定iter在swap之前指向svec1[3],那么在swap之后它指向svec2[3]。

对一个string调用swap会导致迭代器、引用和指针失效。

4、与其他容器不同,swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。

5、对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。

6、容器既提供成员函数版本的swap,也提供非成员版本的swap。统一使用非成员版本的swap是一个好习惯。

9.2.6 容器大小操作

1、成员函数size返回容器中元素的数目。

2、成员函数empty当size为0时返回true,否则返回false。

3、max_size返回一个大于或等于该类型容器所能容纳的最大元素数的值。

4、forward_list支持max_size和empty,但不支持size。

9.2.7 关系运算符

每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。

1、两个容器的比较规则:

(1)如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。

(2)如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。

(3)如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。

2、只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器。

9.3 顺序容器操作

9.3.1 向顺序容器添加元素

使用push_back

除array和forward_list外,每个顺序容器(包括string类型)都支持push_back。此操作将元素插入到容器尾部。

// 用push_back在string末尾添加元素
void pluralize(size_t cnt, string &word)
{
    if (cnt > 1)
        word.push_back('s'); // 等价于word += 's'
}

用一个对象初始化容器时,或将一个对象插入到容器中时,实际上放入容器中的对象值是一个拷贝而不是对象本身

使用push_front

除了push_back,list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部。

// 将元素0, 1, 2, 3添加到ilist头部,形成逆序,序列为3, 2, 1, 0
list<int> ilist;
for (size_t i = 0; i != 4; ++i)
    ilist.push_front(i); 
使用insert在容器中的特定位置添加元素
c.insert(p, t); // 在迭代器p指向的元素插入一个值为t的元素

某些不支持push_front操作的容器,可以使用insert将元素插入在容器开头。这样的操作可能更耗时。

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

slist.insert(slist.begin(), "Hello!"); // 等价于调用slist.push_front("Hello!");
svec.insert(svec.begin(), "Hello!"); // vector不支持push_front,但可以用insert插入到begin()之前;但是插入到vector末尾之外的任何位置都可能很慢
使用insert插入范围内元素

// 1. c.insert(p, n, t)
svec.insert(svec.end(), 10, "Anna"); 
// svec.end()指向尾后元素,将10个值为"Anna"的字符串添加到svec末尾

// 2. c.insert(p, b, e)
slist.insert(slist.begin(), v.end() - 2, v.end());
// 将v的最后两个元素添加到slist的开始位置

// 3. c.insert(p, il)
slist.insert(slist.end(), {"these", "words", "will", "go", "at", "the", "end"});
// 在slist的末尾添加这些字符串
使用insert的返回值

通过使用insert的返回值,可以在容器中一个特定位置反复插入元素:

list<string> lst;
auto iter = lst.begin();
while (cin >> word)
    iter = lst.insert(iter, word); 
// 等价于调用push_front,因为insert返回的迭代器指向新添加的元素。

9.3.2 访问元素

获取首尾元素的引用

1、在调用front和back之前,要确保c非空。

2、获取c中首尾元素的引用的两种方法:

(1)直接方法:调用front和back。

(2)间接方法:解引用begin返回的迭代器获得首元素,递减后解引用end返回的迭代器(指向尾后元素)获得尾元素。

if (!c.empty()) // 先确保c非空
{
    // 获取首元素的引用的两种方法
    auto val1 = *c.begin(), val2 = c.front(); 
    
    // 获取尾元素的引用的两种方法
    auto last = c.end();
    auto val3 = *(--last); // 不能递减forward_list迭代器
    auto val4 = c.back(); // forward_list不支持
}
访问成员函数返回的是引用

在容器中访问元素的成员函数(front, back, 下标和at)返回的都是引用

如果容器是一个const对象,则返回值是const的引用。

如果容器不是const的,则返回值是普通引用,可用于改变元素的值。

if (!c.empty())
{
    c.front() = 42; // 将42赋予c中的第一个元素
    auto &v = c.back();
    v = 1024; // 可以修改c中的元素
    
    auto v2 = c.back();
    v2 = 0; // 不能修改c中的元素,因为v2前面没有&
}

使用auto保存这些函数的返回值,要将变量定义为引用类型。

下标操作和安全的随机访问

1、提供快速随机访问的容器也都提供下标运算符。

2、保证下标有效是程序员的责任;使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误。

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

9.3.3 删除元素

删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它们存在。

pop_front和pop_back成员函数

1、不能对一个空容器执行弹出操作。

2、pop_front、pop_back函数返回void;若需要弹出的元素的值,必须在执行弹出操作之前保存它

使用erase从容器内部删除一个元素
// 下面的循环删除一个list中的所有奇数元素
list<int> lst = { 0,1,2,3,4,5,6,7,8,9 };
auto it = lst.begin();
while (it != lst.end())
    if (*it % 2) // 若元素为奇数
        if = lst.erase(it); // 删除此元素
	else
        ++it;
使用erase从容器内部删除多个元素
// 以下两种写法等价
slist.clear();
slist.erase(slist.begin(), slist.end());

9.3.4 特殊的forward_list操作

9.3.5 改变容器大小

1、array不支持resize

2、如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部。

3、resize接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化

// 注意:以下程序为连续执行
list<int> ilist(10, 42); // 10个int:每个值都是42
ilist.resize(15); // 将5个值为0的元素添加到ilist的末尾
ilist.resize(25, -1); // 将10个值为-1的元素添加到ilist的末尾
ilist.resize(5); // 从ilist末尾删除20个元素

4、如果容器保存的是类类型元素,且resize向容器添加新元素,则必须提供初始值或者元素类型必须提供默认构造函数。

9.3.6 容器操作可能使迭代器失效


建议:每次改变容器的操作后都应正确重新定位迭代器。(尤其对于vector、string和deque)

9.4 vector对象是如何增长的

9.4.1 vector对象增长的问题

vector和string的元素必须连续存储,因此向vector或string添加元素时,如果没有空间容纳新元素,容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。

9.4.2 管理容量的成员函数

1、reserve和resize的区别:

(1)reserve函数并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。

(2)resize函数只改变容器中元素的数目,而不是容器的容量。

2、可以调用shrink_to_fit函数来要求deque、vector或string退回不需要的内存空间。但是该函数不保证一定退回内存空间,可能忽略此请求。

9.4.3 capacity和size

容器的size是指它已经保存的元素的数目;而capacity是指在不分配新的内存空间的前提下它最多可以保存的元素的数目。

9.5 额外的string操作

9.5.1 构造string的其他方法

三个额外的构造string的方法

substr

注意不是左闭右开区间!

string s("hello world");
string s2 = s.substr(0, 5); // s2 = hello
string s3 = s.substr(6); // s3 = world
string s4 = s.substr(6, 11); // s4 = world
string s5 = s.substr(12); // 抛出一个out_of_range异常

1、如果开始位置超过了string的大小,则substr函数抛出一个out_of_range异常。

2、如果开始位置加上计数值大于string的大小,则substr会调整计数值,只拷贝到string的末尾。

9.5.2 改变string的其他方法


9.5.3 string搜索操作


9.5.4 compare函数

9.5.5 数值转换

在这里插入图片描述

int i = 42;
string s = to_string(i); // 将整数i转换为字符串
int num = stoi(s); // 将字符串s转换为整数
double d = stod(s); // 将字符串s转化为浮点数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值