《C++ Primer》2. C++ 标准库

IO 库

包括三种流:iostream(基类)、fstream(文件)、sstream(字符串),这三种流每种都有宽字符和窄字符两类,每一类又可以细分为输入流、输出流、输入输出流

各种不同类型的流有继承关系

IO 对象没有拷贝或者赋值:

ofstream out1, out2;
out1 = out2;           // 错误,不能对流对象赋值

IO 流的条件状态、iostateclear 清除错误、setstate 方法

输出缓冲区刷新的原因:

  • 程序正常结束,作为 main 函数 return 操作的一部分,缓冲刷新被执行
  • 缓冲区满
  • endlflushends 等操作显式刷新缓冲区
  • 可以用操纵符 unitbuf 设置流的内部状态,告诉流接下来的每次写操作后都进行一次 flush 操作来清空缓冲区。默认情况下 cerr 是设置 unitbuf 的,写到 cerr 的内容是立即刷新的
  • 输出流可能会关联到另一个流,读写被关联的流时,关联到流的缓冲区会被刷新。一般 cincerr 是关联 cout

文件流在析构时自动关闭打开的文件

顺序容器

标准库中的顺序容器:

  • vector:可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢
  • deque:双端队列,支持快速随机访问
  • list:双向链表,只支持双向顺序访问
  • forward_list:单向链表,只支持单向顺序访问
  • array:固定大小数组,不能添加或删除元素
  • string:与 vector 相似的容器,但专门用于保存字符

如果没有默认构造函数,那么必须传给容器一个初始化函数

内置数组不支持拷贝,但是 array 支持

array 容器类模板需要指定容器大小,如:

array<int, 42> arr;

容器类型别名

  • iterator:迭代器
  • const_iterator
  • reverse_iterator
  • size_type
  • difference_type:带符号的整数类型,足够保存两个迭代器之间的距离
  • value_type:元素类型
  • reference:元素的左值类型,与 value_type & 含义相同
  • const_reference:元素的 const 左值类型

顺序容器操作

swap 方法:

  • stringarray 外,指向容器的迭代器、引用和指针在 swap 操作后都不会失效,它们仍指向 swap 操作之前所指向的元素,但是在 swap 之后已经属于不同的容器了
  • string 执行 swap 后会导致迭代器、引用和指针失效
  • array 执行 swap 会真正交换它们的元素。因此指针、引用和迭代器所绑定的元素保持不变,但进行了值交换

向顺序容器中添加元素:

  • push_back:除 arrayforward_list 外都支持
  • push_frontlistforward_listdeque 支持
  • insert:传入一个迭代器,将元素插入到该迭代器之前的位置。还可以传入数量。它返回第一个新加入元素的迭代器,如果数量为 0,则直接返回传入的迭代器
  • emplace 操作:将参数传递给元素类型的构造函数,容器直接在相应内存空间构造元素

当用一个对象初始化容器时,或将对象插入到容器中时,实际上将元素拷贝进容器

访问元素:

  • front:每个顺序容器都有
  • back:除去 forward_list 都有
  • at 和下标访问:支持快速随机访问的容器(stringvectordequearray)提供。at 会检查下标是否越界,抛出 out_of_range 异常

如果容器为空,那么访问 frontback 都是未定义行为

这几个方法返回的都是引用,如果容器是 const,那么返回的是 const 引用

删除元素:

  • pop_backpop_front
  • erase:可以传入一个迭代器或两个迭代器(表示范围),返回删除的最后一个元素之后一个位置的迭代器

forward_list 的特殊操作:

由于插入、删除操作需要访问前驱,所以它有所不同,定义了 insert_afteremplace_aftererase_after 等方法

并且,定义了首前(off-the-beginning)迭代器 before_begin() 来让我们将元素放到 head 之前

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

添加元素后:

  • vectorstring 在添加元素后可能会扩容,此时重新分配空间,原来的迭代器、指针和引用会失效
  • 对于 deque
    • 它是分段存储,内部存储了段指针数组,而迭代器依赖于这个指针数组
    • 插入到首尾位置之外的任何位置,可能在段之间移动元素,位置发生变化,所以可能会导致迭代器、指针和引用失效
    • 插入首尾位置,可能会分配新的段,但是底层数据结构不会整体移动,所以引用和指针不会失效,但是迭代器会失效
  • 对于列表,添加元素后迭代器、指针和引用仍然有效

删除元素后:

  • 对于 vectorstring,指向被删元素之前元素的迭代器、指针和引用仍有效
  • 对于 deque
    • 在首尾之外的位置删除元素,指向被删元素外的其他元素的迭代器、指针和引用失效
    • 在首位置删除元素,其他元素不影响
    • 在尾位置删除元素,除了 end() 迭代器外,其他元素不影响
  • 对于列表,其他元素不影响

注意,不要保存 end 返回的迭代器

容器大小管理操作

如果没有空间容纳新元素,那么容器会分配新空间保存已有的元素和新元素,将已有元素从旧位置移动到新空间中,释放旧存储空间

