简介
- C++程序设计语言 第四部分 标准库相关笔记
第三十章 标准库概览
30.1 引言
- 标准库是一个组件集合,在ISO C++标准中定义,在所有实现中都以一致的形式(和性能)提供。出于可移植性和长期维护的考虑,强烈推荐在合适的地方尽量使用表混库。一般而言,不要尝试重新发明轮子。
30.1.1 标准库设施
标准库是所有C++实现都必须提供的,以便每个程序员都能依靠它来编写程序。C++标准库提供
- 语言特性的支持,例如内存管理,范围for语句和运行时类型信息
- 具体C++实现所定义的一些语言相关的信息,如最大float值
- 单纯用语言难以高效实现的基本操作,例如 is_polymorphic, is_scalar 和 is_nothrow_constructible
- 底层(无锁)并发编程设施
- 基于线程的并发编程的支持
- 基于任务的并发的基本支持,例如future和async()
- 大多数程序员难以实现最优且可移植版本的函数,例如 uninitialized_fill()和memmove()
- 无用内存回收(垃圾收集)的基本支持,例如declare_reachable()
- 程序员编写可移植代码所需的复杂基础组件,例如list,map,sort和IO流
- 用于标准库自身扩展的框架,例如允许用户为自定义类型提供与内置类型相似的I/O操作的规范和基础组件以及标准模板库STL
标准库的设计目标之一是成为其他库的公共基础。特别是,组合使用标准库特性可以起到三方面的支撑作用
- 可移植性的基础
- 一组紧凑且高效的组件,可以作为构造性能敏感的库和应用的基础
- 一组实现库内交互的组件。
30.2 头文件
标准库组件都定义在命名空间std中,以一组头文件的形式提供。头文件构成了标准库最主要的部分,因此,列出头文件可以给出标准库的一个概貌。
以字母c开头的标准库头文件对应C标准库中的头文件。每个C标准库头文件<x.h>都定义了一些同时位于全局命名空间和命名空间std中的内容,且有一个定义相同内容的对应头文件
。理想情况下,头文件 中的名字不会污染全局命名空间,但不幸的是(归咎于管理多语言,多操作系统环境的复杂性)大多数实际情况下会发生污染。 容器
可变大小一维数组 双端队列 单向链表 - 双向链表
集合 哈希关联数组 哈希集合 队列 栈 固定大小一维数组 bool数组
关联容器multimap和multiset分别声明在
通用工具
运算符和值对 元组 类型萃取 将type_info用作一个关键字或哈希码 函数对象 资源管理指针 限定作用域的分配器 编译时有理数算术运算 时间工具 C风格日期和时间工具 迭代器及其支持
迭代器机制令标准库算法具有通用性
算法
泛型算法 bseach(), qsort()
一个典型的泛型算法能应用于任何类型的元素构成的序列。C标准库函数bsearch()和qsort()只能用于内置数组,且元素类型不能有用户自定义的拷贝构造函数和析构函数
诊断
异常类 标准异常 断言宏 C风格错误才处理 系统错误支持
字符串和字符
T的字符串 字符分类 宽字符分类 C风格字符串函数 C风格宽字符字符串函数 C风格分配函数 C风格多字节字符串 正则表达式匹配
头文件
声明了strlen(),strcpy()等一族函数。头文件 声明了atof()和atoi(),可将C风格字符串转换为数值。 输入/输出
I/O组件的前置声明 标准iostream对象和操作 iostream基类 流缓冲 输入流模板 输出流模板 操纵符 字符串流 字符分类函数 文件流 printf() I/O函数族 宽字符printf()风格I/O函数
操纵符是操作流状态的对象
本地化
表示文化差异 文化差异C风格表示 代码转换
locale对日期输出格式,货币表示符号和字符串校勘等在不同语言和文化中有差异的内容进行本地化
语言支持
数值限制 数值标量限制C风格宏 浮点数限制C风格宏 标准整数类型名 动态内存管理 运行时类型识别支持 异常处理支持 initializer_list C标准库语言支持 可变长函数参数列表 C风格栈展开 程序终止 系统时钟 C风格信号处理
头文件
定义了sizeof()返回的类型size_t,指针减法和数组下标返回的类型ptrdiff_t以及声名狼藉的NULL宏 C风格栈展开(使用<csetjmp中的setjmp和longjmp>)与析构函数和异常处理不兼容,因此最好避免使用。
数值
复数及其运算 数值向量及其运算 推广的数值运算 标准数学函数 C风格随机数 随机数发生器
由于历史原因,abs()和div()不像其他数学函数那样在
中,而是在 中 并发
原子类型及其操作 等待动作 异步任务 互斥类 线程
C标准库的一些组件与C++程序员有着不同程度的关联,C++标准库提供了对这些组件的访问机制。
C兼容性
公共整数类型的别名 C的bool类型 浮点数环境 C的对齐机制 C的泛型数学函数: 和
30.3 语言支持
语言支持是标准库中很小但至关重要的部分,是程序正常运行所必需的,因为语言特性依赖于这些组件。
标准库支持的语言特性
new和delete typeid()和type_info 范围for initializer_list
30.3.1 initializer_list 支持
一个 {} 列表会依据规则转换为一个 std::initializer_list
类型的对象。 不幸的是, initializer_list并未提供下标运算符。如果你希望用 [] 而不是 *,可对指针使用下标
1
2
3
4
5
6
7
8void f(initializer_list<int> lst)
{
const int* p = lst.begin();
for (int i = 0; i < lst.size(); i++>)
{
std::cerr << p[i] << "\n";
}
}initializer_list自然也可用于范围for语句
1
2
3
4
5
6
7void f2(initializer_list<int> lst)
{
for (auto x : lst)
{
std::cerr << x << "\n";
}
}
30.3.2 范围for支持
- 一条范围for语句会借助迭代器映射为一条for语句。
- 在
中,标准库提供了std::begin()和std::end()两个函数,可用于内置数组及任何提供了begin()和end()成员的类型。 - 所有标准库容器和字符串都支持使用范围for的迭代;容器适配器(例如stack和priority_queue)则不支持。容器的头文件(例如
)会包含 ,因此用户很少需要自己直接包含它。
30.4 错误处理
标准库包含的组件已有将近40年的开发历程。因此,它们处理错误的风格和方法并不统一
- C风格库函数大多数通过设置errno来指示发生了错误
- 很多对元素序列进行操作的算法返回一个尾后迭代器来指示 未找到 或 失败
- I/O流库要依赖于每个流中的一个状态来反映错误,并可能(根据用户需要)通过抛出异常来指示错误。
- 一些标准库组件,例如vector,string和bitset通过抛出异常来指示错误。
标准库的设计目标之一是所有组件都遵守基本保证:即,即使抛出了异常,也不会有资源(例如内存)泄露,且不会有标准库类的不变式被破坏的情况出现。
30.4.1 异常
- 一些标准库组件通过抛出异常来报告错误
- 标准库异常
- bitset: 抛出invalid_argument, out_of_range,overflow_error
- iostream: 如果允许异常的话,抛出ios_base::failure
- regex: 抛出regex_error
- string: 抛出length_error,out_of_range
- vector: 抛出out_of_range
- new T: 如果不能为一个T分配内存,抛出bad_alloc
- dynamic_cast
(r): 如果不能将引用r转换为一个T,抛出bad_cast - typeid(): 如果不能获得一个type_info,抛出bad_typeid
- thread: 抛出system_error
- call_once(): 抛出system_error
- mutex: 抛出system_error
- condition_variable: 抛出system_error
- async(): 抛出system_error
- packaged_task: 抛出system_error
- future和promise: 抛出system_error
- 任何直接或间接使用这些组件的代码都可能遇到这些异常。
- 除非你确认使用组件的方式不会令它们抛出异常,否则坚持在某处(例如main())捕获标准库异常类层次的某个根类(例如exception)和任何异常(…)是一个很好的编程习惯。
30.4.1.1 标准库exception类层次
- 不要抛出int,C风格字符串等内置类型,而应该抛出专门表示异常的类型的对象。
30.4.1.2 异常传播
中提供了一些组件,令异常传播对程序员可见 - 异常传播
- exception_ptr: 非特定的异常指针类型
- ep = current_exception() ep是一个exception_ptr,指向当前异常,若当前无活动异常则不指向任何异常;不抛出异常
- rethrow_exception(ep): 重新抛出ep指向的异常;ep包含的不能是空指针nullptr;无返回值
- ep = make_exception_ptr(e): ep是指向exception e的exception_ptr;不抛出异常。
- 一个exception_ptr可以指向任何异常,不局限于exception类层次中的异常。可将exception_ptr看作一种智能指针(类似shared_ptr)–只要一个exception_ptr还指向其异常,那么这个异常就会保持活跃。这样,我们就可以通过exception_ptr将一个异常从捕获它的函数中传递出来,并在其他地方重新抛出。即,exception_ptr可用来实现在捕获线程之外的其他线程中重抛出异常,这是promise和future所需要的。对一个exception_ptr使用rethrow_exception(在不同线程中)不会引起数据竞争。
- 异常不能从noexcept函数中传播出去.
30.4.1.3 terminate()
- 在
中,标准库提供了处理意外异常的组件 - terminate
- h = get_terminate(): h为当前终止处理程序;不抛出异常
- h2 = set_terminate(): 当前终止处理程序被设定为h;h2为旧终止处理程序;不抛出异常
- terminate(): 终止程序;无返回值;不抛出异常。
- 除了极特殊的情况下使用set_terminate()和terminate()之外,其他情况应避免使用这些函数
30.4.2 断言
- 标准库提供了断言机制
- 断言
- static_assert(e, s) 在编译时对e求职;若!e为假则将s作为编译器错误信息输出
- assert(e): 若宏NOBUG未定义,则在运行时对e进行求职,若!e为假,向cerr输出一个消息并调用abort();若定义了NOBUG,则什么也不做。
- assert()是一个宏,定义在
中,assert()生成什么错误信息由C++具体实现自己决定,但应该包含源文件名(FILE)和assert()所在的源码行号(LINE) - 断言常常用于产品级代码而非教材的小例子中(它也本应如此)
- 函数名(func)也可能包含在消息中。
30.4.3 system_error
- 在
中,标准库提供了一个能从操作系统和底层系统组件报告错误的框架。
30.4.3.1 错误码
- 当一个错误以错误码的形式从程序底层浮上来时,我们必须处理这个错误或将错误码转换为一个异常。
30.4.3.2 错误类别
30.4.3.3 system_error异常
- system_error报告的错误都源自标准库中处理操作系统的部分。它传递一个error_code,并可传递一个错误消息字符串。
- 自然的,system_error可用于标准库之外的程序。它传递一个系统相关的error_code,而不是一个可移植的error_condition
30.4.3.4 可移植的错误状态
- 可移植错误码(error_condition)的表现形式与系统相关的error_code几乎相同。总体思路是每个系统有一组特有的(“原生的”)错误码,可映射到潜在可移植的错误码,这样对于需要跨平台编程的程序员(通常是编写库的程序员)来说会更加方便。
30.5 建议
- 使用标准库组件保持可移植性
- 使用标准库组件尽量减少维护成本
- 将标准库组件作为更广泛和更专门化的库的基础
- 使用标准库组件作为灵活,广泛使用的软件的模型
- 标准库组件定义在命名空间std中,都是在标准库头文件中定义的
- 每个C标准库头文件X.h都有其C++标准库对应的版本
- 必须#include相应的头文件才能使用标准库组件
- 为了对内置数组使用范围for,需要 #include
- 优选基于异常的错误处理而非返回错误码方式的错误处理
- 始终要捕获 exception&(对标准库和语言支持的异常)和…(对意料之外的异常)
- 标准库exception层次可以(但不是必须支持)用于用户自定义异常
- 如果发生严重错误,调用terminate()
- 大量使用static_assert()和assert()
- 不要假定assert()总是会被求值
第三十一章 STL容器
31.1 引言
- STL包含标准库中的迭代器,容器,算法和函数对象几个部分。
31.2 容器概览
一个容器保存着一个对象序列。容器可分类为
- 顺序容器提供对元素(半开)序列的访问
- 关联容器提供基于关键字的关联查询
此外,标准库还提供了一些保存元素的对象类型,它们并未提供顺序容器或关联容器的全部功能
- 容器适配器提供对底层容器的特殊访问
- 拟容器保存元素序列,提供容器的大部分但非全部功能
STL容器都是资源句柄,都定了拷贝和移动操作。所有容器操作都提供了基本保证,确保与基于异常的错误处理机制能够正确协同工作。
顺序容器
- vector<T, A> 空间连续分配的T类型元素序列,默认选择容器
- list<T, A> T类型元素双向链表,当需要插入/删除元素但不移动已有元素时选择它
- forward_list<T, A> T类型元素单向链表,很短的或空序列的理想选择
- deque<T, A> T类型元素双端队列,向量和链表的混合,对大多数应用而言,都比向量和链表其中之一要慢。
这些容器都定义在
, - 和
中。顺序容器为元素连续分配内存(例如vector)或将元素组织为链表(例如forward_list),元素的类型是容器的成员value_type(或者是上表中的T)。deque(发音为 deck)采用链表和连续存储的混合方式。 除非你有充足的理由,否则应该优选vector而不是其他顺序容器。注意,vector提供了添加,删除元素的操作,这些操作都允许vector按需增长或收缩。对于包含少量元素的序列而言,vector是一种完美的支持列表操作的数据结构。
当在一个vector中插入,删除元素时,其他元素可能会移动。与之相反,链表或关联容器中的元素则不会因为插入新元素或删除其他元素而移动。
forward_list(单向链表)是一种专为空链表和极短链表优化过的数据结构。一个空forward_list只占用一个内存字。在实际应用中,有相当多的情况链表是空的(还有很多情况链表是非常短)
有序关联容器
- map<K,V,C,A> 从K到V的有序映射,一个(K,V)对序列
- multimap<K,V,C,A> 从K到V的有序映射,允许重复关键字
- set<K,C,A> K的有序集合
- multiset<K,C,A> K的有序集合,允许重复关键字
这些容器通常用平衡二叉树(通常是红黑树)实现
关键字(K)的默认序标准是 std::less
类似顺序容器,模板参数A是分配器,容器用它来分配和释放内存。对映射,A的默认值是 std::allocator<std::pair<const K,T>>,对集合,A的默认值是std::allocator
无序关联容器
- unordered_map<K,V,H,E,A> 从K到V 的无序映射
- unordered_multimap<K,V,H,E,A> 从K到V 的无序映射,允许关键字重复
- unordered_set<K,H,E,A> K的无序集合
- unordered_multiset<K,H,E,A> 从K的无序集合,允许关键字重复
这些容器都是采用溢出链表法的哈希表实现。关键字类型K的默认哈希函数类型H为 std::hash
。关键字类型K的默认相等判断函数类型E为 std::equal_to ;相等性判定函数用来判断哈希值相同的两个对象是否相等 容器适配器是一类特殊容器,它们为其他容器提供了特殊的接口
- priority_queue<T,C,Cmp> T的优先队列,Cmp是优先级函数类型
- queue<T,C> T的队列,支持push()和pop()操作
- stack<T,C> T的栈,支持push()和pop()操作
一个priority_queue的默认优先级函数Cmp为std::less
.queue的默认容器类型C为 std::deque ,stack和priority_queue的默认容器类型C为std::vector 某些数据类型具有标准容器所应有的大部分特性,但又非全部。我们有时称这些数据类型为 拟容器。
- T[N] 固定大小的内置数组;N个连续存储的类型为T的元素;没有size()或其他成员函数
- array<T, N> 固定大小的数组,N个连续存储的类型为T的元素,类似内置数组,但解决了大部分问题。
- basic_string<C, Tr, A> 一个连续分配空间的类型为C的字符序列,支持文本处理操作,例如连接(+, +=);basic_string通常都经过了优化,短字符串无须使用自由存储空间。
- string basic_string
- u16string basic_string
- u32string basic_string
- wstring basic_string
- valarray
数值向量,支持向量运算,但有一些限制,这些限制是为了鼓励高性能实现;只在做大量向量运算时使用 - bitset
N个二进制位的集合,支持集合操作,例如&和| - vector
vector 的特例化版本,紧凑保存二进制位
对basic_string,A是其分配器,Tr是字符萃取
如果可以选择的话,应该有限选择vector,string或array这样的容器,而不是内置数组。内置数组有两个问题–数组到指针的隐式类型转换和必须要记住大小,它们都是错误的主要来源。
还应该优先选择标准字符串,而不是其他字符串或C风格字符串。C风格字符串的指针语义意味着笨拙的符号表示和程序员的额外工作,它也是主要错误来源之一(例如内存泄漏)
31.2.1
C++标准并未给标准容器规定特定的表示形式,而是指明了容器接口和一些复杂性要求。实现者会选择适当的(通常也是巧妙优化过的)实现方法来满足一般要求和常见用途。除了处理元素所需的一些内容之外,这类句柄还持有一个分配器。
对于一个vector,其元素的数据结构很可能是一个数组。vector会保存指向一个元素组的指针,还会保存元素数目和向量容量(已分配的和尚未使用的位置数)或等价的一些信息
list很可能表示为一个指向元素的链接序列以及元素数目
forward_list很可能表示为一个指向元素的链接序列
map很可能实现为一颗平衡树,树节点指向(键,值)对
unorder_map很可能实现为一个哈希表
string的实现可能为:短string的字符保存在string句柄内,而长string的元素则保存在自由存储空间中的连续区域(类似vector的元素)。类似vector,一个string也会预留空闲空间,以便扩张时不必频繁的重新分配空间
类似内置数组,array就是一个简单的元素序列,无句柄。这意味着一个局部array不会使用任何自由存储空间(除非它本身实在自由存储空间中分配的),而且一个类的array成员也不会悄悄带来任何自由存储空间操作。
31.2.2 对元素的要求
若想作为一个容器的元素,对象类型必须允许容器拷贝,移动以及交换元素。如果容器使用拷贝构造函数或拷贝赋值操作拷贝一个元素,拷贝的结果必须是一个等价的对象。这大致意味着任何对象值相等性检测都必须得到副本和原值相等的结论。换句话说,元素拷贝必须能像int的普通拷贝一样正常工作。
类似的,移动构造函数和移动赋值操作也必须具有常规定义和常规移动语义。此外,元素类型还必须允许按常规语义交换元素。如果一个类型定义了拷贝或移动操作,则标准库swap()就能正常工作。
对元素类型的要求和细节散布在C++标准中,很难阅读,但基本上,如果一个类型具有常规的拷贝或移动操作,容器就能保存该类型元素。只要满足容器元素的基本要求和算法的特定要求(例如元素有序),很多基本算法,例如copy(),find()和sort()都能正常运行。
当无法拷贝对象时,一个替换方案是将对象指针而不是对象本身保存在容器中。最典型的例子就是多态类型。例如,我们使用vector<unique_prt
>或vector<Shape*>,而不是vector 。来保证多态性行为
31.2.2.1 比较操作
- 关联容器要求其元素能够排序,很多可以应用于容器的操作也有此要求。默认情况下,< 运算符被用来定义序。如果 < 不合适,程序员必须提供一个替代操作。
- 排序标准必须定义一个严格弱序(strict weak ordering)。形式化地描述,即小于和相等关系(如果定义了的话)都必须是传递的。
31.3 操作概览
- 常量,表示操作花费的时间不依赖于容器中的元素数目;常量时间(constant time)的另一种常见表示方式是O(1)。O(n)表示操作花费的时间与元素数目成正比
- 所有容器的size()操作都是常量时间的。
31.3.1 成员类型
- 每个容器都定义了如下一组成员类型
- value_type 元素类型
- allocator_type 内存管理类型
- size_type 容器下标,元素数目等无符号类型
- difference_type 迭代器差异的带符号类型
- iterator 行为类似value_type*
- const_iterator 行为类似 const_value_type*
- reverse_iterator 行为类似 value_type*
- const_reverse_iterator 行为类似 const_value_type*
- reference const_value_type&
- const_reference 行为类似value_type*
- pointer 行为类似 value_type*
- const_pointer 行为类似 const_value_type*
- kep_type 关键字类型;仅关联容器具有
- mapped_type 映射值类型;仅关联容器具有
- key_compare 比较标准类型;仅有序容器具有
- hasher 哈希函数类型: 仅无序容器具有
- key_euqal 等价性检验函数类型;仅无需容器具有
- local_iterator 桶迭代器类型;仅无需容器具有
- const_local_iterator 桶迭代器类型;仅无需容器具有
- 每个容器和 拟容器 都提供了上表中大多数成员类型,但是不会提供无意义的类型
31.3.2 构造函数,析构函数和赋值操作
- 赋值操作并不拷贝或移动分配器,目标容器获得了一组新的元素,但会保留其旧分配器,新元素(如果有的话)的空间也是用此分配器分配的。
- 谨记,一个构造函数或是一次元素拷贝可能会抛出异常,来指出它无法完成这个任务
- 紧急,对大小初始化器使用 (),而对其他所有初始化器都是用 {}
- 容器通常都很大,因此我们几乎总是以引用方式传递容器实参。但是,由于容器是资源句柄,我们可以高效地以返回值的方式返回容器(隐含使用移动操作)。类似地,当我们不想用别名的时候,可以用移动方式传递容器实参。
31.3.3 大小和容量
- 大小是指容器中的元素数目;容量是指在重新分配更多内存之前容器能够保存的元素数目。
- 在改变大小或容量时,元素可能会被移动到新的存储位置。这意味着指向元素的迭代器(以及指针和引用)可能会失效(即,指向旧元素的位置)
- 指向关联容器(例如map)元素的迭代器只有当所指元素从容器中删除(erase())时才会失效。与之相反,指向顺序容器(例如vector)元素的迭代器当元素重新分配空间(例如resize(), reverse()或push_back())或所指元素在容器中移动(例如在前一个位置进行erase()或insert())时也会失效。
31.3.4 迭代器
- 容器可以看作按容器迭代器定义的顺序或相反的顺序排列的元素序列。对一个关联容器,元素的序由容器比较标准(默认为 < )决定
- 元素遍历的最常见形式是从头至尾遍历一个容器。最简单的遍历方法是使用范围for语句,它隐含的使用了begin()和end()
- 当我们需要了解一个元素在容器中的位置或需要同时引用多个元素时,就要直接使用迭代器。在这些情况下,auto很有用,它能帮助尽量简化代码并减少输入错误。
31.3.5 元素访问
31.3.6 栈操作
标准vector,deque和list(不包括forward_list和关联容器)提供了高效的元素序列尾部操作
栈操作
- c.push_back(x) 将x添加到c的尾元素之后(使用拷贝或移动)
- c.pop_back() 删除c的尾元素
- c.emplace_back(args) 用args构造一个对象,将它添加到c的尾元素之后
c.push_back(x)将x移动或拷贝入x,这会将c的大小增加1。如果内存耗尽或x的拷贝构造函数抛出一个异常,c.push_back(x)会失败。push_back()失败不会对容器造成任何影响,因为标准库操作都提供了强保证。
31.3.7 列表操作
31.3.8 其他操作
- 容器可以比较和交换
- swap()操作既交换元素也交换分配器
31.4 容器
31.4.1 vector
- STL的vector是默认容器–除非你有充分理由,否则应该使用它。如果你希望使用链表或内置数组替代vector,应慎重考虑后再做决定。
31.4.1.1 vector和增长
- 使用大小(元素数目)和容量(不重新分配空间的前提下可容纳的元素数目)零push_back()操作时的向量增长相当高效:不会在添加每个元素时都分配内存,而是在超出容量时才进行一次重新分配。C++标准并未指出超出容量时向量的增长幅度,但很多C++实现都是增加大小的一半。
- 容量的概念令指向vector元素的迭代器只有在真正发生重分配时才会失效。
31.3.1.2 vector和嵌套
- 与其他数据结构相比,vector(以及类似的连续存储元素的数据结构)有三个主要优势
- vector的元素是紧凑存储的:所有元素都不存在额外的内存开销。类型为vector
的vec的内存消耗大致为 sizeof(vector ) + vec.size() * sizeof(x)。其中sizeof(vector ) 大约为12个字节,对大向量而言是微不足道的 - vector的遍历非常快。为访问下一个元素,我们不必利用指针间接寻址,而且对类vector结构上的连续访问,现代计算机都进行了优化。这使得vector元素的线性扫描(就像find()和copy()所做的)接近最优
- vector支持简单且高效的随机访问。这使得vector上的很多算法(例如sort()和binary_search())非常高效
- vector的元素是紧凑存储的:所有元素都不存在额外的内存开销。类型为vector
31.3.1.3 vector和数组
- vector是一种资源句柄,这是允许改变大小和实现高效移动语义的原因。但是这一特点偶尔也会变为缺点–尤其是与不依赖于元素和句柄分离存储的数据结构(例如内置数组和array)相比。将元素序列保存在栈中或另一个对象中可能会带来性能上的优势。
31.3.1.4 vector和string
- vector
是可改变大小,连续存储的char序列,string也是如此。那么我们应该如何在两者之间进行选择呢 - vector是一种保存值的通用机制,并不对保存的值之间的关系做任何假设。对一个vector
而言,字符串Hello,World只不过是一个13个char类型的元素的序列而已。与之相反,string的设计目的就是保存字符序列,它认为字符间的关系是非常重要的。因此,我们很少会对string中的字符进行排序,因为这会破坏字符串的含义。某些string操作反映了这一点(例如c_str(), >> 和 find() C风格字符串以0结束)。string的实现也反映了对其使用方式的假设。
31.4.2 链表
STL提供了两种链表类型
- list:双向链表
- forward_list:单向链表
list为元素插入和删除操作进行了优化。当你向一个list插入元素或是从一个list删除元素时,list中其他元素的位置不会受到影响。特别的是,指向其他元素的迭代器也不会受到影响。
默认情况下,list的元素都独立分配内存空间,而且要保存指向前驱和后继的指针。与vector相比,每个list元素占用更多内存空间(通常每个元素至少多4个字),遍历(迭代)操作也要慢得多,因为需要通过指针进行间接寻址而不是简单的连续访问。
forward_list是单向链表。你可以将其看作一种为空链表或很短的链表专门优化的数据结构,对这类链表的操作通常是从头开始遍历。
31.4.3 关联容器
- 关联容器支持基于关键字的查找。它有两个变体
- 有序关联容器(ordered associative container)基于一个序标准(默认是小于比较操作 <)进行查找。这类容器用平衡二叉树实现,通常是红黑树
- 无序关联容器(unordered associative container)基于一个哈希函数进行查找。这类容器用哈希表实现,采用溢出链表策略。
- 两类容器都支持
- map: {键,值}对序列
- set: 不带值的map(或者你可以说关键字就是值)
- 最后,映射和集合,无论是有序还是无序的,都有两个变体
- 普通映射或集合:每个关键字只有唯一一项
- 多重映射或集合:每个关键字可对应多项。
- 一个关联容器的名字指出了它在三维空间{集合|映射,普通|无序,普通|多重}中的位置。
31.4.3.1 有序关联容器
实际上,[]并不仅仅是insert()的简写形式,它所做的要更多一些。m[k]的结果等价于 (*(m.insert(make_pair(k,v{})).first)).second,其中V是映射类型。insert(make_pair())这种描述方式相当冗长,我们可以用emplace()取而代之:dictionary.emplace(“sea cow”, “extinct”);
关联容器中元素的关键字是不可变的。因此,我们不能改变set中的值。我们甚至不能改变不参与比较的元素的成员。如果需要修改元素,应使用map。不要尝试修改关键字:假如你成功了,就意味着查找元素的底层机制会崩溃。
31.4.3.2 无序关联容器
- 无序关联容器都是用哈希表实现的。对简单应用而言,无序关联容器与有序容器的差别不大,因为关联容器共享大部分操作。
- unordered_map的遍历顺序取决于插入顺序,哈希函数和装载因子。特别是,元素的遍历顺序并不保证与其插入顺序一致。
31.4.3.3 构造unordered_map
31.4.3.4 哈希和相等判定函数
- 用户可以自定义哈希函数,定义方式有多种,不同的技术可满足不同的需求。
31.4.3.5 装载因子和桶
- 无序容器实现的重要部分对程序员是可见的。我们说具有相同哈希值的关键字 落在同一个桶中。程序员也可以获取并设置哈希表的大小。
- 无序关联容器的装载因子(load factor)定义为已用空间的比例。例如,若capacity()为100个元素,size()为30,则load_factor()为0.3
- 桶接口的一个用途是允许对哈希函数进行实验: 一个糟糕的哈希函数会导致某些关键字值的bucket_count()异常大,即,很多关键字被映射为相同的哈希值。
31.5 容器适配器
- 容器适配器(container adaptor)为容器提供不同的(通常是受限的)的接口。容器适配器的设计用法就是仅通过其特殊接口使用。特别是,STL容器适配器不提供直接访问其底层容器的方式,也不提供迭代器或下标操作。
- 从一个容器创建容器适配器的技术是一种通用的按用户需求非侵入式适配类接口的技术。
31.5.1 stack
- stack是一个容器接口,容器类型是作为模板实参传递给它的。stack通过接口屏蔽了其底层容器上的非栈操作,接口采用常规命名: top(), push()和pop()
- 此外,stack还提供了常用的比较运算符(==, <等)和非成员函数swap()
- 默认情况下,stack用deque保存其元素,但任何提供back(),push_back()和pop_back()操作的序列都可使用。例如
1
2stack<char> s1; // 使用deque<char>保存元素
stack<int, vector<int>> s2; // 使用vector<int>保存元素 - vector通常比deque更快,使用内存也更少
- stack对底层容器使用push_back()来添加元素。因此,只要机器还有可用内存供容器申请,stack就不会溢出。
- 默认情况下,stack使用其底层容器的分配器。如果这不够,有几个构造函数可以指定分配器。
31.5.2 queue
- queue定义在
中,它是一个容器接口,允许在back()中插入元素,在front()中提取元素
31.5.3 priority_queue
- priority_queue是一种队列,其中每个元素都被赋予一个优先级,用来控制元素被top()获取的顺序。priority_queue的声明非常像queue,只是多了处理一个比较对象的代码和一组从序列进行初始化的构造函数。
- 默认情况下,priority_queue简单的用 < 运算符比较元素,用top()返回优先级最高的元素
31.6 建议
- 一个STL容器定义一个序列
- 将vector作为默认容器使用
- insert()和push_back()这样的插入操作在vector上通常比在list上更高效
- 将forward_list用于通常为空的序列
- 当设计性能时,不要盲目信任你的直觉,而要进行测试
- 不要盲目信任渐进复杂性度量;某些序列很短而单一操作的代价差异可能很大
- STL容器都是资源句柄
- map通常实现为红黑树
- unordered_map是哈希表
- STL容器的元素类型必须提供拷贝和移动操作
- 如果你希望保持多态行为,使用指针或智能指针的容器
- 比较操作应该实现一个严格弱序
- 以传引用方式传递容器参数,以传值方式返回容器
- 对一个容器,用()初始化器初始化大小,用{}初始化器语法初始化元素列表
- 用范围for循环或首位迭代器对容器进行简单遍历
- 如果不需要修改容器元素,使用const迭代器
- 当使用迭代器时,用auto避免冗长易错的输入
- 用reserve()壁面指向容器元素的指针和迭代器失效
- 未经测试不要假定reserve()会有性能收益
- 使用容器上的push_back()或resize(),而不是数组上的realloc()
- vector和deque改变大小后,不要继续使用其上的迭代器
- 在需要时使用reserve()令性能可预测
- 不要假定[]会进行范围检查
- 当需要保证进行范围检查时使用at()
- 用emplace()方便符号表示
- 优选紧凑连续存储的数据结构
- 用emplace()避免提前初始化元素
- 遍历list的代价相对较高
- list一般会有每个元素四个字的额外内存开销
- 有序容器序列由其比较对象(默认为 <)定义
- 无序容器(哈希容器)序列并无可预测的序
- 如果你需要在大量数据中快速查找元素,使用无序容器
- 对五自然序的元素类型,使用无序容器
- 如果需要按顺序遍历元素,使用有序关联容器
- 用实验检查你的哈希函数是否可以接受
- 用异或操作组合标准哈希函数得到的哈希函数通常有很好的性能
- 0.7通常是一个合理的装载因子
- 你可以为容器提供其他接口
- STL适配器不提供对其底层容器的直接访问
第三十二章 STL算法
32.1 引言
- STL包含标准库中的迭代器,容器,算法和函数对象几个部分
32.2 算法
中定义了大约80个标准算法。 很多算法都遵循一种常规表示方式: 返回序列的末尾来表示 未找到。
无论是标准库算法还是用户自己设计的算法,都很重要
- 每个算法命名一个特定操作,描述其接口,并指定其语义
- 每个算法都可能广泛使用并被很多程序员熟知
如果你发现你写的一段代码有若干看起来没什么关联的循环,局部变量,或是有很复杂的控制结构,那么就应该考虑是否可以简化代码,将某些部分改写为具有描述性的名字以及良好定义的目的,接口和依赖关系的函数/算法。
32.2.1 序列
- 标准库算法的理想目标是为可优化实现的某些东西提供最通用最灵活的接口。
- 注意,无论一个STL算法返回什么,它都不会是实参的容器。传递给STL算法的实参是迭代器,算法完全不了解迭代器所指向的数据结构。
- 迭代器的存在主要是为了将算法从它所处理的数据结构上分离开来,反之亦然。
32.8 建议
- STL算法操作一个或多个序列
- 一个输入序列是一个半开区间,由一对迭代器定义
- 进行搜索时,算法通常返回输入序列的末尾位置表示 未找到
- 优选精心说明的算法而非 随意代码
- 当你要编写一个循环时,思考它是否可以表达为一个通用算法
- 确保一对迭代器实参确使指定了一个序列
- 当迭代器对风格显得冗长时,引入容器/范围版本的算法
- 用谓词和其他函数对象赋予标准算法更宽泛的含义
- 谓词不能修改其实参
- 指针上默认的==和 < 极少能满足标准算法的需求
- 了解你使用的算法的时间复杂性,但要记住复杂性评价只是对性能的粗略引导
- 只在对一个任务没有更专用的算法时使用 for_each() 和 transform()
- 算法并不直接向其实参序列添加元素或从其中删除元素
- 如果不得不处理未初始化的对象,考虑 uninitialized_* 系列算法
- STL算法基于其排序比较操作实现相等性比较
- 注意,排序和搜索C风格的字符串要求用户提供一个字符串比较操作。
第三十三章 STL迭代器
33.1 引言
- 迭代器是标准库算法和所操作的数据间的粘合剂。反过来,也可以说迭代器机制是为了最小化算法与所操作的数据结构间的依赖性
33.1.1 迭代器模型
- 与指针类似,迭代器提供了简介访问的操作(例如解引用操作 *)和移动到新元素的操作(例如,++操作移动到下一个元素)。一对迭代器定义一个半开区间 [begin:end),即所谓序列(sequence)
- 即,begin指向序列的首元素,end指向序列的尾元素之后的位置。永远也不要从 *end 读取数据,也不要向它写入数据。
- 注意,空序列满足 begin == end;即,对任意迭代器p都有 [p:p)是空序列
- 为了 读取一个序列,算法通常接受一对表示半开区间[begin:end)的迭代器(b, e),并使用++编译序列直至到达末尾
33.1.2 迭代器类别
标准库提供了五种迭代器
- 输入迭代器(input iterator): 利用输入迭代器,我们可以用++向前遍历序列并用*(反复)读取每个元素。我们可以用==和!=比较输入迭代器。istream提供了这种迭代器
- 输出迭代器(output iterator): 利用输出迭代器,我们可以用++向前遍历序列并用*每次写入一个元素。ostream提供了这种迭代器
- 前向迭代器(forward iterator): 利用前向迭代器,我们可以反复使用++向前遍历序列并用*读写元素(除非元素是const的)。如果一个前向迭代器指向一个类对象我们可以用->访问其成员。我们可以用==和!=比较前向迭代器。forward_list提供了这种迭代器
- 双向迭代器(bidirection iterator): 利用双向迭代器,我们可以向前(用++)和向后(用–)遍历序列并用*(反复)读写元素(除非元素是const的)。如果一个双向迭代器指向一个类对象,我们可以用->访问其成员。我们可以用==和!=比较双向迭代器。list,map和set提供了这种迭代器
- 随机访问迭代器(random-access iterator): 对一个随机访问迭代器,我们可以用[]进行下标操作,用+加上一个整数,以及用-减去一个整数。我们可以将指向同一个序列的两个随机访问迭代器相减来获得他们的距离。我们可以用==,!=,<=, >和>=比较双向迭代器。vector提供了这种迭代器
这些迭代器类别是概念而非类,因此这个层次并非用继承实现的类层次。如果你希望用迭代器类别做一些更进阶的事情,可(直接或间接)使用iterator_traits。
33.1.3 迭代器萃取
- 迭代器标签的本质是类型,用来基于迭代器类型选择算法。
- 关键思路是:为了获得迭代器的属性,你应该访问其 iterator_traits而不是迭代器本身
33.1.4 迭代器操作
33.2 迭代器适配器
- 在
中,标准库提供了适配器,能从一个给定的迭代器类型生成有用的相关迭代器类型 - reverse_iterator 反向遍历
- back_insert_iterator 在尾部插入
- front_insert_iterator 在头部插入
- insert_iterator 在任意位置插入
- move_iterator 移动而不是拷贝
- raw_storage_iterator 写入未初始化的存储空间
33.4 函数对象
- 很多标准库算法接受函数对象(或函数)参数,来控制其工作方式。常见的函数对象包括比较标准,谓词(返回bool的函数)和算术运算。在
中,标准库提供了若干常用函数对象。
33.5 函数适配器
- 函数适配器接受一个函数参数,返回一个可用来调用该函数的函数对象。
- g = bind(f, args)
- g(args2)等价于f(args3)是通过用args2中的实参替换args中对应的占位符(例如 _1, _2, _3)得到的
- g = mem_fn(f) 若p是一个指针,则g(p, args)表示p->f(args),否则g(p, args)表示p.mf(args); args是一个实参列表
- g = not1(f) g(x)表示!f(x)
- g = not2(f) g(x, y) 表示 !f(x, y)
- g = bind(f, args)
33.5.3 function
- 我们可以直接使用bind(),以及用它来初始化auto变量。从这个角度看,bind()很像是一个lambda
- 标准库function是一种类型,它可以保存你能调用运算符()调用的任何对象。即,一个function类型对象就是一个函数对象
- 显然,function对回调,将操作作为参数传递等机制非常有用
33.6 建议
- 一个输入序列由一对迭代器定义
- 一个输出序列由单一迭代器定义;程序员应负责避免溢出
- 对任意迭代器p,[p:p)是一个空序列
- 使用序列列尾表示 未找到
- 将迭代器理解为更通用,通常行为也更好的指针
- 使用迭代器类型,例如 list
::iterator,而不是指向容器中元素的指针 - 用iterator_traits获取迭代器的相关信息
- 可以用iterator_traits实现编译时分发
- 用iterator_traits实现基于迭代器类别选择最优算法
- 用base()从reverse_iterator提取iterator
- 可以使用插入迭代器向容器添加元素
- move_iterator可用来将拷贝操作变为移动操作
- 确认你的容器可用范围for语句遍历
- 用bind()创建函数和函数对象的变体
- 注意bind()会提前解引用;如果你希望推迟解引用,使用ref()
- 可使用mem_fn()或lambda将p->f(a)调用规范转换为f(p, a)
- 如果你需要一个可以保存各种可调用对象的变量,使用function
第三十四章 内存和资源
34.1 引言
- STL时标准库中高度结构化的,通用的数据管理和操作组件。本章介绍更为专用的以及处理裸内存的(与处理强类型对象相对)组件
34.2 拟容器
标准库中有一些容器不能很好的纳入STL框架,例如内置数组,array和string。我有时将他们称为拟容器。
- T[N] 固定大小的内置数组,连续存储的N个类型为T的元素,隐式转换为T*
- array<T,N> 固定大小的数组,连续存储的N个类型为T的元素,类似内置数组,但解决了大部分问题
- bitset
固定大小的N个二进制的序列 - vector
vector的特例化版本,紧凑保存二进制位序列 - pair<T, U> 两个元素,类型为T和U
- tuple<T…> 任意数目任意类型的元素的序列
- basic_string
类型为C的字符的序列,提供了字符串操作 - valarray
类型为T的数值的数组,提供了数值运算
为什么标准库会提供这么多容器?这是为了满足很多常见但又有差异(通常也有重叠)的需求。如果标准库不提供这些容器,很多人将不得不自己实现。例如
- pair和tuple是异构的,所有其他容器都是同构的(元素都是相同类型)
- array, vector和tuple连续保存元素;forward_list和map是链式结构
- bitset和vector
保存二进制位,通过代理对象访问这些二进制位;所有其他标准库容器都可以保存不同类型并直接访问元素 - basic_string要求其元素位某种字符类型,它提供了字符串操作,例如连接操作和区域敏感操作,valarray要求其元素为数值类型,并提供数值运算。
34.2.1 array
array定义在
中,是固定大小的给定类型的元素的序列,元素数目在编译时指定。因此,连同其元素可以在栈中,对象内或静态存储中分配空间。 array在哪个作用域中定义,元素就会在其中分配。理解array的最好方式是将其视为固定大小的内置数组,但不会隐式的,出乎意料地转换为指针类型,且提供了一些便利的函数
array中不保存任何管理信息(例如大小)。这意味着移动一个array比拷贝它更为高效(除非array的元素是资源句柄,且定义了高效的移动操作)。array没有构造函数或分配器(因为它不直接分配任何东西)
array的元素数目和下标值是unsigned类型(size_t)
元素数目必须是一个常量表达式
如果需要可变元素数目,应该使用vector。另一方面,由于array的元素数目在编译时即知,array的size()是一个constexpr函数
如果需要,可以将一个array作为指针显式的传递给一个C风格的函数
vector是如此灵活,我们为什么要使用array呢?
- 原因是array虽不如vector灵活,但更简单。少数情况下,直接访问分配在栈中的元素较之在自由存储空间中分配元素,然后通过vector(句柄)间接访问它们,最后将它们释放,会有巨大的性能优势。
- 当然另一方面,栈是一个有限的资源(特别是在一些嵌入式系统中),而栈溢出是非常糟糕的
如果我们可以使用内置数组,又为什么要使用array呢?
- array了解自己的大小,因此很容易使用标准库算法,而且可以拷贝(用=或初始化)。
- 但是选择array的主要原因是,它使我不必为糟糕的指针转换头疼。
34.2.2 bitset
一个bitset
就是一个包含N个二进制位的数组,它定义在 中。它与vector 的不同之处是大小固定,与set的不同之处是二进制位用整数索引而不是关联值,与vector 和set的共同差异是提供了操作二进制位的操作。 用内置指针是不可能寻址一个二进制位的。因此,bitset提供了一种位引用(代理)类型。对那些由于某种原因不适合用内置指针寻址的对象,通常这种技术是很有用的解决方案
bitset设计中的一个关键思想是:能放入单个机器字中的bitset可优化实现。其接口反映了这一思想。
34.2.3 vector
- vector
定义在 中,它是vector的一个特例化版本,提供了二进制(bool值)的紧凑存储 - 显然,vector
与bitset很相似,与bitset不同而与vector 相似的是,vector 具有分配器,也能改变大小。 - 类似vector
,vector 中索引更大的元素保存在高地址。这与bitset的内存布局完全相反。而且标准库也提供将整数和字符串直接转换为vector 的操作。
34.2.4 元组
- 标准库提供了两种将任意类型的值组成单一对象的方法
- pair保存两个值
- tuple保存零个或多个值
- 如果我们预先知道恰好有两个值,pair就很有用处了。而tuple用于我们必须处理任意多个值的情况
34.3 资源管理指针
- 一个指针指向一个对象(或不指向任何东西)。但是,指针并不能指出谁(如果有的话)拥有对象。即,仅仅查看指针,我们得不到任何关于”谁应(或是如何,或是是否)删除对象”的信息。
- 在
中,我们可以找到表达所有权的智能指针 - unique_ptr: 表示互斥的所有权
- shared_ptr: 表示共享的所有权
- weak_ptr: 可打破循环共享数据结构中的回路
34.3.1 unique_ptr
unique_ptr提供了一种严格的所有权语义
- 一个unique_ptr拥有一个对象,它保存一个指针(指向该对象)。即,unique_ptr有责任用所保护的指针销毁所指向的对象(如果有的话)
- unique_ptr不能拷贝(没有拷贝构造函数和拷贝赋值函数),但是可以移动
- unique_ptr保存一个指针,当它自身被销毁时,使用关联的释放器(如果有的话)释放所指向的对象(如果有的话)
unique_ptr的用途包括
- 为动态分配的内存提供异常安全
- 将动态分配内存的所有权传递给函数
- 从函数返回动态分配的内存
- 在容器中保存指针
unique_ptr不提供拷贝构造函数和拷贝赋值运算符。
34.3.2 shared_ptr
shared_ptr表示共享所有权。当两段代码需要访问同一个数据,但两者都没有独享所有权(负责销毁对象)时,可以使用shared_ptr。shared_ptr是一种计数指针,当计数变为零时释放所指向的对象。
我们可以将共享指针理解为包含两个指针的结构:一个指针指向对象,另一个指针指向计数器。
释放器(deleter)用来在计数器变为零时释放共享对象。默认释放器通常是delete(它会调用对象的析构函数(如果存在的话),并释放自由存储空间)
当可以选择时
- 优先选择unique_ptr而不是shared_ptr
- 优先选择普通限域对象而不是在堆中分配空间,由unique_ptr管理所有权的对象
34.3.3 weak_ptr
- weak_ptr指向一个shared_ptr所管理的对象。为了访问对象,可使用成员函数lock()将weak_ptr转换为shared_ptr。weak_ptr允许访问他人拥有的对象
- (仅)当对象存在时你才需要访问他
- 对象可能在任何时间被(其他人)释放
- 在对象最后一次被使用后必须调用其析构函数(通常释放非内存资源)
34.4 分配器
- STL容器和string都是资源句柄,获取和释放内存来保存其元素。为此,它们使用分配器(allocator)。分配器的基本目的是为给定类型提供内存资源以及提供在内存不再需要时将其归还的地方。
- 基本的分配器函数有
1
2p = a.allocate(n); // 为n个类型为T的对象获取空间
a.deallocate(p, n); // 释放p所指的保存n个类型为T的对象的空间
34.4.1 默认分配器
- 所有标准库容器都(默认)使用默认分配器,它用new分配空间,用delete释放空间
34.5 垃圾收集接口
- 垃圾收集(自动回收无引用的内存区域)有时被认为是万能灵药,但它并不是。特别是,垃圾收集器可能无法避免并非纯内存的资源的泄露,例如文件句柄,线程句柄以及锁。
- 我将垃圾收集看作下列常见的防泄漏技术都已用尽时的最后一种方便的手段
- 只要可能,应使用具有正确语义的资源句柄来防止应用程序中的资源泄露。标准库提供了string, vector, unordered_map, thread, lock_guard以及其他很多资源句柄。移动语义允许从函数高效返回这类对象
- 使用unique_ptr保存这样的对象:不隐式管理其所拥有资源(例如指针),需要免受不成熟释放机制之害或是需要特别关注分配方式(释放器)、
- 使用shared_ptr保存需要共享所有权的对象。
34.6 未初始化内存
- 大多数情况下,最好避免使用未初始化的内存。这样做可以简化编程,消除很多错误。
- 除了标准的allocator,
头文件还提供了fill*系列函数用于处理未初始化内存。
34.7 建议
- 当你需要一个具有constexpr大小的序列时,使用array
- 优先选择array而不是内置数组
- 当你需要N个二进制位而N又不到一定是整数类型的位宽时,使用bitset
- 避免使用vector
- 当使用pair时,考虑使用make_pair()进行类型推断
- 当使用tuple时,考虑使用make_tuple()进行类型推断
- 使用unique_ptr表示互斥所有权
- 使用shared_ptr表示共享所有权
- 尽量不适用weak_ptr
- (仅)当由于逻辑上或性能上的原因,常用的new/delete语义不能满足需求时才使用分配器
- 优先选择有特定语义的资源句柄而不是智能指针
- 优先选择unique_ptr而不是shared_ptr
- 优先选择智能指针而不是垃圾收集
- 为通用资源的管理提供一致,完整的策略
- 在大量使用指针的程序中处理泄露问题,垃圾收集是非常有用的
- 垃圾收集是可选的
- 不要伪装指针(即使你不使用垃圾收集)
- 如果你使用垃圾收集,使用declare_no_pointers()令垃圾收集器忽略不可能包含指针的数据
- 不要随机使用未初始化内存,除非你确使必须这么做。
第三十五章 工具
35.1 引言
- 标准库提供了很多应用广泛的工具组件,但它们很难归到某类主要组件中。
35.2 时间
在
中,标准库提供了处理时间段和时间点的组件。 我们通常希望对某事计时或做某些依赖于时间的事情。例如,标准库互斥量和锁提供了让thread等待一段时间(duration)或等待到给定时刻(time_point)的选项。
如果你希望获得当前的time_point,可以对3种时钟之一调用now(): system_clock, steady_clock和high_resolution_clock
时钟返回一个time_point, 一个duration就是相同时钟的两个time_point间的距离。
时间组件的设计目的之一是支持在系统深层中的高效使用;它们不提供社交日历便利维护这类组件。实际上,时间组件源自高能物理的迫切需求。
35.2.1 duration
- 在
中,标准库提供了类型duration来表示两个时间点(time_point)间的距离
35.2.2 time_point
- 在
中,标准库提供了类型time_point,用来表示给定纪元的一个时间点,用给定的clock度量 - 一个纪元(epoch)就是由给定clock确定的一个时间范围,用duration来衡量,从duration::zero()开始
35.2.3 时钟
- time_point和duration值归根结底是从硬件时钟获得的。
- 系统提供了3个命名的时钟
- system_clock 系统实时时钟;可以重置系统时钟(向前或向后跳)来匹配内部时钟
- steady_clock 时间稳定推移的时钟,即时间不会回退且时钟周期的间隔是常量
- high_resolution_clock 一个系统上具有最短时间增量的时钟
35.3 编译时有理数运算
中定义了类ratio,提供了编译时有理数运算。标准库用ratio提供时间段和时间点的编译时表示 - 其基本思想是将一个有理数的分子和分母编码为(值)模板实参。分母必须非零
35.4 类型函数
- 在
中,标准库提供了类型函数,用来确定类型的属性(类型萃取)以及从已有类型生成新类型(类型生成器)
35.4.1 类型萃取
- 在
中,标准库提供了多种类型函数,允许程序员确定一个类型或一对类型的属性。它们的名字大多是自解释的。主类型谓词(primary type predicate)检测类型的基本属性 - 类型萃取返回一个布尔值。为了访问此值,可使用后缀::value
35.4.2 类型生成器
- 在
中,标准库提供了从一个给定类型实参生成另一个类型的类型函数。 - 一个类型转换器返回一个类型。为了访问这个类型,可以使用后缀::type
35.5 其他工具
35.5.1 move()和forward()
- move()进行简单的右值转换。我们用move()告知编译器:此对象在上下文中不再被使用,因此其值可被移动,留下一个空对象
- forward()的典型用法是将一个实参从一个函数 完美转发 到另一个函数。
- 当希望用一个移动操作 窃取 一个对象的表达形式时,使用move();当希望转发一个对象时,使用forward()
- 因此,forward(x)总是安全的,而move(x)标记x将被销毁,因此要小心使用。调用move(x)之后x唯一安全的用法就是析构或者是赋值的目的。
35.5.2 swap()
- 在
中,标准库提供了一个通用的swap()和一个针对内置数组的特例化的版本 - swap()不能用来交换右值
35.5.3 关系运算符
- 在
中,标准库提供了任意类型的关系运算符,它们定义在子命名空间rel_ops中
35.5.4 比较和哈希type_info
- 在
中,标准库提供了比较和哈希type_index的组件。一个type_index是从一个type_info创建的,专门用于这种比较和哈希。
35.6 建议
- 用
组件,如steady_clock, duration和time_point进行计时 - 优先使用
组件而不是 组件 - 用duration_cast获得已知单位的时间段
- 用system_clock::now()获得当前时间
- 可以在编译时查询类型的属性
- 仅当obj的值不再使用时使用move(obj)
- 用forward()进行转发。
第三十六章 字符串
36.1 引言
- 在
中,标准库提供了字符分类操作,在 中提供了字符串相关操作,在 中提供了正则表达式匹配组件,在 中提供了C风格字符串支持。 - 不同字符集的处理,编码和区域习惯将在第三十九章介绍
36.2 字符分类
- 标准库提供了一些分类函数,帮助用户操纵字符串(及其他字符序列),还提供了一些指出字符类型属性的萃取,帮助实现字符串上的操作。
36.2.1 分类函数
在
中,标准库提供了从基本运行字符集中分类字符的函数 - isspace(c)
- isalpha(c)
- isdigit(c)
- isxdigit(c)
- isupper(c)
- islower(c)
- isalnum(c)
- iscntrl(c)
- ispunct(c)
- isprint(c)
- isgraph(c)
此外,标准库提供了两种去除大小写区别的有用函数
- toupper(c)
- tolower(c)
这些字符分类函数很有用,原因之一是字符分类其实比看起来要麻烦很多。
- 例如,一个初学者可能会这样编写代码: if (‘a’ < ch && ch < ‘z’>) // 一个小写字母
- 下面的写法更简洁,而且可能更高效: if (islower(ch)) // 一个小写字母
更重要的是,在编码空间中,字母并不保证是连续编码的,所以第一段代码可能会出错。
36.2.2 字符萃取
- 一个字符类型的属性由其char_traits定义。一个char_traits是以下模板的特例化版本
- template
struct char_traits {};
- template
- 所有char_traits都定义在命名空间std中,头文件
中给出了标准char_traits。通用char_traits本身是没有属性的;只有特定字符类型的特例化char_traits才有属性
36.3 字符串
- 在
中,标准库提供了通用字符串模板basic_string - 元素(字符串)是连续存储的,这样底层输入操作可以安全的将basic_string的字符序列作为源或目的。
- basic_string提供了强保证:若一个basic_string操作抛出了异常,则字符串保持不变。
- 与容器类似,basic_string的设计目的不是为了用作基类,而且它提供了移动语义,因此能高效地以传值方式由函数返回。
36.3.1 string和C风格字符串
- C风格字符串与string的根本区别是,string是具有常规语义的真正类型,而C风格字符串则是一些有用的函数支撑的一组规范。
36.3.2 构造函数
basic_string提供了各式各样令人眼花缭乱的构造函数。
最常用也是最简单的
- string s0;
- string s1 {“As simple as that”};
- string s2 {s1};
不要尝试用一个nullptr初始化一个string。最好情况下,你会得到一个糟糕的运行时错误,而最坏情况下,你会得到难以理解的未定义行为。
值string::npos表示一个超出string长度的位置,通常用来表示 string 尾
36.3.3 基本操作
- basic_string提供了比较操作,大小和容量控制操作以及访问操作
- 用at()进行越界访问会抛出std::out_of_range。若+=(), push_back()或+会令size()超过max_size(),则抛出std::length_error
36.3.4 字符串I/O
- 我们可以用 << 输出basic_string,以及用 >> 读取输入保存到basic_string中。若输入操作会令size()超过max_size(),则抛出std::length_error
- getline()会从输入流中删除结束符(默认为 ‘\n\ ),但不将其存入字符串中。这简化了对输入的逐行处理。
36.3.7 find系列函数
- 标准库提供了各种各样令人眼花缭乱的子串查找操作。照例,find()从s.begin()开始向后搜索,rfind()从s.end()开始向前搜索。find()函数用string::npos(非位置)来表示 未找到
- find_*_of()系列函数与find()和rfind()的不同指出在于它查找单个字符而非一个字符序列
36.4 建议
- 使用字符分类而非手工编写的代码检查字符范围
- 如果实现类字符串的抽象,使用character_traits实现字符上的操作
- 可用basic_string创建任意字符类型的字符串
- 将string用作变量和成员而非基类
- 优先选择string操作而非C风格字符串函数
- 以传值方式返回string(依赖移动语义)
- 将string::npos表示 string 剩余部分
- 不要将nullptr传递给接受C风格字符串的string函数
- string可以按需增长和收缩
- 当需要进行范围检查时,使用at()而非迭代器或[]
- 当需要优化速度时,使用迭代器或[]而非at()
- 如果使用string,应在程序某些地方捕获length_error和out_of_range
- 仅在必要时,用c_str()生成string的C风格字符串表示
- string输入是类型敏感的且不会溢出
- 优先选择string_stream或通用的值抽取函数而非直接使用str*系列数值转换函数
- 使用find()操作在string中定位元素(而不是自己编写循环)
- 直接或间接使用substr()读取字符串,用replace()写入子串
第三十八章 I/O流
38.1 引言
- I/O流库提供了文本和数值的输入输出功能,这种输入输出是带缓冲的,可以是格式化的,也可以是未格式化的。
- I/O流工具定义在
等头文件中 - ostream将有类型的对象转换为字符流(字节流)
- istream将字符流(字节流)转换为有类型的对象
38.2 I/O流层次
- 关键的类是basic_ios,其中定义了大多数实现和很多操作。
38.2.1 文件流
- 在
中,标准库提供了读写文件的流 - ifstream 用于从文件读取数据
- ofstream 用于向文件写入数据
- fstream 用于读写文件
38.2.2 字符串流
- 在
中,标准库提供了读写string的流 - istringstream 用于从string读取数据
- ostringstream 用于向string写入数据
- stringstream 用于读写string
- 字符串流不提供拷贝操作。如果你希望两个名字指向同一个字符串流,可使用引用或指针
38.3 错误处理
- 一个iostream在某个时刻会处于四种状态之一,这些状态的定义来自于
的basic_ios中 - good() 前一个iostream操作成功
- eof() 到达输入尾(文件尾)
- fail() 发生了出乎意料的事情(例如,读取数据却得到一个‘x’)
- bad() 发生了处于医疗的严重事情(例如,磁盘读错误)
- 如果一个流不在good()状态,对它的任何操作都没有效果,即相当于空操作。
38.7 建议
- 若用户自定义类型的值存在有意义的文本表示,可为其定义 << 和 >>
- 用cout进行正常输入,用cerr输出错误
- 标准库提供了普通字符和宽字符的iostream,你可以为任意类型的字符定义iostream
- 标准库为标准I/O流,文件和string定义了标准iostream
- 不要尝试拷贝一个文件流
- 二进制I/O依赖于系统
- 在使用文件流之前记得检查它是否已关联到文件
- 优先选择ifstream和ofstream,而非通用的fstream
- 用stringfstream进行内存中的格式化
- 用异常机制捕获稀少的bad() I/O错误
- 用流状态fail处理潜在的可恢复的I/O错误
- 为了定义新的 << 和 >> 你无需修改istream或ostream
- 当实现iostream原语操作时,使用sentry
- 优先选择格式化输入而非未格式化的,底层的输入
- 读取输入存入string不会导致溢出
- 当使用get(), getline() 和 read() 时,要小心结束标准
默认忽略空白符
- 优先选择操纵夫而非状态标志来控制I/O
- 如果你希望混合C风格I/O和iostream-I/O,使用sync_with_stdio(true)
- 使用sync_with_stdio(false)优化iostream
- 连接流来实现交互式I/O
- 用imbue()来令iostream反应locale的文化差异
- width()说明只应用于紧接着的下一个I/O操作
- precision()说明应用于后续所有浮点输出操作
- 浮点格式说明(例如scientific)应用于后续所有浮点输出操作
- 为使用接受参数的标准操纵符,要 #include
- 你几乎不会需要flush()
- 除非可能有审美趣味上的原因,否则不要使用 endl
- 如果iostream格式化变得令人厌烦,编写你自己的操纵符
- 通过定义一个简单的函数对象,你可以实现三元运算符的效果(和效率)
第四十章 数值计算
40.1 引言
- 当复杂数据结构是计算过程中的必要部分时,C++的优势就变得很重要了。C++因而被广泛用于科学计算,工程计算,金融计算以及其他包含复杂数值计算的任务,这催生了这类计算的语言特性和技术。
- 本章介绍标准库中支持数值计算的部分。我不打算讲授数值方法。数值计算本身就是一个吸引人的话题。学习数值计算需要一门好的数值方法课程或者至少有一本好的教材—而不是仅靠一本编程语言手册和导引就能完成的。
40.2 数值限制
- 为了处理数值相关的有趣事情,我们通常需要了解一些内置数值类型的一般特性的知识。为了让程序员能最充分的利用硬件,这些特性都是依赖于具体C++实现,而不是由语言本身规定的。
- 例如
- 最大的int有多大?
- 最小的正float是什么?
- 将一个double赋予一个float时,是舍入还是截断?
- 一个char有多少位?
- 这类问题的答案由 numeric_limits 的特例化版本提供,它定义在
中 - 每个特例化版本提供其实参类型的相关信息。因此,通用numberic_limits模板就是一组常量和constexpr函数的标志句柄。真正的信息保存在特例化版本中。
40.2.1 数值标量限制C风格宏
- C++ 从C继承了描述整数属性的宏,它们定义在
中
40.3 标准数学函数
- 在
中我们可以找到通常被称为标准数学函数(Standard Mathematical Function)的组件
40.7 随机数发生器
在
中,标准库定义了生成(伪)随机数的特性。这种随机数是按照数学公式生成的值的序列,而不是无法猜测(真随机)的数,后者可以从物理过程中获得,例如放射性衰变或太阳辐射。 标准库提供了四种随机数相关的实体
- 均匀随机数发生器(uniform random number generator)是一个返回无符号整数值的函数对象,值域中每个可能值(理想情况下)被返回的概率相等
- 随机数引擎(random number engine, 简称引擎)是一个均匀随机数发生器,可用默认状态创建,或者用一个seed确定的状态创建
- 随机数引擎适配器(random number engine adaptor, 简称适配器)是一个随机数引擎,它接受某个其他随机数引擎生成的值,并应用算法将这些值转换为另一个具有不同随机特性的值的序列。
- 随机数分布(random number distribution, 简称分布)是一个函数对象,其返回值的分布服从一个关联的数学概率密度函数或一个关联的离散概率函数
符合用户习惯的更简单的描述是,一个随机数发生器就是一个引擎加一个分布。引擎生成一个均匀分布的值的序列,分布再将这些值转换为要求的形状(分布)。即,如果你从随机数发生器接受了大量数值并绘制它们,就会得到一个描述它们分布的相当平滑的图形。
大多数请款下,大多数程序员只需要一个给定范围内假肚腩的均匀分布整数序列或浮点数序列。
40.7.4 C风格随机数
在
和<stdlib.h>中,标准库提供了一些简单的特性来生成随机数 1
2
3
4
int rand(); // 0和RAND_MAX间的伪随机数
void srand(unsigned int i); // 将随机数发生器的种子设置为i调用srand(s)用种子(seed)s(作为参数提供)爱是一个新的随机数序列。为了方便调试,一个给定种子生成固定的序列通常很重要。但是,我们通常希望用一个新种子开始程序的每次运行。
第四十一章 并发
41.1 引言
并发,即多个任务同时执行,广泛用于提高吞吐率(使用多个处理器完成单个运算)或提高响应能力(当程序的一部分等待响应时允许另一部分继续执行)
如果一项活动可能与其他活动并发执行,我们就称之为任务(task)。线程(thread)是执行任务的计算机特性在系统层面的表示。一个标准库thread可执行一个任务。一个线程可与其他线程共享地址空间。即,在单一地址空间中的所有线程能访问相同的内存位置。而并发系统程序员所面临的重要挑战之一就是,确保多线程并发访问内存的方式是合理的。
标准库对并发的支持包括
- 内存模型(memory model): 这是对内存并发访问的一组保证,主要是确保简单的普通访问能按人们的朴素的预期工作
- 对无锁编程(programming without locks)的支持: 这是一些避免数据竞争的细粒度底层机制
- 一个线程(thread)库: 这是一组支持传统线程–锁风格的系统级并发编程的组件,例如thread, condition_variable, mutex
- 一个任务(task)支持库: 这是一些支持任务级并发编程的特性: future, promise, packaged_task, async()
这些主题是按照从最基础,最底层到最高层的顺序排列的。内存模型是所有编程风格所共用的。为提高程序员开发效率,尽量减少错误,应在尽可能高的层次上编程。例如,应该优先选择future而不是mutex实现信息交换;除非是简单的计数器,否则应该优选mutex而不是atomic;诸如此类,尽量将复杂任务留给标准库实现者。
在C++标准库的语境中,一个锁(lock)就是一个mutex(互斥量)以及任何构建于mutex之上的抽象,用来提供对资源的互斥访问或同步多个并发任务的进度。
进程(process)即运行于独立地址空间,通过进程间通信机制进行交互的线程,并不在本书介绍范围之内。
类似的,我们可以以函数对象(例如lambda)的形式定义任务并将它们传递给线程,而无需进行类型转换或担心类型违规。
41.2 内存模型
- C++实现大多标准库组件的形式提供对并发机制的支持。这些组件依赖于一组称为内存模型(memory model)的语言保证。内存模型是计算机设计师和编译器实现者之间关于计算机硬件最佳表示方式的讨论结果。
- 为了理解所涉及的问题,请记住一个简单事实:对内存中对象的操作永远不直接处理内存中的对象,而是将对象加载到处理器的寄存器中,在哪里修改,然后再写回内存。更糟糕的是,对象通常首先从主存加载到缓存中,然后再加载到寄存器。
41.2.1 内存位置
- C++内存模型保证两个更新和访问不同内存位置的线程可以互不影响的执行。这恰是我们的朴素期望。防止我们遇到现代硬件有时很奇怪和微妙的行为是编译器的任务。编译器和硬件如何写作来实现这一目的应该由编译器负责。我们编程所用的机器实际上是由硬件和非常底层的(由编译器生成的)软件组合提供的。
41.2.2 指令重排
- 为提高性能,编译器,优化器以及硬件都可能重排指令顺序。
41.2.3 内存序
- 术语内存序(memory ordering)用来描述一个线程从内存访问一个值时会看到什么。最简单的内存序称为顺序一致性(sequentially consistent)。再一个顺序一致性内存模型中,每个线程看到的是相同的操作执行效果,此顺序就像是所有指令都在单一线程中顺序执行一样。
- 线程仍可重排指令,但对其他线程可以观察变量的每个时间点,时间点前执行的指令集合(因而)和观察家到的内存位置的值必须是明确定义的且对所有线程都一致。
- 观察值从而强制内存位置的一个一致性视图的操作被称为原子操作(atomic operation)
41.3 原子性
- 所谓无锁编程,就是一组来编写不显示使用锁的并发程序的技术。程序员转而依靠原语操作(由硬件直接支持)来避免小对象(通常是单一字或双字)的数据竞争。不必忍受数据竞争的原语操作通常被称为原子操作(atomic operation),可用来实现高层并发机制,例如锁,线程和无锁数据结构。
- 除了简单的原子计数器这一明显例外,无锁编程通常很复杂,最好留给专家使用
41.3.1 atomic类型
- 原子类型(atomic type)是atomic模板的特例化版本。原子类型的对象上的操作是原子的(atomic)。即,操作由单一线程执行,不会受到其它线程干扰
41.3.2 标志和栅栏
- 除了支持原子类型之外,标准库还提供了两种更底层的同步特性: 原子标志和栅栏。它们的主要用途是实现最底层的原子特性,例如自旋锁和原子类型。这两个特性是仅有的每个C++实现都保证支持的无锁机制。
- 基本上没有程序员需要使用标志或栅栏。其使用者通常是和硬件设计师紧密合作的人。
41.4 volatile
- 说明符volatile用来指出一个对象可被线程控制范围之外的东西修改
- volatile说明符主要是告知编译器不要优化掉明显冗余的读写操作
- 除非是直接处理硬件的底层代码中,否则不要使用volatile
- 不要假定volatile在内存模型中有特殊含义,它确使没有。与某些新语言不同,在C++中volatile并非一种同步机制。为了进行同步,应该使用atomic,mutex或condition_variable
41.5
- 用并发提高响应能力或吞吐率
- 只要代价可接受,应在尽可能高的抽象层次上编程
- 优先选择packaged_task和future,而不是直接使用thread和mutex
- 除非是实现简单计数器,否则优先选择mutex和condition_variable,而不是直接使用atomic
- 尽量避免显式共享数据
- 将进程视为线程的代替
- 标准库并发特性是类型安全的
- 内存模型是为了省去程序员从机器体系结构思考计算机的麻烦
- 内存模型令内存行为大致如我们的朴素预取
- 不同线程访问一个struct的不同位域可能互相干扰
- 避免数据竞争
- 原子类型和操作可实现无锁编程
- 无锁编程对避免死锁和确保每个线程持续前进是很重要的
- 将无锁编程留给专家
- 将放松内存模型留给专家
- volatile告知编译器一个对象的值可以被程序之外的东西改变
- C++的volatile不是一种同步机制
第四十二章 线程和任务
42.1 引言
42.2 线程
thread是计算的概念在计算机硬件层面的抽象。C++标准库thread的设计目标是与操作系统线程形成一对一映射。
所有thread工作于同一个地址空间中。如果你希望硬件能防止数据竞争,则应该使用进程。thread间不共享栈,因此局部变量不会产生数据竞争问题,除非你不小心将一个局部变量的指针传递给其他thread。
如果一个thread不能继续前进(例如遇到了一个其他thread所拥有的mutex),我们称它处于阻塞(blocked)或睡眠(asleep)状态
一个thread表示一个系统资源,一个系统线程,甚至可能有专用硬件,因此thread可以移动但是不能拷贝。
42.2.1 身份
- 每个执行线程都有唯一标识符,用thread::id类型的值表示。如果一个thread不表示一个执行线程,则其id为默认的id{}。一个thread的id可以通过调用get_id()获得。
42.2.2 构造
thread的构造函数接受一个要执行的任务,以及该任务要求的参数。参数的数量和类型必须与任务所要求的参数列表匹配。
thread构造完毕之后,一旦运行时系统能获取它运行所需的资源,它就开始执行任务。你可以认为这个过程是 立即的。并不存在单独的启动thread操作。
如果你希望构建一组任务,将它们链接在一起,你应该将任务构造为函数对象,然后,在他们就绪之后启动thread
将任务从一个thread移动到另一个thread并不影响其执行,thread的移动只是改变thread指向的是什么。
42.2.3 析构
- 显然,thread的析构函数销毁thread对象。为了防止发生系统线程的生命期长于其thread的意外情况,thread析构函数调用terminate()结束程序(若thread是joinable()的,即get_id() != id())
42.2.4 join()
- t.join() 告诉当前thread在t结束之前不要继续前进
42.2.5 detach()
注意,thread提供了移动赋值操作和移动构造函数。这令thread可以迁移出它创建时所在的作用域,从而常常可作为detach()的替代方案。我们可以将thread迁移到程序的主模块,通过unique_ptr或shared_ptr访问它们,或者将它们放置于一个容器中(例如vector
),免得失去与它们的联系。 如果你必须detach()一个thread,请确保它没有引用其作用域中的变量
我们必须使用detach()才能让一个thread离开其作用域;除非有非常好的理由,否则不要这么做,即使需要使用detach(),也应首先仔细思考thread的任务可能做什么,然后再使用。
42.2.6 名字空间this_thread
- 对当前thread的操作定义再名字空间this_thread中
- x = get_id() x为当前thread的id;不抛出异常
- yield() 给调度器机会运行另一个thread;不抛出异常
- sleep_until(tp) 令当前thread进行睡眠状态,直到time_point tp
- sleep_for(d) 令当前thread进行睡眠状态,持续duration d
- 在所有主要C++实现中thread都是可抢占的;即,C++实现可以从一个任务切换到另一个任务,以确保所有thread都以一个合理的速度前进。
42.2.7 杀死thread
- thread漏掉了一个重要操作,没有一种简单的标准方法告知一个正在运行的thread对其任务已经失去了兴趣,因此请它停止运行并释放所有资源。此操作(在不同语言和系统中被称为杀死,取消和终止)的缺席有各种历史原因和技术原因。
- 如需要,应用程序员可以编写自己的杀线程操作。例如,很多任务包含一个请求循环。在此情况下,发送一条请自杀消息给一个thread即可令其释放所有资源并结束。如果没有请求循环,线程可以周期性的检查一个需要变量来判断用户是否还需要本线程的结果。
42.2.8 thread_local 数据
如其名,一个thread_local变量是一个thread专有的对象,其他thread不能访问,除非其拥有者将指向它的指针提供给了其他线程。
我们说一个thread_local具有线程存储存续时间(thread storage duration)。每个thread对thread_local变量都有自己的拷贝。thread_local在首次使用前初始化。如果已构造,会在thread退出时销毁。
thread_local存储的一个重要用途是供thread显示缓存互斥访问数据。
一般而言,非局部内存是并发编程的一个难题,因为确定数据是否共享通常不那么简单,因而可能成为数据竞争之源
名字空间变量,局部static和类static成员都可以声明为thread_local。
42.3 避免数据竞争
- 避免数据竞争的最好方法是不共享数据。将感兴趣的数据保存在局部变量中,保存在不与其他线程共享的自由存储中,或是保持在thread_local内存中。不要将这类数据的指针传递给其他thread。当另一个thread需要处理这类数据时(例如并行排序),传递数据特定片段的指针并确保在任务结束之前不触碰此数据片段。
- 这些简单规则背后的思想是避免并发数据访问,因此程序不需要锁机制且能达到最高效率。在不能应用这些规则的场合,例如有大量数据需要共享的场合,可使用某种形式的锁机制
- 互斥量(mutex):互斥量就是一个用来表示某个资源互斥访问权限的对象。为访问资源,先获取互斥量,然后访问数据,最后释放互斥量
- 条件变量(condition variable): 一个thread用条件变量等待另一个thread或计时器生成的事件。
- 严格来说,条件变量不能防止数据竞争,而是帮我们避免引入可能引起数据竞争的共享数据。
42.3.1 互斥量
- mutex对象用来表示资源的互斥访问。因此,它可用来防止数据竞争以及同步多个thread对共享数据的访问。
42.3.2 多重锁
- 为执行某个任务获取多个资源的需求非常常见。不幸的是,获取两个锁就可能产生死锁。
42.3.3 call_once()
- 我们通常希望初始化对象时不会产生数据竞争。谓词,类型once_flag和函数call_once()提供了一种高效且简单的底层工具。
- 可以将call_once()理解为这样一种方法,它简单的修改并发前代码,这些代码依赖于已初始化的static数据。
42.3.4 条件变量
- 我们用条件变量管理thread间的通信。一个thread可等待(阻塞)在一个condition_variable上,直至发生某个事件,例如到达一个特定时刻或者另一个thread完成。
42.4 基于任务的并发
- 本节介绍如何指定一种简单的任务:一种根据给定参数完成一项工作,生成一个结果的任务
42.4.1 future和promise
- 任务间的通信由一对future和promise处理。任务将其结果放入一个promise,需要此结果的任务则从对应的future提取结果。
42.4.2 promise
- 一个promise就是一个共享状态的句柄。它是一个任务可用来存放其结果的地方,供其他任务通过future提取。
42.4.3 packaged_task
- packaged_task保存了一个任务和一个future/promise对
42.4.4 future
- future就是共享状态的句柄,它是任务提取由promise存放的结果的地方
42.4.5 shared_future
- future的结果值只能被读一次,因为读取时它就被移动了。因此,如果你希望反复读取结果值,或是可能有多个读者读取结果,就必须拷贝它,然后读取副本。这正是shared_future所做的。每个可用的shared_future都是通过直接或间接的从具有相同结果类型的future中移出值来进行初始化的。
42.5 建议
- thread是系统线程的类型安全的接口
- 不要销毁正在运行的thread
- 用join()等待thread结束
- 除非不得已,否则不要detach()一个thread
- 用lock_guard或unique_lock管理互斥量
- 用lock()获取多重锁
- 用condition_variable管理thread间通信
- 从并发执行任务的角度思考,而非直接从thread角度思考
- 重视间接性
- 用promise返回结果,从future获取结果
- 不要对一个promise两次执行set_value(), set_exception()
- 用packaged_task管理任务抛出的异常以及安排返回值
- 用packaged_task和future表达对外部服务的请求以及等待其应用
- 不要从一个future两次使用get()
- 用async()启动简单任务
- 选择好的并发粒度很困难:依赖实验和测量做出选择
- 尽量将并发隐藏在并行算法接口之后
- 并行算法在语义上可能与解决统一问题的穿行解决方案不同
- 有时,穿行解决方案比并行版本简单且快速。
第四十三章 C标准库
43.1 引言
- C标准库经过很小改动后已被纳入C++标准库中。
43.2 文件
- C I/O系统是基于文件的。一个文件(FILE *)可以指向一个外存文件或一个标准输入输出流: stdin, stdout, stderr
43.3 printf()系列函数
- 最流行的C标准库函数是输出函数。但是,我倾向于使用iostream库,因为它是类型安全且可扩展的。
43.4 C风格字符串
- 一个C风格字符串就是一个零结尾的char数组。定义于
和 中的一组函数提供了对这种字符串表示方法的支持。
43.5 内存
- 操纵内存的函数通过void*指针(const void *用于只读内存)对裸内存(类型未知)进行操作。
- 注意,malloc()等函数并不调用构造函数,free()也不会调用析构函数。不要对具有构造函数和析构函数的类型使用这些函数。而且memset()也不应该用于具有构造函数的任何类型。
43.6 日期和时间
- 在
中,可以找到一些日期和时间相关的类型和函数
43.8 建议
- 如果担心资源泄露,使用fstream而不是fopen()/fclose()
- 处于类型安全和扩展性的考虑,优先选择
而不是 - 据对不要使用gets()或者scanf(“%s”, s)
- 处于资源管理易用性和简单性考虑,使用
而不是 - 只对裸内存使用C内存管理例程,如memcpy()
- 优先选择vector而不是malloc()和realloc()
- C标准库不了解构造函数和析构函数
- 优先选择
而不是 进行计时 - 考虑到灵活性,易用性和性能,优先选择sort()而不是qsort()
- 不要使用exit(),应该选择抛出异常
- 不要使用longjmp(),应该选择抛出异常
第四十四章 兼容性
44.1 引言
- 本章的目的是
- 给出C++11新特性的简明列表
- 介绍会给程序员的带来难题的差异
- 指出解决问题的方法
44.2 C++11扩展
44.2.1 语言特性
- 研究语言特性列表着实让人眼花缭乱。但要记住,语言特性并不是孤立使用的。特别是,大多数C++11新特性如果脱离了其他特性构成的框架就毫无意义。
- 下面特性列表的顺序大致就是在本书中第一次出现的顺序
- 使用{}列表进行一致且通用的初始化
- 从初始化器进行类型推断:auto
- 避免窄化转换
- 推广的且有保证的常量表达式:constexpr
- 范围for语句
- 空指针关键字: nullptr
- 限域且强类型的枚举:enum class
- 编译时断言:static_assert
- {}列表到std::initializer_list的语言映射
- 右值引用,允许移动语义
- 以>>结束的嵌套模板参数
- lambda
- 可变参数模板
- 类型和模板别名
- Unicode字符
- long long整数类型
- 对齐控制: alignas, alignof
- 在声明中将表达式的类型用作类型的能力:decltype
- 裸字符串字面值常量
- 推广的POD
- 推广的union
- 局部类作为模板实参
- 尾置语法和两种标准属性: [[carries_dependency]]和[[noreturn]]
- 阻止异常传播: noexcept说明符
- 检测表达式抛出异常的可能性:noexcept运算符
- inline名字空间
- 委托构造函数
- 类内成员初始化器
- 默认控制:defult和delete
- 显式转换运算符
- 用户自定义字面值常量
- template实例化更为显式的控制:extern template
- 函数模板的默认模板实参
- 继承构造函数
- 覆盖控制:override和final
- 更简单,更通用的SFINAE规则
- 内存模型
- 线程局部存储:thread_local
44.2.2 标准库组件
44.4 建议
- 在使用新特性编写产品级代码前,应先尝试编写小规模程序来测试它是否符合标准以及你所使用的C++实现是否满足性能要求
- 学习C++时应使用你能获得的最新的,最完整的标准C++实现
- C和C++的公共子集不是学习C++的最佳起点
- 优先选择标准特性而不是非标准特性
- 避免使用throw说明这样的启用特性
- 避免使用C风格类型转换
- 隐式int已被弃用,因此应显式说明每个函数,变量,const等的类型
- 在将C程序转换为C++程序时,首先确保一致使用函数声明(原型)和标准头文件
- 在将C程序转换为C++程序时,需将与C++关键字同名的变量改名
- 处于可以执行和类型安全的考虑,如果必须使用C,应该用C和C++的公共子集编写代码
- 在将C程序转换为C++程序时,应将malloc()的返回结果转换为正确类型或改用new
- 当从malloc()和free()转换为new 和delete时,考虑使用vector,push_back()和reserve()而不是realloc()
- 在将C程序转换为C++程序时,记住C++中没有从int到枚举类型的隐式类型转换,如果需要,应该使用显式类型转换
- 名字空间std中定义的特性都是定义于一个文件名无后缀的头文件中
- 包含
以便使用std::string - 每个标准C头文件<X.h>都将名字置于全局名字空间中,对应的C++头文件
将名字置于名字空间std中 - 声明C函数时使用extern “C”