C---标准库快速参考-全-
C++ 标准库快速参考(全)
一、数字和数学
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1876-1_1) contains supplementary material, which is available to authorized users.
常见数学函数<cmath>
<cmath>
头定义了std
名称空间中常见数学函数的广泛集合。除非另有说明,否则所有函数都被重载以接受所有标准数值类型,并使用以下规则来确定返回类型:
- 如果所有参数都是
float
,那么返回类型也是float
。类似于double
和long double
输入。 - 如果传递的是混合类型或整数,这些数字将被转换为
double
,并且还会返回一个double
。如果其中一个输入是long double
,则使用long double
。
基本功能
功能 | 描述 |
---|---|
abs(x) fabs(x) |
返回x. <cstdlib> 的绝对值,为int 类型定义abs() 、labs() 和llabs() ;那个abs() 有返回类型int ,不同于<cmath> ( double )中的abs() 。 |
fmod(x, y) remainder(x, y) |
返回![]() fmod() ,结果总是和x 有相同的符号;对remainder() 来说,那不一定是真的。例如:mod(1,4) = rem(1,4) = 1 ,但是mod(3,4) = 3 和rem(3,4) = -1 。 |
remquo(x, y, *q) |
返回相同的值,因为remainder(). q 是一个指向int 的指针,并接收一个带有符号![]() |
fma(x, y, z) |
以准确(比简单实现更好的精度和舍入属性)和高效(如果可能,使用单个硬件指令)的方式计算![]() |
fmin(x, y) fmax(x, y) |
返回x 和y 的最小值或最大值。 |
fdim(x, y) |
返回正差值,即![]() |
nan(string)``nanf(string) |
返回一个类型分别为double 、float 、long double 的安静(非信令)NaN(非数字),如果可用的话(否则为0 )。string 参数是一个依赖于实现的标签,可以用来区分不同的 NaN 值。"" 和nullptr 都有效,并产生一个通用的安静 NaN。 |
指数和对数函数
功能 | 公式 | 功能 | 公式 | 功能 | 公式 |
---|---|---|---|---|---|
exp(x) |
e x | exp2(x) |
2 x | expm1(x) |
![]() |
log(x) |
![]() |
log10(x) |
日志 10 x | log2(x) |
日志 2 x |
log1p(x) |
![]() |
幂函数
功能 | 公式 | 功能 | 公式 |
---|---|---|---|
pow(x, y) |
x y | sqrt(x) |
![]() |
hypot(x, y) |
![]() |
cbrt(x) |
![]() |
三角函数和双曲线函数
提供所有基本三角函数(sin()
、cos()
、tan()
、asin()
、acos()
、atan()
)和双曲线函数(sinh()
、cosh()
、tanh()
、asinh()
、acosh()
、atanh()
)。还提供了不太为人所知的三角函数atan2()
。它用于计算矢量(x
、y
)与 X 轴正方向之间的角度,其中atan2(y,x)
与atan(y/x)
相似,只是其结果正确反映了矢量所在的象限(如果x
为0
,它也适用)。实质上,通过将y
除以atan(y/x)
中的x
,您会丢失关于x
和y
符号的信息。
误差和伽玛函数
功能 | 公式 | 功能 | 公式 |
---|---|---|---|
erf(x) |
![]() |
tgamma(x) |
![]() |
erfc(x) |
![]() |
lgamma(x) |
ln(|C(x)|) |
浮点数的整数舍入
功能 | 描述 |
---|---|
ceil(x) floor(x) |
向上/向下舍入到整数。即:返回最接近的不小于/不大于x 的整数。 |
trunc(x) |
返回绝对值不大于x 的最近整数。 |
round(x)``lround(x) |
返回最接近x 的整数值,从零开始四舍五入。round() 的返回类型照常基于x 的类型,而lround() 返回long ,llround() 返回long long 。 |
nearbyint(x) |
以浮点类型返回最接近x 的整数值。使用当前取整模式:参见本章后面算术类型属性一节中的round_style 。 |
rint(x)``lrint(x) |
使用当前舍入模式,返回最接近x 的整数值。rint() 的返回类型照常基于x 的类型,而lrint() 返回long ,llrint() 返回long long 。 |
浮点操作函数
功能 | 描述 |
---|---|
modf(x, *p) |
将x 的值分成整数和小数部分。后者被返回,前者存储在p 中,两者的符号与x 相同。返回类型通常基于x 的类型,并且p 必须指向与该返回类型相同类型的值。 |
frexp(x, *exp) |
用![]() x 的值分解成绝对值在[0.5,1]范围内或等于零(返回值)的归一化分数,以及 2 的整数次幂(存储在exp 中)。 |
logb(x) |
返回x 的浮点指数,即:log radix |x|,以 radix 为基数用于表示所有标准数值类型的浮点值(2 ;因此得名:二进制对数)。 |
ilogb(x) |
与logb(x) 相同,但结果被截断为带符号的int 。 |
ldexp(x, n) |
返回 x * 2 n (带n 和int )。 |
scalbn(x, n) scalbln(x, n) |
返回 x *基数 n (其中n 表示scalbn() ,而long 表示scalbln() )。基数是用于表示浮点值的基数(2 用于所有标准 C++ 数值类型)。 |
nextafter(x, y) nexttoward(x, y) |
沿y 方向返回x 之后的下一个可表示值。如果x 等于y ,则返回y 。对于nexttoward() ,y 的类型永远是long double 。 |
copysign(x, y) |
返回绝对值为x 且符号为y 的值。 |
分类和比较功能
功能 | 描述 |
---|---|
fpclassify(x) |
对浮点值x 进行分类:返回等于FP_INFINITE 、FP_NAN 、FP_NORMAL 、FP_SUBNORMAL 、FP_ZERO 的int ,或者特定于实现的类别。 |
isfinite(x) |
如果x 是有限的,即正常的、次正常的(反正常化的)或零,但不是无限的或 NaN,则返回true 。 |
isinf(x) |
如果x 是正无穷大或负无穷大,则返回true 。 |
isnan(x) |
如果x 是 NaN,则返回true 。 |
isnormal(x) |
如果x 正常,即既不为零、次正常(反正常)、无穷大,也不为 NaN,则返回true 。 |
signbit(x) |
如果x 为负,则返回非零值。 |
isgreater(x, y)``isgreaterequal(x, y)``isless(x, y)``islessequal(x, y) |
比较x 和y 。除了如果 x < y || x > y 则返回true 的islessgreater() 之外,这些名称都是不言自明的。注意,这与!= 不同,例如nan("") != nan("") 是true ,而不是islessgreater(nan(""), nan("")) 。 |
isunordered(x, y) |
返回x 和y 是否无序,即是否一个或两个都是 NaN。 |
错误处理
来自<cmath>
的数学函数可以根据math_errhandling
的值以两种方式报告错误(在<cmath>
中定义,尽管不在std
名称空间中)。它是整数类型,可以是下列值之一或它们的按位“或”组合:
MATH_ERRNO
:使用全局errno
变量(参见第八章)。MATH_ERREXCEPT
:使用浮点环境<cfenv>
,本书不再进一步讨论。
固定宽度的整数类型<cstdint>
<cstdint>
头包含与平台相关的typedef
用于整数类型,与基本整数类型相比,它们具有不同且更易移植的宽度要求:
std::(u)int
X_t
,正好为 X 位的(无符号)整数(X = 8、16、32 或 64)。仅在目标平台支持时出现。std::(u)int_least
X_t
,至少 X 位的最小(无符号)整数类型(X = 8、16、32 或 64)。std::(u)int_fast
X_t
,至少 X 位(X = 8、16、32 或 64)的最快(无符号)整数类型。std::(u)intmax_t
,支持的最大(无符号)整数类型。std::(u)intptr_t
,(无符号)整数类型,大到足以容纳一个指针。这些类型定义是可选的。
头部进一步定义了这些(和一些其他)类型的最小值和最大值的宏:例如,INT_FAST_8_MIN
和INT_FAST_8_MAX
代表std::int_fast8_t
。不过,获得这些值的标准 C++ 方法是使用下面讨论的<limits>
工具。
算术类型属性<limits>
std::numeric_limits<T>
模板类提供了大量的静态函数和常量来获取数字类型T
的属性。它专门用于所有基本的数字类型,包括整型和浮点型,因此也可以用来检查它们所有别名的属性,比如size_t
或者前面的那些。下面列出了提供的各种成员。函数仅仅并且总是用于获得一个T
值;而布尔、int
s 和enum
值被定义为常量。
成员 | 描述 |
---|---|
is_specialized |
指示模板是否专用于给定类型。如果false ,所有其他成员使用零初始化值。 |
min() ,max() |
返回最小/最大有限可表示数。出乎意料的是,对于浮点数,min() 返回可以表示的最小正数(参见lowest() )。 |
lowest() |
返回最小的有限可表示数。与min() 相同,除了浮点类型,其中lowest() 返回最小的负数,对于float 和double 等于-max() 。 |
radix |
用于表示值的基数(2 适用于所有 C++ 数值类型,但特定平台可以支持本机十进制类型等)。 |
digits |
可表示的基数radix 的位数(即通常的位数),不包括整数类型的任何符号位。对于浮点类型,尾数中的位数。 |
digits10 |
该类型可以无损表示的有效十进制数字的数目,例如在文本和文本之间进行转换时。等于⌊digits * log 10 (radix)⌋对于整数:对于char ,例如等于2 ,因为它不能用三位小数表示所有的值。对于浮点数,它等于![]() |
is_signed |
标识有符号类型。所有标准的浮点类型都是有符号的,布尔值不是,对于char 和wchar_t 来说,它是未指定的。 |
is_integer |
标识整数类型(包括布尔值和字符类型)。 |
is_exact |
用精确的表示形式标识类型。与所有标准类型的is_integer 相同,但存在精确但非整数的第三方有理数表示。 |
is_bounded |
用有限表示标识类型。对于所有标准类型,但是存在提供任意精度类型的库。 |
is_modulo |
标识模类型,这意味着如果+ 、- 或* 运算的结果落在范围[ min() 、max() ]之外,则结果值与实际值相差max() - min() + 1 的整数倍。通常true 为整数;false 为浮点型。 |
traps |
标识至少有一个值在用作算术运算的操作数时会导致陷阱(异常)的类型。例如,除以 0 总是会导致陷阱。通常true 用于所有标准整数类型,除了bool 。通常false 用于所有浮点类型。 |
下列成员仅与浮点类型相关。对于整数类型,它们总是等于或返回零:
Member |
描述 |
---|---|
max_digits10 |
无损表示该类型的任何值所需的十进制位数,例如,在转换成文本或转换回来时。将浮点数转换为文本时使用(至少)max_digits10 精度,解析回来后会再次给出完全相同的值(9 表示float ,17 表示double ,22 表示long double )。 |
min_exponent10 、min_exponent 、max_exponent10 、max_exponent |
最小负值(对于min_* )或最大正值(对于max_* )整数 n,使得 10 n (对于*10 )或![]() |
epsilon() |
1.0 和下一个可表示值之间的差值。 |
round_error() |
ISO/IEC 10967-1 中定义的最大舍入误差。 |
is_iec599 |
识别符合所有 IEC 599/IEEE 754 要求的类型。通常true 用于所有标准浮点类型。 |
has_infinity |
标识可以表示正无穷大的类型。通常true 用于所有标准浮点类型。 |
infinity() |
返回正无穷大的值。只有当has_infinity 为true 时才有意义。 |
has_quiet_NaN ,has_signaling_NaN |
标识可以表示安静或信号 NaN 的特殊值的类型。通常true 用于所有标准浮点类型。在操作中使用信号 NaN 会导致异常;使用安静的 NaN 不会。 |
quiet_NaN() ,signaling_NaN() |
返回安静或发信号 NaN 的值。只有当has_quiet_NaN 分别为has_signaling_NaN 和true 时才有意义。 |
tinyness_before |
标识在执行任何舍入之前执行下溢检查的类型。 |
round_style |
包含作为std::float_round_style 值的舍入样式:round_indeterminate 、round_toward_zero 、round_to_nearest 、round_toward_infinity 或round_toward_neg_infinity 。所有整数类型都需要向零舍入。标准的浮点类型通常舍入到最接近的值。 |
has_ denorm |
标识可以表示非规范化值的类型(小于min() 的特殊值,用于处理下溢)。具有类型std::float_denorm_style ,值为denorm_absent 、denorm_present (最常见)和denorm_indeterminate 。 |
denorm_min() |
如果has_denorm != std::denorm_absent ,否则为min() 。 |
has_denorm_loss |
确定精度损失被检测为反规格化损失而不是不精确结果的类型(高级选项应为false ;在 IEEE 754-2008 中被删除)。 |
复数<complex>
为至少等于float
、double
和long double
的T
定义的std::complex<T>
类型用于表示复数,如下所示:
所有期望的操作符都是可用的:+
、-
、*
、/
、+=
、-=
、*=
、/=
、=
、==
和!=
,包括具有浮点操作数的重载(该操作数随后被视为虚部为零的complex
数),以及用于与第五章的流进行交互的>>
和<<
操作符。
std::literals::complex_literals
名称空间定义了方便的文字操作符,用于创建complex<T>
数字:i
、if
和il
,分别创建T
等于double
、float
和long double
的值。使用这个,例如,在前面的例子中的c
值可能已经用:'auto c = 1.f + 2if;
'创建。
标题还定义了前面提到的几个基本数学函数的等价函数complex
:pow()
、sqrt()
、exp()
、log()
和log10()
,以及所有的三角函数和双曲线函数:sin()
、cos()
、tan()
、asin()
、acos()
、atan()
、sinh()
、cosh()
、tanh()
、asinh()
、acosh()
和atanh()
。
除此之外,还存在以下complex
特定的非成员函数:
Function |
描述 | 定义 |
---|---|---|
real() / imag() |
非成员 getters | 真实/图像 |
abs() |
模数或数量 | ![]() |
norm() |
标准 | ![]() |
arg() |
阶段或论点 | atan2(imag,实数) |
conj() |
共轭物 | ![]() |
polar() |
从极坐标(m,φ) (=幅度和相位)构建 | ![]() |
proj() |
黎曼球面上的投影 | ![]() |
编译时有理数<ratio>
来自<ratio>
头的std::ratio<Numerator, Denominator=1>
模板类型代表一个有理数。它的奇特之处在于它是在类型层次而不是通常的值层次上实现的(std::complex
数字就是后者的一个例子)。虽然ratio
值可以默认构造,但这很少是我们的意图。相反,ratio
类型通常被用作其他模板的类型参数。例如,在章节 2 中解释的std::chrono::duration<T, Period=std::ratio<1>>
模板可以被实例化为duration<int,ratio<1,1000>>
,例如,表示毫秒的持续时间,或者表示分钟的持续时间duration<int,ratio<60>>
。
所有标准 SI ratio
都有方便性typedef
s:例如std::kilo
定义为ratio<1000>
,std::centi
定义为ratio<1,100>
。完整的列表是atto
()
femto
()
pico
()
nano
()
micro
()
milli
()
centi
()
deci
()
deca
(101)hecto
(102),】而对于整型宽于 64 位的平台,yocto
()
zepto
()
zetta
(1021)yotta
(1024)。
所有的ratio
类型都定义了两个静态成员num
和den
,包含有理数的分子和分母,但是经过了规范化。ratio
的type
成员等于这个规格化有理数的ratio
类型。
使用ratio
进行算术运算是可能的,但是它们仍然是在类型级别:例如,std::ratio_add
模板接受两个ratio
类型作为模板参数,并计算出对应于这些有理数之和的类型。ratio_subtract
、ratio_multiply
和ratio_divide
模板是类似的。为了比较两种ratio
类型,类似的ratio_xxx
模板有xxx equal
、not_equal
、less
、less_equal
、greater
或greater_equal
。
下面的例子阐明了ratio
算法(在第二章中讨论的<typeinfo>
,在使用typeid
操作符时必须包含):
随机数<random>
<random>
库提供了强大的随机数生成工具,取代了来自<cstdlib>
的有缺陷的 C 风格rand()
函数。核心概念是随机数生成器和分布。生成器是一个函数对象,它以均匀分布的方式在预定义的范围内生成随机数,也就是说,原则上,所述范围内的每个值都具有相同的生成概率。生成器通常被传递给分布函子,以生成根据一些选定的统计分布分布的随机值。例如,这可以是另一种用户指定的均匀分布:
当要生成多个值时,绑定生成器和分布更方便,例如使用<functional>
(第二章)的工具:
std::function<int()> roller = std::bind(distribution, generator);
for (int i = 0; i < 100; ++i) std::cout << roller() << '\n';
随机数生成器
该库定义了两种类型的生成器:生成伪随机数的随机数引擎,以及一个真正的非确定性随机数生成器,std::random_device
。
伪随机数引擎
伪随机数引擎的三个系列以具有各种数字类型参数的通用类模板的形式提供:
std::linear_congruential_
engine
:使用最少量的内存(一个整数),速度相当快,但生成的随机数质量不高。std::mersenne_twister_
engine
:以更大的状态大小为代价产生最高质量的伪随机数(例如,预定义的mt19937
Mersenne twister 的状态由 625 个整数组成)。尽管如此,因为它们也是最快的发电机,如果大小不是问题,这些引擎应该是你的默认选择。std::subtract_with_carry_
engine
:虽然在质量(而不是速度)方面比线性同余发动机有所改进,但这些发动机的质量和性能比梅森捻线机低得多。不过,它们的状态大小更适中(通常为 96 字节)。
所有这些引擎都提供了一个接受可选种子来初始化引擎的构造函数。播种稍后解释。它们还有一个复制构造函数,并支持以下操作:
操作 | 描述 |
---|---|
seed(value) |
用给定的value 种子重新初始化引擎 |
operator() |
生成并返回下一个伪随机数 |
discard(n) |
生成n 伪随机数并将其丢弃 |
min() max() |
返回引擎可能生成的最小值和最大值 |
== / != |
比较两个引擎的内部状态(非成员运算符) |
<< / >> |
向/从流序列化:参见第五章(非成员操作符) |
所有三个引擎模板都需要一系列数字模板参数。因为选择合适的参数最好留给专家,所以每个族都有几个预定义的实例。在我们讨论这些之前,我们首先需要介绍随机数引擎适配器。
发动机适配器
以下函数对象调整底层引擎的输出:
std::discard_block_engine<e,p,r>
:对于底层引擎e
生成的每一组p > 0
个数字,除了r
个保留值(带p >= r > 0
)之外,都丢弃。std::independent_bits_engine<e,w>
:生成w > 0
位的随机数,即使底层引擎e
生成不同宽度的数。std::shuffle_order_engine<e,k>
:以不同的随机顺序传递底层引擎e
的编号。保存一张k > 0
数字表,每次返回并替换随机的一个数字。
所有的适配器都有一组相似的构造函数:一个默认的构造函数,一个带有转发给包装引擎的种子的构造函数,以及接受对现有引擎的左值或右值引用以进行复制或移动的构造函数。
适配器支持与包装引擎完全相同的操作,此外还有:
操作 | 描述 |
---|---|
seed() |
通过使用默认种子对基础引擎进行播种来重新初始化它 |
base() |
返回对底层引擎的const 引用 |
预定义引擎
基于之前的引擎和适配器,该库提供了以下预定义的引擎,您应该使用这些引擎,而不是直接使用这些引擎和/或适配器。这些的数学参数已经由专家定义:
minstd_rand0
/minstd_rand
为【0,2 31 -1】中产生std::uint_fast32_t
数字的linear_congruential_engines
。knuth_b
等于shuffle_order_engine<minstd_rand0,256>
。mt19937
/mt19937_64
是mersenne_twister_engine
产生的uint_fast32_t
/uint_fast64_t
数。ranlux24_base
/ranlux48_base
很少单独使用(见下一条),而是生成uint_fast32_t
/uint_fast64_t
数字的subtract_with_carry_engine
。ranlux24
/ranlux48
是由discard_block_engine
改装的ranlux24_base
/ranlux48_base
发动机。
Tip
因为在所有先前预定义的引擎之间进行选择是令人生畏的,所以一个实现还必须提供一个对大多数应用程序来说足够好的std::default_random_engine
(它可能是其他引擎之一的typedef
)。
非确定性随机数生成器
A random_device
原则上不产生伪随机数,而是真正的非确定性均匀分布随机数。它如何实现这一点是依赖于实现的:例如,它可以在你的 CPU 上使用特殊的硬件来产生基于一些物理现象的数字。如果random_device
实现不能生成真正的非确定性随机数,则允许退回到前面讨论的伪随机数引擎之一。要检测这一点,使用它的entropy()
方法:它返回生成数字质量的度量,但是如果使用伪随机数引擎,则返回零。
random_device
是不可复制的,并且只有一个构造函数接受一个可选的特定于实现的string
来初始化它。它具有类似于引擎提供的成员函数operator()
、min()
、max()
。然而,与伪随机数引擎不同的是,如果它未能生成一个数字(例如,由于硬件故障),它的operator()
可能会抛出一个std::exception
。
虽然一个random_device
生成真正的随机数,可能是加密安全的(检查你的库文档),它通常比任何伪随机引擎都慢。因此,通常的做法是使用random_device
来播种伪随机引擎,如下一节所述。
播种
所有伪随机数引擎都必须被植入初始值。如果您使用相同的种子设置引擎,那么您将始终获得相同的生成数字序列。虽然这对于调试或某些模拟很有用,但大多数时候,您希望每次运行都生成不同的不可预测的数字序列。这就是为什么每次执行程序时用不同的值给引擎播种是很重要的。这必须做一次(例如,在构建时)。推荐的方法是使用random_device
,但是正如您之前看到的,这也可能只是生成伪随机数。一个流行的选择是用当前时间播种(见第二章)。例如:
std::random_device seeder;
const auto seed = seeder.entropy() ? seeder() : std::time(nullptr);
std::default_random_engine generator(
static_cast<std::default_random_engine::result_type>(seed));
随机数分布
到目前为止,我们只讨论了在 32 位或 64 位无符号整数的全范围内均匀分布的随机数的生成。该库提供了大量的分布集合,您可以使用这些分布、范围和/或值类型来满足您的需求。如果你精通统计学,他们的名字听起来会很熟悉。描述它们背后的所有数学原理超出了本书的范围,但是下面几节简要描述了可用的发行版(有些比其他的更详细)。对于每个发行版,我们显示了支持的构造函数。有关这些分布及其参数的详细信息,我们建议您查阅数学参考资料。
均匀分布
- 生成
a
,b
范围内均匀分布的整数/浮点数。
uniform_int_distribution<Int=int>(Int a=0, Int b=numeric_limits<Int>::max())
uniform_real_distribution<Real = double>(Real a = 0.0, Real b = 1.0)
- 这是唯一一个被定义为函数而不是函子的分布。它使用给定的
Generator
作为随机性的来源,生成范围在[0.0,1.0]内的数字。bits
参数决定尾数的随机位数。
Real generate_canonical<Real, size_t bits, Generator>(Generator&)
伯努利分布
- 生成随机布尔值,其
p
等于生成true
的概率。
bernoulli_distribution(double p = 0.5)
- 按照一定的概率密度函数生成随机的非负整数值。
binomial_distribution<Int = int>(Int t = 1, double p = 0.5)
negative_binomial_distribution<Int = int>(Int k = 1, double p = 0.5)
geometric_distribution<Int = int>(double p = 0.5)
正态分布
-
Generates random numbers according to a normal, also called Gaussian, distribution. The parameters specify the expected
mean
and standard deviationstddev
. In Figure 1-1, μ represents the mean and σ the standard deviation.图 1-1。
Probability distributions for some example normal and Poisson distributions, plotting the probability (between 0 and 1) that a value is generated
normal_distribution<Real = double>(Real mean = 0.0, Real stddev = 1.0)
- 一些更高级的正态分布。
lognormal_distribution<Real = double>(Real mean = 0.0, Real stddev = 1.0)
chi_squared_distribution<Real = double>(Real degrees_of_freedom = 1.0)
cauchy_distribution<Real = double>(Real peak_location = 0., Real scale = 1.)
fisher_f_distribution<Real = double>(Real dof_num = 1., Real dof_denom = 1.)
student_t_distribution<Real = double>(Real degrees_of_freedom = 1.0)
泊松分布
- 与经典泊松分布相关的各种分布。后者如图 1-1 所示,其中λ是
mean
(对于该分布,它等于方差)。泊松分布生成整数,因此连接线仅用于说明目的。
poisson_distribution<Int = int>(double mean = 1.0)
exponential_distribution<Real = double>(Real lambda = 1.0)
gamma_distribution<Real = double>(Real alpha = 1.0, Real beta = 1.0)
weibull_distribution<Real = double>(Real a = 1.0, Real b = 1.0)
extreme_value_distribution<Real = double>(Real a = 0.0, Real b = 1.0)
抽样分布
离散分布
离散分布需要一组count
权重,并生成范围为 0,count
的随机数。一个值的概率取决于它的权重。提供了以下构造函数:
discrete_distribution<Int = int>()
discrete_distribution<Int = int>(InputIt first, InputIt last)
discrete_distribution<Int = int>(initializer_list<double> weights)
discrete_distribution<Int = int>(size_t count, double xmin, double xmax,
UnaryOperation op)
默认构造函数用 1.0 的单一权重初始化分布。第二个和第三个构造函数用一组给定的权重初始化它,作为一个迭代器范围,在第 [3 章讨论,或者作为一个initializer_list
,在第二章讨论。而最后一个用调用给定的一元运算生成的count
权重初始化。使用以下公式:
同
分段常数分布
分段常数分布需要一组区间和每个区间的权重。它生成均匀分布在每个区间的随机数。提供了以下构造函数:
- 默认构造函数用边界为 0.0 和 1.0、权重为 1.0 的单个区间初始化分布。
piecewise_constant_distribution<Real = double>()
- 用区间初始化分布,其边界取自
firstBound
,lastBound
迭代器范围,其权重取自从firstWeight
开始的范围。
piecewise_constant_distribution<Real = double>(
InputIt1 firstBound, InputIt1 lastBound, InputIt2 firstWeight)
- 用区间初始化分布,区间的边界以
initializer_list
的形式给出,其权重由给定的一元运算生成。
piecewise_constant_distribution<Real = double>(
initializer_list<Real> bounds, UnaryOperation weightOperation)
- 用范围[
xmin
,xmax
]内的count
均匀间隔和给定一元运算生成的权重初始化分布。
piecewise_constant_distribution<Real = double>(size_t count,
Real xmin, Real xmax, UnaryOperation weightOperation)
piecewise_constant_distribution
有方法intervals()
和densities()
返回区间边界和每个区间值的概率密度。
分段线性分布
由piecewise_linear_distribution
实现的分段线性分布类似于分段常数分布,但在每个区间中具有线性概率分布,而不是均匀分布。它需要一组区间和每个区间边界的一组权重。它还提供了intervals()
和densities()
方法。这组构造函数类似于上一节中讨论的那些,但是需要一个额外的权重,因为每个边界都需要一个权重,而不是每个区间。
例子
图 1-2 左侧的图表显示了使用之前的代码生成一百万个值时,特定值被生成的次数。在图中,您可以清楚地看到区间为(1,20)、(20,40)、(40,60)和(60,80)的piecewise_constant_distribution
,区间权重为 1、3、1 和 3。
图 1-2。
Difference between a piecewise constant and piecewise linear distribution
右图显示了具有相同间隔和边界权重 1、3、1、3 和 1 的piecewise_linear_distribution
。注意,与piecewise_constant_distribution
相比,您需要一个额外的权重,因为您为边界而不是间隔指定了权重。
如果使用间隔大小不同的piecewise_linear_distribution
,图形就不是连续的。这是因为权重是针对区间的边界给出的,所以如果开始的权重为 3,结束的权重为 1,则区间开始时生成的值比结束时生成的值多三倍。因此,如果区间是两倍长,所有概率也是两倍小,包括那些边界。
数字数组<valarray>
是一个类似容器的类,用于存储和有效操作数值的动态数组。一个valarray
内置了对多维数组的支持,并有效地将<cmath>
中定义的大多数数学运算应用于每个元素。存储在valarray
中的类型本质上必须是算术或指针类型或者行为类似的类,比如std::complex
。由于这些限制,一些编译器能够比使用其他容器更好地优化valarray
计算。
std::valarray
提供了以下构造函数:
构造器 | 描述 |
---|---|
valarray() valarray(count) |
构造一个空的valarray 或一个带有count 零初始化/默认构造元素的。 |
valarray(const T& val, n) valarray(const T* vals, n) |
用来自vals 数组的val 的n 个副本或n 个副本构造一个valarray 。 |
valarray(initializer_list) |
构造一个valarray 并用初始化列表中的值初始化它。 |
valarray(const x _array<T>&) |
在 x _array<T> 和valarray<T> 之间转换的构造函数,其中 x 可以是slice 、gslice 、mask 或indirect 。所有四种类型都将在后面讨论。 |
valarray(const valarray&) valarray(valarray&&) |
复制和移动构造函数。 |
这里有一个例子:
valarray
支持以下操作:
操作 | 描述 |
---|---|
operator[] |
检索单个元素或部分,即稍后讨论的slice_array 、gslice_array 、mask_array 或indirect_array 。 |
operator= |
复制、移动和初始化列表赋值操作符。您还可以分配一个元素类型的实例:valarray 中的所有元素都将被替换为它的副本。 |
operator+ 、- 、∼ 、! |
对每个元素应用一元运算。用结果返回一个新的valarray (operator! 返回valarray<bool> )。 |
operator+= 、-= 、*= 、/= 、%= 、&= 、|= 、^= 、<<= 、>>= |
将这些操作应用于每个元素。输入是'const T& '或同样长的'const valarray<T>& '。在后一种情况下,运算符是分段应用的。 |
swap() |
交换两个valarray 。 |
size() resize(n,val=T()) |
返回或更改元素的数量。调整大小时,可以指定分配给新元素的值;默认情况下,它们是零初始化的。 |
sum() 、min() 、max() |
返回所有元素的总和、最小值和最大值。 |
shift(int n) cshift(int n) |
返回一个相同大小的新的valarray ,其中的元素移动了n 个位置。如果选择n < 0 ,元素向左移动。对于shift() ,移出的元素被初始化为零,而cshift() 执行循环移位。 |
apply(func) |
返回一个新的valarray ,其中每个元素都是通过将给定的一元函数应用于当前元素来计算的。 |
还支持以下非成员函数:
操作 | 描述 |
---|---|
swap() |
交换两个valarray |
begin() ,end() |
返回开始和结束迭代器(参见章节 3 和 4 |
abs() |
返回一个带有绝对值的valarray |
operator+ 、- 、* 、/ 、% 、& 、| 、^ 、<< 、>> 、&& 、|| |
将这些二元运算符应用于一个valarray 和一个值,或者应用于两个等长的valarray 的每个元素 |
operator== 、!= 、< 、<= 、> 、>= |
返回一个valarray<bool> ,其中每个元素是两个valarray 的元素或一个valarray 的元素与一个值的比较结果 |
还支持对所有元素同时应用指数(exp()
、log()
和log10()
)、幂(pow()
和sqrt()
)、三角(sin()
、cos()
、…)和双曲线(sinh()
、cosh()
和tanh()
)函数。这些非成员函数返回一个新的valarray
和结果。
标准::切片
这代表了一个valarray
的一部分。A std::slice
本身不包含也不引用任何元素;它只是定义了一系列的索引。这些索引不一定是连续的。它有三个构造函数:slice(start, size, stride)
,一个等价于slice(0,0,0)
的默认构造函数,和一个复制构造函数。提供了三种吸气器:start()
、size()
和stride()
。要使用slice
,创建一个并将其传递给一个valarray
的operator[]
。这从位置start()
开始,以给定的stride()
(步长)从valarray
中选择size()
元素。如果在一个const valarray
上调用,结果是一个带有元素副本的valarray
。否则,它是一个引用元素的slice_array
。
slice_array
比valarray
支持更少的操作,但是可以使用valarray<const slice_array<T>&)
构造函数转换成valarray
。slice_array
有以下三种赋值运算符:
void operator=(const T& value) const
void operator=(const valarray<T>& arr) const
const slice_array& operator=(const slice_array& arr) const
还提供了操作符+=
、-=
、*=
、/=
、%=
、&=
、|=
、^=
、<<=
、>>=
。这些运算符需要一个与slice_array
引用的valarray
类型相同的右侧操作数,它们将运算符应用于slice_array
引用的元素。例如:
切片的一个用例是从代表矩阵的valarray
中选择行或列。它们也可以用来实现矩阵算法,如矩阵乘法。
std::gslice
gslice
代表广义切片。一个gslice
有一个valarray<size_t>
代表尺寸,一个代表步幅,而不是一个代表尺寸和步幅的值。默认构造函数相当于gslice(0, valarray<size_t>(), valarray<size_t>())
,并且还提供了复制构造函数。就像std::slice
一样,吸气剂start()
、size()
和stride()
都是可用的。类似于slice
,通过将gslice
传递给valarray
的operator[]
来使用它,返回带有副本的valarray
或带有引用的gslice_array
。gslice_array
支持一组类似于slice_array
的操作。如何使用不同的尺寸和步幅最好用一个例子来解释:
这个例子有两个大小和步幅值,所以gslice
创建了两个切片。第一个切片具有以下参数:
- Start index =
1
(构造函数gslice
的第一个参数) - 尺寸= 2,步幅= 5(
sizes
和strides
中的第一个值)
因此,这个切片表示索引{1,6}。这样就创建了两个二级切片,每个切片对应一个索引。来自第一级切片的索引被用作两个第二级切片的起始索引。因此,第一个二级切片具有这些参数
- 开始索引= 1(第一个片{1,6}的第一个索引)
- 尺寸= 3,步幅= 2(来自
sizes
和strides
的第二个值)
第二个具有这些参数(注意,两者具有相同的大小和步幅参数):
- 开始索引= 6(第一个片{1,6}的第二个索引)
- 尺寸= 3,步幅= 2(来自
sizes
和strides
的第二个值)
因此,第二级切片表示这些索引:{1,3,5,6,8,10}。如果有第三级(即sizes
和strides
中的第三值),这些索引将作为六个第三级切片的起始索引(都使用那些第三值sizes
和strides
)。因为没有第三级,所以相应的值只是从valarray
中选择:{ 11
、33
、55
、66
、88
、111
}。
标准::掩码 _ 数组
一个valarray
上的operator[]
也接受一个valarray<bool>
,类似地返回一个带有副本的valarray
或者一个带有引用的std::mask_array
。该操作符从valarray
中选择在valarray<bool>
中相应位置具有true
值的所有元素。mask_array
支持一组类似于slice_array
的操作。这里有一个例子:
标准::间接 _ 数组
最后,valarray
上的operator[]
也接受一个valarray<size_t>
,返回一个带有副本的valarray
或者一个带有引用的std::indirect_array
。valarray<size_t>
指定应该选择哪些指标。一个indirect_array
再次支持一组类似于slice_array
的操作。例如:
二、通用设施
移动、前进、交换<utility>
本节解释move()
、move_if_noexcept()
、forward()
、swap()
和exchange()
。顺便还介绍了移动语义和完美转发的概念。
移动的
如果某个对象的前一个用户不再需要它,可以将其移动到其他地方(而不是复制)。将资源从一个对象移动到另一个对象通常比(深度)复制它们更有效。例如,对于一个string
对象,移动通常就像复制一个char*
指针和一个长度(常数时间)一样简单;没有必要复制整个char
数组(线性时间)。
除非另外指定,否则被移出的源对象处于未定义但有效的状态,除非重新初始化,否则不应再使用。例如,移动一个std::string
(参见第六章)的有效实现可以将源的char*
指针设置为nullptr
,以防止数组被删除两次,但这不是标准所要求的。同样,也没有说明length()
被移走后将返回什么。某些操作,尤其是赋值,仍然是允许的,如下例所示:
尽管名字如此,std::move()
函数在技术上并不移动任何东西:相反,它只是简单地标记一个给定的T
、T&
或T&&
值可以被移动,实际上是通过静态地将它转换成一个右值引用T&&
。由于类型强制转换,其他函数可能会被重载决策选择,和/或值参数对象可能会使用它们的移动构造函数(形式为T(T&& t)
)进行初始化,如果可用的话,而不是它们的复制构造函数。这种初始化发生在被调用方,而不是调用方。一个右值参数T&&
强制调用者总是移动。
类似地,也可以使用移动赋值操作符(形式为operator=(T&&)
)将一个对象移动到另一个对象:
如果没有定义移动成员,无论是显式的还是隐式的,T&&
的重载解析回退到T&
或T
,在后一种情况下仍然创建一个副本。生成隐式移动成员的条件包括不能有任何用户定义的复制、移动或析构成员,也不能有任何不能移动的非静态成员变量或基类。
move_if_noexcept()
函数类似于move()
,除了如果T
的 move 构造函数已知不从其异常规范中抛出(noexcept
,或已弃用的throw()
),它只向T&&
进行强制转换;否则,它强制转换为const T&
。
标准定义的所有类都有适当的移动成员。例如,第三章中的许多容器可以在固定时间内移动(不是std::array
,尽管它会移动单个元素以避免深度复制)。
Tip
对于重要的自定义类型,为了获得最佳性能,不仅定义移动成员至关重要,而且始终使用noexcept
说明符来定义成员也同样重要。第三章中的容器类广泛使用移动来加速操作,比如添加一个新元素,或者重新定位元素数组(例如,用顺序容器)。类似地,如果提供有效的移动成员(和/或非成员操作,稍后讨论),第四章的许多算法都会受益。但是,尤其是在移动元素数组时,这些优化通常只有在已知值的移动成员不抛出时才会生效。
促进
std::forward()
helper 函数旨在模板化函数中使用,以便在保留任何 move 语义的同时有效地将其参数传递给其他函数。如果forward<T>()
的参数是一个左值引用T&
,那么这个引用将被原封不动地返回。否则,参数被转换为右值引用T&&
。一个例子将阐明它的预期用途:
good_fwd()
用的成语叫完美转发。它最佳地保留了右值引用(比如那些std::move()
d 或临时对象)。这个习惯用法的第一个要素是所谓的转发或通用引用:一个T&&
参数,一个T
模板类型参数。如果没有它,模板参数演绎将删除所有引用:对于ugly_fwd()
;A&
和A&&
都变成了A
。有了转发引用,分别推导出A&
和A&&
:也就是说,即使转发引用看起来像T&&
,如果传递了A&
,则推导出A&
而不是A&&
。尽管如此,仅使用转发引用是不够的,如bad_fwd()
所示。当按原样使用命名变量t
时,它与一个左值函数参数绑定(所有命名变量都这样),即使其类型被推断为A&&
。这就是std::forward<T>()
的用武之地。与std::move()
类似,它会转换为T&&
,但前提是给定一个右值类型的值(包括类型为A&&
的命名变量)。
所有这些都很微妙,更多的是关于 C++ 语言(特别是类型演绎)而不是标准库。这里的要点是,为了将函数模板的参数正确地转发给函数,您应该考虑使用完美转发——也就是说,转发引用与std::forward()
相结合。
交换
std::swap()
模板函数交换两个对象,好像实现为:
template<typename T> void swap(T& one, T& other)
{ T temp(std::move(one)); one = std::move(other); other = std::move(temp); }
还定义了一个类似的swap()
函数模板来分段交换等长T[N]
数组的所有元素。
虽然如果有合适的移动成员就已经很有效了,但是为了获得真正的最佳性能,您应该考虑专门化这些模板函数:例如,消除移动到临时。比如第四章的很多算法都调用这个非成员swap()
函数。对于标准类型,swap()
在适当的地方已经定义了专门化。
与swap()
类似的一个函数是std::exchange()
,它在返回旧值的同时给某物赋予一个新值。有效的实现是
template<typename T, typename U=T> T exchange(T& x, U&& new_val)
{ T old_val(std::move(x)); x = std::forward<U>(new_val); return old_val; }
Tip
尽管swap()
和exchange()
可能在std
名称空间中被特殊化,但是大多数人建议将它们特殊化到与它们的模板参数类型相同的名称空间中。这样做的好处是所谓的参数相关查找(ADL)是可行的。换句话说,例如swap(x,y)
不需要using
指令或声明,也不需要指定swap()
的名称空间。ADL 规则基本上规定,非成员函数应该首先在其参数的名称空间中查找。如果需要的话,通用代码应该使用下面的习惯用法回到std::swap()
:using std::swap; swap(x,y);
。简单地编写std::swap(x,y)
将不会在std
名称空间之外使用用户定义的swap()
函数,而单独的swap(x,y)
将不会工作,除非有这样的用户定义函数。
对和元组
配对<utility>
std::pair<T1,T2>
模板struct
是一个可复制的、可移动的、可交换的(按字典顺序)可比较的struct
,它在其公共成员变量first
和second
中存储了一对T1
和T2
值。默认构造的对对其值进行零初始化,但也可以提供初始值:
std::pair<unsigned int, Person> p(42u, Person("Douglas", "Adams"));
使用辅助功能std::make_pair()
可以自动推导出两个模板类型参数:
auto p = std::make_pair(42u, Person("Douglas", "Adams"));
Tip
不是所有的类型都可以被有效地移动,并且在构造一个对的时候必须被复制。对于较大的对象(例如,包含固定大小数组的对象),这可能是一个性能问题。其他类型甚至根本不可复制。对于这种情况,std::pair
有一个特殊的“分段”构造函数来执行其两个成员的就地构造。它是用一个特殊的常量调用的,后跟两个元组(见下一节),其中包含要转发给两个成员的构造函数的参数。
例如(forward_as_tuple()
用于不将字符串复制到临时元组):
std::pair<unsigned, Person> p(std::piecewise_construct,
std::make_tuple(42u), std::forward_as_tuple("Douglas", "Adams"));
分段构造也可以与第三章中容器的emplace()
函数一起使用(这些函数的定义类似,以避免不必要的复制),特别是与std::map
和std::unordered_map
的函数一起使用。
元组
std::
tuple
是pair
的推广,允许存储任意数量的值(即零个或更多,而不仅仅是两个):std::tuple<Type...>
。大部分类似于pair
,包括make_tuple()
辅助功能。主要区别在于单个值不存储在公共成员变量中。相反,您可以使用get()
模板函数之一来访问它们:
获取tuple
值的另一种方法是使用tie()
函数解包。特殊的std::ignore
常量可用于排除任何值:
int one, two; double three;
std::tie(one, two, three, std::ignore) = t;
Tip
std::tie()
函数可用于基于多个值紧凑地实现字典式比较。例如,简介中的Person
类的operator<
主体可以写成
return std::tie(lhs.m_isVIP, lhs.m_lastName, lhs.m_firstName)
< std::tie(rhs.m_isVIP, rhs.m_lastName, rhs.m_firstName);
还有两个助手struct
用于获取给定元组的大小和元素类型,这在编写通用代码时非常有用:
注意pair
和std::array
(见第三章)在各自的头中也定义了get()
、tuple_size
和tuple_element
,但没有定义tie()
。
tuple
s 的最后一个帮助函数是std::forward_as_tuple()
,它创建了一个引用其参数的元组。这些通常是左值引用,但是保留右值引用,就像前面解释的std::forward()
一样。它被设计为将参数转发给tuple
的构造函数(也就是说,同时避免复制),特别是在通过值接受元组的函数的上下文中。例如,函数f(tuple<std::string, int>)
可以如下调用:f(std::forward_as_tuple("test", 123));
。
元组也为自定义分配器提供了便利,但是这是一个高级主题,超出了本书的范围。
关系运算符<utility>
在std::rel_ops
名称空间中提供了一组很好的关系操作符:!=
、<=
、>
和>=
。第一个按照operator==
实现,其余的转发到operator<
。所以,你的类只需要实现operator==
和<
,其他的都是在你添加一个using namespace std::rel_ops;
时自动生成的
智能指针<memory>
智能指针是一个 RAII 样式的对象,它(通常)修饰并模仿一个指向堆分配内存的指针,同时保证在适当的时候总是释放这些内存。作为一条规则,现代 C++ 程序不应该使用原始指针来管理(共有的)动态内存:所有由new
或new[]
分配的内存都应该由一个智能指针来管理,或者,对于后者,由一个容器来管理,比如vector
(参见第三章)。因此,C++ 程序应该很少再直接调用delete
或delete[]
。这样做将大大有助于防止内存泄漏。
独占所有权指针
标准::唯一 _ 指针
一个unique_ptr
独占一个指向堆内存的指针,因此不能被复制,只能被移动或交换。除此之外,它的行为很像一个普通的指针。下面说明了它在堆栈上的基本用法:
->
和*
操作符确保了unique_ptr
通常可以像原始指针一样使用。比较运算符==
、!=
、<
、>
、<=
和>=
用于比较两个unique_ptrs
或一个unique_ptr
与nullptr
(按任意顺序),但不用于比较一个unique_ptr<T>
与一个T
值。要实现后者,必须调用get()
来访问原始指针。一个unique_ptr
也方便地转换成一个布尔值来检查nullptr
。
使用助手功能make_unique()
有助于构建。例如:
{ auto jeff = std::make_unique<Person>("Jeffrey");
...
Tip
使用make_unique()
不仅可以缩短代码,还可以防止某些类型的内存泄漏。考虑一下f(unique_ptr<X>(new X), g())
。如果g()
在X
被构造之后,但是在它被分配给它的unique_ptr
之前抛出,那么X
指针就会泄漏。相反,编写f(make_unique<X>(), g())
可以保证这种泄漏不会发生。
使它们成为真正重要的实用工具的其他用途包括:
- 它们是转移独占所有权的最安全和推荐的方法,要么从创建堆对象的函数返回一个
unique_ptr
,要么将一个作为参数传递给接受进一步所有权的函数。这有三个主要优点:在这两种情况下,通常都必须使用std::move()
,使得所有权转移显式。预期的所有权转移也从函数的签名中变得显而易见。它可以防止内存泄漏(这种错误有时很微妙:参见下一篇技巧)。 - 它们可以安全地存放在第三章的容器中。
- 当用作另一个类的成员变量时,它们消除了在析构函数中显式
delete
的需要。此外,它们防止编译器为应该独占动态内存的对象生成容易出错的副本成员。
A unique_ptr
也可以管理用new[]
分配的内存:
对于这个模板专门化,解引用操作符*
和->
被替换为索引数组访问操作符[]
。一个更强大更方便的管理动态数组的类std::vector
,在第三章中解释。
一个unique_ptr<T>
有两个相似的成员经常被混淆:release()
和reset(T*=nullptr)
。前者用nullptr
替换旧的存储指针(如果有的话),而后者用给定的T*
替换。关键区别在于release()
不删除旧指针。相反,release()
旨在释放存储指针的所有权:它只是将存储指针设置为nullptr
并返回它的旧值。这有助于将所有权传递给例如遗留 API。另一方面,reset()
旨在用新值替换存储的指针,不一定是nullptr
。在覆盖旧指针之前,它会被删除。因此,它也不返回任何值:
Tip
使用release()
转移所有权时,注意内存泄漏。假设前面的例子以TakeOwnership(niles.release(), f())
结束。如果对f()
的调用在unique_ptr
拥有release
d 的所有权后抛出,奈尔斯泄密。因此,始终确保包含release()
子表达式的表达式也不包含任何抛出子表达式。在本例中,解决方案是在前面的行中计算f()
,将其结果存储在一个已命名的变量中。前面推荐的用std::move(niles)
传输,顺便说一下也绝对不会漏。但是,对于遗留 API,这并不总是一个选项。
Caution
一个相当常见的错误是在应该使用reset()
的地方使用release()
,后者使用默认的nullptr
参数,忽略由release()
返回的值。先前由unique_ptr
拥有的物品随后泄露,这通常不会被注意到。
的一个高级特性是他们可以使用自定义的删除器。删除器是销毁所拥有的指针时执行的函子。这对于非默认内存分配很有用,可以进行额外的清理,或者,例如,管理由 C 函数fopen()
(在<cstdio>
中定义)返回的文件指针:
这个例子使用了一个类型为std::function
(在<functional>
头中定义,将在本章后面讨论)的删除器,它用一个函数指针初始化,但是也可以使用任何仿函数类型。
std::自动 _ptr
在编写时,<memory>
头仍然为独占所有权定义了第二种智能指针类型,即std::auto_ptr
。然而,在 C++11 中,这已经被弃用,取而代之的是unique_ptr
,并且在 C++17 中将被移除。因此,我们不详细讨论它。本质上,auto_ptr
是一个有缺陷的unique_ptr
,在复制时被隐式移动:这使得它们不仅容易出错,而且与第 3 和 4 章中的标准容器和算法一起使用也很危险(事实上是非法的)。
共享所有权指针
标准::共享 _ptr
当多个实体共享同一个堆分配的对象时,为它分配一个所有者并不总是显而易见或可能的。对于这种情况,shared_ptr
s 存在,在<memory>
中定义。这些智能指针为一个共享内存资源维护一个线程安全的引用计数,一旦它的引用计数达到零,它就会被删除:也就是说,一旦最后一个共同拥有它的shared_ptr
被析构。use_count()
成员返回引用计数,unique()
检查计数是否等于 1。
像unique_ptr
一样,它有->
、*
、转换为布尔和比较操作符来模拟原始指针。同样提供等效的get()
和reset()
成员,但没有release()
。然而,shared_ptr
不能管理动态数组。真正让它与众不同的是shared_ptr
s 可以而且打算被复制:
一个shared_ptr
可以通过将一个unique_ptr
移动到其中来构造,但不能反过来。为了构造一个新的shared_ptr
,再次推荐使用make_shared()
:原因与make_unique()
(更短的代码和内存泄漏预防)相同,但在这种情况下也是因为它更有效。
再次支持自定义删除器。然而,与unique_ptr
不同的是,删除者的类型不是shared_ptr
模板的类型参数。因此,类似于前面示例中的声明变成了:
std::shared_ptr<FILE> smartFilePtr(fopen("test.txt", "r"), fclose);
要获得相关类型的shared_ptr
,请使用std::static_pointer_cast()
、dynamic_pointer_cast()
或const_pointer_cast()
。如果结果不是null
,引用计数安全地增加 1。一个例子将阐明:
shared_ptr
s 的一个鲜为人知的特性叫做别名,用于共享已经共享的对象的部分。最好用一个例子来介绍:
一个shared_ptr
既有自己的指针又有存储的指针。前者决定引用计数,后者由get()
、*
和->
返回。通常两者是相同的,但如果用别名构造函数构造就不同了。几乎所有的操作都使用存储的指针,包括比较运算符<
、>=
等等。为了基于拥有的指针而不是存储的指针进行比较,使用owner_before()
成员或std::owner_less<>
仿函数类(仿函数将很快解释)。例如,当将shared_ptr
存储在std::set
中时,这很有用(参见第三章)。
标准::弱 _ 指针
有时,尤其是在构建共享对象的缓存时,您希望在需要时保留对共享对象的引用,但又不希望引用必然会阻止对象的删除。这个概念通常被称为弱引用,由<memory>
以std::weak_ptr
的形式提供。
一个非空的weak_ptr
由一个shared_ptr
构成,或者是后来给它赋值一个shared_ptr
的结果。这些指针可以自由地复制、移动或交换。虽然一个weak_ptr
并不共同拥有这个资源,但是它可以访问它的use_count()
。为了检查共享资源是否仍然存在,也可以使用expired()
(相当于use_count()==0
)。然而,weak_ptr
不能直接访问共享的原始指针,因为没有什么可以阻止最后一个共有者同时删除它。要访问资源,首先必须使用lock()
成员将weak_ptr
提升为共有shared_ptr
:
功能对象<functional>
函数对象或仿函数是带有operator()(T1,...,Tn)
( n
可能为零)的对象,允许它像函数或运算符一样被调用:
函子不仅可以传递给许多标准算法(第章第四部分)和并发构造(第章第七部分),而且对于创建你自己的通用算法也非常有用,例如,存储或提供回调函数。
本节概述了在<functional>
中定义的函子,以及创建和使用函子的工具。 1 我们还将简要介绍 lambda 表达式,这是一种强大的 C++11 语言构造,用于创建函子。
不过,在我们深入研究函子之前,先简单介绍一下在<functional>
头文件中定义的引用包装器实用程序。
参考包装
函数std::ref()
和cref()
返回std::reference_wrapper<T>
实例,这些实例简单地包装了对其输入参数的(const
) T&
引用。然后,可以使用get()
显式提取该引用,或者通过强制转换为T&
隐式提取该引用。
因为这些包装器可以被安全地复制,所以它们可以被用来传递对模板函数的引用,这些模板函数通过值获取它们的参数,错误地转发它们的参数(转发在本章前面已经讨论过了),或者出于其他原因复制它们的参数。不接受引用作为参数,但使用ref()
/ cref()
的标准模板函数包括std::thread()
和async()
(参见第七章),以及稍后讨论的std::bind()
函数。
这些包装器也可以分配给,这样就可以将引用存储到第三章的容器中。例如,在下面的例子中,您不能声明一个vector<int&>
,因为int&
的值不能赋给:
预定义函子
<functional>
头提供了一整个系列的仿函数struct
,类似于本节介绍中前面使用的my_plus
示例:
plus
、minus
、multiplies
、divides
、modulus
和negate
equal_to
、not_equal_to
、greater
、less
、greater_equal
和less_equal
logical_and
、logical_or
和logical_not
bit_and
、bit_or
、bit_xor
和bit_not
这些函子通常会产生简短易读的代码,甚至比 lambda 表达式更容易理解。以下示例使用第四章中介绍的sort()
算法对数组进行降序排序(默认为升序):
int array[] = { 7, 9, 7, 2, 0, 4 };
std::sort(begin(array), end(array), std::greater<int>());
从 C++14 开始,所有这些仿函数类都有一个特殊的专门化,即T
等于void
,void
也成为了默认的模板类型参数。这些被称为透明运算符函子,因为它们的函数调用运算符可以方便地推导出参数类型。例如,在前面的sort()
示例中,您可以简单地使用std::greater<>
。同一函子甚至可以用于不同的类型:
正如第三章所解释的,透明的std::less<>
和greater<>
函子也是有序关联容器的首选比较函子。
将一元/二元仿函数predicate
传递给std::not1()/not2()
会创建一个新的仿函数(类型为unary_negate
/ binary_negate
),该仿函数对predicate
的结果求反(即计算结果为!predicate()
)。为此,predicate
的类型必须定义一个公共成员类型argument_type
。<functional>
中的所有函子类型都有这个。
通用函数包装
模板类是为包装任何类型的可调用实体而设计的:也就是说,任何类型的函数对象或指针。这包括,例如,bind
或 lambda 表达式的结果(稍后将更详细地解释这两个表达式):
如果调用默认构造的function
对象,就会抛出std::bad_function_call
异常。为了验证一个function
是否可以被调用,它方便地转换为一个布尔值。或者,您可以使用==
或!=
将function
与nullptr
进行比较,就像使用函数指针一样。
其他成员包括target<Type>()
以获得指向包装实体的指针(必须指定正确的Type
;否则成员返回nullptr
,target_type()
返回该包装实体的type_info
(type_info
将在本章后面的“类型实用程序”中解释)。
Tip
前面提到的std::ref()
、cref()
及其返回类型reference_wrapper
的一个鲜为人知的特性是,它们也可以用来包装可调用函数。然而,与存储可调用对象副本的std::function
不同的是,reference_wrapper
存储的是对它的引用。这在将您不希望被复制的仿函数(例如,因为它太大(性能)、有状态或根本不可复制)传递给接受它或可能通过值传递它的算法时非常有用。例如:
function_that_copies_its_callable_argument(std::ref(my_functor));
请注意,对于第四章中的标准算法,通常不指定它们多长时间复制一次参数。所以为了保证没有拷贝,你必须使用(c)ref()
。
绑定函数参数
std::bind()
函数可以用来包装任何可调用函数的副本,同时改变其签名:参数可以被重新排序,被赋予固定值,等等。为了指定将哪些参数转发给包装的可调用函数,一系列值或所谓的占位符(_1
、_2
等)被传递给bind()
。传递给绑定函子的第一个参数被转发给占位符_1
的所有实例,第二个被转发给_2
的实例,依此类推。占位符的最大数量取决于具体的实现;并且返回的函子的类型是未指定的。一些例子将阐明:
类成员的函子
前面介绍的std::function
和bind()
都可以用来创建仿函数,这些仿函数计算给定对象的成员变量,或者调用给定对象的成员函数。第三种选择是使用std::mem_fn()
,它专门用于此目的:
struct my_struct { int val; bool fun(int i) { return val == i; } };
int main() {
my_struct s{234};
std::function<int(my_struct&)> f_get_val = &my_struct::val;
std::function<bool(my_struct&,int)> f_call_fun = &my_struct::fun;
std::cout << f_get_val(s) << ' ' << f_call_fun(s, 123) << std::endl;
using std::placeholders::_1;
auto b_get_val = std::bind(&my_struct::val, _1);
auto b_call_fun_on_s = std::bind(&my_struct::fun, std::ref(s), _1);
std::cout << b_get_val(s) << ' ' << b_call_fun_on_s(234) << std::endl;
auto m_get_val = std::mem_fn(&my_struct::val);
auto m_call_fun = std::mem_fn(&my_struct::fun);
std::cout << m_get_val(s) << ' ' << m_call_fun(s, 456) << std::endl;
}
由bind()
和mem_fn()
创建的成员函子,而不是std::function
创建的成员函子,也可以用一个指针或一个标准智能指针(见上一节)作为第一个参数来调用(也就是说,不用解引用)。关于bind()
选项的有趣之处还在于它可以绑定目标对象本身(参见b_call_fun_on_s
)。如果这不是必需的,std::mem_fn()
通常会产生最短的代码,因为它推导出了整个类型。更现实的例子是这样的(分别在第 3 、 4 和 6 章节中解释了vector
、count_if()
和string
):
LAMBDA EXPRESSIONS
虽然不是标准库的一部分,但是 lambda 表达式是创建函子的强大工具,非常值得简单介绍一下。特别是,当与第四章中的算法、第七章中的并发结构等等结合起来时,它们通常构成了极具表现力的优雅代码的基础。在本书中可以找到几个 lambda 表达式的例子,尤其是在第四章中。
lambda 表达式通常被认为创建了一个匿名函数,但实际上它创建了一个未指定类型的函子,也称为闭包。lambda 表达式不必像<functional>
构造那样从现有的函数开始:其闭包的函数调用操作符的主体可以包含任意代码。
lambda 表达式的基本语法如下:
CaptureBlock
mutable -> ReturnType {Body}
Capture block
:指定从封闭范围中捕获哪些变量。实际上,对于每个捕获的变量,创建的仿函数都有一个同名成员,如果是通过值捕获的,则包含该捕获变量的副本,如果是通过引用捕获的,则包含对该变量的引用。因此,这些变量在身体中变得可用。捕获块的基本语法:
[]
不捕获变量(不能省略)。[x, &y]
通过值捕获x
,通过引用捕获y
。[=, &x]
通过值捕获封闭范围内的所有变量,除了x
通过引用捕获。[&, x,y]
通过引用捕获所有变量,除了x
和y
通过值捕获。[this]
捕获this
指针,授权主体访问周围对象的所有成员。
Parameters
:调用仿函数时要传递的参数。省略等同于指定一个空列表()
。参数类型可以是auto
。
mutable
:默认情况下,lambda 仿函数的函数调用操作符总是被标记为const
,这意味着通过值捕获的变量(即复制到成员变量中的变量)不能被修改(赋值给被调用的非const
成员,等等)。指定mutable
使函数调用操作符非const
。
Return type
:返回值的类型。只要主体的所有返回语句返回完全相同的类型,就可以省略。
Body
:调用 lambda 仿函数时要执行的代码(非可选)。
也可以指定noexcept
和/或属性(在可选的mutable
之后),但是很少使用。
初始化列表<initializer_list>
C++ 编译器使用initializer_list<T>
类型来表示初始化列表声明的结果:
这种花括号语法是创建非空初始化列表的唯一方法。一旦创建,initializer_list
就不可改变。它们的几个操作size()
、begin()
和end()
类似于容器的操作(第三章)。当从一个初始化值列表中构造一个initializer_list
时,该列表存储这些值的一个副本。然而,复制一个initializer_list
并不会复制元素:新的副本只是引用相同的值数组。
最常见的用例可能是初始化列表构造函数,当使用花括号时,它们优先于其他构造函数,这一点很特别:
例如,第三章的所有容器类都有初始化列表构造函数,用一个值列表来初始化它们。
日期和时间实用程序<chrono>
<chrono>
库引入了一些工具,主要用于跟踪不同精度的时间和持续时间,这由所使用的时钟类型决定。要处理日期,您必须使用 C 风格的日期和时间类型以及在<ctime>
中定义的函数。来自<chrono>
的system_clock
允许与<ctime>
的互操作性。
期间
A std::chrono::duration<Rep, Period=std::ratio<1>>
将时间跨度表示为滴答计数,表示为通过count()
( Rep
是或模拟算术类型)可获得的Rep
值。两个连续分笔成交点之间的时间或周期由Period
静态确定,一种表示秒数(或分数)的std::ratio
类型(std::ratio
在第一章中解释)。默认的Period
是一秒钟:
只要不需要截断,duration
构造函数可以在不同Period
的duration
和/或 count Rep
表示之间进行转换。duration_cast()
函数也可以用于截断转换:
为了方便起见,在std::chrono
名称空间中预定义了几个类似于前一个例子的typedef
:hours
、minutes
、seconds
、milliseconds
、microseconds
和nanoseconds
。每个都使用未指定的有符号整数Rep
类型,至少大到足以表示大约 1000 年的持续时间(Rep
分别至少有 23、29、35、45、55 和 64 位)。为了进一步方便起见,名称空间std::literals::chrono_literals
包含文字操作符,可以很容易地分别创建duration
类型的实例:h
、min
、s
、ms
、us
和ns
。它们也可以通过using namespace std::chrono
声明获得。当应用于浮点文字时,结果具有未指定的浮点类型,如Rep
:
支持您直觉上期望使用duration
的所有算术和比较运算符:+
、-
、*
、/
、%
、+=
、-=
、*=
、/=
、%=
、++
、--
、==
、!=
、<
、>
、<=
和>=
。例如,下面的表达式计算出带有count() == 22
的duration
:
duration_cast<minutes>((12min + .5h) / 2 + (100ns >= 1ms? -3h : ++59s))
时间点
一个std::chrono::time_point<Clock, Duration=Clock::duration>
代表一个时间点,表示为从一个Clock
纪元开始的一个Duration
。这个Duration
可以从它的time_since_epoch()
成员那里获得。历元被定义为被选作特定时钟的原点的时刻,即测量时间的参考点。下一节将介绍可用的标准Clock
。
一个time_point
通常最初是从它的Clock
的类的成员中获得的。不过,它也可以由给定的Duration
构建而成。如果默认构造,它代表Clock
的纪元。几个算术(+
、-
、+=
、-=
)和比较(==
、!=
、<
、>
、<=
、>=
)再次可用。减去两个time_point
得到一个Duration
,并且Duration
可以被加到一个time_point
上或者从一个time_point
上减去。不允许将time_point
加在一起,也不允许从Duration
中减去 1:
具有不同Duration
类型的time_point
之间的转换类似于duration
的转换:只要不需要截断,就允许隐式转换;否则,可以使用time_point_cast()
:
auto one_hour = time_point_cast<hours>(sixty_minutes);
时钟
The std::chrono
名称空间提供了三种时钟类型:steady_clock
、system_clock
和high_resolution_clock
。所有时钟都定义了以下静态成员:
now()
:返回当前时间点的函数。rep
、period
、duration
、time_point
:具体实现类型。time_point
是now()
返回的类型:一个std::chrono::time_point
的实例化,其Duration
类型参数等于duration
,进而等于std::chrono::duration<rep, period>
。is_steady
:一个布尔常量,如果时钟滴答之间的时间是常数,并且连续两次调用now()
总是返回time_point
st1
和t2
,其中t1 <= t2
。
唯一保证稳定的时钟是steady_clock
。也就是说,这个时钟不能调整。另一方面,system_clock
对应于系统范围的实时时钟,通常可以由用户随意设置。最后,high_resolution_clock
是库实现支持的周期最短的时钟(可能是steady_clock
或system_clock
的别名)。
为了测量一个操作花费的时间,应该使用一个steady_clock
,除非你的实现的high_resolution_clock
是稳定的:
system_clock
应保留用于日历时间。因为<chrono>
在这方面的功能有些有限,所以这个时钟提供了静态函数来将其time_point
转换为time_t
对象,反之亦然(分别为to_time_t()
和from_time_t()
),然后可以与下一小节中讨论的 C 风格的日期和时间实用程序一起使用:
c 风格的日期和时间实用程序<ctime>
<ctime>
头定义了两种可互换的类型来表示日期和时间:time_t
,算术类型的别名(一般是 64 位有符号整数),以平台特定的方式表示时间;以及tm
,一个便携的struct
,带有这些字段:tm_sec
(范围 0
、60
,其中60
用于闰秒)、tm_min
、tm_hour
、tm_mday
(一个月中的某一天,范围[ 1
、31
)、tm_mon
(范围[ 0
、11
)、tm_year
(自 1900 年以来的年份)、tm_wday
(范围[ 0
、6
),带有)
以下功能可通过<ctime>
使用。本地时区由当前活动的 C 语言环境决定(语言环境在第 [6 章中解释):
请查阅您的实现文档,以获得更安全的localtime()
和gmtime()
的替代方案(例如 Windows 的localtime_s()
或 Linux 的localtime_r()
)。对于将日期和时间转换成字符串,首选的 C 风格函数是strftime()
(在本节的最后,我们指出了 C++ 风格的替代方法):
size_t strftime(char* result, size_t n, const char* format, const tm*);
在<cwchar>
中定义了转换为宽字符串(wchar_t
序列)、wcsftime()
的等价形式。这些函数将一个以null
结尾的字符序列写入result
,该字符序列必须指向一个预先分配的大小为n
的缓冲区。如果这个缓冲区太小,则返回零。否则,返回值等于写入的字符数,不包括终止字符null
。
指定所需文本表示的语法定义如下:format
字符串中的任何字符都被复制到result
,除了某些特殊说明符被替换,如下表所示:
许多说明符的结果,包括那些扩展为名称或首选格式的说明符,取决于活动的语言环境(参见第六章)。例如,当使用法语语言环境执行时,上一个示例的输出可能是“Today is mer. 21/11"
和"10/21/15 16:29:00--10/21/15 16:29:00”
。要使用依赖于语言环境的替代表示法(如果当前语言环境定义了替代表示法),C
、c
、X
、x
、Y
和y
前面可以有E
( %EC
、%Ec
等等);为了使用替代数字符号,d
、e
、H
、I
、M
、m
、S
、u
、U
、V
、W
、w
和y
可以用字母O
进行修改。
如第五章所述,C++ 库也提供了从/向流中读取/写入tm
的工具,即get_time()
和put_time()
。因此,<ctime>
中唯一一个 C 风格的函数是localtime()
(将system_clock
的time_t
转换成tm)
),你通常需要用 C++ 风格输出日历日期和时间。
c 风格文件实用程序<cstdio>
下一版本的 C++ 标准库有望包含一个更强大的 C++ 风格的文件系统库。目前,<cstdio>
头文件中有限的一组 C 风格函数是标准中唯一可用的可移植文件实用程序:
类型实用程序
运行时类型识别、
C++ typeid()
操作符用于获取一个值的运行时类型信息。它返回对在<typeinfo>
中定义的std::type_info
类的全局实例的引用。这些实例不能被复制,但是使用指向它们的引用或指针是安全的。使用它们的==, !=
和before()
成员可以进行比较,并且可以为它们计算一个hash_code()
。特别有趣的是name()
,它返回值的类型的特定于实现的文本表示:
打印的name()
可能类似于“std::basic_string<char, std::char_traits<char>, std::allocator<char>>”
”(见第六章),但对于其他实现,它也可能是“Ss”
”。
当用在指向派生类D
实例的B*
指针上时,如果B
是多态的,即至少有一个virtual
成员,那么typeid()
只给出动态类型D*
,而不是静态类型B*
。
因为type_info
s 不能被复制,所以它们不能直接用作第三章中关联数组的键。正是为了这个目的,<typeindex>
头定义了std::type_index
装饰器类:它模仿包装的type_info&
的接口,但是它是可复制的;有<
、<=
、>
、>=
操作符;并为其定义了一个专门化std::hash
。
类型特征<type_traits>
类型特征是一种构造,用于获取给定类型的编译时信息,或将一种或多种给定类型转换为相关类型。类型特征通常用于在编写泛型代码时检查和操作模板类型参数。
标题定义了许多特征。由于篇幅的限制,并且因为模板元编程是一个高级主题,本书无法对所有这些进行详细介绍。不过,我们提供了不同类型特征的简要参考,这对于基本的使用应该是足够的。
体型鉴定
C++ 中的每个类型都属于 14 个主要类型类别中的一个。除此之外,该标准还定义了几个复合类型类别,以便于引用属于两个或更多相关主要类别的所有类型。对于其中的每一个,存在一个类型特征struct
来检查给定的类型是否属于那个类别。它们的名称形式为is_
类别,类别等于图 2-1 中所示的名称之一。一个名为value
的特征的静态布尔包含了它的类型参数是否属于相应的类别。特征是返回和转换到这个value
的函子。下面是一些例子(代码指int main()
):
图 2-1。
Overview of the type classification traits. The second column lists the 14 primary categories; the other names are those of the composite categories.
类型属性
第二个类型特征系列用于静态查询类型的属性。它们的使用方式与前一小节中的使用方式完全相同,除了一个名称has_virtual_destructor
之外,所有的名称都采用了is_
属性的形式。
以下属性值用于检查指示的类型属性:
- 类型量词的存在:
const
和volatile
- 类的多态性属性:
polymorphic
(有虚拟成员);abstract
(纯虚拟成员)和final
- 算术类型的有符号性:
signed
(包括浮点数)和unsigned
(包括布尔值)
此外,还有一大类特征,其属性是具有指定参数类型的构造或赋值语句的有效性,或者销毁语句的有效性(总是省略is_
):
- 基本的有
constructible<T,Args...>
、assignable<T, Arg>
、destructible<T>
。所有标量类型都是可析构的,前两个属性对于非类类型也适用(因为像int i(0);
这样的构造是有效的)。 - 辅助特征用于检查默认结构(
default_constructible
)和复制/移动结构和赋值(copy_constructible<T> == constructible<T, const T&>
等)的有效性。 - 所有先前的属性名称可以进一步以
trivially
或nothrow
为前缀。比如:trivially_destructible
、nothrow_constructible
或者nothrow_move_assignable
。
如果静态地知道构造、赋值或销毁永远不会抛出,那么nothrow
属性就成立。如果类型是scalar
或者这个操作是默认操作的非多态类(也就是说,不是由用户指定的),则trivial
属性成立,trivial
属性也适用于它的所有基类和非静态成员变量。对于普通的可构造属性,类也不允许有任何带有类内初始值设定项的非静态数据成员。
属性值的最终列表在以下条件下基本成立。满足这些条件的类型数组也具有相同的属性:
trivially_copyable
,如果trivially_destructible
和trivially_(copy|move)_(constructible
|assignable)
都成立。像std::memcpy()
这样的按位复制函数被定义为对trivially_copyable
类型是安全的。trivial
、iftrivially_default_constructible
和trivially_copyable
,并且不存在非默认构造函数。standard_layout
、ifscalar
或者一个类,对于该类,指向该类的指针可以安全地转换为指向其第一个非静态成员类型的指针(也就是说,没有多态、有限的多重继承等等)。这是为了与 C 兼容,因为这种强制转换(用 Cstruct
s)在 C 代码中是常见的做法。pod
(普通旧数据),如果trivial
和standard_layout
。literal_type
,if 值可能用在constexpr
表达式中(即可以静态求值,没有副作用)。empty
,对于没有非静态成员变量的非多态类。
类型属性查询
类型特征的value
不总是布尔值。对于以下特征,它包含指定的size_t
类型属性:
std::alignment_of<T>
:操作符alignof(T)
的值std::rank<T>
:数组维数,如rank<int>() == 0
、rank<int[]>() == 1
、rank<int[][5][6]>() == 3
等std::extent<T,N=0>
:第N
个数组维度的元素个数,如果未知或无效则为 0;比如extent<int[]>() == 0
和extent<int[][5][6], 1>() == 5
。
类型比较
这三个类型特征比较类型:is_same<T1, T2>
、i s_base_of<Base, Derived>
和is_convertible<From, To>
(使用隐式转换)。
类型转换
大多数类型转换特征也非常相似,除了它们没有value
,而是一个名为type
的嵌套typedef
:
std::add_x
同x``const``volatile``cv
(const``volatile
)pointer``lvalue_reference``rvalue_reference
其中之一。std::remove_x``x``const``volatile``cv``pointer``reference
(左值或右值)extent``all_extents
其中之一。除了最后一种情况,只有顶层/第一个类型修饰符被删除。比如:remove_extent<int[][5]>::type == int[5]
。std::decay<T>
:将T
转换为相关的type
,可以通过值存储,模拟通过值传递参数。比如数组类型int[5]
变成指针类型int*
,函数变成函数指针,const
和volatile
被剥离,等等。一个可能的实现作为一个例子简短地示出。std::make_y
同y``signed
或unsigned
。如果应用于整数类型T
,type
分别是带符号或不带符号的整数类型sizeof(type) == sizeof(T)
。- 仅针对
functional
类型定义的std::result_of
,给出函数的返回type
。 - 仅针对
enum
类型定义的std::underlying_type
,给出了这个enum
下面的【整数】type
。 std::common_type<T...>
有一个type
所有类型T
都可以隐式转换为。
这个头还包含两个实用特征来帮助类型元编程。通过几个例子来说明它们的基本用途。
- 如果
constexpr B
评估为true
则std::conditional<B,T1,T2>
有type T1
,否则type T2
。 std::enable_if<B,T=void>
有type T
,但前提是constexpr B
求值为true
。否则,type
就没有定义。
对于该子部分的所有特征,名为std::
特征_t<T>
的便利类型被定义为std::
特征<T>::type
。例如,接下来的例子显示了与完整表达式相比,enable_if_t<>
是多么方便。
第一个例子展示了如何使用 C++ SFINAE 习惯用法在重载决策中有条件地添加或删除函数。SFINAE 是替换失败不是错误的首字母缩写,它利用了这样一个事实,即未能专门化模板并不构成编译错误。在这种情况下,缺少type typedef
会导致替换失败:
第二个例子显示了根据std::conditional
元函数的std::decay
转换特征的可能实现。后者用于在类型层面上基本形成一个if
- else if
- else
结构;
using namespace std;
template<typename T> struct my_decay {
private:
typedef remove_reference_t<T> U;
public:
typedef conditional_t<is_array<U>::value, remove_extent_t<U>*,
conditional_t<is_function<U>::value, add_pointer_t<U>,
remove_cv_t<U>>> type;
};
Footnotes 1
<functional>
包含许多我们没有讨论的不推荐使用的设施:ptr_fun()
、mem_fun()
、mem_fun_ref()
、bind1st()
和bind2nd()
,加上它们的返回类型,以及基类unary_function
和binary_function
。所有这些都已经从标准的 C++17 版本中删除,不应该使用。
三、容器
C++ 标准库提供了一系列不同的数据结构,称为容器,可以用来存储数据。容器与算法协同工作,如第四章所述。容器和算法是以这样一种方式设计的,它们不需要相互了解。它们之间的交互是通过迭代器完成的。所有容器都提供迭代器,算法只需要迭代器就能执行工作。
本章首先解释迭代器的概念,然后描述所有容器。因为这本书是快速参考,所以不可能深入讨论所有的容器。与其他容器相比,std::vector
容器的解释更加详细。一旦你知道如何使用一个容器,你就知道如何使用其他容器。
迭代程序
迭代器是容器和算法之间的粘合剂。它们提供了一种以统一的方式枚举容器中所有元素的方法,而不必知道容器的任何细节。下面的列表简要地提到了标准提供的最重要的迭代器类别,后面的表格解释了所有可能的操作:
- Forward (F):支持正向迭代的输入迭代器
- 双向(B):可以向前和向后移动的前向迭代器
- Random (R):一个双向迭代器,支持跳转到任意索引的元素
下表中,T
是迭代器类型,a
和b
是T
的实例,t
是T
指向的类型的实例,n
是整数。
由此可见,随机迭代器和 C++ 指针非常相似。事实上,指向常规 C 风格数组的指针满足了随机迭代器的所有要求,因此也可以用于第四章中的算法。同样,某些容器,尤其是顺序容器,可能将它们的迭代器定义为常规指针的typedef
。然而,对于更复杂的数据结构,这是不可能的,迭代器是作为小类实现的。
所有标准库兼容容器必须提供一个iterator
和const_iterator
成员类型。此外,支持反向迭代的容器必须提供reverse_iterator
和const_reverse_iterator
成员类型。例如,整数的vector
的反向迭代器类型是std::vector<int>::reverse_iterator
。
迭代器标签
迭代器标签是一种空类型,用于区分前面提到的不同迭代器类别。该标准为以下类别值定义了std::
类别_iterator_tag
类型:forward
、bidirectional
、random_access
。类型特征表达式std::iterator_traits<Iter>::iterator_category
计算给定迭代器类型Iter
的迭代器标签类型。这可以被通用算法用来根据迭代器参数的类别优化它们的实现。例如,在下一节中解释的std::distance()
方法使用迭代器标签在线性计算两个迭代器之间距离的实现和简单减去两个迭代器的更有效的实现之间进行选择。
如果你实现了你自己的迭代器,你应该指定它的标签。你可以通过添加一个typedef
Tag
iterator_category
到你的实现中,其中Tag
是迭代器标签之一,或者通过为你的类型专门化std::iterator_traits
来提供正确的标签类型。
获取迭代器的非成员函数
所有容器都支持返回各种迭代器的成员函数。然而,该标准还提供了非成员函数,可用于获得这种迭代器。此外,这些非成员函数在容器、C 风格数组和initializer_lists
上的工作方式是一样的。提供的非成员函数如下:
解引用由const
版本返回的迭代器,也称为常量迭代器,会导致const
引用,因此不能用于修改容器或数组中的元素。反向迭代器允许以相反的顺序遍历容器的元素:从最后一个元素开始,到第一个元素。当你增加一个反向迭代器时,它实际上会移动到底层容器中的前一个元素。
下面是一个如何在 C 风格数组中使用这种非成员函数的示例:
int myArray[] = { 1,2,3,4 };
auto beginIter = std::cbegin(myArray);
auto endIter = std::cend(myArray);
for (auto iter = beginIter; iter != endIter; ++iter) {
std::cout << *iter << std::endl;
}
然而,我们建议您使用基于范围的for
循环来遍历 C 风格数组或标准库容器的所有元素。它更短、更清晰。例如:
int myArray[] = { 1,2,3,4 };
for (const auto& element : myArray) {
std::cout << element << std::endl;
}
但是,您不能总是使用基于范围的for
循环版本。例如,如果您想循环遍历元素并删除其中一些元素,那么您需要迭代器版本。
迭代器上的非成员操作
以下非成员操作用于对所有类型的迭代器执行随机访问操作。当在不支持随机访问的迭代器上调用时(也参见前面的内容),实现会自动退回到适用于该迭代器的方法(例如,线性遍历):
std::distance(iter1, iter2)
:返回两个迭代器之间的距离。std::advance(iter, dist)
:将迭代器向前移动给定的距离,不返回任何内容。如果迭代器是双向的或随机访问的,距离可以是负的。std::next(iter, dist)
:相当于advance(iter, dist)
,返回iter
。std::prev(iter, dist)
:相当于advance(iter, -dist)
,返回iter
。仅适用于双向和随机访问迭代器。
顺序容器
以下部分描述了五个连续的容器:vector
、deque
、array
、list
和forward_list
。最后是这些容器支持的所有可用方法的参考。
标准::矢量<vector>
一个vector
在内存中连续存储它的元素。它类似于堆分配的 C 风格数组,除了它更安全和更容易使用,因为vector
自动释放它的内存并增长以容纳新元素。
建筑
像所有标准的库容器一样,vector
是基于存储在其中的对象类型的模板。下面这段代码显示了如何定义整数的vector
:
std::vector<int> myVector;
可以使用有支撑的初始化器来指定初始元素:
std::vector<int> myVector1 = { 1,2,3,4 };
std::vector<int> myVector2{ 1,2,3,4 };
也可以构造一个有一定大小的vector
。例如:
std::vector<int> myVector(100, 12);
这将创建包含值为12
的100
元素的myVector
。第二个参数是可选的。如果你省略它,新元素被零初始化,这是整数情况下的0
。
迭代程序
支持随机访问迭代器。使用begin()
或cbegin()
成员获得一个非const
或const
迭代器,指向vector
中的第一个元素。end()
和cend()
方法用于获取一个迭代器,使其超过最后一个元素。rbegin()
和crbegin()
返回一个反向迭代器到最后一个元素,rend()
和crend()
返回一个反向迭代器到第一个元素之前。
和往常一样,也可以使用前面解释过的等价非成员函数,比如std::begin()
、std::cbegin()
等等。
访问元素
可以使用operator[]
访问vector
中的元素,它返回对特定的从零开始的索引处的元素的引用,使其行为与 C 风格的数组完全一样。例如:
使用operator[]
时不执行边界检查。如果需要边界检查,使用at()
方法,如果给定的索引超出边界,该方法会抛出std::out_of_range
异常。
front()
可以用来获取对第一个元素的引用,back()
返回对最后一个元素的引用。
添加元素
向vector
添加元素的一种方法是使用push_back()
。例如,将两个整数加到myVector
上可以如下完成:
std::vector<int> myVector;
myVector.push_back(11);
myVector.push_back(2);
另一种选择是使用insert()
方法,这需要一个迭代器来定位应该插入新元素的位置。例如:
就像任何修改操作一样,插入通常会使现有的迭代器失效。所以在循环中插入时,应该使用下面的习惯用法:
这是可行的,因为insert()
返回一个指向插入元素的有效迭代器(更一般地,指向第一个插入元素,稍后讨论)。如果使用循环,确保不缓存结束迭代器,因为insert()
可能会使它无效。
insert()
也可以用来在vector
的任何地方插入一系列元素,或者连接(附加)两个向量。使用insert()
时,您不必自己调整vector
的大小。例如:
insert()
的两个额外的重载提供了初始化列表的插入或者某个元素的给定数量的副本。使用与之前相同的v1
:
除了构造一个新元素然后将其传递给insert()
或push_back()
之外,元素也可以使用就位方法就地构造,例如emplace()
或emplace_back()
。前者emplace()
,是单元素insert()
的对应,后者push_back()
。假设你有一个vector
的Person
对象。你可以用这两种相似的方法在后面添加一个新的人:
persons.push_back(Person("Sheldon", "Cooper"));
persons.emplace_back("Leonard", "Hofstadter");
定位函数的参数被完美地转发给元素的构造函数。如前例所示,如果避免产生临时物体,安放通常会更有效。如果复制的代价很高,或者如果不能复制的话,这可能是添加元素的唯一方式,那么这就特别有趣了。
另外,容器的添加和插入成员通常完全支持将元素移动到容器中,这也是为了避免创建不必要的副本(移动语义在第二章中解释)。例如:
Person person("Howard", "Wolowitz");
persons.push_back(std::move(person));
尺寸和容量
一个vector
有一个大小,由size()
返回,是vector
中包含的元素个数。使用empty()
检查vector
是否为空。但是,注意不要混淆empty()
和clear()
:前者返回一个布尔值,后者删除所有元素。
一个vector
可以用resize()
调整大小。例如:
std::vector<int> myVector;
myVector.resize(100, 12);
这将vector
的大小设置为 100 个元素。如果必须创建新元素,它们用 12 初始化。第二个参数也是可选的;省略时,新元素从零开始初始化。
一个vector
除了大小,还有一个容量,由capacity()
返回。容量是它可以存储的元素总数(包括已经在vector
中的元素),而不必分配更多的内存。如果添加的元素比容量允许的多,vector
必须执行重新分配,因为它需要在内存中连续存储所有元素。重新分配意味着分配一个新的、更大的内存块,并且vector
中的所有当前元素都被转移到新的位置(如果支持移动并且知道不要抛出,则它们被移动;否则它们被复制;参见第二章。
如果您知道要添加多少元素,那么预先分配足够的容量以避免重新分配对性能至关重要。如果不这样做,将会导致严重的性能下降。这可以通过使用reserve()
来完成:
myVector.reserve(100);
注意,这没有为 100 个额外的元素预留容量;它只是确保myVector
的总容量至少为 100。为非空的vector
预留容量以存储 100 个额外的元素应该按如下方式进行:
myVector.reserve(myVector.size() + 100);
移除元素
使用pop_back()
可以删除vector
中的最后一个元素,使用erase()
可以删除其他元素。erase()
有两种过载:
erase(iter)
:删除给定迭代器指向的元素erase(first, last)
:删除两个迭代器给出的元素范围,所以[first
,last
]
当您删除元素时,vector
的大小会改变,但其容量不会改变。如果想回收未使用的内存,可以使用shrink_to_fit()
。不过,这只是一个提示,实现可能会忽略它:例如,出于性能原因。
要删除所有元素,使用clear()
。这同样不会影响容量。在保证回收内存的同时清空容器的一个经典习惯用法是与空容器交换:
先前空的容器随后被销毁,其中包含所有元素,剩下原来的容器为空。这个成语也经常被更简短地写成如下:
删除-擦除习惯用法
如果您需要从一个vector
中删除一些元素,您可以编写自己的循环来迭代所有的元素。下面的示例从 vector 中删除所有等于 2 的元素:
如果你使用了这里显示的循环,确保你没有缓存结束迭代器,因为erase()
会使它无效。为了避免这样或那样的错误,我们总是建议您使用标准算法,而不是手写的循环。当您想要删除多个元素时,您可以使用删除-擦除习惯用法。这种模式首先使用std::remove()
或std::remove_if()
算法。正如第四章所解释的,这些算法实际上并不删除元素。相反,它们将所有需要保留的元素移至开头,保持这些元素的相对顺序。该算法返回一个迭代器,使其超过最后一个要保留的元素。下一步通常是调用容器上的erase()
来真正删除从remove()
或remove_if()
返回的迭代器开始到结束的元素。例如:
对第二行中的remove()
的调用将所有元素移向vector
的开头。根据编译器的不同,其他元素(即要移除的元素)的内容可能会有所不同。
之前的remove()
和erase()
通话也可以合并成一条线路:
vec.erase(std::remove(begin(vec), end(vec), 2), end(vec));
Caution
在 remove-erase 习惯用法中,不要忘记将结束迭代器指定为erase()
的第二个参数,就像前面例子中用粗体标记的那样。否则只会删除一个元素!
标准::矢量
vector<bool>
是vector<T>
对布尔元素的特化。它允许 C++ 标准库实现以节省空间的方式存储布尔值,但这不是必需的。它与vector<T>
有相同的接口,增加了一个flip()
方法来翻转vector<bool>
中的所有位。
这种专门化与后面讨论的std::bitset
相似。区别在于bitset
的大小是固定的,而vector<bool>
可以根据需要动态增减。
建议使用vector<bool>
和bitset
只是为了节省内存;否则,使用vector<std::uint_fast8_t>
:这通常在访问、遍历或赋值时有更好的性能。
复杂性
对vector
的常见操作的复杂程度如下:
- 插入:末尾摊销常数 O(1);否则从插入点到向量末尾的距离为线性,O(N)
- 删除:O(1)在末端,否则在到向量 O(N)末端的距离上是线性的
- 访问:O(1)
尽管后面讨论的list
和forward_list
在理论上有更好的插入和删除复杂度,但是vector
在实践中通常更快,因此应该是默认的顺序容器。如果有疑问,请始终使用分析器来比较它们在您的应用程序中的性能。
STD::deque
一个deque
是一个双端队列,一个类似于vector
的容器,支持在开始和结束时的高效插入和删除。该标准不要求deque
元素在内存中连续存储,因此由deque
完成的重新分配可能比由vector
完成的更便宜。然而,deque
支持随机访问和随机访问迭代器。
deque
上的操作与vector
上的操作几乎相同,只有一些细微的差别。deque
没有容量的概念,因为它不需要连续存储它的元素,所以与容量相关的方法都不可用。而且,deque
除了push_back()
和pop_back()
之外,还提供了push_front()
和pop_front()
。
下面是一个使用deque
的例子:
复杂性
对deque
的常见操作的复杂程度如下:
- 插入:期初和期末的摊余常数 O(1);否则从插入点到起点或终点 O(N)的距离是线性的
- 删除:O(1)在开头或结尾;否则在到 O(N)的开始或结束的距离上是线性的
- 访问:O(1)
标准::数组<array>
一个array
是一个具有固定大小的容器,在编译时被指定为模板参数,支持随机访问迭代器。对于一个array
,size()
和max_size()
返回相同的结果。
下面定义了一个由三个整数组成的数组:
std::array<int, 3> myArray;
这些整数没有初始化。这与所有其他容器不同,默认情况下,这些容器对其元素进行零初始化。这是因为std::array
被设计成尽可能接近 C 数组。当然,你也可以在定义一个array
的时候初始化元素。初始化值的数量必须等于或小于array
的大小。如果指定更多的值,会出现编译错误。没有指定值的元素被初始化为零。例如:
这也意味着下面的零初始化所有元素:
有一个特殊的方法,fill()
,用某个值填充array
。例如:
对于数组,这可能比第四章中解释的通用std::fill()
算法更有效。
复杂性
- 插入:不可能
- 删除:不可能
- 访问:O(1)
std::list 和 std::forward_list ,<forward_list></forward_list>
A list
将其元素存储为双向链表,而 a forward_list
将其存储为单向链表。因此,两者都在内存中不连续地存储元素。
第一个缺点是随机访问因此在恒定时间内是不可能的。正因为如此,不支持operator[]
。要访问一个特定的元素,你必须使用迭代器执行线性搜索。list
支持双向迭代器,可以从开头开始,也可以从结尾开始;forward_list
只支持正向迭代器,所以你总是需要从头开始。但是,一旦您在容器中的正确位置,在该位置的插入和删除是有效的,因为它们只需要修改几个链接。
第二个缺点是,元素可能会分散在内存中,这不利于局部性,并且会由于缓存未命中次数的增加而影响性能。
Tip
由于前面提到的缺点,如果剖析器显示对于您的用例来说使用list
或forward_list
比使用vector
更有效,那么只使用list
或forward_list
。
list
和forward_list
支持的操作与vector
类似,略有不同。list
或forward_list
没有容量,因此不支持任何与容量相关的方法。两者都支持front()
,返回对第一个元素的引用。一个list
也支持back()
返回对最后一个元素的引用。
复杂性
list
和forward_list
都有相似的复杂性:
- 插入:O(1)一旦你在正确的位置
- 删除:O(1)一旦你在正确的位置
- Access: O(1)访问第一个(对于
list
和forward_list
)或最后一个(仅对于list
)元素;否则为 O(N)
列表特定算法
由于list
和forward_list
存储元素的方式,它们提供了几个实现特定算法的成员函数。下表列出了为list
(L)和forward_list
(F)提供的算法:
对于除splice()
和splice_after()
之外的所有算法,通用版本均可用,详见第四章。这些通用版本适用于所有类型的容器,但是列表容器提供了更有效的特殊实现。
下面是使用这些列表算法的一个例子:
顺序容器参考
以下各小节对vector
(V)deque
(D)array
(A)list
(L)forward_list
(F)支持的所有操作进行了分类概述。
迭代程序
| 操作 | V | D | A | L | F | 描述 | | --- | --- | --- | --- | --- | --- | --- | | `begin()` `end()` | ■和 | ■和 | ■和 | ■和 | ■和 | 返回第一个元素或最后一个元素之后的迭代器 | | `cbegin()` `cend()` | ■和 | ■和 | ■和 | ■和 | ■和 | `begin()`和`end()`的`const`版本 | | `rbegin()` `rend()` | ■和 | ■和 | ■和 | ■和 | □ | 返回最后一个元素或第一个元素之前的一个元素的反向迭代器 | | `crbegin()` `crend()` | ■和 | ■和 | ■和 | ■和 | □ | `rbegin()`和`rend()`的`const`版本 | | `before_begin()` | □ | □ | □ | □ | ■和 | 将迭代器返回到由`begin()`返回的元素之前的元素 | | `cbefore_begin()` | □ | □ | □ | □ | ■和 | `before_begin()`的`const`版本 |尺寸和容量
| 操作 | V | D | A | L | F | 描述 | | --- | --- | --- | --- | --- | --- | --- | | `size()` | ■和 | ■和 | ■和 | ■和 | □ | 返回元素的数量 | | `max_size()` | ■和 | ■和 | ■和 | ■和 | ■和 | 返回容器中可以存储的最大元素数量 | | `resize()` | ■和 | ■和 | □ | ■和 | ■和 | 调整容器的大小 | | `empty()` | ■和 | ■和 | ■和 | ■和 | ■和 | 如果容器是空的,返回`true`,否则返回`false` | | `capacity()` | ■和 | □ | □ | □ | □ | 返回容器的当前容量 | | `reserve()` | ■和 | □ | □ | □ | □ | 储备能力 | | `shrink_to_fit()` | ■和 | ■和 | □ | □ | □ | 提示减少容器的容量以匹配其大小 |接近
| 操作 | V | D | A | L | F | 描述 | | --- | --- | --- | --- | --- | --- | --- | | `operator[]` | ■和 | ■和 | ■和 | □ | □ | 返回给定索引位置的元素的引用。不对索引执行边界检查。 | | `at()` | ■和 | ■和 | ■和 | □ | □ | 返回给定索引位置的元素的引用。如果给定的索引位置超出界限,就会抛出一个`std::out_of_range`异常。 | | `data()` | ■和 | □ | ■和 | □ | □ | 返回一个指向`vector`或`array`数据的指针。这有助于将数据传递给传统的 C 风格数组 API。在旧代码中,你经常会看到等价的`&myContainer[0]`。 | | `front()` | ■和 | ■和 | ■和 | ■和 | ■和 | 返回对第一个元素的引用。空容器上未定义的行为。 | | `back()` | ■和 | ■和 | ■和 | ■和 | □ | 返回对最后一个元素的引用。空容器上未定义的行为。 |修饰语
| 操作 | V | D | A | L | F | 描述 | | --- | --- | --- | --- | --- | --- | --- | | `assign()` | ■和 | ■和 | ■和 | ■和 | ■和 | 用给定值的 N 个副本,或给定范围的元素的副本,或给定`initializer_list`的元素替换容器的内容 | | `clear()` | ■和 | ■和 | □ | ■和 | ■和 | 删除所有元素;大小变为零。 | | `emplace()` | ■和 | ■和 | □ | ■和 | □ | 在给定迭代器指向的元素之前的位置构造一个新元素。迭代器参数后面是零个或多个参数,这些参数将被转发给元素的构造函数。 | | `emplace_back()` | ■和 | ■和 | □ | ■和 | □ | 在末尾的适当位置构造一个新元素。 | | `emplace_after()` | □ | □ | □ | □ | ■和 | 在现有元素之后就地构造一个新元素。 | | `emplace_front()` | □ | ■和 | □ | ■和 | ■和 | 在开始的地方构造一个新元素。 | | `erase()` | ■和 | ■和 | □ | ■和 | □ | 擦除元素。 | | `erase_after()` | □ | □ | □ | □ | ■和 | 删除现有迭代器位置后的元素。 | | `fill()` | □ | □ | ■和 | □ | □ | 用给定元素填充容器。 | | `insert()` | ■和 | ■和 | □ | ■和 | □ | 在给定迭代器指向的元素前插入一个或多个元素。 | | `insert_after()` | □ | □ | □ | □ | ■和 | 在给定迭代器指向的元素后插入一个或多个元素。 | | `push_back()` `pop_back()` | ■和 | ■和 | □ | ■和 | □ | 在末尾添加一个元素,或者分别移除最后一个元素。 | | `push_front()` `pop_front()` | □ | ■和 | □ | ■和 | ■和 | 在开头添加一个元素,或者移除第一个元素。 | | `swap()` | ■和 | ■和 | ■和 | ■和 | ■和 | 在常数时间内交换两个容器的内容,除了`array` s,它需要线性时间。 |非成员函数
所有顺序容器都支持以下非成员函数:
| 操作 | 描述 | | --- | --- | | `==`、`!=`、`<`、`<=`、`>`、`>=` | 比较两个容器中的值(按字典顺序) | | `std::swap()` | 交换两个容器的内容 |<array>
头定义了一个额外的非成员函数std::get<Index>()
,以及助手类型std::tuple_size
和std::tuple_element
,它们等同于在第二章中解释的为元组和对定义的相同函数和类型。
std::bitset <bitset>
bitset
是存储固定位数的容器。位数被指定为模板参数。例如,以下代码创建了一个 10 位的bitset
,全部初始化为 0:
std::bitset<10> myBitset;
可以通过向构造函数传递一个整数或者传入位的字符串表示来初始化各个位的值。例如:
std::bitset<4> myBitset("1001");
一个bitset
可以用to_ulong()
、to_ullong()
和to_string()
转换成整数或字符串。
复杂性
- 插入:不可能
- 删除:不可能
- 访问:O(1)
参考
接近
| 操作 | 描述 | | --- | --- | | `all()``any()` | 如果所有位、至少一个位或没有位被置位,则返回`true`。 | | `count()` | 返回设置的位数。 | | `operator[]` | 访问给定索引处的位。不执行边界检查。 | | `test()` | 访问给定索引处的位。如果给定的索引超出界限,抛出`std::out_of_range`。 | | `==, !=` | 如果两个`bitset`相等或不相等,则返回`true`。 | | `size()` | 返回位集可以容纳的位数。 | | `to_string()``to_ulong()` | 将一个`bitset`分别转换为一个`string`、`unsigned long`或`unsigned long long`。 |操作
| 操作 | 描述 | | --- | --- | | `flip()` | 翻转所有位的值 | | `reset()` | 将所有位或特定位置的位设置为`false` | | `set()` | 将所有位设置为`true`或将特定位置的位设置为特定值 |另外,bitset
支持所有的按位运算符:∼
、&
、&=
、^
、^=
、|
、|=
、<<
、<<=
、>>
、>>=
。
容器适配器
容器适配器构建在其他容器之上,以提供不同的接口。它们阻止您直接访问底层容器,并强迫您使用它们的特殊接口。接下来的三个小节给出了可用容器适配器的概述— queue
、priority_queue
和stack
—后面的一个小节给出了一个例子和一个参考小节。
STD::queue<queue>
一个queue
表示一个具有先进先出(FIFO)语义的容器。你可以把它比作夜总会的排队。在你之前到达的人将被允许在你之前进入。
一个queue
需要访问前端和后端,所以底层容器必须支持back()
、front()
、push_back()
和pop_front()
。标准的list
和deque
支持这些方法,可以用作底层容器。默认的容器是deque
。下面是queue
的模板定义:
template<class T, class Container = std::deque<T>>
class queue;
queue
的复杂度如下:
- 插入:
list
的 O(1)作为底层容器;deque
的摊销 O(1) - 删除:
list
和deque
的 O(1)作为底层容器 - 进入:不可能
标准::优先级 _ 队列<queue>
一个priority_queue
类似于一个queue
,但是根据优先级存储元素。优先级最高的元素位于队列的最前面。在夜总会的情况下,贵宾会员获得更高的优先权,并被允许在非贵宾之前进入。
A priority_queue
需要对底层容器进行随机访问,并且只需要能够在后面修改容器,而不是在前面。因此,底层容器必须支持随机访问、front()
、push_back()
和pop_back()
。vector
和deque
是可用选项,vector
是默认的底层容器。下面是priority_queue
的模板定义:
template<class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>>
class priority_queue;
为了确定优先级,使用被指定为Compare
模板类型参数的类型的仿函数对象来比较元素。默认情况下,这是std::less
,在第二章中有解释,除非特别说明,否则它会转发到元素类型T
的operator<
。一个Compare
实例可以选择性地提供给priority_queue
构造函数;如果没有,则默认构造一个。
priority_queue
的复杂度如下:
- 插入:作为底层容器的
vector
或deque
的摊销 O(log(N)) - 删除:
vector
和deque
作为底层容器的 O(log(N)) - 进入:不可能
标准::堆栈<stack>
一个stack
表示一个具有后进先出(LIFO)语义的容器。你可以把它比作自助餐厅里的一堆盘子。在顶部添加板块,向下推动其他板块。顾客从顶部拿走一个盘子,这是堆叠中最后添加的盘子。
为了实现 LIFO 语义,stack
要求底层容器支持back()
、push_back()
和pop_back()
。vector
、deque
和list
是底层容器的可用选项,deque
是默认选项。下面是stack
的模板定义:
template<class T, class Container = std::deque<T>>
class stack;
stack
的复杂度如下:
- 插入:
list
的 O(1)作为底层容器,vector
和deque
的摊销 O(1) - 删除:
list
、vector
和deque
作为底层容器的 O(1) - 进入:不可能
例子
以下示例演示了如何使用容器适配器。代码后的表格显示了当容器cont
分别被定义为queue
、priority_queue
或stack
时程序的输出:
参考
| 操作 | 描述 | | --- | --- | | `emplace()` | Queue:在后面的地方构造一个新元素。优先级队列:就地构造一个新元素。Stack:在顶部构建一个新元素。 | | `empty()` | 如果为空,则返回`true`,否则返回`false`。 | | `front()` `back()` | Queue:返回对第一个或最后一个元素的引用。优先级队列:不适用堆栈:不适用 | | `pop()` | Queue:从队列中移除第一个元素。优先级队列:删除优先级最高的元素。Stack:移除顶部元素。 | | `push()` | Queue:在队列后面插入一个新元素。优先级队列:插入新元素。Stack:在顶部插入新元素。 | | `size()` | 返回元素的数量。 | | `swap()` | 交换两个队列或堆栈的内容。 | | `top()` | Queue: n/a Priority queue:返回对具有最高优先级的元素的引用。Stack:返回对顶部元素的引用。 |queue
和stack
支持与顺序容器相同的一组非成员函数:==
、!=
、<
、<=
、>
、>=
、std::swap(). priority_queue
只支持std::swap()
非成员函数。
有序关联容器
标准::地图和标准::多重地图<map>
一个map
是存储键值pair
的数据结构,使用在第二章中解释的pair
实用程序类。元素根据键进行排序。也就是说,当遍历一个有序关联容器中包含的所有元素时,它们是按照键值递增的顺序被枚举的,而不是按照这些元素被插入的顺序。对于一个map
,不能有重复的键,而一个multimap
支持重复的键。
当定义一个map
时,您需要指定键类型和值类型。你可以立即用一个支撑初始化器初始化一个map
:
std::map<Person, int> myMap{ {Person("Jenne"), 1}, {Person("Bart"), 2} };
一个map<Key,Value>
或multimap<Key,Value>
的迭代器是双向的,指向一个pair<Key,Value>
。例如:
operator[]
可以用来访问map
中的元素。如果请求的元素不存在,它是默认构造的,因此它也可用于插入元素:
myMap[Person("Peter")] = 3;
您可以使用insert()
向map
添加更多元素:
myMap.insert(std::make_pair(Person("Marc"), 4));
insert()
方法有几种版本:
- 插入给定的键值
pair
。返回一个pair
,迭代器指向插入的元素(一个键-值对)或者已经存在的元素,如果插入了新元素,返回一个布尔值true
,否则返回false
。
std::pair<iterator, bool> insert(pair)
- 插入给定的键值对。实现可以使用给定的提示来开始搜索插入位置。返回一个迭代器,指向插入的元素或阻止插入的元素。
iterator insert(iterHint, pair)
- 插入范围[
iterFirst
,iterLast
]中的键值对。
void insert(iterFirst, iterLast)
- 从给定的
initializer_list
插入键值对。
void insert(initializerList)
还有一个emplace()
方法,允许您就地构造一个新的键值对。它返回一个类似于前面列表中第一个insert()
方法的pair<iterator, bool>
。例如:
myMap.emplace(Person("Anna"), 4);
然而,为了避免创建所有的临时对象,你必须使用所谓的分段构造,正如在第 2 一章的pair
一节中所解释的:
myMap.emplace(std::piecewise_construct,
std::forward_as_tuple("Anna"), std::forward_as_tuple(4));
std::set 和 std::multiset <set>
一个set
类似于一个map
,但是它不存储对,只存储没有值的唯一键(这是标准对它的定义,我们也将这样定义:有些人可能更愿意认为它是没有键的值)。一个multiset
支持重复键。
只有一个模板类型参数:键类型。insert()
方法采用单个键,而不是一个pair
。例如:
insert()
有类似于map
和multimap
的过载。
set
或multiset
的迭代器是双向的,指向实际的键,而不是像map
和multimap
那样指向pair
。键总是排序的。
搜索
如果您想知道某个键是否在关联容器中,您可以使用这些:
find()
:返回一个迭代器到找到的元素(映射的键值对),如果没有找到给定的键,则返回结束迭代器。count()
:返回与给定键匹配的键的个数。对于map
或set
,只能是 0 或 1,而对于multimap
或multiset
,可以大于 1。
元素的顺序
有序关联容器以有序的方式存储它们的元素。默认情况下,std::less<Key>
用于这种排序,除非特别指定,否则它依赖于Key
类型的operator<
。您可以通过指定一个Compare
模板类型参数来更改比较仿函数类型。除非将一个具体的Compare
仿函数实例传递给容器的构造函数,否则它是默认构造的。以下是所有有序关联容器的更完整的模板定义:
template<class Key, class Value, class Compare = std::less<Key>>
class map;
template<class Key, class Value, class Compare = std::less<Key>>
class multimap;
template<class Key, class Compare = std::less<Key>>
class set;
template<class Key, class Compare = std::less<Key>>
class multiset;
Tip
与有序关联容器一起使用的首选函子是所谓的透明运算符函子(参见第二章),例如std::less<>
(是std::less<void>
的缩写),因为这可以提高异构查找的性能。一个经典的例子是用字符串查找std::string
键:std::less<>
,然后避免创建临时的std::string
对象。例如,带有string
键和一个透明运算符的set
声明如下:std::set<std::string, std::less<>> mySet;
。
复杂性
所有四个有序关联容器的复杂性是相同的:
- 插入:O(log(N))
- 删除:O(log(N))
- 访问:O(log(N))
参考
以下小节按类别概述了map
(M)、multimap
(MM)、set
(S)和multiset
(MS)支持的所有操作。
迭代程序
所有有序关联容器都支持与vector
容器相同的一组迭代器相关方法:begin()
、end()
、cbegin()
、cend()
、rbegin()
、rend()
、crbegin()
和crend()
。
大小
所有关联容器都支持以下方法:
| 操作 | 描述 | | --- | --- | | `empty()` | 如果容器是空的,返回`true`,否则返回`false` | | `max_size()` | 返回可以存储的最大元素数 | | `size()` | 返回元素的数量 |访问和查找
| 操作 | M | 梅智节拍器 | S | 女士 | 描述 | | --- | --- | --- | --- | --- | --- | | `at()` | ■和 | □ | □ | □ | 返回具有给定键的元素的引用。如果给定的键不存在,抛出一个`std::out_of_range`异常。 | | `operator[]` | ■和 | □ | □ | □ | 返回具有给定键的元素的引用。如果一个元素还不存在,它默认用给定的键构造一个元素。 | | `count()` | ■和 | ■和 | ■和 | ■和 | 返回与给定键匹配的元素数量。 | | `find()` | ■和 | ■和 | ■和 | ■和 | 查找与给定键匹配的元素。 | | `lower_bound()` | ■和 | ■和 | ■和 | ■和 | 返回第一个元素的迭代器,该元素的键不小于给定的键。 | | `upper_bound()` | ■和 | ■和 | ■和 | ■和 | 返回第一个元素的迭代器,该元素的键大于给定的键。 | | `equal_range()` | ■和 | ■和 | ■和 | ■和 | 以一对迭代器的形式返回与给定键匹配的一系列元素。范围相当于调用`lower_bound()`和`upper_bound()`。对于`map`或`set`,该范围只能包含 0 或 1 个元素。 |修饰语
所有关联容器都支持以下方法:
| 操作 | 描述 | | --- | --- | | `clear()` | 清空容器。 | | `emplace()` | 就地构造一个新元素。 | | `emplace_hint()` | 就地构造一个新元素。一个实现可以使用给定的提示来开始搜索插入位置。 | | `erase()` | 移除特定位置的元素、某个范围的元素或与给定键匹配的所有元素。 | | `insert()` | 插入新元素。 | | `swap()` | 交换两个容器的内容。 |观察者
所有有序关联容器都支持以下观察器:
| 操作 | 描述 | | --- | --- | | `key_comp()` | 返回键比较仿函数 | | `value_comp()` | 返回用于根据键值对的键来比较键值对的函子 |非成员函数
所有有序关联容器都支持与顺序容器相同的一组非成员函数:operator==
、!=
、<
、<=
、>
、>=
和std::swap()
。
无序关联容器<unordered_map>,</unordered_map>
有四个无序关联容器:unordered_map
、unordered_multimap
、unordered_set
和unordered_multiset
。它们类似于有序关联容器(map
、multimap
、set
和multiset
),只是它们不对元素进行排序,而是将它们存储在哈希映射的桶中。这些接口类似于相应的有序关联容器,只是它们公开了与哈希策略和桶相关的哈希特定的接口。
哈希映射
哈希映射或哈希表是一种高效的数据结构,它将其元素存储在桶中。 2 从概念上讲,map 包含一个指向桶的指针数组,这些桶依次是元素的数组或链表。通过一个称为哈希的数学公式,计算出一个哈希整数,然后将其转换为桶索引。导致相同桶索引的两个元素存储在同一个桶中。
哈希映射允许非常快速地检索元素。要检索一个元素,需要计算它的哈希值,这会产生桶号。如果该存储桶中有多个元素,则在单个存储桶中执行快速(通常是线性)搜索,以找到正确的元素。
模板类型参数
无序关联容器允许您指定自己的哈希函数,以及自己的定义,即如何通过指定额外的模板类型参数来决定两个键是否相等。以下是所有无序关联容器的模板定义:
template<class Key, class Value, class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>> class unordered_map;
template<class Key, class Value, class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>> class unordered_multimap;
template<class Key, class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>> class unordered_set;
template<class Key, class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>> class unordered_multiset;
哈希函数
如果太多的键导致相同的哈希(桶索引),哈希映射的性能会下降。在最坏的情况下,所有元素都在同一个桶中结束,所有查找和插入操作都变成线性的。编写合适的散列函数的细节超出了本书的范围。
该标准提供了以下std::hash
模板(基础模板在<functional>
中定义,但也包含在<unordered_xxx>
标题中):
template<class T> struct hash;
提供了几种类型的专门化,如bool
、char
、int
、long
、double
和std::string
。如果你想计算你自己的对象类型的散列,你可以实现你自己的散列函子类。然而,我们建议您实现一个专门化的std::hash
。
下面是一个例子,说明如何为简介一章中定义的Person
类实现一个std::hash
专门化。它对string
对象使用标准的std::hash
专门化来计算名和姓的散列。然后,通过 XOR 运算将两个哈希值组合起来。简单的异或值通常不会给出足够随机分布的整数,但是如果两个操作数都已经是散列,则可以认为是可接受的:
Note
尽管通常不允许向std
名称空间添加类型或函数,但是添加专门化是完全合法的。还要注意,我们在第二章中提出的在类型自身的名称空间中专门化std::swap()
的建议并没有扩展到std::hash
:因为std::hash
是一个类而不是一个函数(就像swap()
),ADL 并不适用(参见第二章中的讨论)。
复杂性
所有四个无序关联容器的复杂性是相同的:
- 插入:平均 O(1),最坏情况 O(N)
- 删除:平均 O(1),最坏情况 O(N)
- 访问:平均 O(1),最坏情况 O(N)
参考
所有无序关联容器都支持与有序关联容器相同的方法,除了反向迭代器、lower_bound()
和upper_bound()
。以下小节将对unordered_map
(UM)、unordered_multimap
(UMM)、unordered_set
(美国)和unordered_multiset
(UMS)支持的所有额外操作进行概述,分为几类。
观察者
所有无序关联容器都支持以下观察器:
| 操作 | 描述 | | --- | --- | | `hash_function()` | 返回用于哈希键的哈希函数 | | `key_eq()` | 返回用于对键执行相等测试的函数 |桶接口
所有无序关联容器都支持以下桶接口:
| 操作 | 描述 | | --- | --- | | `begin(int)` `end(int)` | 返回给定索引的桶中第一个或最后一个元素的迭代器 | | `bucket()` | 返回给定键的桶的索引 | | `bucket_count()` | 返回桶的数量 | | `bucket_size()` | 返回桶中具有给定索引的元素数量 | | `cbegin(int)` `cend(int)` | `begin(int)`和`end(int)`的`const`版本 | | `max_bucket_count()` | 返回可以创建的最大存储桶数 |哈希策略
所有无序关联容器都支持以下哈希策略方法:
| 操作 | 描述 | | --- | --- | | `load_factor()` | 返回存储桶中元素的平均数。 | | `max_load_factor()` | 返回或设置最大负载系数。如果负载系数超过此最大值,则会创建更多的存储桶。 | | `rehash()` | 将存储桶的数量设置为一个特定的值,并重新散列所有当前元素。 | | `reserve()` | 保留一定数量的存储桶以容纳给定数量的元素,而不超过最大负载系数。 |非成员函数
所有无序关联容器只支持operator==
、operator!=
和std::swap()
作为非成员函数。
分配器
除了array
和bitset
之外的所有容器都支持另一个我们还没有展示的模板类型参数——一个允许您指定分配器类型的参数。不过,这总是有一个默认值,您通常应该忽略它。当你想对容器的内存分配有更多的控制时,它就出现了。因此,理论上,您可以编写自己的分配器并将其传递给容器。这是一个超出本书范围的高级话题。
例如,vector
模板的完整定义如下:
template<class T, class Allocator = allocator<T>>
class vector;
Footnotes 1
引言章节中为Person
定义operator<
的方式导致了priority_queue
中的 VIP 和非 VIP 人员按相反的字母顺序排列:姓名按字母顺序排列的人拥有更高的优先级。
2
从技术上讲,您可以很容易地实现没有桶的散列映射:例如,使用所谓的开放寻址。但是,标准无序容器的定义方式强烈建议使用单独的链接方法,这就是我们在这里描述的。
四、算法
前一章讨论了标准库提供的存储数据的容器。除此之外,该库还提供了许多算法来处理这些数据或其他数据。算法独立于容器:它们只基于迭代器工作,因此只要提供合适的迭代器,就可以在任何范围的元素上执行。
这一章从输入/输出迭代器的简单定义开始,接着是按功能组织的所有可用算法的详细概述。本章最后讨论了迭代器适配器。
输入和输出迭代器
前一章简要解释了容器提供的不同种类的迭代器:正向、双向和随机访问。算法上下文中使用了另外两种迭代器类别,与其他三种相比,它们的要求更少。本质上:
- 输入迭代器:必须可取消引用才能读取元素。除此之外,只需要
++
、==
和!=
操作符。 - 输出迭代器:只需要
++
操作符,但是你必须能够在解引用后向它们写入元素。
对于这两者,它们提供单路访问也就足够了。也就是说,一旦增加,它们原则上可以使它们的所有先前副本无效。两个相应的迭代器标签,如在第三章中讨论的,也为这些类别提供:std::input_iterator_tag
和output_iterator_tag
。
标准容器返回的所有迭代器,以及指向 C 风格数组的指针,都是有效的输入迭代器。它们也是有效的输出迭代器,只要它们不指向const
元素。
算法<algorithm>
本节概述了所有可用的算法,根据功能分为几个小节。除非另有说明,所有算法都在<algorithm>
头文件中定义。
术语
以下术语和缩写用于算法定义中的类型:
- function:Callable——即 lambda 表达式、函数对象或函数指针。
- InIt、OutIt、FwIt、BidIt、RanIt:输入、输出、正向、双向或随机访问迭代器。
- UnaOp、BinOp:一元或二元运算,即接受一个 resp 的可调用操作。两个论点。
- UnaPred,BinPred:一元或二元谓词,谓词是返回布尔值的操作。
- Size:表示大小的类型,例如,元素的数量。
- DiffType:表示两个迭代器之间距离的类型。
- t:一个元素类型。
- Compare:用于比较元素的函数对象。如果未指定,则使用
operator<
。函数对象接受两个参数,如果第一个参数小于第二个参数,则返回true
,否则返回false
。强加的排序必须是严格的弱排序,就像默认的operator<
一样。
算法通常接受一个可调用的参数:例如,一元或二元操作或谓词。这个可调用函数可以是 lambda 表达式、函数对象或函数指针。Lambda 表达式和函数对象将在第二章中讨论。
一般准则
首先,尽可能使用标准算法,而不是自己编写的循环,因为它们通常更有效,而且更不容易出错。此外,尤其是在引入 lambda 表达式之后,算法的使用通常会产生更短、可读性更强、不言自明的代码。
其次,对于一些算法,某些容器提供了等价的专用成员函数(见第三章)。这些算法效率更高,因此应该优先于一般算法。在接下来的算法描述中,我们总是列出这些备选方案。
最后,许多算法移动或交换元素。如果没有隐式或显式的移动和/或交换函数可用,这些算法会退回到复制元素。为了获得最佳性能,您应该始终考虑为重要的自定义数据类型实现专门的移动和/或交换函数。标准库提供的类型总是在适当的地方提供这些。关于移动语义和交换功能的更多信息,我们参考第二章。
对范围应用函数
- 为范围
first, last)
中的每个元素调用给定函数,并返回std::move(function)
。注意,当迭代整个容器或 C 风格数组时,基于范围的for
循环更方便。
Function for_each(InIt first, InIt last, Function function)
- 转换范围
[first1, last1)
中的所有元素,并将结果存储在从target
开始的范围中,该范围允许等于first1
或first2
以执行就地转换。对于第一个版本,对每个转换后的元素执行一元运算。对于第二种情况,对每个转换后的元素和第二个范围中的相应元素执行二元运算。设 length =(last1 - first1)
,则对长度为 0 ≤ n <的对(*(first1 +
n), *(first2 +
n))
执行二进制运算。返回目标范围的结束迭代器,所以(target +
长度)
。
OutIt transform(InIt first1, InIt last1, OutIt target, UnaOp operation)
OutIt transform(InIt1 first1, InIt1 last1, InIt2 first2,
OutIt target, BinOp operation)
例子
下面的示例使用transform()
通过 lambda 表达式将vector
中的所有元素加倍,然后使用transform()
通过标准函数对象对元素求反,最后使用for_each()
将所有元素输出到控制台。这段代码还需要<functional>
:
std::vector<int> vec{ 1,2,3,4,5,6 };
std::transform(cbegin(vec), cend(vec), begin(vec),
[ { return element * 2; });
std::transform(cbegin(vec), cend(vec), begin(vec), std::negate<>());
std::for_each(cbegin(vec), cend(vec),
[](auto& element) { std::cout << element << " "; });
输出如下所示:
-2 -4 -6 -8 -10 -12
检查元素是否存在
- 如果范围
[first, last)
中的所有元素、无元素或至少有一个元素满足一元predicate
,则返回true
。如果范围为空,all_of()
和none_of()
返回true
,而any_of()
返回false
。
bool all_of(InIt first, InIt last, UnaPred predicate)
bool none_of(InIt first, InIt last, UnaPred predicate)
bool any_of(InIt first, InIt last, UnaPred predicate)
- 返回
[first, last)
中等于给定value
或满足一元predicate
的元素数量。[替代:所有有序和无序的关联容器都有一个count()
成员。]
DiffType count(InIt first, InIt last, const T& value)
DiffType count_if(InIt first, InIt last, UnaPred predicate)
例子
以下示例演示了如何使用all_of()
来检查所有元素是否都是偶数:
查找元素
- 在范围
[first, last)
的所有元素中搜索第一个等于value
、满足一元predicate
或不满足predicate
的元素。返回找到的元素的迭代器,如果没有找到,返回last
。[替代:所有有序和无序的关联容器都有一个find()
成员。]
InIt find(InIt first, InIt last, const T& value)
InIt find_if(InIt first, InIt last, UnaPred predicate)
InIt find_if_not(InIt first, InIt last, UnaPred predicate)
- 返回一个迭代器到
[first1, last1)
中的第一个元素,它等于[first2, last2)
中的一个元素。如果没有找到这样的元素或者如果[first2, last2)
为空,则返回last1
。如果给出了一个二元谓词,它将用于判断两个范围之间的元素是否相等。
InIt find_first_of(InIt first1, InIt last1,
FwIt first2, FwIt last2[, BinPred predicate])
- 返回范围
[first, last)
中第一对相邻元素的第一个元素的迭代器,这些元素彼此相等或匹配一个二进制数predicate
。如果没有找到合适的相邻元素,返回last
。
FwIt adjacent_find(FwIt first, FwIt last[, BinPred predicate])
例子
以下代码片段使用find_if()
算法在人员列表中查找一个名为 Waldo 的人:
auto people = { Person("Wally"), Person("Wilma"), Person("Wenda"),
Person("Odlaw"), Person("Waldo"), Person("Woof") };
auto iter = std::find_if(begin(people), end(people),
[](const Person& p) { return p.GetFirstName() == "Waldo"; });
二进位检索
以下所有算法都要求给定范围[ first
,last
]在value
上排序或至少分区(分区稍后解释)。如果不满足这个前提条件,算法的行为是未定义的。
- 如果在范围
[first, last)
中有一个等于value
的元素,则返回true
。
bool binary_search(FwIt first, FwIt last, const T& value[, Compare comp])
- 将迭代器返回到
[first, last)
中第一个对lower_bound()
的比较不小于value
的元素,以及第一个对upper_bound()
的比较大于value
的元素。当在一个排序范围内插入时,如果插入发生在迭代器之前,这两个位置都适合插入value
(就像顺序容器的insert()
方法一样;参见下一个“示例”小节)。[替代:所有有序关联容器都有lower_bound()
和upper_bound()
成员。]
FwIt lower_bound(FwIt first, FwIt last, const T& value[, Compare comp])
FwIt upper_bound(FwIt first, FwIt last, const T& value[, Compare comp])
- 返回一个包含下限和上限的
pair
。[替代:所有有序和无序的关联容器都有一个equal_range()
成员。]
pair<FwIt, FwIt> equal_range(FwIt first, FwIt last,
const T& value[, Compare comp])
例子
下面的代码片段演示了如何在vector
的正确位置插入一个新值,以保持元素的排序:
下一个例子使用equal_range()
找到等于 2 的值的范围。它返回一个迭代器的pair
。第一个指向第一个等于 2 的元素,第二个指向最后一个 2:
后续搜索
所有的子序列搜索算法都接受一个可选的二元谓词,用于判断元素是否相等。
- For
search()
/find_end()
,分别返回一个迭代器到[first1, last1)
中第一个/最后一个子序列的开头,等于范围[first2, last2)
。如果第二个范围为空,则返回first1
/last1
,如果没有找到相等的子序列,则返回last1
。
FwIt1 search(FwIt1 first1, FwIt1 last1,
FwIt2 first2, FwIt2 last2[, BinPred predicate])
FwIt1 find_end(FwIt1 first1, FwIt1 last1,
FwIt2 first2, FwIt2 last2[, BinPred predicate])
- 返回第一个子序列的迭代器,这个子序列由重复了
count
次的value
组成。如果count
为零,则返回first
,如果没有找到合适的子序列,则返回last
。
FwIt search_n(FwIt first, FwIt last, Size count,
const T& value[, BinPred predicate])
最小/最大
- 返回对两个值中最小值或最大值的引用,如果两个值相等,则返回第一个值。
constexpr const T& min(const T& a, const T& b[, Compare comp])
constexpr const T& max(const T& a, const T& b[, Compare comp])
- 返回给定
initializer_list
中最小值或最大值的副本,或者如果有几个元素等于这个极值,则返回最左边元素的副本。
constexpr T min(initializer_list<T> t[, Compare comp])
constexpr T max(initializer_list<T> t[, Compare comp])
- 返回一个包含对两个值的最小值和最大值的引用的
pair
,按此顺序。如果两个值相等,则返回pair(a, b)
。
constexpr pair<const T&, const T&> minmax(
const T& a, const T& b[, Compare comp])
- 返回一个
pair
,包含一个initializer_list
中的最小值和最大值的副本,按此顺序。如果几个元素都等于最小值,那么返回最左边一个的副本;如果几个元素等于最大值,则返回最右边的一个副本。
constexpr pair<T, T> minmax(initializer_list<T> t[, Compare comp])
- 返回一个最小值迭代器,一个最大值迭代器,或者分别返回一个包含范围
[first, last)
中最小和最大元素迭代器的pair
。如果范围为空,则返回last
或pair(first, first)
。
FwIt min_element(FwIt first, FwIt last[, Compare comp])
FwIt max_element(FwIt first, FwIt last[, Compare comp])
pair<FwIt, FwIt> minmax_element(FwIt first, FwIt last[, Compare comp])
序列比较
所有的序列比较算法都接受一个可选的二元谓词,用于判断元素是否相等。
- 假设 n =
(last1 - first1)
,如果范围[first1, last1)
和[first2, first2 +
n)
中的所有元素成对匹配,则返回true
。第二个范围必须至少有 n 个元素。因此,后面讨论的四参数版本是避免越界访问的首选。
bool equal(InIt1 first1, InIt1 last1, InIt2 first2[, BinPred predicate])
- 设 n =
(last1 - first1)
,然后返回一个pair
迭代器,指向范围[first1, last1)
和[first2, first2 +
n)
中不匹配的第一个元素。第二个范围必须至少有 n 个元素。因此,为了避免越界访问,最好使用下面的四参数版本。
pair<InIt1, InIt2> mismatch(InIt1 first1, InIt1 last1,
InIt2 first2[, BinPred predicate])
- 早期三参数版本的安全版本,也知道第二个范围的长度。为了使
equal()
成为true
,两个范围必须等长。对于mismatch()
,如果在到达last1
或last2
之前没有发现不匹配对,则返回一对(first1 + m, first2 + m)
和m = min(last1 - first1, last2 - first2)
。
bool equal(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2[, BinPred predicate])
pair<InIt1, InIt2> mismatch(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2[, BinPred predicate])
复制、移动、交换
- 将范围
[first, last)
中的所有元素(copy()
)或仅满足一元元素predicate
(copy_if()
)的元素复制到从targetFirst
开始的范围。对于copy()
,不允许targetFirst
在[first, last)
中:如果是这样的话,copy_backward()
可能是一个选项。对于copy_if()
,范围不允许重叠。对于这两种算法,目标范围必须足够大,以容纳复制的元素。返回结果范围的结束迭代器。
OutIt copy(InIt first, InIt last, OutIt targetFirst)
OutIt copy_if(InIt first, InIt last, OutIt targetFirst, UnaPred predicate)
- 将范围
[first, last)
中的所有元素复制到结束于targetLast
的范围,该范围不在范围[first, last)
中。目标范围必须足够大,以容纳复制的元素。复制是反向进行的,从复制元素(last-1)
到(targetLast-1)
开始,再回到first
。返回一个迭代器到目标范围的开始,所以(targetLast - (last - first))
。
BidIt2 copy_backward(BidIt1 first, BidIt1 last, BidIt2 targetLast)
- 将从
start
开始的count
元素复制到从target
开始的范围。目标范围必须足够大,以容纳这些元素。返回目标结束迭代器,所以(target + count)
。
OutIt copy_n(InIt start, Size count, OutIt target)
- 类似于
copy()
和copy_backward()
,但是移动元素而不是复制它们。
OutIt move(InIt first, InIt last, OutIt targetFirst)
BidIt2 move_backward(BidIt1 first, BidIt1 last, BidIt2 targetLast)
- 将范围
[first1, last1)
中的元素与范围[first2, first2 + (last1 - first1))
中的元素交换。两个范围不允许重叠,第二个范围必须至少与第一个范围一样大。返回一个迭代器,从第二个范围中最后一个交换的元素开始。
FwIt2 swap_ranges(FwIt1 first1, FwIt1 last1, FwIt2 first2)
- 将由
x
指向的元素与由y
指向的元素交换,所以swap(*x, *y)
。
void iter_swap(FwIt1 x, FwIt2 y)
生成序列
- 将
value
分配给范围[first, last)
或[first, first + count)
中的所有元素。如果count
为负,则不会发生任何事情。fill_n()
的范围必须足够大,以容纳count
元素。fill_n()
返回(first + count)
,如果count
为负,则返回first
。【替代品:array::fill()
。]
void fill(FwIt first, FwIt last, const T& value)
OutIt fill_n(OutIt first, Size count, const T& value)
- 生成器是一个没有任何返回值的参数的函数。调用它来计算范围
first, last)
或[first, first + count)
中每个元素的值。如果count
是负的,什么都不会发生。generate_n()
的范围必须足够大,以容纳count
元素。generate_n()
返回(first + count)
,如果count
为负,则返回first
。
void generate(FwIt first, FwIt last, Generator gen)
OutIt generate_n(OutIt first, Size count, Generator gen)
- 该算法在
<numeric>
标题中定义。范围[first, last)
中的每个元素被设置为value
,之后value
递增,因此:
void iota(FwIt first, FwIt last, T value)
*first = value++
*(first + 1) = value++
*(first + 2) = value++
...
例子
以下示例演示了generate()
和iota()
:
![A417649_1_En_4_Figd_HTML.gif
拆卸和更换
- 将范围
first, last)
中不等于value
或不满足一元predicate
的所有元素向范围的开头移动,之后[first, result)
包含要保留的所有元素。返回result
迭代器,指向传递了最后一个要保留的元素的迭代器。算法是稳定的,这意味着保留的元素保持它们的相对顺序。不应该使用[result, last)
中的元素,因为它们可能因移动而处于未指定的状态。通常这些算法后面是对erase()
的调用。这被称为删除-擦除习惯用法,在第 [3 章中讨论。
FwIt remove(FwIt first, FwIt last, const T& value)
FwIt remove_if(FwIt first, FwIt last, UnaPred predicate)
【备选:】和forward_list
有remove()
和remove_if()
成员。]
- 从范围
[first, last)
中的连续相等元素中删除除一个元素之外的所有元素。如果给定一个二元谓词,它将用于判断元素是否相等。否则等同于remove()
,包括它后面通常应该跟一个erase()
的事实。下一个“示例”小节显示了unique()
的典型用法。【替代品:】、forward_list::unique()
。]
FwIt unique(FwIt first, FwIt last[, BinPred predicate])
- 用
newVal
替换范围[first, last)
中等于oldVal
或满足一元predicate
的所有元素。
void replace(FwIt first, FwIt last, const T& oldVal, const T& newVal)
void replace_if(FwIt first, FwIt last, UnaPred predicate, const T& newVal)
- 类似于前面的算法,但是将结果复制到从
target
开始的范围。目标范围必须足够大,以容纳复制的元素。输入和目标范围不允许重叠。返回目标范围的结束迭代器。
OutIt remove_copy(InIt first, InIt last, OutIt target, const T& value)
OutIt remove_copy_if(InIt first, InIt last, OutIt target, UnaPred predicate)
OutIt unique_copy(InIt first, InIt last, OutIt target [, BinPred predicate])
OutIt replace_copy(InIt first, InIt last, OutIt target,
const T& oldVal, const T& newVal)
OutIt replace_copy_if(InIt first, InIt last, OutIt target,
UnaPred predicate, const T& newVal)
例子
下面的例子演示了如何使用unique()
和 remove-erase 习惯用法从vector
中过滤出所有连续的相等元素:
反转和旋转
- 反转范围
[first, last)
中的元素。【替代品:list::reverse()
,forward_list::reverse()
。]
void reverse(BidIt first, BidIt last)
- 向左旋转范围
[first, last)
中的元素,使middle
指向的元素成为范围中的第一个元素,而(middle - 1)
指向的元素成为范围中的最后一个元素(参见下一个“示例”小节)。返回(first + (last - middle))
。
FwIt rotate(FwIt first, FwIt middle, FwIt last)
- 类似于
reverse()
和rotate()
,但是将结果复制到从target
开始的范围。目标范围必须足够大,以容纳复制的元素。输入和目标范围不允许重叠。返回目标范围的结束迭代器。
OutIt reverse_copy(BidIt first, BidIt last, OutIt target)
OutIt rotate_copy(FwIt first, FwIt middle, FwIt last, OutIt target)
例子
下一个代码片段旋转了vector
中的元素。结果是5,6,1,2,3,4
:
std::vector<int> vec{ 1,2,3,4,5,6 };
std::rotate(begin(vec), begin(vec) + 4, end(vec));
分割
- 如果范围
[first, last)
中的元素被分区,使得满足一元谓词的所有元素都在不满足该谓词的所有元素之前,则返回true
。如果范围为空,也返回true
。
bool is_partitioned(InIt first, InIt last, UnaPred predicate)
- 对范围
[first, last)
进行分区,使得满足一元谓词的所有元素都在不满足谓词的所有元素之前。返回不满足谓词的第一个元素的迭代器。stable_partition()
保持两个分区中元素的相对顺序。
FwIt partition(FwIt first, FwIt last, UnaPred predicate)
BidIt stable_partition(BidIt first, BidIt last, UnaPred predicate)
- 通过将满足或不满足一元谓词的所有元素复制到分别从
outTrue
或outFalse
开始的输出范围来划分范围[first, last)
。两个输出范围都必须足够大,以容纳复制的元素。输入和输出范围不允许重叠。返回一个包含两个输出范围的结束迭代器的pair
。
pair<OutIt1, OutIt2> partition_copy(InIt first, InIt last,
OutIt1 outTrue, OutIt2 outFalse, UnaPred predicate)
- 要求基于一元
predicate
对范围[first, last)
进行分区。向第二个分区的第一个元素返回一个迭代器:即不满足谓词的第一个元素。
FwIt partition_point(FwIt first, FwIt last, UnaPred predicate)
整理
- 对范围
[first, last)
中的元素进行排序。稳定版本保持相等元素的顺序。【替代品:list::sort()
,forward_list::sort()
。]
void sort(RanIt first, RanIt last[, Compare comp])
void stable_sort(RanIt first, RanIt last[, Compare comp])
The (middle - first)
范围[first, last)
中最小的元素被排序并移动到范围[first, middle)
。未排序的元素以未指定的顺序移动到范围[middle, last)
。
void partial_sort(RanIt first, RanIt middle, RanIt last[, Compare comp])
min(last - first, targetLast - targetFirst)
范围[first, last)
中的元素被排序并复制到目标范围。返回min(targetLast, targetFirst + (last - first))
。
RanIt partial_sort_copy(InIt first, InIt last,
RanIt targetFirst, RanIt targetLast[, Compare comp])
- 范围
[first, last)
中的元素以这样的方式移动,即在重新排列后,给定的迭代器nth
指向如果整个范围被排序时该位置的元素。但是,实际上并没有对整个范围进行排序。然而,它是在nth
指向的元素上(非稳定)分区的。
void nth_element(RanIt first, RanIt nth, RanIt last[, Compare comp])
- 如果范围
[first, last)
是排序序列,则返回true
。
bool is_sorted(FwIt first, FwIt last[, Compare comp])
- 返回最后一个迭代器
iter
,这样[first, iter)
就是一个有序序列。
FwIt is_sorted_until(FwIt first, FwIt last[, Compare comp])
- 返回范围
[first1, last1)
中的元素是否比范围[first2, last2)
中的元素少。
bool lexicographical_compare(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2[, Compare comp])
例子
partial_sort()
和partial_sort_copy()
算法可用于找出 n 个最大、最小、最差、最佳,...序列中的元素。这比排序整个序列要快。例如:
std::vector<int> vec{ 9,2,4,7,3,6,1 };
std::vector<int> threeSmallestElements(3);
std::partial_sort_copy(begin(vec), end(vec),
begin(threeSmallestElements), end(threeSmallestElements));
nth_element()
是一种所谓的选择算法,用于寻找序列中第 n 个最小的数,平均具有线性复杂度。例如,它可用于计算具有奇数个元素的序列的中值:
洗牌
- 使用由统一随机数生成器生成的随机性打乱范围
first, last)
中的元素。随机数生成库在第 [1 章中解释。
void shuffle(RanIt first, RanIt last, UniformRanGen generator)
- 不赞成使用
shuffle()
,但为了完整性而提及。它打乱了范围[first, last)
中的元素。随机数生成器rng
是一个仿函数,其函数调用操作符接受一个整数参数n
,并返回一个在[0, n)
范围内的整数随机数,其n
>为 0。如果没有提供随机数生成器,实现可以自由决定如何生成随机数。
void random_shuffle(RanIt first, RanIt last[, RNG&& rng])
例子
下面的例子打乱了vector
中的元素。参见第一章了解更多关于随机数生成库的信息。代码片段还需要<random>
和<ctime>
:
排序范围上的操作
以下所有操作都需要对输入范围进行排序。如果不满足这个前提条件,算法的行为是未定义的。
- 将排序范围
[first1, last1)
和[first2, last2)
中的所有元素合并到一个从target
开始的范围中,这样目标范围也被排序。目标范围必须足够大,以容纳所有元素。输入范围不允许与目标范围重叠。返回目标范围的结束迭代器。算法稳定;也就是说,相同元素的顺序保持不变。【替代品:list::merge()
,forward_list::merge()
。]
OutIt merge(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2, OutIt target[, Compare comp])
- 将排序后的范围
[first, middle)
和[middle, last)
合并成一个排序后的序列,存储在范围[first, last)
中。该算法是稳定的,因此保持了相等元素的顺序。
void inplace_merge(BidIt first, BidIt middle, BidIt last[, Compare comp])
- 如果排序范围
[first2, last2)
中的所有元素都在排序范围[first1, last1)
中,或者前者为空,则返回true
,否则返回false
。
bool includes(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2[, Compare comp])
- 对两个排序范围
[first1, last1)
和[first2, last2)
执行集合运算(见下表),并将结果存储在从target
开始的范围内。对目标范围内的元素进行排序。目标范围必须足够大,以容纳集合运算的元素。输入和输出范围不允许重叠。返回构造的目标范围的结束迭代器。- 联合:两个输入范围的所有元素。如果一个元素在两个输入范围内,那么它在输出范围内只出现一次。
- 交集:两个输入范围内的所有元素。
- 差异:所有在
[first1, last1)
中的元素和不在[first2, last2)
中的元素。 - 对称差:所有在
[first1, last1)
和不在[first2, last2)
的元素,以及所有在[first2, last2)
和不在[first1, last1)
的元素。
OutIt set_union(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2, OutIt target[, Compare comp])
OutIt set_intersection(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2, OutIt target[, Compare comp])
OutIt set_difference(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2, OutIt target[, Compare comp])
OutIt set_symmetric_difference(InIt1 first1, InIt1 last1,
InIt2 first2, InIt2 last2, OutIt target[, Compare comp])
排列
- 如果第二个范围是第一个范围的排列,则返回
true
。对于三参数版本,第二个范围被定义为[first2, first2 + (last1 - first1))
,并且该范围必须至少与第一个范围一样大。因此,四参数版本更适合防止越界访问(如果范围长度不同,它们将返回false
)。如果给定一个二进制数predicate
,它将用于判断两个范围之间的元素是否相等。
bool is_permutation(FwIt1 first1, FwIt1 last1,
FwIt2 first2[, BinPred predicate])
bool is_permutation(FwIt1 first1, FwIt1 last1,
FwIt2 first2, FwIt2 last2[, BinPred predicate])
- 将范围
[first, last)
中的元素转换为按字典顺序排列的下一个/上一个排列。如果这样的下一个/前一个排列存在,则返回true
,否则返回false
,并按照可能的最小/最大排列转换元素。
bool next_permutation(BidIt first, BidIt last[, Compare comp])
bool prev_permutation(BidIt first, BidIt last[, Compare comp])
很
在这个上下文中,术语堆不是指 C++ 运行时的动态内存池。在计算机科学中,堆也是一组基本的基于树的数据结构(众所周知的变体包括二进制、二项式和斐波那契堆)。这些数据结构是有效实现各种图形和排序算法的关键构件(经典的例子包括 Prim 算法、Dijkstra 算法和 heapsort)。这也是优先级队列的一种常见实现策略:事实上,前一章讨论的 C++ priority_queue
容器适配器是使用下面定义的堆算法实现的。
对于下面的 C++ 算法,堆的树被展平成以特定方式排序的连续元素序列。虽然确切的排序是特定于实现的,但它必须满足以下关键属性:没有元素大于它的第一个元素,并且移除这个最大的元素和添加任何新元素都可以在对数时间内完成。
- 将范围
[first, last)
变成一个堆(在线性时间内)。
void make_heap(RanIt first, RanIt last[, Compare comp])
- 范围
[first, last)
的最后一个元素被移动到正确的位置,从而成为一个堆。在调用push_heap()
之前,范围[first, last - 1)
需要是一个堆。
void push_heap(RanIt first, RanIt last[, Compare comp])
- 通过用
*(last - 1)
交换*first
并确保新的范围[first, last - 1)
仍然是堆,从堆[first, last)
中移除最大的元素。
void pop_heap(RanIt first, RanIt last[, Compare comp])
- 对范围
[first, last)
中的所有元素进行排序。在调用sort_heap()
之前,该范围需要是一个堆。
void sort_heap(RanIt first, RanIt last[, Compare comp])
- 如果范围
[first, last)
表示堆,则返回true
。
bool is_heap(RanIt first, RanIt last[, Compare comp])
- 返回最后一个迭代器
iter
,这样[first, iter)
表示一个堆。
RanIt is_heap_until(RanIt first, RanIt last[, Compare comp])
数字算法<numeric>
以下算法在<numeric>
标题中定义:
- 返回
result
,从result
等于startValue
开始,然后对范围[first, last)
内的每个element
执行result += element
或result = op(result, element)
计算得到。
T accumulate(InIt first, InIt last, T startValue[, BinOp op])
- 返回
result
,从等于startValue
的result
开始计算,然后依次对范围[first1, last1)
中的每个el1
和范围[first2, first2 + (last1 - first1))
中的每个el2
执行result += (el1 * el2)
或result = op1(result, op2(el1, el2))
。第二个范围必须至少与第一个范围一样大。
T inner_product(InIt1 first1, InIt1 last1, InIt2 first2,
T startValue[, BinOp1 op1, BinOp2 op2])
- 计算从
[first, last)
开始的递增子范围的部分和,并将结果写入从target
开始的范围。使用默认运算符+
,结果就好像是按如下方式计算的:
OutIt partial_sum(InIt first, InIt last, OutIt target[, BinOp op])
- 返回目标范围的结束迭代器,所以
(target + (last - first))
。目标范围必须足够大以容纳结果。通过指定target
等于first
,可以就地完成计算。
*(target) = *first
*(target + 1) = *first + *(first + 1)
*(target + 2) = *first + *(first + 1) + *(first + 2)
...
- 计算范围
[first, last)
中相邻元素的差值,并将结果写入从target
开始的范围。对于默认运算符-
,计算结果如下:
OutIt adjacent_difference(InIt first, InIt last, OutIt target[, BinOp op])
- 返回目标范围的结束迭代器,所以
(target + (last - first))
。目标范围必须足够大以容纳结果。通过指定target
等于first
,可以就地完成计算。
*(target) = *first
*(target + 1) = *(first + 1) - *first
*(target + 2) = *(first + 2) - *(first + 1)
...
例子
以下代码片段使用accumulate()
算法计算序列中所有元素的总和:
inner_product()
算法可用于计算两个数学向量的所谓点积:
迭代器适配器<iterator>
标准库提供了以下迭代器适配器:
reverse_iterator
:反转正在修改的迭代器的顺序。用make_reverse_iterator(Iterator iter)
造一个。move_iterator
:解引用被修改为右值的迭代器。用make_move_iterator(Iterator iter)
造一个。back_insert_iterator
:使用push_back()
在容器后面插入新元素的迭代器适配器。使用back_inserter(Container& cont)
建造一个。front_insert_iterator
:迭代器适配器,使用push_front()
在容器前面插入新元素。使用front_inserter(Container& cont)
建造一个。insert_iterator
:使用insert()
在容器中插入新元素的迭代器适配器。要构建一个,使用inserter(Container& cont, Iterator iter)
,其中iter
是插入位置。
下面的例子通过使用deque
上的front_insert_iterator
适配器,以相反的顺序将所有元素从vector
复制到deque
。接下来,它使用accumulate()
连接vector
中的所有string
(其默认组合运算符+
执行string
的连接)。因为这里使用了move_iterator
适配器,所以string
是移动的,而不是从vector
复制的:
五、流输入输出
基于 C++ 流的 I/O 库允许您执行 I/O 操作,而不必知道有关目标或源的详细信息。流的目标或源可以是字符串、文件、内存缓冲区等等。
流的输入和输出
标准库提供的流类组织在一个层次结构和一组头中,如图 5-1 所示。
图 5-1。
The hierarchy of stream-related classes
更准确地说,该库定义了名为basic_ios
、basic_ostream
、basic_istringstream
等的模板,所有模板都基于一种字符类型。层次结构中的所有类,除了ios_base
,都是这些模板化类的typedef
,模板类型为char
。比如std::ostream
就是std::basic_ostream<char>
的一个typedef
。对于称为wios
、wostream
、wofstream
等的wchar_t
字符类型,有对应的typedef
。本章剩余部分仅使用图 5-1 中所示的char typedef
s。
除了图中的表头,还有<iostream>
。有点令人困惑的是,这并没有真正定义std::iostream
本身,因为这是由<istream>
完成的。相反,<iostream>
包括<ios>
、<streambuf>
、<istream>
、<ostream>
和<iosfwd>
,同时自身增加了标准输入和输出流(w
) cin
、(w
) cout
、(w
) cerr
、(w
) clog
。后两个分别用于输出错误和日志信息。它们的目的地是特定于实现的。
该库还提供了std::basic_streambuf
、basic_filebuf
和basic_stringbuf
模板及其各种typedef
,加上istreambuf_iterator
和ostreambuf_iterator
。这些是流缓冲区,是其他流类实现的基础,比如ostream
、ifstream
等等。在这一章的结尾会对它们进行简要的讨论。
头文件<iosfwd>
包含所有标准 I/O 库类型的前向声明。将它包含在其他头文件中是很有用的,而不必包含您需要的所有类型的完整模板定义。
助手类型<ios>
以下助手类型在<ios>
中定义:
STD::IOs _ base<ios>
在<ios>
中定义的ios_base
类是所有输入和输出流类的基类。它跟踪格式化选项和标志,以操纵数据的读写方式。提供了以下方法:
还可以通过流 I/O 操纵器来修改标志,这将在下一节中讨论。
表 5-2。
std::ios_base::fmtflags
Masks Defined in <ios>
表 5-1。
std::ios_base::fmtflags
Formatting Flags Defined in <ios>
输入/输出操纵器<ios>, <iomanip>
操纵器允许你使用operator<<
和operator>>
而不是flags(fmtflags)
或setf()
来改变旗帜。
<ios>
头为表 5-1 : std::scientific
、std::left
等中定义的所有标志定义了全局std
范围内的 I/O 操纵器。对于属于表 5-2 中定义的掩码的标志,I/O 操纵器使用该掩码。比如std::dec
其实叫ios_base::setf(dec, basefield)
。
对于boolalpha
、showbase
、showpoint
、showpos
、skipws
、uppercase
、unitbuf
,也可以使用负面操纵器,它们的名称相同,但以no
为前缀:例如std::noboolalpha
。
除了std::fixed
和scientific
之外,还有std::hexfloat
( scientific | fixed
)和std::defaultfloat
(无floatfield
标志设置)机械手。
此外,<iomanip>
标题定义了以下操纵器:
例子
这段代码还需要<locale>
:
在美国系统上,输出如下:
Left: $1.23__
Right: __$1.23
Internal: 0x___7b
STD::IOs<ios>
在<ios>
中定义的ios
类继承自ios_base
,并提供了许多检查和修改流状态的方法,它是表 5-3 中列出的状态标志的按位组合。
表 5-3。
std::ios_base::iostate
State Constants Defined in <ios>
提供了以下与状态相关的方法:
| 方法 | 描述 | | --- | --- | | `good()``eof()``bad()` | 如果分别没有设置`badbit`、`failbit`和`eofbit`,设置了`eofbit`,设置了`badbit`,或者设置了`failbit`或`badbit`,则返回`true`。 | | `operator!` | 相当于`fail()`。 | | `operator bool` | 相当于`!fail()`。 | | `rdstate()` | 返回当前的`ios_base::iostate`状态。 | | `clear(state)` | 如果附加了有效的流缓冲区,则将流的状态更改为给定的状态(见下文);否则将其设置为`state | badbit`。 | | `setstate(state)` | 调用`clear(state | rdstate())`。 |除了这些与状态相关的方法之外,以下附加方法由ios
定义:
std::ios
的默认初始化有以下效果:
- 标志被设置为
skipws | dec
。 - 精度设置为 6。
- 字段宽度设置为 0。
- 填充字符被设置为
widen(' ')
。 - 如果附加了有效的流缓冲区(见下文),则将状态设置为
goodbit
,否则设置为badbit
。
错误处理
默认情况下,流操作通过设置流的状态位(good
、bad
、fail
和eof
)来报告错误,但它们不会抛出异常。不过,可以使用exceptions()
方法来启用异常。它要么返回当前异常掩码,要么接受一个。该掩码是std::ios_base::iostate
状态标志的按位组合(见表 5-3 )。对于掩码中设置为 1 的每个状态标志,当该状态位为流设置时,流将引发异常。
例如,下面的代码试图使用文件流打开一个不存在的文件(将在本章后面详细解释)。不会引发任何异常;只有流的失败位被设置为 1:
如果您想使用异常,代码可以重写如下:
一个可能的输出可能是
ios_base::failbit set: iostream stream error
std::ostream <ostream>
ostream
类支持对基于char
的流进行格式化和非格式化输出。格式化输出意味着所写内容的格式会受到格式化选项的影响,例如字段的宽度、浮点数的十进制位数等等。格式化输出通常也会受到流的locale
的影响,如第六章所述。无格式输出只需要按原样写入字符或字符缓冲区。
ostream
提供了一个swap()
方法和下面的高级输出操作。如果没有提到返回类型,操作返回一个ostream&
,允许操作被链接:
表 5-4。
std::ios_base::seekdir
Constants Defined in <ios>
<ostream>
还定义了以下额外的 I/O 操纵器:
<iostream>
头提供了以下全局ostream
实例:
cout/wcout
:输出到标准 C 输出流stdout
cerr/wcerr
:标准 C 错误流的输出,stderr
clog/wclog
:标准 C 错误流的输出,stderr
(w)cout
自动绑定到(w)cin
。这意味着对(w)cin
的输入操作导致(w)cout
刷新其缓冲区。(w)cout
也自动绑定到(w)cerr
,因此(w)cerr
上的任何输出操作都会导致(w)cout
刷新。
std::ios_base
提供了一个名为sync_with_stdio()
的静态方法,用于在每次输出操作后将这些全局ostream
与底层 C 流同步。这确保了它们使用相同的缓冲区,允许您安全地混合 C++ 和 C 风格的输出。它还保证了标准流是线程安全的:也就是说,没有数据竞争。尽管如此,字符交错仍然是可能的。
Note
当使用标准流cout
、cerr
、clog
和cin
(稍后讨论)时,您不必考虑与平台相关的行尾字符。例如,在 Windows 上,一行通常以\r\n
结尾,而在 Linux 上以\n
结尾。然而,翻译会自动发生,所以您可以总是使用\n
。
例子
以下示例演示了三种不同的输出方法:
std::cout << "PI = " << 3.1415 << std::endl;
std::cout.put('\t');
std::cout.write("C++", 3);
STD::ist stream<istream>
istream
类支持来自基于char
的流的格式化和非格式化输入。它提供swap()
及以下高级输入操作。除非另有说明,否则操作返回一个istream&
,这有助于链接:
<istream>
还定义了以下额外的 I/O 操纵器:
<iostream>
头提供了以下全局istream
实例:
cin/wcin
:从标准 C 输入流中读取,stdin
ios_base::sync_with_stdio()
功能也会影响(w)cin
。参见前面对cout
、cerr
、clog
的解释。
如前所述,istream
提供了一个getline()
方法来提取字符。不幸的是,你必须传递给它一个适当大小的char*
缓冲区。<string>
头定义了一个更容易使用的std::getline()
方法,它接受一个std::string
作为目标缓冲区。下面的例子说明了它的用法。
例子
int anInt;
double aDouble;
std::cout << "Enter an integer followed by some whitespace\n"
<< "and a double, and press enter: ";
std::cin >> anInt >> aDouble;
std::cout << "You entered: ";
std::cout << "Integer = " << anInt << ", Double = " << aDouble << std::endl;
std::string message;
std::cout << "Enter a string. End input with a * and enter: ";
std::getline(std::cin >> std::ws, message, '*');
std::cout << "You entered: '" << message << "'" << std::endl;
下面是该程序的一个可能输出:
Enter an integer followed by some whitespace
and a double, and press enter: 1 3.2 ↩
You entered: Integer = 1, Double = 3.2
Enter a string. End input with a * and enter: This is ↩
a multiline test* ↩
You entered: 'This is ↩
a multiline test'
std::iostream <istream>
iostream
类,在<istream>
中定义(不在<iostream>
中!),继承自ostream
和istream
,提供高级输入输出操作。它跟踪流中的两个独立位置:一个输入位置和一个输出位置。这就是为什么ostream
有tellp()
和seekp()
方法,而istream
有tellg()
和seekg()
: iostream
包含所有四个方法,所以它们需要一个不同的名字。除了继承的功能之外,它不提供额外的功能。
字符串流<sstream>
字符串流允许您在字符串上使用流 I/O。该库提供了istringstream
(输入,继承自istream
)、ostringstream
(输出,继承自ostream
)、stringstream
(输入输出,继承自iostream
)。继承图见图 5-1 。这三个类都有一组相似的构造函数:
-
[i|o]stringstream(ios_base::openmode)
: Constructs a new string stream with the givenopenmode
, a bitwise combination of the flags defined in Table 5-5表 5-5。
| 开放模式 | 描述 | | --- | --- | | `app` | 追加的简称。在每次写入前查找到流的末尾。 | | `binary` | 以二进制模式打开的流。如果未指定,则以文本模式打开流。差异参见文件流部分。 | | `in / out` | 分别为读/写而打开的流。 | | `trunc` | 打开流后移除流的内容。 | | `ate` | 打开流后查找到流的末尾。 |std::ios_base::openmode
Constants Defined in<ios>
-
[i|o]stringstream(string&, ios_base::openmode)
:用给定字符串的副本作为初始流内容,用给定的openmode
构造一个新的字符串流 -
[i|o]stringstream([i|o]stringstream&&)
:移动构造器
前两个构造函数中的openmode
有一个默认值:out
代表ostringstream
,in
代表istringstream
,out|in
代表stringstream
。对于ostringstream
和istringstream
,给定的openmode
总是和默认的组合在一起;比如对于ostringstream
,实际的openmode
是给定 _openmode |ios_base::out
。
这三个类只添加了两个方法:
string str()
:返回底层字符串对象的副本void str(string&)
:将底层字符串对象设置为给定对象的副本
例子
std::ostringstream oss;
oss << 123 << " " << 3.1415;
std::string myString = oss.str();
std::cout << "ostringstream contains: '" << myString << "'" << std::endl;
std::istringstream iss(myString);
int myInt; double myDouble;
iss >> myInt >> myDouble;
std::cout << "int = " << myInt << ", double = " << myDouble << std::endl;
对象
文件流允许您对文件使用流 I/O。该库提供了一个ifstream
(输入,继承自istream
)、ofstream
(输出,继承自ostream
)、fstream
(输入输出,继承自iostream
)。继承图见图 5-1 。这三个类都有一组相似的构造函数:
[i|o]fstream(filename, ios_base::openmode)
:构造一个文件流,用给定的openmode
打开给定的文件。文件可以指定为const char*
或std::string&
。[i|o]fstream([i|o]fstream&&)
:移动构造器。
这三个类都添加了以下方法:
open(filename, ios_base::openmode)
:打开一个类似于第一个构造函数的文件is_open()
:如果打开文件进行输入和/或输出,则返回true
close()
:关闭当前打开的文件
构造函数和open()
方法中的openmode
(见表 5-5 )有一个默认:ofstream
用out
,ifstream
用in
,fstream
用out|in
。对于ofstream
和ifstream
,给定的openmode
总是和默认的组合在一起;例如:对于ofstream
,实际openmode
是给定 _openmode |ios_base::out
。
如果指定了ios_base::in
标志,无论是否与ios_base::out
结合,您试图打开的文件必须已经存在。以下代码打开一个用于输入和输出的文件,如果该文件尚不存在,则创建该文件:
如果一个文件以文本模式打开,而不是二进制模式,库被允许翻译某些特殊字符来匹配平台如何使用这些字符。例如,在 Windows 上,行通常以\r\n
结尾,而在 Linux 上,它们通常以\n
结尾。当一个文件在文本模式下打开时,你并不是自己在 Windows 上读/写\r
;库为您处理这种翻译。
与其他组合的输入和输出流(如stringstream
)相比,fstream
类支持输入和输出,处理当前位置的方式不同。文件流只有一个位置,因此输出和输入位置总是相同的。
Tip
文件流的析构函数自动关闭文件。
例子
下面的示例类似于前面给出的字符串流示例,但使用了一个文件。在这个例子中,ofstream
是使用close()
显式关闭的,ifstream
是通过ifs
的析构函数隐式关闭的:
const std::string filename = "output.txt";
std::ofstream ofs(filename);
ofs << 123 << " " << 3.1415;
ofs.close();
std::ifstream ifs(filename);
int myInt; double myDouble;
ifs >> myInt >> myDouble;
std::cout << "int = " << myInt << ", double = " << myDouble << std::endl;
自定义类型的运算符<< and >
您可以编写自己版本的流输出和提取操作符operator<<
和operator>>
。下面是一个关于Person
类的两个操作符的例子,使用std::quoted()
操作符来处理名字中的空格:
std::ostream& operator<<(std::ostream& os, const Person& person) {
os << std::quoted(person.GetFirstName()) << ' '
<< std::quoted(person.GetLastName());
return os;
}
std::istream& operator>>(std::istream& is, Person& person) {
std::string firstName, lastName;
is >> std::quoted(firstName) >> std::quoted(lastName);
person.SetFirstName(firstName); person.SetLastName(lastName);
return is;
}
这些运算符可以如下使用(<sstream>
也是必需的):
流迭代器
除了在第 3 和 4 章节中讨论的其他迭代器之外,<iterator>
头定义了两个流迭代器std::istream_iterator
和std::ostream_iterator
。
std::ostream_iterator
ostream_iterator
是一个输出迭代器,能够使用operator<<
向ostream
输出某种类型的对象序列。要输出的对象的类型被指定为模板类型参数。有一个构造函数接受对要使用的ostream
的引用和一个可选的分隔符,该分隔符在每次输出后被写入流中。
结合第四章讨论的算法,流迭代器非常强大。例如,下面的代码片段使用std::copy()
算法将double
的vector
写入控制台,其中每个double
后跟一个制表符(另外还需要<vector>
和<algorithm>
):
std::vector<double> vec{ 1.11, 2.22, 3.33, 4.44 };
std::copy(cbegin(vec), cend(vec),
std::ostream_iterator<double>(std::cout, "\t"));
std::istream_iterator
istream_iterator
是一个输入迭代器,能够通过使用operator>>
逐个提取对象来迭代istream
中的某种类型的对象。要从流中提取的对象的类型被指定为模板类型参数。有三个构造函数:
istream_iterator()
:默认构造函数,导致迭代器指向流的末尾istream_iterator(istream&)
:构造一个迭代器,从给定的istream
中提取对象istream_iterator(istream_iterator&)
:复制构造函数
就像一个ostream_iterator
,istream_iterator
s 结合算法非常厉害。以下示例结合使用for_each()
算法和istream_iterator
从标准输入流中读取未指定数量的double
值,并将它们相加以计算平均值(还需要<algorithm>
):
std::istream_iterator<double> begin(std::cin), end;
double sum = 0.0; int count = 0;
std::for_each(begin, end, &{ sum += value; ++count;});
std::cout << sum / count << std::endl;
在 Windows 上按 Ctrl+Z 或在 Linux 上按 Ctrl+D 终止输入,然后按 Enter。
第二个例子使用一个istream_iterator
从控制台读取不确定数量的double
和一个ostream_iterator
将读取的double
写入由制表符分隔的stringstream
(另外需要<sstream>
和<algorithm>
):
std::ostringstream oss;
std::istream_iterator<double> begin(std::cin), end;
std::copy(begin, end, std::ostream_iterator<double>(oss, "\t"));
std::cout << oss.str() << std::endl;
流缓冲器<streambuf>
流类不直接处理目标,如内存中的字符串、磁盘上的文件等。相反,他们使用由std::basic_streambuf<CharType>
定义的流缓冲区的概念。提供两个typedef
、std::streambuf
和std::wstreambuf
,模板类型分别为char
或wchar_t
。文件流使用std::(w)filebuf
,字符串流使用std::(w)stringbuf
,两者都继承自(w)streambuf
。
每个流都有一个与之相关联的流缓冲区,您可以使用rdbuf()
获得指向该缓冲区的指针。对rdbuf(streambuf*)
的调用返回当前关联的流缓冲区,并将其更改为给定的流缓冲区。
流缓冲区可用于编写流重定向器类,将一个流重定向到另一个流。作为一个基本的例子,下面的代码片段将所有的std::cout
输出重定向到一个文件(另外还需要<fstream>
):
Caution
当更改一个标准流的缓冲区时,不要忘记在终止应用程序之前恢复旧的缓冲区,就像上一个示例中所做的那样。否则,您的代码可能会因某些库实现而崩溃。
它还可以用于实现一个 tee 类,该类将输出重定向到两个或多个目标流。另一个用途是轻松读取整个文件:
std::ifstream ifs("test.txt");
std::stringstream buffer;
buffer << ifs.rdbuf();
流缓冲区的确切行为取决于实现。直接使用流缓冲区是一个高级主题,由于页面限制,我们不能进一步详细讨论。
c 型输出和输入<cstdio>
除了在第二章中解释的文件实用程序外,<cstdio>
头文件还定义了 C 风格的 I/O 库,包括基于字符的 I/O ( getc()
、putc()
),...)和格式化的 I/O ( printf()
,scanf()
,...).所有 C 风格的 I/O 功能都包含在类型安全的 C++ 流中,它也有更好的定义,可移植的错误处理。 1 这一节确实讨论了std::printf()
和std::scanf()
函数族,而且只讨论这些,因为它们的格式语法紧凑,有时比 C++ 流更方便。
std::printf()系列
以下printf()
系列函数在<cstdio>
中定义:
std::printf(const char* format, ...)
std::fprintf(FILE* file, const char* format, ...)
std::snprintf(char* buffer, size_t bufferSize, const char* format, ...)
std::sprintf(char* buffer, const char* format, ...)
它们将格式化的输出分别写入标准输出、文件、给定大小的缓冲区或缓冲区,并返回写出的字符数。最后一个sprintf()
,不如snprintf()
安全。它们在format
字符串后都有数量可变的参数。也有以v
为前缀的版本接受va_list
作为参数:例如vprintf(const char* format, va_list)
。对于前三种,还提供了宽字符版本:(v)wprintf()
、(v)fwprintf()
和(v)swprintf()
。
如何格式化输出由给定的format
字符串控制。除了以%
开头的序列之外,它的所有字符都按原样写出。格式化选项的基本语法是%
后跟一个转换说明符。这告诉printf()
如何解释变长参数列表中的下一个值。传递给printf()
的参数必须与format
中的%
指令顺序相同。表 5-6 解释了可用的转换说明符。列出的预期参数类型适用于不使用长度修饰符的情况(稍后讨论)。
表 5-6。
Available Conversion Specifiers for printf()
-Like Functions
Caution
C 风格的 I/O 函数不是类型安全的。如果您的转换说明符要求将参数值解释为double
,那么该参数必须是真的double
(而不是,例如,float
或整数)。如果传递了错误的类型,它会编译并运行,但这很少会有好结果。这也意味着永远不要将 C++ std::string
原样作为字符串转换说明符的参数传递:而是使用c_str()
,如下例所示。
下面的例子打印了美国传统民歌“99 瓶啤酒”的歌词(假设有一个using namespace std
):
string bottles = "bottles of beer";
char on_wall[99];
for (int i = 99; i > 0; --i) {
snprintf(on_wall, sizeof(on_wall), "%s on the wall", bottles.c_str());
printf("%d %s, %d %s.\n", i, on_wall, i, bottles.c_str());
printf("Take one down, pass it around, %d %s.\n", i-1, on_wall);
}
格式化选项比到目前为止讨论的基本转换要强大得多。%
指令的完整语法如下:
%<flags><width><precision><length_modifier><conversion>
随着
-
<flags>
: Zero or more flags that change the meaning of the conversion specifier. See Table 5-7.表 5-7。
Available Flags
| 旗 | 描述 | | --- | --- | | `-` | 左对齐输出。默认情况下,输出右对齐。 | | `+` | 始终输出数字的符号,即使是正数。 | | 空格字符 | 如果要输出的数字是非负的或者没有字符,则在输出前加上一个空格。如果还指定了`+`,则忽略。 | | `#` | 输出一个所谓的另类形式。对于`x`和`X`,如果数字不为零,则结果以`0x`或`0X`为前缀。对于所有浮点说明符(`a`、`A`、`e`、`E`、`f`、`F`、`g`和`G`),输出总是包含一个小数点字符。对于`g`和`G`,尾随零不会被删除。对于 o,精度增加,因此输出的第一个数字是零。 | | `0` | 对于所有的整数和浮点转换说明符(`d`、`i`、`o`、`u`、`x`、`X`、`a`、`A`、`e`、`E`、`f`、`F`、`g`和`G`),用零代替空格进行填充。如果也指定了`-`,或者对于所有整数说明符与精度的组合,则忽略此选项。 | -
<width>
:可选最小字段宽度(不截断:仅填充)。如果转换后的值的字符数少于指定的宽度,则应用填充。默认情况下,空格用于填充。<width>
可以是非负整数,也可以是*
,这意味着从参数列表中的整数参数中获取宽度。此宽度必须在要格式化的值之前。 -
<precision>
:一个点,后面跟一个可选的非负整数(如果没有指定,则假定为 0),或者一个*
,这也意味着从参数列表中的一个整数参数中获取精度。精度是可选的,它决定了以下内容:- s 的最大字节数,默认情况下,应该是以零结尾的字符数组。
- 所有整数转换说明符(d、I、o、u、X 和 X)的最小输出位数。默认值:1。
- 对于大多数浮点转换说明符(A、A、E、E、F 和 F),小数点后要输出的位数。如果未指定,默认精度为 6。
- g 和 g 的最大有效位数。默认值也是 6。
-
<length_modifier>
: An optional modifier that alters the type of the argument to be passed. Table 5-8 gives an overview of all supported modifiers for numeric conversions. For character and strings (c
ands
conversion specifiers, respectively), thel
length modifier (note: this is the letterl
) changes the expected input type fromint
andchar*
towint_t
andwchar_t*
, respectively.2表 5-8。
Length Modifiers for All Numeric Conversion Specifiers
| 修饰语 | `d`,`i` | `o`、`u`、`x`、`X` | `n` | `a`、`A`、`e`、`E`、`f`、`F`、`g`、`G` | | --- | --- | --- | --- | --- | | (无) | `int` | `unsigned int` | `int*` | `double` | | `hh` | `char` | `unsigned char` | `char*` | | | `h` | `short` | `unsigned short` | `short*` | | | `l` | `long` | `unsigned long` | `long*` | | | `ll` | `long long` | `unsigned long long` | `long long*` | | | `j` | `intmax_t` | `uintmax_t` | `intmax_t*` | | | `z` | `size_t` | `size_t` | `size_t*` | | | `t` | `ptrdiff_t` | `ptrdiff_t` | `ptrdiff_t*` | | | `L` | | | | `long double` | -
<conversion>
:唯一必需的组件,指定要应用于参数的转换。(见表 5-6 。)
表 5-8 中的修饰符决定了必须按指示传递的输入类型。std::intmax_t
、uintmax_
、t
在<cstdint>
(见第章 1 )中定义,size_t
、ptrdiff_t
在<cstddef>
中定义。还要注意的是long
和 l ong long
修饰符使用字母l
,而不是数字1
。
例子
std::scanf()系列
以下scanf()
系列函数在<cstdio>
中定义:
std::scanf(const char* format, ...)
std::fscanf(FILE* file, const char* format, ...)
std::sscanf(const char* buffer, const char* format, ...)
它们分别从标准输入、文件或缓冲区中读取。除了这些在format
字符串后面有可变数量的参数的函数之外,还有一些名称以v
为前缀并接受va_list
作为参数的版本:例如,vscanf(const char* format, va_list)
。还提供了宽字符版本:(v)wscanf()
、(v)fwscanf()
和(v)swscanf()
。
它们都根据给定的format
字符串读取格式化数据。使用的scanf()
格式语法类似于前面看到的printf()
格式语法。格式字符串中的所有字符只是用来与输入进行比较,除了以%
开头的序列。这些%
指令导致值被解析并按顺序存储在函数参数所指向的位置。基本语法是一个%
符号,后跟表 5-9 中的一个转换说明符。最后一列显示了未指定长度修饰符时的参数类型(见表 5-10 )。
表 5-10。
Available Length Modifiers for the Numeric Conversion Specifiers of scanf()
-Like Functions
表 5-9。
Available Conversion Specifiers for scanf()
-Like Functions
对于除转换说明符c
、s
或[...]
之外的所有指令,任何空白字符都会被跳过,直到第一个非空白字符出现。当到达输入字符串的末尾、出现流输入错误或出现解析错误时,解析会停止。返回值等于指定值的数量,或者如果在开始第一次转换前发生输入故障,则返回值为EOF
。如果到达流的末尾或出现解析错误,则赋值的数量将小于指令的数量:例如,如果在第一次转换期间出现这种情况,则赋值的数量为零。
%
指令的完整语法如下:
%<*><width><length_modifier><conversion>
与:
<*>
:一个可选的*
符号,使scanf()
从输入中解析数据,而不把它存储在任何参数中。<width>
:可选最大字段宽度,以字符为单位。<length_modifier>
:可选长度修改量:见表 5-10 。当应用于c
、s
或[...]
说明符时,l
(字母l
)将所需的输入类型从char**
修改为wchar_t**
。<conversion>
:必选。指定要应用的转换;见表 5-9 。
表 5-10 和表 5-8 之间唯一不明显的区别是,默认情况下,浮点参数必须指向一个float
而不是一个double
。
例子
std::string s = "int: +123; double: -2.34E-3; chars: abcdef";
int i = 0; double d = 0.0; char chars[4] = { 0 };
std::sscanf(s.data(), "int: %i; double: %lE; chars: %[abc]", &i, &d, chars);
std::printf("int: %+i; double: %.2lE; chars: %s", i, d, chars);
Footnotes 1
一些库实现使用errno
(参见第八章)来报告 C 风格 I/O 函数的错误,包括printf()
和scanf()
函数:请查阅您的库文档以确认。
2
wint_t
在<cwchar>
中定义,是一个足够大的整型的typedef
,可以容纳任何宽字符(wchar_t
值)和至少一个不是有效宽字符的值(WEOF)。
六、字符和字符串
字符串<string>
该标准定义了四种不同的字符串类型,每种类型对应一种不同的类似于char
的类型:
第一列中的名字纯粹是指示性的,因为字符串完全不知道它们所包含的类似于char
的项目——或者代码单元——所使用的字符编码。例如,窄字符串可用于存储 ASCII 字符串,以及使用 UTF-8 或 DBCS 编码的字符串。
为了说明,我们将主要使用std::string
。不过,本节中的所有内容同样适用于所有类型。除非另有说明,此后讨论的区域设置和正则表达式功能只需要为窄字符串和宽字符串实现。
所有四种字符串类型都是同一个类模板std::basic_string<CharT>
的实例化。一个basic_string<CharT>
本质上是一个vector<CharT>
,它有额外的函数和重载,或者是为了方便普通的字符串操作,或者是为了兼容 C 风格的字符串(const CharT*
)。vector
的所有成员也都是为字符串提供的,除了就位功能(对字符用处不大)。这意味着,与其他主流语言不同,如。NET、Python 和 Java,C++ 中的字符串是可变的。这也意味着,例如,字符串可以很容易地用于第四章中的所有算法:
本节的剩余部分将重点介绍与vector
相比,字符串增加的功能。对于字符串与vector
共有的功能,我们参考第三章。有一点需要注意,特定于字符串的函数和重载大多是基于索引的,而不是基于迭代器的。例如,前一个示例中的最后三行可以更方便地写成
或者
在处理字符串索引时,end()
迭代器的等效物是basic_string::npos
。这个常量一贯用于表示半开放范围(也就是说,表示“直到字符串末尾”),并且,正如您接下来看到的,作为类似find()
的函数的“未找到”返回值。
在字符串中搜索
字符串提供了六个成员函数来搜索子字符串或字符:find()
和rfind()
、find_first_of()
和find_last_of()
以及find_first_not_of()
和find_last_not_of()
。这些总是成对出现:一个从前到后搜索,一个从后到前搜索。所有这些也都具有以下形式的相同的四个重载:
要搜索的模式可以是单个字符,也可以是字符串,后者表示为 C++ 字符串、以 null 结尾的 C-string,或者是使用第一个n
值的字符缓冲区。(r)find()
函数搜索完整模式的一次出现,find_xxx_of()
/ find_xxx_not_of()
函数族搜索模式中出现/未出现的任何单个字符。结果是从开头或结尾开始的第一个匹配项的索引,如果没有找到匹配项,则为npos
。
最可选的pos
参数是搜索应该开始的索引。对于向后搜索的功能,pos
的默认值为npos
。
修改字符串
要修改一个字符串,可以使用从vector
开始已知的所有成员,包括erase()
、clear()
、push_back()
等等(参见第三章)。附加函数或具有字符串特定重载的函数有assign()
、insert()
、append()
、+=
和replace()
。他们的行为应该是明显的;只有replace()
可能需要一些解释。首先,让我们介绍一下这五个函数的大量有用的重载。这些通常是这样的形式:
对于移动字符串,assign(string&&)
也被定义。因为+=
操作符本质上只有一个参数,自然只有C++ string
、C 风格的字符串和初始化列表重载是可能的。
类似于它的vector
对应物,对于insert()
,标有(*)
的重载返回一个iterator
而不是一个string
。出于同样的原因,insert()
函数有两个额外的重载:
只有insert()
和replace()
需要一个Position
。对于insert()
,这通常是一个索引(一个size_t
,除了最后两个重载,它是一个迭代器(再次类似于vector::insert()
)。对于replace()
,Position
是一个范围,使用两个const_iterator
指定(不适用于substring
重载),或者使用一个起始索引和一个长度指定(不适用于最后两个重载)。
换句话说,replace()
并不像您所期望的那样,用另一个字符或字符串替换出现的给定字符或字符串。相反,它用一个新的序列(字符串、子字符串、填充模式等,长度可能不同)替换指定的子范围。您之前已经看到了它的使用示例(2 是被替换范围的长度):
s.replace(s.find("be"), 2, "are");
要替换所有出现的子字符串或给定模式,您可以使用正则表达式和本章稍后解释的std::regex_replace()
函数。为了替换单个字符,第四章中的通用std::replace()
和replace_if()
算法也是一种选择。
最后一个修改函数与它的vector
对应物有一个显著的不同,那就是erase()
:除了两个基于迭代器的重载,它还有一个处理索引的重载。用它来删除尾部或子区域,或者,如果你愿意,可以clear()
它:
string& erase(size_t pos = 0, size_t len = npos);
构造字符串
除了创建一个空字符串的默认构造函数之外,该构造函数还有与前一小节中的函数相同的七个重载,当然还有一个用于string&&
。(和其他容器一样,所有的字符串构造函数都有一个可选的自定义分配器参数,但这只是高级用法。)
从 C++14 开始,各种字符类型的basic_string
对象也可以通过添加后缀s
从相应的字符串文字中构造。这个文字运算符是在std::literals::string_literals
名称空间中定义的:
字符串长度
要获得string
的长度,可以使用典型的容器成员size()
或其特定于字符串的别名length()
。两者都返回字符串包含的类似于char
的元素的数量。但是要注意:C++ 字符串不知道所使用的字符编码,所以它们的长度等于技术上所说的代码单元的数量,这可能大于代码点或字符的数量。众所周知的编码是可变长度 Unicode 编码 UTF-8 和 UTF-16,其中并非所有字符都表示为单个代码单元:
获得码位数的一种方法是先转换成 UTF-32 编码的字符串,使用本章后面介绍的字符编码转换工具。
复制(子)字符串
另一个有特定字符串别名的vector
函数(紧挨着size()
)是data()
,它的等价函数是c_str()
。两者都返回一个指向内部字符数组的const
指针(没有复制)。要将字符串复制到 C 风格的字符串,请使用copy()
:
size_t copy(char* out, size_t len, size_type pos = 0) const;
这会将从pos
开始的len char
值复制到out
。也就是说,它也可以用来复制子串。要将子字符串创建为 C++ 字符串,请使用substr()
:
string substr(size_t pos = 0, size_t len = npos) const;
比较字符串
可以使用非成员比较运算符(==
、<
、>=
等)或它们的compare()
成员,将字符串与其他 C++ 字符串或 C 风格字符串进行词典式比较。后者具有以下重载:
int compare(const string& str) const noexcept;
int compare(size_type pos1, size_type n1, const string& str
[, size_type pos2, size_type n2 = npos]) const;
int compare(const char* s) const;
int compare(size_type pos1, size_type n1, const char* s
[, size_type n2]) const;
pos1
/ pos2
是第一个/第二个字符串中应该开始比较的位置,n1
/ n2
是从第一个/第二个字符串开始比较的字符数。如果两个字符串相等,返回值为零;如果第一个字符串小于/大于第二个字符串,返回值为负/正数。
字符串转换
为了从字符串中解析各种类型的整数,定义了以下形式的一系列非成员函数:
int stoi(const (w)string&, size_t* index = nullptr, int base = 10);
有以下几种变体:stoi()
、stol()
、stoll()
、stoul()
、stoull()
,其中i
代表int
、l
代表long
、u
代表unsigned
。这些函数跳过所有前导空白字符,之后解析由base
确定的语法所允许的尽可能多的字符。如果提供了一个index
指针,它将接收未转换的第一个字符的索引。
类似地,为了解析浮点数,存在一组如下形式的函数:
float stof(const (w)string&, size_t* index = nullptr);
提供stof()
、stod()
、stold()
分别转换为float
、double
、long double
。
为了进行相反的转换,将数值型转换为 a (w)string
,提供了函数to_(w)string(
X )
,其中 X 可以是int
、unsigned
、long
、unsigned long
、long long
、unsigned long long
、float
、double
或long double
。返回值是一个std::(w)string
。
人物分类,
<cctype>
和<cwctype>
标题提供了一系列函数来分别对char
和wchar_t
字符进行分类。这些函数是std::is
类(int)
(只为代表char
s 的int
s 定义)和std::isw
类(wint_t)
(类比;wint_t
是一个整数typedef
,其中 class 等于表 6-1 中的一个值。如果给定的字符属于该类,所有函数都返回非零值int
,否则返回零。
表 6-1。
The 12 Standard Character Classes
| 班级 | 描述 | | --- | --- | | `cntrl` | 控制字符:所有非`print`字符。包括:`'\0'`、`'\t'`、`'\n'`、`'\r'`等。 | | `print` | 可打印字符:数字、字母、空格、标点符号等。 | | `graph` | 带图形表示的字符:除“”以外的所有`print`字符。 | | `blank` | 一行中分隔单词的白色字符。至少“”和`'\t'`。 | | `space` | 空白字符:至少所有的`blank`字符、`'\n'`、`'\r'`、`'\v'`和`'\f'`。从不`alpha`人物。 | | `digit` | 十进制数字(`0`–`9`)。 | | `xdigit` | 十六进制数字(`0`–`9`、`A`–`F`、`a`–`f`)。 | | `alpha` | 字母字符。至少是所有的`lowercase`和`uppercase`字符,而绝不是`cntrl`、`digit`、`punct`和`space`字符中的任何一个。 | | `lower` | 小写字母`alpha`(默认区域设置为`a`–`z`)。 | | `upper` | 大写字母`alpha`(默认区域设置为`A`–`Z`)。 | | `alnum` | 字母数字字符:所有`alpha`和`digit`字符的组合。 | | `punct` | 标点符号(`! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ∼`为默认区域设置)。从来没有一个`space`或`alnum`的角色。 |相同的头还提供了tolower()
/ toupper()
和towlower()
/ towupper()
函数,用于在小写和大写字符之间进行转换。字符再次使用整数int
和wint_t
类型表示。如果转换没有定义或不可能,这些函数只返回它们的输入值。
所有字符分类和转换的确切行为取决于活动的 C 语言环境。本章稍后将详细解释区域设置,但本质上这意味着活动的语言和区域设置可能会导致不同的字符集被认为是字母、小写或大写、数字、空白等等。表 6-1 列出了不同字符类别的所有一般属性和关系,并给出了一些默认C
区域设置的例子。
Note
在“本地化”部分,您还可以看到 C++ <locale>
头提供了一个重载列表,这些重载用于使用给定的locale
而不是活动的C
语言环境的std::is
class
()
和std::tolower()
/ toupper()
(都在字符类型上模板化)。
字符编码转换,
字符编码决定了代码点(许多但不是所有代码点都是字符)如何表示为二进制代码单元。示例包括 ASCII(具有 7 位代码单元的经典编码)、固定长度的 UCS-2 和 UCS-4 编码(分别为 16 位和 32 位代码单元),以及三种主要的 Unicode 编码:固定长度的 UTF-32(对每个代码点使用单个 32 位代码单元)和可变长度的 UTF-8 和 UTF-16 编码(分别将每个代码点表示为一个或多个 8 位或 16 位代码单元;UTF-8 最多 4 个单元,UTF-16 最多 2 个单元)。Unicode 和各种字符编码和转换的细节可以写满一本书;我们在这里解释在实践中在编码之间转换你需要知道什么。
包含低级编码转换逻辑的对象的类模板是std::codecvt<CharType1, CharType2, State>
( cvt
可能是converter
的简称)。它是在<locale>
中定义的(正如您在下一节中看到的,这实际上是一个 locale 方面)。前两个参数是 C++ 字符类型,用于表示两种编码的代码单元。对于所有标准实例化,CharType2
是char. State
是一个高级参数,我们不做进一步解释(所有标准专门化都使用来自<cwchar>
的std::mbstate_t
)。
表 6-2 中列出的四个codecvt
专门化在<locale>
中定义。此外,<codecvt>
标题定义了表 6-3 中列出的三个std::codecvt
子类。 1 对于这些,CharT
对应codecvt
基类的CharType1
参数;如前所述,CharType2
永远是char
。
表 6-3。
Character-Encoding Conversion Classes Defined in <codecvt>
表 6-2。
Character-Encoding Conversion Classes Defined in <locale>
尽管理论上可以直接使用codecvt
实例,但是使用来自<locale>
的std::wstring_convert<CodecvtT, WCharT=wchar_t>
类要容易得多。这个助手类方便了char
字符串和(通常更宽的)字符类型WCharT
字符串之间的双向转换。尽管wstring_convert
的名字容易引起误解(已经过时),但它也可以与u16string
s 或u32string
s 相互转换,而不仅仅是与wstring
s 相互转换。
回想一下std::string
长度部分的以下例子:
要将该字符串转换为 UTF-32,您可能希望实现以下功能:
不幸的是,这不能编译。对于在<codecvt>
中定义的转换器子类,这将编译。但是codecvt
基类的析构函数是protected
(像所有标准的 locale facets 一样:稍后讨论),并且wstring_convert
析构函数调用它来删除它拥有的转换器实例。这种设计缺陷可以通过使用如下的帮助器包装器来规避(类似的技巧可以应用于使任何受保护的函数可公开访问,而不仅仅是析构函数):
为了编译代码,您可以将第一行替换为下面的 2 :
typedef deletable<std::codecvt<char32_t,char,std::mbstate_t>> cvt;
要使用这些转换器潜在的特定于语言环境的变体(见下一节),请使用以下代码(除了""
之外,也可以使用其他语言环境名称):
typedef deletable<std::codecvt_byname<char32_t,char,std::mbstate_t>> cvt;
std::wstring_convert<cvt, char32_t> convertor(new cvt(""));
一个相关的类是wbuffer_convert<CodecvtT, WCharT=wchar_t>
,它包装了一个basic_streambuf<char>
并使其充当一个basic_streambuf<WCharT>
(流缓冲区在第五章中有非常简要的解释)。一个wbuffer_convert
实例由一个可选的basic_streambuf<char>*
、CodecvtT*
和状态构成。包装缓冲区的 getter 和 setter 都被称为rdbuf()
,当前转换状态可以使用state()
获得。下面的代码构造了一个接受宽字符串的流,但是将它写入一个 UTF-8 编码的文件(需要<fstream>
):
本地化<locale>
日期、货币值和数字的文本表示由区域和文化约定控制。举例来说,以下三个句子是类似的,但是使用本地货币、数字和日期格式编写:
In the U.S., John Doe has won $100,000.00 on the lottery on 3/14/2015.
In India, Ashok Kumar has won ₹1,00,000.00 on the lottery on 14-03-2015.
En France, Monsieur Brun a gagné 100.000,00 € à la loterie sur 14/3/2015.
在 C++ 中,所有与以特定于语言环境的方式处理文本相关的参数和功能都包含在一个std::locale
对象中。这些不仅包括刚刚说明的数值和日期的格式化,还包括特定于地区的字符串排序和转换。
区域名称
标准的locale
对象是由一个地区名构成的:
std::locale(const char* locale_name);
std::locale(const std::string& locale_name);
这些名称通常由两个字母的 ISO-639 语言代码和两个字母的 ISO-3166 国家代码组成。然而,精确的格式是特定于平台的:例如,在 Windows 上,英美地区的名称是"en-US"
,而在基于 POSIX 的系统上是"en_US"
。大多数平台支持,或者有时需要额外的规范,比如区域代码、字符编码等等。有关支持的区域设置名称和选项的完整列表,请参考您的平台文档。
只有两个可移植的语言环境名称,""
和"C"
:
- 使用
""
,您可以从程序的执行环境(也就是操作系统)中获取用户偏好的区域和语言设置来构造一个std::locale
。 "C"
语言环境表示经典或中性语言环境,这是所有 C 和 C++ 程序默认使用的标准化、可移植的语言环境。
使用"C"
语言环境,前面的例句变成了
Anywhere, a C/C++ programmer may win 100000 on the lottary on 3/14/2015.
Tip
当写入计算机程序要读取的文件(配置文件、数字数据输出等)时,强烈建议您使用非特定的"C"
语言环境,以避免解析过程中出现问题。当向用户显示值时,应该考虑使用基于用户偏好的语言环境(""
)。
全球语言环境
活动的全局语言环境影响各种格式化或解析文本的标准 C++ 函数,最直接的是本章后面讨论的正则表达式算法和第五章中看到的 I/O 流。是有一个程序范围的全局区域设置实例,还是每个执行线程有一个,这取决于实现。
全球语言环境总是以经典的"C"
语言环境开始。要设置全局语言环境,可以使用静态的std::locale::global()
函数。要获得当前活动的全局语言环境的副本,只需默认构造一个std::locale
。例如:
Note
为了避免竞争情况,标准 C++ 对象(比如新创建的流或regex
对象)总是在构造时复制全局locale
。因此调用global()
不会影响现有的对象,包括std::cout
和<iostream>
的其他标准流。要更改他们的区域设置,您必须调用他们的imbue()
成员。
基本标准::区域设置成员
下表列出了由一个std::locale
提供的最基本的功能,不包括复制成员。用于组合或自定义区域设置的更多高级成员将在本节末尾讨论:
局部多面
从上一小节可以明显看出,std::locale
公共接口没有提供太多的功能。所有的本地化工具都以 facets 的形式提供。每个locale
对象封装了许多这样的方面,对这些方面的引用可以通过std::use_facet<FacetType>()
函数获得。例如,下面的示例使用经典区域设置的数字标点符号方面来打印区域设置的十进制标记,以格式化浮点数:
对于所有标准刻面,不能复制、移动、交换或删除由use_facet()
的结果引用的实例。该方面由给定的locale
共同拥有,并与拥有它的(最后一个)locale
一起被删除。当请求给定的locale
不拥有的FacetType
时,引发bad_cast
异常。要验证刻面的存在,您可以使用std::has_facet<FacetType>()
。
Caution
永远不要做类似于auto& f = use_facet<...>(std::locale("..."));
的事情:facet f
由临时locale
对象拥有,所以使用它可能会崩溃。
默认情况下,locale
s 包含本节剩余部分中介绍的所有刻面的专门化,每个刻面又至少专门化了char
和wchar_t
字符类型(其他最低要求将在本节中讨论)。实现可能包括更多的方面,程序甚至可以自己添加定制的方面,这将在后面解释。
我们现在按顺序讨论表 6-4 中列出的 12 个标准刻面类别,按类别分组。之后,我们将展示如何组合不同地区的方面并创建定制的方面。虽然这可能不是大多数程序员经常使用的东西,但是偶尔确实需要定制方面。无论如何,了解本地化的范围和各种影响,并在开发显示或处理用户文本的程序(即大多数程序)时将它们牢记在心是值得的。
表 6-4。
Overview of the 12 Basic Facet Classes , Grouped by Category
| 种类 | 面状 | | --- | --- | | `numeric` | `numpunct数字格式
numeric
和monetary
类别的方面遵循相同的模式:有一个punct
方面(标点符号的缩写)带有特定于地区的格式参数,另外还有一个put
和一个get
方面分别负责实际的格式和值的解析。后两个方面主要供第五章中介绍的流对象使用。它们用来读取或写入值的具体格式由在punct
facet 中设置的参数和使用流成员或流操纵器设置的其他参数的组合决定。
数字标点符号
std::numpunct<CharT>
方面提供了检索以下与数值和布尔值格式相关的信息的函数:
decimal_point()
:返回小数点分隔符thousands_sep()
:返回千位分隔符grouping()
:返回一个编码数字分组的std::string
truename()
和falsename()
:返回带有布尔值文本表示的basic_string<CharT>
在本节开头的彩票示例中,数值 100000.00 使用三种不同的语言环境格式化:"100,000.00"
、"1,00,000.00"
和"100.000,00"
。前两个区域分别使用逗号(,
)和点(.
)作为千位和小数点分隔符,而第三个区域则相反。
数字grouping()
被编码为一系列char
值,表示每组中的数字数量,从最右边组中的数字开始。序列中的最后一个char
也用于所有后续组。例如,大多数地区将数字三个一组,编码为"\3"
。(注意:不要使用"3"
,因为'3'
ASCII 字符会产生值为 51 的char
;也就是:'3' == '\51'
。)然而,对于印度地区,如"1,00,000.00"
所示,只有最右边的组包含三位数;所有其他组只包含两个。这被编码为"\3\2"
。为了表示无限组,可以在最后一个位置使用std::numeric_limits<char>::max()
值。空的grouping()
string
表示根本不应该使用分组,例如,对于经典的"C"
语言环境就是这种情况。
数值的格式化和解析
std::num_put
和num_get
方面构成了第五章中描述的<<
和>>
流操作符的实现,并提供了具有以下签名的两组方法:
Iter put(Iter target, ios_base& stream, char fill, X value)
Iter get(Iter begin, Iter end, ios_base& stream, iostate& error, X& result)
这里的X
可以是bool
、long
、long long
、unsigned int
、unsigned long
、unsigned long long
、double
、long double
,也可以是一个void
指针。对于get()
,unsigned short
,float
也是可以的。这些方法要么格式化一个给定的数字value
,要么尝试解析范围begin, end)
中的字符。在这两种情况下,ios_base
参数是对一个流的引用,从该流中获取区域设置和格式信息(例如,包括流的格式标志和精度:参见第 [5 章)。
所有的put()
函数在写入格式化后的字符序列后简单地返回target
。如果格式化长度小于stream.width()
,则使用fill
字符进行填充(填充规则参见第五章)。
如果解析成功,get()
将数值存储在result
中。如果输入与格式不匹配,result
被设置为零,并且iostate
参数中的failbit
被设置(参见第五章)。如果解析值对于类型X
过大/过小,则failbit
也被设置,并且result
被设置为std::numeric_limits<X>::max()
/ lowest()
(参见第一章)。如果到达输入的结尾(可能是成功或失败),则eofbit
被设置。返回解析序列后第一个字符的迭代器。
我们在这里没有展示示例代码,但是这些方面类似于接下来介绍的货币格式化方面,对此我们包括了一个完整的示例。
货币格式
货币标点符号
std::moneypunct<CharType, International=false>
方面提供了检索以下与格式化货币值相关的信息的函数:
decimal_point()
、thousands_sep()
、grouping()
:类似前面看到的数字标点符号成员。frac_digits()
:返回小数点后的位数。典型值是2
。curr_symbol()
:如果International
模板参数为false
,则返回货币符号,如'€'
,如果International
为true
,则返回国际货币代码(通常为三个字母)后加一个空格,如"EUR"
。pos_format()
和neg_format()
返回一个money_base::pattern
结构(稍后讨论),描述如何格式化正负货币值。positive_sign()
和negative_sign()
:返回正负货币值的格式string
。
后四个成员需要更多的解释。它们使用在moneypunct
的基类std::money_base
中定义的类型。定义为struct pattern{ char field[4]; }
的money_base::pattern
结构是一个包含money_base::part
枚举的四个值的数组,这些值受支持:
比如,假设neg_format()
模式是{none, symbol, sign, value}
,货币符号是'$'
,negative_sign()
返回"()"
,frac_digits()
返回2
。然后值-123456
被格式化为"$(1,234.56)"
。
Note
对于美国和许多欧洲地区,frac_digits()
等于2
,这意味着无格式的值要用分来表示,而不是美元或欧元。不过,情况并不总是这样:例如,对于日本的语言环境来说,frac_digits()
就是0
。
货币值的格式化和解析
方面std::money_put
和money_get
处理货币值的格式化和解析,主要供第五章中讨论的put_money()
和get_money()
I/O 操纵器使用。方面提供了这种形式的方法:
Iter put(Iter target, bool intl, ios_base& stream, char fill, X value)
Iter get(Iter begin, Iter end, bool intl, ios_base& stream,
iostate& error, X& result)
这里的X
不是std::string
就是long double
。参数的行为和含义与之前讨论的num_put
和num_get
相似。如果intl
为false
,则使用类似$
的货币符号;否则,使用类似USD
的字符串。
下面说明了如何使用这些方面,尽管您通常简单地使用std::put_
/ get_money()
(使用<cassert>
和<sstream>
):
时间和日期格式
两个面std::time_get
和time_put
处理时间和日期的解析和格式化,并为第五章中的get_time()
和put_time()
操纵器提供动力。它们提供具有以下签名的方法:
Iter put(Iter target, ios_base& stream, char fill, tm* value, <format>)
Iter get(Iter begin, Iter end, ios_base& stream, iostate& error, tm* result,
<format>)
<format>
或者是'const char* from, const char* to'
,指向使用与第二章中解释的strftime()
相同的语法表达的时间格式模式,或者是具有可选修饰符'char format, char modifier'
的相同语法的单个时间格式说明符。参数的行为和含义类似于数字和货币格式方面的行为和含义。第二章也解释了std::tm
的结构。只有那些在格式化模式中提到的成员被使用/写入。
除了通用的get()
函数之外,time_get
方面还有一系列更受限制的解析函数,所有这些函数都有以下签名:
Iter get_x(Iter begin, Iter end, ios_base& stream, iostate& error, tm*)
字符分类、变换和转换
字符分类和转换
ctype<CharType>
方面提供了一系列依赖于地区的字符分类和转换函数,包括前面看到的<cctype>
和<cwctype>
头文件的等价函数。
为了在下面列出的字符分类函数中使用,定义了 12 个位掩码类型的成员常量ctype_base::mask
(ctype_base
是ctype
的基类),每个字符类一个。它们的名称与表 6-1 中给出的类名相同。虽然它们的值未指定,alnum == alpha|digit
和graph == alnum|punct
。下表列出了所有分类功能(输入字符范围用两个CharType*
指针b
和e
表示):
相同的方面也提供这些转换功能:
| 成员 | 描述 | | --- | --- | | `tolower(c)``toupper(c)``tolower(b,e)` | 对单个字符(返回结果)或字符范围[ `b`,`e`)(就地转换)执行从上至下的转换,反之亦然;`e`又回来了)。无法转换的字符保持不变。 | | `widen(c)` `widen(b,e,o)` | 将`char`值转换为单个字符(返回结果)或字符范围[ `b`,`e`]上的刻面的字符类型(转换后的字符放入从`*o`开始的输出范围中);`e`又回来了)。变换的角色从不属于它们的源角色不属于的类。 | | `narrow(c,d)` `narrow(b,e,d,o)` | 转换到`char`;与`widen()`相反。然而,只有对于 96 个基本源字符(除了`| 成员 | 描述 | | --- | --- | | `tolower(c)``toupper(c)``tolower(b,e)` | 对单个字符(返回结果)或字符范围[ `b`,`e`)(就地转换)执行从上至下的转换,反之亦然;`e`又回来了)。无法转换的字符保持不变。 | | `widen(c)` `widen(b,e,o)` | 将`char`值转换为单个字符(返回结果)或字符范围[ `b`,`e`]上的刻面的字符类型(转换后的字符放入从`*o`开始的输出范围中);`e`又回来了)。变换的角色从不属于它们的源角色不属于的类。 | 、```cpp 和`@`之外的所有`space`和【ASCII 字符),关系`widen(narrow(c,0)) == c`才保证成立。如果没有现成的转换字符,则使用给定的默认值`char d`。 |<locale>
头为ctype
面的那些函数定义了一系列方便函数,这些函数也存在于<cctype> and <cwctype>
: std::is
类(c, locale&)
中,a 类名称来自表 6-1 和tolower(c, locale&)
/ toupper(c, locale&)
。它们的实现都有如下形式(返回类型不是bool
就是CharT
):
template <typename CharT> ... function(CharT c, const std::locale& l) {
return std::use_facet<std::ctype<CharT>>(l).function(c);
}
```cpp
##### 字符编码转换
一个`std::codecvt`刻面在两种字符编码之间转换字符序列。这在前面的“字符编码转换”中有所解释,因为这些方面在语言环境之外也很有用。每个`std::locale`至少包含表 6-2 中列出的四个`codecvt`专门化的实例,它们实现了潜在的特定于地区的转换器。这些被 Chapter 5 的流隐式地使用,例如在宽字符串和窄字符串之间进行转换。因为不建议直接使用这些低级方面,所以我们在这里不解释它们的成员。请始终使用“字符编码转换”一节中讨论的助手类。
#### 字符串排序和散列
`std::collate<CharType>`方面实现了以下依赖于地区的字符串排序比较和散列函数。使用 begin(包含)和 end(不包含)`CharType*`指针指定所有字符序列:
<colgroup><col> <col></colgroup>
| 成员 | 描述 |
| --- | --- |
| `compare()` | 两个字符序列的依赖于区域设置的三向比较,如果第一个在第二个之前,则返回- `1`,如果两个相等,则返回`0`,否则返回`+1`。不一定等同于简单的字典序比较。 |
| `transform()` | 将给定的字符序列转换为特定的规范化形式,该形式作为`basic_string<CharType>`返回。对两个转换后的字符串应用简单的词典排序(就像对它们的`operator<`)会返回与对未转换的序列应用 facet 的`compare()`函数相同的结果。 |
| `hash()` | 返回给定序列的一个`long`散列值(参见第三章的散列),该散列值对于所有`transform()`具有相同规范化形式的序列都是相同的。 |
一个`std::locale`本身是一个类似于`std::less<std::basic_string<CharT>>`的函子(见第二章),使用其`collate<CharT>` facet 的`compare()`函数比较两个`basic_string<CharT>`。下面的示例使用经典区域设置和法语区域设置(要使用的区域设置名称是特定于平台的)按字典顺序对法语字符串进行排序。除了`<locale>`,还需要`<vector>`、`<string>`、`<algorithm>`:
![A417649_1_En_6_Figp_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figp_HTML.gif)
#### 消息检索
`std::messages<CharT>`方面有助于从消息目录中检索文本消息。这些目录本质上是将一系列整数映射到本地化字符串的关联数组。原则上,这可用于根据错误类别和代码检索翻译后的错误信息(参见第八章)。哪些目录可用,以及它们的结构如何,完全是特定于平台的。对于某些应用程序,使用了标准化的消息目录 API(比如 POSIX 的`catgets()`或 GNU 的`gettext()`),而其他应用程序可能不提供任何目录(Windows 通常就是这种情况)。方面提供了以下功能:
<colgroup><col> <col></colgroup>
| 成员 | 描述 |
| --- | --- |
| `open(n,l)` | 基于给定的特定于平台的字符串`n` (a `basic_string<CharT>`)和给定的`std::locale l`打开一个目录。返回某个有符号整数类型的唯一标识符`catalog`。 |
| `get(c,set,id,def)` | 从具有给定的`catalog`标识符`c`的目录中检索由`set`和`id`(两个`int`值,其解释特定于目录)标识的消息,并将其作为`basic_string<CharT>`返回。如果没有找到这样的消息,则返回`def`。 |
| `close(c)` | 用给定的`catalog`标识符`c`关闭目录。 |
### 组合和自定义语言环境
在组合或定制语言环境方面时,`<locale>`库的构造被设计得非常灵活。
#### 组合面
`std::locale`提供`combine<FacetType>(const locale& c)`,它返回调用`combine()`的`locale`的副本,除了从给定参数复制的`FacetType`方面。这里有一个例子(`using namespace std`是假设的):
![A417649_1_En_6_Figq_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figq_HTML.gif)
或者,`std::locale`有一个构造函数,它接受一个基类`locale`和一个重载方面,这个重载方面做的和`combine()`一样。例如,前面示例中的`combined`的创建可以表示如下:
locale combined(locale::classic(), &use_facet<moneypunct
此外,`std::locale`有许多构造函数可以一次覆盖一个或多个类别的所有方面(`String`或者是一个`std::string`或者是一个表示特定地区名称的 C 风格字符串):
locale(const locale& base, String name, category cat)
locale(const locale& base, const locale& overrides, category cat)
对于表 6-4 中列出的六个类别中的每一个,`std::locale`定义了一个具有该名称的常数。`std::locale::category`类型是位掩码类型,这意味着可以使用位运算符组合类别。例如,`all`常数被定义为`collate | ctype | monetary | numeric | time | messages`。这些构造函数可以用来创建一个类似于前面的`combined`方面:
locale combined(locale::classic(), chinese, locale::monetary);
#### 自定义方面
facet 的所有公共函数 func `()`简单地调用 facet 上的一个受保护的虚拟方法,这个方法叫做`do_` func `()`。 <sup>3</sup> 你可以通过继承现有的方法并覆盖这些`do`-方法来实现定制的方面。
第一个简单的例子改变了`numpunct`方面的行为,使用字符串`"yes"`和`"no"`代替`"true"`和`"false"`进行布尔输入和输出:
class yes_no_numpunct : public std::numpunct
protected:
virtual string_type do_truename() const override { return "yes"; }
virtual string_type do_falsename() const override { return "no"; }
};
您可以使用这个自定义方面,例如,通过将它注入到流中。下面将`"yes / no"`打印到控制台:
std::cout.imbue(std::locale(std::cout.getloc(), new yes_no_numpunct));
std::cout << std::boolalpha << true << " / " << false << std::endl;
回想一下,facet 是引用计数的,`std::locale`的析构函数因此正确地清理了您的自定义 facet。
从像`numpunct`和`moneypunct`这样的方面派生的缺点是,这些通用基类实现了与地区无关的行为。相反,要从特定于地区的方面开始,可以使用方面类,如`numpunct_byname`。对于到目前为止看到的所有刻面,除了`numeric`和`monetary put`和`get`刻面之外,还有一个同名的刻面子类,但是附加了`_byname`。它们是通过传递一个地区名(`const char*`或`std::string`)来构造的,然后表现得好像取自相应的`locale`。您可以从这些方面重写,以便只修改给定区域设置的方面的特定方面。
下一个示例修改了货币标点方面,以便使用会计中的格式标准进行输出:负数放在括号中,填充以特定的方式完成。您可以从`std::moneypunct_byname` ( `string_type`在`std::moneypunct`中定义)开始,而无需覆盖一个地区的货币符号或大多数其他设置:
![A417649_1_En_6_Figr_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figr_HTML.gif)
该面可用于如下用途(参见第五章了解`<iomanip>`的流 I/O 操纵器的详细信息):
![A417649_1_En_6_Figs_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figs_HTML.gif)
这个程序的输出应该是
$ 1,000.00
$ (5.00)
理论上,您可以通过直接从`std::facet`继承来创建一个新的 facet 类,并使用相同的构造函数将其添加到`locale`中,以便稍后在您自己的库代码中使用。唯一的额外要求是定义一个默认构造的静态常量,名为`std::locale::id`类型的`id`。
### c 处所
C 标准库中的区域敏感函数(包括`<cctype>`中的大多数函数以及`<cstdio>`和`<ctime>`的 I/O 操作)不会直接受到全局 C++ `locale`的影响。相反,它们由相应的 C 语言环境控制。这个 C 语言环境通过以下两个函数之一进行更改:
* `std::locale::global()`保证修改 C 语言环境以匹配给定的 C++ `locale`,只要后者有名字。否则,它对 C 语言环境的影响(如果有的话)是由实现定义的。
* 使用`<clocale>`的`std::setlocale()`功能。这丝毫不会影响 C++ 的全局`locale`。
换句话说,当使用标准语言环境时,C++ 程序应该简单地调用`std::locale::global()`。然而,为了在组合多个地区时编写可移植的代码,您必须同时调用 C++ 和 C 函数,因为当将`global()` C++ 地区更改为组合的`locale`时,并非所有的实现都如预期的那样设置 C 地区。这是按如下方式完成的:
![A417649_1_En_6_Figt_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figt_HTML.gif)
`setlocale()`函数采用单个类别号(不是位掩码类型;支持的值至少包括`LC_ALL, LC_COLLATE`、`LC_CTYPE`、`LC_MONETARY`、`LC_NUMERIC`和`LC_TIME`以及一个语言环境名,所有这些都类似于它们的 C++ 等价物。如果成功,它将返回活动 C 语言环境的名称,作为指向重用的全局缓冲区的`char*`指针,如果失败,则返回`nullptr`。如果为语言环境名传递了`nullptr`,则 C 语言环境不会被修改。
不幸的是,C 语言环境的功能远不如 C++ 强大:定制方面或选择单个方面进行组合是不可能的,这使得在可移植代码中使用这种高级语言环境变得不可能。
`<clocale>`表头还有一个功能:`std::localeconv()`。它返回一个指向全局`std::lconv struct`的指针,其公共成员相当于`std::numpunct` ( `decimal_point`、`thousands_sep`、`grouping`)和`std::moneypunct`方面(`mon_decimal_point`、`mon_thousands_sep`、`mon_grouping`、`positive_sign`、`negative_sign`、`currency_symbol`、`frac_digits`等)的函数。这些值应该被视为只读的:写入它们会导致未定义的行为。
## 正则表达式`<regex>`
正则表达式是与目标字符序列匹配的一个或多个模式的文本表示。例如,正则表达式`ab*a`匹配任何以字符`a`开始、后跟零个或多个`b`并再次以`a`结束的目标序列。正则表达式可用于搜索或替换目标中的特定模式,或者验证它是否匹配所需的模式。稍后您将看到如何使用`<regex>`库来执行这些操作;首先,我们介绍如何形成和创建正则表达式。
### ECMAScript 正则表达式语法
用于以文本形式表达模式的语法是由语法定义的。默认情况下,`<regex>`使用 ECMAScript 脚本语言(以其广泛使用的方言 JavaScript、JScript 和 ActionScript 而闻名)使用的语法的修改版本。以下是这种语法的简明、全面的参考。
正则表达式模式是术语序列的析取,每个术语要么是一个原子,要么是一个断言,要么是一个量化的原子。表 6-5 和表 6-6 中列出了支持的原子和断言,表 6-7 显示了原子如何被量化以表达重复模式。这些术语在没有分隔符的情况下连接在一起,然后使用`|`运算符选择性地组合成析取项。允许空析取,模式`|`匹配给定模式或空序列。一些例子应该阐明:
* `\r\n?|\n`匹配所有主要平台的换行符序列(即`\r`、`\r\n`或`\n`)。
* `<(.+)>(.*)</\1>`匹配一个类似 XML 的序列,其形式为`<`标签`>`任何东西`</`标签`>`,使用反向引用来匹配结束标签,并在中间进行额外的分组以允许检索第二子匹配(稍后讨论)。
* `(?:\d{1,3}\.){3}\d{1,3}`匹配 IPv4 地址。不过,这个天真的版本也匹配非法地址,比如`999.0.0.1`,而且糟糕的分组会阻止四个匹配的数字在以后被检索。注意,如果没有`?:`,`\1`仍然只表示第三个匹配的号码。
表 6-7。
Quantifiers That Can Be Used for Repeated Matches of Atoms
<colgroup><col> <col></colgroup>
| 数量词 | 意义 |
| --- | --- |
| 原子 `*` | 贪婪地匹配 atom 零次或多次。 |
| 原子 `+` | 贪婪地匹配 atom 一次或多次。 |
| 原子 `?` | 贪婪地匹配原子零次或一次。 |
| 原子 `{i}` | 贪婪地精确匹配 atom】次。 |
| 原子 `{i,}` | 贪婪地匹配 atom `i`或更多次。 |
| 原子 `{i,j}` | 贪婪地在`i`和`j`之间匹配 atom 次。 |
表 6-6。
Assertions Supported by the ECMAScript Grammar
<colgroup><col> <col></colgroup>
| 主张 | 如果当前位置为,则匹配... |
| --- | --- |
| ^ | 目标的开始(除非指定了`match_not_bol`),或者紧随行结束符之后的位置。<sup>4</sup> |
| `| 主张 | 如果当前位置为,则匹配... |
| --- | --- |
| ^ | 目标的开始(除非指定了`match_not_bol`),或者紧随行结束符之后的位置。<sup>4</sup> |
| 目标的结尾(除非指定了`match_not_eol`),或者行结束符的位置。 |
| `\b` | 单词边界:下一个字符是单词字符 <sup>5</sup> ,而前一个字符不是,反之亦然。如果目标以单词字符开始/结束(并且没有分别指定`match_not_bow` / `match_not_eow`),则目标的开始和结束也是单词边界。 |
| `\B` | 不是单词边界:上一个和下一个字符都是单词或非单词字符。当目标的开始和结束是字边界时,见`\b`。 |
| `(?=`图案`)` | 给定模式的下一个匹配位置。这被称为积极的前瞻。 |
| `(?!`图案`)` | 给定模式下一次不匹配的位置。这被称为消极前瞻。 |
表 6-5。
All Atoms with a Special Meaning in the ECMAScript Grammar
<colgroup><col> <col></colgroup>
| 原子 | 比赛 |
| --- | --- |
| `.` | 除行结束符 4 之外的任何单个字符。 |
| `\0`、`\f`、`\n`、`\r`、`\t`、`\v` | 常见的控制字符之一:空、换页(FF)、换行(LF)、回车(CR)、水平制表符(HT)和垂直制表符(VT)。 |
| `\c`信 | 其代码单位等于给定 ASCII 小写或大写字母模 32 的控制字符。例如`\cj == \cJ == \n` (LF) as(码元为`j`或`J` ) % 32 = (106 或 74)% 32 = 10 = LF 的码元。 |
| `\x` hh | 带有十六进制代码单位 hh(正好两个十六进制数字)的 ASCII 字符。例如`\x0A == \n` (LF),以及\x6A `== J`。 |
| `\u` hhhh | 带有十六进制代码单位 hhhh(正好四个十六进制数字)的 Unicode 字符。例如`\u006A == J`和`\u03c0 ==` π(希腊字母 pi)。 |
| `[`类`]` | 给定类的一个角色(见正文):`[abc]`、`[a-z]`、`[[:alpha:]]`等等。 |
| `[^`类`]` | 不属于给定类别的字符(见正文)。例如:`[⁰-9]`、`[^[:s:]]`等等。 |
| `\d` | 十进制数字字符(简称`[[:d:]]`或`[[:digit:]]`)。 |
| `\s` | 空白字符(是`[[:s:]]`或`[[:space:]]`的缩写)。 |
| `\w` | 一个单词字符,即:一个字母数字或下划线字符(简称`[[:w:]]`或`[_[:alnum:]]`)。 |
| `\D`、`\S`、`\W` | `\d`、`\s`、`\w`的补语。换句话说,任何不是十进制数字、空格或单词字符的字符(简称`[^[:d:]]`等等)。 |
| `\`人物 | 给定的字符。只对`\ . * + ? ^ $ ( ) [ ] { } |`有要求,因为没有转义,这些有特殊意义;但是可以使用任何字符,只要`\`字符没有特殊含义。 |
| `(`图案`)` | 匹配模式并创建一个标记的子表达式,将它转换成可以量化的原子。它匹配的序列(称为子匹配)可以从`match_results`中检索或使用反向引用(稍后讨论)引用,当使用`regex_replace()`时,可以在周围模式或替换模式中进一步引用。 |
| `(?:`图案`)` | 同上,但是子表达式没有标记,这意味着子匹配没有存储在`match_results`中,也不能被引用。 |
| `\`整数 | 反向引用:匹配与前面带有索引整数的标记子表达式完全相同的序列。子表达式按照它们的左括号在完整模式中出现的顺序从左到右计数,从 1 开始(回忆:`\0`匹配空字符)。 |
Tip
当在 C++ 程序中以字符串形式输入正则表达式时,必须对所有反斜杠进行转义。第一个例子变成了`"\\r\\n?|\\n"`。因为这既乏味又晦涩,我们建议使用原始字符串:例如,`R"(\r\n?|\n)"`。请记住,括号是原始字符串文字符号的一部分,并不构成正则表达式组。
原子和断言的区别在于,前者消耗目标序列中的字符(通常是一个),而后者不消耗。模式中的(量化的)原子一个接一个地消耗目标字符,同时从左到右通过模式和目标序列。对于匹配的断言,特定的条件必须保持在目标中的当前位置(在键入文本时,将其视为插入符号位置)。
表 6-5 中的大部分原子匹配单个字符;只有子表达式和反向引用可以匹配一个序列。任何其他单个字符也是简单匹配该字符的原子。表 6-6 中提到的 match_ xxx 标志可选地传递给后面讨论的匹配函数或迭代器。
#### 字符类别
一个字符类是一个`[` d `]`或`[^` d `]`原子,它定义了一组可能(对于`[` d `]`)或可能(`[^` d `]`)匹配的字符。类定义 d 是一系列的类原子,每一个都
* 个性。
* 从`-`到(包括边界)的字符范围。
* 以反斜杠(`\`)开始:表 6-5 中任何原子的等价物,除了反向引用,具有明显的含义。注意,在这个上下文中,像`* + . $`这样的字符不需要转义,但是`- [ ] : ^`可能需要。同样,在类定义中,`\b`表示退格字符(`\u0008`)。
* 包围在嵌套方括号中的三种特殊字符类原子之一(稍后描述)。
描述符没有分隔符连接在一起。例如:`[_a-zA-Z]`匹配下划线或 A-Z 或 A-Z 范围内的单个字符,而`[^\d]`匹配任何非十进制数字的单个字符。
第一个特殊类原子具有形式`[:`名称`:]`。至少支持以下名称:字符分类一节中解释的所有 12 个字符类的等价物— `alnum`、`alpha`、`blank`、`cntrl`、`digit`、`graph`、`lower`、`print`、`punct`、`space`、`upper`、`xdigit`—以及`d`、`s`、`w`。后者中,`d`和`s`是`digit`和`space`的简称,`w`是与`[:w:]`相当于`_[:alnum:]`的一类字符(注意下划线!).也就是对于经典的`"C"`地区,`[[:w:]] == [_a-zA-Z]`。再比如,`[\D] == [^\d] == [^[:d:]] == [^[:digit:]] == [⁰-9]`。
第二种特殊的类原子类似于`[.` name `.]`,其中 name 是特定于地区和实现的整理元素名。这个名字可以是单个字符 c,在这种情况下`[[.` c `.]]`相当于`[` c `]`。类似地,`[[.comma.]]`可能等于`[,]`。有些名称指的是多字符排序元素:即,在特定字母表及其排序顺序中被视为单个字符的多个字符。后者的可能名称包括有向图的名称:`ae`、`ch`、`dz`、`ll`、`lj`、`nj`、`ss`等等。例如,`[[.ae.]]`匹配两个字符,而`[ae]`匹配一个字符。
最后,形式为`[=` name `=]`的类原子类似于`[.` name `.]`,除了它们匹配作为命名整理元素的相同主等价类的一部分的所有字符。本质上,这意味着法语中的`[=e=]`不仅要匹配 E,还要匹配é、è、ê、E、é等等。同样,德语中的`[=ss=]`应该匹配有向图 ss,但也要匹配 Eszett 字符()。
#### 贪婪与非贪婪量化
默认情况下,表 6-7 中定义的量化原子是贪婪的:它们首先匹配尽可能长的序列,如果匹配不成功,只尝试较短的序列。为了使它们不贪婪,也就是说,让它们首先尝试最短的可能序列,在量词后面添加一个问号(`?`)。
回想一下,比如之前的例子`"<(.+)>(.*)</\1>"`。当在`"<b>Bold</b>, not bold, <b>bold again</b>"`中搜索或替换其第一个匹配时,该模式匹配整个序列。非贪婪版本`"<(.+)>(.*?)</\1>"`,只匹配想要的`"<b>Bold</b>"`。
作为非贪婪量词的替代,也可以考虑负字符类(可能更有效),比如`"<(.+)>([^<]*)</\1>"`。
### 正则表达式对象
`<regex>`库将正则表达式建模为`std::basic_regex<CharT>`对象。其中,至少有两种专门化可用于窄串(`char`序列)和宽串(`wchar_t`序列):`std::regex`和`std::wregex`。示例使用了`regex`,但是`wregex`完全类似。
#### 构造和语法选项
默认构造的`regex`不匹配任何序列。更有用的`regular expressions`是使用以下形式的构造函数创建的:
regex(Pattern, regex::flag_type flags = regex::ECMAScript);
期望的正则表达式`Pattern`可以表示为`std::string`、空终止的`char*`数组、具有`size_t`长度的`char*`(要从缓冲区读取的`char`的数量)、`initializer_list<char>`或由开始和结束迭代器形成的范围。
当给定的模式无效时(不匹配的括号、错误的反向引用等等),抛出一个`std::regex_error`。这是一个带有额外的`code()`成员的`std::runtime_exception`,返回类型为`std::regex_constants::error_type` ( `error_paren`、`error_backref`等等)的 11 个错误代码之一。
最后一个参数决定使用哪种语法,并可用于切换某些语法选项。`flag_type`值是`std::regex_constants::syntax_option_type`值的别名。因为它是位掩码类型,所以它的值可以使用`|`操作符进行组合。支持以下语法选项:
<colgroup><col> <col></colgroup>
| [计]选项 | 影响 |
| --- | --- |
| `collate` | 形式`[a-z]`的字符范围变得对地区敏感。例如,对于法语地区,`[a-z]`应该匹配é、è等等。 |
| `icase` | 字符匹配以不区分大小写的方式进行。 |
| `nosubs` | 没有子表达式的子匹配存储在`match_results`(稍后讨论)。反向引用也可能会失败。 |
| `optimize` | 在构造正则表达式对象的过程中,提示实现优先考虑提高匹配速度而不是性能。 |
| `ECMAScript` | 使用基于 ECMAScript 的正则表达式语法(默认)。 |
| `basic` | 使用 POSIX 基本正则表达式语法(BRE)。 |
| `extended` | 使用 POSIX 扩展正则表达式语法(ERE)。 |
| `grep` | 使用 POSIX 实用程序`grep`(一个 BRE 变种)的语法。 |
| `egrep` | 使用 POSIX 实用程序`grep –E`(一个 ERE 变体)的语法。 |
| `awk` | 使用 POSIX 实用程序`awk`(另一个 ERE 变体)的语法。 |
最后六个选项中,只允许指定一个;如果未指定,则默认使用`ECMAScript`。所有 POSIX 语法都比 ECMAScript 语法老,功能也不如 ECMAScript 语法。因此,使用它们的唯一原因是您已经熟悉它们,或者已经有了预先存在的正则表达式。不管怎样,没有理由在这里详述这些语法。
#### 基本成员函数
`A regex` object 主要用于传递给一个全局函数或迭代器适配器,这将在后面解释,因此没有多少成员函数对其进行操作:
* 一个`regex`可以被复制、移动和交换。
* 可以使用一个新的正则表达式和可选的语法选项使用`assign()`对它进行(重新)初始化,它具有与其非默认构造函数完全相同的一组签名。
* `flags()`成员返回初始化时使用的语法选项标志,`mark_count()`返回其正则表达式中被标记的子表达式的个数(见表 6-5 )。
* 正则表达式`std::locale`由`getloc()`返回。这以多种方式影响匹配行为,并在构造时用活动的全局 C++ 语言环境进行初始化。施工后,可使用`imbue()`功能进行更改。
### 匹配和搜索模式
`std::regex_match()`函数验证完整的目标序列与给定的模式匹配,而类似的`std::regex_search()`函数搜索目标中模式的第一次出现。如果没有找到匹配,两者都返回`false`。这些函数模板有一组类似的重载,所有重载都具有以下形式的签名:
bool regex_match (Target [, Results&], const Regex&, match_flag_type = 0);
bool regex_search(Target [, Results&], const Regex&, match_flag_type = 0);
除了最后一个参数之外,所有参数都以相同的字符类型`CharT`为模板,至少有`char`和`wchar_t`可以实现。至于论点:
<colgroup><col> <col></colgroup>
| 匹配标志 | 影响 |
| --- | --- |
| `match_default` | 使用默认匹配行为(该常量的值为零)。 |
| `match_not_bol``match_not_eol``match_not_bow` | 目标序列中的第一个或最后一个位置不再被认为是行/词的开始/结束。影响`^`、`| 匹配标志 | 影响 |
| --- | --- |
| `match_default` | 使用默认匹配行为(该常量的值为零)。 |
、`\b`和`\B`注释,如表 6-6 中所述。 |
| `match_any` | 如果一个析取关系中的多个析取关系匹配,不需要找到它们中最长的匹配:任何匹配都可以(例如,找到第一个,如果这样可以加快速度的话)。与 ECMAScript 语法无关,因为它已经规定了对析取使用最左边的成功匹配。 |
| `match_not_null` | 该模式将与空序列不匹配。 |
| `match_continuous` | 该模式只匹配从目标序列开始处开始的序列(暗示用于`regex_match()`)。 |
| `match_prev_avail` | 当决定`^`、`$`、`\b`和`\B`注释的行和词的边界时,匹配算法查看`--first`处的字符,其中`first`指向目标序列的开始。置位时,`match_not_bol`和`match_not_bow`被忽略。在连续的目标子序列上重复调用`regex_search()`时非常有用。后面解释的迭代器可以正确地做到这一点,并且是枚举匹配的推荐方法。 |
* 前三个参数的典型组合是`(w)string`、`(w)smatch`、`(w)regex`。
* 除了`basic_string<CharT>`,`Target`序列也可以表示为空终止的`CharT` *数组(也用于字符串),或者一对双向迭代器,用于标记`CharT`序列的边界。在这两种情况下,正常的`Results`类型变成了`std::(w)cmatch`。
* 用于可选匹配输出参数的类型将在下一小节中讨论。
* 传递的`Regex`对象没有被复制,所以这些函数不能使用临时对象调用。
* 为了控制匹配行为,可以传递位掩码类型`std::regex_constants::match_flag_type`的值。下表显示了支持的值:
如果任何一个算法失败,就会产生一个`std::regex_error`。因为正则表达式的语法已经在构造`regex`对象时得到了验证(见前面),如果算法耗尽了资源,这种情况很少发生在非常复杂的表达式中。
#### 匹配结果
一个`std::match_results<CharIter>`实际上是一个`sub_match<CharIter>`元素的顺序容器(参见第三章),这些元素是指向目标序列的双向`CharIter`的`std::pair`,标记子匹配序列的边界。在索引 0 处,有一个用于完全匹配的`sub_match`,后面是每个标记的子表达式的一个`sub_match`,按照它们的左括号在正则表达式中出现的顺序(见表 6-5 )。提供了以下模板专门化:
<colgroup><col> <col> <col> <col></colgroup>
| 目标 | 匹配结果 | 子匹配 | 宪章 |
| --- | --- | --- | --- |
| `std::string` `std::wstring` | `std::smatch` `std::wsmatch` | `std::ssub_match` `std::wssub_match` | `std::string::const_iterator` `std::wstring::const_iterator` |
| `const char*` `const wchar_t*` | `std::cmatch` `std::wcmatch` | `std::csub_match``std::wcsub_` | `const char*` `const wchar_t*` |
##### 标准::子匹配
除了从`std::pair`继承的`first`和`second`成员,`sub_match` es 还有第三个成员变量叫做`matched`。如果匹配失败或者如果相应的子表达式没有参与匹配,则该布尔为`false`。例如,如果子表达式是非匹配析取项的一部分,或者是用`?`、`*`或`{0,` n `}`量化的非匹配原子的一部分,则会出现后一种情况。例如,当将`"(a)?b|(c)"`与`"b"`进行匹配时,匹配成功,匹配的`match_result`包含两个空的`sub_match`和`matched == false`。
下表总结了可用于`sub_match` es 的操作:
<colgroup><col> <col></colgroup>
| 操作 | 描述 |
| --- | --- |
| `length()` | 匹配序列的长度(如果不是`matched`则为 0) |
| `str()` /演职人员 | 将匹配序列作为`std::basic_string`返回 |
| `compare()` | 如果`sub_match`等于给定的`sub_match`、`basic_string`或空终止字符数组,则返回 0,如果大于/小于给定的`sub_match`、【】或空终止字符数组,则返回正/负数 |
| `==, !=,` `<, <=, >, >=` | 用于在`sub_match`和`sub_match`、`basic_string`或字符数组之间进行`compare()`运算的非成员运算符,反之亦然 |
| `<<` | 流向输出流的非成员运算符 |
##### 标准::匹配结果
使用`==`和`!=`可以复制、移动、交换和比较`match_results`是否相等。除了这些操作之外,还可以使用以下成员函数(省略了与自定义分配器相关的函数)。注意,与字符串不同,`size()`和`length()`在这里是不等价的:
<colgroup><col> <col></colgroup>
| 操作 | 描述 |
| --- | --- |
| `ready()` | 默认构造的`match_results`未就绪,在执行匹配算法后变为就绪。 |
| `empty()` | 返回`size()==0`(如果不是`ready()`或者匹配失败后返回`true`)。 |
| `size()` | 如果`ready()`匹配成功,返回包含的`sub_match`的数量(1 加上标记的子表达式的数量),否则返回零。 |
| `max_size()` | 由于实施或内存限制,理论上的最大值`size()`。 |
| `operator[]` | 返回带有指定索引 n 的`sub_match`(见前面)或带有`sub.matched == false`的空`sub_match sub`(如果 n `>= size()`)。 |
| `length(size_t=0)` | `results.length(` n `)`相当于`results[` n `].length()`。 |
| `str(size_t=0)` | `results.str(` n `)`相当于`results[` n `].str()`。 |
| `position(size_t=0)` | 目标序列的起点和`results[` n `].first`之间的距离。 |
| `prefix()` | 返回从目标序列开始(包含)到匹配序列开始(不包含)的范围内的一个`sub_match`。对于`regex_match()`总是空的。未定义如果不是`ready()`。 |
| `suffix()` | 返回一个`sub_match`,范围从完全匹配的结尾(不包括)到目标序列的结尾(包括)。对于`regex_match()`总是空的。未定义如果不是`ready()`。 |
| `begin()`、`cbegin()`、`end()`、`cend()` | 返回指向包含在`match_results`中的第一个或者倒数第二个`sub_match`的迭代器。 |
| `format()` | 根据指定的格式格式化匹配的序列。不同的重载(基于字符串或基于迭代器)有输出、模式和格式标志参数,类似于后面解释的`std::regex_replace()`函数。任何`match_xxx`标志都被忽略;只使用`format_yyy`旗。 |
#### 例子
下面的例子说明了`regex_match()`、`regex_search()`和`match_results` ( `smatch`)的用法:
![A417649_1_En_6_Figu_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figu_HTML.gif)
但是枚举所有匹配的首选方法是使用下一小节中讨论的迭代器。
### 匹配迭代器
`std::regex_iterator`和`regex_token_iterator`类有助于遍历目标序列中模式的所有匹配。和`match_results`一样,两者都是用一种字符迭代器(`CharIter`)模板化的。对于最常见的情况,也存在四种类似的`typedef`:前缀为`s`、`ws`、`c`或`wc`的迭代器类型。例如,上一小节末尾示例中的`while`循环可以重写如下:
![A417649_1_En_6_Figv_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figv_HTML.gif)
换句话说,`regex_iterator`是一个前向迭代器,它枚举一个模式的所有`sub_match` es,就像通过重复调用`regex_search()`找到的一样。之前的`for_each()`循环不仅更短更清晰,而且总体上比我们天真的`while`循环更正确:例如,迭代器在第一次迭代后设置`match_prev_avail`标志。只有一个非平凡的构造函数可用,创建一个指向目标序列中给定`Regex`的第一个`sub_match`(如果有的话)的`regex_iterator<CharIter>`,该目标序列由两个双向`CharIters`限定:
regex_iterator(CharIter, CharIter, const Regex&, match_flag_type = 0);
类似于 a `regex_iterator`枚举`match_results`,a `regex_token_iterator`枚举这些`match_results`中包含的所有或特定的`sub_match` es。例如,同一个示例可以写成
![A417649_1_En_6_Figw_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-c-cpp-zh/raw/master/docs/cpp-stdlib-quick-ref/img/A417649_1_En_6_Figw_HTML.gif)
`regex_token_iterator`的构造函数类似于`regex_iterator`的构造函数,但是有一个额外的参数来指示要枚举哪些`sub_match`。为单个`int`(如示例中所示)、`vector<int>`、`int[`、`]`和`initializer_list<int>`定义重载。例如,将示例中的`2`替换为`{0,1}`,输出`"<b>Bold</b>"`、`"b"`、`"<b>bold again</b>"`,然后输出`"b"`。省略时,该参数默认为`0`,表示仅枚举完整模式`sub_match`(然后该示例打印`"<b>Bold</b>"`和`"<b>bold again</b>"`)。
regex_token_iterator 的最后一个参数也可以是-1,这将把它变成一个字段拆分器或标记器。这是对来自<cstring>的 C 函数 strtok()的安全替代。在这种模式下,regex_token_iterator 遍历所有不匹配正则表达式模式的子序列。例如,它可以用于将逗号分隔的字符串拆分成不同的字段(或标记)。在这种情况下使用的正则表达式只是“,”。</cstring>
### 替换模式
最终的正则表达式算法`std::regex_replace()`,用另一个模式替换给定模式的所有匹配。签名如下:
String regex_replace(Target, Regex&, Format, match_flag_type = 0);
Out regex_replace(Out, Begin, End, Regex&, Format, match_flag_type = 0);
和以前一样,参数类型被模板化为相同的字符类型`CharT`,至少支持`char`和`wchar_t`。替换的`Format`被表示为一个`(w)string`或者一个空终止的 C 风格字符串。对于目标序列,有两组重载。第一个函数将`Target`表示为一个`(w)string`或 C 风格的字符串,并将结果作为`(w)string`返回。第二个使用双向`Begin`和`End`字符迭代器表示目标,并将结果复制到输出迭代器`Out`。后者的返回值是一个迭代器,指向输出的最后一个字符之后的一个字符。
给定`Regex`的所有匹配被替换为`Format`序列,默认情况下可能包含以下特殊字符序列:
<colgroup><col> <col></colgroup>
| 格式 | 更换 |
| --- | --- |
| 【例】n | 匹配的第 n 个标记子表达式的副本,其中 n > 0 被计为具有反向引用:参见表 6-5 。 |
| `| `$&` | 整场比赛的拷贝。 |
| `$`` | 前缀的副本,即匹配之前的目标部分。 |
| `$´` | 后缀的一个副本,后缀是匹配之后的目标的一部分。 |
| `$$` | 一个`$`字符(这是唯一需要的转义)。 |
` | 整场比赛的拷贝。 |
| `| 格式 | 更换 |
| --- | --- |
| 【例】n | 匹配的第 n 个标记子表达式的副本,其中 n > 0 被计为具有反向引用:参见表 6-5 。 |
` | 前缀的副本,即匹配之前的目标部分。 |
| `$´` | 后缀的一个副本,后缀是匹配之后的目标的一部分。 |
| `$| 格式 | 更换 |
| --- | --- |
| 【例】n | 匹配的第 n 个标记子表达式的副本,其中 n > 0 被计为具有反向引用:参见表 6-5 。 |
| 一个`| 格式 | 更换 |
| --- | --- |
| 【例】n | 匹配的第 n 个标记子表达式的副本,其中 n > 0 被计为具有反向引用:参见表 6-5 。 |
字符(这是唯一需要的转义)。 |
与前面类似,只有当算法没有足够的资源来评估匹配时,才会抛出一个`std::regex_` `error`。
例如,下面的代码打印了`"d*v*w*l*d"`和`"debolded"`:
std::regex vowels("[aeiou]");
std::cout << std::regex_replace("devoweled", vowels, "*") << '\n';
std::regex bolds("(.*?)");
std::string target = "debolded";
std::ostream_iterator
std::regex_replace(out, target.cbegin(), target.cend(), bolds, "$1");
最后一个参数也是一个`std::regex_constants::match_flag_` `type`,对于`regex_replace()`,它可以用来调整正则表达式的匹配行为——使用前面列出的相同的`match_xxx`值——以及替换的格式。对于后者,支持以下值:
<colgroup><col> <col></colgroup>
| 格式标志 | 影响 |
| --- | --- |
| `format_default` | 使用默认格式(该常量的值为零)。 |
| `format_sed` | 对`Format`使用与 POSIX 实用程序`sed`相同的语法。 |
| `format_no_copy` | `Target`序列中与正则表达式模式不匹配的部分不会被复制到输出中。 |
| `format_first_only` | 只有第一次出现的模式会被替换。 |
Footnotes 1
这些类还有两个可选的模板参数:一个指定要无错误输出的最大代码点的数字,一个带有可能值`little_endian`(输出编码)和`consume_header` / `generate_header`(读/写初始 BOM 头以确定字符顺序)的`codecvt_mode`位掩码值(默认为`0`)。
2
此示例在 Visual Studio 2015 中不起作用。将`char32_t`替换为`__int32`,将`u32string`替换为`basic_string<__int32>`后编译,但结果是错误的。
3
几乎所有的函数:为了性能,`ctype<char>`专门化的`is()`、`scan_is()`和`scan_not()`不调用虚函数,而是在`mask*`数组中执行查找(`ctype::classic_table()`用于`"C"`地区)。可以通过将自定义查找数组传递给方面的构造函数来创建自定义实例。
4
行结束符是以下四种字符之一:换行符(`\n`)、回车符(`\r`)、行分隔符(`\u2028`)或段落分隔符(`\u2029`)。
5
单词字符是`[[:w:]]`或`[_[:alnum:]]`类中的任何字符:即下划线或任何字母或数字字符。
# 七、并发
## 线程`<thread>`
线程是能够编写并行运行的代码的基本构件。
### 启动新线程
要在新的执行线程中运行任何函数指针、仿函数或 lambda 表达式,请将它传递给`std::thread`的构造函数,以及任意数量的参数。例如,这两行是等价的:
```cpp
std::thread worker1(function, "arg", anotherArg);
std::thread worker2([=] { function("arg", anotherArg); });
在从thread
的构造函数返回之前,带有参数的函数在新启动的执行线程中被调用。
函数及其参数必须首先被复制或移动(例如,对于临时对象或如果使用了std::move()
)到这个新线程可访问的内存中。因此,要将引用作为参数传递,首先必须使其可复制:例如,使用std::ref()
/ std::cref()
包装它。当然,您也可以简单地使用带有引用捕获的 lambda 表达式。函子、引用包装器和 lambda 表达式都将在第二章中详细讨论。
thread
类不提供任何检索函数结果的工具。相反,它的返回值被忽略,如果它引发了一个未被捕获的异常,就调用std::terminate()
(默认情况下会终止进程:参见第八章)。通过使用在<future>
头文件中定义的结构,检索函数结果变得更加容易,这将在本章后面详述。
Tip
为了异步执行一个函数并在以后检索它的结果,推荐使用std::async()
(在<future>
中定义)而不是thread
。这通常更容易也更有效(实现async()
可能使用线程池)。为不一定返回结果的长期运行的并发任务保留线程的使用。
线程的生存期
如果一个std::thread
与一个执行线程相关联,那么它就是可接合的。使用用函数 start out joinable 初始化的joinable(). thread
来查询该属性,而默认构造的函数 start out non-joinable。之后,线程实例可以按预期移动和交换。然而,复制thread
对象是不可能的。这确保了在任何时候,最多一个thread
实例代表一个给定的执行线程。底层本地线程表示的句柄可以通过可选的native_handle()
成员获得。
关于std::thread
s,需要记住的两个最重要的事实如下:
- 即使在线程函数执行完毕后,
thread
仍保持可连接状态。 - 如果一个
thread
对象在被析构时仍然是可连接的,那么从它的析构函数中调用std::terminate()
。
因此,为了确保后一种情况不会发生,一定要确保最终在每个可连接的thread
上调用以下函数之一:
join()
:阻塞,直到线程函数执行完毕detach()
:解除thread
对象与可能继续执行的线程的关联
注意,分离thread
是以一劳永逸的方式异步执行函数的唯一标准方式。
std::thread
没有提供终止、中断或恢复底层执行线程的方法。因此,停止线程功能或与之同步必须使用其他方法,如互斥或条件变量,这两种方法将在本章后面讨论。
线程标识符
每个活动线程都有一个唯一的thread::id
,它提供了线程标识符通常需要的所有操作:
- 它们可以输出到字符串流(例如,用于日志记录)。
- 可以使用
==
对它们进行比较(例如,测试/断言某个函数在某个特定线程上执行)。 - 它们可以在有序和无序的关联容器中用作键:所有的比较操作符(
<
、>=
等等)都被定义,就像std::hash()
的专门化一样。
如果一个std::thread
对象是可连接的,你可以在它上面调用get_id()
来获得相关线程的标识符。所有不可连接的thread
都有一个等同于默认构造的thread::id
的标识符。要获取当前活动线程的标识符,还可以调用全局std::this_thread::get_id()
函数。
实用功能
静态std::thread::hardware_concurrency()
函数返回当前硬件支持的并发线程数(或其近似值),如果无法确定,则返回零。这个数字可能大于物理内核的数量:例如,如果硬件支持同步多线程(英特尔称之为超线程),这将是内核数量的偶数倍(通常是两倍)。
除了get_id()
,std::this_thread
名称空间包含三个额外的函数来操纵当前的执行线程:
yield()
提示实现重新调度,允许其他活动线程继续执行。sleep_for(duration)
和sleep_until(time_point)
暂停当前线程一段时间或直到给定时间;使用第二章中描述的<chrono>
中的类型指定超时。
例外
除非在此注明,否则<thread>
中的所有函数都声明为noexcept
。几个std::thread
成员调用本地系统函数来操纵本地线程。如果这些失败,则抛出一个std::
system_error
并带有以下错误代码之一(参见第八章了解更多关于system_error
s 和错误代码的信息):
resource_unavailable_try_again
如果在构造函数中不能创建新的本机线程invalid_argument
如果join()
或detach()
在不可连接的线程上被调用no_such_process
如果join()
或detach()
被调用并且线程无效resource_deadlock_would_occur
如果从相应的执行线程调用可加入线程上的join()
通过抛出一个std::bad_
alloc
的实例或者一个从bad_alloc
派生的类,也可以报告在构造函数中分配存储失败。
期货<future>
<future>
头提供了从正在、将要或已经执行的函数中检索结果(值或异常)的工具,通常在不同的线程中。从概念上讲,线程安全的通信通道是在单个提供者和一个或多个返回对象(T
可能是void
或引用类型)之间建立的:
共享状态是一个内部引用计数对象,在单个提供者和一个或多个返回对象之间共享。提供者异步地将结果存储到它的共享状态中,然后该状态被称为就绪。获得这个结果的唯一方法是通过一个相应的返回对象。
返回对象
所有返回对象都有一个同步的get()
函数,该函数会阻塞,直到相关的共享状态就绪,然后或者返回提供的值(可能是void
)或者在调用线程中重新抛出提供的异常。
要等到结果准备好而不实际检索它,使用等待函数之一:wait()
、wait_until(time_point)
或wait_for(duration)
。前者无限期等待,后两者等待的时间不会超过使用<chrono>
(第二章中定义的类型之一指定的超时时间。
与共享状态相关联的返回对象被认为是有效的。可以使用valid()
检查有效性。有效的future
不能直接构造,但必须总是从共享状态的单一提供者获得。
std::futures
有两个重要的限制:
- 每个共享状态只能有一个有效的
future
,就像只能有一个提供者一样。也就是说,每个提供者只允许创建一个future
,并且future
永远不能被复制,只能被移动(future
也不能被交换)。 get()
只能调用一次;也就是说,调用get()
释放了future
对共享状态的引用,使得future
无效。在这之后再次调用get()
抛出一个异常。本节末尾总结了出现的异常以及出现的时间。
一个shared_future
完全等价于一个future
,但没有这两个限制:即它们可以被复制,get()
可能被调用不止一次。一个shared_future
是通过在一个future
上调用share()
获得的。这也只能做一次,因为它使future
无效。但是一旦你有了一个shared_future
,更多的可以通过复制来创造。以下是一个概述:
提供者
<future>
库提供了三个不同的提供者:std::async()
、packaged_task
和promise
s。本节将依次讨论每一个。作为异步计算的工作负载示例,我们使用以下最大公约数函数:
异步ˌ非同步(asynchronous)
在返回可用于检索结果的std::future
对象之前,调用std::async()
调度给定函数的异步执行:
与std::thread
构造函数一样,几乎可以使用任何类型的函数或函数对象,函数及其参数都被移动或复制到它们的异步执行上下文中。
一旦函数执行完毕,函数调用的结果就进入共享状态。如果函数抛出一个异常,这个异常被捕获并被放入共享状态;如果成功,返回值将被移动到那里。
该标准定义了额外的对std::async()
的覆盖,将std::launch
的实例作为第一个参数。支持的值至少包括以下enum
值(允许实现定义更多):
- 使用
std::launch::async
,函数就像在一个新的执行线程中一样被执行,尽管实现可能使用例如线程池来提高性能。 - 对于
std::launch::deferred
,直到对async()
的这次调用的返回对象之一调用get()
时,该函数才被执行。该函数在调用get()
的第一个线程中执行。
这些选项可以使用|
操作符进行组合。例如,组合async | deferred
鼓励实现利用任何可用的并发性,但是如果没有足够的并发性,允许推迟到调用get()
时。这种组合也是在没有指定显式启动策略时使用的默认策略。
当使用包含async
的启动策略时,有一个重要的注意事项(也就是说,使用默认策略)。从概念上讲,执行异步函数的线程归共享状态所有,共享状态的析构函数与之相联。结果,下面变成了f()
的同步执行:
这是因为async()
返回的临时future
的销毁会一直阻塞到f()
执行完毕(内部共享状态的销毁与f()
运行的thread
汇合)。
Tip
要启动一个函数而不等待它的结果,也就是所谓的一劳永逸,创建一个std::thread
对象并detach()
它。
打包的任务
一个packaged_task
是一个函子,当它的operator()
被调用时执行一个给定的函数,然后将结果(即一个值或一个异常)存储到一个共享状态中。例如,这可以用来获取由std::thread
执行的函数的结果(回想一下,thread
函数的返回值被忽略,如果函数抛出异常,则调用std::terminate()
):
用任何函数、仿函数或 lambda 表达式构造的packaged_task
有一个相关的共享状态,因此称为valid()
;默认构造的任务不是valid()
。使用get_future()
可以获得单个future
到get()
函数的结果。
像所有的提供者一样,packaged_task
不能被复制,只能被移动或交换。这就是为什么在前面的例子中,我们必须将任务仿函数移动到thread
(在首先获得它的future
之后)。然而,它是唯一可以被多次使用的提供者:有效的packaged_task
上的reset()
释放其旧的共享状态,并将其与新创建的状态相关联。重置无效任务会引发异常。
有一个额外的成员函数make_ready_at_thread_exit()
,它像operator()
一样执行任务的函数,除了它直到调用线程退出时才使共享状态就绪。这是在销毁所有线程本地对象之后完成的,用于避免争用情况:
承诺
promise
与future
相似,但代表通信通道的输入端,而不是输出端。未来具有阻塞get()
功能,承诺提供非阻塞set_value()
和set_exception()
功能。
新的promise
是默认构造的,不能复制,只能移动或交换。从每个promise
中,可以使用get_future()
获得一个单独的future
。如果请求第二个,则会引发异常。这里有一个例子:
还有第二组成员函数来填充结果:set_value_at_thread_exit()
和set_exception_at_thread_exit()
。这再次推迟了共享状态的准备,直到调用线程退出,从而确保这发生在销毁任何线程本地对象之后。
例外
如果被误用,<future>
头中的大多数函数都会抛出异常。因为所有提供者和返回对象的行为是一致的,所以这一节提供了概述。以下讨论涉及标准异常类以及错误代码和类别的概念,所有这些都将在第八章中详细解释。
像往常一样,默认和移动构造函数、移动赋值操作符和swap()
函数被声明为noexcept
,当然析构函数也从不抛出异常。除了这些,只有valid()
功能是noexcept
。
provider 和 return 对象的大多数其他成员函数在出错时抛出一个std::future_error
,它是std::logic_error
的子类。不过,与std::system_error
更相似的是,future_error
也有一个返回std::error_code
的code()
成员,在本例中,这个成员的category()
等于std::future_category()
(其name()
等于"future"
)。对于future_error
s,error_code
的value()
始终等于错误代码enum
类std::future_
的四个值之一errc
:
broken_promise
,如果get()
在共享状态的返回对象上被调用,而该对象是由共享状态的提供者释放的——因为它的析构函数、移动赋值函数或reset()
函数被调用——而没有首先使共享状态就绪。future_already_retrieved
,如果get_future()
在同一个提供者上被调用两次(没有packaged_task
的reset()
)。promise_already_satisfied
,如果通过set
功能或通过重新执行packaged_task
多次使共享状态就绪。no_state
,如果在没有关联状态的提供程序上调用除前面列出的非抛出成员之外的任何成员。对于非valid()
返回对象,我们鼓励实现也这样做。
当使用async
启动策略时,async()
可能会抛出一个带有错误代码resource_unavailable_try_again
的system_error
,如果它无法创建一个新线程的话。
互斥<mutex>
互斥(互斥的缩写)是同步对象,用于防止或限制对共享内存和其他资源(如外围设备、网络连接和文件)的并发访问。
除了大量的互斥和锁类型选择之外,<mutex>
头还定义了std::call_once()
,用于确保给定的函数只被调用一次。本节末尾介绍了call_once()
实用程序。
互斥和锁
std::mutex
对象m
的基本用法如下:
lock()
函数会一直阻塞,直到线程获得互斥体的所有权。对于一个基本的std::mutex
对象,在任何给定时间只有一个线程被授予独占所有权。目的是只有拥有给定互斥体的线程才被允许访问它所保护的资源,从而防止数据竞争。一个线程保留这个所有权,直到它通过调用unlock()
来释放它。一旦解锁,另一个被阻塞在mutex
上的线程(如果有的话)被唤醒并被授予所有权。线程被唤醒的顺序是未定义的。
至关重要的是,任何和所有对锁定函数的成功调用都与对unlock()
的调用成对出现。为了确保这是以一致和异常安全的方式完成的,您应该避免直接调用这些锁定和解锁函数,而是使用资源获取是初始化(RAII)习惯用法。为此,标准库提供了几个锁类。最简单、最精简的锁是lock_guard
,它简单地在其构造函数中调用lock()
,在其析构函数中调用unlock()
:
例子
结果 2000。移除lock_guard
几乎肯定会导致小于 2000 的值,当然,除非您的系统不能并发执行线程。
互斥类型
标准库提供了几种风格的互斥体,每一种都比基本的std::mutex
有更多的功能。更受限制的互斥类型通常可以更有效地实现。
通用功能
除了前面解释的lock()
和unlock()
函数之外,所有的互斥类型还支持try_lock()
,一个lock()
的非阻塞版本。如果可以立即获得所有权,则返回true
;否则返回false
。 2
实现还可能提供一个native_handle()
成员,返回底层本机对象的句柄。
没有一种互斥类型允许复制、移动或交换。
递归
递归互斥体(也称为可重入互斥体)允许已经拥有互斥体的线程调用锁函数。这样做时,锁定会立即成功。但是要小心:为了释放所有权,每次成功调用一个锁函数,都必须调用一次unlock()
。因此,和往常一样,最好使用 RAII 锁对象。
对于非递归互斥类型,按照标准,锁定已经拥有的互斥体的行为是未定义的,但是这很可能导致死锁。
超时设定
定时互斥增加了两个额外的锁功能,它们会一直阻塞到给定的超时:try_lock_for(duration)
和try_lock_until(time_point)
。通常,使用<chrono>
中定义的类型指定超时,在第二章中解释。两个函数都返回一个布尔值:true
如果互斥体的所有权获得成功,或者false
如果指定的超时首先发生。
共享所有权<shared_mutex>
只要不被修改,许多类型的共享资源可以被安全地并发访问。例如,对于共享内存,多个线程可以安全地从一个给定的位置读取数据,只要没有线程同时向它写入数据。在这种情况下,限制对单个线程的读访问过于保守,可能会损害性能。
因此,<shared_mutex>
头定义了支持共享锁定的互斥体,在它们与所有其他互斥体类型共有的独占锁定模式之上。这种互斥体通常也称为读者-作者互斥体或多读者/单作者互斥体。
想要修改/写入资源的线程必须获得互斥体的独占所有权。这是通过使用与所有互斥类型完全相同的一组函数或锁对象来完成的。然而,只想检查/读取资源的线程可以获得共享所有权。获得共享所有权的成员完全类似于获得独占所有权的成员,除了他们的名称中的lock
被替换为lock_shared
;也就是说,它们被命名为lock_shared()
、try_lock_shared_for()
等等。使用unlock_shared()
释放共享所有权。
当一个或多个线程获得共享所有权时,不授予独占所有权,反之亦然。该标准没有定义授予所有权的顺序,也没有定义线程以任何方式被解除阻塞的顺序。
该标准定义的共享锁目前不支持在没有首先解锁的情况下将所有权从共享升级到独占,或者从独占降级到共享。
锁类型
该标准提供了三种锁类型:std::lock_guard、unique_lock 和 shared_lock。
标准::锁定 _ 保护
lock_guard
是一个简单的教科书式的 RAII 模板类:默认情况下,它在构造函数中锁定一个互斥体,在析构函数中解锁。唯一的额外成员是一个构造函数,用于调用线程已经拥有的互斥体。这个构造函数通过传递全局std::adopt_lock
常量来调用:
std::lock_guard<std::mutex> lock(m, std::adopt_lock);
标准::唯一 _ 锁定
虽然lock_guard
很简单,效率也很高,但是它的功能有限。为了促进更高级的场景,标准定义了unique_lock
。
基本用法是一样的:
std::unique_lock<std::mutex> lock(m);
然而,unique_lock
与lock_guard
相比有几个额外的特性,包括:
- 一个
unique_lock
可以移动和交换(当然不能复制)。 - 它有一个
release()
函数来解除它与底层互斥体的关联,而不用解锁它。 - 成员返回一个指向底层互斥体的指针。
然而,真正使unique_lock
与众不同的是,它提供了释放和(重新)获得互斥体所有权的功能。具体来说,它支持与底层互斥类型完全相同的一组锁定函数:lock()
、try_lock()
和unlock()
,以及针对定时互斥类型的定时锁定函数。unique_lock
的锁定函数只能被调用一次,即使底层互斥体是递归的,否则将抛出异常。要检查unique_lock
是否会在销毁时解锁,调用owns_lock()
( unique_lock
也会将这个值转换为布尔值)。
除了带有给定互斥体的显而易见的构造函数之外,unique_lock
类还支持三种可选的构造函数,在这些构造函数中传递一个额外的常量:
adopt_lock
:当互斥体已经被当前线程拥有时使用(类似于等价的lock_guard
构造函数)。defer_lock
:施工中不要上锁的信号;其中一个锁定功能可以在以后用来锁定互斥体。- 尝试在构建期间锁定,但如果失败,则不进行锁定。
owns_lock()
可用于检查是否成功。
std::shared_lock <shared_mutex>
lock_guard
和unique_lock
都管理互斥体的独占所有权。为了可靠地管理共享所有权,<shared_mutex>
定义了std::shared_lock
,除了获取/释放共享所有权之外,它完全等同于unique_lock
。即使他们获得共享所有权,其锁定和解锁成员的名称也不包含shared
。这样做是为了确保shared_lock
满足其他实用程序的要求,如std::lock()
和std::condition_variable_any
,两者将在后面讨论。
锁定多个互斥体
一旦线程需要同时获得多个互斥体的所有权,死锁的风险就迫在眉睫。可以采用不同的技术来防止这种死锁:例如,以相同的顺序锁定所有线程中的互斥锁(容易出错),或者所谓的尝试后退方案。标准库提供了模板化的助手函数来促进这一点:
std::lock(lockable1, lockable2, ..., lockableN);
该函数将一直阻塞,直到获得传递给它的所有可锁定对象的所有权。这些可以是互斥体(在锁定后,您应该使用它们的adopt_lock
构造函数将其转移到 RAII 锁),但也可以是unique_
或shared_lock
(例如,用defer_lock
构造)。尽管标准没有规定如何实现这一点,但是如果所有线程都使用std::lock()
,就不会出现死锁。
当然,也存在相当于std::lock()
的非阻塞std::try_lock()
。它按照对象被传递的顺序对所有对象调用try_lock()
,并返回失败的第一个try_lock()
的从 0 开始的索引,如果它们都成功,则返回-1。如果它未能锁定对象,任何已锁定的对象将首先被再次解锁。
例外
在互斥体被完全构造之前或被析构之后使用它会导致未定义的行为。如果使用得当,只有下面提到的函数可能会抛出异常。
对于互斥体,所有的lock()
和lock_shared()
函数(不是try_
的变体)可能会抛出一个system_error
,其中包含一个错误代码(参见第八章):
operation_not_permitted
,如果调用线程权限不足。resource_deadlock_would_occur
如果实现检测到死锁将会发生。不过,死锁检测只是可选的:千万不要依赖它!device_or_resource_busy
如果因为底层句柄已经锁定而无法锁定。当然,只针对非递归互斥体,但同样:检测只是可选的。
任何超时的锁定函数,包括try_
变量,也可能抛出超时相关的异常。
通过扩展,std::lock()
和 RAII 锁的构造函数和锁定函数也可能抛出相同的异常。如果owns_lock() == true
(即使底层互斥体是递归的),任何 RAII 锁定函数(包括try_
变体)肯定会抛出一个带有resource_deadlock_would_occur
的system_error
,如果owns_lock() == false
,它们的unlock()
成员将抛出一个带有operation_not_permitted
的。
如果任何锁定函数抛出异常,就可以保证没有互斥锁被锁定。
调用一次函数<mutex>
std::call_once()
是一个线程安全的实用函数,确保其他函数最多被调用一次。例如,这对于实现惰性初始化习惯用法很有用:
std::once_flag flag;
...
std::call_once(flag, initialise, "a string argument");
只有用给定的std::once_flag
实例调用call_once()
的单个线程——一个默认可构造的、不可复制的、不可移动的助手类——有效地执行与其一起传递的函数。任何后续调用都没有效果。如果多个线程同时用同一个标志调用call_once()
,那么除了一个之外,所有线程都被挂起,直到执行该函数的线程完成调用。用相同的标志递归调用call_once()
会导致未定义的行为。
函数的任何返回值都会被忽略。如果运行函数抛出异常,这将在调用线程中抛出,并且允许另一个线程使用该标志再次执行。如果有线程被阻塞,其中一个会被唤醒。
请注意,call_once()
通常比容易出错的双重检查锁定(反)模式更有效,应该始终优先使用。
Tip
函数局部静态(又名魔术静态)与call_once()
有着完全相同的语义,但实现起来可能更加高效。因此,尽管call_once()
可以很容易地用于单例设计模式的线程安全实现(留给您作为练习),但建议使用函数局部静态:
Singleton& GetInstance() {
static Singleton instance;
return instance;
}
条件变量<condition_variable>
条件变量是一个同步原语,它允许线程等待,直到某个用户指定的条件变为true
。条件变量总是与互斥体协同工作。这个互斥体还旨在防止检查和设置条件之间的竞争,这本来是由不同的线程完成的。
等待一个条件
假设以下变量在线程间以某种方式共享:
std::mutex m;
std::condition_variable cv;
bool ready = false;
那么等待ready
变成true
的典型模式是
要使用condition_variable
等待,线程必须首先使用std::unique_lock<std::mutex>
锁定相应的互斥体。 3 当wait()
阻塞线程时,它也解锁互斥体:这允许其他线程锁定互斥体以满足共享条件。当一个等待线程被唤醒时,在从wait()
返回之前,它总是首先使用unique_lock
再次锁定互斥体,使得重新检查条件变得安全。
Caution
虽然等待条件变量的线程通常保持阻塞状态,直到对该变量发出通知(稍后讨论),但是它们也有可能(尽管不太可能)在没有通知的情况下随时自动醒来。这些被称为虚假唤醒。这种现象使得像示例中那样始终检查循环中的条件变得至关重要。
或者,所有等待函数都有一个重载,该重载将谓词函数作为参数:可以使用任何返回可以计算为布尔值的函数或仿函数。例如,示例中的循环相当于
cv.wait(lock, [&]{ return ready; });
有两组额外的等待函数,它们永远不会阻塞超过给定的超时:wait_until(time_point)
和wait_for(duration)
。超时总是使用在<chrono>
头中定义的类型来表示。wait_until()和 wait_for()的返回值如下:
- 没有谓词的函数版本从枚举类
std::cv_status
返回值:timeout
或no_timeout
。 - 接受谓词函数的重载返回一个布尔值:
true
,如果谓词在一个通知、一个虚假的唤醒或超时到达后返回true
;否则,他们返回false
。
通知
提供了两个通知函数:notify_all()
,它释放所有等待条件变量的线程,和notify_one()
,它只释放一个线程。未指定唤醒多个等待线程的顺序。
通知通常发生在条件发生变化时:
{ std::lock_guard<std::mutex> lock(m);
ready = true;
}
cv.notify_all();
请注意,在调用通知函数时,通知线程不需要拥有互斥体。事实上,任何未阻塞的线程做的第一件事就是试图锁定互斥体,因此在通知之前释放所有权实际上可能会提高性能。 4
还有一个通知函数,但它是非成员函数,具有以下签名:
void std::notify_all_at_thread_exit(condition_variable& cv,
unique_lock<mutex> lock);
当互斥体已经被调用线程通过给定的unique_lock
拥有时,并且当没有线程正在等待使用不同互斥体的条件变量时,它将被调用;否则,行为是未定义的。当被调用时,它在删除所有线程本地对象后,在线程退出时调度以下操作序列:
lock.unlock();
cv.notify_all();
例外
如果可用内存不足,条件变量的构造函数可能抛出一个std::bad_alloc
,或者如果由于非内存相关的资源限制而无法创建条件变量,则抛出一个带有resource_unavailable_try_again
的std::system_error
作为错误代码。
析构线程仍在等待的条件变量会导致未定义的行为。
同步
非正式地说,对于单线程程序,优化实现(编译器、内存缓存和处理器的组合)受假设规则的约束。本质上,在一个结构良好的程序中,只要程序的可观察行为(I/O 操作等)就好像指令是按照编写的那样执行的,就可以随意地对指令进行重新排序、省略、发明等等。
然而,在多线程程序中,这还不够。如果没有适当的同步,并发访问共享资源不可避免地会导致数据和其他竞争,即使每个线程都遵守假设规则。
尽管对内存模型的完整、正式的描述超出了本快速参考的范围,但本章还是对不同构造所施加的同步约束进行了简要的非正式介绍,重点放在编写多线程程序时的实际应用上。我们首先使用互斥体介绍所有基本的同步原理。回忆以下内容:
首先,同步构造引入了对单个执行线程中允许的代码重新排序的约束。例如,锁定和解锁互斥体会注入特殊指令,分别称为获取和释放栅栏。这些指令告诉实现(不仅仅是编译器,还有所有执行代码的硬件!)遵守这些规则:任何代码都不能上移获取栏或下移发布栏。总之,这确保了没有代码在临界区之外执行,临界区在lock()
和unlock()
之间。
第二,栅栏在不同的执行线程之间施加约束。这可以解释为对允许并发线程的指令交错到假想的单个指令序列中的限制。例如,在一个线程中释放互斥体的所有权被认为是与在另一个线程中获取互斥体的所有权同步:本质上,在任何交错中,前者必须发生在后者之前。结合前面解释的线程内约束,这意味着在后一个线程进入其临界段之前,前一个线程的整个临界段被保证完全执行。
对于条件变量,同步属性由相应互斥体上的操作所隐含。
对于std::thread
s,以下适用:
- 当启动一个
thread
时,它的构造函数注入一个释放栅栏,它与线程函数执行的开始同步。这意味着您可以在启动thread
之前写入共享内存(例如,初始化它或传递输入),然后安全地(无需额外的同步)从线程函数中访问它。 - 相反,
thread
函数执行的结束与它的join()
函数内的获取栅栏同步。这确保了加入线程可以安全地读取由线程函数写入的所有共享数据。
最后,对于<future>
头中的构造,通过提供者使共享状态就绪包含一个释放栅栏,它与同一共享状态的返回对象的get()
内的获取栅栏同步。因此,调用get()
的线程不仅可以安全地读取结果(幸运的是),还可以安全地读取提供者编写的任何其他值。例如,future<void>
可以用来等待,直到一个线程完成了对共享内存的异步写入。或者一个future<T*>
可以指向由提供者函数创建的整个数据结构。
Note
所有这些可以总结如下:不同步的数据竞争(线程并发访问内存,至少有一次写入)的行为是未定义的。然而,只要您始终使用标准库提供的同步结构,您的程序通常会完全按照预期运行。
原子操作<atomic>
首先也是最重要的是,<atomic>
头定义了两种类型的原子变量,其操作是原子的或无数据竞争的特殊变量:std::atomic<T>
和std::atomic_flag
。此外,它提供了一些低级函数来显式地引入栅栏,如本节末尾所解释的。
原子变量
std::atomic<T>
类型的变量大多表现得像常规的T
变量——感谢明显的构造函数、赋值和强制转换操作符——提供了一组有限的细粒度原子操作,具有特定的内存一致性属性。稍后会有更多的细节,但是首先我们介绍一下atomic<T>
的模板专门化。
模板专门化和类型定义
atomic<T>
模板至少可以与任何普通的可复制的 5 类型T
一起使用,并且为布尔以及所有其他整型和指针类型T*
定义了专门化。后两者提供了额外的操作,如下所述。
对于布尔和整数特化,定义了便利的typedef
。对于std::atomic<xxx>
,这些大多等于std::atomic_xxx
。具体来说,对于xxx
等于bool
、char
、char16_t
、char32_t
、wchar_t
、short
、int
、long
或者<cstdint>
中定义的任何整数类型都是如此(参见第章 1 )。对于剩余的整数类型,typedef
缩写了xxx
类型的第一个字:
通用原子操作
一个atomic<T>
变量的默认构造函数的行为与一个常规T
变量的声明完全一样:也就是说,它通常不初始化值;只有静态或线程本地的atomic
变量是零初始化的。用给定的T
值初始化的构造函数也存在。不过,这种初始化不是原子的:来自另一个线程的并发访问,即使是通过原子操作,也是一种数据竞争。原子变量不能被复制、移动或交换。
所有的atomic<T>
类型都有一个接受T
值的赋值操作符和一个转换为T
的转换操作符,因此可以用作常规的T
变量:
与这些操作符相当的是store()
和load()
成员。例如,前面代码片段的最后两行也可以写成
无论哪种方式,这些操作都是原子的,换句话说,是无数据竞争的。也就是说,如果一个线程同时将一个值存储到一个原子变量中,而另一个线程正在从该原子变量中加载,那么后者看到的要么是存储之前的旧值,要么是新存储的值,而不是两者之间的值(没有半写值)。或者用技术术语来说,不存在撕裂读数。类似地,当两个线程同时存储一个值时,其中一个值被完全存储;从来没有被撕掉的字迹。对于常规变量,这种情况是数据竞争,因此会导致未定义的行为,包括可能的读写错误。
所有的原子变量还提供一些不太明显的原子操作,exchange()
和compare_exchange
。这些成员函数的行为就好像实现如下:
当然,这两种操作都是原子性的。也就是说,它们(有条件地)以这样一种方式交换值,即在交换期间没有线程可以并发地存储另一个值或经历一次损坏读取。
没有名为compare_exchange
的实际成员。相反,有两种不同的变体:compare_exchange_weak()
和compare_exchange_strong()
。唯一(微妙)的区别是前者被允许虚假地失败:也就是说,即使可以进行有效的交换,也会偶尔返回false
。这种“弱”变体可能比“强”变体稍快,但只用于循环中。后者旨在用作独立的陈述。
exchange()
和compare_exchange
操作是实现无锁数据结构的关键构件:不使用阻塞互斥的线程安全数据结构。这是一个高级的话题,最好留给专家们来讨论。不过,一个经典的例子是在单链表的开头添加一个新节点:
本节介绍的所有操作对于任何基本类型T
都是原子的。对于布尔、整数和指针等类型,大多数编译器只是生成一些保证原子性的特殊指令(目前大多数 CPU 都支持这一点)。如果是,lock_free()
返回true
。对于其他类型,原子变量大多依靠类似互斥的结构来实现原子性。对于这样的类型,lock_free()
返回false
。
注意:尽管原子变量确保加载和存储是原子的,但这并不意味着底层对象上的操作是原子的。在下面的例子中,如果另一个线程同时调用person
对象上的GetLastName()
,那么就会与SetLastName()
发生数据竞争:
整数和指针类型的原子操作
某些模板专门化提供了额外的操作符来自动更新变量。选择基于当前硬件通常支持的原子指令(例如,无乘法):
- 原子积分变量:
++
、--
、+=
、-=
、&=
、|=
、^=
- 原子指针变量:
++
、--
、+=
、-=
支持前缀和后缀版本的++
和--
。对于其他操作员,等效的非操作员成员也同样可用:分别为:fetch_add()
、fetch_sub()
、fetch_and()
、fetch_or()
和fetch_xor()
。
同步
除了原子性之外,原子变量的一个鲜为人知的属性是它们提供了与互斥或线程相同的同步保证。具体来说,所有写入变量的操作(store()
、exchange
s、fetch_xxx
())都包含与从同一变量读取的操作(load()
、exchange
s、fetch_xxx()
等)中的获取围栏同步的释放围栏。这使得下面的习惯用法成为可能,在将潜在的复杂对象或数据结构存储到共享原子变量中之前,先对其进行初始化:
任何加载指向新对象的指针的线程(在这个例子中是一个Person
)也可以安全地读取它所指向的所有其他内存(例如名称字符串),只要这是在释放栅栏之前完全写入的。
所有原子操作(当然除了操作符)都接受一个额外的可选参数(或多个参数),允许调用者微调内存顺序约束。可能的值有memory_order_relaxed, memory_order_consume
、memory_order_acquire
、memory_order_release, memory_order_acq_rel
和memory_order_seq_cst
(默认)。例如,第一个选项memory_order_relaxed
表示操作必须是原子的,并且不需要进一步的内存顺序约束。其他选项之间的细微差别超出了本书的范围。除非您是专家,否则我们建议您始终坚持使用默认值。否则,您可能会引入微妙的错误。
原子标志
std::atomic_
flag
是一个简单的、保证无锁的、原子的、类似布尔的类型。它只能是默认构造的,不能复制、移动或交换。没有指定默认构造函数是否初始化该标志。唯一保证有效的初始化就是这个表达式:
一个atomic_flag
只提供另外两个成员:
void clear()
:自动将标志设置为false
bool test_and_set()
:自动将标志设置为true
,同时返回其先前值
这两个函数都具有类似于atomic_bool
s 的同步属性,并且同样接受可选的std::memory_order
参数。
非成员函数
为了与 C 兼容,<atomic>
为std::atomic<T>
和std::atomic_flag
: atomic_init()
、atomic_load()
、atomic_fetch_add()
、atomic_flag_test_and_set()
等等的所有成员函数定义了非成员对等函数。作为一名 C++ 程序员,通常不需要这些:只需使用类的成员函数。
藩篱
<atomic>
头还提供了两个函数来显式创建获取和/或释放围栏:std::atomic_thread_fence()
和std::atomic_signal_fence()
。栅栏的概念在本章前面已经解释过了。两者都采用一个std::memory_order
参数来指定期望的栅栏类型:memory_order_release
表示释放栅栏,memory_order_acquire
或memory_order_consume
表示获取栅栏,memory_order_acq_rel
和memory_order_seq_cst
表示同时是获取和释放栅栏,后一个选项表示栅栏必须是顺序一致的变体(它们的语义差异不在本书讨论范围之内)。带memory_order_relaxed
的栅栏没有效果。
这两个函数的区别在于,后者只限制线程和在同一线程中执行的信号处理程序之间的重新排序。后者只约束编译器,但不注入任何指令来约束硬件(内存缓存和 CPU)。
Caution
不鼓励使用显式栅栏:原子变量或其他同步结构具有更有趣的同步属性,通常应该优先使用。
Footnotes 1
预定由标准库的 C++17 版本添加。
2
虽然通常不常见,但是允许try_lock()
虚假地失败:也就是说,即使互斥体不属于任何其他线程,也返回false
。在设计更高级的同步场景时要考虑到这一点。
3
对于condition_variable
,必须使用这种确切的锁和互斥类型。为了使用其他标准类型,或者任何具有公共lock()
和unlock()
功能的对象,声明更通用的std::condition_variable_any
类,这在其他方面类似于condition_variable
。
4
必须注意:它在设置条件和通知等待线程之间引入了一个竞争条件窗口。在某些情况下,在持有锁的同时进行通知实际上可能会导致更可预测的结果,并避免微妙的竞争。如果有疑问,最好不要在通知时解锁互斥体,因为对性能的影响可能很小。
5
普通的可复制类型没有普通的复制/移动构造函数/赋值,没有虚函数或虚基,也没有普通的析构函数。本质上,这些是可以安全地逐位复制的类型(例如,使用memcpy()
)。
八、诊断
断言<cassert>
断言是布尔表达式,在代码中的给定点应该是true
。<cassert>
的assert
宏定义如下:
#ifdef NDEBUG
#define assert(_)
#else
#define assert(CONDITION) if (!CONDITION) { print_msg(...); std::abort(); }
#endif
如果断言失败,诊断消息将被写入标准错误输出,并调用std::abort()
,这将终止应用程序而不执行任何清理。在调试应用程序时,如果断言失败,某些 ide 会让您选择继续执行。通常的做法是使用断言作为调试辅助,并在构建应用程序的发布版本时定义NDEBUG
,将assert
变成无操作。
断言通常用于检查不变量,比如循环不变量,或者函数前置和后置条件。一个例子是参数验证:
该程序的一个可能输出是
Assertion failed: msg != nullptr, file d:\Test\Test.cpp, line 13
Caution
确保您提供给assert()
的条件没有任何副作用,而这些副作用是正确执行您的程序所必需的,因为如果定义了NDEBUG
(例如,对于一个发布版本),这个表达式就不会被计算。
异常,
在<exception>
中定义的std::exception
,它本身并不打算被抛出,而是作为标准库定义的所有异常的基类,并且可以作为你自己的基类。图 8-1 概述了所有标准例外情况。
图 8-1。
The C++ Standard Library exception hierarchy
一个exception
可以被复制,并提供一个what()
方法来返回错误的字符串表示。此函数是虚拟的,应该被重写。返回类型是const char*
,但是没有指定字符编码(例如,可以使用编码为 UTF-8 的 Unicode 字符串;参见第六章。
在<stdexcept>
中定义的异常是唯一由应用程序代码抛出的标准异常。通常,logic_error
s 代表程序逻辑中可避免的错误,而runtime_error
s 是由超出程序范围的不可预测的事件引起的。logic_error
、runtime_error
和它们的大部分子类(除了system_error
s 和future_error
,它们需要一个错误代码,这将在后面讨论)必须在构造时传递一个std::string
或const char*
指针,之后由what()
返回。因此,无需进一步覆盖what()
。
异常指针<exception>
<exception>
头提供了std::exception_ptr
,一种未指定的类似指针的类型,用于存储和传输捕获的异常,即使不知道具体的异常类型。一个exception_ptr
可以指向任何类型的值,而不仅仅是一个std::exception
。它可以指向自定义异常类、整数、字符串等。只要至少有一个exception_ptr
仍在引用它,任何指向的值都保持有效(也就是说,引用计数的智能指针可用于实现exception_ptr
)。
在<exception>
中定义了几个函数来处理异常指针:
- 当从
catch()
块内部直接或间接调用时,创建并返回一个引用当前正在运行的异常的exception_ptr
(记住,这可以是任何类型的异常)(catch()
块可以调用一个助手函数来处理异常)。如果在没有异常被处理时调用,返回的exception_ptr
指的是空值。
exception_ptr std::current_exception() noexcept
- 创建并返回一个指向
t
的exception_ptr
。
template<typename T>
exception_ptr std::make_exception_ptr(T t) noexcept
- 重新抛出给定的
exception_ptr
指向的异常。这是获得由一个exception_ptr
指向的对象的唯一方法。一个exception_ptr
不能被解引用,也没有 getter 函数。
[[noreturn]] void std::rethrow_exception(exception_ptr)
一旦被创建,exception_ptr
可以被复制、比较,特别是与nullptr
进行赋值和比较。这使得它们在存储和移动异常以及稍后测试异常是否发生时非常有用。为此,exception_ptr
也可以转换为布尔值:true
如果它指向一个异常,false
如果它是一个空指针。默认构造的实例相当于nullptr
。
例如,异常指针可用于将异常从工作线程转移到主线程(注意,这也是上一章讨论的<future>
实用程序隐式为您做的事情):
嵌套异常<exception>
<exception>
头文件还提供了处理嵌套异常的工具。它们允许您将捕获的异常封装在另一个异常中:例如,用额外的上下文信息扩充它,或者将其转换为更适合您的应用程序的异常。std::nested_exception
是一个可复制的 mixin 1 类,其默认构造函数捕获current_exception()
并存储。这个嵌套的异常可以作为一个带有nested_ptr()
的exception_ptr
来检索,或者通过使用rethrow_nested()
来重新抛出它。但是要小心:当调用rethrow_nested()
而没有存储任何异常时,会调用std::terminate()
。因此,通常建议您不要直接使用nested_exception
,而是使用这些辅助方法:
- 抛出一个从
std::nested_exception
和T
派生的未定义类型(去掉了引用限定符),可以使用常规的catch (const T&)
表达式处理,忽略嵌套的异常。作为一个std::nested_exception
,它也包含了std::current_exception()
的结果,可以随意地检索和处理。
[[noreturn]] template<typename T> void std::throw_with_nested(T&& t)
- 如果
t
是从nested_exception
派生出来的,就在上面调用rethrow_nested()
;否则什么也不做。
template <typename T> void std::rethrow_if_nested(const T& t)
下面的示例演示了嵌套异常:
void execute_helper() {
throw std::range_error("Out-of-range error in execute_helper()");
}
void execute() {
try { execute_helper(); }
catch (...) {
std::throw_with_nested(std::runtime_error("Caught in execute()"));
}
}
void print(const std::exception& exc) {
std::cout << "Exception: " << exc.what() << std::endl;
try { std::rethrow_if_nested(exc); }
catch (const std::exception& e) {
std::cout << " Nested ";
print(e);
}
}
int main() {
try { execute(); }
catch (const std::exception& e) { print(e); }
}
这段代码的输出如下:
Exception: Caught in execute()
Nested Exception: Out-of-range error in execute_helper()
系统错误<system_error>
来自操作系统或其他低级 API 的错误称为系统错误。这些由在<system_error>
头中定义的类和函数处理:
-
error_code
: Generally wraps a platform-specific error code (anint
), although for some categories the error codes are defined by the standard (see Table 8-1).表 8-1。
Available Error Category Functions and Corresponding Error Condition and Error Code Enum Classes
| 单一函数 | 错误条件 | 错误代码 | 页眉 | | --- | --- | --- | --- | | `generic_category()` | `std::errc` | | `` | | `system_category()` | | | ` ` | | `iostream_category()` | | `std::io_errc` | ` ` | | `future_category()` | | `std::future_errc` | ` ` | -
error_condition
: Wraps a portable, platform-independent error condition (anint
). The enum classstd::errc
lists the built-in conditions. They correspond to the standard POSIX error codes, defined also as macros in<cerrno>
. See Table 8-2 at the end of this chapter.表 8-2。
| `std::errc enum`值 | `std::errc
Error Condition Values and Corresponding<cerrno>
Macros`宏 | | --- | --- | | `address_family_not_supported` | `EAFNOSUPPORT` | | `address_in_use` | `EADDRINUSE` | | `address_not_available` | `EADDRNOTAVAIL` | | `already_connected` | `EISCONN` | | `argument_list_too_long` | `E2BIG` | | `argument_out_of_domain` | `EDOM` | | `bad_address` | `EFAULT` | | `bad_file_descriptor` | `EBADF` | | `bad_message` | `EBADMSG` | | `broken_pipe` | `EPIPE` | | `connection_aborted` | `ECONNABORTED` | | `connection_already_in_progress` | `EALREADY` | | `connection_refused` | `ECONNREFUSED` | | `connection_reset` | `ECONNRESET` | | `cross_device_link` | `EXDEV` | | `destination_address_required` | `EDESTADDRREQ` | | `device_or_resource_busy` | `EBUSY` | | `directory_not_empty` | `ENOTEMPTY` | | `executable_format_error` | `ENOEXEC` | | `file_exists` | `EEXIST` | | `file_too_large` | `EFBIG` | | `filename_too_long` | `ENAMETOOLONG` | | `function_not_supported` | `ENOSYS` | | `host_unreachable` | `EHOSTUNREACH` | | `identifier_removed` | `EIDRM` | | `illegal_byte_sequence` | `EILSEQ` | | `inappropriate_io_control_operation` | `ENOTTY` | | `interrupted` | `EINTR` | | `invalid_argument` | `EINVAL` | | `invalid_seek` | `ESPIPE` | | `io_error` | `EIO` | | `is_a_directory` | `EISDIR` | | `message_size` | `EMSGSIZE` | | `network_down` | `ENETDOWN` | | `network_reset` | `ENETRESET` | | `network_unreachable` | `ENETUNREACH` | | `no_buffer_space` | `ENOBUFS` | | `no_child_process` | `ECHILD` | | `no_link` | `ENOLINK` | | `no_lock_available` | `ENOLOCK` | | `no_message` | `ENOMSG` | | `no_message_available` | `ENODATA` | | `no_protocol_option` | `ENOPROTOOPT` | | `no_space_on_device` | `ENOSPC` | | `no_stream_resources` | `ENOSR` | | `no_such_device` | `ENODEV` | | `no_such_device_or_address` | `ENXIO` | | `no_such_file_or_directory` | `ENOENT` | | `no_such_process` | `ESRCH` | | `not_a_directory` | `ENOTDIR` | | `not_a_socket` | `ENOTSOCK` | | `not_a_stream` | `ENOSTR` | | `not_connected` | `ENOTCONN` | | `not_enough_memory` | `ENOMEM` | | `not_supported` | `ENOTSUP` | | `operation_canceled` | `ECANCELED` | | `operation_in_progress` | `EINPROGRESS` | | `operation_not_permitted` | `EPERM` | | `operation_not_supported` | `EOPNOTSUPP` | | `operation_would_block` | `EWOULDBLOCK` | | `owner_dead` | `EOWNERDEAD` | | `permission_denied` | `EACCES` | | `protocol_error` | `EPROTO` | | `protocol_not_supported` | `EPROTONOSUPPORT` | | `read_only_file_system` | `EROFS` | | `resource_deadlock_would_occur` | `EDEADLK` | | `resource_unavailable_try_again` | `EAGAIN` | | `result_out_of_range` | `ERANGE` | | `state_not_recoverable` | `ENOTRECOVERABLE` | | `stream_timeout` | `ETIME` | | `text_file_busy` | `ETXTBSY` | | `timed_out` | `ETIMEDOUT` | | `too_many_files_open` | `EMFILE` | | `too_many_files_open_in_system` | `ENFILE` | | `too_many_links` | `EMLINK` | | `too_many_symbolic_link_levels` | `ELOOP` | | `value_too_large` | `EOVERFLOW` | | `wrong_protocol_type` | `EPROTOTYPE` | -
error_category
:错误代码和情况属于一个类别。类别单例对象负责两种数字之间的转换。 -
system_error
:一个异常类(见图 8-1 ),有一个额外的code()
成员返回一个error_code
。
除了一个数值之外,error_code
和error_condition
对象都有一个对它们的error_category
的引用。在一个类别中,一个编号是唯一的,但是同一编号可能被不同的类别使用。
所有这些看起来相当复杂,但是这些错误的主要用途仍然很简单。为了比较一个给定的错误代码,比如来自一个被捕获的system_error
异常的错误代码,可以使用==
和!=
操作符。例如:
if (systemError.code() == std::errc::argument_out_of_domain)
...
Note
与std::ios_base::failure
(第章 5 )和future_error
(第章 7 )工作类似。它们还有一个code()
成员返回一个error_code
,可以使用==
和!=
与已知的代码值(见表 8-1 )进行比较。
标准::错误 _ 类别
不同的std::error_category
实例被实现为单例:也就是说,每个类别只有一个全局的、不可复制的实例。存在许多预定义的类别,可从表 8-1 中列出的全局函数中获得。
一个std::error_category
有以下方法:
标准::错误代码
std::error_code
封装一个错误码值和一个error_category
。有三个构造函数:
- 将错误代码设置为
0
(这通常表示“无错误”)并将其与system_category
相关联的默认设置。 - 一个接受错误代码
int
和一个error_category
。 - 一个是通过调用
std::make_error_code(e)
从错误代码枚举值e
构造一个error_code
。参数类型必须是错误代码枚举类型,即std::is_error_code_enum
类型特征的值为true
的枚举类型(类型特征参见第二章)。这也会自动设置正确的类别。标准类别的枚举类如表 8-1 所示。
要提升你自己的std::system_error
,你必须提供一个error_code
,它可以用它的一个构造函数或者用make_error_code()
来创建。例如:
std::error_code
提供了以下方法:
标准::错误条件
std::error_condition
类封装了一个可移植的条件代码和相关的错误类别。这个类有一组类似于error_code
的构造函数和方法,除了
- 它没有从错误状态到错误代码的
default_error_condition()
方法或等效函数。 - 使用错误条件枚举来代替错误代码枚举:这些枚举类型的
is_error_condition_enum
类型特征的值为true
。 - 使用
std::make_error_code()
的成员使用std::make_error_condition()
代替。
c 错误号<cerrno>
<cerrno>
头定义了errno
,一个扩展到与int&
相等的值的宏。函数可以将errno
的值设置为特定的错误值,以发出错误信号。每个执行线程都有一个单独的errno
。设置errno
对于 C 头文件中的函数来说非常常见。C++ 库大多在失败时抛出异常,尽管有些库也设置了errno
(例如std::string
-数字转换)。表 8-2 列出了由<cerrno>
定义的带有默认 POSIX 错误号的宏。
如果您想使用errno
来检测使用errno
来报告错误的函数中的错误,那么您必须确保在调用该函数之前将errno
设置为0
,就像本例中所做的那样(需要<cmath>
) 2 :
输出取决于您的平台,但可能如下所示:
Error: result out of range
为了完整起见,我们展示了两种报告当前errno
的错误字符串的替代方法。它们分别使用来自<cstring>
的strerror()
(注意:这个函数不是线程安全的!)和来自<cstdio>
的std::perror()
。下面两行打印了一条类似于前面代码的消息:
故障处理<exception>
STD::un capture _ exception()
如果在代码中的任何地方,您想知道当前是否有一个尚未被捕获的异常正在进行中——换句话说,检测堆栈展开正在进行中——使用uncaught_exception()
,如果是这样,它将返回true
。
Note
通常没有理由或安全的方法来使用uncaught_exception()
,所以我们建议不要使用它。这里提到它只是为了完整。
std::terminate()
如果异常处理由于某种原因失败了——例如,异常被抛出但从未被捕获——那么运行时调用std::terminate()
,它调用终止处理程序。默认处理程序调用std::abort()
,这反过来中止应用程序而不执行任何进一步的清理。使用来自<exception>
的以下函数管理主动终止处理程序,其中std::terminate_handler
是函数指针类型,必须指向不带参数的void
函数:
std::terminate_handler std::set_terminate(std::terminate_handler) noexcept
std::terminate_handler std::get_terminate() noexcept
自定义终止处理程序的一个用例是在调用std::terminate()
时自动生成一个进程转储。拥有一个转储文件来进行分析极大地帮助了追踪触发流程到terminate()
的 bug。您应该考虑为任何专业应用程序设置此功能。
std::意外()
如果动态异常规范 3 被忽略,运行时调用std::unexpected()
:也就是说,如果一个函数抛出了它不被允许的东西。类似于terminate()
,这个函数调用一个std::unexpected_handler
函数,可以使用std::set_unexpected()
/ get_unexpected()
来管理这个函数。默认处理程序调用std::terminate()
。
Note
动态异常规范和std::unexpected()
都已被弃用,这里仅是为了完整性而提及。
Footnotes 1
mixin 是一个类,它提供了一些添加到其他类的功能(在这种情况下,存储指向嵌套异常和一些相关函数的指针的能力)。在 C++ 中,mixins 一般通过多重继承来实现。
2
std::exp()
仅对<cmath>
中定义的math_errhandling
包含MATH_ERRNO
的实现设置errno
:参见第一章。不过,大多数情况似乎都是如此。
3
动态异常规范是函数声明的一部分,用逗号分隔的列表指定允许函数抛出哪些异常。比如:ReturnType Func(...) throw(exception1, exception2, ...);
。