一般一次扩容会分配 1.5 或 2 倍空间

容器大小管理操作:

  • shrink_to_fit:只适用于 vectorstringdeque,将 capacity() 减少为与 size() 相同大小。具体实现并不一定退回内存空间
  • capacity:返回不重新分配空间的前提下可以保存元素的数量
  • reserve:传入整数 n,分配至少能容纳 n 个元素的空间

下面两个只适用于 vectorstring

原则:只有当迫不得已时才可以分配新的内存空间

额外的 string 操作

  • substr
  • appendreplace
  • findrfindfind_first_of
  • compare 函数
  • 转换函数:to_stringstoistolstof

容器适配器

除了顺序容器外,标准库还定义了三个顺序容器适配器:

  • stack:接受除 arrayforward_list 外的顺序容器,默认基于 deque
  • queue:接受 deque(默认)和 list
  • priority_queue:接受 vector(默认)和 deque

泛型算法

只读算法、写容器元素的算法、重排容器元素的算法

谓词:可调用的表达式,一元谓词、二元谓词

可调用对象、lambda 表达式

lambda 表达式不能有默认参数

lambda 实际上是个匿名对象,可以捕获环境(值方式和引用方式)

对于值方式捕获的变量,如果想要改变被捕获的变量,需要加 mutable 关键字:

void fcn3() {
    size_t v1 = 42;
    auto f = [v1] () mutable { return ++v1; }
    v1 = 0;
    auto j = f();   // j 为 43
}

引用捕获的直接改就行

参数绑定

bind 函数进行参数绑定:

int f(int, int, int, int, int);

auto g = bind(f, a, b, _2, c, _1);

上面的代码将有五个参数的函数 f 转换为有两个参数的 g

其中,_1_2是占位符,在 std::placeholders 命名空间下

默认情况下,bind 通过拷贝的方式将非占位符参数传递给原函数,但是我们可以通过 ref 函数让它通过引用方式传递:

ostream & print(ostring & os, const string & s, char c) {
    return os << s << c;
}

for_each(words.begin(), words.end(), bind(print, os, _1, ' '));        // 错误,os 不能拷贝
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));   // 正确

除此之外,还有 cref 函数,传递 const 引用

这些函数都定义在头文件 functional

旧版本的 bind1stbind2nd 等已被弃用

额外的迭代器

除了为每个容器定义的迭代器外,还定义了额外的几种迭代器:

插入迭代器

一种迭代器适配器,实现给容器添加元素

it = t 表示在 it 处插入值 t,根据类型会调用 push_backpush_frontinsert

*it++itit++ 都不会对 it 做任何事情,每个操作都返回 it

有三种类型:back_inserterfront_inserterinserter

注意插入顺序: copy(a.begin(), a.end(), front_inserter(...)) 会将 a 中的元素依次插入到头部,这会导致顺序颠倒

流迭代器

用于 IO 类型对象的迭代器

istream_iterator 用于输入流,ostream_iterator 用于输出流。这两个迭代器要求容器实现了 >> 或者 <<

istream_iterator

  • 默认构造函数构造了一个处于尾后位置的迭代器,如:

    istream_iterator<int> in_iter(cin), eof;
    vector<int> vec(in_iter, eof);
    
  • 允许懒惰读取

ostream_iterator

  • 构造函数允许传入一个输出流 os 和一个字符串 d(可选),字符串 d 表示输出时每个值后面都输出一个 d
  • 解引用、递增无效

反向迭代器

除了 forward_list 之外其他容器都支持反向迭代器,使用 rbeginrend 等来获取反向迭代器

反向迭代器需要递减运算符,因此不能从 forward_list 和流中构造反向迭代器

使用 base 方法可以转回正常的迭代器

移动迭代器

后面的章节会介绍

泛型算法结构

算法要求的迭代器操作可以分为 5 个迭代器类别:

  • 输入迭代器:只读不写,单遍扫描,只能递增
  • 输出迭代器:只写不读,单遍扫描,只能递增
  • 前向迭代器:可读写,多遍扫描,只能递增
  • 双向迭代器:可读写,多遍扫描,可递增递减
  • 随机访问迭代器:可读写,多遍扫描,支持全部迭代器运算

C++ 标准指明了泛型和数值算法的每个迭代器参数的最小类别

如果名字后面加一个 _copy,那就是算法的拷贝版本

链表的特定算法

listforward_list 定义了几个成员函数形式的算法,如 sortmergeremovereverse

它们还定义了 splice 算法

链表特有的操作会改变容器

关联容器

标准库提供 8 个管理容器:

  • map
  • set
  • multimapmultiset:关键字可重复出现的 mapset
  • unordered_mapunordered_setunordered_multimapunordered_multiset:哈希组织的关联容器

有序的关联容器使用 < 运算符来比较两个关键字,并且关键字的比较函数必须严格弱序

