# 《C++ Primer中文版(第5版)》笔记 ![Cover](Images/Cover.png) ## 目录 [第1章 开始](#1) ### 第I部分 C++基础 [第2章 变量和基本类型](#第2章) [第3章 字符串、向量和数组](#第3章) [第4章 表达式](#第4章) [第5章 语句](第5章) [第6章 函数](第6章) [第7章 类](第7章) ### 第II部分 C++标准库 [第8章 IO库](第8章) [第9章 顺序容器](第9章) [第10章 泛型算法](第10章) [第11章 关联容器](第11章) [第12章 动态内存](第12章) ### 第III部分 类设计者的工具 [第13章 拷贝控制](第13章) [第14章 操作重载与类型转换](第14章) [第15章 面向对象程序设计](第15章) [第16章 模板与泛型编程](第16章) ### 第IV部分 高级主题 [第17章 标准库特殊设施](第17章) [第18章 用于大型程序的工具](第18章) [第19章 特殊工具与技术](第19章) # 第1章 开始 最简单的`main`函数: ```c++ int main() { return 0; } ``` C++包含两种注释,注释界定符`/**/`通常用于多行注释,而双斜杠`//`通常用于单行或半行注释。 ```c++ #include /* * Simple main function: * Read two numbers and write their sum */ int main() { int sum = 0, val = 1; // keep executing the while as long as val is less than or equal to 10 while (val <= 10) { sum += val; // assigns sum + val to sum ++val; // add 1 to val } std::cout << "Sum of 1 to 10 inclusive is "<< sum << std::endl; return 0; } ``` # 第2章 变量和基本类型 ## 基本内置类型(Primitive Built-in Types) ### 算数类型(Arithmetic Types) 算数类型分为两类:整型(integral type)、浮点型(floating-point type)。 ![2-1](Images/2-1.png) `bool`类型的取值是`true`或`false`。 一个`char`的大小和一个机器字节一样,确保可以存放机器基本字符集中任意字符对应的数字值。`wchar_t`确保可以存放机器最大扩展字符集中的任意一个字符。 在整型类型大小方面,C++规定`short` ≤ `int` ≤ `long` ≤ `long long`(`long long`是C++11定义的类型)。 浮点型可表示单精度(single-precision)、双精度(double-precision)和扩展精度(extended-precision)值,分别对应`float`、`double`和`long double`类型。 除去布尔型和扩展字符型,其他整型可以分为带符号(signed)和无符号(unsigned)两种。带符号类型可以表示正数、负数和0,无符号类型只能表示大于等于0的数值。类型`int`、`short`、`long`和`long long`都是带符号的,在类型名前面添加`unsigned`可以得到对应的无符号类型,如`unsigned int`。 字符型分为`char`、`signed char`和`unsigned char`三种,但是表现形式只有带符号和无符号两种。类型`char`和`signed char`并不一样, `char`的具体形式由编译器(compiler)决定。 如何选择算数类型: - 当明确知晓数值不可能为负时,应该使用无符号类型。 - 使用`int`执行整数运算,如果数值超过了`int`的表示范围,应该使用`long long`类型。 - 在算数表达式中不要使用`char`和`bool`类型。如果需要使用一个不大的整数,应该明确指定它的类型是`signed char`还是`unsigned char`。 - 执行浮点数运算时建议使用`double`类型。 ### 类型转换(Type Conversions) 进行类型转换时,类型所能表示的值的范围决定了转换的过程。 - 把非布尔类型的算术值赋给布尔类型时,初始值为0则结果为`false`,否则结果为`true`。 - 把布尔值赋给非布尔类型时,初始值为`false`则结果为0,初始值为`true`则结果为1。 - 把浮点数赋给整数类型时,进行近似处理,结果值仅保留浮点数中的整数部分。 - 把整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。 - 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数(8比特大小的`unsigned char`能表示的数值总数是256)取模后的余数。 - 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。 避免无法预知和依赖于实现环境的行为。 无符号数不会小于0这一事实关系到循环的写法。 ```C++ // WRONG: u can never be less than 0; the condition will always succeed for (unsigned u = 10; u >= 0; --u) std::cout << u << std::endl; ``` 当*u*等于0时,*--u*的结果将会是4294967295。一种解决办法是用`while`语句来代替`for`语句,前者可以在输出变量前先减去1。 ```c++ unsigned u = 11; // start the loop one past the first element we want to print while (u > 0) { --u; // decrement first, so that the last iteration will print 0 std::cout << u << std::endl; } ``` 不要混用带符号类型和无符号类型。 ### 字面值常量(Literals) 以`0`开头的整数代表八进制(octal)数,以`0x`或`0X`开头的整数代表十六进制(hexadecimal)数。在C++14中,`0b`或`0B`开头的整数代表二进制(binary)数。 整型字面值具体的数据类型由它的值和符号决定。 C++14新增了单引号`'`形式的数字分隔符。数字分隔符不会影响数字的值,但可以通过分隔符将数字分组,使数值读写更容易。 ```c++ // 按照书写形式,每3位分为一组 std::cout << 0B1'101; // 输出"13" std::cout << 1'100'000; // 输出"1100000" ``` 浮点型字面值默认是一个`double`。 由单引号括起来的一个字符称为`char`型字面值,双引号括起来的零个或多个字符称为字符串字面值。 字符串字面值的类型是由常量字符构成的数组(array)。编译器在每个字符串的结尾处添加一个空字符`'\0'`,因此字符串字面值的实际长度要比它的内容多一位。 转义序列: | 含义 | 转义字符 | | :-------------: | :------: | | newline | `\n` | | horizontal tab | `\t` | | alert (bell) | `\a` | | vertical tab | `\v` | | backspace | `\b` | | double quote | `\"` | | backslash | `\\` | | question mark | `\?` | | single quote | `\'` | | carriage return | `\r` | | formfeed | `\f` | ```c++ std::cout << '\n'; // prints a newline std::cout << "\tHi!\n"; // prints a tab followd by "Hi!" and a newline ``` 泛化转义序列的形式是`\x`后紧跟1个或多个十六进制数字,或者`\`后紧跟1个、2个或3个八进制数字,其中数字部分表示字符对应的数值。如果`\`后面跟着的八进制数字超过3个,则只有前3个数字与`\`构成转义序列。相反,`\x`要用到后面跟着的所有数字。 ```c++ std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline std::cout << '\115' << '\n'; // prints M followed by a newline ``` 添加特定的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。 ![2-2](Images/2-2.png) 使用一个长整型字面值时,最好使用大写字母`L`进行标记,小写字母`l`和数字`1`容易混淆。 ## 变量(Variables) ### 变量定义(Variable Definitions) 变量定义的基本形式:类型说明符(type specifier)后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。定义时可以为一个或多个变量赋初始值(初始化,initialization)。 初始化不等于赋值(assignment)。初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,再用一个新值来替代。 用花括号初始化变量称为列表初始化(list initialization)。当用于内置类型的变量时,如果使用了列表初始化并且初始值存在丢失信息的风险,则编译器会报错。 ```c++ long double ld = 3.1415926536; int a{ld}, b = {ld}; // error: narrowing conversion required int c(ld), d = ld; // ok: but value will be truncated ``` 如果定义变量时未指定初值,则变量被默认初始化(default initialized)。 对于内置类型,定义于任何函数体之外的变量被初始化为0,函数体内部的变量将不被初始化(uninitialized)。 定义于函数体内的内置类型对象如果没有初始化,则其值未定义,使用该类值是一种错误的编程行为且很难调试。类的对象如果没有显式初始化,则其值由类确定。 建议初始化每一个内置类型的变量。 ### 变量声明和定义的关系(Variable Declarations and Definitions) 声明(declaration)使得名字为程序所知。一个文件如果想使用其他地方定义的名字,则必须先包含对那个名字的声明。 定义(definition)负责创建与名字相关联的实体如果想声明一个变量而不定义它,就在变量名前添加关键字`extern`,并且不要显式地初始化变量。 ```c++ extern int i; // declares but does not define i int j; // declares and defines j ``` `extern`语句如果包含了初始值就不再是声明了,而变成了定义。 变量能且只能被定义一次,但是可以被声明多次。 如果要在多个文件中使用同一个变量,就必须将声明和定义分开。此时变量的定义必须出现且只能出现在一个文件中,其他使用该变量的文件必须对其进行声明,但绝对不能重复定义。 ### 标识符(Identifiers) C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感。C++为标准库保留了一些名字。用户自定义的标识符不能连续出现两个下划线也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。 ![2-3](Images/2-3.png) ### 名字的作用域(Scope of a Name) 定义在函数体之外的名字拥有全局作用域(global scope)。声明之后,该名字在整个程序范围内都可使用。 最好在第一次使用变量时再去定义它。这样做更容易找到变量的定义位置,并且也可以赋给它一个比较合理的初始值。 作用域中一旦声明了某个名字,在它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字,此时内层作用域中新定义的名字将屏蔽外层作用域的名字。 可以用作用域操作符`::`来覆盖默认的作用域规则。因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,会向全局作用域发出请求获取作用域操作符右侧名字对应的变量。 ```c++ #include // Program for illustration purposes only: It is bad style for a function // to use a global variable and also define a local variable with the same name int reused = 42; // reused has global scope int main() { int unique = 0; // unique has block scope // output #1: uses global reused; prints 42 0 std::cout << reused << " " << unique << std::endl; int reused = 0; // new, local object named reused hides global reused // output #2: uses local reused; prints 0 0 std::cout << reused << " " << unique << std::endl; // output #3: explicitly requests the global reused; prints 42 0 std::cout << ::reused << " " << unique << std::endl; return 0; } ``` 如果函数有可能用到某个全局变量,则不宜再定义一个同名的局部变量。 ## 复合类型(Compound Type) ### 引用(References) 引用为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成`&d`的形式来定义引用类型,其中*d*是变量名称。 ```c++ int ival = 1024; int &refVal = ival; // refVal refers to (is another name for) ival int &refVal2; // error: a reference must be initialized ``` 定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,将无法再令引用重新绑定到另一个对象,因此引用必须初始化。 引用不是对象,它只是为一个已经存在的对象所起的另外一个名字。 声明语句中引用的类型实际上被用于指定它所绑定的对象类型。大部分情况下,引用的类型要和与之绑定的对象严格匹配。 引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起。 ### 指针(Pointer) 与引用类似,指针也实现了对其他对象的间接访问。 - 指针本身就是一个对象,允许对指针赋值和拷贝,而且在生命周期内它可以先后指向不同的对象。 - 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。 通过将声明符写成`*d`的形式来定义指针类型,其中*d*是变量名称。如果在一条语句中定义了多个指针变量,则每个量前都必须有符号`*`。 ```c++ int *ip1, *ip2; // both ip1 and ip2 are pointers to int double dp, *dp2; // dp2 is a pointer to double; dp is a double ``` 指针存放某个对象的地址,要想获取对象的地址,需要使用取地址符`&`。 ```c++ int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival ``` 因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。 声明语句中指针的类型实际上被用于指定它所指向的对象类型。大部分情况下,指针的类型要和它指向的对象严格匹配。 指针的值(即地址)应属于下列状态之一: - 指向一个对象。 - 指向紧邻对象所占空间的下一个位置。 - 空指针,即指针没有指向任何对象。 - 无效指针,即上述情况之外的其他值。 试图拷贝或以其他方式访问无效指针的值都会引发错误。 如果指针指向一个对象,可以使用解引用(dereference)符`*`来访问该对象。 ```c++ int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival cout << *p; // * yields the object to which p points; prints 42 ``` 给解引用的结果赋值就是给指针所指向的对象赋值。 解引用操作仅适用于那些确实指向了某个对象的有效指针。 空指针(null pointer)不指向任何对象,在试图使用一个指针前代码可以先检查它是否为空。得到空指针最直接的办法是用字面值`nullptr`来初始化指针。 旧版本程序通常使用`NULL`(预处理变量,定义于头文件*cstdlib*中,值为0)给指针赋值,但在C++11中,最好使用`nullptr`初始化空指针。 ```c++ int *p1 = nullptr; // equivalent to int *p1 = 0; int *p2 = 0; // directly initializes p2 from the literal constant 0 // must #include cstdlib int *p3 = NULL; // equivalent to int *p3 = 0; ``` 建议初始化所有指针。 `void*`是一种特殊的指针类型,可以存放任意对象的地址,但不能直接操作`void*`指针所指的对象。 ### 理解复合类型的声明(Understanding Compound Type Declarations) 指向指针的指针(Pointers to Pointers): ```c++ int ival = 1024; int *pi = &ival; // pi points to an int int **ppi = π // ppi points to a pointer to an int ``` ![2-4](Images/2-4.png) 指向指针的引用(References to Pointers): ```C++ int i = 42; int *p; // p is a pointer to int int *&r = p; // r is a reference to the pointer p r = &i; // r refers to a pointer; assigning &i to r makes p point to i *r = 0; // dereferencing r yields i, the object to which p points; changes i to 0 ``` 面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清它的真实含义。 ## const限定符(Const Qualifier) 在变量类型前添加关键字`const`可以创建值不能被改变的对象。`const`变量必须被初始化。 ```c++ const int bufSize = 512; // input buffer size bufSize = 512; // error: attempt to write to const object ``` 默认情况下,`const`对象被设定成仅在文件内有效。当多个文件中出现了同名的`const`变量时,其实等同于在不同文件中分别定义了独立的变量。 如果想在多个文件间共享`const`对象: - 若`const`对象的值在编译时已经确定,则应该定义在头文件中。其他源文件包含该头文件时,不会产生重复定义错误。 - 若`const`对象的值直到运行时才能确定,则应该在头文件中声明,在源文件中定义。此时`const`变量的声明和定义前都应该添加`extern`关键字。 ```c++ // file_1.cc defines and initializes a const that is accessible to other files extern const int bufSize = fcn(); // file_1.h extern const int bufSize; // same bufSize as defined in file_1.cc ``` ### const的引用(References to const) 把引用绑定在`const`对象上即为对常量的引用(reference to const)。对常量的引用不能被用作修改它所绑定的对象。 ```c++ const int ci = 1024; const int &r1 = ci; // ok: both reference and underlying object are const r1 = 42; // error: r1 is a reference to const int &r2 = ci; // error: non const reference to a const object ``` 大部分情况下,引用的类型要和与之绑定的对象严格匹配。但是有两个例外: - 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。 ```c++ int i = 42; const int &r1 = i; // we can bind a const int& to a plain int object const int &r2 = 42; // ok: r1 is a reference to const const int &r3 = r1 * 2; // ok: r3 is a reference to const int &r4 = r * 2; // error: r4 is a plain, non const reference ``` - 允许为一个常量引用绑定非常量的对象、字面值或者一般表达式。 ```c++ double dval = 3.14; const int &ri = dval; ``` ### 指针和const(Pointers and const) 指向常量的指针(pointer to const)不能用于修改其所指向的对象。常量对象的地址只能使用指向常量的指针来存放,但是指向常量的指针可以指向一个非常量对象。 ```c++ const double pi = 3.14; // pi is const; its value may not be changed double *ptr = π // error: ptr is a plain pointer const double *cptr = π // ok: cptr may point to a double that is const *cptr = 42; // error: cannot assign to *cptr double dval = 3.14; // dval is a double; its value can be changed cptr = &dval; // ok: but can't change dval through cptr ``` 定义语句中把`*`放在`const`之前用来说明指针本身是一个常量,常量指针(const pointer)必须初始化。 ```c++ int errNumb = 0; int *const curErr = &errNumb; // curErr will always point to errNumb const double pi = 3.14159; const double *const pip = π // pip is a const pointer to a const object ``` 指针本身是常量并不代表不能通过指针修改其所指向的对象的值,能否这样做完全依赖于其指向对象的类型。 ### 顶层const(Top-Level const) 顶层`const`表示指针本身是个常量,底层`const`(low-level const)表示指针所指的对象是一个常量。指针类型既可以是顶层`const`也可以是底层`const`。 ```c++ int i = 0; int *const p1 = &i; // we can't change the value of p1; const is top-level const int ci = 42; // we cannot change ci; const is top-level const int *p2 = &ci; // we can change p2; const is low-level const int *const p3 = p2; // right-most const is top-level, left-most is not const int &r = ci; // const in reference types is always low-level ``` 当执行拷贝操作时,常量是顶层`const`还是底层`const`区别明显: - 顶层`const`没有影响。拷贝操作不会改变被拷贝对象的值,因此拷入和拷出的对象是否是常量无关紧要。 ```c++ i = ci; // ok: copying the value of ci; top-level const in ci is ignored p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored ``` - 拷入和拷出的对象必须具有相同的底层`const`资格。或者两个对象的数据类型可以相互转换。一般来说,非常量可以转换成常量,反之则不行。 ```c++ int *p = p3; // error: p3 has a low-level const but p doesn't p2 = p3; // ok: p2 has the same low-level const qualification as p3 p2 = &i; // ok: we can convert int* to const int* int &r = ci; // error: can't bind an ordinary int& to a const int object const int &r2 = i; // ok: can bind const int& to plain int ``` ### constexpr和常量表达式(constexpr and Constant Expressions) 常量表达式(constant expressions)指值不会改变并且在编译过程就能得到计算结果的表达式。 一个对象是否为常量表达式由它的数据类型和初始值共同决定。 ```c++ const int max_files = 20; // max_files is a constant expression const int limit = max_files + 1; // limit is a constant expression int staff_size = 27; // staff_size is not a constant expression const int sz = get_size(); // sz is not a constant expression ``` C++11允许将变量声明为`constexpr`类型以便由编译器来验证变量的值是否是一个常量表达式。 ```c++ constexpr int mf = 20; // 20 is a constant expression constexpr int limit = mf + 1; // mf + 1 is a constant expression constexpr int sz = size(); // ok only if size is a constexpr function ``` 指针和引用都能定义成`constexpr`,但是初始值受到严格限制。`constexpr`指针的初始值必须是0、`nullptr`或者是存储在某个固定地址中的对象。 函数体内定义的普通变量一般并非存放在固定地址中,因此`constexpr`指针不能指向这样的变量。相反,函数体外定义的变量地址固定不变,可以用来初始化`constexpr`指针。 在`constexpr`声明中如果定义了一个指针,限定符`constexpr`仅对指针本身有效,与指针所指的对象无关。`constexpr`把它所定义的对象置为了顶层`const`。 ```c++ constexpr int *p = nullptr; // p是指向int的const指针 constexpr int i = 0; constexpr const int *cp = &i; // cp是指向const int的const指针 ``` `const`和`constexpr`限定的值都是常量。但`constexpr`对象的值必须在编译期间确定,而`const`对象的值可以延迟到运行期间确定建议使用`constexpr`修饰表示数组大小的对象,因为数组的大小必须在编译期间确定且不能改变。 ## 处理类型(Dealing with Types) ### 类型别名(Type Aliases) 类型别名是某种类型的同义词,传统方法是使用关键字`typedef`定义类型别名。 ```c++ typedef double wages; // wages is a synonym for double typedef wages base, *p; // base is a synonym for double, p for double* ``` C++11使用关键字`using`进行别名声明(alias declaration),作用是把等号左侧的名字规定成等号右侧类型的别名。 ```c++ using SI = Sales_item; // SI is a synonym for Sales_item ``` ### auto类型说明符(The auto Type Specifier) C++11新增`auto`类型说明符,能让编译器自动分析表达式所属的类型。`auto`定义的变量必须有初始值。 ```c++ // the type of item is deduced from the type of the result of adding val1 and val2 auto item = val1 + val2; // item initialized to the result of val1 + val2 ``` 编译器推断出来的`auto`类型有时和初始值的类型并不完全一样。 - 当引用被用作初始值时,编译器以引用对象的类型作为`auto`的类型。 ```c++ int i = 0, &r = i; auto a = r; // a is an int (r is an alias for i, which has type int) ``` - `auto`一般会忽略顶层`const`。 ```c++ const int ci = i, &cr = ci; auto b = ci; // b is an int (top-level const in ci is dropped) auto c = cr; // c is an int (cr is an alias for ci whose const is top-level) auto d = &i; // d is an int*(& of an int object is int*) auto e = &ci; // e is const int*(& of a const object is low-level const) ``` 如果希望推断出的`auto`类型是一个顶层`const`,需要显式指定`const auto`。 ```C++ const auto f = ci; // deduced type of ci is int; f has type const int ``` 设置类型为`auto`的引用时,原来的初始化规则仍然适用,初始值中的顶层常量属性仍然保留。 ```c++ auto &g = ci; // g is a const int& that is bound to ci auto &h = 42; // error: we can't bind a plain reference to a literal const auto &j = 42; // ok: we can bind a const reference to a literal ``` ### decltype类型指示符(The decltype Type Specifier) C++11新增`decltype`类型指示符,作用是选择并返回操作数的数据类型,此过程中编译器不实际计算表达式的值。 ```c++ decltype(f()) sum = x; // sum has whatever type f returns ``` `decltype`处理顶层`const`和引用的方式与`auto`有些不同,如果`decltype`使用的表达式是一个变量,则`decltype`返回该变量的类型(包括顶层`const`和引用)。 ```c++ const int ci = 0, &cj = ci; decltype(ci) x = 0; // x has type const int decltype(cj) y = x; // y has type const int& and is bound to x decltype(cj) z; // error: z is a reference and must be initialized ``` 如果`decltype`使用的表达式不是一个变量,则`decltype`返回表达式结果对应的类型。如果表达式的内容是解引用操作,则`decltype`将得到引用类型。如果`decltype`使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则`decltype`会得到引用类型,因为变量是一种可以作为赋值语句左值的特殊表达式。 `decltype((var))`的结果永远是引用,而`decltype(var)`的结果只有当*var*本身是一个引用时才会是引用。 ## 自定义数据结构(Defining Our Own Data Structures) C++11规定可以为类的数据成员(data member)提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员,没有初始值的成员将被默认初始化。 类内初始值不能使用圆括号。 类定义的最后应该加上分号。 头文件一旦改变,相关的源文件必须重新编译以获取更新之后的声明。 头文件保护符(header guard)依赖于预处理变量(preprocessor variable)。预处理变量有两种状态:已定义和未定义。`#define`指令把一个名字设定为预处理变量。`#ifdef`指令当且仅当变量已定义时为真,`#ifndef`指令当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到`#endif`指令为止。 ```c++ #ifndef SALES_DATA_H #define SALES_DATA_H #include struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; #endif ``` 在高级版本的IDE环境中,可以直接使用`#pragma once`命令来防止头文件的重复包含。 预处理变量无视C++语言中关于作用域的规则。 整个程序中的预处理变量,包括头文件保护符必须唯一。预处理变量的名字一般均为大写。 头文件即使目前还没有被包含在任何其他头文件中,也应该设置保护符。 # 第3章 字符串、向量和数组 ## 命名空间的using声明(Namespace using Declarations) 使用`using`声明后就无须再通过专门的前缀去获取所需的名字了。 ```c++ using std::cout; ``` 程序中使用的每个名字都需要用独立的`using`声明引入。 头文件中通常不应该包含`using`声明。 ## 标准库类型string(Library string Type) 标准库类型`string`表示可变长的字符序列,定义在头文件*string*中。 ### 定义和初始化string对象(Defining and Initializing strings) 初始化`string`的方式: ![3-1](Images/3-1.png) 如果使用等号初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,则执行的是直接初始化(direct initialization)。 ### string对象上的操作(Operations on strings) `string`的操作: ![3-2](Images/3-2.png) 在执行读取操作时,`string`对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。 使用`getline`函数可以读取一整行字符。该函数只要遇到换行符就结束读取并返回结果,如果输入的开始就是一个换行符,则得到空`string`。触发`getline`函数返回的那个换行符实际上被丢弃掉了,得到的`string`对象中并不包含该换行符。 `size`函数返回`string`对象的长度,返回值是`string::size_type`类型,这是一种无符号类型。要使用`size_type`,必须先指定它是由哪种类型定义的。 如果一个表达式中已经有了`size`函数就不要再使用`int`了,这样可以避免混用`int`和`unsigned int`可能带来的问题。 当把`string`对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是`string`。 ```c++ string s4 = s1 + ", "; // ok: adding a string and a literal string s5 = "hello" + ", "; // error: no string operand string s6 = s1 + ", " + "world"; // ok: each + has a string operand ``` 为了与C兼容,C++语言中的字符串字面值并不是标准库`string`的对象。 ### 处理string对象中的字符(Dealing with the Characters in a string) 头文件*cctype*中的字符操作函数: ![3-3](Images/3-3.png) 建议使用C++版本的C标准库头文件。C语言中名称为*name.h*的头文件,在C++中则被命名为*cname*。 C++11提供了范围`for`(range for)语句,可以遍历给定序列中的每个元素并执行某种操作。 ```c++ for (declaration : expression) statement ``` *expression*部分是一个对象,用于表示一个序列。*declaration*部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,*declaration*部分的变量都会被初始化为*expression*部分的下一个元素值。 ```c++ string str("some string"); // print the characters in str one character to a line for (auto c : str) // for every char in str cout << c << endl; // print the current character followed by a newline ``` 如果想在范围`for`语句中改变`string`对象中字符的值,必须把循环变量定义成引用类型。 下标运算符接收的输入参数是`string::size_type`类型的值,表示要访问字符的位置,返回值是该位置上字符的引用。 下标数值从0记起,范围是0至*size - 1*。使用超出范围的下标将引发不可预知的后果。 C++标准并不要求标准库检测下标是否合法。编程时可以把下标的类型定义为相应的`size_type`,这是一种无符号数,可以确保下标不会小于0,此时代码只需要保证下标小于`size`的值就可以了。另一种确保下标合法的有效手段就是使用范围`for`语句。 ## 标准库类型vector(Library vector Type) 标准库类型`vector`表示对象的集合,也叫做容器(container),定义在头文件*vector*中。`vector`中所有对象的类型都相同,每个对象都有一个索引与之对应并用于访问该对象。 `vector`是模板(template)而非类型,由`vector`生成的类型必须包含`vector`中元素的类型,如`vector`。 因为引用不是对象,所以不存在包含引用的`vector`。 在早期的C++标准中,如果`vector`的元素还是`vector`,定义时必须在外层`vector`对象的右尖括号和其元素类型之间添加一个空格,如`vector >`。但是在C++11标准中,可以直接写成`vector>`,不需要添加空格。 ### 定义和初始化vector对象(Defining and Initializing vectors) 初始化`vector`对象的方法: ![3-4](Images/3-4.png) 初始化`vector`对象时如果使用圆括号,可以说提供的值是用来构造(construct)`vector`对象的;如果使用的是花括号,则是在列表初始化(list initialize)该`vector`对象。 可以只提供`vector`对象容纳的元素数量而省略初始值,此时会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。这个初值由`vector`对象中的元素类型决定。 ### 向vector对象中添加元素(Adding Elements to a vector) `push_back`函数可以把一个值添加到`vector`的尾端。 ```c++ vector v2; // empty vector for (int i = 0; i != 100; ++i) v2.push_back(i); // append sequential integers to v2 // at end of loop v2 has 100 elements, values 0 . . . 99 ``` 范围`for`语句体内不应该改变其所遍历序列的大小。 ### 其他vector操作(Other vector Operations) `vector`支持的操作: ![3-5](Images/3-5.png) `size`函数返回`vector`对象中元素的个数,返回值是由`vector`定义的`size_type`类型。`vector`对象的类型包含其中元素的类型。 ```c++ vector::size_type // ok vector::size_type // error ``` `vector`和`string`对象的下标运算符只能用来访问已经存在的元素,而不能用来添加元素。 ```c++ vector ivec; // empty vector for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) { ivec[ix] = ix; // disaster: ivec has no elements ivec.push_back(ix); // ok: adds a new element with value ix } ``` ## 迭代器介绍(Introducing Iterators) 迭代器的作用和下标类似,但是更加通用。所有标准库容器都可以使用迭代器,但是其中只有少数几种同时支持下标运算符。 ### 使用迭代器(Using Iterators) 定义了迭代器的类型都拥有`begin`和`end`两个成员函数。`begin`函数返回指向第一个元素的迭代器,`end`函数返回指向容器“尾元素的下一位置(one past the end)”的迭代器,通常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。尾后迭代器仅是个标记,表示程序已经处理完了容器中的所有元素。迭代器一般为`iterator`类型。 ```c++ // b denotes the first element and e denotes one past the last element in ivec auto b = ivec.begin(), e = ivec.end(); // b and e have the same type ``` 如果容器为空,则`begin`和`end`返回的是同一个迭代器,都是尾后迭代器。 标准容器迭代器的运算符: ![3-6](Images/3-6.png) 因为`end`返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。 在`for`或者其他循环语句的判断条件中,最好使用`!=`而不是`<`。所有标准库容器的迭代器都定义了`==`和`!=`,但是只有其中少数同时定义了`<`运算符。 如果`vector`或`string`对象是常量,则只能使用`const_iterator`迭代器,该迭代器只能读元素,不能写元素。 `begin`和`end`返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回`const_iterator`;如果对象不是常量,则返回`iterator`。 ```c++ vector v; const vector cv; auto it1 = v.begin(); // it1 has type vector::iterator auto it2 = cv.begin(); // it2 has type vector::const_iterator ``` C++11新增了`cbegin`和`cend`函数,不论`vector`或`string`对象是否为常量,都返回`const_iterator`迭代器。 任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。 ### 迭代器运算(Iterator Arithmetic) `vector`和`string`迭代器支持的操作: ![3-7](Images/3-7.png) `difference_type`类型用来表示两个迭代器间的距离,这是一种带符号整数类型。 ## 数组(Arrays) 数组类似`vector`,但数组的大小确定不变,不能随意向数组中添加元素。 如果不清楚元素的确切个数,应该使用`vector`。 ### 定义和初始化内置数组(Defining and Initializing Built-in Arrays) 数组是一种复合类型,声明形式为`a[d]`,其中*a*是数组名称,*d*是数组维度(dimension)。维度必须是一个常量表达式。 默认情况下,数组的元素被默认初始化。 定义数组的时候必须指定数组的类型,不允许用`auto`关键字由初始值列表推断类型。 如果定义数组时提供了元素的初始化列表,则允许省略数组维度,编译器会根据初始值的数量计算维度。但如果显式指明了维度,那么初始值的数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化。 ```c++ const unsigned sz = 3; int ia1[sz] = {0,1,2}; // array of three ints with values 0, 1, 2 int a2[] = {0, 1, 2}; // an array of dimension 3 int a3[5] = {0, 1, 2}; // equivalent to a3[] = {0, 1, 2, 0, 0} string a4[3] = {"hi", "bye"}; // same as a4[] = {"hi", "bye", ""} int a5[2] = {0,1,2}; // error: too many initializers ``` 可以用字符串字面值初始化字符数组,但字符串字面值结尾处的空字符也会一起被拷贝到字符数组中。 ```c++ char a1[] = {'C', '+', '+'}; // list initialization, no null char a2[] = {'C', '+', '+', '\0'}; // list initialization, explicit null char a3[] = "C++"; // null terminator added automatically const char a4[6] = "Daniel"; // error: no space for the null! ``` 不能用一个数组初始化或直接赋值给另一个数组。 从数组的名字开始由内向外阅读有助于理解复杂数组声明的含义。 ```c++ int *ptrs[10]; // ptrs is an array of ten pointers to int int &refs[10] = /* ? */; // error: no arrays of references int (*Parray)[10] = &arr; // Parray points to an array of ten ints int (&arrRef)[10] = arr; // arrRef refers to an array of ten ints ``` ### 访问数组元素(Accessing the Elements of an Array) 数组下标通常被定义成`size_t`类型,这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。`size_t`定义在头文件*cstddef*中大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。 ### 指针和数组(Pointers and Arrays) 在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。 ```c++ string nums[] = {"one", "two", "three"}; // array of strings string *p = &nums[0]; // p points to the first element in nums string *p2 = nums; // equivalent to p2 = &nums[0] ``` 一维数组寻址公式: ![3-8](Images/3-8.png) 当使用数组作为一个`auto`变量的初始值时,推断得到的类型是指针而非数组。但`decltype`关键字不会发生这种转换,直接返回数组类型。 ```c++ int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints auto ia2(ia); // ia2 is an int* that points to the first element in ia ia2 = 42; // error: ia2 is a pointer, and we can't assign an int to a pointer auto ia2(&ia[0]); // now it's clear that ia2 has type int* // ia3 is an array of ten ints decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; ia3 = p; // error: can't assign an int* to an array ia3[4] = i; // ok: assigns the value of i to an element in ia3 ``` C++11在头文件*iterator*中定义了两个名为`begin`和`end`的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。 ```c++ int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints int *beg = begin(ia); // pointer to the first element in ia int *last = end(ia); // pointer one past the last element in ia ``` 两个指针相减的结果类型是`ptrdiff_t`,这是一种定义在头文件*cstddef*中的带符号类型。 标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。 ### C风格字符串(C-Style Character Strings) C风格字符串是将字符串存放在字符数组中,并以空字符结束(null terminated)。这不是一种类型,而是一种为了表达和使用字符串而形成的书写方法。 C++标准支持C风格字符串,但是最好不要在C++程序中使用它们。对大多数程序来说,使用标准库`string`要比使用C风格字符串更加安全和高效。 C风格字符串的函数: ![3-9](Images/3-9.png) C风格字符串函数不负责验证其参数的正确性,传入此类函数的指针必须指向以空字符作为结尾的数组。 ### 与旧代码的接口(Interfacing to Older Code) 任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替: - 允许使用以空字符结束的字符数组来初始化`string`对象或为`string`对象赋值。 - 在`string`对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。 - 在`string`对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧运算对象。 不能用`string`对象直接初始化指向字符的指针。为了实现该功能,`string`提供了一个名为`c_str`的成员函数,返回`const char*`类型的指针,指向一个以空字符结束的字符数组,数组的数据和`string`对象一样。 ```c++ string s("Hello World"); // s holds Hello World char *str = s; // error: can't initialize a char* from a string const char *str = s.c_str(); // ok ``` 针对`string`对象的后续操作有可能会让`c_str`函数之前返回的数组失去作用,如果程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。 可以使用数组来初始化`vector`对象,但是需要指明要拷贝区域的首元素地址和尾后地址。 ```c++ int int_arr[] = {0, 1, 2, 3, 4, 5}; // ivec has six elements; each is a copy of the corresponding element in int_arr vector ivec(begin(int_arr), end(int_arr)); ``` 在新版本的C++程序中应该尽量使用`vector`、`string`和迭代器,避免使用内置数组、C风格字符串和指针。 ## 多维数组(Multidimensional Arrays) C++中的多维数组其实就是数组的数组。当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。 多维数组初始化的几种方式: ```c++ int ia[3][4] = { // three elements; each element is an array of size 4 {0, 1, 2, 3}, // initializers for the row indexed by 0 {4, 5, 6, 7}, // initializers for the row indexed by 1 {8, 9, 10, 11} // initializers for the row indexed by 2 }; // equivalent initialization without the optional nested braces for each row int ib[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; // explicitly initialize only element 0 in each row int ic[3][4] = {{ 0 }, { 4 }, { 8 }}; // explicitly initialize row 0; the remaining elements are value initialized int id[3][4] = {0, 3, 6, 9}; ``` 可以使用下标访问多维数组的元素,数组的每个维度对应一个下标运算符。如果表达式中下标运算符的数量和数组维度一样多,则表达式的结果是给定类型的元素。如果下标运算符数量比数组维度小,则表达式的结果是给定索引处的一个内层数组。 ```c++ // assigns the first element of arr to the last element in the last row of ia ia[2][3] = arr[0][0][0]; int (&row)[4] = ia[1]; // binds row to the second four-element array in ia ``` 多维数组寻址公式: ![3-10](Images/3-10.png) 使用范围`for`语句处理多维数组时,为了避免数组被自动转换成指针,语句中的外层循环控制变量必须声明成引用类型。 ```c++ for (const auto &row : ia) // for every element in the outer array for (auto col : row) // for every element in the inner array cout << col << endl; ``` 如果*row*不是引用类型,编译器初始化*row*时会自动将数组形式的元素转换成指向该数组内首元素的指针,这样得到的*row*就是`int*`类型,而之后的内层循环则试图在一个`int*`内遍历,程序将无法通过编译。 ```c++ for (auto row : ia) for (auto col : row) ``` 使用范围`for`语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。 因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。 ```c++ int ia[3][4]; // array of size 3; each element is an array of ints of size 4 int (*p)[4] = ia; // p points to an array of four ints p = &ia[2]; // p now points to the last element in ia ``` 声明指向数组类型的指针时,必须带有圆括号。 ```c++ int *ip[4]; // array of pointers to int int (*ip)[4]; // pointer to an array of four ints ``` 使用`auto`和`decltype`能省略复杂的指针定义。 ```c++ // print the value of each element in ia, with each inner array on its own line // p points to an array of four ints for (auto p = ia; p != ia + 3; ++p) { // q points to the first element of an array of four ints; that is, q points to an int for (auto q = *p; q != *p + 4; ++q) cout << *q << ' '; cout << endl; } ``` # 第4章 表达式 ## 基础(Fundamentals) 表达式(expression)由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。 ### 基础概念(Basic Concepts) C++定义了一元运算符(unary operator)和二元运算符(binary operator)。除此之外,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。 表达式求值过程中,小整数类型(如`bool`、`char`、`short`等)通常会被提升(promoted)为较大的整数类型,主要是`int`。 C++定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自定义其含义,这被称作运算符重载(overloaded operator)。 C++的表达式分为右值(rvalue)和左值(lvalue)。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的地址。需要右值的地方可以用左值代替,反之则不行。 - 赋值运算符需要一个非常量左值作为其左侧运算对象,返回结果也是一个左值。 - 取地址符作用于左值运算对象,返回指向该运算对象的指针,该指针是一个右值。 - 内置解引用运算符、下标运算符、迭代器解引用运算符、`string`和`vector`的下标运算符都返回左值。 - 内置类型和迭代器的递增递减运算符作用于左值运算对象。前置版本返回左值,后置版本返回右值。 如果`decltype`作用于一个求值结果是左值的表达式,会得到引用类型。 ### 优先级与结合律(Precedence and Associativity) 复合表达式(compound expression)指含有两个或多个运算符的表达式。优先级与结合律决定了运算对象的组合方式。 括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。 ### 求值顺序(Order of Evaluation) 对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。 ```c++ int i = 0; cout << i << " " << ++i << endl; // undefined ``` 处理复合表达式时建议遵循以下两点: - 不确定求值顺序时,使用括号来强制让表达式的组合关系符合程序逻辑的要求。 - 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。 当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,第二条规则无效。如`*++iter`,递增运算符改变了*iter*的值,而改变后的*iter*又是解引用运算符的运算对象。类似情况下,求值的顺序不会成为问题。 ## 算术运算符(Arithmetic Operators) 算术运算符(左结合律): ![4-1](Images/4-1.png) 在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。 ## 逻辑和关系运算符(Logical and Relational Operators) 关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。 ![4-2](Images/4-2.png) 逻辑与(logical AND)运算符`&&`和逻辑或(logical OR)运算符`||`都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。 - 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。 - 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。 进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值`true`和`false`作为运算对象。 ## 赋值运算符(Assignment Operators) 赋值运算符`=`的左侧运算对象必须是一个可修改的左值。 C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。 ```c++ vector vi; // initially empty vi = {0,1,2,3,4,5,6,7,8,9}; // vi now has ten elements, values 0 through 9 ``` 赋值运算符满足右结合律。 ```c++ int ival, jval; ival = jval = 0; // ok: each assigned 0 ``` 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。 不要混淆相等运算符`==`和赋值运算符`=`。 复合赋值运算符包括`+=`、`-=`、`*=`、`/=`、`%=`、`<<=`、`>>=`、`&=`、`^=`和`|=`。任意一种复合运算都完全等价于*a = a op b*。 ## 递增和递减运算符(Increment and Decrement Operators) 递增和递减运算符是为对象加1或减1的简洁书写形式。很多不支持算术运算的迭代器可以使用递增和递减运算符。 递增和递减运算符分为前置版本和后置版本: - 前置版本首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。 - 后置版本也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。 ```c++ int i = 0, j; j = ++i; // j = 1, i = 1: prefix yields the incremented value j = i++; // j = 1, i = 2: postfix yields the unincremented value ``` 除非必须,否则不应该使用递增或递减运算符的后置版本。后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。 在某些语句中混用解引用和递增运算符可以使程序更简洁。 ```c++ cout << *iter++ << endl; ``` ## 成员访问运算符(The Member Access Operators) 点运算符`.`和箭头运算符`->`都可以用来访问成员,表达式`ptr->mem`等价于`(*ptr).mem`。 ```c++ string s1 = "a string", *p = &s1; auto n = s1.size(); // run the size member of the string s1 n = (*p).size(); // run size on the object to which p points n = p->size(); // equivalent to (*p).size() ``` ## 条件运算符(The Conditional Operator) 条件运算符的使用形式如下: ```c++ cond ? expr1 : expr2; ``` 其中*cond*是判断条件的表达式,如果*cond*为真则对*expr1*求值并返回该值,否则对*expr2*求值并返回该值。 只有当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果才是左值,否则运算的结果就是右值。 条件运算符可以嵌套,但是考虑到代码的可读性,运算的嵌套层数最好不要超过两到三层。 条件运算符的优先级非常低,因此当一个长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。 ## 位运算符(The Bitwise Operators) 位运算符(左结合律): ![4-3](Images/4-3.png) 在位运算中符号位如何处理并没有明确的规定,所以建议仅将位运算符用于无符号类型的处理。 左移运算符`<<`在运算对象右侧插入值为0的二进制位。右移运算符`>>`的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。 ## sizeof运算符(The sizeof Operator) `sizeof`运算符返回一个表达式或一个类型名字所占的字节数,返回值是`size_t`类型。 在`sizeof`的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。 `sizeof`运算符的结果部分依赖于其作用的类型: - 对`char`或者类型为`char`的表达式执行`sizeof`运算,返回值为1。 - 对引用类型执行`sizeof`运算得到被引用对象所占空间的大小。 - 对指针执行`sizeof`运算得到指针本身所占空间的大小。 - 对解引用指针执行`sizeof`运算得到指针指向的对象所占空间的大小,指针不需要有效。 - 对数组执行`sizeof`运算得到整个数组所占空间的大小。 - 对`string`或`vector`对象执行`sizeof`运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。 ## 逗号运算符(Comma Operator) 逗号运算符`,`含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在`for`循环中。 ```c++ vector::size_type cnt = ivec.size(); // assign values from size... 1 to the elements in ivec for(vector::size_type ix = 0; ix != ivec.size(); ++ix, --cnt) ivec[ix] = cnt; ``` ## 类型转换(Type Conversions) 无须程序员介入,会自动执行的类型转换叫做隐式转换(implicit conversions)。 ### 算术转换(Integral Promotions) 把一种算术类型转换成另一种算术类型叫做算术转换。 整型提升(integral promotions)负责把小整数类型转换成较大的整数类型。 ### 其他隐式类型转换(Other Implicit Conversions) 在大多数表达式中,数组名字自动转换成指向数组首元素的指针。 常量整数值0或字面值`nullptr`能转换成任意指针类型;指向任意非常量的指针能转换成`void*`;指向任意对象的指针能转换成`const void*`任意一种算术类型或指针类型都能转换成布尔类型。如果指针或算术类型的值为0,转换结果是`false`,否则是`true`。 指向非常量类型的指针能转换成指向相应的常量类型的指针。 ### 显式转换(Explicit Conversions) 显式类型转换也叫做强制类型转换(cast)。虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。 命名的强制类型转换(named cast)形式如下: ```c++ cast-name(expression); ``` 其中*type*是转换的目标类型,*expression*是要转换的值。如果*type*是引用类型,则转换结果是左值。*cast-name*是`static_cast`、`dynamic_cast`、`const_cast`和`reinterpret_cast`中的一种,用来指定转换的方式。 - `dynamic_cast`支持运行时类型识别。 - 任何具有明确定义的类型转换,只要不包含底层`const`,都能使用`static_cast`。 - `const_cast`只能改变运算对象的底层`const`,不能改变表达式的类型。同时也只有`const_cast`能改变表达式的常量属性。`const_cast`常常用于函数重载。 - `reinterpret_cast`通常为运算对象的。 早期版本的C++语言中,显式类型转换包含两种形式: ```c++ type (expression); // function-style cast notation (type) expression; // C-language-style cast notation ``` # 第5章 语句 ## 简单语句(Simple Statements) 如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,则应该使用空语句(null statement)。空语句中只含有一个单独的分号`;`。 ```c++ // read until we hit end-of-file or find an input equal to sought while (cin >> s && s != sought) ; // null statement ``` 使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。 多余的空语句并非总是无害的。 ```c++ // disaster: extra semicolon: loop body is this null statement while (iter != svec.end()) ; // the while body is the empty statement ++iter; // increment is not part of the loop ``` 复合语句(compound statement)是指用花括号括起来的(可能为空)语句和声明的序列。复合语句也叫做块(block),一个块就是一个作用域。在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在(最内层)块的结尾处为止。 语句块不以分号作为结束。 空块的作用等价于空语句。 ## 语句作用域(Statement Scope) 可以在`if`、`switch`、`while`和`for`语句的控制结构内定义变量,这些变量只在相应语句的内部可见,一旦语句结束,变量也就超出了其作用范围。 ```c++ while (int i = get_num()) // i is created and initialized on each iteration cout << i << endl; i = 0; // error: i is not accessible outside the loop ``` ## 条件语句(Conditional Statements) ### if语句(The if Statement) `if`语句的形式: ```c++ if (condition) statement ``` `if-else`语句的形式: ```c++ if (condition) statement else statement2 ``` 其中*condition*是判断条件,可以是一个表达式或者初始化了的变量声明。*condition*必须用圆括号括起来。 - 如果*condition*为真,则执行*statement*。执行完成后,程序继续执行`if`语句后面的其他语句。 - 如果*condition*为假,则跳过*statement*。对于简单`if`语句来说,程序直接执行`if`语句后面的其他语句;对于`if-else`语句来说,程序先执行*statement2*,再执行`if`语句后面的其他语句。 `if`语句可以嵌套,其中`else`与离它最近的尚未匹配的`if`相匹配。 ### switch语句(The switch Statement) `switch`语句的形式: ![5-1](Images/5-1.png) `switch`语句先对括号里的表达式求值,值转换成整数类型后再与每个`case`标签(case label)的值进行比较。如果表达式的值和某个`case`标签匹配,程序从该标签之后的第一条语句开始执行,直到到达`switch`的结尾或者遇到`break`语句为止。`case`标签必须是整型常量表达式。 通常情况下每个`case`分支后都有`break`语句。如果确实不应该出现`break`语句,最好写一段注释说明程序的逻辑。 尽管`switch`语句没有强制要求在最后一个`case`标签后写上`break`,但为了安全起见,最好添加`break`。这样即使以后增加了新的`case`分支,也不用再在前面补充`break`语句了。 `switch`语句中可以添加一个`default`标签(default label),如果没有任何一个`case`标签能匹配上`switch`表达式的值,程序将执行`default`标签后的语句。 即使不准备在`default`标签下做任何操作,程序中也应该定义一个`default`标签。其目的在于告诉他人我们已经考虑到了默认情况,只是目前不需要实际操作。 不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。如果需要为`switch`的某个`case`分支定义并初始化一个变量,则应该把变量定义在块内。 ```c++ case true: { // ok: declaration statement within a statement block string file_name = get_file_name(); // ... } ``` ## 迭代语句(Iterative Statements) 迭代语句通常称为循环,它重复执行操作直到满足某个条件才停止。`while`和`for`语句在执行循环体之前检查条件,`do-while`语句先执行循环体再检查条件。 ### while语句(The while Statement) `while`语句的形式: ```c++ while (condition) statement ``` 只要*condition*的求值结果为`true`,就一直执行*statement*(通常是一个块)。*condition*不能为空,如果*condition*第一次求值就是`false`,*statement*一次都不会执行。 定义在`while`条件部分或者循环体内的变量每次迭代都经历从创建到销毁的过程。 在不确定迭代次数,或者想在循环结束后访问循环控制变量时,使用`while`比较合适。 ### 传统的for语句(Traditional for Statement) `for`语句的形式: ```c++ for (initializer; condition; expression) statement ``` 一般情况下,*initializer*负责初始化一个值,这个值会随着循环的进行而改变。*condition*作为循环控制的条件,只要*condition*的求值结果为`true`,就执行一次*statement*。执行后再由*expression*负责修改*initializer*初始化的变量,这个变量就是*condition*检查的对象。如果*condition*第一次求值就是`false`,*statement*一次都不会执行。*initializer*中也可以定义多个对象,但是只能有一条声明语句,因此所有变量的基础类型必须相同。 `for`语句头中定义的对象只在`for`循环体内可见。 ### 范围for语句(Range for Statement) 范围`for`语句的形式: ```c++ for (declaration : expression) statement ``` 其中*expression*表示一个序列,拥有能返回迭代器的`begin`和`end`成员。*declaration*定义一个变量,序列中的每个元素都应该能转换成该变量的类型(可以使用`auto`)。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。每次迭代都会重新定义循环控制变量,并将其初始化为序列中的下一个值,之后才会执行*statement*。 ### do-while语句(The do-while Statement) `do-while`语句的形式: ```c++ do statement while (condition); ``` 计算*condition*的值之前会先执行一次*statement*,*condition*不能为空。如果*condition*的值为`false`,循环终止,否则重复执行*statement*。 因为`do-while`语句先执行语句或块,再判断条件,所以不允许在条件部分定义变量。 ## 跳转语句(Jump Statements) 跳转语句中断当前的执行过程。 ### break语句(The break Statement) `break`语句只能出现在迭代语句或者`switch`语句的内部,负责终止离它最近的`while`、`do-while`、`for`或者`switch`语句,并从这些语句之后的第一条语句开始执行。 ```c++ string buf; while (cin >> buf && !buf.empty()) { switch(buf[0]) { case '-': // process up to the first blank for (auto it = buf.begin()+1; it != buf.end(); ++it) { if (*it == ' ') break; // #1, leaves the for loop // . . . } // break #1 transfers control here // remaining '-' processing: break; // #2, leaves the switch statement case '+': // . . . } // end switch // end of switch: break #2 transfers control here } // end while ``` ### continue语句(The continue Statement) `continue`语句只能出现在迭代语句的内部,负责终止离它最近的循环的当前一次迭代并立即开始下一次迭代。和`break`语句不同的是,只有当`switch`语句嵌套在迭代语句内部时,才能在`switch`中使用`continue`。 `continue`语句中断当前迭代后,具体操作视迭代语句类型而定: - 对于`while`和`do-while`语句来说,继续判断条件的值。 - 对于传统的`for`语句来说,继续执行`for`语句头中的第三部分,之后判断条件的值。 - 对于范围`for`语句来说,是用序列中的下一个元素初始化循环变量。 ### goto语句(The goto Statement) `goto`语句(labeled statement)是一种特殊的语句,在它之前有一个标识符和一个冒号。 ```c++ end: return; // labeled statement; may be the target of a goto ``` 标签标识符独立于变量和其他标识符的名字,它们之间不会相互干扰。 `goto`语句的形式: ```c++ goto label; ``` `goto`语句使程序无条件跳转到标签为*label*的语句处执行,但两者必须位于同一个函数内,同时`goto`语句也不能将程序的控制权从变量的作用域之外转移到作用域之内。 建议不要在程序中使用`goto`语句,它使得程序既难理解又难修改。 ## try语句块和异常处理(try Blocks and Exception Handling) 异常(exception)是指程序运行时的反常行为,这些行为超出了函数正常功能的范围。当程序的某一部分检测到一个它无法处理的问题时,需要使用异常处理(exception handling)。 异常处理机制包括`throw`表达式(throw expression)、`try`语句块(try block)和异常类(exception class)。 - 异常检测部分使用`throw`表达式表示它遇到了无法处理的问题(`throw`引发了异常)。 - 异常处理部分使用`try`语句块处理异常。`try`语句块以关键字`try`开始,并以一个或多个`catch`子句(catch clause)结束。`try`语句块中代码抛出的异常通常会被某个`catch`子句处理,`catch`子句也被称作异常处理代码(exception handler)。 - 异常类用于在`throw`表达式和相关的`catch`子句之间传递异常的具体信息。 ### throw表达式(A throw Expression) `throw`表达式包含关键字`throw`和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。 ### try语句块(The try Block) `try`语句块的通用形式: ```c++ try { program-statements } catch (exception-declaration) { handler-statements } catch (exception-declaration) { handler-statements } // . . . ``` `try`语句块中的*program-statements*组成程序的正常逻辑,其内部声明的变量在块外无法访问,即使在`catch`子句中也不行。`catch`子句包含关键字`catch`、括号内一个对象的声明(异常声明,exception declaration)和一个块。当选中了某个`catch`子句处理异常后,执行与之对应的块。`catch`一旦完成,程序会跳过剩余的所有`catch`子句,继续执行后面的语句。 如果最终没能找到与异常相匹配的`catch`子句,程序会执行名为`terminate`的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。类似的,如果一段程序没有`try`语句块且发生了异常,系统也会调用`terminate`函数并终止当前程序的执行。 ### 标准异常(Standard Exceptions) 异常类分别定义在4个头文件中: - 头文件*exception*定义了最通用的异常类`exception`。它只报告异常的发生,不提供任何额外信息。 - 头文件*stdexcept*定义了几种常用的异常类。 ![5-2](Images/5-2.png) - 头文件*new*定义了`bad_alloc`异常类。 - 头文件*type_info*定义了`bad_cast`异常类。 标准库异常类的继承体系: ![5-3](Images/5-3.png) 只能以默认初始化的方式初始化`exception`、`bad_alloc`和`bad_cast`对象,不允许为这些对象提供初始值。其他异常类的对象在初始化时必须提供一个`string`或一个C风格字符串,通常表示异常信息。`what`成员函数可以返回该字符串的`string`副本。 # 第6章 函数 ## 函数基础(Function Basics) 典型的函数定义包括返回类型(return type)、函数名字、由0个或多个形式参数(parameter,简称形参)组成的列表和函数体(function body)。函数执行的操作在函数体中指明。 ```c++ // factorial of val is val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1) int fact(int val) { int ret = 1; // local variable to hold the result as we calculate it while (val > 1) ret *= val--; // assign ret * val to ret and decrement val return ret; // return the result } ``` 程序通过调用运算符(call operator)来执行函数。调用运算符的形式之一是一对圆括号`()`,作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是一个用逗号隔开的实际参数(argument,简称实参)列表,用来初始化函数形参。调用表达式的类型就是函数的返回类型。 ```c++ int main() { int j = fact(5); // j equals 120, i.e., the result of fact(5) cout << "5! is " << j << endl; return 0; } ``` 函数调用完成两项工作: - 用实参初始化对应的形参。 - 将控制权从主调函数转移给被调函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。 `return`语句结束函数的执行过程,完成两项工作: - 返回`return`语句中的值(可能没有值)。 - 将控制权从被调函数转移回主调函数,函数的返回值用于初始化调用表达式的结果。 实参是形参的初始值,两者的顺序和类型必须一一对应。 函数的形参列表可以为空,但是不能省略。 ```c++ void f1() { /* ... */ } // implicit void parameter list void f2(void) { /* ... */ } // explicit void parameter list ``` 形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。 ```c++ int f3(int v1, v2) { /* ... */ } // error int f4(int v1, int v2) { /* ... */ } // ok ``` 函数的任意两个形参不能同名,函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。 形参的名字是可选的,但是无法使用未命名的形参。即使某个形参不被函数使用,也必须为它提供一个实参。 函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。 ### 局部对象(Local Objects) 形参和函数体内定义的变量统称为局部变量(local variable)。 局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序结束才被销毁,对象所在的函数结束执行并不会对它产生影响。在变量类型前添加关键字`static`可以定义局部静态对象。 如果局部静态对象没有显式的初始值,它将执行值初始化。 ### 函数声明(Function Declarations) 和变量类似,函数只能定义一次,但可以声明多次。函数声明也叫做函数原型(function prototype)。 函数应该在头文件中声明,在源文件中定义。定义函数的源文件应该包含含有函数声明的头文件。 ### 分离式编译(Separate Compilation) 分离式编译允许我们把程序按照逻辑关系分割到几个文件中去,每个文件独立编译。这一过程通常会产生后缀名是*.obj*或*.o*的文件,该文件包含对象代码(object code)。之后编译器把对象文件链接(link)在一起形成可执行文件。 ## 参数传递(Argument Passing) 形参初始化的机理与变量初始化一样。 形参的类型决定了形参和实参交互的方式: - 当形参是引用类型时,它对应的实参被引用传递(passed by reference),函数被传引用调用(called by reference)。引用形参是它对应实参的别名。 - 当形参不是引用类型时,形参和实参是两个相互独立的对象,实参的值会被拷贝给形参(值传递,passed by value),函数被传值调用(called by value)。 ### 传值参数(Passing Arguments by Value) 如果形参不是引用类型,则函数对形参做的所有操作都不会影响实参。 使用指针类型的形参可以访问或修改函数外部的对象。 ```c++ // function that takes a pointer and sets the pointed-to value to zero void reset(int *ip) { *ip = 0; // changes the value of the object to which ip points ip = 0; // changes only the local copy of ip; the argument is unchanged } ``` 如果想在函数体内访问或修改函数外部的对象,建议使用引用形参代替指针形参。 ### 传引用参数(Passing Arguments by Reference) 通过使用引用形参,函数可以改变实参的值。 ```c++ // function that takes a reference to an int and sets the given object to zero void reset(int &i) // i is just another name for the object passed to reset { i = 0; // changes the value of the object to which i refers } ``` 使用引用形参可以避免拷贝操作,拷贝大的类类型对象或容器对象比较低效。另外有的类类型(如IO类型)根本就不支持拷贝操作,这时只能通过引用形参访问该类型的对象。 除了内置类型、函数对象和标准库迭代器外,其他类型的参数建议以引用方式传递。 如果函数无须改变引用形参的值,最好将其声明为常量引用。 一个函数只能返回一个值,但利用引用形参可以使函数返回额外信息。 ### const形参和实参(const Parameters and Arguments) 当形参有顶层`const`时,传递给它常量对象或非常量对象都是可以的。 可以使用非常量对象初始化一个底层`const`形参,但是反过来不行。 把函数不会改变的形参定义成普通引用会极大地限制函数所能接受的实参类型,同时也会给别人一种误导,即函数可以修改实参的值。 ### 数组形参(Array Parameters) 因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式。 ```c++ // each function has a single parameter of type const int* void print(const int*); void print(const int[]); // shows the intent that the function takes an array void print(const int[10]); // dimension for documentation purposes (at best) ``` 因为数组会被转换成指针,所以当我们传递给函数一个数组时,实际上传递的是指向数组首元素的指针。 因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。 以数组作为形参的函数必须确保使用数组时不会越界。 如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向常量的指针。 形参可以是数组的引用,但此时维度是形参类型的一部分,函数只能作用于指定大小的数组。 将多维数组传递给函数时,数组第二维(以及后面所有维度)的大小是数组类型的一部分,不能省略。 ```c++ f(int &arr[10]) // error: declares arr as an array of references f(int (&arr)[10]) // ok: arr is a reference to an array of ten ints ``` ### main:处理命令行选项(main:Handling Command-Line Options) 可以在命令行中向`main`函数传递参数,形式如下: ```c++ int main(int argc, char *argv[]) { /*...*/ } int main(int argc, char **argv) { /*...*/ } ``` 第二个形参*argv*是一个数组,数组元素是指向C风格字符串的指针;第一个形参*argc*表示数组中字符串的数量。 当实参传递给`main`函数后,*argv*的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。 在*Visual Studio*中可以设置`main`函数调试参数: ![6-1](Images/6-1.png) ### 含有可变形参的函数(Functions with Varying Parameters) C++11新标准提供了两种主要方法处理实参数量不定的函数s。 - 如果实参类型相同,可以使用`initializer_list`标准库类型。 ```c++ void error_msg(initializer_list il) { for (auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " " ; cout << endl; } ``` - 如果实参类型不同,可以定义可变参数模板。 C++还可以使用省略符形参传递可变数量的实参,但这种功能一般只用在与C函数交换的接口程序中。 `initializer_list`是一种标准库类型,定义在头文件*initializer_list*中,表示某种特定类型的值的数组。 `initializer_list`提供的操作: ![6-2](Images/6-2.png) 拷贝或赋值一个`initializer_list`对象不会拷贝列表中的元素。拷贝后,原始列表和副本共享元素。 `initializer_list`对象中的元素永远是常量值。 如果想向`initializer_list`形参传递一个值的序列,则必须把序列放在一对花括号内。 ```c++ if (expected != actual) error_msg(ErrCode(42), {"functionX", expected, actual}); else error_msg(ErrCode(0), {"functionX", "okay"}); ``` 因为`initializer_list`包含`begin`和`end`成员,所以可以使用范围`for`循环处理其中的元素。 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为`varargs`的C标准库功能。通常,省略符形参不应该用于其他目的。 省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。 ## 返回类型和return语句(Return Types and the return Statement) `return`语句有两种形式,作用是终止当前正在执行的函数并返回到调用该函数的地方。 ```c++ return; return expression; ``` ### 无返回值函数(Functions with No Return Value) 没有返回值的`return`语句只能用在返回类型是`void`的函数中。返回`void`的函数可以省略`return`语句,因为在这类函数的最后一条语句后面会隐式地执行`return`。 通常情况下,如果`void`函数想在其中间位置提前退出,可以使用`return`语句。 一个返回类型是`void`的函数也能使用`return`语句的第二种形式,不过此时`return`语句的*expression*必须是另一个返回`void`的函数。 强行令`void`函数返回其他类型的表达式将产生编译错误。 ### 有返回值函数(Functions That Return a Value) `return`语句的第二种形式提供了函数的结果。只要函数的返回类型不是`void`,该函数内的每条`return`语句就必须返回一个值,并且返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型(`main`函数例外)。 在含有`return`语句的循环后面应该也有一条`return`语句,否则程序就是错误的,但很多编译器无法发现此错误。 函数返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。 如果函数返回引用类型,则该引用仅仅是它所引用对象的一个别名。 函数不应该返回局部对象的指针或引用,因为一旦函数完成,局部对象将被释放。 ```c++ // disaster: this function returns a reference to a local object const string &manip() { string ret; // transform ret in some way if (!ret.empty()) return ret; // WRONG: returning a reference to a local object! else return "Empty"; // WRONG: "Empty" is a local temporary string } ``` 如果函数返回指针、引用或类的对象,则可以使用函数调用的结果访问结果对象的成员。 调用一个返回引用的函数会得到左值,其他返回类型得到右值。 C++11规定,函数可以返回用花括号包围的值的列表。同其他返回类型一样,列表也用于初始化表示函数调用结果的临时量。如果列表为空,临时量执行值初始化;否则返回的值由函数的返回类型决定。 - 如果函数返回内置类型,则列表内最多包含一个值,且该值所占空间不应该大于目标类型的空间。 - 如果函数返回类类型,由类本身定义初始值如何使用。 ```c++ vector process() { // . . . // expected and actual are strings if (expected.empty()) return {}; // return an empty vector else if (expected == actual) return {"functionX", "okay"}; // return list-initialized vector else return {"functionX", expected, actual}; } ``` `main`函数可以没有`return`语句直接结束。如果控制流到达了`main`函数的结尾处并且没有`return`语句,编译器会隐式地插入一条返回0的`return`语句。 `main`函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。 为了使`main`函数的返回值与机器无关,头文件*cstdlib*定义了`EXIT_SUCCESS`和`EXIT_FAILURE`这两个预处理变量,分别表示执行成功和失败。 ```c++ int main() { if (some_failure) return EXIT_FAILURE; // defined in cstdlib else return EXIT_SUCCESS; // defined in cstdlib } ``` 建议使用预处理变量`EXIT_SUCCESS`和`EXIT_FAILURE`表示`main`函数的执行结果。 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。 ```c++ // calculate val!, which is 1 * 2 * 3 . . . * val int factorial(int val) { if (val > 1) return factorial(val-1) * val; return 1; } ``` 在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。 相对于循环迭代,递归的效率较低。但在某些情况下使用递归可以增加代码的可读性。循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继),而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)。 `main`函数不能调用它自身。 ### 返回数组指针(Returning a Pointer to an Array) 因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。 返回数组指针的函数形式如下: ```c++ Type (*function(parameter_list))[dimension] ``` 其中*Type*表示元素类型,*dimension*表示数组大小,*(\*function (parameter_list))*两端的括号必须存在。 C++11允许使用尾置返回类型(trailing return type)简化复杂函数声明。尾置返回类型跟在形参列表后面,并以一个`->`符号开头。为了表示函数真正的返回类型在形参列表之后,需要在本应出现返回类型的地方添加`auto`关键字。 ```c++ // fcn takes an int argument and returns a pointer to an array of ten ints auto func(int i) -> int(*)[10]; ``` 任何函数的定义都能使用尾置返回类型,但是这种形式更适用于返回类型比较复杂的函数。 如果我们知道函数返回的指针将指向哪个数组,就可以使用`decltype`关键字声明返回类型。但`decltype`并不会把数组类型转换成指针类型,所以还要在函数声明中添加一个`*`符号。 ```c++ int odd[] = {1,3,5,7,9}; int even[] = {0,2,4,6,8}; // returns a pointer to an array of five int elements decltype(odd) *arrPtr(int i) { return (i % 2) ? &odd : &even; // returns a pointer to the array } ``` ## 函数重载(Overloaded Functions) 同一作用域内的几个名字相同但形参列表不同的函数叫做重载函数。 `main`函数不能重载。 不允许两个函数除了返回类型以外的其他所有要素都相同。 顶层`const`不影响传入函数的对象,一个拥有顶层`const`的形参无法和另一个没有顶层`const`的形参区分开来。 ```c++ Record lookup(Phone); Record lookup(const Phone); // redeclares Record lookup(Phone) Record lookup(Phone*); Record lookup(Phone* const); // redeclares Record lookup(Phone*) ``` 如果形参是某种类型的指针或引用,则通过区分其指向的对象是常量还是非常量可以实现函数重载,此时的`const`是底层的。当我们传递给重载函数一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。 ```c++ // functions taking const and nonconst references or pointers have different parameters // declarations for four independent, overloaded functions Record lookup(Account&); // function that takes a reference to Account Record lookup(const Account&); // new function that takes a const reference Record lookup(Account*); // new function, takes a pointer to Account Record lookup(const Account*); // new function, takes a pointer to const ``` `const_cast`可以用于函数的重载。当函数的实参不是常量时,将得到普通引用。 ```c++ // return a reference to the shorter of two strings const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } string &shorterString(string &s1, string &s2) { auto &r = shorterString(const_cast(s1), const_cast(s2)); return const_cast(r); } ``` 函数匹配(function matching)也叫做重载确定(overload resolution),是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。 调用重载函数时有三种可能的结果: - 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。 - 编译器找不到任何一个函数与实参匹配,发出无匹配(no match)的错误信息。 - 有一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出二义性调用(ambiguous call)的错误信息。 ### 重载与作用域(Overloading and Scope) 在不同的作用域中无法重载函数名。一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。 ```c++ string read(); void print(const string &); void print(double); // overloads the print function void fooBar(int ival) { bool read = false; // new scope: hides the outer declaration of read string s = read(); // error: read is a bool variable, not a function // bad practice: usually it's a bad idea to declare functions at local scope void print(int); // new scope: hides previous instances of print print("Value: "); // error: print(const string &) is hidden print(ival); // ok: print(int) is visible print(3.14); // ok: calls print(int); print(double) is hidden } ``` 在C++中,名字查找发生在类型检查之前。 ## 特殊用途语言特性(Features for Specialized Uses) ### 默认实参(Default Arguments) 默认实参作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。 ```c++ typedef string::size_type sz; string screen(sz ht = 24, sz wid = 80, char backgrnd = ' '); ``` 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。 如果想使用默认实参,只要在调用函数的时候省略该实参即可。 虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。 ```c++ // no default for the height or width parameters string screen(sz, sz, char = ' '); string screen(sz, sz, char = '*'); // error: redeclaration string screen(sz = 24, sz = 80, char); // ok: adds default ``` 默认实参只能出现在函数声明和定义其中一处。通常应该在函数声明中指定默认实参,并将声明放在合适的头文件中。 ```c++ // 函数声明 void fun(int n); int main() { // Error: 编译器向前查找函数声明 // fun调用形式与声明不符 fun(); return EXIT_SUCCESS; } // 函数定义 void fun(int n = 0) { /*...*/ } ``` 局部变量不能作为函数的默认实参。 用作默认实参的名字在函数声明所在的作用域内解析,但名字的求值过程发生在函数调用时。 ```c++ // the declarations of wd, def, and ht must appear outside a function sz wd = 80; char def = ' '; sz ht(); string screen(sz = ht(), sz = wd, char = def); string window = screen(); // calls screen(ht(), 80, ' ') void f2() { def = '*'; // changes the value of a default argument sz wd = 100; // hides the outer definition of wd but does not change the default window = screen(); // calls screen(ht(), 80, '*') } ``` ### 内联函数和constexpr函数(Inline and constexpr Functions) 内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。定义内联函数时需要在函数的返回类型前添加关键字`inline`。 ```c++ // inline version: find the shorter of two strings inline const string &horterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } ``` 在函数声明和定义中都能使用关键字`inline`,但是建议只在函数定义时使用。 一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和`switch`语句,否则函数会被编译为普通函数。 `constexpr`函数是指能用于常量表达式的函数。`constexpr`函数的返回类型及所有形参的类型都得是字面值类型。另外C++11标准要求`constexpr`函数体中必须有且只有一条`return`语句,但是此限制在C++14标准中被删除。 ```c++ constexpr int new_sz() { return 42; } constexpr int foo = new_sz(); // ok: foo is a constant expression ``` `constexpr`函数的返回值可以不是一个常量。 ```c++ // scale(arg) is a constant expression if arg is a constant expression constexpr size_t scale(size_t cnt) { return new_sz() * cnt; } int arr[scale(2)]; // ok: scale(2) is a constant expression int i = 2; // i is not a constant expression int a2[scale(i)]; // error: scale(i) is not a constant expression ``` `constexpr`函数被隐式地指定为内联函数。 和其他函数不同,内联函数和`constexpr`函数可以在程序中多次定义。因为在编译过程中,编译器需要函数的定义来随时展开函数。对于某个给定的内联函数或`constexpr`函数,它的多个定义必须完全一致。因此内联函数和`constexpr`函数通常定义在头文件中。 ### 调试帮助(Aids for Debugging) | 变量名称 | 内容 | | :--------: | :----------: | | `__func__` | 当前函数名称 | | `__FILE__` | 当前文件名称 | | `__LINE__` | 当前行号 | | `__TIME__` | 文件编译时间 | | `__DATE__` | 文件编译日期 | ## 函数匹配(Function Matching) 函数实参类型与形参类型越接近,它们匹配得越好。 重载函数集中的函数称为候选函数(candidate function)。 可行函数(viable function)的形参数量与函数调用所提供的实参数量相等,并且每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。 调用重载函数时应该尽量避免强制类型转换。 ### 实参类型转换(Argument Type Conversions) 所有算术类型转换的级别都一样。 如果重载函数的区别在于它们的引用或指针类型的形参是否含有底层`const`,则调用发生时编译器通过实参是否是常量来决定函数的版本。 ```c++ Record lookup(Account&); // function that takes a reference to Account Record lookup(const Account&); // new function that takes a const reference const Account a; Account b; lookup(a); // calls lookup(const Account&) lookup(b); // calls lookup(Account&) ``` ## 函数指针(Pointers to Functions) 要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。 ```c++ // compares lengths of two strings bool lengthCompare(const string &, const string &); // pf points to a function returning bool that takes two const string references bool (*pf)(const string &, const string &); // uninitialized ``` 可以直接使用指向函数的指针来调用函数,无须提前解引用指针。 ```c++ pf = lengthCompare; // pf now points to the function named lengthCompare pf = &lengthCompare; // equivalent assignment: address-of operator is optional bool b1 = pf("hello", "goodbye"); // calls lengthCompare bool b2 = (*pf)("hello", "goodbye"); // equivalent call bool b3 = lengthCompare("hello", "goodbye"); // equivalent call ``` 对于重载函数,编译器通过指针类型决定函数版本,指针类型必须与重载函数中的某一个精确匹配。 ```c++ void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff; // pf1 points to ff(unsigned) ``` 可以把函数的形参定义成指向函数的指针。调用时允许直接把函数名当作实参使用,它会自动转换成指针。 ```c++ // third parameter is a function type and is automatically treated as a pointer to function void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); // equivalent declaration: explicitly define the parameter as a pointer to function void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &)); // automatically converts the function lengthCompare to a pointer to function useBigger(s1, s2, lengthCompare); ``` 关键字`decltype`作用于函数时,返回的是函数类型,而不是函数指针类型。 函数可以返回指向函数的指针。但返回类型不会像函数类型的形参一样自动地转换成指针,必须显式地将其指定为指针类型。 # 第7章 类 类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程及设计技术。类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及其他私有函数。 ## 定义抽象数据类型(Defining Abstract Data Types) ### 设计Sales_data类(Designing the Sales_data Class) 类的用户是程序员,而非应用程序的最终使用者。 ### 定义改进的Sales_data类(Defining the Revised Sales_data Class) 成员函数(member function)的声明必须在类的内部,定义则既可以在类的内部也可以在类的外部。定义在类内部的函数是隐式的内联函数。 ```c++ struct Sales_data { // new members: operations on Sales_data objects std::string isbn() const { return bookNo; } Sales_data& combine(const Sales_data&); double avg_price() const; // data members std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; ``` 成员函数通过一个名为`this`的隐式额外参数来访问调用它的对象。`this`参数是一个常量指针,被初始化为调用该函数的对象地址。在函数体内可以显式使用`this`指针。 ```c++ total.isbn() // pseudo-code illustration of how a call to a member function is translated Sales_data::isbn(&total) std::string isbn() const { return this->bookNo; } std::string isbn() const { return bookNo; } ``` 默认情况下,`this`的类型是指向类类型非常量版本的常量指针。`this`也遵循初始化规则, C++允许在成员函数的参数列表后面添加关键字`const`,表示`this`是一个指向常量的指针。使用关键字`const`的成员函数被称作常量成员函数(const member function)。 ```c++ // pseudo-code illustration of how the implicit this pointer is used // this code is illegal: we may not explicitly define the this pointer ourselves // note that this is a pointer to const because isbn is a const member std::string Sales_data::isbn(const Sales_data *const this) { return this->isbn; } ``` 常量对象和指向常量对象的引用或指针都只能调用常量成员函数。 类本身就是一个作用域,成员函数的定义嵌套在类的作用域之内。编译器处理类时,会先编译成员声明,再编译成员函数体(如果有的话),因此成员函数可以随意使用类的其他成员而无须在意这些成员的出现顺序。 在类的外部定义成员函数时,成员函数的定义必须与它的声明相匹配。如果成员函数被声明为常量成员函数,那么它的定义也必须在参数列表后面指定`const`属性。同时,类外部定义的成员名字必须包含它所属的类名。 ```c++ double Sales_data::avg_price() const { if (units_sold) return revenue / units_sold; else return 0; } ``` 可以定义返回`this`对象的成员函数。 ```c++ Sales_data& Sales_data::combine(const Sales_data &rhs) { units_sold += rhs.units_sold; // add the members of rhs into revenue += rhs.revenue; // the members of 'this' object return *this; // return the object on which the function was called } ``` ### 定义类相关的非成员函数(Defining Nonmember Class-Related Functions) 类的作者通常会定义一些辅助函数,尽管这些函数从概念上来说属于类接口的组成部分,但实际上它们并不属于类本身。 ```c++ // input transactions contain ISBN, number of copies sold, and sales price istream &read(istream &is, Sales_data &item) { double price = 0; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.units_sold; return is; } ostream &print(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; } ``` 如果非成员函数是类接口的组成部分,则这些函数的声明应该与类放在同一个头文件中。 一般来说,执行输出任务的函数应该尽量减少对格式的控制。 ### 构造函数(Constructors) 类通过一个或几个特殊的成员函数来控制其对象的初始化操作,这些函数被称作构造函数。只要类的对象被创建,就会执行构造函数。 构造函数的名字和类名相同,没有返回类型,且不能被声明为`const`函数。构造函数在`const`对象的构造过程中可以向其写值。 ```c++ struct Sales_data { // constructors added Sales_data() = default; Sales_data(const std::string &s): bookNo(s) { } Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { } Sales_data(std::istream &); // other members as before }; ``` 类通过默认构造函数(default constructor)来控制默认初始化过程,默认构造函数无须任何实参。 如果类没有显式地定义构造函数,则编译器会为类隐式地定义一个默认构造函数,该构造函数也被称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,合成的默认构造函数初始化数据成员的规则如下: - 如果存在类内初始值,则用它来初始化成员。 - 否则默认初始化该成员。 某些类不能依赖于合成的默认构造函数。 - 只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。一旦类定义了其他构造函数,那么除非再显式地定义一个默认的构造函数,否则类将没有默认构造函数。 - 如果类包含内置类型或者复合类型的成员,则只有当这些成员全部存在类内初始值时,这个类才适合使用合成的默认构造函数。否则用户在创建类的对象时就可能得到未定义的值。 - 编译器不能为某些类合成默认构造函数。例如类中包含一个其他类类型的成员,且该类型没有默认构造函数,那么编译器将无法初始化该成员。 在C++11中,如果类需要默认的函数行为,可以通过在参数列表后面添加`=default`来要求编译器生成构造函数。其中`=default`既可以和函数声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果`=default`在类的内部,则默认构造函数是内联的。 ```c++ Sales_data() = default; ``` 构造函数初始值列表(constructor initializer list)负责为新创建对象的一个或几个数据成员赋初始值。形式是每个成员名字后面紧跟括号括起来的(或者在花括号内的)成员初始值,不同成员的初始值通过逗号分隔。 ```c++ Sales_data(const std::string &s): bookNo(s) { } Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { } ``` 当某个数据成员被构造函数初始值列表忽略时,它会以与合成默认构造函数相同的方式隐式初始化。 ```c++ // has the same behavior as the original constructor defined above Sales_data(const std::string &s): bookNo(s), units_sold(0), revenue(0) { } ``` 构造函数不应该轻易覆盖掉类内初始值,除非新值与原值不同。如果编译器不支持类内初始值,则所有构造函数都应该显式初始化每个内置类型的成员。 ### 拷贝、赋值和析构(Copy、Assignment,and Destruction) 编译器能合成拷贝、赋值和析构函数,但是对于某些类来说合成的版本无法正常工作。特别是当类需要分配类对象之外的资源时,合成的版本通常会失效。 ## 访问控制与封装(Access Control and Encapsulation) 使用访问说明符(access specifier)可以加强类的封装性: - 定义在`public`说明符之后的成员在整个程序内都可以被访问。`public`成员定义类的接口。 - 定义在`private`说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。`private`部分封装了类的实现细节。 ```c++ class Sales_data { public: // access specifier added Sales_data() = default; Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { } Sales_data(const std::string &s): bookNo(s) { } Sales_data(std::istream&); std::string isbn() const { return bookNo; } Sales_data &combine(const Sales_data&); private: // access specifier added double avg_price() const { return units_sold ? revenue/units_sold : 0; } std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; ``` 一个类可以包含零或多个访问说明符,每个访问说明符指定了接下来的成员的访问级别,其有效范围到出现下一个访问说明符或类的结尾处为止。 使用关键字`struct`定义类时,定义在第一个访问说明符之前的成员是`public`的;而使用关键字`class`时,这些成员是`private`的。二者唯一的区别就是默认访问权限不同。 ### 友元(Friends) 类可以允许其他类或函数访问它的非公有成员,方法是使用关键字`friend`将其他类或函数声明为它的友元。 ```C++ class Sales_data { // friend declarations for nonmember Sales_data operations added friend Sales_data add(const Sales_data&, const Sales_data&); friend std::istream &read(std::istream&, Sales_data&); friend std::ostream &print(std::ostream&, const Sales_data&); // other members and access specifiers as before public: Sales_data() = default; Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { } Sales_data(const std::string &s): bookNo(s) { } Sales_data(std::istream&); std::string isbn() const { return bookNo; } Sales_data &combine(const Sales_data&); private: std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; // declarations for nonmember parts of the Sales_data interface Sales_data add(const Sales_data&, const Sales_data&); std::istream &read(std::istream&, Sales_data&); std::ostream &print(std::ostream&, const Sales_data&); ``` 友元声明只能出现在类定义的内部,具体位置不限。友元不是类的成员,也不受它所在区域访问级别的约束。 通常情况下,最好在类定义开始或结束前的位置集中声明友元。 封装的好处: - 确保用户代码不会无意间破坏封装对象的状态。 - 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。 友元声明仅仅指定了访问权限,而并非一个通常意义上的函数声明。如果希望类的用户能调用某个友元函数,就必须在友元声明之外再专门对函数进行一次声明(部分编译器没有该限制)。 为了使友元对类的用户可见,通常会把友元的声明(类的外部)与类本身放在同一个头文件中。 ## 类的其他特性(Additional Class Features) ### 类成员再探(Class Members Revisited) 由类定义的类型名字和其他成员一样存在访问限制,可以是`public`或`private`中的一种。 ```c++ class Screen { public: // alternative way to declare a type member using a type alias using pos = std::string::size_type; // other members as before }; ``` 与普通成员不同,用来定义类型的成员必须先定义后使用。类型成员通常位于类起始处。 定义在类内部的成员函数是自动内联的。 如果需要显式声明内联成员函数,建议只在类外部定义的位置说明`inline``inline`成员函数该与类定义在同一个头文件中。 使用关键字`mutable`可以声明可变数据成员(mutable data member)。可变数据成员永远不会是`const`的,即使它在`const`对象内。因此`const`成员函数可以修改可变成员的值。 ```c++ class Screen { public: void some_member() const; private: mutable size_t access_ctr; // may change even in a const object // other members as before }; void Screen::some_member() const { ++access_ctr; // keep a count of the calls to any member function // whatever other work this member needs to do } ``` 提供类内初始值时,必须使用`=`或花括号形式。 ### 返回\*this的成员函数(Functions That Return \*this) `const`成员函数如果以引用形式返回`*this`,则返回类型是常量引用。 通过区分成员函数是否为`const`的,可以对其进行重载。在常量对象上只能调用`const`版本的函数;在非常量对象上,尽管两个版本都能调用,但会选择非常量版本。 ```c++ class Screen { public: // display overloaded on whether the object is const or not Screen &display(std::ostream &os) { do_display(os); return *this; } const Screen &display(std::ostream &os) const { do_display(os); return *this; } private: // function to do the work of displaying a Screen void do_display(std::ostream &os) const { os << contents; } // other members as before }; Screen myScreen(5,3); const Screen blank(5, 3); myScreen.set('#').display(cout); // calls non const version blank.display(cout); // calls const version ``` ### 类类型(Class Types) 每个类定义了唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型。 可以仅仅声明一个类而暂时不定义它。这种声明被称作前向声明(forward declaration),用于引入类的名字。在类声明之后定义之前都是一个不完全类型(incomplete type)。 ```c++ class Screen; // declaration of the Screen class ``` 可以定义指向不完全类型的指针或引用,也可以声明(不能定义)以不完全类型作为参数或返回类型的函数。 只有当类全部完成后才算被定义,所以一个类的成员类型不能是该类本身。但是一旦类的名字出现,就可以被认为是声明过了,因此类可以包含指向它自身类型的引用或指针。 ```c++ class Link_screen { Screen window; Link_screen *next; Link_screen *prev; }; ``` ### 友元再探(Friendship Revisited) 除了普通函数,类还可以把其他类或其他类的成员函数声明为友元。友元类的成员函数可以访问此类包括非公有成员在内的所有成员。 ```c++ class Screen { // Window_mgr members can access the private parts of class Screen friend class Window_mgr; // ... rest of the Screen class }; ``` 友元函数可以直接定义在类的内部,这种函数是隐式内联的。但是必须在类外部提供相应声明令函数可见。 ```c++ struct X { friend void f() { /* friend function can be defined in the class body */ } X() { f(); } // error: no declaration for f void g(); void h(); }; void X::g() { return f(); } // error: f hasn't been declared void f(); // declares the function defined inside X void X::h() { return f(); } // ok: declaration for f is now in scope ``` 友元关系不存在传递性。 把其他类的成员函数声明为友元时,必须明确指定该函数所属的类名。 ```c++ class Screen { // Window_mgr::clear must have been declared before class Screen friend void Window_mgr::clear(ScreenIndex); // ... rest of the Screen class }; ``` 如果类想把一组重载函数声明为友元,需要对这组函数中的每一个分别声明。 ## 类的作用域(Class Scope) 当成员函数定义在类外时,返回类型中使用的名字位于类的作用域之外,此时返回类型必须指明它是哪个类的成员。 ```c++ class Window_mgr { public: // add a Screen to the window and returns its index ScreenIndex addScreen(const Screen&); // other members as before }; // return type is seen before we're in the scope of Window_mgr Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) { screens.push_back(s); return screens.size() - 1; } ``` ### 名字查找与作用域(Name Lookup and Class Scope) 成员函数体直到整个类可见后才会被处理,因此它能使用类中定义的任何名字。 声明中使用的名字,包括返回类型或参数列表,都必须确保使用前可见。 如果类的成员使用了外层作用域的某个名字,而该名字表示一种类型,则类不能在之后重新定义该名字。 ```c++ typedef double Money; class Account { public: Money balance() { return bal; } // uses Money from the outer scop private: typedef double Money; // error: cannot redefine Money Money bal; // ... }; ``` 类型名定义通常出现在类起始处,这样能确保所有使用该类型的成员都位于类型名定义之后。 成员函数中名字的解析顺序: - 在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才会被考虑。 - 如果在成员函数内没有找到,则会在类内继续查找,这时会考虑类的所有成员。 - 如果类内也没有找到,会在成员函数定义之前的作用域查找。 ```c++ // it is generally a bad idea to use the same name for a parameter and a member int height; // defines a name subsequently used inside Screen class Screen { public: typedef std::string::size_type pos; void dummy_fcn(pos height) { cursor = width * height; // which height? the parameter } private: pos cursor = 0; pos height = 0, width = 0; }; ``` 可以通过作用域运算符`::`或显式`this`指针来强制访问被隐藏的类成员。 ```c++ // bad practice: names local to member functions shouldn't hide member names void Screen::dummy_fcn(pos height) { cursor = width * this->height; // member height // alternative way to indicate the member cursor = width * Screen::height; // member height } // good practice: don't use a member name for a parameter or other local variable void Screen::dummy_fcn(pos ht) { cursor = width * height; // member height } ``` ## 构造函数再探(Constructors Revisited) ### 构造函数初始值列表(Constructor Initializer List) 如果没有在构造函数初始值列表中显式初始化成员,该成员会在构造函数体之前执行默认初始化如果成员是`const`、引用,或者是某种未定义默认构造函数的类类型,必须在初始值列表中将其初始化。 ```c++ class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; // ok: explicitly initialize reference and const members ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { } ``` 最好令构造函数初始值的顺序与成员声明的顺序一致,并且尽量避免使用某些成员初始化其他成员。 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。 ### 委托构造函数(Delegating Constructors) C++11扩展了构造函数初始值功能,可以定义委托构造函数。委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。 ```c++ class Sales_data { public: // defines the default constructor as well as one that takes a string argument Sales_data(std::string s = ""): bookNo(s) { } // remaining constructors unchanged Sales_data(std::string s, unsigned cnt, double rev): bookNo(s), units_sold(cnt), revenue(rev*cnt) { } Sales_data(std::istream &is) { read(is, *this); } // remaining members as before } ``` ### 默认构造函数的作用(The Role of the Default Constructor) 当对象被默认初始化或值初始化时会自动执行默认构造函数。 默认初始化的发生情况: - 在块作用域内不使用初始值定义非静态变量或数组。 - 类本身含有类类型的成员且使用合成默认构造函数。 - 类类型的成员没有在构造函数初始值列表中显式初始化值初始化的发生情况: - 数组初始化时提供的初始值数量少于数组大小。 - 不使用初始值定义局部静态变量。 - 通过`T()`形式(*T*为类型)的表达式显式地请求值初始化。 类必须包含一个默认构造函数。 如果想定义一个使用默认构造函数进行初始化的对象应该去掉对象名后的空括号对。 ```c++ Sales_data obj(); // oops! declares a function, not an object Sales_data obj2; // ok: obj2 is an object, not a function ``` ### 隐式的类类型转换(Implicit Class-Type Conversions) 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数被称为转换构造函数(converting constructor)。 ```c++ string null_book = "9-999-99999-9"; // constructs a temporary Sales_data object // with units_sold and revenue equal to 0 and bookNo equal to null_book item.combine(null_book); ``` 编译器只会自动执行一步类型转换。 ```c++ // error: requires two user-defined conversions: // (1) convert "9-999-99999-9" to string // (2) convert that (temporary) string to Sales_data item.combine("9-999-99999-9"); // ok: explicit conversion to string, implicit conversion to Sales_data item.combine(string("9-999-99999-9")); // ok: implicit conversion to string, explicit conversion to Sales_data item.combine(Sales_data("9-999-99999-9")); ``` 在要求隐式转换的程序上下文中,可以通过将构造函数声明为`explicit`的加以阻止。 ```c++ class Sales_data { public: Sales_data() = default; Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { } explicit Sales_data(const std::string &s): bookNo(s) { } explicit Sales_data(std::istream&); // remaining members as before }; ``` `explicit`关键字只对接受一个实参的构造函数有效。 只能在类内声明构造函数时使用`explicit`关键字,在类外定义时不能重复。 执行拷贝初始化时(使用`=`)会发生隐式转换,所以`explicit`构造函数只能用于直接初始化。 ```c++ Sales_data item1 (null_book); // ok: direct initialization // error: cannot use the copy form of initialization with an explicit constructor Sales_data item2 = null_book; ``` 可以使用`explicit`构造函数显式地强制转换类型。 ```c++ // ok: the argument is an explicitly constructed Sales_data object item.combine(Sales_data(null_book)); // ok: static_cast can use an explicit constructor item.combine(static_cast(cin)); ``` ### 聚合类(Aggregate Classes) 聚合类满足如下条件: - 所有成员都是`public`的。 - 没有定义任何构造函数。 - 没有类内初始值。 - 没有基类。 - 没有虚函数。 ```c++ struct Data { int ival; string s; }; ``` 可以使用一个用花括号包围的成员初始值列表初始化聚合类的数据成员。初始值顺序必须与声明顺序一致。如果初始值列表中的元素个数少于类的成员个数,则靠后的成员被值初始化。 ```c++ // val1.ival = 0; val1.s = string("Anna") Data val1 = { 0, "Anna" }; ``` ### 字面值常量类(Literal Classes) 数据成员都是字面值类型的聚合类是字面值常量类。或者一个类不是聚合类,但符合下列条件,则也是字面值常量类: - 数据成员都是字面值类型。 - 类至少含有一个`constexpr`构造函数。 - 如果数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式。如果成员属于类类型,则初始值必须使用成员自己的`constexpr`构造函数。 - 类必须使用析构函数的默认定义。 `constexpr`构造函数用于生成`constexpr`对象以及`constexpr`函数的参数或返回类型。 `constexpr`构造函数必须初始化所有数据成员,初始值使用`constexpr`构造函数或常量表达式。 ## 类的静态成员(static Class Members) 使用关键字`static`可以声明类的静态成员。静态成员存在于任何对象之外,对象中不包含与静态成员相关的数据。 ```c++ class Account { public: void calculate() { amount += amount * interestRate; } static double rate() { return interestRate; } static void rate(double); private: std::string owner; double amount; static double interestRate; static double initRate(); }; ``` 由于静态成员不与任何对象绑定,因此静态成员函数不能声明为`const`的,也不能在静态成员函数内使用`this`指针。 用户代码可以使用作用域运算符访问静态成员,也可以通过类对象、引用或指针访问。类的成员函数可以直接访问静态成员。 ```c++ double r; r = Account::rate(); // access a static member using the scope operator Account ac1; Account *ac2 = &ac1; // equivalent ways to call the static member rate function r = ac1.rate(); // through an Account object or reference r = ac2->rate(); // through a pointer to an Account object class Account { public: void calculate() { amount += amount * interestRate; } private: static double interestRate; // remaining members as before }; ``` 在类外部定义静态成员时,不能重复`static`关键字,其只能用于类内部的声明语句。 由于静态数据成员不属于类的任何一个对象,因此它们并不是在创建类对象时被定义的。通常情况下,不应该在类内部初始化静态成员。而必须在类外部定义并初始化每个静态成员。一个静态成员只能被定义一次。一旦它被定义,就会一直存在于程序的整个生命周期中。 ```c++ // define and initialize a static class member double Account::interestRate = initRate(); ``` 建议把静态数据成员的定义与其他非内联函数的定义放在同一个源文件中,这样可以确保对象只被定义一次。 尽管在通常情况下,不应该在类内部初始化静态成员。但是可以为静态成员提供`const`整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的`constexpr`。初始值必须是常量表达式。 ```c++ class Account { public: static double rate() { return interestRate; } static void rate(double); private: static constexpr int period = 30; // period is a constant double daily_tbl[period]; }; ``` 静态数据成员的类型可以是它所属的类类型。 ```c++ class Bar { static Bar mem1; // ok: static member can have incomplete type Bar *mem2; // ok: pointer member can have incomplete type Bar mem3; // error: data members must have complete type } ``` 可以使用静态成员作为函数的默认实参。 ```c++ class Screen { public: // bkground refers to the static member // declared later in the class definition Screen& clear(char = bkground); private: static const char bkground; }; ``` # 第8章 IO库 部分IO库设施: - `istream`:输入流类型,提供输入操作。 - `ostream`:输出流类型,提供输出操作。 - `cin`:`istream`对象,从标准输入读取数据。 - `cout`:`ostream`对象,向标准输出写入数据。 - `cerr`:`ostream`对象,向标准错误写入数据。 - `>>`运算符:从`istream`对象读取输入数据。 - `<<`运算符:向`ostream`对象写入输出数据。 - `getline`函数:从`istream`对象读取一行数据,写入`string`对象。 ## IO类(The IO Classes) 头文件*iostream*定义了用于读写流的基本类型,*fstream*定义了读写命名文件的类型,*sstream*定义了读写内存中`string`对象的类型。 ![8-1](Images/8-1.png) 宽字符版本的IO类型和函数的名字以`w`开始,如`wcin`、`wcout`和`wcerr`分别对应`cin`、`cout`和`cerr`。它们与其对应的普通`char`版本都定义在同一个头文件中,如头文件*fstream*定义了`ifstream`和`wifstream`类型。 可以将派生类的对象当作其基类的对象使用。 ### IO象无拷贝或赋值(No Copy or Assign for IO Objects) 不能拷贝或对IO对象赋值。 ```c++ ofstream out1, out2; out1 = out2; // error: cannot assign stream objects ofstream print(ofstream); // error: can't initialize the ofstream parameter out2 = print(out2); // error: cannot copy stream objects ``` 由于IO对象不能拷贝,因此不能将函数形参或返回类型定义为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是`const`的。 ### 条件状态(Condition States) IO库条件状态: ![8-2](Images/8-2.png) `badbit`表示系统级错误,如不可恢复的读写错误。通常情况下,一旦`badbit`被置位,流就无法继续使用了。在发生可恢复错误后,`failbit`会被置位,如期望读取数值却读出一个字符。如果到达文件结束位置,`eofbit`和`failbit`都会被置位。如果流未发生错误,则`goodbit`的值为0。如果`badbit`、`failbit`和`eofbit`任何一个被置位,检测流状态的条件都会失败。 ```c++ while (cin >> word) // ok: read operation successful... ``` `good`函数在所有错误均未置位时返回`true`。而`bad`、`fail`和`eof`函数在对应错误位被置位时返回`true`。此外,在`badbit`被置位时,`fail`函数也会返回`true`。因此应该使用`good`或`fail`函数确定流的总体状态,`eof`和`bad`只能检测特定错误。 流对象的`rdstate`成员返回一个`iostate`值,表示流的当前状态。`setstate`成员用于将指定条件置位(叠加原始流状态)。`clear`成员的无参版本清除所有错误标志;含参版本接受一个`iostate`值,用于设置流的新状态(覆盖原始流状态)。 ```c++ // remember the current state of cin auto old_state = cin.rdstate(); // remember the current state of cin cin.clear(); // make cin valid process_input(cin); // use cin cin.setstate(old_state); // now reset cin to its old state ``` ### 管理输出缓冲(Managing the Output Buffer) 每个输出流都管理一个缓冲区,用于保存程序读写的数据。导致缓冲刷新(即数据真正写入输出设备或文件)的原因有很多: - 程序正常结束。 - 缓冲区已满。 - 使用操纵符(如`endl`)显式刷新缓冲区。 - 在每个输出操作之后,可以用`unitbuf`操纵符设置流的内部状态,从而清空缓冲区。默认情况下,对`cerr`是设置`unitbuf`的,因此写到`cerr`的内容都是立即刷新的。 - 一个输出流可以被关联到另一个流。这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。默认情况下,`cin`和`cerr`都关联到`cout`,因此,读`cin`或写`cerr`都会刷新`cout`的缓冲区。 `flush`操纵符刷新缓冲区,但不输出任何额外字符。`ends`向缓冲区插入一个空字符,然后刷新缓冲区。 ```c++ cout << "hi!" << endl; // writes hi and a newline, then flushes the buffer cout << "hi!" << flush; // writes hi, then flushes the buffer; adds no data cout << "hi!" << ends; // writes hi and a null, then flushes the buffer ``` 如果想在每次输出操作后都刷新缓冲区,可以使用`unitbuf`操纵符。它令流在接下来的每次写操作后都进行一次`flush`操作。而`nounitbuf`操纵符则使流恢复使用正常的缓冲区刷新机制。 ```C++ cout << unitbuf; // all writes will be flushed immediately // any output is flushed immediately, no buffering cout << nounitbuf; // returns to normal buffering ``` 如果程序异常终止,输出缓冲区不会被刷新。 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将`cout`和`cin`关联在一起,因此下面的语句会导致`cout`的缓冲区被刷新: ```c++ cin >> ival; ``` 交互式系统通常应该关联输入流和输出流。这意味着包括用户提示信息在内的所有输出,都会在读操作之前被打印出来。 使用`tie`函数可以关联两个流。它有两个重载版本:无参版本返回指向输出流的指针。如果本对象已关联到一个输出流,则返回的就是指向这个流的指针,否则返回空指针。`tie`的第二个版本接受一个指向`ostream`的指针,将本对象关联到此`ostream`。 ```c++ cin.tie(&cout); // illustration only: the library ties cin and cout for us // old_tie points to the stream (if any) currently tied to cin ostream *old_tie = cin.tie(nullptr); // cin is no longer tied // ties cin and cerr; not a good idea because cin should be tied to cout cin.tie(&cerr); // reading cin flushes cerr, not cout cin.tie(old_tie); // reestablish normal tie between cin and cout ``` 每个流同时最多关联一个流,但多个流可以同时关联同一个`ostream`。向`tie`传递空指针可以解开流的关联。 ## 文件输入输出(File Input and Output) 头文件*fstream*定义了三个类型来支持文件IO:`ifstream`从给定文件读取数据,`ofstream`向指定文件写入数据,`fstream`可以同时读写指定文件。 ![8-3](Images/8-3.png) ### 使用文件流对象(Using File Stream Objects) 每个文件流类型都定义了`open`函数,它完成一些系统操作,定位指定文件,并视情况打开为读或写模式。 创建文件流对象时,如果提供了文件名(可选),`open`会被自动调用。 ```C++ ifstream in(ifile); // construct an ifstream and open the given file ofstream out; // output file stream that is not associated with any file ``` 在C++11中,文件流对象的文件名可以是`string`对象或C风格字符数组。旧版本的标准库只支持C风格字符数组。 在要求使用基类对象的地方,可以用继承类型的对象代替。因此一个接受`iostream`类型引用或指针参数的函数,可以用对应的`fstream`类型来调用。 可以先定义空文件流对象,再调用`open`函数将其与指定文件关联。如果`open`调用失败,`failbit`会被置位。 对一个已经打开的文件流调用`open`会失败,并导致`failbit`被置位。随后试图使用文件流的操作都会失败。如果想将文件流关联到另一个文件,必须先调用`close`关闭当前文件,再调用`clear`重置流的条件状态(`close`不会重置流的条件状态)。 当`fstream`对象被销毁时,`close`会自动被调用。 ### 文件模式(File Modes) 每个流都有一个关联的文件模式,用来指出如何使用文件。 ![8-4](Images/8-4.png) - 只能对`ofstream`或`fstream`对象设定`out`模式。 - 只能对`ifstream`或`fstream`对象设定`in`模式。 - 只有当`out`被设定时才能设定`trunc`模式。 - 只要`trunc`没被设定,就能设定`app`模式。在`app`模式下,即使没有设定`out`模式,文件也是以输出方式打开。 - 默认情况下,即使没有设定`trunc`,以`out`模式打开的文件也会被截断。如果想保留以`out`模式打开的文件内容,就必须同时设定`app`模式,这会将数据追加写到文件末尾;或者同时设定`in`模式,即同时进行读写操作。 - `ate`和`binary`模式可用于任何类型的文件流对象,并可以和其他任何模式组合使用。 - 与`ifstream`对象关联的文件默认以`in`模式打开,与`ofstream`对象关联的文件默认以`out`模式打开,与`fstream`对象关联的文件默认以`in`和`out`模式打开。 默认情况下,打开`ofstream`对象时,文件内容会被丢弃,阻止文件清空的方法是同时指定`app`或`in`模式。 流对象每次打开文件时都可以改变其文件模式。 ```c++ ofstream out; // no file mode is set out.open("scratchpad"); // mode implicitly out and trunc out.close(); // close out so we can use it for a different file out.open("precious", ofstream::app); // mode is out and app out.close(); ``` ## string流(string Streams) ![8-5](Images/8-5.png) ### 使用istringstream(Using an istringstream) ```c++ // members are public by default struct PersonInfo { string name; vector phones; }; string line, word; // will hold a line and word from input, respectively vector people; // will hold all the records from the input // read the input a line at a time until cin hits end-of-file (or another error) while (getline(cin, line)) { PersonInfo info; // create an object to hold this record's data istringstream record(line); // bind record to the line we just read record >> info.name; // read the name while (record >> word) // read the phone numbers info.phones.push_back(word); // and store them people.push_back(info); // append this record to people } ``` ### 使用ostringstream(Using ostringstreams) ```C++ for (const auto &entry : people) { // for each entry in people ostringstream formatted, badNums; // objects created on each loop for (const auto &nums : entry.phones) { // for each number if (!valid(nums)) { badNums << " " << nums; // string in badNums } else // ''writes'' to formatted's string formatted << " " << format(nums); } if (badNums.str().empty()) // there were no bad numbers os << entry.name << " " // print the name << formatted.str() << endl; // and reformatted numbers else // otherwise, print the name and bad numbers cerr << "input error: " << entry.name << " invalid number(s) " << badNums.str() << endl; } ``` # 第9章 顺序容器 ## 顺序容器概述(Overview of the Sequential Containers) 顺序容器类型: ![9-1](Images/9-1.png) | 类型 | 特性 | | :------------: | :----------------------------------------------------------: | | `vector` | 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素可能很慢 | | `deque` | 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 | | `list` | 双向链表。只支持双向顺序访问。在任何位置插入/删除速度都很快 | | `forward_list` | 单向链表。只支持单向顺序访问。在任何位置插入/删除速度都很快 | | `array` | 固定大小数组。支持快速随机访问。不能添加/删除元素 | | `string` | 类似`vector`,但用于保存字符。支持快速随机访问。在尾部插入/删除速度很快 | `forward_list`和`array`是C++11新增类型。与内置数组相比,`array`更安全易用。`forward_list`没有`size`操作。 容器选择原则: - 除非有合适的理由选择其他容器,否则应该使用`vector`。 - 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用`list`或`forward_list`。 - 如果程序要求随机访问容器元素,则应该使用`vector`或`deque`。 - 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用`deque`。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则: - 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向`vector`追加数据,再调用标准库的`sort`函数重排元素,从而避免在中间位置添加元素。 - 如果必须在中间位置插入元素,可以在输入阶段使用`list`。输入完成后将`list`中的内容拷贝到`vector`中。 - 不确定应该使用哪种容器时,可以先只使用`vector`和`list`的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择`vector`或`list`都很方便。 ## 容器库概览(Container Library Overview) 每个容器都定义在一个头文件中,文件名与类型名相同。容器均为模板类型。 ![9-2](Images/9-2.png) ### 迭代器(Iterators) `forward_list`类型不支持递减运算符`--`。 一个迭代器范围(iterator range)由一对迭代器表示。这两个迭代器通常被称为`begin`和`end`,分别指向同一个容器中的元素或尾后地址。`end`迭代器不会指向范围中的最后一个元素,而是指向尾元素之后的位置。这种元素范围被称为左闭合区间(left-inclusive interval),迭代器`begin`和`end`必须指向相同的容器,`end`可以与`begin`指向相同的位置,但不能指向`begin`之前的位置(由程序员确保)。 假定`begin`和`end`构成一个合法的迭代器范围,则: - 如果`begin`等于`end`,则范围为空。 - 如果`begin`不等于`end`,则范围内至少包含一个元素,且`begin`指向该范围内的第一个元素。 - 可以递增`begin`若干次,令`begin`等于`end`。 ```c++ while (begin != end) { *begin = val; // ok: range isn't empty so begin denotes an element ++begin; // advance the iterator to get the next element } ``` ### 容器类型成员(Container Type Members) 通过类型别名,可以在不了解容器元素类型的情况下使用元素。如果需要元素类型,可以使用容器的`value_type`。如果需要元素类型的引用,可以使用`reference`或`const_reference`。 ### begin和end成员(begin and end Members) `begin`和`end`操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围。 `begin`和`end`操作有多个版本:带`r`的版本返回反向迭代器。以`c`开头的版本(C++11新增)返回`const`迭代器。不以`c`开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对`const`对象调用时,返回`const`迭代器。 ```c++ list a = {"Milton", "Shakespeare", "Austen"}; auto it1 = a.begin(); // list::iterator auto it2 = a.rbegin(); // list::reverse_iterator auto it3 = a.cbegin(); // list::const_iterator auto it4 = a.crbegin(); // list::const_reverse_iterator ``` 当`auto`与`begin`或`end`结合使用时,返回的迭代器类型依赖于容器类型。但调用以`c`开头的版本仍然可以获得`const`迭代器,与容器是否是常量无关。 当程序不需要写操作时,应该使用`cbegin`和`cend`。 ### 容器定义和初始化(Defining and Initializing a Container) 容器定义和初始化方式: ![9-3](Images/9-3.png) 将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。 传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。 ```c++ // each container has three elements, initialized from the given initializers list authors = {"Milton", "Shakespeare", "Austen"}; vector articles = {"a", "an", "the"}; list list2(authors); // ok: types match deque authList(authors); // error: container types don't match vector words(articles); // error: element types must match // ok: converts const char* elements to string forward_list words(articles.begin(), articles.end()); ``` C++11允许对容器进行列表初始化。 ```c++ // each container has three elements, initialized from the given initializers list authors = {"Milton", "Shakespeare", "Austen"}; vector articles = {"a", "an", "the"}; ``` 定义和使用`array`类型时,需要同时指定元素类型和容器大小。 ```c++ array // type is: array that holds 42 ints array // type is: array that holds 10 strings array::size_type i; // array type includes element type and size array::size_type j; // error: array is not a type ``` 对`array`进行列表初始化时,初始值的数量不能大于`array`的大小。如果初始值的数量小于`array`的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。 可以对`array`进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。 ### 赋值和swap(Assignment and swap) 容器赋值操作: ![9-4](Images/9-4.png) 赋值运算符两侧的运算对象必须类型相同。`assign`允许用不同但相容的类型赋值,或者用容器的子序列赋值。 ```c++ list names; vector oldstyle; names = oldstyle; // error: container types don't match // ok: can convert from const char*to string names.assign(oldstyle.cbegin(), oldstyle.cend()); ``` 由于其旧元素被替换,因此传递给`assign`的迭代器不能指向调用`assign`的容器本身。 `swap`交换两个相同类型容器的内容。除`array`外,`swap`不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。 ```c++ vector svec1(10); // vector with ten elements vector svec2(24); // vector with 24 elements swap(svec1, svec2); ``` 赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而`swap`操作交换容器内容,不会导致迭代器、引用和指针失效(`array`和`string`除外)。 对于`array`,`swap`会真正交换它们的元素。因此在`swap`操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。 ```c++ array a = { 1, 2, 3 }; array b = { 4, 5, 6 }; auto p = a.cbegin(), q = a.cend(); a.swap(b); // 输出交换后的值,即4、5、6 while (p != q) { cout << *p << endl; ++p; } ``` 对于其他容器类型(除`string`),指针、引用和迭代器在`swap`操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。 ```c++ vector a = { 1, 2, 3 }; vector b = { 4, 5, 6 }; auto p = a.cbegin(), q = a.cend(); a.swap(b); // 输出交换前的值,即1、2、3 while (p != q) { cout << *p << endl; ++p; } ``` `array`不支持`assign`,也不允许用花括号列表进行赋值。 ```c++ array a1 = {0,1,2,3,4,5,6,7,8,9}; array a2 = {0}; // elements all have value 0 a1 = a2; // replaces elements in a1 a2 = {0}; // error: cannot assign to an array from a braced list ``` 新标准库同时提供了成员和非成员函数版本的`swap`。非成员版本的`swap`在泛型编程中非常重要,建议统一使用非成员版本的`swap`。 ### 容器大小操作(Container Size Operations) `size`成员返回容器中元素的数量;`empty`当`size`为0时返回`true`,否则返回`false`;`max_size`返回一个大于或等于该类型容器所能容纳的最大元素数量的值。`forward_list`支持`max_size`和`empty`,但不支持`size`。 ### 关系运算符(Relational Operators) 每个容器类型都支持相等运算符(`==`、`!=`)除无序关联容器外,其他容器都支持关系运算符(`>`、`>=`、`<`、`<=`)。关系运算符两侧的容器类型和保存元素类型都必须相同。 两个容器的比较实际上是元素的逐对比较,其工作方式与`string`的关系运算符类似: - 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。 - 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。 - 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。 ```c++ vector v1 = { 1, 3, 5, 7, 9, 12 }; vector v2 = { 1, 3, 9 }; vector v3 = { 1, 3, 5, 7 }; vector v4 = { 1, 3, 5, 7, 9, 12 }; v1 < v2 // true; v1 and v2 differ at element [2]: v1[2] is less than v2[2] v1 < v3 // false; all elements are equal, but v3 has fewer of them; v1 == v4 // true; each element is equal and v1 and v4 have the same size() v1 == v2 // false; v2 has fewer elements than v1 ``` 容器的相等运算符实际上是使用元素的`==`运算符实现的,而其他关系运算符则是使用元素的`<`运算符。如果元素类型不支持所需运算符,则保存该元素的容器就不能使用相应的关系运算。 ## 顺序容器操作(Sequential Container Operations) ### 向顺序容器添加元素(Adding Elements to a Sequential Container) 除`array`外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。 ![9-5](Images/9-5.png) `push_back`将一个元素追加到容器尾部,`push_front`将元素插入容器头部。 ```c++ // read from standard input, putting each word onto the end of container string word; while (cin >> word) container.push_back(word); ``` `insert`将元素插入到迭代器指定的位置之前。一些不支持`push_front`的容器可以使用`insert`将元素插入开始位置。 ```c++ vector svec; list slist; // equivalent to calling slist.push_front("Hello!"); slist.insert(slist.begin(), "Hello!"); // no push_front on vector but we can insert before begin() // warning: inserting anywhere but at the end of a vector might be slow svec.insert(svec.begin(), "Hello!"); ``` 将元素插入到`vector`、`deque`或`string`的任何位置都是合法的,但可能会很耗时。 在新标准库中,接受元素个数或范围的`insert`版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回`void`。如果范围为空,不插入任何元素,`insert`会返回第一个参数。 ```c++ list 1st; auto iter = 1st.begin(); while (cin >> word) iter = 1st.insert(iter, word); // same as calling push_front ``` 新标准库增加了三个直接构造而不是拷贝元素的操作:`emplace_front`、`emplace_back`和`emplace`,其分别对应`push_front`、`push_back`和`insert`。当调用`push`或`insert`时,元素对象被拷贝到容器中。而调用`emplace`时,则是将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素。 ```c++ // construct a Sales_data object at the end of c // uses the three-argument Sales_data constructor c.emplace_back("978-0590353403", 25, 15.99); // error: there is no version of push_back that takes three arguments c.push_back("978-0590353403", 25, 15.99); // ok: we create a temporary Sales_data object to pass to push_back c.push_back(Sales_data("978-0590353403", 25, 15.99)); ``` 传递给`emplace`的参数必须与元素类型的构造函数相匹配。 `forward_list`有特殊版本的`insert`和`emplace`操作,且不支持`push_back`和`emplace_back`。`vector`和`string`不支持`push_front`和`emplace_front`。 ### 访问元素(Accessing Elements) 每个顺序容器都有一个`front`成员函数,而除了`forward_list`之外的顺序容器还有一个`back`成员函数。这两个操作分别返回首元素和尾元素的引用。 在调用`front`和`back`之前,要确保容器非空。 顺序容器的元素访问操作: ![9-6](Images/9-6.png) 在容器中访问元素的成员函数都返回引用类型。如果容器是`const`对象,则返回`const`引用,否则返回普通引用。 可以快速随机访问的容器(`string`、`vector`、`deque`和`array`)都提供下标运算符。保证下标有效是程序员的责任。如果希望确保下标合法,可以使用`at`成员函数。`at`类似下标运算,但如果下标越界,`at`会抛出`out_of_range`异常。 ```c++ vector svec; // empty vector cout << svec[0]; // run-time error: there are no elements in svec! cout << svec.at(0); // throws an out_of_range exception ``` ### 删除元素(Erasing Elements) 顺序容器的元素删除操作: ![9-7](Images/9-7.png) 删除`deque`中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。删除`vector`或`string`的元素后,指向删除点之后位置的迭代器、引用和指针也都会失效。 删除元素前,程序员必须确保目标元素存在。 `pop_front`和`pop_back`函数分别删除首元素和尾元素。`vector`和`string`类型不支持`pop_front`,`forward_list`类型不支持`pop_back`。 `erase`函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的`erase`都返回指向删除元素(最后一个)之后位置的迭代器。 ```c++ // delete the range of elements between two iterators // returns an iterator to the element just after the last removed element elem1 = slist.erase(elem1, elem2); // after the call elem1 == elem2 ``` `clear`函数删除容器内的所有元素。 ### 特殊的forward_list操作(Specialized forward_list Operations) 在`forward_list`中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。 ![9-8](Images/9-8.png) `forward_list`的插入和删除操作: ![9-9](Images/9-9.png) ### 改变容器大小(Resizing a Container) 顺序容器的大小操作: ![9-10](Images/9-10.png) `resize`函数接受一个可选的元素值参数,用来初始化添加到容器中的元素,否则新元素进行值初始化。如果容器保存的是类类型元素,且`resize`向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。 ### 容器操作可能使迭代器失效(Container Operations May Invalidate Iterators) 向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。 - 向容器中添加元素后: - 如果容器是`vector`或`string`类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。 - 如果容器是`deque`类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。 - 如果容器是`list`或`forward_list`类型,指向容器的迭代器、指针和引用仍然有效。 - 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效: - 如果容器是`list`或`forward_list`类型,指向容器其他位置的迭代器、指针和引用仍然有效。 - 如果容器是`deque`类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。 - 如果容器是`vector`或`string`类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。 必须保证在每次改变容器后都正确地重新定位迭代器。 不要保存`end`函数返回的迭代器。 ```c++ // safer: recalculate end on each trip whenever the loop adds/erases elements while (begin != v.end()) { // do some processing ++begin; // advance begin because we want to insert after this element begin = v.insert(begin, 42); // insert the new value ++begin; // advance begin past the element we just added } ``` ## vector对象是如何增长的(How a vector Grows) `vector`和`string`的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。 容器大小管理操作: ![9-11](Images/9-11.png) `capacity`函数返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。`reserve`函数告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小。 ![9-12](Images/9-12.png) 只有当需要的内存空间超过当前容量时,`reserve`才会真正改变容器容量,分配不小于需求大小的内存空间。当需求容量小于当前容量时,`reserve`并不会退回内存空间。因此在调用`reserve`之后,`capacity`会大于或等于传递给`reserve`的参数。 在C++11中可以使用`shrink_to_fit`函数来要求`deque`、`vector`和`string`退回不需要的内存空间(并不保证退回)。 ## 额外的string操作(Additional string Operations) ### 构造string的其他方法(Other Ways to Construct strings) 构造`string`的其他方法: ![9-13](Images/9-13.png) 从另一个`string`对象拷贝字符构造`string`时,如果提供的拷贝开始位置(可选)大于给定`string`的大小,则构造函数会抛出`out_of_range`异常。 子字符串操作: ![9-14](Images/9-14.png) 如果传递给`substr`函数的开始位置超过`string`的大小,则函数会抛出`out_of_range`异常。 ### 改变string的其他方法(Other Ways to Change a string) 修改`string`的操作: ![9-15](Images/9-15.png) `append`函数是在`string`末尾进行插入操作的简写形式。 ```c++ string s("C++ Primer"), s2 = s; // initialize s and s2 to "C++ Primer" s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed." s2.append(" 4th Ed."); // equivalent: appends " 4th Ed." to s2; s == s2 ``` `replace`函数是调用`erase`和`insert`函数的简写形式。 ```c++ // equivalent way to replace "4th" by "5th" s.erase(11, 3); // s == "C++ Primer Ed." s.insert(11, "5th"); // s == "C++ Primer 5th Ed." // starting at position 11, erase three characters and then insert "5th" s2.replace(11, 3, "5th"); // equivalent: s == s2 ``` ### string搜索操作(string Search Operations) `string`的每个搜索操作都返回一个`string::size_type`值,表示匹配位置的下标。如果搜索失败,则返回一个名为`string::npos`的`static`成员。标准库将`npos`定义为`const string::size_type`类型,并初始化为-1。 不建议用`int`或其他带符号类型来保存`string`搜索函数的返回值。 `string`搜索操作: ![9-16](Images/9-16.png) ### compare函数(The compare Functions) `string`类型提供了一组`compare`函数进行字符串比较操作,类似C标准库的`strcmp`函数。 `compare`函数的几种参数形式: ![9-17](Images/9-17.png) ### 数值转换(Numeric Conversions) C++11增加了`string`和数值之间的转换函数: ![9-18](Images/9-18.png) 进行数值转换时,`string`参数的第一个非空白字符必须是符号(`+`或`-`)或数字。它可以以`0x`或`0X`开头来表示十六进制数。对于转换目标是浮点值的函数,`string`参数也可以以小数点开头,并可以包含`e`或`E`来表示指数部分。 如果给定的`string`不能转换为一个数值,则转换函数会抛出`invalid_argument`异常。如果转换得到的数值无法用任何类型表示,则抛出`out_of_range`异常。 ## 容器适配器(Container Adaptors) 标准库定义了`stack`、`queue`和`priority_queue`三种容器适配器。容器适配器可以改变已有容器的工作机制。 所有容器适配器都支持的操作和类型: ![9-19](Images/9-19.png) 默认情况下,`stack`和`queue`是基于`deque`实现的,`priority_queue`是基于`vector`实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。 ```c++ // empty stack implemented on top of vector stack> str_stk; // str_stk2 is implemented on top of vector and initially holds a copy of svec stack> str_stk2(svec); ``` 所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在`array`上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用`forward_list`构造适配器。 栈适配器`stack`定义在头文件*stack*中,其支持的操作如下: ![9-20](Images/9-20.png) 队列适配器`queue`和`priority_queue`定义在头文件*queue*中,其支持的操作如下: ![9-21](Images/9-21.png) `queue`使用先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。 # 第10章 泛型算法 ## 概述(Overview) 大多数算法都定义在头文件*algorithm*中,此外标准库还在头文件*numeric*中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的元素范围进行操作。 `find`函数将范围中的每个元素与给定值进行比较,返回指向第一个等于给定值的元素的迭代器。如果无匹配元素,则返回其第二个参数来表示搜索失败。 ```c++ int val = 42; // value we'll look for // result will denote the element we want if it's in vec, or vec.cend() if not auto result = find(vec.cbegin(), vec.cend(), val); // report the result cout << "The value " << val << (result == vec.cend() ? " is not present" : " is present") << endl; ``` 迭代器参数令算法不依赖于特定容器,但依赖于元素类型操作。 泛型算法本身不会执行容器操作,它们只会运行于迭代器之上,执行迭代器操作。算法可能改变容器中元素的值,或者在容器内移动元素,但不会改变底层容器的大小(当算法操作插入迭代器时,迭代器可以向容器中添加元素,但算法自身不会进行这种操作)。 ## 初识泛型算法(A First Look at the Algorithms) ### 只读算法(Read-Only Algorithms) `accumulate`函数(定义在头文件*numeric*中)用于计算一个序列的和。它接受三个参数,前两个参数指定需要求和的元素范围,第三个参数是和的初值(决定加法运算类型和返回值类型)。 ```c++ // sum the elements in vec starting the summation with the value 0 int sum = accumulate(vec.cbegin(), vec.cend(), 0); string sum = accumulate(v.cbegin(), v.cend(), string("")); // error: no + on const char* string sum = accumulate(v.cbegin(), v.cend(), ""); ``` 建议在只读算法中使用`cbegin`和`cend`函数。 `equal`函数用于确定两个序列是否保存相同的值。它接受三个迭代器参数,前两个参数指定第一个序列范围,第三个参数指定第二个序列的首元素。`equal`函数假定第二个序列至少与第一个序列一样长。 ```c++ // roster2 should have at least as many elements as roster1 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin()); ``` 只接受单一迭代器表示第二个操作序列的算法都假定第二个序列至少与第一个序列一样长。 ### 写容器元素的算法(Algorithms That Write Container Elements) `fill`函数接受两个迭代器参数表示序列范围,还接受一个值作为第三个参数,它将给定值赋予范围内的每个元素。 ```c++ // reset each element to 0 fill(vec.begin(), vec.end(), 0); ``` `fill_n`函数接受单个迭代器参数、一个计数值和一个值,它将给定值赋予迭代器指向位置开始的指定个元素。 ```c++ // reset all the elements of vec to 0 fill_n(vec.begin(), vec.size(), 0); ``` 向目的位置迭代器写入数据的算法都假定目的位置足够大,能容纳要写入的元素。 插入迭代器(insert iterator)是一种向容器内添加元素的迭代器。通过插入迭代器赋值时,一个与赋值号右侧值相等的元素会被添加到容器中。 `back_inserter`函数(定义在头文件*iterator*中)接受一个指向容器的引用,返回与该容器绑定的插入迭代器。通过此迭代器赋值时,赋值运算符会调用`push_back`将一个具有给定值的元素添加到容器中。 ```c++ vector vec; // empty vector auto it = back_inserter(vec); // assigning through it adds elements to vec *it = 42; // vec now has one element with value 42 // ok: back_inserter creates an insert iterator that adds elements to vec fill_n(back_inserter(vec), 10, 0); // appends ten elements to vec ``` `copy`函数接受三个迭代器参数,前两个参数指定输入序列,第三个参数指定目的序列的起始位置。它将输入序列中的元素拷贝到目的序列中,返回目的位置迭代器(递增后)的值。 ```c++ int a1[] = { 0,1,2,3,4,5,6,7,8,9 }; int a2[sizeof(a1) / sizeof(*a1)]; // a2 has the same size as a1 // ret points just past the last element copied into a2 auto ret = copy(begin(a1), end(a1), a2); // copy a1 into a2 ``` `replace`函数接受四个参数,前两个迭代器参数指定输入序列,后两个参数指定要搜索的值和替换值。它将序列中所有等于第一个值的元素都替换为第二个值。 ```c++ // replace any element with the value 0 with 42 replace(ilst.begin(), ilst.end(), 0, 42); ``` 相对于`replace`,`replace_copy`函数可以保留原序列不变。它接受第三个迭代器参数,指定调整后序列的保存位置。 ```c++ // use back_inserter to grow destination as needed replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42); ``` 很多算法都提供“copy”版本,这些版本不会将新元素放回输入序列,而是创建一个新序列保存结果。 ### 重排容器元素的算法(Algorithms That Reorder Container Elements) `sort`函数接受两个迭代器参数,指定排序范围。它利用元素类型的`<`运算符重新排列元素。 ```c++ void elimDups(vector &words) { // sort words alphabetically so we can find the duplicates sort(words.begin(), words.end()); // unique reorders the input range so that each word appears once in the // front portion of the range and returns an iterator one past the unique range auto end_unique = unique(words.begin(), words.end()); // erase uses a vector operation to remove the nonunique elements words.erase(end_unique, words.end()); } ``` `unique`函数重排输入序列,消除相邻的重复项,返回指向不重复值范围末尾的迭代器。 ![10-1](Images/10-1.png) ## 定制操作(Customizing Operations) 默认情况下,很多比较算法使用元素类型的`<`或`==`运算符完成操作。可以为这些算法提供自定义操作来代替默认运算符。 ### 向算法传递函数(Passing a Function to an Algorithm) 谓词(predicate)是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为一元谓词(unary predicate,接受一个参数)和二元谓词(binary predicate,接受两个参数)。接受谓词参数的算法会对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。 ```c++ // comparison function to be used to sort by word length bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size(); } // sort on word length, shortest to longest sort(words.begin(), words.end(), isShorter); ``` 稳定排序函数`stable_sort`可以维持输入序列中相等元素的原有顺序。 ### lambda表达式(Lambda Expressions) `find_if`函数接受两个迭代器参数和一个谓词参数。迭代器参数用于指定序列范围,之后对序列中的每个元素调用给定谓词,并返回第一个使谓词返回非0值的元素。如果不存在,则返回尾迭代器。 对于一个对象或表达式,如果可以对其使用调用运算符`()`,则称它为可调用对象(callable object)。可以向算法传递任何类别的可调用对象。 一个`lambda`表达式表示一个可调用的代码单元,类似未命名的内联函数,但可以定义在函数内部。其形式如下: ```c++ [capture list] (parameter list) -> return type { function body } ``` 其中,*capture list*(捕获列表)是一个由`lambda`所在函数定义的局部变量的列表(通常为空)。*return type*、*parameter list*和*function body*与普通函数一样,分别表示返回类型、参数列表和函数体。但与普通函数不同,`lambda`必须使用尾置返回类型,且不能有默认实参。 定义`lambda`时可以省略参数列表和返回类型,但必须包含捕获列表和函数体。省略参数列表等价于指定空参数列表。省略返回类型时,若函数体只是一个`return`语句,则返回类型由返回表达式的类型推断而来。否则返回类型为`void`。 ```c++ auto f = [] { return 42; }; cout << f() << endl; // prints 42 ``` `lambda`可以使用其所在函数的局部变量,但必须先将其包含在捕获列表中。捕获列表只能用于局部非`static`变量,`lambda`可以直接使用局部`static`变量和其所在函数之外声明的名字。 ```c++ // get an iterator to the first element whose size() is >= sz auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; }); ``` `for_each`函数接受一个输入序列和一个可调用对象,它对输入序列中的每个元素调用此对象。 ```c++ // print words of the given size or longer, each one followed by a space for_each(wc, words.end(), [] (const string &s) { cout << s << " "; }); ``` ### lambda捕获和返回(Lambda Captures and Returns) 被`lambda`捕获的变量的值是在`lambda`创建时拷贝,而不是调用时拷贝。在`lambda`创建后修改局部变量不会影响`lambda`内对应的值。 ```c++ size_t v1 = 42; // local variable // copies v1 into the callable object named f auto f = [v1] { return v1; }; v1 = 0; auto j = f(); // j is 42; f stored a copy of v1 when we created it ``` `lambda`可以以引用方式捕获变量,但必须保证`lambda`执行时变量存在。 ```c++ size_t v1 = 42; // local variable // the object f2 contains a reference to v1 auto f2 = [&v1] { return v1; }; v1 = 0; auto j = f2(); // j is 0; f2 refers to v1; it doesn't store it ``` 可以让编译器根据`lambda`代码隐式捕获函数变量,方法是在捕获列表中写一个`&`或`=`符号。`&`为引用捕获,`=`为值捕获。 可以混合使用显式捕获和隐式捕获。混合使用时,捕获列表中的第一个元素必须是`&`或`=`符号,用于指定默认捕获方式。显式捕获的变量必须使用与隐式捕获不同的方式。 ```c++ // os implicitly captured by reference; c explicitly captured by value for_each(words.begin(), words.end(), [&, c] (const string &s) { os << s << c; }); // os explicitly captured by reference; c implicitly captured by value for_each(words.begin(), words.end(), [=, &os] (const string &s) { os << s << c; }); ``` `lambda`捕获列表形式: ![10-2](Images/10-2.png) 默认情况下,对于值方式捕获的变量,`lambda`不能修改其值。如果希望修改,就必须在参数列表后添加关键字`mutable`。 ```c++ size_t v1 = 42; // local variable // f can change the value of the variables it captures auto f = [v1] () mutable { return ++v1; }; v1 = 0; auto j = f(); // j is 43 ``` 对于引用方式捕获的变量,`lambda`是否可以修改依赖于此引用指向的是否是`const`类型。 `transform`函数接受三个迭代器参数和一个可调用对象。前两个迭代器参数指定输入序列,第三个迭代器参数表示目的位置。它对输入序列中的每个元素调用可调用对象,并将结果写入目的位置。 ```c++ transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; }); ``` 为`lambda`定义返回类型时,必须使用尾置返回类型。 ### 参数绑定(Binding Arguments) `bind`函数定义在头文件*functional*中,相当于一个函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表。一般形式如下: ```c++ auto newCallable = bind(callable, arg_list); ``` 其中,*newCallable*本身是一个可调用对象,*arg_list*是一个以逗号分隔的参数列表,对应给定的*callable*的参数。之后调用*newCallable*时,*newCallable*会再调用*callable*,并传递给它*arg_list*中的参数。*arg_list*中可能包含形如`_n`的名字,其中*n*是一个整数。这些参数是占位符,表示*newCallable*的参数,它们占据了传递给*newCallable*的参数的位置。数值*n*表示生成的可调用对象中参数的位置:`_1`为*newCallable*的第一个参数,`_2`为*newCallable*的第二个参数,依次类推。这些名字都定义在命名空间*placeholders*中,它又定义在命名空间*std*中,因此使用时应该进行双重限定。 ```c++ using std::placeholders::_1; using namespace std::placeholders; bool check_size(const string &s, string::size_type sz); // check6 is a callable object that takes one argument of type string // and calls check_size on its given string and the value 6 auto check6 = bind(check_size, _1, 6); string s = "hello"; bool b1 = check6(s); // check6(s) calls check_size(s, 6) ``` `bind`函数可以调整给定可调用对象中的参数顺序。 ```c++ // sort on word length, shortest to longest sort(words.begin(), words.end(), isShorter); // sort on word length, longest to shortest sort(words.begin(), words.end(), bind(isShorter, _2, _1)); ``` 默认情况下,`bind`函数的非占位符参数被拷贝到`bind`返回的可调用对象中。但有些类型不支持拷贝操作。 如果希望传递给`bind`一个对象而又不拷贝它,则必须使用标准库的`ref`函数。`ref`函数返回一个对象,包含给定的引用,此对象是可以拷贝的。`cref`函数生成保存`const`引用的类。 ```c++ ostream &print(ostream &os, const string &s, char c); for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' ')); ``` ## 再探迭代器(Revisiting Iterators) 除了为每种容器定义的迭代器之外,标准库还在头文件*iterator*中定义了另外几种迭代器。 - 插入迭代器(insert iterator):该类型迭代器被绑定到容器对象上,可用来向容器中插入元素。 - 流迭代器(stream iterator):该类型迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。 - 反向迭代器(reverse iterator):该类型迭代器向后而不是向前移动。除了`forward_list`之外的标准库容器都有反向迭代器。 - 移动迭代器(move iterator):该类型迭代器用来移动容器元素。 ### 插入迭代器(Insert Iterators) 插入器是一种迭代器适配器,它接受一个容器参数,生成一个插入迭代器。通过插入迭代器赋值时,该迭代器调用容器操作向给定容器的指定位置插入一个元素。 插入迭代器操作: ![10-3](Images/10-3.png) 插入器有三种类型,区别在于元素插入的位置: - `back_inserter`:创建一个调用`push_back`操作的迭代器。 - `front_inserter`:创建一个调用`push_front`操作的迭代器。 - `inserter`:创建一个调用`insert`操作的迭代器。此函数接受第二个参数,该参数必须是一个指向给定容器的迭代器,元素会被插入到该参数指向的元素之前。 ```c++ list 1st = { 1,2,3,4 }; list lst2, lst3; // empty lists // after copy completes, 1st2 contains 4 3 2 1 copy(1st.cbegin(), lst.cend(), front_inserter(lst2)); // after copy completes, 1st3 contains 1 2 3 4 copy(1st.cbegin(), lst.cend(), inserter(lst3, lst3.begin())); ``` ### iostream迭代器(iostream Iterators) `istream_iterator`从输入流读取数据,`ostream_iterator`向输出流写入数据。这些迭代器将流当作特定类型的元素序列处理。 创建流迭代器时,必须指定迭代器读写的对象类型。`istream_iterator`使用`>>`来读取流,因此`istream_iterator`要读取的类型必须定义了`>>`运算符。创建`istream_iterator`时,可以将其绑定到一个流。如果默认初始化,则创建的是尾后迭代器。 ```c++ istream_iterator int_it(cin); // reads ints from cin istream_iterator int_eof; // end iterator value ifstream in("afile"); istream_iterator str_it(in); // reads strings from "afile" ``` 对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或IO错误,迭代器的值就与尾后迭代器相等。 ```c++ istream_iterator in_iter(cin); // read ints from cin istream_iterator eof; // istream ''end'' iterator while (in_iter != eof) // while there's valid input to read // postfix increment reads the stream and returns the old value of the iterator // we dereference that iterator to get the previous value read from the stream vec.push_back(*in_iter++); ``` 可以直接使用流迭代器构造容器。 ```c++ istream_iterator in_iter(cin), eof; // read ints from cin vector vec(in_iter, eof); // construct vec from an iterator range ``` `istream_iterator`操作: ![10-4](Images/10-4.png) 将`istream_iterator`绑定到一个流时,标准库并不保证迭代器立即从流读取数据。但可以保证在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。 定义`ostream_iterator`对象时,必须将其绑定到一个指定的流。不允许定义空的或者表示尾后位置的`ostream_iterator`。 `ostream_iterator`操作: ![10-5](Images/10-5.png) `*`和`++`运算符实际上不会对`ostream_iterator`对象做任何操作。但是建议代码写法与其他迭代器保持一致。 ```c++ ostream_iterator out_iter(cout, " "); for (auto e : vec) *out_iter++ = e; // the assignment writes this element to cout cout << endl; ``` 可以为任何定义了`<<`运算符的类型创建`istream_iterator`对象,为定义了`>>`运算符的类型创建`ostream_iterator`对象。 ### 反向迭代器(Reverse Iterators) 递增反向迭代器会移动到前一个元素,递减会移动到后一个元素。 ```c++ sort(vec.begin(), vec.end()); // sorts vec in "normal" order // sorts in reverse: puts the smallest element at the end of vec sort(vec.rbegin(), vec.rend()); ``` ![10-6](Images/10-6.png) 不能从`forward_list`或流迭代器创建反向迭代器。 调用反向迭代器的`base`函数可以获得其对应的普通迭代器。 ```c++ // find the last element in a comma-separated list auto rcomma = find(line.crbegin(), line.crend(), ','); // WRONG: will generate the word in reverse order cout << string(line.crbegin(), rcomma) << endl; // ok: get a forward iterator and read to the end of line cout << string(rcomma.base(), line.cend()) << endl; ``` ![10-7](Images/10-7.png) 反向迭代器的目的是表示元素范围,而这些范围是不对称的。用普通迭代器初始化反向迭代器,或者给反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同元素。 ## 泛型算法结构(Structure of Generic Algorithms) 算法要求的迭代器操作可以分为5个迭代器类别(iterator category): ![10-8](Images/10-8.png) ### 5类迭代器(The Five Iterator Categories) C++标准指定了泛型和数值算法的每个迭代器参数的最小类别。对于迭代器实参来说,其能力必须大于或等于规定的最小类别。向算法传递更低级的迭代器参数会产生错误(大部分编译器不会提示错误)。 迭代器类别: - 输入迭代器(input iterator):可以读取序列中的元素,只能用于单遍扫描算法。必须支持以下操作: - 用于比较两个迭代器相等性的相等`==`和不等运算符`!=`。 - 用于推进迭代器位置的前置和后置递增运算符`++`。 - 用于读取元素的解引用运算符`*`;解引用只能出现在赋值运算符右侧。 - 用于读取元素的箭头运算符`->`。 - 输出迭代器(output iterator):可以读写序列中的元素,只能用于单遍扫描算法,通常指向目的位置。必须支持以下操作: - 用于推进迭代器位置的前置和后置递增运算符`++`。 - 用于读取元素的解引用运算符`*`;解引用只能出现在赋值运算符左侧(向已经解引用的输出迭代器赋值,等价于将值写入其指向的元素)。 - 前向迭代器(forward iterator):可以读写序列中的元素。只能在序列中沿一个方向移动。支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此可以使用前向迭代器对序列进行多遍扫描。 - 双向迭代器(bidirectional iterator):可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,还支持前置和后置递减运算符`--`。除`forward_list`之外的其他标准库容器都提供符合双向迭代器要求的迭代器。 - 随机访问迭代器s(random-access iterator):可以在常量时间内访问序列中的任何元素。除了支持所有双向迭代器的操作之外,还必须支持以下操作: - 用于比较两个迭代器相对位置的关系运算符`<`、`<=`、`>`、`>=`。 - 迭代器和一个整数值的加减法运算`+`、`+=`、`-`、`-=`,计算结果是迭代器在序列中前进或后退给定整数个元素后的位置。 - 用于两个迭代器上的减法运算符`-`,计算得到两个迭代器的距离。 - 下标运算符`[]`。 ### 算法形参模式(Algorithm Parameter Patterns) 大多数算法的形参模式是以下四种形式之一: ```c++ alg(beg, end, other args); alg(beg, end, dest, other args); alg(beg, end, beg2, other args); alg(beg, end, beg2, end2, other args); ``` 其中*alg*是算法名称,*beg*和*end*表示算法所操作的输入范围。几乎所有算法都接受一个输入范围,是否有其他参数依赖于算法操作。*dest*表示输出范围,*beg2*和*end2*表示第二个输入范围。 向输出迭代器写入数据的算法都假定目标空间足够容纳要写入的数据。 接受单独一个迭代器参数表示第二个输入范围的算法都假定从迭代器参数开始的序列至少与第一个输入范围一样大。 ### 算法命名规范(Algorithm Naming Conventions) 接受谓词参数的算法都有附加的`_if`后缀。 ```c++ find(beg, end, val); // find the first instance of val in the input range find_if(beg, end, pred); // find the first instance for which pred is true ``` 将执行结果写入额外目的空间的算法都有`_copy`后缀。 ```c++ reverse(beg, end); // reverse the elements in the input range reverse_copy(beg, end, dest); // copy elements in reverse order into dest ``` 一些算法同时提供`_copy`和`_if`版本。 ## 特定容器算法(Container-Specific Algorithms) 对于`list`和`forward_list`类型,应该优先使用成员函数版本的算法,而非通用算法。 `list`和`forward_list`成员函数版本的算法: ![10-9](Images/10-9.png) `list`和`forward_list`的`splice`函数可以进行容器合并,其参数如下: ![10-10](Images/10-10.png) 链表特有版本的算法操作会改变底层容器。 # 第11章 关联容器 关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是`map`和`set`。 - `map`中的元素是一些键值对(key-value):关键字起索引作用,值表示与索引相关联的数据。 - `set`中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在`set`中。 标准库提供了8个关联容器,它们之间的不同体现在三个方面: - 是`map`还是`set`类型。 - 是否允许保存重复的关键字。 - 是否按顺序保存元素。 允许重复保存关键字的容器名字都包含单词`multi`;无序保存元素的容器名字都以单词`unordered`开头。 ![11-1](Images/11-1.png) `map`和`multimap`类型定义在头文件*map*中;`set`和`multiset`类型定义在头文件*set*中;无序容器定义在头文件*unordered_map*和*unordered_set*中。 ## 使用关联容器(Using an Associative Container) `map`类型通常被称为关联数组(associative array)。 从`map`中提取一个元素时,会得到一个`pair`类型的对象。`pair`是一个模板类型,保存两个名为`first`和`second`的公有数据成员。`map`所使用的`pair`用`first`成员保存关键字,用`second`成员保存对应的值。 ```c++ // count the number of times each word occurs in the input map word_count; // empty map from string to size_t string word; while (cin >> word) ++word_count[word]; // fetch and increment the counter for word for (const auto &w : word_count) // for each element in the map // print the results cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << endl; ``` `set`类型的`find`成员返回一个迭代器。如果给定关键字在`set`中,则迭代器指向该关键字,否则返回的是尾后迭代器。 ## 关联容器概述(Overview of the Associative Containers) ### 定义关联容器(Defining an Associative Container) 定义`map`时,必须指定关键字类型和值类型;定义`set`时,只需指定关键字类型。 初始化`map`时,提供的每个键值对用花括号`{}`包围。 ```C++ map word_count; // empty // list initialization set exclude = { "the", "but", "and" }; // three elements; authors maps last name to first map authors = { {"Joyce", "James"}, {"Austen", "Jane"}, {"Dickens", "Charles"} }; ``` `map`和`set`中的关键字必须唯一,`multimap`和`multiset`没有此限制。 ### 关键字类型的要求(Requirements on Key Type) 对于有序容器——`map`、`multimap`、`set`和`multiset`,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的`<`运算符来进行比较操作。 用来组织容器元素的操作的类型也是该容器类型的一部分。如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型。操作类型在尖括号中紧跟着元素类型给出。 ```c++ bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() < rhs.isbn(); } // bookstore can have several transactions with the same ISBN // elements in bookstore will be in ISBN order multiset bookstore(compareIsbn); ``` ### pair类型(The pair Type) `pair`定义在头文件*utility*中。一个`pair`可以保存两个数据成员,分别命名为`first`和`second`。 ```c++ pair anon; // holds two strings pair word_count; // holds a string and an size_t pair> line; // holds string and vector ``` `pair`的默认构造函数对数据成员进行值初始化。 `pair`支持的操作: ![11-2](Images/11-2.png) 在C++11中,如果函数需要返回`pair`,可以对返回值进行列表初始化。早期C++版本中必须显式构造返回值。 ```c++ pair process(vector &v) { // process v if (!v.empty()) // list initialize return { v.back(), v.back().size() }; else // explicitly constructed return value return pair(); } ``` ## 关联容器操作(Operations on Associative Containers) 关联容器定义了类型别名来表示容器关键字和值的类型: ![11-3](Images/11-3.png) 对于`set`类型,`key_type`和`value_type`是一样的。`set`中保存的值就是关键字。对于`map`类型,元素是关键字-值对。即每个元素是一个`pair`对象,包含一个关键字和一个关联的值。由于元素关键字不能改变,因此`pair`的关键字部分是`const`的。另外,只有`map`类型(`unordered_map`、`unordered_multimap`、`multimap`、`map`)才定义了`mapped_type`。 ```c++ set::value_type v1; // v1 is a string set::key_type v2; // v2 is a string map::value_type v3; // v3 is a pair map::key_type v4; // v4 is a string map::mapped_type v5; // v5 is an int ``` ### 关联容器迭代器(Associative Container Iterators) 解引用关联容器迭代器时,会得到一个类型为容器的`value_type`的引用。对`map`而言,`value_type`是`pair`类型,其`first`成员保存`const`的关键字,`second`成员保存值。 ```c++ // get an iterator to an element in word_count auto map_it = word_count.begin(); // *map_it is a reference to a pair object cout << map_it->first; // prints the key for this element cout << " " << map_it->second; // prints the value of the element map_it->first = "new key"; // error: key is const ++map_it->second; // ok: we can change the value through an iterator ``` 虽然`set`同时定义了`iterator`和`const_iterator`类型,但两种迭代器都只允许只读访问`set`中的元素。类似`map`,`set`中的关键字也是`const`的。 ```c++ set iset = {0,1,2,3,4,5,6,7,8,9}; set::iterator set_it = iset.begin(); if (set_it != iset.end()) { *set_it = 42; // error: keys in a set are read-only cout << *set_it << endl; // ok: can read the key } ``` `map`和`set`都支持`begin`和`end`操作。使用迭代器遍历`map`、`multimap`、`set`或`multiset`时,迭代器按关键字升序遍历元素。 通常不对关联容器使用泛型算法。 ### 添加元素(Adding Elements) 使用`insert`成员可以向关联容器中添加元素。向`map`和`set`中添加已存在的元素对容器没有影响。 通常情况下,对于想要添加到`map`中的数据,并没有现成的`pair`对象。可以直接在`insert`的参数列表中创建`pair`。 ```c++ // four ways to add word to word_count word_count.insert({word, 1}); word_count.insert(make_pair(word, 1)); word_count.insert(pair(word, 1)); word_count.insert(map::value_type(word, 1)); ``` 关联容器的`insert`操作: ![11-4](Images/11-4.png) `insert`或`emplace`的返回值依赖于容器类型和参数: - 对于不包含重复关键字的容器,添加单一元素的`insert`和`emplace`版本返回一个`pair`,表示操作是否成功。`pair`的`first`成员是一个迭代器,指向具有给定关键字的元素;`second`成员是一个`bool`值。如果关键字已在容器中,则`insert`直接返回,`bool`值为`false`。如果关键字不存在,元素会被添加至容器中,`bool`值为`true`。 - 对于允许包含重复关键字的容器,添加单一元素的`insert`和`emplace`版本返回指向新元素的迭代器。 ### 删除元素(Erasing Elements) 关联容器的删除操作: ![11-5](Images/11-5.png) 与顺序容器不同,关联容器提供了一个额外的`erase`操作。它接受一个`key_type`参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量。对于不包含重复关键字的容器,`erase`的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。 ### map的下标操作(Subscripting a map) `map`下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。 由于下标运算符可能向容器中添加元素,所以只能对非`const`的`map`使用下标操作。 `map`和`unordered_map`的下标操作: ![11-6](Images/11-6.png) 对`map`进行下标操作时,返回的是`mapped_type`类型的对象;解引用`map`迭代器时,返回的是`value_type`类型的对象。 ### 访问元素(Accessing Elements) 关联容器的查找操作: ![11-7](Images/11-7.png) 如果`multimap`或`multiset`中有多个元素具有相同关键字,则这些元素在容器中会相邻存储。 ```c++ multimap authors; // adds the first element with the key Barth, John authors.insert({"Barth, John", "Sot-Weed Factor"}); // ok: adds the second element with the key Barth, John authors.insert({"Barth, John", "Lost in the Funhouse"}); string search_item("Alain de Botton"); // author we'll look for auto entries = authors.count(search_item); // number of elements auto iter = authors.find(search_item); // first entry for this author // loop through the number of entries there are for this author while(entries) { cout << iter->second << endl; // print each title ++iter; // advance to the next title --entries; // keep track of how many we've printed } ``` `lower_bound`和`upper_bound`操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,`lower_bound`返回的迭代器会指向第一个匹配给定关键字的元素,而`upper_bound`返回的迭代器则指向最后一个匹配元素之后的位置。如果关键字不在`multimap`中,则`lower_bound`和`upper_bound`会返回相等的迭代器,指向一个不影响排序的关键字插入位置。因此用相同的关键字调用`lower_bound`和`upper_bound`会得到一个迭代器范围,表示所有具有该关键字的元素范围。 ```c++ // definitions of authors and search_item as above // beg and end denote the range of elements for this author for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg) cout << beg->second << endl; // print each title ``` `lower_bound`和`upper_bound`有可能返回尾后迭代器。如果查找的元素具有容器中最大的关键字,则`upper_bound`返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则`lower_bound`也返回尾后迭代器。 `equal_range`操作接受一个关键字,返回一个迭代器`pair`。若关键字存在,则第一个迭代器指向第一个匹配关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置。若关键字不存在,则两个迭代器都指向一个不影响排序的关键字插入位置。 ```c++ // definitions of authors and search_item as above // pos holds iterators that denote the range of elements for this key for (auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first) cout << pos.first->second << endl; // print each title ``` ## 无序容器(The Unordered Containers) 新标准库定义了4个无序关联容器(unordered associative container),这些容器使用哈希函数(hash function)和关键字类型的`==`运算符组织元素。 无序容器和对应的有序容器通常可以相互替换。但是由于元素未按顺序存储,使用无序容器的程序输出一般会与有序容器的版本不同。 无序容器在存储上组织为一组桶,每个桶保存零或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。 无序容器管理操作: ![11-8](Images/11-8.png) 默认情况下,无序容器使用关键字类型的`==`运算符比较元素,还使用一个`hash`类型的对象来生成每个元素的哈希值。标准库为内置类型和一些标准库类型提供了hash模板。因此可以直接定义关键字是这些类型的无序容器,而不能直接定义关键字类型为自定义类类型的无序容器,必须先提供对应的hash模板版本。 # 第12章 动态内存 程序用堆(heap)来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序控制。 ## 动态内存与智能指针(Dynamic Memory and Smart Pointers) C++中的动态内存管理通过一对运算符完成:`new`在动态内存中为对象分配空间并返回指向该对象的指针,可以选择对对象进行初始化;`delete`接受一个动态对象的指针,销毁该对象并释放与之关联的内存。 新标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,但它自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:`shared_ptr`允许多个指针指向同一个对象;`unique_ptr`独占所指向的对象。标准库还定义了一个名为`weak_ptr`的伴随类,它是一种弱引用指向`shared_ptr`所管理的对象。这三种类型都定义在头文件*memory*中。 ### shared_ptr类(The shared_ptr Class) 智能指针是模板,创建时需要指明指针可以指向的类型默认初始化的智能指针中保存着一个空指针。 ```c++ shared_ptr p1; // shared_ptr that can point at a string shared_ptr> p2; // shared_ptr that can point at a list of ints ``` `shared_ptr`和`unique_ptr`都支持的操作: ![12-1](Images/12-1.png) `shared_ptr`独有的操作: ![12-2](Images/12-2.png) `make_shared`函数(定义在头文件*memory*中)在动态内存中分配一个对象并初始化它,返回指向此对象的`shared_ptr`。 ```c++ // shared_ptr that points to an int with value 42 shared_ptr p3 = make_shared(42); // p4 points to a string with value 9999999999 shared_ptr p4 = make_shared(10, '9'); // p5 points to an int that is value initialized shared_ptr p5 = make_shared(); ``` 进行拷贝或赋值操作时,每个`shared_ptr`会记录有多少个其他`shared_ptr`与其指向相同的对象。 ```c++ auto p = make_shared(42); // object to which p points has one user auto q(p); // p and q point to the same object // object to which p and q point has two users ``` 每个`shared_ptr`都有一个与之关联的计数器,通常称为引用计数(reference count)。拷贝`shared_ptr`时引用计数会递增。例如使用一个`shared_ptr`初始化另一个`shared_ptr`,或将它作为参数传递给函数以及作为函数的返回值返回。给`shared_ptr`赋予新值或`shared_ptr`被销毁时引用计数会递减。例如一个局部`shared_ptr`离开其作用域。一旦一个`shared_ptr`的引用计数变为0,它就会自动释放其所管理的对象。 ```c++ auto r = make_shared(42); // int to which r points has one user r = q; // assign to r, making it point to a different address // increase the use count for the object to which q points // reduce the use count of the object to which r had pointed // the object r had pointed to has no users; that object is automatically freed ``` `shared_ptr`的析构函数会递减它所指向对象的引用计数。如果引用计数变为0,`shared_ptr`的析构函数会销毁对象并释放空间。 如果将`shared_ptr`存放于容器中,而后不再需要全部元素,而只使用其中一部分,应该用`erase`删除不再需要的元素。 程序使用动态内存通常出于以下三种原因之一: - 不确定需要使用多少对象。 - 不确定所需对象的准确类型。 - 需要在多个对象间共享数据。 ### 直接管理内存(Managing Memory Directly) 相对于智能指针,使用`new`和`delete`管理内存很容易出错。 默认情况下,动态分配的对象是默认初始化的。所以内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。 ```c++ string *ps = new string; // initialized to empty string int *pi = new int; // pi points to an uninitialized int ``` 可以使用值初始化方式、直接初始化方式、传统构造方式(圆括号`()`)或新标准下的列表初始化方式(花括号`{}`)初始化动态分配的对象。 ```c++ int *pi = new int(1024); // object to which pi points has value 1024 string *ps = new string(10, '9'); // *ps is "9999999999" // vector with ten elements with values from 0 to 9 vector *pv = new vector{0,1,2,3,4,5,6,7,8,9}; string *ps1 = new string; // default initialized to the empty string string *ps = new string(); // value initialized to the empty string int *pi1 = new int; // default initialized; *pi1 is undefined int *pi2 = new int(); // value initialized to 0; *pi2 is 0 ``` 只有当初始化的括号中仅有单一初始化器时才可以使用`auto`。 ```c++ auto p1 = new auto(obj); // p points to an object of the type of obj // that object is initialized from obj auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer ``` 可以用`new`分配`const`对象,返回指向`const`类型的指针。动态分配的`const`对象必须初始化。 默认情况下,如果`new`不能分配所要求的内存空间,会抛出`bad_alloc`异常。使用定位`new`(placement new)可以阻止其抛出异常。定位`new`表达式允许程序向`new`传递额外参数。如果将`nothrow`传递给`new`,则`new`在分配失败后会返回空指针。`bad_alloc`和`nothrow`都定义在头文件*new*中。 ```c++ // if allocation fails, new returns a null pointer int *p1 = new int; // if allocation fails, new throws std::bad_alloc int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer ``` 使用`delete`释放一块并非`new`分配的内存,或者将相同的指针值释放多次的行为是未定义的。 由内置指针管理的动态对象在被显式释放前一直存在。 `delete`一个指针后,指针值就无效了(空悬指针,dangling pointer)。为了防止后续的错误访问,应该在`delete`之后将指针值置空。 ### shared_ptr和new结合使用(Using shared_ptrs with new) 可以用`new`返回的指针初始化智能指针。该构造函数是`explicit`的,因此必须使用直接初始化形式。 ```c++ shared_ptr p1 = new int(1024); // error: must use direct initialization shared_ptr p2(new int(1024)); // ok: uses direct initialization ``` 默认情况下,用来初始化智能指针的内置指针必须指向动态内存,因为智能指针默认使用`delete`释放它所管理的对象。如果要将智能指针绑定到一个指向其他类型资源的指针上,就必须提供自定义操作来代替`delete`。 ![12-3](Images/12-3.png) 不要混合使用内置指针和智能指针。当将`shared_ptr`绑定到内置指针后,资源管理就应该交由`shared_ptr`负责。不应该再使用内置指针访问`shared_ptr`指向的内存。 ```c++ // ptr is created and initialized when process is called void process(shared_ptr ptr) { // use ptr } // ptr goes out of scope and is destroyed int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer process(x); // error: cannot convert int* to shared_ptr process(shared_ptr(x)); // legal, but the memory will be deleted! int j = *x; // undefined: x is a dangling pointer! shared_ptr p(new int(42)); // reference count is 1 process(p); // copying p increments its count; in process the reference count is 2 int i = *p; // ok: reference count is 1 ``` 智能指针的`get`函数返回一个内置指针,指向智能指针管理的对象。主要用于向不能使用智能指针的代码传递内置指针。使用`get`返回指针的代码不能`delete`此指针不要使用`get`初始化另一个智能指针或为智能指针赋值。 ```c++ shared_ptr p(new int(42)); // reference count is 1 int *q = p.get(); // ok: but don't use q in any way that might delete its pointer { // new block // undefined: two independent shared_ptrs point to the same memory shared_ptr(q); } // block ends, q is destroyed, and the memory to which q points is freed int foo = *p; // undefined; the memory to which p points was freed ``` 可以用`reset`函数将新的指针赋予`shared_ptr`。与赋值类似,`reset`会更新引用计数,如果需要的话,还会释放内存空间。`reset`经常与`unique`一起使用,来控制多个`shared_ptr`共享的对象。 ```c++ if (!p.unique()) p.reset(new string(*p)); // we aren't alone; allocate a new copy *p += newVal; // now that we know we're the only pointer, okay to change this object ``` ### 智能指针和异常(Smart Pointers and Exceptions) 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。 ```c++ void f() { int *ip = new int(42); // dynamically allocate a new object // code that throws an exception that is not caught inside f delete ip; // free the memory before exiting } void f() { shared_ptr sp(new int(42)); // allocate a new object // code that throws an exception that is not caught inside f } // shared_ptr freed automatically when the function ends ``` 默认情况下`shared_ptr`假定其指向动态内存,使用`delete`释放对象。创建`shared_ptr`时可以传递一个(可选)指向删除函数的指针参数,用来代替`delete`。 ```c++ struct destination; // represents what we are connecting to struct connection; // information needed to use the connection connection connect(destination*); // open the connection void disconnect(connection); // close the given connection void end_connection(connection *p) { disconnect(*p); } void f(destination &d /* other parameters */) { connection c = connect(&d); shared_ptr p(&c, end_connection); // use the connection // when f exits, even if by an exception, the connection will be properly closed } ``` 智能指针规范: - 不使用相同的内置指针值初始化或`reset`多个智能指针。 - 不释放`get`返回的指针。 - 不使用`get`初始化或`reset`另一个智能指针。 - 使用`get`返回的指针时,如果最后一个对应的智能指针被销毁,指针就无效了。 - 使用`shared_ptr`管理并非`new`分配的资源时,应该传递删除函数。 ### unique_ptr(unique_ptr) 与`shared_ptr`不同,同一时刻只能有一个`unique_ptr`指向给定的对象。当`unique_ptr`被销毁时,它指向的对象也会被销毁。 `make_unique`函数(C++14新增,定义在头文件*memory*中)在动态内存中分配一个对象并初始化它,返回指向此对象的`unique_ptr`。 ```c++ unique_ptr p1(new int(42)); // C++14 unique_ptr p2 = make_unique(42); ``` 由于`unique_ptr`独占其指向的对象,因此`unique_ptr`不支持普通的拷贝赋值操作。 `unique_ptr`操作: ![12-4](Images/12-4.png) `release`函数返回`unique_ptr`当前保存的指针并将其置为空。 `reset`函数成员接受一个可选的指针参数,重新设置`unique_ptr`保存的指针。如果`unique_ptr`不为空,则它原来指向的对象会被释放。 ```c++ // transfers ownership from p1 (which points to the string Stegosaurus) to p2 unique_ptr p2(p1.release()); // release makes p1 null unique_ptr p3(new string("Trex")); // transfers ownership from p3 to p2 p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed ``` 调用`release`会切断`unique_ptr`和它原来管理的对象之间的联系。`release`返回的指针通常被用来初始化另一个智能指针或给智能指针赋值。如果没有用另一个智能指针保存`release`返回的指针,程序就要负责资源的释放。 ```c++ p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer auto p = p2.release(); // ok, but we must remember to delete(p) ``` 不能拷贝`unique_ptr`的规则有一个例外:可以拷贝或赋值一个即将被销毁的`unique_ptr`(移动构造、移动赋值)。 ```c++ unique_ptr clone(int p) { unique_ptr ret(new int (p)); // . . . return ret; } ``` 老版本的标准库包含了一个名为`auto_ptr`的类, 类似`shared_ptr`,默认情况下`unique_ptr`用`delete`释放其指向的对象。`unique_ptr`的删除器同样可以重载,但`unique_ptr`管理删除器的方式与`shared_ptr`不同。定义`unique_ptr`时必须在尖括号中提供删除器类型。创建或`reset`这种`unique_ptr`类型的对象时,必须提供一个指定类型的可调用对象(删除器)。 ```c++ // p points to an object of type objT and uses an object of type delT to free that object // it will call an object named fcn of type delT unique_ptr p (new objT, fcn); void f(destination &d /* other needed parameters */) { connection c = connect(&d); // open the connection // when p is destroyed, the connection will be closed unique_ptr p(&c, end_connection); // use the connection // when f exits, even if by an exception, the connection will be properly closed } ``` ### weak_ptr(weak_ptr) `weak_ptr`是一种不控制所指向对象生存期的智能指针,它指向一个由`shared_ptr`管理的对象。将`weak_ptr`绑定到`shared_ptr`不会改变`shared_ptr`的引用计数。如果`shared_ptr`被销毁,即使有`weak_ptr`指向对象,对象仍然有可能被释放。 ![12-5](Images/12-5.png) 创建一个`weak_ptr`时,需要使用`shared_ptr`来初始化它。 ```c++ auto p = make_shared(42); weak_ptr wp(p); // wp weakly shares with p; use count in p is unchanged ``` 使用`weak_ptr`访问对象时,必须先调用`lock`函数。该函数检查`weak_ptr`指向的对象是否仍然存在。如果存在,则返回指向共享对象的`shared_ptr`,否则返回空指针。 ```c++ if (shared_ptr np = wp.lock()) { // true if np is not null // inside the if, np shares its object with p } ``` ## 动态数组(Dynamic Arrays) 使用`allocator`类可以将内存分配初始化过程分离,这通常会提供更好的性能和更灵活的内存管理能力。 ### new和数组(new and Arrays) 使用`new`分配对象数组时需要在类型名之后跟一对方括号,在其中指明要分配的对象数量(必须是整型,但不必是常量)。`new`返回指向第一个对象的指针(元素类型)。 ```c++ // call get_size to determine how many ints to allocate int *pia = new int[get_size()]; // pia points to the first of these ints ``` 由于`new`分配的内存并不是数组类型,因此不能对动态数组调用`begin`和`end`,也不能用范围`for`语句处理其中的元素。 默认情况下,`new`分配的对象是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小后面跟一对空括号`()`。在新标准中,还可以提供一个元素初始化器的花括号列表。如果初始化器数量大于元素数量,则`new`表达式失败,不会分配任何内存,并抛出`bad_array_new_length`异常。 ```c++ int *pia = new int[10]; // block of ten uninitialized ints int *pia2 = new int[10](); // block of ten ints value initialized to 0 string *psa = new string[10]; // block of ten empty strings string *psa2 = new string[10](); // block of ten empty strings // block of ten ints each initialized from the corresponding initializer int *pia3 = new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // block of ten strings; the first four are initialized from the given initializers // remaining elements are value initialized string *psa3 = new string[10] { "a", "an", "the", string(3,'x') }; ``` 虽然可以使用空括号对`new`分配的数组元素进行值初始化,但不能在括号中指定初始化器。这意味着不能用`auto`分配数组动态分配一个空数组是合法的,此时`new`会返回一个合法的非空指针。对于零长度的数组来说,该指针类似尾后指针,不能解引用。 使用`delete[]`释放动态数组。 ```c++ delete p; // p must point to a dynamically allocated object or be null delete [] pa; // pa must point to a dynamically allocated array or be null ``` 如果在`delete`数组指针时忘记添加方括号,或者在`delete`单一对象时使用了方括号,编译器很可能不会给出任何警告,程序可能会在执行过程中行为异常。 `unique_ptr`可以直接管理动态数组,定义时需要在对象类型后添加一对空方括号`[]`。 ```c++ // up points to an array of ten uninitialized ints unique_ptr up(new int[10]); up.release(); // automatically uses delete[] to destroy its pointer ``` 指向数组的`unique_ptr`: ![12-6](Images/12-6.png) 与`unique_ptr`不同,`shared_ptr`不直接支持动态数组管理。如果想用`shared_ptr`管理动态数组,必须提供自定义的删除器。 ```c++ // to use a shared_ptr we must supply a deleter shared_ptr sp(new int[10], [](int *p) { delete[] p; }); sp.reset(); // uses the lambda we supplied that uses delete[] to free the array ``` `shared_ptr`未定义下标运算符,智能指针类型也不支持指针算术运算。因此如果想访问`shared_ptr`管理的数组元素,必须先用`get`获取内置指针,再用内置指针进行访问。 ```c++ // shared_ptrs don't have subscript operator and don't support pointer arithmetic for (size_t i = 0; i != 10; ++i) *(sp.get() + i) = i; // use get to get a built-in pointer ``` ### allocator类(The allocator Class) `allocator`类是一个模板,定义时必须指定其可以分配的对象类型。 ```c++ allocator alloc; // object that can allocate strings auto const p = alloc.allocate(n); // allocate n unconstructed strings ``` 标准库`allocator`类及其算法: ![12-7](Images/12-7.png) `allocator`分配的内存是未构造的,程序需要在此内存中构造对象。新标准库的`construct`函数接受一个指针和零或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象,必须与对象类型相匹配。 ```c++ auto q = p; // q will point to one past the last constructed element alloc.construct(q++); // *q is the empty string alloc.construct(q++, 10, 'c'); // *q is cccccccccc alloc.construct(q++, "hi"); // *q is hi! ``` 直接使用`allocator`返回的未构造内存是错误行为,其结果是未定义的。 对象使用完后,必须对每个构造的元素调用`destroy`进行销毁。`destroy`函数接受一个指针,对指向的对象执行析构函数。 ```c++ while (q != p) alloc.destroy(--q); // free the strings we actually allocated ``` `deallocate`函数用于释放`allocator`分配的内存空间。传递给`deallocate`的指针不能为空,它必须指向由`allocator`分配的内存。而且传递给`deallocate`的大小参数必须与调用`allocator`分配内存时提供的大小参数相一致。 ```c++ alloc.deallocate(p, n); ``` `allocator`算法: ![12-8](Images/12-8.png) 传递给`uninitialized_copy`的目的位置迭代器必须指向未构造的内存,它直接在给定位置构造元素。返回(递增后的)目的位置迭代器。 # 第13章 拷贝控制 一个类通过定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作。 - 拷贝构造函数(copy constructor) - 拷贝赋值运算符(copy-assignment operator) - 移动构造函数(move constructor) - 移动赋值运算符(move-assignment operator) - 析构函数(destructor) 这些操作统称为拷贝控制操作(copy control)。 在定义任何类时,拷贝控制操作都是必要部分。 ## 拷贝、赋值与销毁(Copy,Assign,and Destroy) ### 拷贝构造函数(The Copy Constructor) 如果一个构造函数的第一个参数是自身类类型的引用(几乎总是`const`引用),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。 ```c++ class Foo { public: Foo(); // default constructor Foo(const Foo&); // copy constructor // ... }; ``` 由于拷贝构造函数在一些情况下会被隐式使用,因此通常不会声明为`explicit`的。 如果类未定义自己的拷贝构造函数,编译器会为类合成一个。一般情况下,合成拷贝构造函数(synthesized copy constructor)会将其参数的非`static`成员逐个拷贝到正在创建的对象中。 ```c++ class Sales_data { public: // other members and constructors as before // declaration equivalent to the synthesized copy constructor Sales_data(const Sales_data&); private: std::string bookNo; int units_sold = 0; double revenue = 0.0; }; // equivalent to the copy constructor that would be synthesized for Sales_data Sales_data::Sales_data(const Sales_data &orig): bookNo(orig.bookNo), // uses the string copy constructor units_sold(orig.units_sold), // copies orig.units_sold revenue(orig.revenue) // copies orig.revenue { } // empty bod ``` 使用直接初始化时,实际上是要求编译器按照函数匹配规则来选择与实参最匹配的构造函数。使用拷贝初始化时,是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。 ```c++ string dots(10, '.'); // direct initialization string s(dots); // direct initialization string s2 = dots; // copy initialization string null_book = "9-999-99999-9"; // copy initialization string nines = string(100, '9'); // copy initialization ``` 拷贝初始化通常使用拷贝构造函数来完成。但如果一个类拥有移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。 发生拷贝初始化的情况: - 用`=`定义变量。 - 将对象作为实参传递非引用类型的形参。 - 从返回类型为非引用类型的函数返回对象。 - 用花括号列表初始化数组中的元素或聚合类中的成员。 当传递一个实参或者从函数返回一个值时,不能隐式使用`explicit`构造函数。 ```c++ vector v1(10); // ok: direct initialization vector v2 = 10; // error: constructor that takes a size is explicit void f(vector); // f's parameter is copy initialized f(10); // error: can't use an explicit constructor to copy an argument f(vector(10)); // ok: directly construct a temporary vector from an int ``` ### 拷贝赋值运算符(The Copy-Assignment Operator) 重载运算符(overloaded operator)的参数表示运算符的运算对象。 如果一个运算符是成员函数,则其左侧运算对象会绑定到隐式的`this`参数上。 赋值运算符通常应该返回一个指向其左侧运算对象的引用。 ```c++ class Foo { public: Foo& operator=(const Foo&); // assignment operator // ... }; ``` 标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。 如果类未定义自己的拷贝赋值运算符,编译器会为类合成一个。一般情况下,合成拷贝赋值运算符(synthesized copy-assignment operator)会将其右侧运算对象的非`static`成员逐个赋值给左侧运算对象的对应成员,之后返回左侧运算对象的引用。 ```c++ // equivalent to the synthesized copy-assignment operator Sales_data& Sales_data::operator=(const Sales_data &rhs) { bookNo = rhs.bookNo; // calls the string::operator= units_sold = rhs.units_sold; // uses the built-in int assignment revenue = rhs.revenue; // uses the built-in double assignment return *this; // return a reference to this object } ``` ### 析构函数(The Destructor) 析构函数负责释放对象使用的资源,并销毁对象的非`static`数据成员。 析构函数的名字由波浪号`~`接类名构成,它没有返回值,也不接受参数。 ```c++ class Foo { public: ~Foo(); // destructor // ... }; ``` 由于析构函数不接受参数,所以它不能被重载。 如果类未定义自己的析构函数,编译器会为类合成一个。合成析构函数(synthesized destructor)的函数体为空。 析构函数首先执行函数体,然后再销毁数据成员。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。成员按照初始化顺序的逆序销毁。 隐式销毁一个内置指针类型的成员不会`delete`它所指向的对象。 无论何时一个对象被销毁,都会自动调用其析构函数。 当指向一个对象的引用或指针离开作用域时,该对象的析构函数不会执行。 ### 三/五法则(The Rule of Three/Five) 需要析构函数的类一般也需要拷贝赋值操作。 需要拷贝操作的类一般也需要赋值操作,反之亦然。 ### 使用=default(Using =default) 可以通过将拷贝控制成员定义为`=default`来显式地要求编译器生成合成版本。 ```c++ class Sales_data { public: // copy control; use defaults Sales_data() = default; Sales_data(const Sales_data&) = default; ~Sales_data() = default; // other members as before }; ``` 在类内使用`=default`修饰成员声明时,合成的函数是隐式内联的。如果不希望合成的是内联函数,应该只对成员的类外定义使用`=default`。 只能对具有合成版本的成员函数使用`=default`。 ### 阻止拷贝(Preventing Copies) 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显式地还是隐式地。 在C++11新标准中,将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function)可以阻止类对象的拷贝。删除的函数是一种虽然进行了声明,但是却不能以任何方式使用的函数。定义删除函数的方式是在函数的形参列表后面添加`=delete`。 ```c++ struct NoCopy { NoCopy() = default; // use the synthesized default constructor NoCopy(const NoCopy&) = delete; // no copy NoCopy &operator=(const NoCopy&) = delete; // no assignment ~NoCopy() = default; // use the synthesized destructor // other members }; ``` `=delete`和`=default`有两点不同: - `=delete`可以对任何函数使用;`=default`只能对具有合成版本的函数使用。 - `=delete`必须出现在函数第一次声明的地方;`=default`既能出现在类内,也能出现在类外。 析构函数不能是删除的函数。对于析构函数,不能定义该类型的变量或者释放指向该类型动态分配对象的指针。 如果一个类中有数据成员不能默认构造、拷贝或销毁,则对应的合成拷贝控制成员将被定义为删除的。 在旧版本的C++标准中,类通过将拷贝构造函数和拷贝赋值运算符声明为`private`成员来阻止类对象的拷贝。在新标准中建议使用`=delete`而非`private`。 ## 拷贝控制和资源管理(Copy Control and Resource Management) 通常,管理类外资源的类必须定义拷贝控制成员。 ### 行为像值的类(Classes That Act Like Values) ```C++ class HasPtr { public: HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { } // each HasPtr has its own copy of the string to which ps points HasPtr(const HasPtr &p): ps(new std::string(*p.ps)), i(p.i) { } HasPtr& operator=(const HasPtr &); ~HasPtr() { delete ps; } private: std::string *ps; int i; }; ``` 编写赋值运算符时有两点需要注意: - 即使将一个对象赋予它自身,赋值运算符也能正确工作。 ```c++ // WRONG way to write an assignment operator! HasPtr& HasPtr::operator=(const HasPtr &rhs) { delete ps; // frees the string to which this object points // if rhs and *this are the same object, we're copying from deleted memory! ps = new string(*(rhs.ps)); i = rhs.i; return *this; } ``` - 赋值运算符通常结合了拷贝构造函数和析构函数的工作。 编写赋值运算符时,一个好的方法是先将右侧运算对象拷贝到一个局部临时对象中。拷贝完成后,就可以安全地销毁左侧运算对象的现有成员了。 ```c++ HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); // copy the underlying string delete ps; // free the old memory ps = newp; // copy data from rhs into this object i = rhs.i; return *this; // return this object } ``` ### 定义行为像指针的类(Defining Classes That Act Like Pointers) ```c++ class HasPtr { public: // constructor allocates a new string and a new counter, which it sets to 1 HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0), use(new std::size_t(1)) {} // copy constructor copies all three data members and increments the counter HasPtr(const HasPtr &p): ps(p.ps), i(p.i), use(p.use) { ++*use; } HasPtr& operator=(const HasPtr&); ~HasPtr(); private: std::string *ps; int i; std::size_t *use; // member to keep track of how many objects share *ps }; ``` 析构函数释放内存前应该判断是否还有其他对象指向这块内存。 ```c++ HasPtr::~HasPtr() { if (--*use == 0) { // if the reference count goes to 0 delete ps; // delete the string delete use; // and the counter } } HasPtr& HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; // increment the use count of the right-hand operand if (--*use == 0) { // then decrement this object's counter delete ps; // if no other users delete use; // free this object's allocated members } ps = rhs.ps; // copy data from rhs into this object i = rhs.i; use = rhs.use; return *this; // return this object } ``` ## 交换操作(Swap) 通常,管理类外资源的类会定义`swap`函数。如果一个类定义了自己的`swap`函数,算法将使用自定义版本,否则将使用标准库定义的`swap`。 ```c++ class HasPtr { friend void swap(HasPtr&, HasPtr&); // other members as in § 13.2.1 (p. 511) }; inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // swap the pointers, not the string data swap(lhs.i, rhs.i); // swap the int members } ``` 一些算法在交换两个元素时会调用`swap`函数,其中每个`swap`调用都应该是未加限定的。如果存在类型特定的`swap`版本,其匹配程度会优于*std*中定义的版本(假定作用域中有`using`声明)。 ```c++ void swap(Foo &lhs, Foo &rhs) { // WRONG: this function uses the library version of swap, not the HasPtr version std::swap(lhs.h, rhs.h); // swap other members of type Foo } void swap(Foo &lhs, Foo &rhs) { using std::swap; swap(lhs.h, rhs.h); // uses the HasPtr version of swap // swap other members of type Foo } ``` 与拷贝控制成员不同,`swap`函数并不是必要的。但是对于分配了资源的类,定义`swap`可能是一种重要的优化手段。 由于`swap`函数的存在就是为了优化代码,所以一般将其声明为内联函数。 定义了`swap`的类通常用`swap`来实现赋值运算符。在这种版本的赋值运算符中,右侧运算对象以值方式传递,然后将左侧运算对象与右侧运算对象的副本进行交换(拷贝并交换,copy and swap)。这种方式可以正确处理自赋值情况。 ```c++ // note rhs is passed by value, which means the HasPtr copy constructor // copies the string in the right-hand operand into rhs HasPtr& HasPtr::operator=(HasPtr rhs) { // swap the contents of the left-hand operand with the local variable rhs swap(*this, rhs); // rhs now points to the memory this object had used return *this; // rhs is destroyed, which deletes the pointer in rhs } ``` ## 拷贝控制示例(A Copy-Control Example) 拷贝赋值运算符通常结合了构造函数和析构函数的工作。在这种情况下,公共部分应该放在`private`的工具函数中完成。 ## 动态内存管理类(Classes That Manage Dynamic Memory) 移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象中。 ## 对象移动(Moving Objects) 某些情况下,一个对象拷贝后就立即被销毁了,此时移动而非拷贝对象会大幅度提高性能。 在旧版本的标准库中,容器所能保存的类型必须是可拷贝的。但在新标准中,可以用容器保存不可拷贝,但可移动的类型。 标准库容器、`string`和`shared_ptr`类既支持移动也支持拷贝。IO类和`unique_ptr`类可以移动但不能拷贝。 ### 右值引用(Rvalue Reference) 为了支持移动操作,C++11引入了右值引用类型。右值引用就是必须绑定到右值的引用。可以通过`&&`来获得右值引用。 ```c++ int i = 42; int &r = i; // ok: r refers to i int &&rr = i; // error: cannot bind an rvalue reference to an int &r2 = i * 42; // error: i * 42 is an rvalue const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication ``` 右值引用只能绑定到即将被销毁,并且没有其他用户的临时对象上。使用右值引用的代码可以自由地接管所引用对象的资源。 变量表达式都是左值,所以不能将一个右值引用直接绑定到一个变量上,即使这个变量的类型是右值引用也不行。 ```c++ int &&rr1 = 42; // ok: literals are rvalues int &&rr2 = rr1; // error: the expression rr1 is an lvalue! ``` 调用`move`函数可以获得绑定在左值上的右值引用,此函数定义在头文件*utility*中。 ```c++ int &&rr3 = std::move(rr1); ``` 调用`move`函数的代码应该使用`std::move`而非`move`,这样做可以避免潜在的名字冲突。 ### 移动构造函数和移动赋值运算符(Move Constructor and Move Assignment) 移动构造函数的第一个参数是该类类型的右值引用,其他任何额外参数都必须有默认值。 除了完成资源移动,移动构造函数还必须确保移后源对象是可以安全销毁的。 在函数的形参列表后面添加关键字`noexcept`可以指明该函数不会抛出任何异常。 对于构造函数,`noexcept`位于形参列表和初始化列表开头的冒号之间。在类的头文件声明和定义中(如果定义在类外)都应该指定`noexcept`。 ```c++ class StrVec { public: StrVec(StrVec&&) noexcept; // move constructor // other members as before }; StrVec::StrVec(StrVec &&s) noexcept : /* member initializers */ { /* constructor body */ } ``` 标准库容器能对异常发生时其自身的行为提供保障。虽然移动操作通常不抛出异常,但抛出异常也是允许的。为了安全起见,除非容器确定元素类型的移动操作不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝而非移动操作。 不抛出异常的移动构造函数和移动赋值运算符必须标记为`noexcept`。 在移动操作之后,移后源对象必须保持有效的、可销毁的状态,但是用户不能使用它的值。 ```c++ StrVec &StrVec::operator=(StrVec &&rhs) noexcept { // direct test for self-assignment if (this != &rhs) { free(); // free existing elements elements = rhs.elements; // take over resources from rhs first_free = rhs.first_free; cap = rhs.cap; // leave rhs in a destructible state rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; } ``` 只有当一个类没有定义任何拷贝控制成员,且类的每个非`static`数据成员都可以移动时,编译器才会为类合成移动构造函数和移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,则编译器也能移动该成员。 ```c++ // the compiler will synthesize the move operations for X and hasX struct X { int i; // built-in types can be moved std::string s; // string defines its own move operations }; struct hasX { X mem; // X has synthesized move operations }; X x, x2 = std::move(x); // uses the synthesized move constructor hasX hx, hx2 = std::move(hx); // uses the synthesized move constructor ``` 与拷贝操作不同,移动操作永远不会被隐式定义为删除的函数。但如果显式地要求编译器生成`=default`的移动操作,且编译器不能移动全部成员,则移动操作会被定义为删除的函数。 定义了移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则这些成员会被默认地定义为删除的函数。 如果一个类有可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的,即使调用`move`函数时也是如此。拷贝赋值运算符和移动赋值运算符的情况类似。 ```c++ class Foo { public: Foo() = default; Foo(const Foo&); // copy constructor // other members, but Foo does not define a move constructor }; Foo x; Foo y(x); // copy constructor; x is an lvalue Foo z(std::move(x)); // copy constructor, because there is no move constructor ``` 使用非引用参数的单一赋值运算符可以实现拷贝赋值和移动赋值两种功能。依赖于实参的类型,左值被拷贝,右值被移动。 ```c++ // assignment operator is both the move- and copy-assignment operator HasPtr& operator=(HasPtr rhs) { swap(*this, rhs); return *this; } hp = hp2; // hp2 is an lvalue; copy constructor used to copy hp2 hp = std::move(hp2); // move constructor moves hp2 ``` 建议将五个拷贝控制成员当成一个整体来对待。如果一个类需要任何一个拷贝操作,它就应该定义所有五个操作。 移动赋值运算符可以直接检查自赋值情况。 C++11标准库定义了移动迭代器(move iterator)适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。移动迭代器的解引用运算符返回一个右值引用。 调用`make_move_iterator`函数能将一个普通迭代器转换成移动迭代器。原迭代器的所有其他操作在移动迭代器中都照常工作。 最好不要移动构造函数移动赋值运算符这些类实现代码之外的地方随意使用`move`操作。 ### 右值引用和成员函数(Rvalue References and Member Functions) 区分移动和拷贝的重载函数通常有一个版本接受一个`const T&`参数,另一个版本接受一个`T&&`参数(*T*为类型)。 ```c++ void push_back(const X&); // copy: binds to any kind of X void push_back(X&&); // move: binds only to modifiable rvalues of type X ``` 有时可以对右值赋值: ```c++ string s1, s2; s1 + s2 = "wow!"; ``` 在旧标准中,没有办法阻止这种使用方式。为了维持向下兼容性,新标准库仍然允许向右值赋值。但是可以在自己的类中阻止这种行为,规定左侧运算对象(即`this`指向的对象)必须是一个左值。 在非`static`成员函数的形参列表后面添加引用限定符(reference qualifier)可以指定`this`的左值/右值属性。引用限定符可以是`&`或者`&&`,分别表示`this`可以指向一个左值或右值对象。引用限定符必须同时出现在函数的声明和定义中。 ```c++ class Foo { public: Foo &operator=(const Foo&) &; // may assign only to modifiable lvalues // other members of Foo }; Foo &Foo::operator=(const Foo &rhs) & { // do whatever is needed to assign rhs to this object return *this; } ``` 一个非`static`成员函数可以同时使用`const`和引用限定符,此时引用限定符跟在`const`限定符之后。 ```c++ class Foo { public: Foo someMem() & const; // error: const qualifier must come first Foo anotherMem() const &; // ok: const qualifier comes first }; ``` 引用限定符也可以区分成员函数的重载版本。 ```c++ class Foo { public: Foo sorted() &&; // may run on modifiable rvalues Foo sorted() const &; // may run on any kind of Foo }; retVal().sorted(); // retVal() is an rvalue, calls Foo::sorted() && retFoo().sorted(); // retFoo() is an lvalue, calls Foo::sorted() const & ``` 如果一个成员函数有引用限定符,则具有相同参数列表的所有重载版本都必须有引用限定符。 ```c++ class Foo { public: Foo sorted() &&; Foo sorted() const; // error: must have reference qualifier // Comp is type alias for the function type // that can be used to compare int values using Comp = bool(const int&, const int&); Foo sorted(Comp*); // ok: different parameter list }; ``` # 第14章 重载运算与类型转换 ## 基本概念(Basic Concepts) 重载的运算符是具有特殊名字的函数,它们的名字由关键字`operator`和其后要定义的运算符号组成。 重载运算符函数的参数数量和该运算符作用的运算对象数量一样多。对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。除了重载的函数调用运算符`operator()`之外,其他重载运算符不能含有默认实参。 如果一个运算符函数是类的成员函数,则它的第一个运算对象会绑定到隐式的`this`指针上。因此成员运算符函数的显式参数数量比运算对象的数量少一个。 当运算符作用于内置类型的运算对象时,无法改变该运算符的含义。 只能重载大多数已有的运算符,无权声明新的运算符号。 ![14-1](Images/14-1.png) 重载运算符的优先级和结合律与对应的内置运算符一致。 可以像调用普通函数一样直接调用运算符函数。 ```c++ // equivalent calls to a nonmember operator function data1 + data2; // normal expression operator+(data1, data2); // equivalent function call data1 += data2; // expression-based ''call'' data1.operator+=(data2); // equivalent call to a member operator function ``` 通常情况下,不应该重载逗号`,`、取地址`&`、逻辑与`&&`和逻辑或`||`运算符。 建议只有当操作的含义对于用户来说清晰明了时才使用重载运算符,重载运算符的返回类型也应该与其内置版本的返回类型兼容。 如果类中含有算术运算符或位运算符,则最好也提供对应的复合赋值运算符。 把运算符定义为成员函数时,它的左侧运算对象必须是运算符所属类型的对象。 ```C++ string s = "world"; string t = s + "!"; // ok: we can add a const char* to a string string u = "hi" + s; // would be an error if + were a member of string ``` 如何选择将运算符定义为成员函数还是普通函数: - 赋值`=`、下标`[]`、调用`()`和成员访问箭头`->`运算符必须是成员函数。 - 复合赋值运算符一般是成员函数,但并非必须。 - 改变对象状态或者与给定类型密切相关的运算符,如递增、递减、解引用运算符,通常是成员函数。 - 具有对称性的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符,通常是普通函数。 ## 输入和输出运算符(Input and Output Operators) ### 重载输出运算符<<(Overloading the Output Operator <<) 通常情况下,输出运算符的第一个形参是`ostream`类型的普通引用,第二个形参是要打印类型的常量引用,返回值是它的`ostream`形参。 ```c++ ostream &operator<<(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; } ``` 输出运算符应该尽量减少格式化操作。 输入输出运算符必须是非成员函数。而由于IO操作通常需要读写类的非公有数据,所以输入输出运算符一般被声明为友元。 ### 重载输入运算符>>(Overloading the Input Operator >>) 通常情况下,输入运算符的第一个形参是要读取的流的普通引用,第二个形参是要读入的目的对象的普通引用,返回值是它的第一个形参。 ```c++ istream &operator>>(istream &is, Sales_data &item) { double price; // no need to initialize; we'll read into price before we use it is >> item.bookNo >> item.units_sold >> price; if (is) // check that the inputs succeeded item.revenue = item.units_sold * price; else item = Sales_data(); // input failed: give the object the default state return is; } ``` 输入运算符必须处理输入失败的情况,而输出运算符不需要。 以下情况可能导致读取操作失败: - 读取了错误类型的数据。 - 读取操作到达文件末尾。 - 遇到输入流的其他错误。 当读取操作发生错误时,输入操作符应该负责从错误状态中恢复。 如果输入的数据不符合规定的格式,即使从技术上看IO操作是成功的,输入运算符也应该设置流的条件状态以标示出失败信息。通常情况下,输入运算符只设置`failbit`状态。`eofbit`、`badbit`等错误最好由IO标准库自己标示。 ## 算术和关系运算符(Arithmetic and Relational Operators) 通常情况下,算术和关系运算符应该定义为非成员函数,以便两侧的运算对象进行转换。其次,由于这些运算符一般不会改变运算对象的状态,所以形参都是常量引用。 算术运算符通常会计算它的两个运算对象并得到一个新值,这个值通常存储在一个局部变量内,操作完成后返回该局部变量的副本作为结果(返回类型建议设置为原对象的`const`类型)。 ```c++ // assumes that both objects refer to the same book Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs) { Sales_data sum = lhs; // copy data members from lhs into sum sum += rhs; // add rhs into sum return sum; } ``` 如果类定义了算术运算符,则通常也会定义对应的复合赋值运算符,此时最有效的方式是使用复合赋值来实现算术运算符。 ### 相等运算符(Equality Operators) 相等运算符设计准则: - 如果类在逻辑上有相等性的含义,则应该定义`operator==`而非一个普通的命名函数。这样做便于使用标准库容器和算法,也更容易记忆。 - 通常情况下,`operator==`应该具有传递性。 - 如果类定义了`operator==`,则也应该定义`operator!=`。 - `operator==`和`operator!=`中的一个应该把具体工作委托给另一个。 ```c++ bool operator==(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue; } bool operator!=(const Sales_data &lhs, const Sales_data &rhs) { return !(lhs == rhs); } ``` ### 关系运算符(Relational Operators) 定义了相等运算符的类通常也会定义关系运算符。因为关联容器和一些算法要用到小于运算符,所以定义`operator<`会比较实用。 关系运算符设计准则: - 定义顺序关系,令其与关联容器中对关键字的要求保持一致。 - 如果类定义了`operator==`,则关系运算符的定义应该与`operator==`保持一致。特别是,如果两个对象是不相等的,那么其中一个对象应该小于另一个对象。 - 只有存在唯一一种逻辑可靠的小于关系时,才应该考虑为类定义`operator<`。 ## 赋值运算符(Assignment Operators) 赋值运算符必须定义为成员函数,复合赋值运算符通常也是如此。这两类运算符都应该返回其左侧运算对象的引用。 ```c++ StrVec &StrVec::operator=(initializer_list il) { // alloc_n_copy allocates space and copies elements from the given range auto data = alloc_n_copy(il.begin(), il.end()); free(); // destroy the elements in this object and free the space elements = data.first; // update data members to point to the new space first_free = cap = data.second; return *this; } // member binary operator: left-hand operand is bound to the implicit this pointer // assumes that both objects refer to the same book Sales_data& Sales_data::operator+=(const Sales_data &rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; } ``` ## 下标运算符(Subscript Operator) 下标运算符必须定义为成员函数。 类通常会定义两个版本的下标运算符:一个返回普通引用,另一个是类的常量成员并返回常量引用。 ```c++ class StrVec { public: std::string& operator[](std::size_t n) { return elements[n]; } const std::string& operator[](std::size_t n) const { return elements[n]; } private: std::string *elements; // pointer to the first element in the array } ``` ## 递增和递减运算符(Increment and Decrement Operators) 定义递增和递减运算符的类应该同时定义前置和后置版本,这些运算符通常定义为成员函数。 为了与内置操作保持一致,前置递增或递减运算符应该返回运算后对象的引用。 ```c++ // prefix: return a reference to the incremented/decremented object StrBlobPtr& StrBlobPtr::operator++() { ++curr; // advance the current state return *this; } ``` 后置递增或递减运算符接受一个额外的(不被使用)`int`类型形参,该形参的唯一作用就是区分运算符的前置和后置版本。 ```c++ class StrBlobPtr { public: // increment and decrement StrBlobPtr& operator++(); // prefix operators StrBlobPtr& operator--(); StrBlobPtr operator++(int); // postfix operators StrBlobPtr operator--(int); }; ``` 为了与内置操作保持一致,后置递增或递减运算符应该返回运算前对象的原值(返回类型建议设置为原对象的`const`类型)。 ```c++ StrBlobPtr StrBlobPtr::operator++(int) { StrBlobPtr ret = *this; // save the current value ++*this; // advance one element; prefix ++ checks the increment return ret; // return the saved state } ``` 如果想通过函数调用的方式使用后置递增或递减运算符,则必须为它的整型参数传递一个值。 ```c++ StrBlobPtr p(a1); // p points to the vector inside a1 p.operator++(0); // call postfix operator++ p.operator++(); // call prefix operator++ ``` ## 成员访问运算符(Member Access Operators) 箭头运算符必须定义为成员函数,解引用运算符通常也是如此。 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的类的对象。 ```c++ class StrBlobPtr { public: std::string& operator*() const { return (*p)[curr]; // (*p) is the vector to which this object points } std::string* operator->() const { // delegate the real work to the dereference operator return & this->operator*(); } }; ``` 对于形如`point->mem`的表达式来说,*point*必须是指向类对象的指针或者是一个重载了`operator->`的类的对象。*point*类型不同,`point->mem`的含义也不同。 - 如果*point*是指针,则调用内置箭头运算符,表达式等价于`(*point).mem`。 - 如果*point*是重载了`operator->`的类的对象,则使用`point.operator->()`的结果来获取*mem*,表达式等价于`(point.operator->())->mem`。其中,如果该结果是一个指针,则执行内置操作,否则重复调用当前操作。 ## 函数调用运算符(Function-Call Operator) 函数调用运算符必须定义为成员函数。一个类可以定义多个不同版本的调用运算符,相互之间必须在参数数量或类型上有所区别。 ```c++ class PrintString { public: PrintString(ostream &o = cout, char c = ' '): os(o), sep(c) { } void operator()(const string &s) const { os << s << sep; } private: ostream &os; // stream on which to write char sep; // character to print after each output }; PrintString printer; // uses the defaults; prints to cout printer(s); // prints s followed by a space on cout ``` 如果类定义了调用运算符,则该类的对象被称作函数对象(function object),函数对象常常作为泛型算法的实参。 ```c++ for_each(vs.begin(), vs.end(), PrintString(cerr, '\n')); ``` ### lambda是函数对象(Lambdas Are Function Objects) 编写一个`lambda`后,编译器会将该表达式转换成一个未命名类的未命名对象,类中含有一个重载的函数调用运算符。 ```c++ // sort words by size, but maintain alphabetical order for words of the same size stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); }); // acts like an unnamed object of a class that would look something like class ShorterString { public: bool operator()(const string &s1, const string &s2) const { return s1.size() < s2.size(); } }; ``` `lambda`默认不能改变它捕获的变量。因此在默认情况下,由`lambda`产生的类中的函数调用运算符是一个`const`成员函数。如果`lambda`被声明为可变的,则调用运算符就不再是`const`函数了。 `lambda`通过引用捕获变量时,由程序负责确保`lambda`执行时该引用所绑定的对象确实存在。因此编译器可以直接使用该引用而无须在`lambda`产生的类中将其存储为数据成员。相反,通过值捕获的变量被拷贝到`lambda`中,此时`lambda`产生的类必须为每个值捕获的变量建立对应的数据成员,并创建构造函数,用捕获变量的值来初始化数据成员。 ```c++ // get an iterator to the first element whose size() is >= sz auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; }); // would generate a class that looks something like class SizeComp { public: SizeComp(size_t n): sz(n) { } // parameter for each captured variable // call operator with the same return type, parameters, and body as the lambda bool operator()(const string &s) const { return s.size() >= sz; } private: size_t sz; // a data member for each variable captured by value }; ``` `lambda`产生的类不包含默认构造函数、赋值运算符和默认析构函数,它是否包含默认拷贝/移动构造函数则通常要视捕获的变量类型而定。 ### 标准库定义的函数对象(Library-Defined Function Objects) 标准库在头文件*functional*中定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。这些类都被定义为模板的形式,可以为其指定具体的应用类型(即调用运算符的形参类型)。 ![14-2](Images/14-2.png) 关系运算符的函数对象类通常被用来替换算法中的默认运算符,这些类对于指针同样适用。 ```c++ vector nameTable; // vector of pointers // error: the pointers in nameTable are unrelated, so < is undefined sort(nameTable.begin(), nameTable.end(), [](string *a, string *b) { return a < b; }); // ok: library guarantees that less on pointer types is well defined sort(nameTable.begin(), nameTable.end(), less()); ``` ### 可调用对象与function(Callable Objects and function) 调用形式指明了调用返回的类型以及传递给调用的实参类型。不同的可调用对象可能具有相同的调用形式。 标准库`function`类型是一个模板,定义在头文件*functional*中,用来表示对象的调用形式。 ![14-3](Images/14-3.png) 创建一个具体的`function`类型时必须提供其所表示的对象的调用形式。 ```c++ // ordinary function int add(int i, int j) { return i + j; } // function-object class struct div { int operator()(int denominator, int divisor) { return denominator / divisor; } }; function f1 = add; // function pointer function f2 = div(); // object of a function-object class function f3 = [](int i, int j) { return i * j; }; // lambda cout << f1(4,2) << endl; // prints 6 cout << f2(4,2) << endl; // prints 2 cout << f3(4,2) << endl; // prints 8 ``` 不能直接将重载函数的名字存入`function`类型的对象中,这样做会产生二义性错误。消除二义性的方法是使用`lambda`或者存储函数指针而非函数名字。 C++11新标准库中的`function`类与旧版本中的`unary_function`和`binary_function`没有关系,后两个类已经被`bind`函数代替。 ## 重载、类型转换与运算符(Overloading,Conversions,and Operators) 转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversion)。 ### 类型转换运算符(Conversion Operators) 类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型。它不能声明返回类型,形参列表也必须为空,一般形式如下: ```c++ operator type() const; ``` 类型转换运算符可以面向除了`void`以外的任意类型(该类型要能作为函数的返回类型)进行定义。 ```c++ class SmallInt { public: SmallInt(int i = 0): val(i) { if (i < 0 || i > 255) throw std::out_of_range("Bad SmallInt value"); } operator int() const { return val; } private: std::size_t val; }; ``` 隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。 ```c++ // the double argument is converted to int using the built-in conversion SmallInt si = 3.14; // calls the SmallInt(int) constructor // the SmallInt conversion operator converts si to int; si + 3.14; // that int is converted to double using the built-in conversion ``` 应该避免过度使用类型转换函数。如果在类类型和转换类型之间不存在明显的映射关系,则这样的类型转换可能具有误导性。 C++11引入了显示的类型转换运算符(explicit conversion operator)。和显式构造函数一样,编译器通常不会将显式类型转换运算符用于隐式类型转换。 ```c++ class SmallInt { public: // the compiler won't automatically apply this conversion explicit operator int() const { return val; } // other members as before }; SmallInt si = 3; // ok: the SmallInt constructor is not explicit si + 3; // error: implicit is conversion required, but operator int is explicit static_cast(si) + 3; // ok: explicitly request the conversion ``` 如果表达式被用作条件,则编译器会隐式地执行显式类型转换。 - `if`、`while`、`do-while`语句的条件部分。 - `for`语句头的条件表达式。 - 条件运算符`? :`的条件表达式。 - 逻辑非运算符`!`、逻辑或运算符`||`、逻辑与运算符`&&`的运算对象。 类类型向`bool`的类型转换通常用在条件部分,因此`operator bool`一般被定义为显式的。 ### 避免有二义性的类型转换(Avoiding Ambiguous Conversions) 在两种情况下可能产生多重转换路径: - *A*类定义了一个接受*B*类对象的转换构造函数,同时*B*类定义了一个转换目标是*A*类的类型转换运算符。 ```c++ // usually a bad idea to have mutual conversions between two class types struct B; struct A { A() = default; A(const B&); // converts a B to an A // other members }; struct B { operator A() const; // also converts a B to an A // other members }; A f(const A&); B b; A a = f(b); // error ambiguous: f(B::operator A()) // or f(A::A(const B&)) ``` - 类定义了多个类型转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。 ```c++ struct A { A(int = 0); // usually a bad idea to have two A(double); // conversions from arithmetic types operator int() const; // usually a bad idea to have two operator double() const; // conversions to arithmetic types // other members }; void f2(long double); A a; f2(a); // error ambiguous: f(A::operator int()) // or f(A::operator double()) long lg; A a2(lg); // error ambiguous: A::A(int) or A::A(double) ``` 可以通过显式调用类型转换运算符或转换构造函数解决二义性问题,但不能使用强制类型转换,因为强制类型转换本身也存在二义性。 ```c++ A a1 = f(b.operator A()); // ok: use B's conversion operator A a2 = f(A(b)); // ok: use A's constructor ``` 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标都是算术类型的转换。 使用两个用户定义的类型转换时,如果转换前后存在标准类型转换,则由标准类型转换决定最佳匹配。 如果在调用重载函数时需要使用构造函数或者强制类型转换来改变实参的类型,通常意味着程序设计存在不足。 调用重载函数时,如果需要额外的标准类型转换,则该转换只有在所有可行函数都请求同一个用户定义类型转换时才有用。如果所需的用户定义类型转换不止一个,即使其中一个调用能精确匹配而另一个调用需要额外的标准类型转换,也会产生二义性错误。 ```c++ struct C { C(int); // other members }; struct E { E(double); // other members }; void manip2(const C&); void manip2(const E&); // error ambiguous: two different user-defined conversions could be used manip2(10); // manip2(C(10) or manip2(E(double(10))) ``` ### 函数匹配与重载运算符(Function Matching and Overloaded Operators) 表达式中运算符的候选函数集既包括成员函数,也包括非成员函数。 ```c++ class SmallInt { friend SmallInt operator+(const SmallInt&, const SmallInt&); public: SmallInt(int = 0); // conversion from int operator int() const { return val; } // conversion to int private: std::size_t val; }; SmallInt s1, s2; SmallInt s3 = s1 + s2; // uses overloaded operator+ int i = s3 + 0; // error: ambiguous ``` 如果类既定义了转换目标是算术类型的类型转换,也定义了重载的运算符,则会遇到重载运算符与内置运算符的二义性问题。 # 第15章 面向对象程序设计 ## OOP:概述(OOP:An Overview) 面向对象程序设计(object-oriented programming)的核心思想是数据抽象(封装)、继承和动态绑定(多态)。 通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class),其他类则直接或间接地从基类继承而来,这些继承得到的类叫做派生类(derived class)。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类应该将这些函数声明为虚函数(virtual function)。方法是在函数名称前添加`virtual`关键字。 ```C++ class Quote { public: std::string isbn() const; virtual double net_price(std::size_t n) const; }; ``` 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个或哪些基类继承而来的。类派生列表的形式首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以添加访问说明符。 ```c++ class Bulk_quote : public Quote { // Bulk_quote inherits from Quote public: double net_price(std::size_t) const override; }; ``` 派生类必须在其内部对所有重新定义的虚函数进行声明。 使用基类的引用或指针调用一个虚函数时将发生动态绑定(dynamic binding),也叫运行时绑定(run-time binding)。函数的运行版本将由实参决定。 ## 定义基类和派生类(Defining Base and Derived Classes) ### 定义基类(Defining a Base Class) 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。 除构造函数之外的任何非静态函数都能定义为虚函数`virtual`关键字只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数。 成员函数如果没有被声明为虚函数,则其解析过程发生在编译阶段而非运行阶段派生类能访问基类的公有成员,不能访问私有成员。如果基类希望定义外部代码无法访问,但是派生类对象可以访问的成员,可以使用受保护的(protected)访问运算符进行说明。 ### 定义派生类(Defining a Derived Class) 类派生列表中的访问说明符用于控制派生类从基类继承而来的成员是否对派生类的用户可见。 如果派生类没有覆盖其基类的某个虚函数,则该虚函数的行为类似于其他的普通函数,派生类会直接继承其在基类中的版本。 C++标准并没有明确规定派生类的对象在内存中如何分布,一个对象中继承自基类的部分和派生类自定义的部分不一定是连续存储的。 因为在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当作基类对象来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分上。这种转换通常称为派生类到基类的(derived-to-base)类型转换,编译器会隐式执行。 ```c++ Quote item; // object of base type Bulk_quote bulk; // object of derived type Quote *p = &item; // p points to a Quote object p = &bulk; // p points to the Quote part of bulk Quote &r = bulk; // r bound to the Quote part of bulk ``` 每个类控制它自己的成员初始化过程,派生类必须使用基类的构造函数来初始化它的基类部分。派生类的构造函数通过构造函数初始化列表来将实参传递给基类构造函数。 ```c++ Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) { } ``` 除非特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。 派生类初始化时首先初始化基类部分,然后按照声明的顺序依次初始化派生类成员。 派生类可以访问基类的公有成员和受保护成员。 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。如果某静态成员是可访问的,则既通过基类也能通过派生类使用它。 已经完整定义的类才能被用作基类。 ```c++ class Base { /* ... */ } ; class D1: public Base { /* ... */ }; class D2: public D1 { /* ... */ }; ``` *Base*是*D1*的直接基类(direct base),是*D2*的间接基类(indirect base)。最终的派生类将包含它直接基类的子对象以及每个间接基类的子对象。 C++11中,在类名后面添加`final`关键字可以禁止其他类继承它。 ```c++ class NoDerived final { /* */ }; // NoDerived can't be a base class class Base { /* */ }; // Last is final; we cannot inherit from Last class Last final : Base { /* */ }; // Last can't be a base class class Bad : NoDerived { /* */ }; // error: NoDerived is final class Bad2 : Last { /* */ }; // error: Last is final ``` ### 类型转换与继承(Conversions and Inheritance) 和内置指针一样,智能指针类也支持派生类到基类的类型转换,所以可以将一个派生类对象的指针存储在一个基类的智能指针内表达式的静态类型(static type)在编译时总是已知的,它是变量声明时的类型表达式生成的类型;动态类型(dynamic type)则是变量或表达式表示的内存中对象的类型,只有运行时才可知。 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致不存在从基类到派生类的隐式类型转换,即使一个基类指针或引用绑定在一个派生类对象上也不行,因为编译器只能通过检查指针或引用的静态类型来判断转换是否合法。 ```c++ Quote base; Bulk_quote* bulkP = &base; // error: can't convert base to derived Bulk_quote& bulkRef = base; // error: can't convert base to derived ``` 如果在基类中含有一个或多个虚函数,可以使用`dynamic_cast`运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用,该转换的安全检查将在运行期间执行。 如果已知某个基类到派生类的转换是安全的,可以使用`static_cast`强制覆盖掉编译器的检查工作派生类到基类的自动类型转换只对指针或引用有效,在派生类类型和基类类型之间不存在这种转换。 派生类到基类的转换允许我们给基类的拷贝/移动操作传递一个派生类的对象,这些操作是基类定义的,只会处理基类自己的成员,派生类的部分被切掉(sliced down)了。 ```c++ Bulk_quote bulk; // object of derived type Quote item(bulk); // uses the Quote::Quote(const Quote&) constructor item = bulk; // calls Quote::operator=(const Quote&) ``` 用一个派生类对象为一个基类对象初始化或赋值时,只有该对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉。 ## 虚函数(Virtual Functions) 当且仅当通过指针或引用调用虚函数时,才会在运行过程解析该调用,也只有在这种情况下对象的动态类型有可能与静态类型不同。 在派生类中覆盖某个虚函数时,可以再次使用`virtual`关键字说明函数性质,但这并非强制要求。因为一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。 在派生类中覆盖某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配派生类可以定义一个与基类中的虚函数名字相同但形参列表不同的函数,但编译器会认为该函数与基类中原有的函数是相互独立的,此时派生类的函数并没有覆盖掉基类中的版本。 C++11允许派生类使用`override`关键字显式地注明虚函数。如果`override`标记了某个函数,但该函数并没有覆盖已存在的虚函数,编译器将报告错误。`override`位于函数参数列表之后。 ```c++ struct B { virtual void f1(int) const; virtual void f2(); void f3(); }; struct D1 : B { void f1(int) const override; // ok: f1 matches f1 in the base void f2(int) override; // error: B has no f2(int) function void f3() override; // error: f3 not virtual void f4() override; // error: B doesn't have a function named f4 } ``` 与禁止类继承类似,函数也可以通过添加`final`关键字来禁止覆盖操作。 ```c++ struct D2 : B { // inherits f2() and f3() from B and overrides f1(int) void f1(int) const final; // subsequent classes can't override f1(int) }; ``` `final`和`override`关键字出现在形参列表(包括任何`const`或引用修饰符)以及尾置返回类型之后。 虚函数也可以有默认实参,每次函数调用的默认实参值由本次调用的静态类型决定。如果通过基类的指针或引用调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。 如果虚函数使用默认实参,则基类和派生类中定义的默认实参值最好一致。 使用作用域运算符`::`可以强制执行虚函数的某个版本,不进行动态绑定。 ```c++ // calls the version from the base class regardless of the dynamic type of baseP double undiscounted = baseP->Quote::net_price(42); ``` 通常情况下,只有成员函数友元中的代码才需要使用作用域运算符回避虚函数的动态绑定机制。 如果一个派生类虚函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用会被解析为对派生类版本自身的调用,从而导致无限递归。 ## 抽象基类(Abstract Base Classes) 在类内部虚函数声明语句的分号前添加`=0`可以将一个虚函数声明为纯虚(pure virtual)函数。一个纯虚函数无须定义。 ```c++ double net_price(std::size_t) const = 0; ``` 可以为纯虚函数提供定义,但函数体必须定义在类的外部。 含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。 不能创建抽象基类的对象。 派生类构造函数只初始化它的直接基类。 重构(refactoring)负责重新设计类的体系以便将操作或数据从一个类移动到另一个类中。 ## 访问控制与继承(Access Control and Inheritance) 一个类可以使用`protected`关键字来声明外部代码无法访问,但是派生类对象可以访问的成员。 派生类的成员友元只能通过派生类对象来访问基类的`protected`成员。派生类对于一个基类对象中的`protected`成员没有任何访问权限。 ```c++ class Base { protected: int prot_mem; // protected member }; class Sneaky : public Base { friend void clobber(Sneaky&); // can access Sneaky::prot_mem friend void clobber(Base&); // can't access Base::prot_mem int j; // j is private by default }; // ok: clobber can access the private and protected members in Sneaky objects void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } // error: clobber can't access the protected members in Base void clobber(Base &b) { b.prot_mem = 0; } ``` 基类中成员的访问说明符和派生列表中的访问说明符都会影响某个类对其继承成员的访问权限。 派生访问说明符对于派生类的成员及友元能否访问其直接基类的成员没有影响,对基类成员的访问权限只与基类中的访问说明符有关。 派生访问说明符的作用是控制派生类(包括派生类的派生类)用户对于基类成员的访问权限。 - 如果使用公有继承,则基类的公有成员和受保护成员在派生类中属性不发生改变。 - 如果使用受保护继承,则基类的公有成员和受保护成员在派生类中变为受保护成员。 - 如果使用私有继承,则基类的公有成员和受保护成员在派生类中变为私有成员。 派生类到基类转换的可访问性(假定*D*继承自*B*): - 只有当*D*公有地继承*B*时,用户代码才能使用派生类到基类的转换。 - 不论*D*以什么方式继承*B*,*D*的成员函数和友元都能使用派生类到基类的转换。 - 如果*D*继承*B*的方式是公有的或者受保护的,则*D*的派生类的成员函数和友元可以使用*D*到*B*的类型转换;反之,如果*D*继承*B*的方式是私有的,则不能使用。 对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类到基类的类型转换也是可访问的。 友元对基类的访问权限由基类自身控制,即使对于派生类中的基类部分也是如此。 ```c++ class Base { // added friend declaration; other members as before friend class Pal; // Pal has no access to classes derived from Base }; class Pal { public: int f(Base b) { return b.prot_mem; } // ok: Pal is a friend of Base int f2(Sneaky s) { return s.j; } // error: Pal not friend of Sneaky // access to a base class is controlled by the base class, even inside a derived object int f3(Sneaky s) { return s.prot_mem; } // ok: Pal is a friend }; ``` 友元关系不能继承,每个类负责控制各自成员的访问权限。 使用`using`声明可以改变派生类继承的某个名字的访问级别。新的访问级别由该`using`声明之前的访问说明符决定。 ```c++ class Base { public: std::size_t size() const { return n; } protected: std::size_t n; }; class Derived : private Base { // note: private inheritance public: // maintain access levels for members related to the size of the object using Base::size; protected: using Base::n; }; ``` 派生类只能为那些它可以访问的名字提供`using`声明。 默认情况下,使用`class`关键字定义的派生类是私有继承的,而使用`struct`关键字定义的派生类是公有继承的。 建议显式地声明派生类的继承方式,不要仅仅依赖于默认设置。 ## 继承中的类作用域(Class Scope under Inheritance) 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见性。 派生类定义的成员会隐藏同名的基类成员。 ```c++ struct Base { protected: int mem; }; struct Derived : Base { int get_mem() { return mem; } // returns Derived::mem protected: int mem; // hides mem in the base }; ``` 可以通过作用域运算符`::`来使用被隐藏的基类成员。 ```c++ struct Derived : Base { int get_base_mem() { return Base::mem; } // ... }; ``` 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。 和其他函数一样,成员函数无论是否是虚函数都能被重载。 派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对它来说都是可见的,那么它就需要覆盖所有版本,或者一个也不覆盖。 有时一个类仅需覆盖重载集合中的一些而非全部函数,此时如果我们不得不覆盖基类中的每一个版本的话,操作会极其繁琐。为了简化操作,可以为重载成员提供`using`声明。`using`声明指定了一个函数名字但不指定形参列表,所以一条基类成员函数的`using`声明语句就可以把该函数的所有重载实例添加到派生类作用域中。 ```c++ class Base { private: int x; public: virtual void mf1() = 0; virtual void mf1(int); virtual void mf2(); void fm3(); void fm3(double); }; class Derived : public Base { public: // 让Base内名为mf1和mf3的所有定义 // 在Derived作用域内可见 using Base::mf1; using Base::mf3; virtual void mf1(); void fm3(); void fm4(); }; ``` ![15-1](Images/15-1.png) 类内使用`using`声明改变访问级别的规则同样适用于重载函数的名字。 ## 构造函数与拷贝控制(Constructors and Copy Control) ### 虚析构函数(Virtual Destructors) 一般来说,如果一个类需要析构函数,那么它也需要拷贝和赋值操作。但基类的析构函数不遵循该规则。 基类通常应该定义一个虚析构函数。 ```c++ class Quote { public: // virtual destructor needed if a base pointer pointing to a derived object is deleted virtual ~Quote() = default; // dynamic binding for the destructor }; ``` 如果基类的析构函数不是虚函数,则`delete`一个指向派生类对象的基类指针会产生未定义的结果。 ```c++ Quote *itemP = new Quote; // same static and dynamic type delete itemP; // destructor for Quote called itemP = new Bulk_quote; // static and dynamic types differ delete itemP; // destructor for Bulk_quote called ``` 虚析构函数会阻止编译器为类合成移动操作。 ### 合成拷贝控制与继承(Synthesized Copy Control and Inheritance) 对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类直接基类的成员。 派生类中删除的拷贝控制与基类的关系: - 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的或者不可访问的函数,则派生类中对应的成员也会是被删除的。因为编译器不能使用基类成员来执行派生类对象中基类部分的构造、赋值或销毁操作。 - 如果基类的析构函数是被删除的或者不可访问的,则派生类中合成的默认和拷贝构造函数也会是被删除的。因为编译器无法销毁派生类对象中的基类部分。 - 编译器不会合成一个被删除的移动操作。当我们使用`=default`请求一个移动操作时,如果基类中对应的操作是被删除的或者不可访问的,则派生类中的操作也会是被删除的。因为派生类对象中的基类部分不能移动。同样,如果基类的析构函数是被删除的或者不可访问的,则派生类的移动构造函数也会是被删除的。 在实际编程中,如果基类没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作。 因为基类缺少移动操作会阻止编译器为派生类合成自己的移动操作,所以当我们确实需要执行移动操作时,应该首先在基类中进行定义。 ### 派生类的拷贝控制成员(Derived-Class Copy-Control Members) 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类成员在内的整个对象。 当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分。 ```c++ class Base { /* ... */ } ; class D: public Base { public: // by default, the base class default constructor initializes the base part of an object // to use the copy or move constructor, we must explicitly call that // constructor in the constructor initializer list D(const D& d): Base(d) // copy the base members /* initializers for members of D */ { /* ... */ } D(D&& d): Base(std::move(d)) // move the base members /* initializers for members of D */ { /* ... */ } }; // probably incorrect definition of the D copy constructor // base-class part is default initialized, not copied D(const D& d) /* member initializers, but no base-class initializer */ { /* ... */ } ``` 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝或移动基类部分,则必须在派生类的构造函数初始化列表中显式地使用基类的拷贝或移动构造函数。 派生类的赋值运算符必须显式地为其基类部分赋值。 ```c++ // Base::operator=(const Base&) is not invoked automatically D &D::operator=(const D &rhs) { Base::operator=(rhs); // assigns the base part // assign the members in the derived class, as usual, // handling self-assignment and freeing existing resources as appropriate return *this; } ``` 派生类的析构函数只负责销毁派生类自己分配的资源。 ```c++ class D: public Base { public: // Base::~Base invoked automatically ~D() { /* do what it takes to clean up derived members */ } }; ``` 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。 ### 继承的构造函数(Inherited Constructors) C++11新标准允许派生类重用(非常规方式继承)其直接基类定义的构造函数。继承方式是提供一条注明了直接基类名的`using`声明语句。 ```c++ class Bulk_quote : public Disc_quote { public: using Disc_quote::Disc_quote; // inherit Disc_quote's constructors double net_price(std::size_t) const; }; ``` 通常情况下,`using`声明语句只是令某个名字在当前作用域内可见。而作用于构造函数时,`using`声明将令编译器产生代码。对于基类的每个构造函数,编译器都会生成一个与其形参列表完全相同的派生类构造函数。如果派生类含有自己的数据成员,则这些成员会被默认初始化。 构造函数的`using`声明不会改变该函数的访问级别,不能指定`explicit`或`constexpr`属性。 定义在派生类中的构造函数会替换继承而来的具有相同形参列表的构造函数。 派生类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器会为其合成它们。 当一个基类构造函数含有默认实参时,这些默认值不会被继承。相反,派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认值的形参。 ## 容器与继承(Containers and Inheritance) 因为容器中不能保存不同类型的元素,所以不能把具有继承关系的多种类型的对象直接存储在容器中。 容器不能和存在继承关系的类型兼容。 如果想在容器中存储具有继承关系的对象,则应该存放基类的指针。 # 第16章 模板与泛型编程 ## 定义模板(Defining a Template) ### 函数模板(Function Templates) 函数模板可以用来生成针对特定类型的函数版本。 模板定义以关键字`template`开始,后跟一个模板参数列表(template parameter list)。模板参数列表以尖括号`<>`包围,内含用逗号分隔的一个或多个模板参数(template parameter)。 ```c++ template int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; } ``` 定义模板时,模板参数列表不能为空。 模板参数表示在类或函数定义中用到的类型或值。当使用模板时,需要显式或隐式地指定模板实参(template argument),并将其绑定到模板参数上。 使用函数模板时,编译器用推断出的模板参数来实例化(instantiate)一个特定版本的函数,这些生成的函数通常被称为模板的实例(instantiation)。 ```c++ // instantiates int compare(const int&, const int&) cout << compare(1, 0) << endl; // T is int // instantiates int compare(const vector&, const vector&) vector vec1{1, 2, 3}, vec2{4, 5, 6}; cout << compare(vec1, vec2) << endl; // T is vector ``` 模板类型参数(type parameter)可以用来指定函数的返回类型或参数类型,以及在函数体内用于变量声明和类型转换。类型参数前必须使用关键字`class`或`typename`。 ```c++ // ok: same type used for the return type and parameter template T foo(T* p) { T tmp = *p; // tmp will have the type to which p points // ... return tmp; } // error: must precede U with either typename or class template T calc(const T&, const U&); // ok: no distinction between typename and class in a template parameter list template calc (const T&, const U&); ``` 建议使用`typename`而不是`class`来指定模板类型参数,这样更加直观。 模板非类型参数(nontype parameter)需要用特定的类型名来指定,表示一个值而非一个类型。非类型参数可以是整型、指向对象或函数类型的指针或左值引用。 ```c++ template int compare(const char (&p1)[N], const char (&p2)[M]) { return strcmp(p1, p2); } int compare(const char (&p1)[3], const char (&p2)[4]); ``` 绑定到整型非类型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期,不能用普通局部变量或动态对象作为指针或引用非类型参数的实参。 函数模板也可以声明为`inline`或`constexpr`的,说明符放在模板参数列表之后,返回类型之前。 ```c++ // ok: inline specifier follows the template parameter list template inline T min(const T&, const T&); // error: incorrect placement of the inline specifier inline template T min(const T&, const T&); ``` 模板程序应该尽量减少对实参类型的要求。 ```c++ // expected comparison if (v1 < v2) return -1; if (v1 > v2) return 1; return 0; // version of compare that will be correct even if used on pointers template int compare(const T &v1, const T &v2) { if (less()(v1, v2)) return -1; if (less()(v2, v1)) return 1; return 0; } ``` 只有当模板的一个特定版本被实例化时,编译器才会生成代码。此时编译器需要掌握生成代码所需的信息,因此函数模板和类模板成员函数的定义通常放在头文件中。 使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的设计者来保证的。模板设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。 调用者负责保证传递给模板的实参能正确支持模板所要求的操作。 ### 类模板(Class Templates) 使用一个类模板时,必须提供显式模板实参(explicit template argument)列表,编译器使用这些模板实参来实例化出特定的类。 ```c++ template class Blob { public: Blob(); Blob(std::initializer_list il); void push_back(const T &t) { data->push_back(t); } void push_back(T &&t) { data->push_back(std::move(t)); } // ... private: std::shared_ptr> data; }; Blob ia; // empty Blob Blob ia2 = { 0, 1, 2, 3, 4 }; // Blob with five elements // these definitions instantiate two distinct Blob types Blob names; // Blob that holds strings Blob prices; // different element type ``` 一个类模板的每个实例都形成一个独立的类,相互之间没有关联。 如果一个类模板中的代码使用了另一个模板,通常不会将一个实际类型(或值)的名字用作其模板实参,而是将模板自己的参数用作被使用模板的实参。 类模板的成员函数具有和类模板相同的模板参数,因此定义在类模板外的成员函数必须以关键字`template`开始,后跟类模板参数列表。 ```c++ template ret-type Blob::member-name(parm-list) ``` 默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。 在类模板自己的作用域内,可以直接使用模板名而不用提供模板实参。 ```c++ template class BlobPtr { public: // 类模板作用域内不需要写成BlobPtr形式 BlobPtr& operator++(); } // 类外定义时需要提供模板实参 template BlobPtr& BlobPtr::operator++() { // 进入类模板作用域 BlobPtr Ret = *this; } ``` 当一个类包含一个友元声明时,类与友元各自是否是模板并无关联。如果一个类模板包含一个非模板友元,则友元可以访问所有类模板实例。如果友元自身是模板,则类可以给所有友元模板实例授予访问权限,也可以只授权给特定实例。 - 一对一友元关系 为了引用模板的一个特定实例,必须首先声明模板自身。模板声明包括模板参数列表。 ```c++ // forward declarations needed for friend declarations in Blob template class BlobPtr; template class Blob; // needed for parameters in operator== template bool operator==(const Blob&, const Blob&); template class Blob { // each instantiation of Blob grants access to the version of // BlobPtr and the equality operator instantiated with the same type friend class BlobPtr; friend bool operator==(const Blob&, const Blob&); }; ``` - 通用和特定的模板友元关系 为了让模板的所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。 ```c++ // forward declaration necessary to befriend a specific instantiation of a template template class Pal; class C { // C is an ordinary, nontemplate class friend class Pal; // Pal instantiated with class C is a friend to C // all instances of Pal2 are friends to C; // no forward declaration required when we befriend all instantiations template friend class Pal2; }; template class C2 { // C2 is itself a class template // each instantiation of C2 has the same instance of Pal as a friend friend class Pal; // a template declaration for Pal must be in scope // all instances of Pal2 are friends of each instance of C2, prior declaration needed template friend class Pal2; // Pal3 is a nontemplate class that is a friend of every instance of C2 friend class Pal3; // prior declaration for Pal3 not needed }; ``` C++11中,类模板可以将模板类型参数声明为友元。 ```c++ template class Bar { friend Type; // grants access to the type used to instantiate Bar // ... }; ``` C++11允许使用`using`为类模板定义类型别名。 ```c++ template using twin = pair; twin authors; // authors is a pair ``` 类模板可以声明`static`成员。 ```c++ template class Foo { public: static std::size_t count() { return ctr; } private: static std::size_t ctr; }; // instantiates static members Foo::ctr and Foo::count Foo fs; // all three objects share the same Foo::ctr and Foo::count members Foo fi, fi2, fi3; ``` 类模板的每个实例都有一个独有的`static`对象,而每个`static`成员必须有且只有一个定义。因此与定义模板的成员函数类似,`static`成员也应该定义成模板。 ```c++ template size_t Foo::ctr = 0; // define and initialize ctr ``` ### 模板参数(Template Parameters) 模板参数遵循普通的作用域规则。与其他任何名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是在模板内不能重用模板参数名。 ```c++ typedef double A; template void f(A a, B b) { A tmp = a; // tmp has same type as the template parameter A, not double double B; // error: redeclares template parameter B } ``` 由于模板参数名不能重用,所以一个名字在一个特定模板参数列表中只能出现一次。 与函数参数一样,声明中模板参数的名字不必与定义中的相同。 一个特定文件所需要的所有模板声明通常一起放置在文件开始位置,出现在任何使用这些模板的代码之前。 模板中的代码使用作用域运算符`::`时,编译器无法确定其访问的名字是类型还是`static`成员。 默认情况下,C++假定模板中通过作用域运算符访问的名字是`static`成员。因此,如果需要使用一个模板类型参数的类型成员,就必须使用关键字`typename`显式地告知编译器该名字是一个类型。 ```c++ template typename T::value_type top(const T& c) { if (!c.empty()) return c.back(); else return typename T::value_type(); } ``` C++11允许为函数和类模板提供默认实参。 ```c++ // compare has a default template argument, less // and a default function argument, F() template > int compare(const T &v1, const T &v2, F f = F()) { if (f(v1, v2)) return -1; if (f(v2, v1)) return 1; return 0; } ``` 如果一个类模板为其所有模板参数都提供了默认实参,在使用这些默认实参时,必须在模板名后面跟一个空尖括号对`<>`。 ```c++ template class Numbers { // by default T is int public: Numbers(T v = 0): val(v) { } // various operations on numbers private: T val; }; Numbers lots_of_precision; Numbers<> average_precision; // empty <> says we want the default type ``` ### 成员模板(Member Templates) 一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数,这种成员被称为成员模板。成员模板不能是虚函数。 ```c++ class DebugDelete { public: DebugDelete(std::ostream &s = std::cerr): os(s) { } // as with any function template, the type of T is deduced by the compiler template void operator()(T *p) const { os << "deleting unique_ptr" << std::endl; delete p; } private: std::ostream &os; }; ``` 在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。 ```c++ template class Blob { template Blob(It b, It e); }; template // type parameter for the class template // type parameter for the constructor Blob::Blob(It b, It e): data(std::make_shared>(b, e)) { } ``` 为了实例化一个类模板的成员模板,必须同时提供类和函数模板的实参。 ### 控制实例化(Controlling Instantiations) 因为模板在使用时才会进行实例化,所以相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。 在大型程序中,多个文件实例化相同模板的额外开销可能非常严重。C++11允许通过显式实例化(explicit instantiation)来避免这种开销。 显式实例化的形式如下: ```c++ extern template declaration; // instantiation declaration template declaration; // instantiation definition ``` *declaration*是一个类或函数声明,其中所有模板参数已被替换为模板实参。当编译器遇到`extern`模板声明时,它不会在本文件中生成实例化代码。对于一个给定的实例化版本,可能有多个`extern`声明,但必须只有一个定义。 ```c++ // templateBuild.cc // instantiation file must provide a (nonextern) definition for every // type and function that other files declare as extern template int compare(const int&, const int&); template class Blob; // instantiates all members of the class template // Application.cc // these template types must be instantiated elsewhere in the program extern template class Blob; extern template int compare(const int&, const int&); Blob sa1, sa2; // instantiation will appear elsewhere // Blob and its initializer_list constructor instantiated in this file Blob a1 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Blob a2(a1); // copy constructor instantiated in this file int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere ``` 当编译器遇到类模板的实例化定义时,它不清楚程序会使用哪些成员函数。和处理类模板的普通实例化不同,编译器会实例化该模板的所有成员,包括内联的成员函数。因此,用来显式实例化类模板的类型必须能用于模板的所有成员。 ### 效率与灵活性(Efficiency and Flexibility) `unique_ptr`在编译时绑定删除器,避免了间接调用删除器的运行时开销。`shared_ptr`在运行时绑定删除器,使用户重载删除器的操作更加简便。 ## 模板实参推断(Template Argument Deduction) 对于函数模板,编译器通过调用的函数实参来确定其模板参数。这个过程被称作模板实参推断。 ### 类型转换与模板类型参数(Conversions and Template Type Parameters) 与非模板函数一样,调用函数模板时传递的实参被用来初始化函数的形参。如果一个函数形参的类型使用了模板类型参数,则会采用特殊的初始化规则,只有有限的几种类型转换会自动地应用于这些实参。编译器通常会生成新的模板实例而不是对实参进行类型转换。 有3种类型转换可以在调用中应用于函数模板: - 顶层`const`会被忽略。 - 可以将一个非`const`对象的引用或指针传递给一个`const`引用或指针形参。 - 如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。数组实参可以转换为指向其首元素的指针。函数实参可以转换为该函数类型的指针。 其他的类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。 一个模板类型参数可以作为多个函数形参的类型。由于允许的类型转换有限,因此传递给这些形参的实参必须具有相同的类型,否则调用失败。 ```c++ long lng; compare(lng, 1024); // error: cannot instantiate compare(long, int) ``` 如果想增强函数的兼容性,可以使用两个类型参数定义函数模板。 ```c++ // argument types can differ but must be compatible template int flexibleCompare(const A& v1, const B& v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; } long lng; flexibleCompare(lng, 1024); // ok: calls flexibleCompare(long, int) ``` 函数模板中使用普通类型定义的参数可以进行正常的类型转换。 ```c++ template ostream &print(ostream &os, const T &obj) { return os << obj; } print(cout, 42); // instantiates print(ostream&, int) ofstream f("output"); print(f, 10); // uses print(ostream&, int); converts f to ostream& ``` ### 函数模板显式实参(Function-Template Explicit Arguments) 某些情况下,编译器无法推断出模板实参的类型。 ```c++ // T1 cannot be deduced: it doesn't appear in the function parameter list template T1 sum(T2, T3); ``` 显式模板实参(explicit template argument)可以让用户自己控制模板的实例化。提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号`<>`中指定,位于函数名之后,实参列表之前。 ```c++ // T1 is explicitly specified; T2 and T3 are inferred from the argument types auto val3 = sum(i, lng); // long long sum(int, long) ``` 显式模板实参按照从左到右的顺序与对应的模板参数匹配,只有尾部参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。 ```c++ // poor design: users must explicitly specify all three template parameters template T3 alternative_sum(T2, T1); // error: can't infer initial template parameters auto val3 = alternative_sum(i, lng); // ok: all three parameters are explicitly specified auto val2 = alternative_sum(i, lng); ``` 对于模板类型参数已经显式指定了的函数实参,可以进行正常的类型转换。 ```c++ long lng; compare(lng, 1024); // error: template parameters don't match compare(lng, 1024); // ok: instantiates compare(long, long) compare(lng, 1024); // ok: instantiates compare(int, int) ``` ### 尾置返回类型与类型转换(Trailing Return Types and Type Transformation) 由于尾置返回出现在函数列表之后,因此它可以使用函数参数来声明返回类型。 ```c++ // a trailing return lets us declare the return type after the parameter list is seen template auto fcn(It beg, It end) -> decltype(*beg) { // process the range return *beg; // return a reference to an element from the range } ``` 标准库在头文件*type_traits*中定义了类型转换模板,这些模板常用于模板元程序设计。其中每个模板都有一个名为`type`的公有类型成员,表示一个类型。此类型与模板自身的模板类型参数相关。如果不可能(或不必要)转换模板参数,则`type`成员就是模板参数类型本身。 ![16-1](Images/16-1.png) 使用`remove_reference`可以获得引用对象的元素类型,如果用一个引用类型实例化`remove_reference`,则`type`表示被引用的类型。因为`type`是一个类的类型成员,所以在模板中必须使用关键字`typename`来告知编译器其表示一个类型。 ```c++ // must use typename to use a type member of a template parameter template auto fcn2(It beg, It end) -> typename remove_reference::type { // process the range return *beg; // return a copy of an element from the range } ``` ### 函数指针和实参推断(Function Pointers and Argument Deduction) 使用函数模板初始化函数指针或为函数指针赋值时,编译器用指针的类型来推断模板实参。 ```c++ template int compare(const T&, const T&); // pf1 points to the instantiation int compare(const int&, const int&) int (*pf1)(const int&, const int&) = compare; ``` 如果编译器不能从函数指针类型确定模板实参,则会产生错误。使用显式模板实参可以消除调用歧义。 ```c++ // overloaded versions of func; each takes a different function pointer type void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); func(compare); // error: which instantiation of compare? // ok: explicitly specify which version of compare to instantiate func(compare); // passing compare(const int&, const int&) ``` ### 模板实参推断和引用(Template Argument Deduction and References) 当一个函数参数是模板类型参数的普通(左值)引用(形如`T&`)时,只能传递给它一个左值(如一个变量或一个返回引用类型的表达式)。*T*被推断为实参所引用的类型,如果实参是`const`的,则*T*也为`const`类型。 ```c++ template void f1(T&); // argument must be an lvalue // calls to f1 use the referred-to type of the argument as the template parameter type f1(i); // i is an int; template parameter T is int f1(ci); // ci is a const int; template parameter T is const int f1(5); // error: argument to a & parameter must be an lvalue ``` 当一个函数参数是模板类型参数的常量引用(形如`const T&`)时,可以传递给它任何类型的实参。函数参数本身是`const`时,*T*的类型推断结果不会是`const`类型。`const`已经是函数参数类型的一部分了,因此不会再是模板参数类型的一部分。 ```c++ template void f2(const T&); // can take an rvalue // parameter in f2 is const &; const in the argument is irrelevant // in each of these three calls, f2's function parameter is inferred as const int& f2(i); // i is an int; template parameter T is int f2(ci); // ci is a const int, but template parameter T is int f2(5); // a const & parameter can be bound to an rvalue; T is int ``` 当一个函数参数是模板类型参数的右值引用(形如`T&&`)时,如果传递给它一个右值,类型推断过程类似普通左值引用函数参数的推断过程,推断出的*T*类型是该右值实参的类型。 ```c++ template void f3(T&&); f3(42); // argument is an rvalue of type int; template parameter T is int ``` 模板参数绑定的两个例外规则: - 如果将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。 - 如果间接创建了一个引用的引用(通过类型别名或者模板类型参数间接定义),则这些引用会被“折叠”。右值引用的右值引用会被折叠为右值引用。其他情况下,引用都被折叠为普通左值引用。 | 折叠前 | 折叠后 | | :----------------------: | :----: | | `T& &`、`T& &&`、`T&& &` | `T&` | | `T&& &&` | `T&&` | ```c++ f3(i); // argument is an lvalue; template parameter T is int& f3(ci); // argument is an lvalue; template parameter T is const int& // invalid code, for illustration purposes only void f3(int& &&); // when T is int&, function parameter is int& && void f3(int&); // when T is int&, function parameter collapses to int& ``` 模板参数绑定的两个例外规则导致了两个结果: - 如果一个函数参数是指向模板类型参数的右值引用,则可以传递给它任意类型的实参。 - 如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。 当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难。 ```c++ template void f3(T&& val) { T t = val; // copy or binding a reference? t = fcn(t); // does the assignment change only t or val and t? if (val == t) { /* ... */ } // always true if T is a reference type } ``` 实际编程中,模板的右值引用参数通常用于两种情况:模板转发其实参或者模板被重载。函数模板的常用重载形式如下: ```c++ template void f(T&&); // binds to nonconst rvalues template void f(const T&); // lvalues and const rvalues ``` ### 理解std::move(Understanding std::move) `std::move`的定义如下: ```c++ template typename remove_reference::type&& move(T&& t) { return static_cast::type&&>(t); } ``` `std::move`的工作过程: ```c++ string s1("hi!"), s2; s2 = std::move(string("bye!")); // ok: moving from an rvalue s2 = std::move(s1); // ok: but after the assigment s1 has indeterminate value ``` - 在`std::move(string("bye!"))`中传递的是右值。 - - 推断出的*T*类型为`string`。 - `remove_reference`用`string`进行实例化。 - `remove_reference`的`type`成员是`string`。 - `move`的返回类型是`string&&`。 - `move`的函数参数*t*的类型为`string&&`。 - 在`std::move(s1)`中传递的是左值。 - - 推断出的*T*类型为`string&`。 - `remove_reference`用`string&`进行实例化。 - `remove_reference`的`type`成员是`string`。 - `move`的返回类型是`string&&`。 - `move`的函数参数t的类型为`string& &&`,会折叠成`string&`。 可以使用`static_cast`显式地将一个左值转换为一个右值引用。 ### 转发(Forwarding) 某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在这种情况下,需要保持被转发实参的所有性质,包括实参的`const`属性以及左值/右值属性。 ```c++ // template that takes a callable and two parameters // and calls the given callable with the parameters ''flipped'' // flip1 is an incomplete implementation: top-level const and references are lost template void flip1(F f, T1 t1, T2 t2) { f(t2, t1); } void f(int v1, int &v2) // note v2 is a reference { cout << v1 << " " << ++v2 << endl; } f(42, i); // f changes its argument i flip1(f, j, 42); // f called through flip1 leaves j unchanged // void flip1(void(*fcn)(int, int&), int t1, int t2) ``` 上例中,*j*被传递给`flip1`的参数*t1*,该参数是一个普通(非引用)类型`int`,而非`int&`,因此`flip1(f, j, 42)`调用会被实例化为`void flip1(void(*fcn)(int, int&), int t1, int t2)`。*j*的值被拷贝至*t1*中,`f`中的引用参数被绑定至*t1*,而非*j*,因此*j*不会被修改。 将函数参数定义为指向模板类型参数的右值引用(形如`T&&`),通过引用折叠,可以保持翻转实参的左值/右值属性。并且引用参数(无论是左值还是右值)可以保持实参的`const`属性,因为在引用类型中的`const`是底层的。 ```c++ template void flip2(F f, T1 &&t1, T2 &&t2) { f(t2, t1); } ``` 对于修改后的版本,若调用`flip2(f, j, 42)`,会传递给参数*t1*一个左值*j*,但此时推断出的*T1*类型为`int&`,*t1*的类型会被折叠为`int&`,从而解决了`flip1`的错误。 但`flip2`只能用于接受左值引用的函数,不能用于接受右值引用的函数。函数参数与其他变量一样,都是左值表达式。所以即使是指向模板类型的右值引用参数也只能传递给接受左值引用的函数,不能传递给接受右值引用的函数。 ```c++ void g(int &&i, int& j) { cout << i << " " << j << endl; } // error: can't initialize int&& from an lvalue flip2(g, i, 42); // flip2 passes an lvalue to g’s rvalue reference parameter ``` C++11在头文件*utility*中定义了`forward`。与`move`不同,`forward`必须通过显式模板实参调用,返回该显式实参类型的右值引用。即`forward`返回类型`T&&`。 通常情况下,可以使用`forward`传递定义为指向模板类型参数的右值引用函数参数。通过其返回类型上的引用折叠,`forward`可以保持给定实参的左值/右值属性。 ```c++ template intermediary(Type &&arg) { finalFcn(std::forward(arg)); // ... } ``` - 如果实参是一个右值,则*Type*是一个普通(非引用)类型,`forward`返回类型`Type&&`。 - 如果实参是一个左值,则通过引用折叠,*Type*也是一个左值引用类型,`forward`返回类型`Type&& &`,对返回类型进行引用折叠��得到`Type&`。 使用`forward`编写完善的转发函数。 ```c++ template void flip(F f, T1 &&t1, T2 &&t2) { f(std::forward(t2), std::forward(t1)); } ``` 与`std::move`一样,对`std::forward`也不应该使用`using`声明。 ## 重载与模板(Overloading and Templates) 函数模板可以被另一个模板或普通非模板函数重载。 如果重载涉及函数模板,则函数匹配规则会受到一些影响: - 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。 - 候选的函数模板都是可行的,因为模板实参推断会排除任何不可行的模板。 - 和往常一样,可行函数(模板与非模板)按照类型转换(如果需要的话)来排序。但是可以用于函数模板调用的类型转换非常有限。 - 和往常一样,如果恰有一个函数提供比其他任何函数都更好的匹配,则选择此函数。但是如果多个函数都提供相同级别的匹配,则: - 如果同级别的函数中只有一个是非模板函数,则选择此函数。 - 如果同级别的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。 - 否则该调用有歧义。 通常,如果使用了一个没有声明的函数,代码将无法编译。但对于重载函数模板的函数而言,如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不再重要了。 ```c++ template string debug_rep(const T &t); template string debug_rep(T *p); // the following declaration must be in scope // for the definition of debug_rep(char*) to do the right thing string debug_rep(const string &); string debug_rep(char *p) { // if the declaration for the version that takes a const string& is not in scope // the return will call debug_rep(const T&) with T instantiated to string return debug_rep(string(p)); } ``` 在定义任何函数之前,应该声明所有重载的函数版本。这样编译器就不会因为未遇到你希望调用的函数而实例化一个并非你所需要的版本。 ## 可变参数模板(Variadic Templates) 可变参数模板指可以接受可变数量参数的模板函数或模板类。可变数量的参数被称为参数包(parameter pack),分为两种: - 模板参数包(template parameter pack),表示零个或多个模板参数。 - 函数参数包(function parameter pack),表示零个或多个函数参数。 用一个省略号`…`来指出模板参数或函数参数表示一个包。在一个模板参数列表中,`class…`或`typename…`指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数列表。在函数参数列表中,如果一个参数的类型是模板参数包,则此参数也是函数参数包。 ```C++ // Args is a template parameter pack; rest is a function parameter pack // Args represents zero or more template type parameters // rest represents zero or more function parameters template void foo(const T &t, const Args& ... rest); ``` 对于一个可变参数模板,编译器会推断模板参数类型和参数数量。 可以使用`sizeof…`运算符获取参数包中的元素数量。类似`sizeof`,`sizeof…`也返回一个常量表达式,而且不会对其实参求值。 ```c++ template void g(Args ... args) { cout << sizeof...(Args) << endl; // number of type parameters cout << sizeof...(args) << endl; // number of function parameters } ``` ### 编写可变参数函数模板(Writing a Variadic Function Template) 可变参数函数通常是递归的,第一步调用参数包中的第一个实参,然后用剩余实参调用自身。为了终止递归,还需要定义一个非可变参数的函数。 ```c++ // function to end the recursion and print the last element // this function must be declared before the variadic version of print is defined template ostream &print(ostream &os, const T &t) { return os << t; // no separator after the last element in the pack } // this version of print will be called for all but the last element in the pack template ostream &print(ostream &os, const T &t, const Args&... rest) { os << t << ", "; // print the first argument return print(os, rest...); // recursive call; print the other arguments } ``` | Call | t | rest... | | :---------------------: | :--: | :-----: | | `print(cout, i, s, 42)` | i | s, 42 | | `print(cout, s, 42)` | s | 42 | | `print(cout, 42)` | | | ### 包扩展(Pack Expansion) 对于一个参数包,除了获取其大小外,唯一能对它做的事情就是扩展。当扩展一个包时,需要提供用于每个扩展元素的模式(pattern)。扩展一个包就是将其分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边添加一个省略号`…`来触发扩展操作。 包扩展工作过程: ```c++ template ostream& print(ostream &os, const T &t, const Args&... rest) // expand Args { os << t << ", "; return print(os, rest...); // expand rest } ``` - 第一个扩展操作扩展模板参数包,为`print`生成函数参数列表。编译器将模式`const Args&`应用到模板参数包*Args*中的每个元素上。因此该模式的扩展结果是一个以逗号分隔的零个或多个类型的列表,每个类型都形如`const type&`。 ```c++ print(cout, i, s, 42); // two parameters in the pack ostream& print(ostream&, const int&, const string&, const int&); ``` - 第二个扩展操作扩展函数参数包,模式是函数参数包的名字。扩展结果是一个由包中元素组成、以逗号分隔的列表。 ```c++ print(os, s, 42); ``` 扩展操作中的模式会独立地应用于包中的每个元素。 ```c++ // call debug_rep on each argument in the call to print template ostream &errorMsg(ostream &os, const Args&... rest) { // print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an) return print(os, debug_rep(rest)...); } // passes the pack to debug_rep; print(os, debug_rep(a1, a2, ..., an)) print(os, debug_rep(rest...)); // error: no matching function to call ``` ### 转发参数包(Forwarding Parameter Packs) 在C++11中,可以组合使用可变参数模板和`forward`机制来编写函数,实现将其实参不变地传递给其他函数。 ```c++ // fun has zero or more parameters each of which is // an rvalue reference to a template parameter type template void fun(Args&&... args) // expands Args as a list of rvalue references { // the argument to work expands both Args and args work(std::forward(args)...); } ``` ## 模板特例化(Template Specializations) 在某些情况下,通用模板的定义对特定类型是不合适的,可能编译失败或者操作不正确。如果不希望或不能使用模板版本时,可以定义类或函数模板的特例化版本。一个特例化版本就是模板的一个独立定义,其中的一个或多个模板参数被指定为特定类型。 ```c++ // first version; can compare any two types template int compare(const T&, const T&); // second version to handle string literals template int compare(const char (&)[N], const char (&)[M]); const char *p1 = "hi", *p2 = "mom"; compare(p1, p2); // calls the first template compare("hi", "mom"); // calls the template with two nontype parameters // special version of compare to handle pointers to character arrays template <> int compare(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); } ``` 特例化一个函数模板时,必须为模板中的每个模板参数都提供实参。为了指明我们正在实例化一个模板,应该在关键字`template`后面添加一个空尖括号对`<>`。 特例化版本的参数类型必须与一个先前声明的模板中对应的类型相匹配。 定义特例化函数版本本质上是接管编译器的工作,为模板的一个特殊实例提供了定义。特例化并非重载,因此不影响函数匹配。 将一个特殊版本的函数定义为特例化模板还是独立的非模板函数会影响到重载函数匹配。 模板特例化遵循普通作用域规则。为了特例化一个模板,原模板的声明必须在作用域中。而使用模板实例时,也必须先包含特例化版本的声明。 通常,模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明放在文件开头,后面是这些模板的特例化版本。 类模板也可以特例化。与函数模板不同,类模板的特例化不必为所有模板参数提供实参,可以只指定一部分模板参数。一个类模板的部分特例化(partial specialization)版本本身还是一个模板,用户使用时必须为那些未指定的模板参数提供实参。 只能部分特例化类模板,不能部分特例化函数模板。 由于类模板的部分特例化版本是一个模板,所以需要定义模板参数。对于每个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。在类名之后,需要为特例化的模板参数指定实参,这些实参位于模板名之后的尖括号中,与原始模板中的参数按位置相对应。 ```c++ // 通用版本 template struct remove_reference { typedef T type; }; // 部分特例化版本 template struct remove_reference // 左值引用 { typedef T type; }; template struct remove_reference // 右值引用 { typedef T type; }; ``` 类模板部分特例化版本的模板参数列表是原始模板参数列表的一个子集或特例化版本。 可以只特例化类模板的指定成员函数,而不用特例化整个模板。 ```c++ template struct Foo { Foo(const T &t = T()): mem(t) { } void Bar() { /* ... */ } T mem; // other members of Foo }; template<> // we're specializing a template void Foo::Bar() // we're specializing the Bar member of Foo { // do whatever specialized processing that applies to ints } Foo fs; // instantiates Foo::Foo() fs.Bar(); // instantiates Foo::Bar() Foo fi; // instantiates Foo::Foo() fi.Bar(); // uses our specialization of Foo::Bar() ``` # 第17章 标准库特殊设施 ## tuple类型(The tuple Type) `tuple`是类似`pair`的模板,定义在头文件*tuple*中。与`pair`不同,`tuple`可以有任意数量的成员。如果希望将一些数据组合成单一对象,但又不想定义新数据结构时,可以使用`tuple`(“快速而随意”的数据结构)。 ![17-1](Images/17-1.png) ### 定义和初始化tuple(Defining and Initializing tuples) 定义`tuple`时需要指定每个成员的类型。创建`tuple`对象时,可以使用`tuple`的默认构造函数,它会对每个成员进行值初始化。或者给每个成员提供初始值。包含初始值的构造函数是`explicit`的,因此必须使用直接初始化语法。 ```c++ tuple threeD = { 1, 2, 3 }; // error tuple threeD{ 1, 2, 3 }; // ok ``` 类似`make_pair`,`make_tuple`函数可以生成`tuple`对象。`tuple`的类型由初始值决定。 ```c++ // tuple that represents a bookstore transaction: ISBN, count, price per book auto item = make_tuple("0-999-78345-X", 3, 20.00); ``` 可以使用`get`访问`tuple`的成员。`get`是一个函数模板,使用时必须指定一个显式模板实参,表示要访问的成员索引。传递给`get`一个`tuple`实参后,会返回其指定成员的引用。 ```c++ auto book = get<0>(item); // returns the first member of item auto cnt = get<1>(item); // returns the second member of item auto price = get<2>(item)/cnt; // returns the last member of item get<2>(item) *= 0.8; // apply 20% discount ``` 可以使用`tuple_size`和`tuple_element`这两个辅助类模板查询`tuple`成员的数量和类型。 - `tuple_size`通过一个`tuple`类型来初始化,它有一个名为`value`的静态公有数据成员,类型为`size_t`,表示给定`tuple`中成员的数量。 - `tuple_element`通过一个索引值(整型常量)和一个`tuple`类型来初始化,它有一个名为`type`的公有数据成员,表示给定`tuple`中指定成员的类型。 使用`decltype`可以确定一个对象的类型。 ```c++ typedef decltype(item) trans; // trans is the type of item // returns the number of members in object's of type trans size_t sz = tuple_size::value; // returns 3 // cnt has the same type as the second member in item tuple_element<1, trans>::type cnt = get<1>(item); // cnt is an int ``` `tuple`的关系和相等运算符逐对比较两个`tuple`对象的成员。只有当两个`tuple`的成员数量相等时才可以进行比较。使用`tuple`的相等或不等运算符时,每对成员必须支持`==`运算符;使用`tuple`的关系运算符时,每对成员必须支持`<`运算符。 由于`tuple`定义了`<`和`==`运算符,因此`tuple`序列可以被传递给算法,无序容器的关键字也可以使用`tuple`类型。 ### 使用tuple返回多个值(Using a tuple to Return Multiple Values) `tuple`的一个常见用途是从一个函数返回多个值。 ## bitset类型(The bitset Type) 标准库在头文件*bitset*中定义了`bitset`类,用于处理二进制位。`bitset`可以处理超过最长整型类型大小的位集合。 ### 定义和初始化bitset(Defining and Initializing bitsets) `bitset`类是一个模板,类似`array`,具有固定的大小。定义一个`bitset`时需要指明它包含的二进制位数。 ![17-2](Images/17-2.png) 使用一个整型值初始化`bitset`时,此值会被转换为`unsigned long long`类型并被当作位模式处理。`bitset`中的二进制位就是此模式的副本。如果`bitset`的大小大于`unsigned long long`中的二进制位数,剩余的高位会被置为0。如果`bitset`的大小小于`unsigned long long`中的二进制位数,则只使用给定值的低位部分。 ```c++ // bitvec1 is smaller than the initializer; high-order bits from the initializer are discarded bitset<13> bitvec1 (0xbeef); // bits are 1111011101111 // bitvec2 is larger than the initializer; high-order bits in bitvec2 are set to zero bitset<20> bitvec2(0xbeef); // bits are 00001011111011101111 // on machines with 64-bit long long 0ULL is 64 bits of 0, so ~0ULL is 64 ones bitset<128> bitvec3(~0ULL); // bits 0 ... 63 are one; 63 ... 127 are zero ``` 可以使用`string`或字符数组指针来初始化`bitset`,字符直接表示位模式。使用字符串表示数时,字符串中下标最小的字符对应`bitset`的高位。如果`string`包含的字符数比`bitset`少,则`bitset`的高位被置为0。 ```c++ bitset<32> bitvec4("1100"); // bits 2 and 3 are 1, all others are 0 string str("1111111000000011001101"); bitset<32> bitvec5(str, 5, 4); // four bits starting at str[5], 1100 bitset<32> bitvec6(str, str.size()-4); // use last four characters ``` ![17-3](Images/17-3.png) ### bitset操作(Operations on bitsets) `bitset`操作: ![17-4](Images/17-4.png) `bitset`的下标运算符对`const`属性进行了重载。`const`版本的下标运算符在指定位置置位时返回`true`,否则返回`false`。非`const`版本返回`bitset`定义的一个特殊类型,用来控制指定位置的值。 `to_ulong`和`to_ullong`操作用来提取`bitset`的值。只有当`bitset`的大小不大于对应操作的返回值(`to_ulong`为`unsigned long`,`to_ullong`为`unsigned long long`)时,才能使用这两个操作。如果`bitset`中的值不能存入给定类型,则会引发`overflow_error`异常。 ```c++ unsigned long ulong = bitvec3.to_ulong(); cout << "ulong = " << ulong << endl; ``` `bitset`的输入运算符从输入流读取字符,保存到临时的`string`对象中。遇到下列情况时停止读取: - 读取的字符数达到对应`bitset`的大小。 - 遇到不是1和0的字符。 - 遇到文件结尾。 - 输入出现错误。 读取结束后用临时`string`对象初始化`bitset`。如果读取的字符数小于`bitset`的大小,则`bitset`的高位被置为0。 ## 正则表达式(Regular Expressions) 正则表达式是一种描述字符序列的方法。C++11新标准增加了正则表达式库(RE库),定义在头文件*regex*中,包含多个组件。 ![17-5](Images/17-5.png) `regex`类表示一个正则表达式。 ```c++ // find the characters ei that follow a character other than c string pattern("[^c]ei"); // we want the whole word in which our pattern appears pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"; regex r(pattern); // construct a regex to find pattern smatch results; // define an object to hold the results of a search // define a string that has text that does and doesn't match pattern string test_str = "receipt freind theif receive"; // use r to find a match to pattern in test_str if (regex_search(test_str, results, r)) // if there is a match cout << results.str() << endl; // print the matching word ``` `regex_match`和`regex_search`函数确定一个给定的字符序列与一个`regex`是否匹配。如果整个输入序列与表达式匹配,则`regex_match`函数返回`true`;如果输入序列中的一个子串与表达式匹配,则`regex_search`函数返回`true`。这两个函数的其中一个重载版本接受一个类型为`smatch`的附加参数。如果匹配成功,函数会将匹配信息保存在给定的`smatch`对象中。二者的参数形式如下: ![17-6](Images/17-6.png) ### 使用正则表达式库(Using the Regular Expression Library) 默认情况下,`regex`使用的正则表达式语言是ECMAScript。 定义一个`regex`或者对一个`regex`调用`assign`为其赋新值时,可以指定一些标志来影响`regex`的操作。`ECMAScript`、`basic`、`extended`、`awk`、`grep`和`egrep`这六个标志指定编写正则表达式时所使用的语言。这六个标志中必须设置其中之一,且只能设置一个。默认情况下,`ECMAScript`标志被设置,`regex`会使用ECMA-262规范,这也是很多Web浏览器使用的正则表达式语言。 ![17-7](Images/17-7.png) 正则表达式的语法是否正确是在运行期间解析的。如果正则表达式存在错误,标准库会抛出类型为`regex_error`的异常。除了`what`操作外,`regex_error`还有一个名为`code`的成员,用来返回错误类型对应的数值编码。`code`返回的值是由具体实现定义的。RE库能抛出的标准错误如下,`code`返回对应错误的编号(从0开始)。 ![17-8](Images/17-8.png) 正则表达式在程序运行时才编译,这是一个非常慢的操作。因此构造一个`regex`对象或者给一个已经存在的`regex`赋值是很耗时间的。为了最小化这种开销,应该尽量避免创建不必要的`regex`。特别是在循环中使用正则表达式时,应该在循环体外部创建`regex`对象。 RE库为不同的输入序列都定义了对应的类型。使用时RE库类型必须与输入类型匹配。 - `regex`类保存`char`类型的正则表达式;`wregex`保存`wchar_t`类型的正则表达式。 - `smatch`表示`string`类型的输入序列;`cmatch`表示字符数组类型的输入序列;`wsmatch`表示`wstring`类型的输入序列;`wcmatch`表示宽字符数组类型的输入序列。 ![17-9](Images/17-9.png) ### 匹配与Regex迭代器类型(The Match and Regex Iterator Types) `regex`迭代器是一种迭代器适配器,它被绑定到一个输入序列和一个`regex`对象上,每种输入类型都有对应的迭代器类型。 `sregex_iterator`操作: ![17-10](Images/17-10.png) 以`sregex_iterator`为例,将`sregex_iterator`绑定到一个`string`和一个`regex`对象时,迭代器自动定位至给定`string`中的第一个匹配位置。即,`sregex_iterator`构造函数对给定`string`和`regex`调用`regex_search`。解引用迭代器时,返回最近一次搜索结果的`smatch`对象。递增迭代器时,它调用`regex_search`在输入`string`中查找下一个匹配位置。 ```c++ // find the characters ei that follow a character other than c string pattern("[^c]ei"); // we want the whole word in which our pattern appears pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"; regex r(pattern, regex::icase); // we'll ignore case in doing the match // it will repeatedly call regex_search to find all matches in file for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) cout << it->str() << endl; // matched word ``` ![17-11](Images/17-11.png) 匹配类型有两个名为`prefix`和`suffix`的成员,分别返回表示输入序列中当前匹配之前和之后部分的`ssub_match`对象。一个`ssub_match`对象有两个名为`str`和`length`的成员,分别返回匹配的`string`和该`string`的长度。 ```c++ // same for loop header as before for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) { auto pos = it->prefix().length(); // size of the prefix pos = pos > 40 ? pos - 40 : 0; // we want up to 40 characters cout << it->prefix().str().substr(pos) // last part of the prefix << "\n\t\t>>> " << it->str() << " <<<\n" // matched word << it->suffix().str().substr(0, 40) // first part of the suffix << endl; } ``` ![17-12](Images/17-12.png) `smatch`支持的操作: ![17-13](Images/17-13.png) ### 使用子表达式(Using Subexpressions) 正则表达式中的模式通常包含一个或多个子表达式。子表达式是模式的一部分,本身也有意义。正则表达式语法通常用括号表示子表达式。 ```c++ // r has two subexpressions: the first is the part of the file name before the period // the second is the file extension regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex::icase); ``` 匹配对象除了提供匹配整体的相关信息外,还可以用来访问模式中的每个子表达式。子匹配是按位置来访问的,第一个子匹配位置为0,表示整个模式对应的匹配,随后是每个子表达式对应的匹配。 子表达式的一个常见用途是验证必须匹配特定格式的数据,如电话号码和电子邮箱地址。 ECMAScript正则表达式语言的一些特性: - 模式`[[:alnum:]]`匹配任意字母。 - 符号`+`表示匹配一个或多个字符。 - 符号`*`表示匹配零个或多个字符。 - `\{d}`表示单个数字,`\{d}{n}`表示一个n个数字的序列。 - 在方括号中的字符集合表示匹配这些字符中的任意一个。 - 后接`?`的组件是可选的。 - 类似C++,ECMAScript使用反斜线进行转义。由于模式包含括号,而括号是ECMAScript中的特殊字符,因此需要用`\(`和`\)`来表示括号是模式的一部分。 因为反斜线`\`是C++中的特殊字符,所以在模式中使用`\`时,需要一个额外的反斜线进行转义。 子匹配操作: ![17-14](Images/17-14.png) ### 使用regex_replace(Using regex_replace) 正则表达式替换操作: ![17-15](Images/17-15.png) 标准库定义了用于在正则表达式替换过程中控制匹配或格式的标志。这些标志可以传递给`regex_search`、`regex_match`函数或者`smatch`类的`format`成员。匹配和格式化标志的类型为`match_flag_type`,定义在命名空间*regex_constants*中。由于*regex_constants*定义在*std*中,因此在使用这些名字时,需要同时加上两个命名空间的限定符。 ![17-16](Images/17-16.png) 默认情况下,`regex_replace`输出整个输入序列。未与正则表达式匹配的部分会原样输出,匹配的部分按照格式字符串指定的格式输出。使用`format_no_copy`标志可以只输出匹配部分。 ```c++ // generate just the phone numbers: use a new format string string fmt2 = "$2.$5.$7 "; // put space after the last number as a separator // tell regex_replace to copy only the text that it replaces cout << regex_replace(s, r, fmt2, format_no_copy) << endl; ``` ## 随机数(Random Numbers) 在新标准出现之前,C和C++都依赖于一个简单的C库函数`rand`来生成随机数。该函数生成均匀分布的伪随机整数,每个随机数的范围在0和一个系统相关的最大值(至少为32767)之间。 头文件*random*中的随机数库定义了一组类来解决`rand`函数的一些问题:随机数引擎类(random-number engines)可以生成`unsigned`随机数序列;随机数分布类(random-number distribution classes)使用引擎类生成指定类型、范围和概率分布的随机数。 ![17-17](Images/17-17.png) C++程序不应该使用`rand`函数,而应该使用`default_random_engine`类和恰当的分布类对象。 ### 随机数引擎和分布(Random-Number Engines and Distribution) 随机数引擎是函数对象类,定义了一个不接受参数的调用运算符,返回一个随机`unsigned`整数。调用一个随机数引擎对象可以生成原始随机数。 ```c++ default_random_engine e; // generates random unsigned integers for (size_t i = 0; i < 10; ++i) // e() "calls" the object to produce the next random number cout << e() << " "; ``` 标准库定义了多个随机数引擎类,区别在于性能和随机性质量。每个编译器都会指定其中一个作为`default_random_engine`类型,此类型一般具有最常用的特性。 随机数引擎操作: ![17-18](Images/17-18.png) 大多数情况下,随机数引擎的输出是不能直接使用的,因为生成的随机数范围通常与程序所需要的不符。 使用分布类对象可以得到指定范围的随机数。新标准库的`uniform_int_distribution`类型生成均匀分布的`unsigned`值。 ```c++ // uniformly distributed from 0 to 9 inclusive uniform_int_distribution u(0,9); default_random_engine e; // generates unsigned random integers for (size_t i = 0; i < 10; ++i) // u uses e as a source of numbers // each call returns a uniformly distributed value in the specified range cout << u(e) << " "; ``` 类似引擎类型,分布类型也是函数对象类。分布类型定义了一个接受一个随机数引擎参数的调用运算符。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布区间。 随机数发生器指分布对象和引擎对象的组合。 `rand`函数的生成范围在0到`RAND_MAX`之间,随机数引擎生成的`unsigned`整数在一个系统定义的范围内。一个引擎类型的范围可以通过调用该类型对象的`min`和`max`成员来获得。 即使随机数发生器生成的数看起来是随机的,但对于一个给定的发生器,每次运行程序时它都会返回相同的数值序列。 如果函数需要局部的随机数发生器,应该将其(包括引擎和分布对象)定义为`static`对象,这样随机数发生器就能在函数调用期间保持状态。否则每次调用函数都会生成相同的序列。 ```c++ // returns a vector of 100 uniformly distributed random numbers vector good_randVec() { // because engines and distributions retain state, they usually should be // defined as static so that new numbers are generated on each call static default_random_engine e; static uniform_int_distribution u(0,9); vector ret; for (size_t i = 0; i < 100; ++i) ret.push_back(u(e)); return ret; } ``` 通过为引擎提供一个种子(seed),可以让引擎在程序每次运行时生成不同的序列。种子是一个数值,引擎利用它从序列中的一个新位置重新开始生成随机数。 为引擎设置种子有两种方式: - 在创建对象时提供种子。 - 调用引擎的`seed`成员设置种子。 ```c++ default_random_engine e1; // uses the default seed default_random_engine e2(2147483646); // use the given seed value default_random_engine e3; // uses the default seed value e3.seed(32767); // call seed to set a new seed value ``` 选择种子的常用方法是调用系统函数`time`。该函数定义在头文件*ctime*中,返回从一个特定时刻到当前经过的秒数。`time`函数接受单个指针参数,指向用于写入时间的数据结构。如果指针为空,则函数简单地返回时间。 ```c++ default_random_engine e1(time(0)); // a somewhat random seed ``` 由于`time`函数返回以秒计算的时间,因此用`time`返回值作为种子的方式只适用于生成种子的间隔为秒级或更长时间的应用。另外如果程序作为一个自动过程的一部分反复运行,这种方式也会无效,可能多次使用的是相同的种子。 ### 其他随机数分布(Other Kinds of Distributions) 从`rand`函数获得随机浮点数的一个常用但不正确的方法是用`rand`的结果除以`RAND_MAX`。但因为随机整数的精度通常低于随机浮点数,所以使用这种方法时,有一些浮点值永远不会被生成。 使用新标准库的`uniform_real_distribution`类型可以获得随机浮点数。 ```c++ default_random_engine e; // generates unsigned random integers // uniformly distributed from 0 to 1 inclusive uniform_real_distribution u(0,1); for (size_t i = 0; i < 10; ++i) cout << u(e) << " "; ``` 分布类型操作: ![17-19](Images/17-19.png) 除了总是生成`bool`类型的`bernouilli_distribution`外,其他分布类型都是模板。每个模板都接受单个类型参数,指定分布生成的结果类型。 分布类型限制了可以作为模板类型的参数类型,一些模板只能生成浮点数,而其他模板只能生成整数。分布类型还定义了一个默认模板类型参数,整型分布的默认参数是`int`,浮点数分布的默认参数是`double`。使用默认类型时应该在模板名后使用空尖括号。 ```c++ // empty <> signify we want to use the default result type uniform_real_distribution<> u(0,1); // generates double by default ``` `bernouilli_distribution`类型是一个普通类,而非模板。该分布返回一个`bool`值,其中`true`的概率是一个常数,默认为0.5。 由于引擎会返回相同的随机数序列,因此需要在循环中使用引擎时,必须在循环体外定义引擎对象。否则每次循环都会创建新引擎,生成相同序列。同样,分布对象也需要保持运行状态,也必须在循环体外定义。 ## IO库再探(The IO Library Revisited) ### 格式化输入与输出(Formatted Input and Output) 除了条件状态外,每个`iostream`对象还维护着一个格式状态来控制IO格式化细节。 标准库定义了一组操纵符(manipulator)来修改流的格式状态。操纵符是一个函数或对象,会影响流的状态,并能作为输入和输出运算符的运算对象。类似输入和输出运算符,操纵符也返回它所处理的流对象。 操纵符用于两大类输出控制:控制数值的输出格式,控制补白的数量和位置。 操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。大多数改变格式状态的操纵符都是设置/复原成对的,一个操纵符用于设置新格式,另一个用于恢复正常格式。 默认情况下,`bool`值输出为1(`true`)或0(`false`)。对流使用`boolalpha`操纵符可以输出`true`或`false`,还原格式时使用`noboolalpha`操纵符。 ```c++ cout << "default bool values: " << true << " " << false << "\nalpha bool values: " << boolalpha << true << " " << false << endl; ``` 输出: ``` default bool values: 1 0 alpha bool values: true false ``` 默认情况下,整型值的输入输出使用十进制。可以使用`hex`、`oct`和`dec`操纵符将其改为十六进制、八进制或改回十进制。 ```c++ cout << "default: " << 20 << " " << 1024 << endl; cout << "octal: " << oct << 20 << " " << 1024 << endl; cout << "hex: " << hex << 20 << " " << 1024 << endl; cout << "decimal: " << dec << 20 << " " << 1024 << endl; ``` 输出: ``` default: 20 1024 octal: 24 2000 hex: 14 400 decimal: 20 1024 ``` `hex`、`oct`和`dec`操纵符只影响整型运算对象,浮点值的表示形式不受影响。 默认情况下,在输出数值时,没有可见的标识指出当前使用的进制模式。如果需要输出八进制或十六进制值,应该使用`showbase`操纵符。对流应用`showbase`后,在输出结果中会显示进制,显示模式和指定整型常量进制的规范相同。 - 前导`0x`表示十六进制。 - 前导`0`表示八进制。 - 无前导字符表示十进制。 还原格式时使用`noshowbase`操纵符。 ```c++ cout << showbase; // show the base when printing integral values cout << "default: " << 20 << " " << 1024 << endl; cout << "in octal: " << oct << 20 << " " << 1024 << endl; cout << "in hex: " << hex << 20 << " " << 1024 << endl; cout << "in decimal: " << dec << 20 << " " << 1024 << endl; cout << noshowbase; // reset the state of the stream ``` 输出: ``` default: 20 1024 in octal: 024 02000 in hex: 0x14 0x400 in decimal: 20 1024 ``` 默认情况下,十六进制值(包括前导字符)以小写格式输出。使用`uppercase`操纵符可以输出大写字母。还原格式时使用`nouppercase`操纵符。 ```c++ cout << uppercase << showbase << hex << "printed in hexadecimal: " << 20 << " " << 1024 << nouppercase << noshowbase << dec << endl; ``` 输出: ``` printed in hexadecimal: 0X14 0X400 ``` 浮点数的输出格式涉及三个方面: - 输出精度(即输出多少个数字)。 - 十六进制、定点十进制或者科学记数法形式输出。 - 没有小数部分的浮点值是否输出小数点。 默认情况下,浮点值按六位数字精度输出;如果浮点值没有小数部分,则不输出小数点;根据浮点数的值选择输出为定点十进制或科学计数法形式:非常大或非常小的值输出为科学记数法形式,其他值输出为定点十进制形式。 默认情况下,精度控制输出的数字总位数。输出时,浮点值按照当前精度四舍五入而非截断。 调用IO对象的`precision`成员或者使用`setprecision`操纵符可以改变精度。 - `precision`成员是重载的。一个版本接受一个`int`值,将精度设置为此值,并返回旧精度值。另一个版本不接受参数,直接返回当前精度值。 - `setprecision`操纵符接受一个参数来设置精度。 `setprecision`操纵符和其他接受参数的操纵符都定义在头文件*iomanip*中。 ```c++ // cout.precision reports the current precision value cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) << endl; // cout.precision(12) asks that 12 digits of precision be printed cout.precision(12); cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) << endl; // alternative way to set precision using the setprecision manipulator cout << setprecision(3); cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) << endl; ``` 输出: ``` Precision: 6, Value: 1.41421 Precision: 12, Value: 1.41421356237 Precision: 3, Value: 1.41 ``` 定义在头文件*iostream*中的操纵符: ![17-20](Images/17-20.png) 操纵符可以强制流使用科学记数法、定点十进制或十六进制形式输出浮点值。 - `scientific`使用科学记数法表示浮点值。 - `fixed`使用定点十进制表示浮点值。 - `hexfloat`(新标准库)使用十六进制表示浮点值。 - `defaultfloat`(新标准库)将流恢复到默认状态。 除非程序需要控制浮点数的表示方式,否则最好由标准库来选择计数法。 ```c++ cout << "default format: " << 100 * sqrt(2.0) << '\n' << "scientific: " << scientific << 100 * sqrt(2.0) << '\n' << "fixed decimal: " << fixed << 100 * sqrt(2.0) << '\n' << "hexadecimal: " << hexfloat << 100 * sqrt(2.0) << '\n' << "use defaults: " << defaultfloat << 100 * sqrt(2.0) << '\n'; ``` 输出: ``` default format: 141.421 scientific: 1.414214e+002 fixed decimal: 141.421356 hexadecimal: 0x1.1ad7bcp+7 use defaults: 141.421 ``` `scientific`、`fixed`和`hexfloat`操纵符会改变流的精度含义。执行这些操纵符后,精度控制的将是小数点后面的数字位数,而默认情况下控制的是数字总位数。 默认情况下,当浮点值的小数部分为0时,不显示小数点。使用`showpoint`操纵符可以强制输出小数点,`noshowpoint`操纵符还原默认行为。 ```c++ cout << 10.0 << endl; // prints 10 cout << showpoint << 10.0 // prints 10.0000 << noshowpoint << endl; // revert to default format for the decimal point ``` 按列输出时,通常需要非常精细地控制数据格式。 - `setw`指定下一个数字或字符串值的最小空间。 - `left`表示左对齐输出。 - `right`表示右对齐输出(默认格式)。 - `internal`控制负数的符号位置,它左对齐符号,右对齐值,中间空间用空格填充。 - `setfill`指定一个字符代替默认的空格进行补白。 `setw`类似`endl`,不改变输出流的内部状态,只影响下一次输出的大小。 ```c++ int i = -16; double d = 3.14159; // pad the first column to use a minimum of 12 positions in the output cout << "i: " << setw(12) << i << "next col" << '\n' << "d: " << setw(12) << d << "next col" << '\n'; // pad the first column and left-justify all columns cout << left << "i: " << setw(12) << i << "next col" << '\n' << "d: " << setw(12) << d << "next col" << '\n' << right; // restore normal justification // pad the first column and right-justify all columns cout << right << "i: " << setw(12) << i << "next col" << '\n' << "d: " << setw(12) << d << "next col" << '\n'; // pad the first column but put the padding internal to the field cout << internal << "i: " << setw(12) << i << "next col" << '\n' << "d: " << setw(12) << d << "next col" << '\n'; // pad the first column, using # as the pad character cout << setfill('#') << "i: " << setw(12) << i << "next col" << '\n' << "d: " << setw(12) << d << "next col" << '\n' << setfill(' '); // restore the normal pad character ``` 输出: ``` i: -16next col d: 3.14159next col i: -16 next col d: 3.14159 next col i: -16next col d: 3.14159next col i: - 16next col d: 3.14159next col i: -#########16next col d: #####3.14159next col ``` 头文件*iomanip*中定义的操纵符: ![17-21](Images/17-21.png) 默认情况下,输入运算符会忽略空白字符(空格符、制表符、换行符、换纸符和回车符)。使用`noskipws`操纵符可以让输入运算符读取空白符,`skipws`操纵符还原默认行为。 ```c++ cin >> noskipws; // set cin so that it reads whitespace while (cin >> ch) cout << ch; cin >> skipws; // reset cin to the default state so that it discards whitespace ``` ### 未格式化的输入/输出操作(Unformatted Input/Output Operations) 标准库提供了一组低层操作,支持未格式化IO(unformatted IO)。这些操作可以将一个流当作无解释的字节序列来处理。 一些未格式化操作每次处理流的一个字节,它们会读取而不是忽略空白符。 ![17-22](Images/17-22.png) 使用未格式化IO操作`get`和`put`可以读取和写入一个字符。 ```c++ char ch; while (cin.get(ch)) cout.put(ch); ``` 有时读取完一个字符后才发现目前无法处理该字符,希望将其放回流中。标准库提供了三种方法退回字符。 - `peek`返回输入流中下一个字符的副本,但不会将其从流中删除。 - `unget`使输入流向后移动,令最后读取的值回到流中。即使不知道最后从流中读取了什么值,也可以调用`unget`。 - `putback`是特殊版本的`unget`,它退回从流中读取的最后一个值,但它接受一个参数,该参数必须与最后读取的值相同。 一般情况下,在读取下一个值之前,标准库保证程序可以退回最多一个值。 `peek`和无参数的`get`函数都以`int`类型从输入流返回字符。这些函数使用`int`的原因是可以返回文件尾标记。`char`范围中的每个值都表示一个真实字符,因此没有额外的值可以表示文件尾。返回`int`的函数先将要返回的字符转换为`unsigned char`,再将结果提升为`int`。因此即使字符集中有字符映射到负值,返回的`int`也是正值。而标准库使用负值表示文件尾,这样就能保证文件尾与任何合法字符的值都不相同。头文件*cstdio*定义了一个名为`EOF`的常量值,可以用它检测函数返回的值是否是文件尾。 ```c++ int ch; // use an int, not a char to hold the return from get() // loop to read and write all the data in the input while ((ch = cin.get()) != EOF) cout.put(ch); ``` 一个常见的编程错误是将`get`或`peek`函数的返回值赋给`char`而非`int`对象,但编译器不能发现这个错误。 ```c++ char ch; // using a char here invites disaster! // the return from cin.get is converted to char and then compared to an int while ((ch = cin.get()) != EOF) cout.put(ch); ``` 当`get`返回`EOF`时,该值会先被转换为`unsigned char`,之后提升得到的`int`值与`EOF`值不再相等,因此循环永远不会停止。 一些未格式化IO操作一次处理大块数据,这些操作可以提高程序执行速度,但需要自己分配并管理用来保存和提取数据的字符数组。 ![17-23](Images/17-23.png) `get`和`getline`函数接受相同的参数,它们的行为类似但不相同。两个函数都一直读取数据,直到遇到下列情况之一: - 已经读取了*size - 1*个字符。 - 遇到了文件尾(`EOF`)。 - 遇到了分隔符。 两个函数的区别在于处理分隔符的方式:`get`将分隔符留在输入流中作为下一个字符,而`getline`读取并丢弃分隔符。两个函数都不会将分隔符保存在结果数组中。 读取流数据时的一个常见错误是忘记从流中删除分隔符。 一些操作可能从输入流中读取了未知个数的字节,使用`gcount`函数可以确定上一次未格式化输入操作读取了多少字符。`gcount`函数应该在任何后续未格式化输入操作前调用,将字符退回流的操作也属于未格式化输入操作。如果在调用`gcount`前使用了`peek`、`unget`或`putback`操作,则`gcount`的返回值为0。 使用`clear`、`ignore`和`sync`函数可以清空输入流中的数据。读到非法字符时,输入流将处于错误状态。为了继续获取输入数据,先调用`clear`函数重置流的错误标记。再调用`ignore`清空流中指定大小的数据,或者调用`sync`直接清空流中所有数据。`numeric_limits::max()`返回流的缓冲区大小。 ```c++ // 重置错误标志 cin.clear(); // 清除流中所有数据 cin.clear(); cin.ignore(numeric_limits::max()); // 清除流中一行数据 cin.ignore(numeric_limits::max(), '\n'); ``` ### 流随机访问(Random Access to a Stream) 随机IO本质上是依赖于操作系统的。 为了支持随机访问,IO类型通过维护一个标记来确定下一次读写操作的位置。`seek`函数用于移动标记,`tell`函数用于获取标记。标准库实际上定义了两对`seek`和`tell`函数,一对用于输入流(后缀为`g`,表示get),一对用于输出流(后缀为`p`,表示put)。 ![17-24](Images/17-24.png) 虽然标准库为所有流类型都定义了`seek`和`tell`函数,但它们是否有意义取决于流绑定到哪个设备。在大多数系统中,绑定到`cin`、`cout`、`cerr`和`clog`的流不支持随机访问。对这些流可以调用`seek`和`tell`函数,但在运行时会出现错误,流也会被置为无效状态。 从逻辑上考虑,`seek`和`tell`函数的使用范围如下: - 可以对`istream`、`ifstream`、`istringstream`类型使用`g`版本。 - 可以对`ostream`、`ofstream`、`ostringstream`类型使用`p`版本。 - 可以对`iostream`、`fstream`、`stringstream`类型使用`g`和`p`版本。 一个流中只有一个标记——不存在独立的读标记和写标记。`fstream`和`stringstream`类型可以读写同一个流。在这些类型中,有单一的缓冲区用于保存读写的数据,同时标记也只有一个,表示缓冲区中的当前位置。标准库将两个版本的`seek`和`tell`函数都映射到这个标记。 由于流中只有一个标记,因此在切换读写操作时,必须使用`seek`函数来重定位标记。 `seek`函数有两个重载版本:一个版本使用绝对地址移动流标记;另一个版本使用指定位置和偏移量移动流标记。 ```c++ // set the marker to a fixed position seekg(new_position); // set the read marker to the given pos_type location seekp(new_position); // set the write marker to the given pos_type location // offset some distance ahead of or behind the given starting point seekg(offset, from); // set the read marker offset distance from from seekp(offset, from); // offset has type off_type ``` 参数*new_position*和*offset*的类型分别是`pos_type`和`off_type`,这两个类型都是机器相关的,定义在头文件*istream*和*ostream*中。`pos_type`表示文件位置,而`off_type`表示距离当前位置的偏移量,偏移量可以是正数也可以是负数。 `tellg`和`tellp`函数返回一个`pos_type`值,表示流的当前位置。 # 第18章 用于大型程序的工具 ## 异常处理(Exception Handling) 异常处理机制允许程序中独立开发的部分能够在运行期间就出现的问题进行通信并做出相应的处理,使问题检测和解决过程相互分离。 ### 抛出异常(Throwing an Exception) 在C++中,通过抛出(throwing)一条表达式来引发(raised)一个异常。被抛出的表达式类型和当前的调用链共同决定了应该使用哪段处理代码(handler)来处理该异常。被选中的处理代码是在调用链中与抛出对象类型匹配且距离最近的代码。 执行一个`throw`语句时,跟在`throw`后面的语句将不再执行。程序的控制权从`throw`转移到与之匹配的`catch`语句中。该`catch`可能是同一个函数中的局部`catch`,也可能位于直接或间接调用了发生异常的函数的另一个函数中。控制权的转移意味着两个问题: - 沿着调用链的函数可能会提前退出。 - 一旦程序开始执行异常处理代码,则沿着调用链创建的对象会被销毁。 抛出异常后,程序暂停执行当前函数并立即寻找对应`catch`语句的过程叫做栈展开(stack unwinding)。栈展开沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的`catch`语句为止。如果没有对应的`catch`语句,则退出主函数后查找过程结束。 - 如果找到了匹配的`catch`语句,则程序进入该子句并执行其中的代码。`catch`语句执行结束后,程序会转移到与`try`块关联的最后一个`catch`语句之后的位置继续执行。 - 如果没有找到匹配的`catch`语句,程序会调用标准库的`terminate`函数,终止运行。 在栈展开过程中,位于调用链上的语句块可能会提前退出,其中的局部对象也会被销毁。如果异常发生在构造函数或者数组及容器的元素初始化过程中,则当前的对象可能只构造了一部分,此时必须确保已构造的成员能被正确销毁。 如果一个块分配了资源,并且在执行资源释放代码前发生了异常,则资源不会被释放。 由于栈展开可能会调用析构函数,因此析构函数不应该抛出不能被它自身处理的异常。即,如果析构函数需要执行某个可能引发异常的操作,则该操作应该被放置在一个`try`语句块中,并在析构函数内部得到处理。实际编程中,析构函数仅仅是释放资源,不太可能引发异常。所有的标准库类型都能确保它们的析构函数不会引发异常。 编译器使用异常抛出表达式对异常对象(exception object)进行拷贝初始化,因此`throw`语句中的表达式必须具有完全类型。如果该表达式是类类型,则相应的类必须含有可访问的析构函数和拷贝/移动构造函数。如果该表达式是数组或函数类型,则表达式会被转换成对应的指针类型。 抛出一条表达式时,该表达式的静态编译类型决定了异常对象的类型。如果`throw`表达式解引用一个基类指针,而该指针实际指向派生类对象,则只有基类部分会被抛出。 抛出指针时必须确保在任何对应的处理代码中,指针指向的对象一定存在。 ### 捕获异常(Catching an Exception) `catch`语句(catch clause)中的异常声明(exception declaration)类似只包含一个形参的函数形参列表。声明的类型决定了处理代码所能捕获的异常类型。该类型必须是完全类型,可以是左值引用,但不能是右值引用。如果`catch`无须访问抛出的表达式,则可以忽略捕获形参的名字。 进入`catch`语句后,使用异常对象初始化异常声明中的参数。`catch`参数的特性和函数参数类似。 - 如果`catch`的参数类型是非引用类型,则该参数是异常对象的一个副本,改变参数不会影响异常对象本身。 - 如果`catch`的参数类型是引用类型,则该参数是异常对象的一个别名,改变参数就是改变异常对象本身。 - 在继承体系中,如果`catch`的参数类型是基类类型,则可以使用其派生类类型的异常对象对其初始化。 - `catch`的参数是基类非引用类型时,异常对象会被切除一部分。 - `catch`的参数是基类引用类型时,以常规方式绑定到异常对象。 异常声明的静态类型决定了`catch`语句所能执行的操作。如果`catch`的参数是基类类型,则无法使用派生类特有的成员。 通常情况下,如果`catch`接受的异常与某个继承体系有关,则最好将`catch`参数定义为引用类型。 查找异常处理代码时,最终结果是第一个与异常匹配的`catch`语句,但这未必是最佳匹配。因此,越特殊的`catch`越应该位于整个`catch`列表的前端。当程序使用具有继承关系的异常时,派生类异常的处理代码应该位于基类异常的处理代码之前。 异常和异常声明的匹配规则比函数参数严格,绝大多数类型转换都不能使用。 - 允许从非常量到常量的类型转换。 - 允许从派生类到基类的类型转换。 - 数组被转换成指向数组元素类型的指针,函数被转换成指向该函数类型的指针。 除此之外,包括标准算术类型转换和类类型转换在内的其他所有转换规则都不能在`catch`匹配过程中使用。 有时一个单独的`catch`语句不能完整处理某个异常。执行完一些校正操作后,当前的`catch`可能会让位于调用链上层的函数继续处理异常。一个`catch`语句通过重新抛出(rethrowing)的操作将异常传递给另一个`catch`语句。重新抛出是一条不包含表达式的`throw`语句。 ```c++ throw; ``` 空`throw`语句只能出现在`catch`或`catch`语句调用的函数之内。如果在异常处理代码之外的区域遇到了空`throw`语句,编译器将调用`terminate`函数。 重新抛出语句不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。如果`catch`语句修改了其参数并重新抛出异常,则只有当`catch`异常声明是引用类型时,程序对参数所做的改变才会被保留并继续传播。 ```c++ catch (my_error &eObj) { // specifier is a reference type eObj.status = errCodes::severeErr; // modifies the exception object throw; // the status member of the exception object is severeErr } catch (other_error eObj) { // specifier is a nonreference type eObj.status = errCodes::badErr; // modifies the local copy only throw; // the status member of the exception object is unchanged } ``` 使用省略号`...`作为异常声明可以一次性捕获所有异常,这种处理代码被称为捕获所有异常(catch-all)的处理代码,可以与任意类型的异常相匹配。 ```c++ try { // actions that cause an exception to be thrown } catch (...) { // work to partially handle the exception throw; } ``` `catch(…)`通常与重新抛出语句一起使用。 如果`catch(…)`与其他`catch`语句一起使用,则`catch(…)`必须位于最后,否则`catch(…)`后面的`catch`语句永远不会被匹配。 ### 函数try语句块与构造函数(Function try Blocks and Constructors) 要想处理构造函数初始值列表抛出的异常,必须将构造函数写成函数`try`语句块(function try block)的形式。函数`try`语句块使得一组`catch`语句可以同时处理构造函数体和构造函数初始化过程中的异常。 ```c++ template Blob::Blob(std::initializer_list il) try : data(std::make_shared>(il)) { /* empty body */ } catch(const std::bad_alloc &e) { handle_out_of_memory(e); } ``` 函数`try`语句块的`catch`语句会在结尾处隐式地重新抛出异常,通知上层函数对象构造失败。上层函数需要继续处理该异常。 在初始化构造函数参数时发生的异常不属于函数`try`语句块处理的范围。 ### noexcept异常说明(The noexcept Exception Specification) 在C++11中,可以通过提供`noexcept`说明(noexcept specification)来指出某个函数不会抛出异常。 ```c++ void recoup(int) noexcept; // won't throw void alloc(int); // might throw ``` `noexcept`说明的出现位置: - 关键字`noexcept`位于函数的参数列表之后,尾置返回类型之前。 - 对于一个函数来说,`noexcept`说明必须同时出现在该函数的所有声明和定义语句中。 - 函数指针的声明和定义也可以指定`noexcept`。 - 在`typedef`或类型别名中不能使用`noexcept`。 - 在成员函数中,关键字`noexcept`位于`const`或引用限定符之后,`final`、`override`或虚函数的`=0`之前。 编译器并不会在编译时检查`noexcept`说明。如果一个函数在指定了`noexcept`的同时又含有`throw`语句或其他可能抛出异常的操作,仍然会通过编译(个别编译器可能会提出警告)。 ```c++ // this function will compile, even though it clearly violates its exception specification void f() noexcept // promises not to throw any exception { throw exception(); // violates the exception specification } ``` 一旦`noexcept`函数抛出异常,程序会调用`terminate`函数终止运行(该过程是否执行栈展开未作规定)。因此`noexcept`可以用于两种情况: - 确认函数不会抛出异常。 - 不知道该如何处理函数抛出的异常。 指明某个函数不会抛出异常可以让调用者不必再考虑异常处理操作。 早期的C++版本设计了一套更详细的异常说明方案。函数可以使用一个关键字`throw`,后面跟上用括号包围的异常类型列表,用于指定函数可能抛出的异常类型。关键字`throw`出现的位置与C++11的`noexcept`相同。该方案在C++11中被取消。但如果一个函数被声明为`throw()`的,则也说明该函数不会抛出异常。 ```c++ void recoup(int) noexcept; // recoup doesn't throw void recoup(int) throw(); // equivalent declaration ``` `noexcept`说明符接受一个可选的实参,该实参必须能转换为`bool`类型。如果实参为`true`,则函数不会抛出异常;如果实参为`false`,则函数可能抛出异常。 ```c++ void recoup(int) noexcept(true); // recoup won't throw void alloc(int) noexcept(false); // alloc can throw ``` `noexcept`运算符(noexcept operator)是一个一元运算符,返回`bool`类型的右值常量表达式,表示给定的运算对象是否会抛出异常。和`sizeof`类似,`noexcept`运算符也不会对运算对象求值。 ```c++ noexcept(e) ``` 当*e*调用的函数都含有`noexcept`说明且*e*本身不含有`throw`语句时,上述表达式返回`true`,否则返回`false`。 `noexcept`运算符通常在`noexcept`说明符的实参中使用。 ```c++ void f() noexcept(noexcept(g())); // f has same exception specifier as g ``` 函数指针与该指针指向的函数必须具有一致的异常说明。如果某个函数指针是`noexcept`的,则该指针只能指向`noexcept`函数;如果显式或隐式地说明了函数指针可能抛出异常,则该指针可以指向任何函数。 ```c++ // both recoup and pf1 promise not to throw void (*pf1)(int) noexcept = recoup; // ok: recoup won't throw; it doesn't matter that pf2 might void (*pf2)(int) = recoup; pf1 = alloc; // error: alloc might throw but pf1 said it wouldn't pf2 = alloc; // ok: both pf2 and alloc might throw ``` 如果一个虚函数是`noexcept`的,则后续派生出来的虚函数必须也是`noexcept`的。如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许,也可以禁止抛出异常。 ```c++ class Base { public: virtual double f1(double) noexcept; // doesn't throw virtual int f2() noexcept(false); // can throw virtual void f3(); // can throw }; class Derived : public Base { public: double f1(double); // error: Base::f1 promises not to throw int f2() noexcept(false); // ok: same specification as Base::f2 void f3() noexcept; // ok: Derived f3 is more restrictive }; ``` 编译器合成拷贝控制成员时,也会生成一个异常声明。如果所有的成员和基类操作都含有`noexcept`说明,则合成成员也是`noexcept`的。 ### 异常类层次(Exception Class Hierarchies) 标准库异常类的继承体系: ![18-1](Images/18-1.png) `exception`类型只定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为`what`的虚成员。`what`函数返回一个`const char*`,指向一个以`NULL`结尾的字符数组,并且不会抛出异常。 `exception`、`bad_cast`和`bad_alloc`类型定义了默认构造函数。`runtime_error`和`logic_error`类型没有默认构造函数,但是有一个接受C风格字符串或`string`类型实参的构造函数,该实参通常用于提供错误信息。`what`函数返回用于初始化异常对象的错误信息。 实际编程中通常会自定义`exception`(或者`exception`的标准库派生类)的派生类以扩展其继承体系。这些面向具体应用的异常类表示了与应用相关的异常状态。 ## 命名空间(Namespaces) 大型应用程序通常会使用多个独立开发的库,其中某些名字可能会相互冲突。多个库将名字放置在全局命名空间中会产生命名空间污染(namespace pollution)。 命名空间分割了全局命名空间,其中每个命名空间都是一个作用域。 ### 命名空间定义(Namespace Definitions) 命名空间的定义包含两部分:关键字`namespace`和随后的命名空间名字。在命名空间名字后面是一系列由花括号包围的声明和定义。能出现在全局作用域中的声明就也能出现在命名空间中。 ```c++ namespace cplusplus_primer { class Sales_data { / * ... * /}; Sales_data operator+(const Sales_data&, const Sales_data&); class Query { /* ... */ }; class Query_base { /* ... */}; } // like blocks, namespaces do not end with a semicolon ``` 命名空间作用域后面不需要分号结束。 和其他名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。命名空间可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。 每个命名空间都是一个作用域,不同命名空间内可以有相同名字的成员。 定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须明确指出所使用的名字属于哪个命名空间。 ```c++ cplusplus_primer::Query q = cplusplus_primer::Query("hello"); ``` 命名空间的定义可以是不连续的。 ```c++ namespace nsp { // declarations } ``` 如果之前没有名为*nsp*的命名空间定义,则上述代码创建一个新的命名空间;否则,上述代码打开已经存在的命名空间定义并为其添加新的成员声明。 利用命名空间不连续的特性可以将几个独立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似管理自定义类和函数的方式。 - 命名空间的一部分成员用于定义类,以及声明作为类接口的函数和对象。这些成员应该放置在头文件中。 - 命名空间成员的定义部分放置在另外的源文件中。源文件需要包含对应的头文件。 程序中的某些实体只能定义一次,如非内联函数、静态数据成员等,命名空间中定义的名字也需要满足该要求。 ```c++ // ---- Sales_data.h---- // #includes should appear before opening the namespace # include namespace cplusplus_primer { class Sales_data { /* ... */}; Sales_data operator+(const Sales_data&, const Sales_data&); // declarations for the remaining functions in the Sales_data interface } // ---- Sales_data.cc---- // be sure any #includes appear before opening the namespace # include "Sales_data.h" namespace cplusplus_primer { // definitions for Sales_data members and overloaded operators } ``` 通常情况下,`#include`不应该出现在命名空间内部。否则头文件中的所有名字都会被定义为该命名空间的成员。 定义多个类型不相关的命名空间时应该使用单独的文件分别表示每个类型。 可以在命名空间的外部定义该命名空间的成员。命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间。 ```c++ // namespace members defined outside the namespace must use qualified names cplusplus_primer::Sales_data cplusplus_primer::operator+(const Sales_data& lhs, const Sales_data& rhs) { Sales_data ret(lhs); // ... } ``` 模板特例化必须定义在原始模板所属的命名空间中。可以在命名空间内部添加模板特例化声明,而在外部对其进行定义。 ```c++ // we must declare the specialization as a member of std namespace std { template <> struct hash; } // having added the declaration for the specialization to std // we can define the specialization outside the std namespace template <> struct std::hash { size_t operator()(const Sales_data& s) const { return hash()(s.bookNo) ^ hash()(s.units_sold) ^ hash()(s.revenue); } // other members as before }; ``` 全局作用域中定义的名字被隐式添加到全局命名空间(global namespace)中。全局命名空间以隐式方式声明,在所有程序中都存在。 作用域运算符`::`可以用于全局命名空间的成员。因为全局命名空间是隐式声明的,所以它并没有名字。 ```c++ ::member_name ``` 命名空间可以嵌套。嵌套的命名空间同时也是一个嵌套的作用域,它嵌套在外层命名空间的作用域内。内层命名空间声明的名字会隐藏外层命名空间的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码在访问时需要在名字前添加限定符。 C++11新增了内联命名空间(inline namespace)。和一般的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。定义内联命名空间的方式是在`namespace`前添加关键字`inline`。`inline`必须出现在该命名空间第一次定义的地方。 ```c++ inline namespace FifthEd { // namespace for the code from the Primer Fifth Edition } namespace FifthEd { // implicitly inline class Query_base { /* ... */ }; // other Query-related declarations } ``` 当应用程序的代码在两次发布之间发生了改变时,通常会使用内联命名空间。 ```c++ namespace FourthEd { class Item_base { /* ... */}; class Query_base { /* ... */}; // other code from the Fourth Edition } namespace cplusplus_primer { #include "FifthEd.h" #include "FourthEd.h" } ``` 因为*FifthEd*是内联的,所以形如`cplusplus_primer::`的代码可以直接获得*FifthEd*的成员。如果想使用早期版本,则必须加上完整的外层命名空间名字。 未命名的命名空间(unnamed namespace)指关键字`namespace`后紧跟以花括号包围的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,直到程序结束才销毁。 一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字在每个包含该头文件的文件中对应不同实体。 定义在未命名的命名空间中的名字可以直接使用,不能对其使用作用域运算符。 定义在未命名的命名空间中的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在最外层作用域中,则该命名空间中的名字必须要与全局作用域中的名字有所区别。 ```c++ int i; // global declaration for i namespace { int i; } // ambiguous: defined globally and in an unnested, unnamed namespace i = 10; namespace local { namespace { int i; } } // ok: i defined in a nested unnamed namespace is distinct from global i local::i = 42; ``` 在标准C++引入命名空间的概念之前,程序需要将名字声明为`static`的以令其对整个文件有效。在文件中进行静态声明的做法是从C语言继承而来的。在C语言中,声明为`static`的全局实体在其所在的文件之外不可见。该做法已经被C++标准取消,现在应该使用未命名的命名空间。 ### 使用命名空间成员(Using Namespace Members) 可以使用关键字`namespace`和`=`为命名空间定义别名(namespace alias)。别名必须出现在命名空间的定义之后。 ```c++ namespace primer = cplusplus_primer; ``` 一个命名空间可以有多个别名,它们都与命名空间的原名等价。 一条`using`声明(using declaration)一次只引入命名空间的一个成员。 `using`声明的有效范围从`using`声明语句开始,一直到`using`声明所在的作用域结束为止。在此过程中,外层作用域的同名实体会被隐藏。未加限定的名字只能在`using`声明所在的作用域及其内层作用域中使用。 `using`声明可以出现在全局作用域、局部作用域、命名空间作用域和类的作用域中。在类的作用域中使用时,`using`声明只能指向基类成员。 和`using`声明不同,`using`指示使某个命名空间中的所有名字都可见。 `using`指示可以出现在全局作用域、局部作用域和命名空间作用域中,不能出现在类的作用域中。 如果对*std*等命名空间使用了`using`指示而未做任何特殊控制的话,会重新引入多个库之间的名字冲突问题。 `using`指示具有将命名空间成员提升到包含命名空间本身和`using`指示的最近外层作用域的能力。 ```c++ // namespace A and function f are defined at global scope namespace A { int i, j; } void f() { using namespace A; // injects the names from A into the global scope cout << i * j << endl; // uses i and j from namespace A // ... } ``` 当命名空间被注入到其外层作用域之后,该命名空间中定义的名字可能会与其外层作用域的成员冲突。这种冲突允许存在,但是要想使用冲突的名字,就必须明确指出名字的版本。 ```c++ namespace blip { int i = 16, j = 15, k = 23; // other declarations } int j = 0; // ok: j inside blip is hidden inside a namespace void manip() { // using directive; the names in blip are ''added'' to the global scope using namespace blip; // clash between ::j and blip::j // detected only if j is used ++i; // sets blip::i to 17 ++j; // error ambiguous: global j or blip::j? ++::j; // ok: sets global j to 1 ++blip::j; // ok: sets blip::j to 16 int k = 97; // local k hides blip::k ++k; // sets local k to 98 } ``` 头文件如果在其顶层作用域中使用`using`声明或`using`指示,则会将名字注入到包含该头文件的所有文件中。通常,头文件只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用`using`声明或`using`指示。 相比于使用`using`指示,在程序中对命名空间中的每个成员分别使用`using`声明效果更好。 - 如果程序使用了多个不同的库,而这些库中的名字通过`using`指示变得可见,则全局命名空间污染问题将重新出现。 - `using`指示引发的二义性错误只有在使用了冲突名字的地方才会被发现。而`using`声明引发的二义性错误在声明处就能发现。 建议在命名空间本身的实现文件中使用`using`指示。 ### 类、命名空间与作用域(Classes,Namespaces,and Scope) 对命名空间内部名字的查找遵循常规查找规则:由内向外依次查找每个外层作用域。只有位于开放的块中且在使用点之前声明的名字才会被考虑。 ```c++ namespace A { int i; namespace B { int i; // hides A::i within B int j; int f1() { int j; // j is local to f1 and hides A::B::j return i; // returns B::i } } // namespace B is closed and names in it are no longer visible int f2() { return j; // error: j is not defined } int j = i; // initialized from A::i } ``` 对于位于命名空间中的类来说,名字的常规查找规则依然适用:当成员函数使用某个名字时,首先在该成员中查找,然后在类(包括基类)中查找,接着在外层作用域中查找。 可以从函数的限定名推断出名字查找时检查作用域的顺序,限定名以相反的顺序指出被查找的作用域。 命名空间中名字的隐藏规则有一个例外:传递给函数一个类类型的对象、指向类的引用或指针时,除了在常规作用域查找名字外,还会查找实参类所属的命名空间。该例外允许概念上作为类接口一部分的非成员函数无须单独的`using`声明就能被程序使用。 ```c++ std::string s; std::cin >> s; // 等价于 std::operator>>(std::cin, s); // 若该规则不存在,则必须为>>运算符提供using声明 using std::operator>>; // 或者显式使用std::operator>> std::operator>>(std::cin, s); ``` 标准库定义的`move`和`forward`模板函数接受一个右值引用形参,可以匹配任何类型。如果应用程序也定义了一个接受单一参数的`move`和`forward`函数,则不管形参是什么类型,都会与标准库的版本冲突。对于这两个函数来说,冲突大多是无意的,因此建议使用它们的含有限定语的完整版本(即`std::move`、`std::forward`)。 如果一个未声明的类或函数第一次出现在友元声明中,则会被认定是离它最近的外层命名空间的成员。 ### 重载与命名空间(Overloading and Namespaces) `using`声明和`using`指示能将某些函数添加到候选函数集。 确定候选函数集时,会在函数的每个实参类(以及实参类的基类)所属的命名空间中搜索候选函数。这些命名空间中所有与被调用函数同名的函数都会被添加到候选集当中,即使其中某些函数在调用语句处不可见也是如此。 `using`声明语句声明的是一个名字,而非一个特定的函数。一个`using`声明囊括了重载函数的所有版本以确保不违反命名空间的接口。 ```C++ using NS::print(int); // error: cannot specify a parameter list using NS::print; // ok: using declarations specify names only ``` 一个`using`声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。如果`using`声明出现在局部作用域中,则引入的名字会隐藏外层作用域的相关声明。如果`using`声明所在的作用域中已经有一个函数与引入的函数同名且形参列表相同,则该`using`声明会引发错误。除此之外,`using`声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模。 `using`指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域中的函数同名,则命名空间的函数会被添加到重载集合中。 ```c++ namespace libs_R_us { extern void print(int); extern void print(double); } // ordinary declaration void print(const std::string &); // this using directive adds names to the candidate set for calls to print: using namespace libs_R_us; // the candidates for calls to print at this point in the program are: // print(int) from libs_R_us // print(double) from libs_R_us // print(const std::string &) declared explicitly void fooBar(int ival) { print("Value: "); // calls global print(const string &) print(ival); // calls libs_R_us::print(int) } ``` 与`using`声明不同,`using`指示引入一个与已有函数形参列表完全相同的函数并不会引发错误。但需要明确指出调用的是命名空间中的函数版本还是当前作用域中的版本。 如果存在多个`using`指示,则来自每个命名空间的名字都会成为候选函数集的一部分。 ## 多重继承与虚继承(Multiple and Virtual Inheritance) 多重继承(Multiple inheritance)是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。 ### 多重继承(Multiple Inheritance) 派生类的派生列表中可以包含多个基类。每个基类都包含一个可选的访问说明符。和单继承相同,如果访问说明符被省略,则关键字`class`对应的默认访问说明符是`private`,关键字`struct`对应的是`public`。 ```c++ class Bear : public ZooAnimal { /* ... */ }; class Panda : public Bear, public Endangered { /* ... */ }; ``` 和单继承相同,多重继承的派生列表也只能包含已经被定义过的类,且这些类不能是`final`的。 多重继承关系中,派生类对象包含每个基类的子对象。 ![18-2](Images/18-2.png) 构造一个多重继承的派生类对象将同时构造并初始化它的所有基类子对象。 ```c++ // explicitly initialize both base classes Panda::Panda(std::string name, bool onExhibit) : Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) { } // implicitly uses the Bear default constructor to initialize the Bear subobject Panda::Panda() : Endangered(Endangered::critical) { } ``` 派生类的构造函数初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类的出现顺序一致,与构造函数初始值列表中基类的顺序无关。 C++11允许派生类从它的一个或多个基类中继承构造函数,但如果从多个基类中继承了相同的构造函数(即形参列表完全相同),程序会产生错误。 ```c++ struct Base1 { Base1() = default; Base1(const std::string&); Base1(std::shared_ptr); }; struct Base2 { Base2() = default; Base2(const std::string&); Base2(int); }; // error: D1 attempts to inherit D1::D1 (const string&) from both base classes struct D1: public Base1, public Base2 { using Base1::Base1; // inherit constructors from Base1 using Base2::Base2; // inherit constructors from Base2 }; ``` 如果一个类从它的多个基类中继承了相同的构造函数,则必须为该构造函数定义其自己的版本。 ```c++ struct D2: public Base1, public Base2 { using Base1::Base1; // inherit constructors from Base1 using Base2::Base2; // inherit constructors from Base2 // D2 must define its own constructor that takes a string D2(const string &s): Base1(s), Base2(s) { } D2() = default; // needed once D2 defines its own constructor }; ``` 和单继承相同,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行这些操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动处理其基类部分。在合成版本的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。 ### 类型转换与多个基类(Conversions and Multiple Base Classes) 多重继承和单继承相同,某个可访问基类的指针或引用可以直接指向派生类对象。 编译器不会在派生类向基类的几种转换中进行比较和选择。 ```c++ void print(const Bear&); void print(const Endangered&); Panda ying_yang("ying_yang"); print(ying_yang); // error: ambiguous ``` 和单继承相同,对象、指针和引用的静态类型决定了我们可以使用的成员。 ### 多重继承下的类作用域(Class Scope under Multiple Inheritance) 在单继承中,派生类的作用域嵌套在直接基类和间接基类的作用域中。名称查找沿着继承体系自底向上进行,直到找到所需的名字。派生类的名字会隐藏基类的同名成员。在多重继承中,相同的查找过程在所有基类中同时进行。如果名字在多个基类中都被找到,则会产生二义性错误。 派生类可以从多个基类中分别继承名字相同的成员,但是在使用该名字时必须明确指出其版本。避免潜在二义性的最好方法是在派生类中定义新的版本。 ### 虚继承(Virtual Inheritance) 尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再次间接继承该类。 默认情况下,派生类含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中会包含该类的多个子对象。这种默认情况对某些类并不适用。例如`iostream`,它直接继承自`istream`和`ostream`,而`istream`和`ostream`都继承自`base_ios`,所以`iostream`继承了`base_ios`两次。如果`iostream`对象包含`base_ios`的两份拷贝,则无法在同一个缓冲区中进行读写操作。 虚继承可以让某个类共享它的基类,其中共享的基类子对象称为虚基类(virtual base class)。在该机制下,不论虚基类在继承体系中出现了多少次,派生类都只包含唯一一个共享的虚基类子对象。 通常情况下,使用虚继承的类层次是由一个人或一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个是虚基类,况且新基类的开发者也无法改变已存在的类体系。 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。 指定虚基类的方式是在派生列表中添加关键字`virtual`。 ```c++ // the order of the keywords public and virtual is not significant class Raccoon : public virtual ZooAnimal { /* ... */ }; class Bear : virtual public ZooAnimal { /* ... */ }; ``` 如果某个类指定了虚基类,则该类的派生仍按照常规方式进行。 ```c++ class Panda : public Bear, public Raccoon, public Endangered { /* ... */ }; ``` ![18-3](Images/18-3.png) 不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。 因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,而且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则也可以直接访问该成员。但如果成员被多个基类覆盖,则一般情况下派生类必须为该成员定义新的版本。例如,假设类*B*定义了一个名为*X*的成员,*D1*和*D2*都从*B*虚继承得到,*D*继承了*D1*和*D2*。则在*D*的作用域中,*X*通过*D*的两个基类都是可见的。如果通过*D*的对象使用*X*,则有三种可能性: - 如果*D1*和*D2*中都没有*X*的定义,则*X*会被解析为*B*的成员,此时不存在二义性。 - 如果*D1*和*D2*中的某一个定义了*X*,派生类的*X*会比共享虚基类*B*的*X*优先级更高,此时同样没有二义性。 - 如果*D1*和*D2*都定义了*X*,则直接访问*X*会产生二义性问题。 ### 构造函数与虚继承(Constructors and Virtual Inheritance) 在虚派生中,虚基类是由最低层的派生类初始化的。如果按普通规则处理,虚基类将会在多条继承路径上被重复初始化。 继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。只要能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。即使虚基类不是派生类的直接基类,构造函数也可以进行初始化。 ```c++ Bear::Bear(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Bear") { } Raccoon::Raccoon(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Raccoon") { } Panda::Panda(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Panda"), Bear(name, onExhibit), Raccoon(name, onExhibit), Endangered(Endangered::critical), sleeping flag(false) { } ``` 构造含有虚基类的对象时,首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,之后再按照直接基类在派生列表中出现的顺序依次对其初始化。 一个类可以有多个虚基类,此时这些虚子对象按照它们在派生列表中出现的顺序依次构造。 # 第19章 特殊工具与技术 ## 控制内存分配(Controlling Memory Allocation) ### 重载new和delete(Overloading new and delete) 使用`new`表达式时,实际执行了三步操作: - `new`表达式调用名为`operator new`(或`operator new[]`)的标准库函数。该函数分配一块足够大、原始、未命名的内存空间以便存储特定类型的对象(或对象数组)。 - 编译器调用对应的构造函数构造这些对象并初始化。 - 对象被分配了空间并构造完成,返回指向该对象的指针。 使用`delete`表达式时,实际执行了两步操作: - 对指针所指向的对象(或对象数组)执行对应的析构函数。 - 编译器调用名为`operator delete`(或`operator delete[]`)的标准库函数释放内存空间。 如果程序希望控制内存分配的过程,则需要定义自己的`operator new`和`operator delete`函数。编译器会用自定义版本替换标准库版本。 程序可以在全局作用域中定义`operator new`和`operator delete`函数,也可以将其定义为成员函数。编译器发现`new`或`delete`表达式后,将在程序中查找可供调用的`operator`函数。如果被分配或释放的对象是类类型,编译器会先在类及其基类的作用域中查找。如果该类含有`operator`成员,则表达式会调用这些成员。否则编译器会继续在全局作用域查找。如果找到自定义版本,则使用该版本的函数。如果没找到,则使用标准库定义的版本。 可以使用作用域运算符令`new`或`delete`表达式忽略定义在类中的函数,直接执行全局作用域版本。 标准库定义了`operator new`和`operator delete`函数的8个重载版本,其中前4个版本可能抛出`bad_alloc`异常,后4个版本不会抛出异常。重载这些运算符时,必须使用关键字`noexcept`指定其不抛出异常。 ```c++ // these versions might throw an exception void *operator new(size_t); // allocate an object void *operator new[](size_t); // allocate an array void *operator delete(void*) noexcept; // free an object void *operator delete[](void*) noexcept; // free an array // versions that promise not to throw void *operator new(size_t, nothrow_t&) noexcept; void *operator new[](size_t, nothrow_t&) noexcept; void *operator delete(void*, nothrow_t&) noexcept; void *operator delete[](void*, nothrow_t&) noexcept ``` `nothrow_t`类型是定义在头文件*new*中的一个结构体,这个类型不包含任何成员。头文件*new*还定义了一个名为`nothrow`的`const`对象,用户可以通过这个对象请求`new`的非抛出版本。 将`operator`函数定义为类的成员时,它们是隐式静态的,无须显式地声明`static`。因为`operator new`用在对象构造之前,`operator delete`用在对象销毁之后,所以它们必须是静态成员,而且不能操纵类的任何数据成员。 `operator new`和`operator new[]`函数的返回类型必须是`void*`,第一个形参的类型必须是`size_t`且不能有默认实参。编译器调用`operator new`时,用存储指定类型对象所需的字节数初始化`size_t`形参;调用`operator new[]`时,传入函数的则是存储数组中所有元素所需的空间。 自定义`operator new`函数时可以为它提供额外的形参,用到这些自定义函数的`new`表达式必须使用`new`的定位形式传递参数。下面这种形式的`new`函数只供标准库使用,不能被用户重定义: ```c++ void *operator new(size_t, void*); // this version may not be redefined ``` `operator delete`和`operator delete[]`函数的返回类型必须是`void`,第一个形参的类型必须是`void*`。函数被调用时,编译器会用指向待释放内存的指针来初始化`void*`形参。 将`operator delete`或`operator delete[]`定义为类的成员时,可以包含另一个类型为`size_t`的形参。该形参的初始值是第一个形参所指向对象的字节数。`size_t`形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给`operator delete`的字节数会因待删除指针所指向对象的动态类型不同而有所区别。实际运行的`operator delete`函数版本也由对象的动态类型决定。 `malloc`函数接受一个表示待分配字节数的`size_t`参数,返回指向分配空间的指针,或者返回0以表示分配失败。`free`函数接受一个`void*`参数,它是`malloc`返回的指针的副本,`free`将相关内存返回给系统。调用`free(0)`没有任何意义。 ```c++ void *operator new(size_t size) { if (void *mem = malloc(size)) return mem; else throw bad_alloc(); } void operator delete(void *mem) noexcept { free(mem); } ``` ### 定位new表达式(Placement new Expressions) 在C++的早期版本中,`allocator`类还不是标准库的一部分。如果程序想分开内存分配和初始化过程,需要直接调用`operator new`和`operator delete`函数。它们类似`allocator`类的`allocate`和`deallocate`成员,负责分配或释放内存空间,但不会构造或销毁对象。 不能使用`allocator`类的`construct`函数在`operator new`分配的内存空间中构造对象,而应该使用定位`new`表达式构造。 ```c++ new (place_address) type new (place_address) type (initializers) new (place_address) type [size] new (place_address) type [size] { braced initializer list } ``` 其中*place_address*是一个指针。*initializers*是一个以逗号分隔的初始值列表(可能为空),该列表用于构造新分配的对象。 当仅通过一个地址值调用定位`new`时,它会使用`operator new(size_t, void*)`函数(用户无法重载的版本)。该函数不分配任何内存,直接返回指针形参。然后由`new`表达式负责在指定的地址初始化对象。 传递给`construct`函数的指针必须指向同一个`allocator`对象分配的空间,但是传递给定位`new`的指针无须指向`operator new`分配的内存,甚至不需要指向动态内存。 调用析构函数会销毁对象,但不会释放内存。如果需要的话,可以重新使用该空间。 ## 运行时类型识别(Run-Time Type Identification) 运行时类型识别(RTTI)的功能由两个运算符实现: - `typeid`运算符,用于返回表达式的类型。 - `dynamic_cast`运算符,用于将基类的指针或引用安全地转换为派生类的指针或引用。 RTTI运算符适用于以下情况:想通过基类对象的指针或引用执行某个派生类操作,并且该操作不是虚函数。 ### dynamic_cast运算符(The dynamic_cast Operator) `dynamic_cast`运算符的形式如下: ```c++ dynamic_cast(e) dynamic_cast(e) dynamic_cast(e) ``` 其中*type*是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,*e*必须是一个有效指针;在第二种形式中,*e*必须是一个左值;在第三种形式中,*e*不能是左值。在所有形式中,*e*的类型必须符合以下条件之一: - *e*是*type*的公有派生类。 - *e*是*type*的公有基类。 - *e*和*type*类型相同。 如果条件符合,则类型转换成功,否则转换失败。转换失败可能有两种结果: - 如果`dynamic_cast`语句的转换目标是指针类型,则结果为0。 ```c++ if (Derived *dp = dynamic_cast(bp)) { // use the Derived object to which dp points } else { // bp points at a Base object // use the Base object to which bp points } ``` - 如果`dynamic_cast`语句的转换目标是引用类型,则抛出`bad_cast`异常(定义在头文件*typeinfo*中)。 ```c++ void f(const Base &b) { try { const Derived &d = dynamic_cast(b); // use the Derived object to which b referred } catch (bad_cast) { // handle the fact that the cast failed } } ``` 在条件判断部分执行`dynamic_cast`可以确保类型转换和结果检查在同一条表达式中完成。 可以对一个空指针执行`dynamic_cast`,结果是所需类型的空指针。 ### typeid运算符(The typeid Operator) `typeid`表达式的形式是`typeid(e)`,其中*e*可以是任意表达式或类型名称。`typeid`的结果是一个指向常量对象的引用,该对象的类型是标准库`type_info`(定义在头文件*typeinfo*中)或`type_info`的公有派生类型。 `typeid`可以作用于任何类型的表达式,其中的顶层`const`会被忽略。如果表达式是一个引用,则`typeid`返回该引用所指对象的类型。当`typeid`作用于数组或函数时,不会执行向指针的标准类型转换。 当`typeid`的运算对象不属于类类型或者是一个不包含任何虚函数的类时,`typeid`返回其静态类型。而当运算对象是至少包含一个虚函数的类的左值时,`typeid`的结果直到运行期间才会确定。 通常情况下,`typeid`用于比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同。 ```c++ C++Derived *dp = new Derived; Base *bp = dp; // both pointers point to a Derived object // compare the type of two objects at run time if (typeid(*bp) == typeid(*dp)) { // bp and dp point to objects of the same type } // test whether the run-time type is a specific type if (typeid(*bp) == typeid(Derived)) { // bp actually points to a Derived } ``` `typeid`应该作用于对象。当`typeid`作用于指针时,返回的结果是该指针的静态编译类型。 ```c++ // test always fails: the type of bp is pointer to Base if (typeid(bp) == typeid(Derived)) { // code never executed } ``` 只有当类型含有虚函数时,编译器才会对`typeid`的表达式求值以确定返回类型。对于`typeid(*p)`,如果指针*p*所指向的类型不包含虚函数,则*p*可以是一个无效指针。否则`*p`会在运行期间求值,此时*p*必须是一个有效指针。如果*p*是空指针,`typeid(*p)`会抛出`bad_typeid`异常。 ### 使用RTTI(Using RTTI) 使用RTTI可以为具有继承关系的类实现相等运算符。 相等运算符的形参是基类的引用。 ```c++ class Base { friend bool operator==(const Base&, const Base&); public: // interface members for Base protected: virtual bool equal(const Base&) const; // data and other implementation members of Base }; class Derived: public Base { public: // other interface members for Derived protected: bool equal(const Base&) const; // data and other implementation members of Derived }; ``` 使用`typeid`检查两个运算对象的类型是否一致,类型一致才会继续判断每个数据成员的取值是否相同。 ```c++ bool operator==(const Base &lhs, const Base &rhs) { // returns false if typeids are different; otherwise makes a virtual call to equal return typeid(lhs) == typeid(rhs) && lhs.equal(rhs); } ``` 每个类定义的`equal`函数负责比较类型自己的数据成员。`equal`函数的形参都是基类的引用,但是在比较之前需要先把运算对象转换成自己的类型。 ```c++ bool Derived::equal(const Base &rhs) const { // we know the types are equal, so the cast won't throw auto r = dynamic_cast(rhs); // do the work to compare two Derived objects and return the result } bool Base::equal(const Base &rhs) const { // do whatever is required to compare to Base objects } ``` ### type_info类(The type_info Class) `type_info`类的精确定义会根据编译器的不同而略有差异。但是C++规定`type_info`必须定义在头文件*typeinfo*中,并且至少提供以下操作: ![19-1](Images/19-1.png) `type_info`类一般是作为一个基类出现,所以它还应该提供一个公有虚析构函数。当编译器希望提供额外的类型信息时,通常在`type_info`的派生类中完成。 `type_info`类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义为删除的。创建`type_info`对象的唯一方式就是使用`typeid`运算符。 对于某种给定类型来说,`name`成员的返回值因编译器而异并且不一定与在程序中使用的名字一致。对于`name`返回值的唯一要求就是类型不同则返回的字符串必须有所区别。 ## 枚举类型(Enumerations) 和类一样,每个枚举类型都定义了一种新的类型。枚举属于字面值常量类型。 C++包含两种枚举: - 限定作用域的枚举(scoped enumeration,C++11新增)。定义形式是关键字`enum class`(或`enum struct`)后接枚举类型名字以及用花括号包围、以逗号分隔的枚举成员(enumerator)列表。 ```c++ enum class open_modes { input, output, append }; ``` - 不限定作用域的枚举(unscoped enumeration)。定义时省略关键字`class`(或`struct`),枚举类型名字是可选的。 ```c++ C++// unscoped enumeration enum color { red, yellow, green }; // unnamed, unscoped enum enum { floatPrec = 6, doublePrec = 10, double_doublePrec = 10 }; ``` 如果枚举是未命名的,则只能在定义该枚举时一同定义它的对象。 在限定作用域的枚举类型中,枚举成员的名字遵循常规作用域规则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。 ```c++ enum color { red, yellow, green }; // unscoped enumeration enum stoplight { red, yellow, green }; // error: redefines enumerators enum class peppers { red, yellow, green }; // ok: enumerators are hidden color eyes = green; // ok: enumerators are in scope for an unscoped enumeration peppers p = green; // error: enumerators from peppers are not in scope // color::green is in scope but has the wrong type color hair = color::red; // ok: we can explicitly access the enumerators peppers p2 = peppers::red; // ok: using red from peppers ``` 默认情况下,枚举值从0开始,依次加1。也可以直接为枚举成员指定特定的值。 ```c++ enum class intTypes { charTyp = 8, shortTyp = 16, intTyp = 16, longTyp = 32, long_longTyp = 64 }; ``` 枚举值可以不唯一。如果没有显式提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。 枚举成员是`const`的,因此在初始化枚举成员时提供的初始值必须是常量表达式。 可以在任何需要常量表达式的地方使用枚举成员。如: - 定义枚举类型的`constexpr`变量。 - 将枚举类型对象作为`switch`语句的表达式,而将枚举值作为`case`标签。 - 将枚举类型作为非类型模板形参使用。 - 在类的定义中初始化枚举类型的静态数据成员。 初始化枚举对象或者给枚举对象赋值时,必须使用该类型的一个枚举成员或者该类型的另一个对象。即使某个整型值恰好与枚举成员的值相等,也不能用其初始化枚举对象。 ```c++ open_modes om = 2; // error: 2 is not of type open_modes om = open_modes::input; // ok: input is an enumerator of open_modes ``` 不限定作用域的枚举类型对象或枚举成员能自动转换成整型。 ```c++ int i = color::red; // ok: unscoped enumerator implicitly converted to int int j = peppers::red; // error: scoped enumerations are not implicitly converted ``` 枚举是由某种整数类型表示的。C++11中,可以在枚举名字后面指定用来表示枚举成员的整型类型。 ```c++ enum intValues : unsigned long long { /*...*/ }; ``` 如果没有指定枚举的潜在类型,则默认情况下限定作用域的枚举成员类型是`int`。不限定作用域的枚举成员不存在默认类型。 C++11中可以提前声明枚举。枚举的前置声明必须指定(无论隐式或显式)其成员的类型。 ```c++ // forward declaration of unscoped enum named intValues enum intValues : unsigned long long; // unscoped, must specify a type enum class open_modes; // scoped enums can use int by default ``` ## 类成员指针(Pointer to Class Member) 成员指针(pointer to member)是指可以指向类的非静态成员的指针。 成员指针的类型包括类的类型和成员的类型。初始化成员指针时,会令其指向类的某个成员,但是不指定该成员所属的对象。直到使用成员指针时,才提供成员所属的对象。 ### 数据成员指针(Pointers to Data Members) 声明成员指针时必须在`*`前添加`classname::`以表示当前定义的指针可以指向*classname*的成员。 ```c++ class Screen { public: typedef std::string::size_type pos; char get_cursor() const { return contents[cursor]; } char get() const; char get(pos ht, pos wd) const; private: std::string contents; pos cursor; pos height, width; } // pdata can point to a string member of a const (or non const) Screen object const string Screen::*pdata; ``` 初始化或者给成员指针赋值时,需要指定它所指向的成员。 ```c++ pdata = &Screen::contents; ``` 成员指针使用`.*`和`->*`来获得其指向对象的成员。 ```c++ Screen myScreen, *pScreen = &myScreen; // .* dereferences pdata to fetch the contents member from the object myScreen auto s = myScreen.*pdata; // ->* dereferences pdata to fetch contents from the object to which pScreen points s = pScreen->*pdata; ``` 常规的访问控制规则对成员指针同样有效。数据成员一般是私有的,因此通常不能直接获得数据成员的指针。如果类希望外部代码能访问它的私有数据成员,可以定义一个函数,令其返回指向私有成员的指针。 ### 成员函数指针(Pointers to Member Functions) 类似于其他函数指针,指向成员函数的指针也需要指定目标函数的返回类型和形参列表。如果成员函数是`const`成员或引用成员,则指针也必须包含`const`或引用限定符。 ```c++ // pmf is a pointer that can point to a Screen member function that is const // that returns a char and takes no arguments auto pmf = &Screen::get_cursor; ``` 如果成员函数存在重载问题,则必须显式声明指针指向的函数类型。 ```c++ char (Screen::*pmf2)(Screen::pos, Screen::pos) const; pmf2 = &Screen::get; ``` 和普通函数指针不同,在成员函数和指向该成员的指针之间不存在自动转换规则。 ```c++ // pmf points to a Screen member that takes no arguments and returns char pmf = &Screen::get; // must explicitly use the address-of operator pmf = Screen::get; // error: no conversion to pointer for member functions ``` 成员函数指针使用`.*`和`->*`来调用类的成员函数。 ```c++ Screen myScreen, *pScreen = &myScreen; // call the function to which pmf points on the object to which pScreen points char c1 = (pScreen->*pmf)(); // passes the arguments 0, 0 to the two-parameter version of get on the object myScreen char c2 = (myScreen.*pmf2)(0, 0); ``` 可以使用类型别名来增强含有成员指针的代码的可读性。 ### 将成员函数用作可调用对象(Using Member Functions as Callable Objects) 成员指针不是一个可调用对象,不支持函数调用运算符。 ```c++ auto fp = &string::empty; // fp points to the string empty function // error: must use .* or ->* to call a pointer to member find_if(svec.begin(), svec.end(), fp); ``` 从成员函数指针获取可调用对象的一种方法是使用标准库模板`function`。 ```c++ function fcn = &string::empty; find_if(svec.begin(), svec.end(), fcn); ``` 定义一个`function`对象时,必须指定该对象所能表示的函数类型(即可调用对象的形式)。如果可调用对象是一个成员函数,则第一个形参必须表示该成员是在哪个对象上执行的。 使用标准库功能`mem_fn`(定义在头文件*functional*中)可以让编译器推断成员的类型。和`function`一样,`mem_fn`可以从成员指针生成可调用对象。但`mem_fn`可以根据成员指针的类型推断可调用对象的类型,无须显式指定。 ```c++ find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); ``` `mem_fn`生成的可调用对象可以通过对象和指针调用。 ```c++ auto f = mem_fn(&string::empty); // f takes a string or a string* f(*svec.begin()); // ok: passes a string object; f uses .* to call empty f(&svec[0]); // ok: passes a pointer to string; f uses .-> to call empty ``` ## 嵌套类(Nested Classes) 一个类可以定义在另一个类的内部,前者被称为嵌套类或嵌套类型(nested type)。嵌套类通常用于定义作为实现部分的类。 外层类的对象和嵌套类的对象是相互独立的。在嵌套类对象中不包含任何外层类定义的成员,在外层类对象中也不包含任何嵌套类定义的成员。 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。 外层类对嵌套类的成员没有特殊的访问权限,嵌套类对外层类的成员也没有特殊的访问权限。 嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类的访问说明符决定。 嵌套类必须声明在类的内部,但是可以定义在类的内部或外部。在外层类之外定义嵌套类时,必须用外层类的名字限定嵌套类的名字。 ```c++ class TextQuery { class QueryResult; // nested class to be defined later }; // we're defining the QueryResult class that is a member of class TextQuery class TextQuery::QueryResult { /*...*/ }; ``` 在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型。 ## union:一种节省空间的类(union: A Space-Saving Class) 联合(union)是一种特殊的类。一个联合可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。给联合的某个成员赋值之后,其他成员会变为未定义状态。分配给联合对象的存储空间至少要能容纳它的最大数据成员。 联合不能包含引用类型的成员。在C++11中,含有构造函数或析构函数的类类型也可以作为联合的成员类型。 联合可以为其成员指定`public`、`protected`和`private`等保护标记。默认情况下,联合的成员都是公有的。 联合可以定义包括构造函数和析构函数在内的成员函数。但是由于联合既不能继承自其他类,也不能作为基类使用,所以在联合中不能含有虚函数。 定义联合时,首先是关键字`union`,随后是该联合的名字(可选)以及在花括号内的一组成员声明。 ```c++ // objects of type Token have a single member, which could be of any of the listed types union Token { // members are public by default char cval; int ival; double dval; }; ``` 默认情况下,联合是未初始化的。可以像显式初始化聚合类一样显式初始化联合,提供的初始值会被用于初始化第一个成员。 ```c++ Token first_token = { 'a' }; // initializes the cval member ``` 可以使用通用的成员访问运算符访问联合对象的成员。 ```c++ last_token.cval = 'z'; pt->ival = 42; ``` 匿名联合(anonymous union)是一个未命名的联合,并且在右花括号和分号之间没有任何声明。一旦定义了一个匿名联合,编译器就会自动地为该联合创建一个未命名的对象。在匿名联合的定义所在的作用域内,该联合的成员都是可以直接访问的。 ```c++ union { // anonymous union char cval; int ival; double dval; }; // defines an unnamed object, whose members we can access directly cval = 'c'; // assigns a new value to the unnamed, anonymous union object ival = 42; // that object now holds the value 42 ``` 匿名联合不能包含`protected`和`private`成员,也不能定义成员函数。 C++的早期版本规定,在联合中不能含有定义了构造函数或拷贝控制成员的类类型成员。C++11取消了该限制。但是如果联合的成员类型定义了自己的构造函数或拷贝控制成员,该联合的用法会比只含有内置类型成员的联合复杂得多。 - 当联合只包含内置类型的成员时,可以使用普通的赋值语句改变联合的值。但是如果想将联合的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须构造或析构该类类型的成员。 - 当联合只包含内置类型的成员时,编译器会按照成员顺序依次合成默认构造函数或拷贝控制成员。但是如果联合含有类类型成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器会为该联合合成对应的版本并将其声明为删除的。 对于联合来说,构造或销毁类类型成员的操作非常复杂。通常情况下,可以把含有类类型成员的联合内嵌在另一个类中,这个类可以管理并控制与联合的类类型成员相关的状态转换。 ## 局部类(Local Classes) 类可以定义在某个函数的内部,这种类被称为局部类。局部类定义的类型只能在定义它的作用域内可见。 局部类的所有成员(包括成员函数)都必须完整定义在类的内部,因此局部类的作用与嵌套类相比相差很远。 局部类中不允许声明静态数据成员。 局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员,不能使用普通局部变量。 ```c++ int a, val; void foo(int val) { static int si; enum Loc { a = 1024, b }; // Bar is local to foo struct Bar { Loc locVal; // ok: uses a local type name int barVal; void fooBar(Loc l = a) // ok: default argument is Loc::a { barVal = val; // error: val is local to foo barVal = ::val; // ok: uses a global object barVal = si; // ok: uses a static local object locVal = b; // ok: uses an enumerator } }; // . . . } ``` 常规的访问保护规则对于局部类同样适用。外层函数对局部类的私有成员没有任何访问特权。局部类可以将外层函数声明为友元。 可以在局部类的内部再嵌套一个类。此时嵌套类的定义可以出现在局部类之外,不过嵌套类必须定义在与局部类相同的作用域中。 ```c++ void foo() { class Bar { public: class Nested; // declares class Nested }; // definition of Nested class Bar::Nested { }; } ``` 局部类内的嵌套类也是一个局部类,必须遵循局部类的各种规定。 ## 固有的不可移植的特性(Inherently Nonportable Features) ### 位域(Bit-fields) 类可以将其非静态数据成员定义成位域,在一个位域中含有一定数量的二进制位。当程序需要向其他程序或硬件设备传递二进制数据时,通常会使用位域。 位域的声明形式是在成员名字之后紧跟一个冒号和一个常量表达式,该表达式用于指定成员所占的二进制位数。 位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以通常情况下使用无符号类型保存位域。位域类型的大小不能小于位域结构的总大小。 ```c++ struct Descriptor { // error: should use unsigned long long unsigned int LimitLow : 16; unsigned int BaseLow : 24; unsigned int Attribute : 16; unsigned int BaseHigh : 8; } ``` 定义位域时建议结合`#pragma pack`指令将结构体对齐值修改为1,防止数据结构错位。 ```c++ // 保存原始对齐值,设置新对齐 #pragma pack(push, 1) // 结构体定义…… // 恢复原始对齐值 #pragma pack(pop) ``` 位域成员按定义顺序在内存中由低地址向高地址排列,具体布局与机器相关。 取地址符`&`不能作用于位域,因此任何指针都无法指向类的位域。 如果可能的话,类内部连续定义的位域会压缩在同一整数的相邻位,从而提供存储压缩。 访问位域的方式与访问类的其他数据成员的方式类似。操作超过1位的位域时,通常会使用内置的位运算符。 ```c++ File &File::open(File::modes m) { mode |= READ; // set the READ bit by default // other processing if (m & WRITE) // if opening READ and WRITE // processing to open the file in read/write mode return *this; } ``` ### volatile限定符(volatile Qualifier) 当对象的值可能在程序的控制或检测之外被改变时(如子线程),应该将该对象声明为`volatile`。关键字`volatile`的作用是告知编译器不要优化这样的对象。 `volatile`的确切含义与机器有关,只能通过查阅编译器文档来理解。要想让一个使用了`volatile`的程序在移植到新机器或新编译器后仍然有效,通常需要对该程序进行一些修改。 `volatile`的用法和`const`类似,都是对类型的额外修饰。二者相互之间并没有影响。 ```c++ volatile int display_register; // int value that might change volatile Task *curr_task; // curr_task points to a volatile object volatile int iax[max_size]; // each element in iax is volatile ``` 类可以将成员函数定义为`volatile`的。`volatile`对象只能调用`volatile`成员函数。 `volatile`和指针的关系类似`const`。可以声明`volatile`指针、指向`volatile`对象的指针和指向`volatile`对象的`volatile`指针。 ```c++ volatile int v; // v is a volatile int int *volatile vip; // vip is a volatile pointer to int volatile int *ivp; // ivp is a pointer to volatile int // vivp is a volatile pointer to volatile int volatile int *volatile vivp; int *ip = &v; // error: must use a pointer to volatile *ivp = &v; // ok: ivp is a pointer to volatile vivp = &v; // ok: vivp is a volatile pointer to volatile ``` 不能使用合成的拷贝/移动构造函数和赋值运算符初始化`volatile`对象或者给`volatile`对象赋值。合成的成员接受的形参类型是非`volatile`常量引用,不能把非`volatile`引用绑定到`volatile`对象上。 如果类需要拷贝、移动或赋值它的`volatile`对象,则必须自定义拷贝或移动操作。 ```c++ class Foo { public: Foo(const volatile Foo&); // copy from a volatile object // assign from a volatile object to a nonvolatile object Foo& operator=(volatile const Foo&); // assign from a volatile object to a volatile object Foo& operator=(volatile const Foo&) volatile; // remainder of class Foo }; ``` ### 链接指示:extern "C"(Linkage Directives:extern "C") C++程序有时需要调用使用其他语言编写的函数,最常见的是调用C语言函数。其他语言中的函数名字也必须在C++中进行声明。对于这些函数,编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别。C++使用链接指示指出任意非C++函数所用的语言。 链接指示有单个形式和复合形式,其不能出现在类定义或函数定义的内部。同样的链接指示必须出现在函数的每个声明处。 ```c++ // illustrative linkage directives that might appear in the C++ header // single-statement linkage directive extern "C" size_t strlen(const char *); // compound-statement linkage directive extern "C" { int strcmp(const char*, const char*); char *strcat(char*, const char*); } ``` 链接指示包含关键字`extern`、字符串字面值常量和一个函数声明。其中的字符串字面值常量指出了编写函数所用的语言。 复合形式的链接指示可以应用于整个头文件。当一个`#include`指示被放置在复合链接指示的花括号中时,头文件中的所有函数声明都会被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含自带链接指示的函数,该函数不会受到影响。 ```c++ // compound-statement linkage directive extern "C" { #include // C functions that manipulate C-style strings } ``` C++从C语言继承的标准库函数可以定义成C函数,但并非必须。选择使用C还是C++实现C标准库,是由每个C++实现决定的。 编写函数所使用的语言是函数类型的一部分。因此对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示,而且指向这类函数的指针也必须使用与函数本身一样的链接指示。 ```c++ // pf points to a C function that returns void and takes an int extern "C" void (*pf)(int); ``` 指向C函数的指针与指向C++函数的指针是不同的类型,两者不能相互赋值或初始化(少数C++编译器支持这种赋值操作并将其视为对语言的扩展,但是从严格意义上来说它是非法的)。 ```c++ void (*pf1)(int); // points to a C++ function extern "C" void (*pf2)(int); // points to a C function pf1 = pf2; // error: pf1 and pf2 have different types ``` 链接指示不仅对函数本身有效,对作为返回类型或形参类型的函数指针也有效。所以如果希望给C++函数传入指向C函数的指针,必须使用类型别名。 ```c++ // f1 is a C function; its parameter is a pointer to a C function extern "C" void f1(void(*)(int)); // FC is a pointer to a C function extern "C" typedef void FC(int); // f2 is a C++ function with a parameter that is a pointer to a C function void f2(FC *); ``` 通过链接指示定义函数,可以令C++函数在其他语言编写的程序中可用。编译器会为该函数生成适合于指定语言的代码。 ```c++ // the calc function can be called from C programs extern "C" double calc(double dparm) { /* ... */ } ``` 如果需要在C和C++中编译同一个源文件,可以在编译C++版本时使用预处理定义`__cplusplus`。 ```c++ # ifdef __cplusplus // ok: we're compiling C++ extern "C" # endif int strcmp(const char*, const char*); ``` 链接指示与重载函数的相互作用依赖于目标语言。C语言不支持函数重载,所以一个C链接指示只能用于说明一组重载函数中的某一个。 ```c++ // error: two extern "C" functions with the same name extern "C" void print(const char*); extern "C" void print(int); ```