关联容器的类型别名:

  • key_value
  • mapped_type:每个关键字关联的类型(仅 map
  • value_typeset 中与 key_value 相同,map 中为 pair<const key_type, mapped_type>

set 的迭代器是 const 的,map 的迭代器中关键字也是 const

通常不对关联容器使用泛型算法

插入元素

可以使用 insertemplace,对于非 multi 容器,它们的返回值通常是一个 pair

  • 第一个元素是迭代器,指向具有给定关键字的元素
  • 第二个元素是一个 bool 值,表示是否插入成功

而对于 multi 容器,无需返回 bool 值,它们直接返回一个指向新元素的迭代器

删除元素

关联容器定义了三个版本的 erase,可以传入迭代器、元素值、首尾迭代器

返回实际删除的元素数量

访问元素

map 的下标操作:

如果关键字不在 map 中,那么下标操作会添加一个具有此关键字的元素到 map

at 方法会进行参数检查,如果没有该关键字,返回 out_of_range 异常

下标操作和 at 方法都返回一个左值

可以使用 mapfind 方法代替下标操作,此外还提供 countlower_bound 之类的访问元素的方法

multi 容器中,使用迭代器加 count 来访问元素:

string search_item("aaa");
auto entries = authors.count(search_item);
auto iter = authors.find(search_item);

while (entries) {
    cout << iter->second << endl;
    ++iter;
    --entries;
}

或者使用 lower_boundupper_bound 的组合

或者直接使用 equal_range 来直接得到两个迭代器表示区间

动态内存

两种智能指针:shared_ptrunique_ptr,此外还有一个 weak_ptr

shared_ptrunique_ptr 都支持的操作:

  • 默认构造函数返回一个空智能指针
  • operator bool() 返回内部的指针是否为空指针
  • get 方法返回内部的裸指针
  • swap 方法用来交换两个只能指针中的指针

shared_ptr 类支持:

  • make_shared<T>(...) 方法,传入参数,使用这些参数来初始化一个 T 类型的对象
  • shared_ptr<T> p(q):拷贝构造函数,会递增计数器,并且 q 内的指针要能转换成 T*
  • p = q:拷贝赋值运算符,会递减 p 原来的引用计数,递增 q 的引用计数
  • unique 方法:返回是否只有当前这一个引用
  • use_count 方法:返回当前的引用计数,可能很慢
  • 还有一些传入删除器的方法和构造函数,下面讲

unique_ptr 类支持:

  • 带删除器的构造函数
  • release 方法:放弃控制权,返回指针
  • resetreset(q)reset(nullptr):释放当前指针,然后换成 q 或者空指针

unique_ptr 不能被拷贝,只能被移动;shared_ptr 可以被拷贝和移动

new 的一些操作

可以使用 auto 推断要 new 的对象的类型,但只要当括号中仅有单一初始化器时才可以使用

auto p1 = new auto(obj);

使用 new 分配内存时如果发生错误,会抛出 bad_alloc 异常,可以这样阻止:

int * p1 = new int;              // 如果分配失败,抛出异常
int * p2 = new (nothrow) int;    // 如果分配失败,返回空指针

这是定位 new 的一种

shared_ptrnew 结合使用

只支持显式调用构造函数:

shared_ptr<int> p1(new int(42));   // 正确
shared_ptr<int> p2 = new int(42);  // 错误

还可以:

  • 使用 shared_ptr<T> p(q, d) 构造函数来用 d 替代 shared_ptr 中的 delete 操作
  • reset() 方法:更新引用计数,并替换为空指针
  • reset(q):更新引用计数,并替换为 q
  • reset(q, d):注意此时 d 是用来释放 q

不要混合使用普通指针和智能指针

不要使用 get 初始化另一个智能指针或为智能指针赋值

发生异常时,智能指针也可以自动释放资源

上面的 d 被叫做删除器(deleter),如:

void end_connection(connection * p ) { disconnect(*p); }
void f(destination & d) {
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // 使用连接
    // 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
}

weak_ptr

image-20250322211821242

weak_ptr 不能直接访问对象,需要使用 lock 方法:

if (shared_ptr<int> np = wp.lock()) {
    // ...
}

使用 lock 时会增加相应 shared_ptr 的引用计数

动态数组

创建大小为 0 的动态数组是合法的:

char arr[0];                // 错误
char * cp = new char[0];    // 正确

此时 cp 是一个合法的非空指针,但是它不能解引用

unique_ptr 有适用于动态数组的版本,如:

unique_ptr<int[]> up(new int[10]);
up.release();

注意,此时 release 方法会自动销毁这个动态数组!

shared_ptr 不直接支持管理动态数组,我们必须提供一个删除器:

shared_ptr<int> sp(new int[10], [](int * p) { delete [] p; });
sp.reset();

并且必须使用 get 获得原始指针后再使用下标

allocator

allocator 类帮助我们将内存分配和对象构造分离开来,它的用法如下:

image-20250322213120522

它默认分配的内存是未构造的(unconstructed),也可以接受额外参数来构造

我们只能对已经真正构造了的元素进行 destory 操作

标准库还为 alloctor 类定义了两个伴随算法:

image-20250322213648722