C++Primer读书总结


第一章 开始

std::cin 中的循环流使用

C++ 中可以使用while(std::cin >> value){//Code }的方式来进行循环数据的读入,直到没有输出为止;示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 输入样例: 3 4 5 6 输出 : Sum is :18 */

#inlude <iostream> int main()
{
int sum =0;value=0;
//循环读取数据

while(std::cin>>value){
sm+=value;
}
std::out<<"Sum is:"<<sum<<std:endl;
return 0;
}

注意:

  • 当键盘向程序中输入数据时,对于文件结束;Windows是Ctrl+Z然后 Enter或者Return;UNIX 中是Ctrl+D 然后再加enter;
  • 当缓冲区中有残留数据时,std::cin会直接去读取缓冲区的数据而不会请求键盘输入。重要的是,回车符也会被存在输入缓冲区中。
  • 当程序中有多个等待循环输入时,需要使用cin.clear()来重置循环状态,方便再次输入;

IO

  • #include <iostream>
  • std::cout << "hello"
  • std::cin >> v1

记住>><<返回的结果都是左操作数,也就是输入流和输出流本身。

endl:这是一个被称为操纵符(manipulator)的特殊值,效果是结束当前行,并将设备关联的缓冲区(buffer)中的内容刷到设备中。

UNIX和Mac下键盘输入文件结束符:ctrl+d,Windows下:ctrl+z

头文件:类的类型一般存储在头文件中,标准库的头文件使用<>,非标准库的头文件使用""。申明写在.h文件,定义实现写在.cpp文件。

避免多次包含同一头文件

1
2
3
4
#ifndef SALESITEM_H
#define SALESITEM_H
// Definition of Sales_itemclass and related functions goes here
#endif

成员函数(类方法):使用.调用。

命名空间(namespace):使用作用域运算符::调用。

第二章 变量和基本类型

基本内置类型

基本算数类型

类型 含义 最小尺寸
bool 布尔类型 8bits
char 字符 8bits
wchar_t 宽字符 16bits
char16_t Unicode字符 16bits
char32_t Unicode字符 32bits
short 短整型 16bits
int 整型 16bits (在32位机器中是32bits)
long 长整型 32bits
long long 长整型 64bits (是在C++11中新定义的)
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

注意:

  • 关于不同类型,字节内存分配的问题,不同的操作系统存在不同的内存分配策略;因此不一定按照上面的进行分配(倍数规则和整圆规则);
  • 对于C++中的字节对齐内容需要重点考虑
  • c++中除去布尔类型和扩展的字符类型之外,其它类型可以划分为带符号的(signed)和无符号的(unsigned)两种;无符号仅能表示大于0的值。器字节内存分配也有所不同;
据类型 32位 64位 取值范围(32位)
char 1 1 -128~127
unsigned char(当byte使用) 1 1 0~255
short int /short 2 2 –32,768~32,767
unsigned short 2 2 0~65,535
int 4 4 -2,147,483,648~2,147,483,647
unsigned int 4 4 0~4,294,967,295
long int /long 4 8 –2,147,483,648~2,147,483,647
unsigned long 4 8 0~4,294,967,295
long long int/long long 8 8 -9,223,372,036,854,775,808~9,223,372,036,854,775,807
指针 4 8
float 4 4 3.4E +/- 38 (7 digits)
double 8 8 1.7E +/- 308 (15 digits)
  • bool类型的取值是truefalse

  • 一个char的大小和一个机器字节一样,确保可以存放机器基本字符集中任意字符对应的数字值。wchar_t确保可以存放机器最大扩展字符集中的任意一个字符。

  • 在整型类型大小方面,C++规定shortintlonglong longlong long是C++11定义的类型)。

  • 浮点型可表示单精度(single-precision)、双精度(double-precision)和扩展精度(extended-precision)值,分别对应floatdoublelong double类型。

  • 除去布尔型和扩展字符型,其他整型可以分为带符号(signed)和无符号(unsigned)两种。带符号类型可以表示正数、负数和0,无符号类型只能表示大于等于0的数值。类型intshortlonglong long都是带符号的,在类型名前面添加unsigned可以得到对应的无符号类型,如unsigned int

  • 字符型分为charsigned charunsigned char三种,但是表现形式只有带符号和无符号两种。类型charsigned char并不一样, char的具体形式由编译器(compiler)决定。

如何选择类型

  • 当明确知晓数值不可能是负数时,选用无符号类型;
  • 使用int执行整数运算。一般long的大小和int一样,而short常常显得太小。除非超过了int的范围,选择long long
  • 算术表达式中不要使用charbool
  • 浮点运算选用double

类型转换

进行类型转换时,类型所能表示的值的范围决定了转换的过程。

  • 把非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true
  • 把布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
  • 把浮点数赋给整数类型时,进行近似处理,结果值仅保留浮点数中的整数部分。
  • 把整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
  • 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数(8比特大小的unsigned char能表示的数值总数是256)取模后的余数。
  • 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。

避免无法预知和依赖于实现环境的行为。

无符号数不会小于0这一事实关系到循环的写法。

1
2
3
// 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。

1
2
3
4
5
6
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;
}

不要混用带符号类型和无符号类型。

字面值常量

  • 一个形如42的值被称作字面值常量(literal)。

    • 整型和浮点型字面值。
    • 字符和字符串字面值。
      • 使用空格连接,继承自C。
      • 字符字面值:单引号, 'a'
      • 字符串字面值:双引号, "Hello World""
    • 转义序列。\n\t等。
    • 布尔字面值。truefalse
    • 指针字面值。nullptr
  • 0开头的整数代表八进制(octal)数,以0x0X开头的整数代表十六进制(hexadecimal)数。在C++14中,0b0B开头的整数代表二进制(binary)数。

  • 整型字面值具体的数据类型由它的值和符号决定。

  • C++14新增了单引号'形式的数字分隔符。数字分隔符不会影响数字的值,但可以通过分隔符将数字分组,使数值读写更容易。

    1
    2
    3
    // 按照书写形式,每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
1
2
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要用到后面跟着的所有数字。

    1
    2
    std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline
    std::cout << '\115' << '\n'; // prints M followed by a newline
  • 添加特定的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。

  • 使用一个长整型字面值时,最好使用大写字母L进行标记,小写字母l和数字1容易混淆。

变量

变量提供一个具名的、可供程序操作的存储空间。 C++变量对象一般可以互换使用。

变量定义(define)

  • 定义形式:类型说明符(type specifier)后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。定义时可以为一个或多个变量赋初始值(初始化,initialization)。

    int sum = 0, value, units_sold = 0;

  • 初始化(initialize):对象在创建时获得了一个特定的值。

    • 初始化不是赋值!:初始化不等于赋值(assignment)。初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,再用一个新值来替代。
    • 初始化 = 创建变量 + 赋予初始值
    • 赋值 = 擦除对象的当前值 + 用新值代替
    • 列表初始化:使用花括号{},如int units_sold{0};
    • 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化。
    • 建议初始化每一个内置类型的变量。
  • 用花括号初始化变量称为列表初始化(list initialization)。当用于内置类型的变量时,如果使用了列表初始化并且初始值存在丢失信息的风险,则编译器会报错。

    1
    2
    3
    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)负责创建与名字相关联的实体。

  • 为了支持分离式编译,C++将声明和定义区分开。声明使得名字为程序所知。定义负责创建与名字关联的实体。

  • extern:只是说明变量定义在其他地方。

    如果想声明一个变量而不定义它,就在变量名前添加关键字extern,并且不要显式地初始化变量。

    1
    2
    extern int i; // declares but does not define i
    int j; // declares and defines j

    extern语句如果包含了初始值就不再是声明了,而变成了定义。

  • 只声明而不定义: 在变量名前添加关键字 extern,如extern int i;。但如果包含了初始值,就变成了定义:extern double pi = 3.14;

  • 变量只能被定义一次,但是可以多次声明。

  • 名字的作用域(namescope)

  • 如果要在多个文件中使用同一个变量,就必须将声明和定义分开。此时变量的定义必须出现且只能出现在一个文件中,其他使用该变量的文件必须对其进行声明,但绝对不能重复定义。

标识符(Identifiers)

C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感。C++为标准库保留了一些名字。用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。

名字的作用域(Scope of a Name)

  • 定义在函数体之外的名字拥有全局作用域(global scope)。声明之后,该名字在整个程序范围内都可使用。

  • 最好在第一次使用变量时再去定义它。这样做更容易找到变量的定义位置,并且也可以赋给它一个比较合理的初始值。

  • 作用域中一旦声明了某个名字,在它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字,此时内层作用域中新定义的名字将屏蔽外层作用域的名字。

  • 可以用作用域操作符::来覆盖默认的作用域规则。因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,会向全局作用域发出请求获取作用域操作符右侧名字对应的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
// 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;
}

如果函数有可能用到某个全局变量,则不宜再定义一个同名的局部变量。

左值和右值

  • 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
  • 右值(r-value)只能出现在赋值语句的右边,比如常量。

复合类型(Compound Type)

引用(References)

  • 引用:引用是一个对象的别名,引用类型引用(refer to)另外一种类型。如int &refVal = val;

    1
    2
    3
    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是变量名称。如果在一条语句中定义了多个指针变量,则每个量前都必须有符号*

1
2
int *ip1, *ip2;     // both ip1 and ip2 are pointers to int
double dp, *dp2; // dp2 is a pointer to double; dp is a double

指针存放某个对象的地址,要想获取对象的地址,需要使用取地址符&

1
2
int ival = 42;
int *p = &ival; // p holds the address of ival; p is a pointer to ival

因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

声明语句中指针的类型实际上被用于指定它所指向的对象类型。大部分情况下,指针的类型要和它指向的对象严格匹配。


指针的值(即地址)应属于下列状态之一:

  • 指向一个对象。
  • 指向紧邻对象所占空间的下一个位置。
  • 空指针,即指针没有指向任何对象。
  • 无效指针,即上述情况之外的其他值。

  • 试图拷贝或以其他方式访问无效指针的值都会引发错误。

  • 如果指针指向一个对象,可以使用解引用(dereference)符*来访问该对象。

1
2
3
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初始化空指针。

1
2
3
4
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):

1
2
3
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = &pi; // ppi points to a pointer to an int

指向指针的引用(References to Pointers):

1
2
3
4
5
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可以创建值不能被改变的对象。const变量必须被初始化。
  • const变量默认不能被其他文件访问,非要访问,必须在指定const前加extern。
1
2
const int bufSize = 512;    // input buffer size
bufSize = 512; // error: attempt to write to const object

const的引用 (References to const)

  • reference to const(对常量的引用):指向const对象的引用。

  • 临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。

  • 对临时量的引用是非法行为。

  • 把引用绑定在const对象上即为对常量的引用(reference to const)。对常量的引用不能被用作修改它所绑定的对象。

    1
    2
    3
    4
    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

    大部分情况下,引用的类型要和与之绑定的对象严格匹配。但是有两个例外:

  • 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。

    1
    2
    3
    4
    5
    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
  • 允许为一个常量引用绑定非常量的对象、字面值或者一般表达式。

    1
    2
    double dval = 3.14;
    const int &ri = dval;

    指针和const(Pointers and const)

  • 指向常量的指针(pointer to const)不能用于修改其所指向的对象。常量对象的地址只能使用指向常量的指针来存放,但是指向常量的指针可以指向一个非常量对象。

    1
    2
    3
    4
    5
    6
    const double pi = 3.14;     // pi is const; its value may not be changed
    double *ptr = &pi; // error: ptr is a plain pointer
    const double *cptr = &pi; // 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)必须初始化。

    1
    2
    3
    4
    int errNumb = 0;
    int *const curErr = &errNumb; // curErr will always point to errNumb
    const double pi = 3.14159;
    const double *const pip = &pi; // pip is a const pointer to a const object
  • 指针本身是常量并不代表不能通过指针修改其所指向的对象的值,能否这样做完全依赖于其指向对象的类型。

顶层const(Top-Level const)

  • 顶层const表示指针本身是个常量,底层const(low-level const)表示指针所指的对象是一个常量。指针类型既可以是顶层const也可以是底层const

    1
    2
    3
    4
    5
    6
    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没有影响。拷贝操作不会改变被拷贝对象的值,因此拷入和拷出的对象是否是常量无关紧要。

    1
    2
    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资格。或者两个对象的数据类型可以相互转换。一般来说,非常量可以转换成常量,反之则不行。

    1
    2
    3
    4
    5
    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)指值不会改变并且在编译过程就能得到计算结果的表达式。

  • 一个对象是否为常量表达式由它的数据类型和初始值共同决定。

  • 常量表达式(constant expressions)指值不会改变并且在编译过程就能得到计算结果的表达式。

  • 一个对象是否为常量表达式由它的数据类型和初始值共同决定。

    1
    2
    3
    4
    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类型以便由编译器来验证变量的值是否是一个常量表达式。

    1
    2
    3
    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

    1
    2
    3
    constexpr int *p = nullptr;     // p是指向int的const指针
    constexpr int i = 0;
    constexpr const int *cp = &i; // cp是指向const int的const指针
  • constconstexpr限定的值都是常量。但constexpr对象的值必须在编译期间确定,而const对象的值可以延迟到运行期间确定。

  • 建议使用constexpr修饰表示数组大小的对象,因为数组的大小必须在编译期间确定且不能改变。

类型别名(Type Aliases)

  • 传统别名:使用typedef来定义类型的同义词。 typedef double wages;
  • 新标准别名:别名声明(alias declaration): using SI = Sales_item;(C++11)

auto类型说明符

  • auto类型说明符:让编译器自动推断类型

  • C++11新增auto类型说明符,能让编译器自动分析表达式所属的类型。auto定义的变量必须有初始值。

    1
    2
    int i = 0, &r = i;
    auto a = r; // a is an int (r is an alias for i, which has type int)

编译器推断出来的auto类型有时和初始值的类型并不完全一样。

  • 当引用被用作初始值时,编译器以引用对象的类型作为auto的类型。

    1
    2
    int i = 0, &r = i;
    auto a = r; // a is an int (r is an alias for i, which has type int)
  • auto一般会忽略顶层const

    1
    2
    3
    4
    5
    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

    1
    const auto f = ci;  // deduced type of ci is int; f has type const int

    设置类型为auto的引用时,原来的初始化规则仍然适用,初始值中的顶层常量属性仍然保留。

1
2
3
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类型指示符,作用是选择并返回操作数的数据类型,此过程中编译器不实际计算表达式的值。

    1
    decltype(f()) sum = x;  // sum has whatever type f returns
  • decltype处理顶层const和引用的方式与auto有些不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用)。

    1
    2
    3
    4
    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)

struct

  • 类可以以关键字struct开始,紧跟类名和类体。
  • 类数据成员:类体定义类的成员。
  • C++11:可以为类数据成员提供一个类内初始值(in-class initializer)。
  • 类内初始值不能使用圆括号。
  • 类定义的最后应该加上分号。

编写自己的头文件

  • 头文件通常包含哪些只能被定义一次的实体:类、constconstexpr变量。

  • 头文件一旦改变,相关的源文件必须重新编译以获取更新之后的声明。

  • 预处理器(preprocessor):确保头文件多次包含仍能安全工作。

  • 当预处理器看到#include标记时,会用指定的头文件内容代替#include

  • 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。

    1
    2
    3
    4
    5
    6
    #ifndef SALES_DATA_H
    #define SALES_DATA_H
    strct Sale_data{
    ...
    }
    #endif
  • 在高级版本的IDE环境中,可以直接使用#pragma once命令来防止头文件的重复包含。

  • 预处理变量无视C++语言中关于作用域的规则。

  • 整个程序中的预处理变量,包括头文件保护符必须唯一。预处理变量的名字一般均为大写。

  • 头文件即使目前还没有被包含在任何其他头文件中,也应该设置保护符。

第三章 字符串、向量和数组

命名空间的using声明(Namespace using Declarations)

使用using声明后就无须再通过专门的前缀去获取所需的名字了。

1
using std::cout;
  • 程序中使用的每个名字都需要用独立的using声明引入。

  • 头文件中不应该包含using声明。这样使用了该头文件的源码也会使用这个声明,会带来风险。

string

  • 标准库类型string表示可变长的字符序列,定义在头文件string中。

  • #include <string>,然后 using std::string;

  • string对象:注意,不同于字符串字面值。

定义和初始化string对象

初始化string对象的方式:

方式 解释
string s1 默认初始化,s1是个空字符串
string s2(s1) s2s1的副本
string s2 = s1 等价于s2(s1)s2s1的副本
string s3("value") s3是字面值“value”的副本,除了字面值最后的那个空字符外
string s3 = "value" 等价于s3("value")s3是字面值"value"的副本
string s4(n, 'c') s4初始化为由连续n个字符c组成的串
  • 拷贝初始化(copy initialization):使用等号=将一个已有的对象拷贝到正在创建的对象。
  • 直接初始化(direct initialization):通过括号给对象赋值。
  • 如果使用等号初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,则执行的是直接初始化(direct initialization)。

string对象上的操作

string的操作:

操作 解释
os << s s写到输出流os当中,返回os
is >> s is中读取字符串赋给s,字符串以空白分割,返回is
getline(is, s) is中读取一行赋给s,返回is
s.empty() s为空返回true,否则返回false
s.size() 返回s中字符的个数
s[n] 返回s中第n个字符的引用,位置n从0计起
s1+s2 返回s1s2连接后的结果
s1=s2 s2的副本代替s1中原来的字符
s1==s2 如果s1s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感
s1!=s2 同上
<, <=, >, >= 利用字符在字典中的顺序进行比较,且对字母的大小写敏感
  • string io:

    • 执行读操作>>:忽略掉开头的空白(包括空格、换行符和制表符),直到遇到下一处空白为止。
    • getline:读取一整行,包括空白符
  • 字符串字面值和string是不同的类型。

  • 在执行读取操作时,string对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。

  • 使用getline函数可以读取一整行字符。该函数只要遇到换行符就结束读取并返回结果,如果输入的开始就是一个换行符,则得到空string。触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。

  • size函数返回string对象的长度,返回值是string::size_type类型,这是一种无符号类型。要使用size_type,必须先指定它是由哪种类型定义的。

  • 如果一个表达式中已经有了size函数就不要再使用int了,这样可以避免混用intunsigned int可能带来的问题。

  • 当把string对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是string

    1
    2
    3
    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对象中的字符

cctype头文件中定义了一组标准函数:

函数 解释
isalnum(c) c是字母或数字时为真
isalpha(c) c是字母时为真
iscntrl(c) c是控制字符时为真
isdigit(c) c是数字时为真
isgraph(c) c不是空格但可以打印时为真
islower(c) c是小写字母时为真
isprint(c) c是可打印字符时为真
ispunct(c) c是标点符号时为真
isspace(c) c是空白时为真(空格、横向制表符、纵向制表符、回车符、换行符、进纸符)
isupper(c) c是大写字母时为真
isxdigit(c) c是十六进制数字时为真
tolower(c) c是大写字母,输出对应的小写字母;否则原样输出c
toupper(c) c是小写字母,输出对应的大写字母;否则原样输出c

  • C++11提供了范围for(range for)语句,可以遍历给定序列中的每个元素并执行某种操作。

    1
    2
    for (declaration : expression)
    statement

  • expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration部分的变量都会被初始化为expression部分的下一个元素值。

    1
    2
    3
    4
    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<int>

  • 因为引用不是对象,所以不存在包含引用的vector

  • 在早期的C++标准中,如果vector的元素还是vector,定义时必须在外层vector对象的右尖括号和其元素类型之间添加一个空格,如vector<vector<int> >。但是在C++11标准中,可以直接写成vector<vector<int>>,不需要添加空格。

定义和初始化vector对象

初始化vector对象的方法

方法 解释
vector<T> v1 v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1) v2中包含有v1所有元素的副本
vector<T> v2 = v1 等价于v2(v1)v2中包含v1所有元素的副本
vector<T> v3(n, val) v3包含了n个重复的元素,每个元素的值都是val
vector<T> v4(n) v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a, b, c...} v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5={a, b, c...} 等价于v5{a, b, c...}
  • 列表初始化: vector<string> v{"a", "an", "the"}; (C++11)可以只提供vector对象容纳的元素数量而省略初始值,此时会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中的元素类型决定。
  • 初始化vector对象时如果使用圆括号,可以说提供的值是用来构造(construct)vector对象的;如果使用的是花括号,则是在列表初始化(list initialize)该vector对象。

其他vector操作

vector支持的操作:

操作 解释
v.emtpy() 如果v不含有任何元素,返回真;否则返回假
v.size() 返回v中元素的个数
v.push_back(t) v的尾端添加一个值为t的元素
v[n] 返回v中第n个位置上元素的引用
v1 = v2 v2中的元素拷贝替换v1中的元素
v1 = {a,b,c...} 用列表中元素的拷贝替换v1中的元素
v1 == v2 v1v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
v1 != v2 同上
<,<=,>, >= 以字典顺序进行比较
  • 范围for语句内不应该改变其遍历序列的大小。

  • vector对象(以及string对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能用于添加元素。

  • size函数返回vector对象中元素的个数,返回值是由vector定义的size_type类型。vector对象的类型包含其中元素的类型。

    1
    2
    vector<int>::size_type  // ok
    vector::size_type // error

  • vectorstring对象的下标运算符只能用来访问已经存在的元素,而不能用来添加元素。

    1
    2
    3
    4
    5
    6
    vector<int> 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
    }
    ### 迭代器iterator

  • 标准库容器可以使用迭代器。

  • 类似于指针类型,迭代器也提供了对对象的间接访问。

  • 迭代器的作用和下标类似,但是更加通用。所有标准库容器都可以使用迭代器,但是其中只有少数几种同时支持下标运算符。

使用迭代器

  • 容器:可以包含其他对象;但所有的对象必须类型相同。

  • 迭代器(iterator):每种标准容器都有自己的迭代器。C++倾向于用迭代器而不是下标遍历元素。

  • const_iterator:只能读取容器内元素不能改变。

  • 箭头运算符: 解引用 + 成员访问,it->mem等价于 (*it).mem

  • 谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。

  • 定义了迭代器的类型都拥有beginend两个成员函数。begin函数返回指向第一个元素的迭代器,end函数返回指向容器“尾元素的下一位置(one past the end)”的迭代器,通常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。尾后迭代器仅是个标记,表示程序已经处理完了容器中的所有元素。迭代器一般为iterator类型。

    1
    2
    // 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

  • 如果容器为空,则beginend返回的是同一个迭代器,都是尾后迭代器。


标准容器迭代器的运算符:

运算符 解释
*iter 返回迭代器iter所指向的元素的引用
iter->mem 等价于(*iter).mem
++iter iter指示容器中的下一个元素
--iter iter指示容器中的上一个元素
iter1 == iter2 判断两个迭代器是否相等
  • 因为end返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。

  • for或者其他循环语句的判断条件中,最好使用!=而不是<。所有标准库容器的迭代器都定义了==!=,但是只有其中少数同时定义了<运算符。

  • 如果vectorstring对象是常量,则只能使用const_iterator迭代器,该迭代器只能读元素,不能写元素。

  • beginend返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回const_iterator;如果对象不是常量,则返回iterator

    1
    2
    3
    4
    vector<int> v;
    const vector<int> cv;
    auto it1 = v.begin(); // it1 has type vector<int>::iterator
    auto it2 = cv.begin(); // it2 has type vector<int>::const_iterator

  • C++11新增了cbegincend函数,不论vectorstring对象是否为常量,都返回const_iterator迭代器。

  • 任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。

迭代器运算

vectorstring迭代器支持的运算:

运算符 解释
iter + n 迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。
iter - n 迭代器减去一个证书仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。
iter1 += n 迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1
iter1 -= n 迭代器减法的复合赋值语句,将iter2减n的加过赋给iter1
iter1 - iter2 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
>>=<<= 迭代器的关系运算符,如果某迭代器
  • difference_type:保证足够大以存储任何两个迭代器对象间的距离,可正可负。

数组

  • 相当于vector的低级版,长度固定
  • 数组类似vector,但数组的大小确定不变,不能随意向数组中添加元素。
  • 如果不清楚元素的确切个数,应该使用vector

定义和初始化内置数组 (Defining and Initializing Built-in Arrays)

  • 数组是一种复合类型,声明形式为a[d],其中a是数组名称,d是数组维度(dimension)。维度必须是一个常量表达式。

  • 默认情况下,数组的元素被默认初始化。

  • 定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值列表推断类型。

  • 如果定义数组时提供了元素的初始化列表,则允许省略数组维度,编译器会根据初始值的数量计算维度。但如果显式指明了维度,那么初始值的数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化。

    1
    2
    3
    4
    5
    6
    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

  • 可以用字符串字面值初始化字符数组,但字符串字面值结尾处的空字符也会一起被拷贝到字符数组中。

    1
    2
    3
    4
    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!

  • 不能用一个数组初始化或直接赋值给另一个数组。

  • 从数组的名字开始由内向外阅读有助于理解复杂数组声明的含义。

    1
    2
    3
    4
    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
    ### 访问数组元素

  • 数组下标的类型:size_t 。这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。size_t定义在头文件cstddef中。

  • 字符数组的特殊性:结尾处有一个空字符,如 char a[] = "hello";

  • 用数组初始化 vectorint a[] = {1,2,3,4,5}; vector<int> v(begin(a), end(a));

  • 大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。

数组和指针

  • 在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。

    1
    2
    3
    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]
    一维数组寻址公式: \[ \begin{eqnarray} &&\text { Type array[N]; }\\ &&{ array }[n]:^{\star}(\text { array }+n \times \text { sizeof ( Type ) }) \end{eqnarray} \]


  • 当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。但decltype关键字不会发生这种转换,直接返回数组类型。

    1
    2
    3
    4
    5
    6
    7
    8
    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中定义了两个名为beginend的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。

    1
    2
    3
    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继承来的字符串。

  • C风格字符串是将字符串存放在字符数组中,并以空字符结束(null terminated)。这不是一种类型,而是一种为了表达和使用字符串而形成的书写方法。

  • C++标准支持C风格字符串,但是最好不要在C++程序中使用它们。对大多数程序来说,使用标准库string要比使用C风格字符串更加安全和高效。

  • C风格字符串函数不负责验证其参数的正确性,传入此类函数的指针必须指向以空字符作为结尾的数组。

  • 用空字符结束(\0)。

  • 对大多数应用来说,使用标准库 string比使用C风格字符串更安全、更高效。

C标准库String函数,定义在<cstring> 中:

函数 介绍
strlen(p) 返回p的长度,空字符不计算在内
strcmp(p1, p2) 比较p1p2的相等性。如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值。
strcat(p1, p2) p2附加到p1之后,返回p1
strcpy(p1, p2) p2拷贝给p1,返回p1

与旧代码的接口(Interfacing to Older Code)

任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。

  • string对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。

  • string对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧运算对象。

  • 不能用string对象直接初始化指向字符的指针。为了实现该功能,string提供了一个名为c_str的成员函数,返回const char*类型的指针,指向一个以空字符结束的字符数组,数组的数据和string对象一样。

    1
    2
    3
    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对象,但是需要指明要拷贝区域的首元素地址和尾后地址。

    1
    2
    3
    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<int> ivec(begin(int_arr), end(int_arr));

  • 在新版本的C++程序中应该尽量使用vectorstring和迭代器,避免使用内置数组、C风格字符串和指针。

多维数组(Multidimensional Arrays)

  • C++中的多维数组其实就是数组的数组。

  • 当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。

  • 多维数组的初始化int ia[3][4] = {{0,1,2,3}, ...}

  • 使用范围for语句时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

  • 多维数组初始化的几种方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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};
  • 可以使用下标访问多维数组的元素,数组的每个维度对应一个下标运算符。如果表达式中下标运算符的数量和数组维度一样多,则表达式的结果是给定类型的元素。如果下标运算符数量比数组维度小,则表达式的结果是给定索引处的一个内层数组。

    1
    2
    3
    // 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
  • 多维数组寻址公式:

\[ \begin{eqnarray} &&\text {Type array[N][M][L];} \\ &&array [n][m][l]:^{\star}( array +n \times sizeof ( Type[M][L] ) +m \times sizeof ( Type[L] ) ) +l \times sizeof ( Type) \end{eqnarray} \]


  • 使用范围for语句处理多维数组时,为了避免数组被自动转换成指针,语句中的外层循环控制变量必须声明成引用类型。

    1
    2
    3
    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*内遍历,程序将无法通过编译。

    1
    2
    for (auto row : ia)
    for (auto col : row)

  • 使用范围for语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。

    因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。

    1
    2
    3
    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

  • 声明指向数组类型的指针时,必须带有圆括号。

    1
    2
    int *ip[4];     // array of pointers to int
    int (*ip)[4]; // pointer to an array of four ints

  • 使用autodecltype能省略复杂的指针定义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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;
    }

第四章 表达式

表达式基础(Fundamentals)

  • 表达式(expression)由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。

  • C++定义了一元运算符(unary operator)和二元运算符(binary operator)。除此之外,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。

  • 表达式求值过程中,小整数类型(如boolcharshort等)通常会被提升(promoted)为较大的整数类型,主要是int

  • C++定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自定义其含义,这被称作运算符重载(overloaded operator)。

  • C++的表达式分为右值(rvalue)和左值(lvalue)。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的地址。需要右值的地方可以用左值代替,反之则不行。

    • 赋值运算符需要一个非常量左值作为其左侧运算对象,返回结果也是一个左值。
    • 取地址符作用于左值运算对象,返回指向该运算对象的指针,该指针是一个右值。
    • 内置解引用运算符、下标运算符、迭代器解引用运算符、stringvector的下标运算符都返回左值。
    • 内置类型和迭代器的递增递减运算符作用于左值运算对象。前置版本返回左值,后置版本返回右值。
  • 如果decltype作用于一个求值结果是左值的表达式,会得到引用类型。

优先级与结合律(Precedence and Associativity)

  • 复合表达式(compound expression)指含有两个或多个运算符的表达式。

  • 优先级与结合律决定了运算对象的组合方式。

  • 括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。

求值顺序(Order of Evaluation)

对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。

1
2
int i = 0;
cout << i << " " << ++i << endl; // undefined

处理复合表达式时建议遵循以下两点:

  • 不确定求值顺序时,使用括号来强制让表达式的组合关系符合程序逻辑的要求。
  • 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。

当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,第二条规则无效。如*++iter,递增运算符改变了iter的值,而改变后的iter又是解引用运算符的运算对象。类似情况下,求值的顺序不会成为问题。

算术运算符(Logical and Relational Operators)

  • 关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。

  • 在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。

  • 溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。

运算符 描述
+ 把两个操作数相加
- 从第一个操作数中减去第二个操作数
* 把两个操作数相乘
/ 分子除以分母
% 取模运算符,整除后的余数
++ 自增运算符,整数值增加 1
-- 自减运算符,整数值减少 1

逻辑和关系运算符(Logical and Relational Operators)

  • 关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。

  • 逻辑与(logical AND)运算符&&和逻辑或(logical OR)运算符||都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)

    • 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
    • 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。

    进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值truefalse作为运算对象。

运算符 描述
== 检查两个操作数的值是否相等,如果相等则条件为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。

赋值运算符(Assignment Operators)

  • 赋值运算符=的左侧运算对象必须是一个可修改的左值。

  • C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。

    1
    2
    vector<int> vi;     // initially empty
    vi = {0,1,2,3,4,5,6,7,8,9}; // vi now has ten elements, values 0 through 9

  • 赋值运算符满足右结合律。

    1
    2
    int ival, jval;
    ival = jval = 0; // ok: each assigned 0

  • 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。

  • 不要混淆相等运算符==和赋值运算符=

  • 复合赋值运算符包括+=-=*=/=%=<<=>>=&=^=|=。任意一种复合运算都完全等价于a = a op b

  • 如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。

  • 赋值运算符满足右结合律,这点和其他二元运算符不一样。 ival = jval = 0;等价于ival = (jval = 0);

  • 赋值运算优先级比较低。

递增和递减运算符(Increment and Decrement Operators)

递增和递减运算符是为对象加1或减1的简洁书写形式。很多不支持算术运算的迭代器可以使用递增和递减运算符。

递增和递减运算符分为前置版本和后置版本:

  • 前置版本首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
  • 后置版本也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。
1
2
3
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

除非必须,否则不应该使用递增或递减运算符的后置版本。后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。

在某些语句中混用解引用和递增运算符可以使程序更简洁。

1
cout << *iter++ << endl;

成员访问运算符(The Member Access Operators)

点运算符.和箭头运算符->都可以用来访问成员,表达式ptr->mem等价于(*ptr).mem

1
2
3
4
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)

条件运算符的使用形式如下:

1
cond ? expr1 : expr2;
  • 其中cond是判断条件的表达式,如果cond为真则对expr1求值并返回该值,否则对expr2求值并返回该值。

  • 只有当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果才是左值,否则运算的结果就是右值。

  • 条件运算符可以嵌套,但是考虑到代码的可读性,运算的嵌套层数最好不要超过两到三层。

  • 条件运算符的优先级非常低,因此当一个长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。

位运算符(The Bitwise Operators)

  • 在位运算中符号位如何处理并没有明确的规定,所以建议仅将位运算符用于无符号类型的处理。

  • 左移运算符<<在运算对象右侧插入值为0的二进制位。右移运算符>>的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。

位运算符(左结合律):

运算符 功能
~ 位求反
<< 左移,相当于乘2^n
>> 右移,相当于除2^n
& 位与
^ 位异或
"|" 位或

sizeof运算符(The sizeof Operator)

sizeof运算符返回一个表达式或一个类型名字所占的字节数,返回值是size_t类型。

sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。

sizeof运算符的结果部分依赖于其作用的类型:

  • char或者类型为char的表达式执行sizeof运算,返回值为1。
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
  • 对指针执行sizeof运算得到指针本身所占空间的大小。
  • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需要有效。
  • 对数组执行sizeof运算得到整个数组所占空间的大小。
  • stringvector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。

逗号运算符(Comma Operator)

逗号运算符,含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在for循环中。

1
2
3
4
vector<int>::size_type cnt = ivec.size();
// assign values from size... 1 to the elements in ivec
for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)
ivec[ix] = cnt;

类型转换(Type Conversions)

隐式类型转换(implicit conversions)

  • 无须程序员介入,会自动执行的类型转换叫做隐式转换(implicit conversions)。
  • int类型小的整数值先提升为较大的整数类型。
  • 条件中,非布尔转换成布尔。
  • 初始化中,初始值转换成变量的类型。
  • 算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。
  • 函数调用时。

显式类型转换(Explicit Conversions)

  • 显式类型转换也叫做强制类型转换(cast)。虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。

  • static_cast:任何明确定义的类型转换,只要不包含底层const,都可以使用。 double slope = static_cast<double>(j);

  • dynamic_cast:支持运行时类型识别。

  • const_cast:只能改变运算对象的底层const,一般可用于去除const性质。 const char *pc; char *p = const_cast<char*>(pc)

  • reinterpret_cast:通常为运算对象的位模式提供低层次上的重新解释。

  • 早期版本的C++语言中,显式类型转换包含两种形式:

    1
    2
    type (expression);    // function-style cast notation
    (type) expression; // C-language-style cast notation

    算术转换(Integral Promotions)

  • 把一种算术类型转换成另一种算术类型叫做算术转换。

  • 整型提升(integral promotions)负责把小整数类型转换成较大的整数类型。

其他隐式类型转换(Other Implicit Conversions)

  • 在大多数表达式中,数组名字自动转换成指向数组首元素的指针。

  • 常量整数值0或字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*

  • 任意一种算术类型或指针类型都能转换成布尔类型。如果指针或算术类型的值为0,转换结果是false,否则是true

  • 指向非常量类型的指针能转换成指向相应的常量类型的指针。


运算符优先级表格如下

优先级 运算符 说明 结合性
1 :: 范围解析 自左向右
2 ++ - - 后缀自增/后缀自减
() 括号
[] 数组下标
. 成员选择(对象)
-> 成员选择(指针)
3 ++ - - 前缀自增/前缀自减 自右向左
+ - 加减
! ~ 逻辑非/按位取反
(type) 强制类型转换
* 取指针指向的值
& 某某的地址
sizeof 某某的大小
new,new[] 动态内存分配/动态数组内存分配
delete,delete[] 动态内存分配/动态数组内存释放
4 .* ->* - - 成员对象选择/成员指针选择 自左向右
5 * / % 乘法/除法/取余
6 + - 加号/减号
7 << >> 位左移/位右移
8 < <= 小于/小于等于
> >= 大于/大于等于
9 == != 等于/不等于
10 & 按位与
11 ^ 按位异或
12 | 按位或
13 && 与运算
14 || 或运算
15 ?: 三目运算符 自右向左
16 = 赋值
+= -= 相加后赋值/相减后赋值
*= /= %= 相乘后赋值相值/取余后赋值
<<= >>= 位左移赋值/位右移赋值
&= ^= |= 位与运算后赋值/位异或运算后赋值/位或运算后赋值
17 throw 抛出异常
18 , 逗号 自左向右

第五章 语句

简单语句(Simple Statements)

  • 如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,则应该使用空语句(null statement)。空语句中只含有一个单独的分号;
1
2
3
// read until we hit end-of-file or find an input equal to sought
while (cin >> s && s != sought)
; // null statement
  • 使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。

多余的空语句并非总是无害的。

1
2
3
// 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)

可以在ifswitchwhilefor语句的控制结构内定义变量,这些变量只在相应语句的内部可见,一旦语句结束,变量也就超出了其作用范围。

1
2
3
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语句的形式:

1
2
if (condition)
statement

if-else语句的形式:

1
2
3
4
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语句的形式:

  • 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分支定义并初始化一个变量,则应该把变量定义在块内。

1
2
3
4
5
6
case true:
{
// ok: declaration statement within a statement block
string file_name = get_file_name();
// ...
}

迭代语句(Iterative Statements)

迭代语句通常称为循环,它重复执行操作直到满足某个条件才停止。whilefor语句在执行循环体之前检查条件,do-while语句先执行循环体再检查条件。

while语句(The while Statement)

while语句的形式:

1
2
while (condition)
statement
  • 只要condition的求值结果为true,就一直执行statement(通常是一个块)。condition不能为空,如果condition第一次求值就是falsestatement一次都不会执行。

  • 定义在while条件部分或者循环体内的变量每次迭代都经历从创建到销毁的过程。

  • 在不确定迭代次数,或者想在循环结束后访问循环控制变量时,使用while比较合适。

传统的for语句(Traditional for Statement)

for语句的形式:

1
2
for (initializer; condition; expression)
statement
  • 一般情况下,initializer负责初始化一个值,这个值会随着循环的进行而改变。

  • condition作为循环控制的条件,只要condition的求值结果为true,就执行一次statement

  • 执行后再由expression负责修改initializer初始化的变量,这个变量就是condition检查的对象。如果condition第一次求值就是falsestatement一次都不会执行。initializer中也可以定义多个对象,但是只能有一条声明语句,因此所有变量的基础类型必须相同。

  • for语句头中定义的对象只在for循环体内可见。

范围for语句(Range for Statement)

范围for语句的形式:

1
2
for (declaration : expression)
statement
  • 其中expression表示一个序列,拥有能返回迭代器的beginend成员。declaration定义一个变量,序列中的每个元素都应该能转换成该变量的类型(可以使用auto)。

  • 如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。每次迭代都会重新定义循环控制变量,并将其初始化为序列中的下一个值,之后才会执行statement

do-while语句(The do-while Statement)

do-while语句的形式:

1
2
3
do
statement
while (condition);
  • 计算condition的值之前会先执行一次statementcondition不能为空。

  • 如果condition的值为false,循环终止,否则重复执行statement

  • 因为do-while语句先执行语句或块,再判断条件,所以不允许在条件部分定义变量。

跳转语句(Jump Statements)

跳转语句中断当前的执行过程。

break语句(The break Statement)

break语句只能出现在迭代语句或者switch语句的内部,负责终止离它最近的whiledo-whilefor或者switch语句,并从这些语句之后的第一条语句开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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语句中断当前迭代后,具体操作视迭代语句类型而定:

  • 对于whiledo-while语句来说,继续判断条件的值。
  • 对于传统的for语句来说,继续执行for语句头中的第三部分,之后判断条件的值。
  • 对于范围for语句来说,是用序列中的下一个元素初始化循环变量。

goto语句(The goto Statement)

goto语句(labeled statement)是一种特殊的语句,在它之前有一个标识符和一个冒号。

1
end: return; // labeled statement; may be the target of a goto

标签标识符独立于变量和其他标识符的名字,它们之间不会相互干扰。

goto语句的形式:

1
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语句块的通用形式:

1
2
3
4
5
6
7
8
9
10
11
12
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函数并终止当前程序的执行。

第六章 函数

函数基础(Function Basics)

典型的函数定义包括返回类型(return type)、函数名字、由0个或多个形式参数(parameter,简称形参)组成的列表和函数体(function body)。函数执行的操作在函数体中指明。

1
2
3
4
5
6
7
8
// 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,简称实参)列表,用来初始化函数形参。调用表达式的类型就是函数的返回类型。

1
2
3
4
5
6
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语句中的值(可能没有值)。
  • 将控制权从被调函数转移回主调函数,函数的返回值用于初始化调用表达式的结果。

  • 实参是形参的初始值,两者的顺序和类型必须一一对应。

  • 函数的形参列表可以为空,但是不能省略。

1
2
void f1() { /* ... */ }      // implicit void parameter list
void f2(void) { /* ... */ } // explicit void parameter list

形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。

1
2
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)

  • 如果形参不是引用类型,则函数对形参做的所有操作都不会影响实参。

  • 使用指针类型的形参可以访问或修改函数外部的对象。

1
2
3
4
5
6
// 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)

通过使用引用形参,函数可以改变实参的值。

1
2
3
4
5
// 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)

因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式。

1
2
3
4
// 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)
  • 因为数组会被转换成指针,所以当我们传递给函数一个数组时,实际上传递的是指向数组首元素的指针。

  • 因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。

  • 以数组作为形参的函数必须确保使用数组时不会越界。

  • 如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向常量的指针。

  • 形参可以是数组的引用,但此时维度是形参类型的一部分,函数只能作用于指定大小的数组。

  • 将多维数组传递给函数时,数组第二维(以及后面所有维度)的大小是数组类型的一部分,不能省略。

1
2
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函数传递参数,形式如下:

1
2
int main(int argc, char *argv[]) { /*...*/ }
int main(int argc, char **argv) { /*...*/ }

第二个形参argv是一个数组,数组元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。

当实参传递给main函数后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

含有可变形参的函数(Functions with Varying Parameters)

C++11新标准提供了两种主要方法处理实参数量不定的函数。

  • 如果实参类型相同,可以使用initializer_list标准库类型。

    1
    2
    3
    4
    5
    6
    void error_msg(initializer_list<string> il)
    {
    for (auto beg = il.begin(); beg != il.end(); ++beg)
    cout << *beg << " " ;
    cout << endl;
    }
  • 如果实参类型不同,可以定义可变参数模板。

  • C++还可以使用省略符形参传递可变数量的实参,但这种功能一般只用在与C函数交换的接口程序中。

  • initializer_list是一种标准库类型,定义在头文件initializer_list中,表示某种特定类型的值的数组。

initializer_list提供的操作:

操作 解释
initializer_list <T> lst 默认初始化;类型的空列表
initializer_list <T> lst {a,b,c...} lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst) or lst2=lst 拷贝复制一个元素
lst.size() 列表中的元素数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中尾元素下一位置的指针
  • 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素。拷贝后,原始列表和副本共享元素。

  • initializer_list对象中的元素永远是常量值。

  • 如果想向initializer_list形参传递一个值的序列,则必须把序列放在一对花括号内。

1
2
3
4
if (expected != actual)
error_msg(ErrCode(42), {"functionX", expected, actual});
else
error_msg(ErrCode(0), {"functionX", "okay"});
  • 因为initializer_list包含beginend成员,所以可以使用范围for循环处理其中的元素。

  • 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应该用于其他目的。

  • 省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

返回类型和return语句(Return Types and the return Statement)

return语句有两种形式,作用是终止当前正在执行的函数并返回到调用该函数的地方。

1
2
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语句,否则程序就是错误的,但很多编译器无法发现此错误。

  • 函数返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

  • 如果函数返回引用类型,则该引用仅仅是它所引用对象的一个别名。

  • 函数不应该返回局部对象的指针或引用,因为一旦函数完成,局部对象将被释放。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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规定,函数可以返回用花括号包围的值的列表。同其他返回类型一样,列表也用于初始化表示函数调用结果的临时量。如果列表为空,临时量执行值初始化;否则返回的值由函数的返回类型决定。

  • 如果函数返回内置类型,则列表内最多包含一个值,且该值所占空间不应该大于目标类型的空间。

  • 如果函数返回类类型,由类本身定义初始值如何使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    vector<string> 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_SUCCESSEXIT_FAILURE这两个预处理变量,分别表示执行成功和失败。

1
2
3
4
5
6
7
int main()
{
if (some_failure)
return EXIT_FAILURE; // defined in cstdlib
else
return EXIT_SUCCESS; // defined in cstdlib
}
  • 建议使用预处理变量EXIT_SUCCESSEXIT_FAILURE表示main函数的执行结果。

  • 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。

1
2
3
4
5
6
7
// 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)

  • 因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。

  • 返回数组指针的函数形式如下:

1
Type (*function(parameter_list))[dimension]
  • 其中Type表示元素类型,dimension表示数组大小,(function (parameter_list))*两端的括号必须存在。

  • C++11允许使用尾置返回类型(trailing return type)简化复杂函数声明。尾置返回类型跟在形参列表后面,并以一个->符号开头。为了表示函数真正的返回类型在形参列表之后,需要在本应出现返回类型的地方添加auto关键字。

1
2
// fcn takes an int argument and returns a pointer to an array of ten ints
auto func(int i) -> int(*)[10];
  • 任何函数的定义都能使用尾置返回类型,但是这种形式更适用于返回类型比较复杂的函数。

  • 如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。但decltype并不会把数组类型转换成指针类型,所以还要在函数声明中添加一个*符号。

1
2
3
4
5
6
7
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的形参区分开来。

1
2
3
4
Record lookup(Phone);
Record lookup(const Phone); // redeclares Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // redeclares Record lookup(Phone*)
  • 如果形参是某种类型的指针或引用,则通过区分其指向的对象是常量还是非常量可以实现函数重载,此时的const是底层的。当我们传递给重载函数一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
1
2
3
4
5
6
// 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可以用于函数的重载。当函数的实参不是常量时,将得到普通引用。
1
2
3
4
5
6
7
8
9
10
11
12
// 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<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
  • 函数匹配(function matching)也叫做重载确定(overload resolution),是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。

  • 调用重载函数时有三种可能的结果:

    • 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。

    • 编译器找不到任何一个函数与实参匹配,发出无匹配(no match)的错误信息。

    • 有一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出二义性调用(ambiguous call)的错误信息。

重载与作用域(Overloading and Scope)

在不同的作用域中无法重载函数名。一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

  • 默认实参作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
1
2
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
  • 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

  • 如果想使用默认实参,只要在调用函数的时候省略该实参即可。

  • 虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

1
2
3
4
// 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
  • 默认实参只能出现在函数声明和定义其中一处。通常应该在函数声明中指定默认实参,并将声明放在合适的头文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数声明
void fun(int n);

int main()
{
// Error: 编译器向前查找函数声明
// fun调用形式与声明不符
fun();
return EXIT_SUCCESS;
}

// 函数定义
void fun(int n = 0) { /*...*/ }
  • 局部变量不能作为函数的默认实参。

  • 用作默认实参的名字在函数声明所在的作用域内解析,但名字的求值过程发生在函数调用时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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
1
2
3
4
5
// 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标准中被删除。

1
2
3
4
5
6
constexpr int new_sz()
{
return 42;
}

constexpr int foo = new_sz(); // ok: foo is a constant expression
  • constexpr函数的返回值可以不是一个常量。
1
2
3
4
5
6
7
8
9
// 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,则调用发生时编译器通过实参是否是常量来决定函数的版本。

1
2
3
4
5
6
7
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)

  • 要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。
1
2
3
4
// 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
  • 可以直接使用指向函数的指针来调用函数,无须提前解引用指针。
1
2
3
4
5
6
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
  • 对于重载函数,编译器通过指针类型决定函数版本,指针类型必须与重载函数中的某一个精确匹配。
1
2
3
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1 points to ff(unsigned)
  • 可以把函数的形参定义成指向函数的指针。调用时允许直接把函数名当作实参使用,它会自动转换成指针。
1
2
3
4
5
6
7
// 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作用于函数时,返回的是函数类型,而不是函数指针类型。

  • 函数可以返回指向函数的指针。但返回类型不会像函数类型的形参一样自动地转换成指针,必须显式地将其指定为指针类型。

第七章 类

  • 类的基本思想是数据抽象(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)的声明必须在类的内部,定义则既可以在类的内部也可以在类的外部。定义在类内部的函数是隐式的内联函数。
1
2
3
4
5
6
7
8
9
10
11
12
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指针。
1
2
3
4
5
6
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也遵循初始化规则,所以默认不能把this绑定到一个常量对象上,即不能在常量对象上调用普通的成员函数。

  • C++允许在成员函数的参数列表后面添加关键字const,表示this是一个指向常量的指针。使用关键字const的成员函数被称作常量成员函数(const member function)。

1
2
3
4
5
6
7
// 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属性。同时,类外部定义的成员名字必须包含它所属的类名。

1
2
3
4
5
6
7
double Sales_data::avg_price() const 
{
if (units_sold)
return revenue / units_sold;
else
return 0;
}
  • 可以定义返回this对象的成员函数。
1
2
3
4
5
6
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
}
  • 类的作者通常会定义一些辅助函数,尽管这些函数从概念上来说属于类接口的组成部分,但实际上它们并不属于类本身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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对象的构造过程中可以向其写值。

1
2
3
4
5
6
7
8
9
10
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在类的内部,则默认构造函数是内联的。

1
Sales_data() = default;
  • 构造函数初始值列表(constructor initializer list)负责为新创建对象的一个或几个数据成员赋初始值。形式是每个成员名字后面紧跟括号括起来的(或者在花括号内的)成员初始值,不同成员的初始值通过逗号分隔。
1
2
3
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) { }
  • 当某个数据成员被构造函数初始值列表忽略时,它会以与合成默认构造函数相同的方式隐式初始化。
1
2
3
// 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部分封装了类的实现细节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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将其他类或函数声明为它的友元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)

由类定义的类型名字和其他成员一样存在访问限制,可以是publicprivate中的一种。

1
2
3
4
5
6
7
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成员函数可以修改可变成员的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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版本的函数;在非常量对象上,尽管两个版本都能调用,但会选择非常量版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)。

1
class Screen;   // declaration of the Screen class
  • 可以定义指向不完全类型的指针或引用,也可以声明(不能定义)以不完全类型作为参数或返回类型的函数。

  • 只有当类全部完成后才算被定义,所以一个类的成员类型不能是该类本身。但是一旦类的名字出现,就可以被认为是声明过了,因此类可以包含指向它自身类型的引用或指针。

1
2
3
4
5
6
class Link_screen
{
Screen window;
Link_screen *next;
Link_screen *prev;
};

友元再探(Friendship Revisited)

  • 除了普通函数,类还可以把其他类或其他类的成员函数声明为友元。友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
1
2
3
4
5
6
class Screen 
{
// Window_mgr members can access the private parts of class Screen
friend class Window_mgr;
// ... rest of the Screen class
};
  • 友元函数可以直接定义在类的内部,这种函数是隐式内联的。但是必须在类外部提供相应声明令函数可见。
1
2
3
4
5
6
7
8
9
10
11
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
  • 友元关系不存在传递性。

  • 把其他类的成员函数声明为友元时,必须明确指定该函数所属的类名。

1
2
3
4
5
6
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)

  • 当成员函数定义在类外时,返回类型中使用的名字位于类的作用域之外,此时返回类型必须指明它是哪个类的成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

  • 成员函数体直到整个类可见后才会被处理,因此它能使用类中定义的任何名字。

  • 声明中使用的名字,包括返回类型或参数列表,都必须确保使用前可见。

  • 如果类的成员使用了外层作用域的某个名字,而该名字表示一种类型,则类不能在之后重新定义该名字。

1
2
3
4
5
6
7
8
9
10
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;
// ...
};
  • 类型名定义通常出现在类起始处,这样能确保所有使用该类型的成员都位于类型名定义之后。

  • 成员函数中名字的解析顺序:

    • 在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才会被考虑。

    • 如果在成员函数内没有找到,则会在类内继续查找,这时会考虑类的所有成员。

    • 如果类内也没有找到,会在成员函数定义之前的作用域查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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指针来强制访问被隐藏的类成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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、引用,或者是某种未定义默认构造函数的类类型,必须在初始值列表中将其初始化。

1
2
3
4
5
6
7
8
9
10
11
12
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扩展了构造函数初始值功能,可以定义委托构造函数。委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。

1
2
3
4
5
6
7
8
9
10
11
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为类型)的表达式显式地请求值初始化。

  • 类必须包含一个默认构造函数。

  • 如果想定义一个使用默认构造函数进行初始化的对象,应该去掉对象名后的空括号对。

1
2
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)。
1
2
3
4
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);
  • 编译器只会自动执行一步类型转换。
1
2
3
4
5
6
7
8
// 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的加以阻止。
1
2
3
4
5
6
7
8
9
10
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构造函数只能用于直接初始化。

1
2
3
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构造函数显式地强制转换类型。
1
2
3
4
// 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<Sales_data>(cin));

聚合类(Aggregate Classes)

聚合类满足如下条件:

  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类。
  • 没有虚函数。
1
2
3
4
5
struct Data
{
int ival;
string s;
};

可以使用一个用花括号包围的成员初始值列表初始化聚合类的数据成员。初始值顺序必须与声明顺序一致。如果初始值列表中的元素个数少于类的成员个数,则靠后的成员被值初始化。

1
2
// val1.ival = 0; val1.s = string("Anna")
Data val1 = { 0, "Anna" };

字面值常量类(Literal Classes)

数据成员都是字面值类型的聚合类是字面值常量类。或者一个类不是聚合类,但符合下列条件,则也是字面值常量类:

  • 数据成员都是字面值类型。
  • 类至少含有一个constexpr构造函数。
  • 如果数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式。如果成员属于类类型,则初始值必须使用成员自己的constexpr构造函数。
  • 类必须使用析构函数的默认定义。

constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型。

constexpr构造函数必须初始化所有数据成员,初始值使用constexpr构造函数或常量表达式。

类的静态成员(static Class Members)

  • 使用关键字static可以声明类的静态成员。静态成员存在于任何对象之外,对象中不包含与静态成员相关的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
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指针。

  • 用户代码可以使用作用域运算符访问静态成员,也可以通过类对象、引用或指针访问。类的成员函数可以直接访问静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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关键字,其只能用于类内部的声明语句。

  • 由于静态数据成员不属于类的任何一个对象,因此它们并不是在创建类对象时被定义的。通常情况下,不应该在类内部初始化静态成员。而必须在类外部定义并初始化每个静态成员。一个静态成员只能被定义一次。一旦它被定义,就会一直存在于程序的整个生命周期中。

1
2
// define and initialize a static class member
double Account::interestRate = initRate();
  • 建议把静态数据成员的定义与其他非内联函数的定义放在同一个源文件中,这样可以确保对象只被定义一次。

  • 尽管在通常情况下,不应该在类内部初始化静态成员。但是可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式。

1
2
3
4
5
6
7
8
9
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];
};
  • 静态数据成员的类型可以是它所属的类类型。
1
2
3
4
5
6
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
}
  • 可以使用静态成员作为函数的默认实参。
1
2
3
4
5
6
7
8
9
class Screen
{
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
static const char bkground;
};

第八章 IO

部分IO库设施:

  • istream:输入流类型,提供输入操作。
  • ostream:输出流类型,提供输出操作。
  • cinistream对象,从标准输入读取数据。
  • coutostream对象,向标准输出写入数据。
  • cerrostream对象,向标准错误写入数据。
  • >>运算符:从istream对象读取输入数据。
  • <<运算符:向ostream对象写入输出数据。
  • getline函数:从istream对象读取一行数据,写入string对象。

IO类(The IO Classes)

头文件iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存中string对象的类型。

头文件 类型
iostream istream,wistream 从流读取数据;ostream,wostream 从流输出数据;iostream,wiostream读写流
fstream ifstream,wifstream 从文件读取数据;ofstream,wofstream 从文件写入数据;fstream,wfstream 读写文件;
sstream istringstream,wistringstreamstring读取数据;ostringstream,wostringstreamstring写入数据;stringstream,wstringstream 读写string
  • 宽字符版本的IO类型和函数的名字以w开始,如wcinwcoutwcerr分别对应cincoutcerr。它们与其对应的普通char版本都定义在同一个头文件中,如头文件fstream定义了ifstreamwifstream类型。

  • 可以将派生类的对象当作其基类的对象使用。

IO象无拷贝或赋值(No Copy or Assign for IO Objects)

不能拷贝或对IO对象赋值。

1
2
3
4
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库条件状态:

状态 解释
strm:iostate 是一种机器无关的类型,提供了表达条件状态的完整功能
strm:badbit 用来指出流已经崩溃
strm:failbit 用来指出一个IO操作失败了
strm:eofbit 用来指出流到达了文件结束
strm:goodbit 用来指出流未处于错误状态,此值保证为零
s.eof() 若流seofbit置位,则返回true
s.fail() 若流sfailbit置位,则返回true
s.bad() 若流sbadbit置位,则返回true
s.good() 若流s处于有效状态,则返回true
s.clear() 将流s中所有条件状态位复位,将流的状态设置成有效,返回void
s.clear(flags) 将流s中指定的条件状态位复位,返回void
s.setstate(flags) 根据给定的标志位,将流s中对应的条件状态位置位,返回void
s.rdstate() 返回流s的当前条件状态,返回值类型为strm::iostate
  • badbit表示系统级错误,如不可恢复的读写错误。
  • 通常情况下,一旦badbit被置位,流就无法继续使用了。
  • 在发生可恢复错误后,failbit会被置位,如期望读取数值却读出一个字符。
  • 如果到达文件结束位置,eofbitfailbit都会被置位。如果流未发生错误,则goodbit的值为0。
  • 如果badbitfailbiteofbit任何一个被置位,检测流状态的条件都会失败。
1
2
while (cin >> word)
// ok: read operation successful...
  • good函数在所有错误均未置位时返回true
  • badfaileof函数在对应错误位被置位时返回true
  • 此外,在badbit被置位时,fail函数也会返回true
  • 因此应该使用goodfail函数确定流的总体状态,eofbad只能检测特定错误。

  • 流对象的rdstate成员返回一个iostate值,表示流的当前状态。
  • setstate成员用于将指定条件置位(叠加原始流状态)。
  • clear成员的无参版本清除所有错误标志;含参版本接受一个iostate值,用于设置流的新状态(覆盖原始流状态)。
1
2
3
4
5
// 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的内容都是立即刷新的。
  • 一个输出流可以被关联到另一个流。这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。默认情况下,cincerr都关联到cout,因此,读cin或写cerr都会刷新cout的缓冲区。

flush操纵符刷新缓冲区,但不输出任何额外字符。ends向缓冲区插入一个空字符,然后刷新缓冲区。

1
2
3
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操纵符则使流恢复使用正常的缓冲区刷新机制。
1
2
3
cout << unitbuf;    // all writes will be flushed immediately
// any output is flushed immediately, no buffering
cout << nounitbuf; // returns to normal buffering
  • 如果程序异常终止,输出缓冲区不会被刷新。

  • 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将coutcin关联在一起,因此下面的语句会导致cout的缓冲区被刷新:

1
cin >> ival;
  • 交互式系统通常应该关联输入流和输出流。这意味着包括用户提示信息在内的所有输出,都会在读操作之前被打印出来。

  • 使用tie函数可以关联两个流。它有两个重载版本:无参版本返回指向输出流的指针。如果本对象已关联到一个输出流,则返回的就是指向这个流的指针,否则返回空指针。tie的第二个版本接受一个指向ostream的指针,将本对象关联到此ostream

1
2
3
4
5
6
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可以同时读写指定文件。

操作 解释
fstream fstrm; 创建一个未绑定的文件流。
fstream fstrm(s); 创建一个文件流,并打开名为s的文件,s可以是string也可以是char指针
fstream fstrm(s, mode); 与前一个构造函数类似,但按指定mode打开文件
fstrm.open(s) 打开名为s的文件,并和fstrm绑定
fstrm.close() 关闭和fstrm绑定的文件
fstrm.is_open() 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭

使用文件流对象(Using File Stream Objects)

  • 每个文件流类型都定义了open函数,它完成一些系统操作,定位指定文件,并视情况打开为读或写模式。

  • 创建文件流对象时,如果提供了文件名(可选),open会被自动调用。

1
2
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)

每个流都有一个关联的文件模式,用来指出如何使用文件。

文件模式 解释
in 以读的方式打开
out 以写的方式打开
app 每次写操作前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式进行IO操作。
  • 只能对ofstreamfstream对象设定out模式。
  • 只能对ifstreamfstream对象设定in模式。
  • 只有当out被设定时才能设定trunc模式。
  • 只要trunc没被设定,就能设定app模式。在app模式下,即使没有设定out模式,文件也是以输出方式打开。
  • 默认情况下,即使没有设定trunc,以out模式打开的文件也会被截断。如果想保留以out模式打开的文件内容,就必须同时设定app模式,这会将数据追加写到文件末尾;或者同时设定in模式,即同时进行读写操作。
  • atebinary模式可用于任何类型的文件流对象,并可以和其他任何模式组合使用。
  • ifstream对象关联的文件默认以in模式打开,与ofstream对象关联的文件默认以out模式打开,与fstream对象关联的文件默认以inout模式打开。

  • 默认情况下,打开ofstream对象时,文件内容会被丢弃,阻止文件清空的方法是同时指定appin模式。

  • 流对象每次打开文件时都可以改变其文件模式。

1
2
3
4
5
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)

头文件sstream定义了三个类型来支持内存IO:istringstreamstring读取数据,ostringstreamstring写入数据,stringstream可以同时读写string的数据。

操作 解释
sstream strm 定义一个未绑定的stringstream对象
sstream strm(s) s初始化对象
strm.str() 返回strm所保存的string的拷贝
strm.str(s) s拷贝到strm中,返回void

使用istringstream(Using an istringstream)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// members are public by default
struct PersonInfo
{
string name;
vector<string> phones;
};

string line, word; // will hold a line and word from input, respectively
vector<PersonInfo> 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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

第九章 顺序容器

顺序容器概述(Overview of the Sequential Containers)

顺序容器类型:

类型 特性
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表。只支持双向顺序访问。在任何位置插入/删除速度都很快
forward_list 单向链表。只支持单向顺序访问。在任何位置插入/删除速度都很快
array 固定大小数组。支持快速随机访问。不能添加/删除元素
string 类似vector,但用于保存字符。支持快速随机访问。在尾部插入/删除速度很快

forward_listarray是C++11新增类型。与内置数组相比,array更安全易用。forward_list没有size操作。

容器选择原则:

  • 除非有合适的理由选择其他容器,否则应该使用vector
  • 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用listforward_list
  • 如果程序要求随机访问容器元素,则应该使用vectordeque
  • 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用deque
  • 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
    • 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向vector追加数据,再调用标准库的sort函数重排元素,从而避免在中间位置添加元素。
    • 如果必须在中间位置插入元素,可以在输入阶段使用list。输入完成后将list中的内容拷贝到vector中。
  • 不确定应该使用哪种容器时,可以先只使用vectorlist的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vectorlist都很方便。

容器库概览(Container Library Overview)

每个容器都定义在一个头文件中,文件名与类型名相同。容器均为模板类型。

迭代器(Iterators)

操作 解释
iterator 此容器类型的迭代器类型
const_iterator 可以读取元素但不能修改元素的迭代器类型
size_type 无符号整数类型,足够保存此种容器类型最大可能的大小
difference_type 带符号整数类型,足够保存两个迭代器之间的距离
value_type 元素类型
reference 元素的左值类型;和value_type &含义相同
const_reference 元素的const左值类型,即const value_type &
  • forward_list类型不支持递减运算符--

  • 一个迭代器范围(iterator range)由一对迭代器表示。这两个迭代器通常被称为beginend,分别指向同一个容器中的元素或尾后地址。end迭代器不会指向范围中的最后一个元素,而是指向尾元素之后的位置。这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为[begin,end)。迭代器beginend必须指向相同的容器,end可以与begin指向相同的位置,但不能指向begin之前的位置(由程序员确保)。

假定beginend构成一个合法的迭代器范围,则:

  • 如果begin等于end,则范围为空。
  • 如果begin不等于end,则范围内至少包含一个元素,且begin指向该范围内的第一个元素。
  • 可以递增begin若干次,令begin等于end
1
2
3
4
5
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。如果需要元素类型的引用,可以使用referenceconst_reference

begin和end成员(begin and end Members)

  • beginend操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围。

  • beginend操作有多个版本:带r的版本返回反向迭代器。以c开头的版本(C++11新增)返回const迭代器。不以c开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对const对象调用时,返回const迭代器。

1
2
3
4
5
list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin(); // list<string>::const_iterator
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
  • autobeginend结合使用时,返回的迭代器类型依赖于容器类型。但调用以c开头的版本仍然可以获得const迭代器,与容器是否是常量无关。

  • 当程序不需要写操作时,应该使用cbegincend

容器定义和初始化(Defining and Initializing a Container)

容器定义和初始化方式:

操作 解释
C c; 默认构造函数,构造空容器
C c1(c2);C c1 = c2; 构造c2的拷贝c1
C c(b, e) 构造c,将迭代器be指定范围内的所有元素拷贝到c
C c(a, b, c...) 列表初始化c
C c(n) 只支持顺序容器,且不包括array,包含n个元素,这些元素进行了值初始化
C c(n, t) 包含n个初始值为t的元素
  • 将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。

  • 传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。

1
2
3
4
5
6
7
8
// each container has three elements, initialized from the given initializers
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
list<string> list2(authors); // ok: types match
deque<string> authList(authors); // error: container types don't match
vector<string> words(articles); // error: element types must match
// ok: converts const char* elements to string
forward_list<string> words(articles.begin(), articles.end());
  • C++11允许对容器进行列表初始化。
1
2
3
// each container has three elements, initialized from the given initializers
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
  • 定义和使用array类型时,需要同时指定元素类型和容器大小。
1
2
3
4
array<int, 42>      // type is: array that holds 42 ints
array<string, 10> // type is: array that holds 10 strings
array<int, 10>::size_type i; // array type includes element type and size
array<int>::size_type j; // error: array<int> is not a type
  • array进行列表初始化时,初始值的数量不能大于array的大小。如果初始值的数量小于array的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。

  • 可以对array进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。

赋值和swap(Assignment and swap)

容器赋值操作:

操作 解释
c1 = c2; c1中的元素替换成c2中的元素
c1 = {a, b, c...} c1中的元素替换成列表中的元素(不适用于array
c1.swap(c2) 交换c1c2的元素
swap(c1, c2) 等价于c1.swap(c2)
c.assign(b, e) c中的元素替换成迭代器be表示范围中的元素,be不能指向c中的元素
c.assign(il) c中的元素替换成初始化列表il中的元素
c.assign(n, r) c中的元素替换为n个值是t的元素
  • 赋值运算符两侧的运算对象必须类型相同。assign允许用不同但相容的类型赋值,或者用容器的子序列赋值。
1
2
3
4
5
list<string> names;
vector<const char*> 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不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。

1
2
3
vector<string> svec1(10);   // vector with ten elements
vector<string> svec2(24); // vector with 24 elements
swap(svec1, svec2);
  • 赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作交换容器内容,不会导致迭代器、引用和指针失效(arraystring除外)。

  • 对于arrayswap会真正交换它们的元素。因此在swap操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。

1
2
3
4
5
6
7
8
9
10
array<int, 3> a = { 1, 2, 3 };
array<int, 3> 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操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。
1
2
3
4
5
6
7
8
9
10
vector<int> a = { 1, 2, 3 };
vector<int> 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,也不允许用花括号列表进行赋值。
1
2
3
4
array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> 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)

操作 解释
c.size() c中元素的数目(不支持forward_list
c.max_size() c中可保存的最大元素数目
c.empty() c中存储了元素,返回false,否则返回true

size成员返回容器中元素的数量;emptysize为0时返回true,否则返回falsemax_size返回一个大于或等于该类型容器所能容纳的最大元素数量的值。forward_list支持max_sizeempty,但不支持size

关系运算符(Relational Operators)

每个容器类型都支持相等运算符(==!=)。除无序关联容器外,其他容器都支持关系运算符(>>=<<=)。关系运算符两侧的容器类型和保存元素类型都必须相同。

两个容器的比较实际上是元素的逐对比较,其工作方式与string的关系运算符类似

  • 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。
  • 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
  • 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。
1
2
3
4
5
6
7
8
vector<int> v1 = { 1, 3, 5, 7, 9, 12 };
vector<int> v2 = { 1, 3, 9 };
vector<int> v3 = { 1, 3, 5, 7 };
vector<int> 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外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。

操作 解释
c.push_back(t) c尾部创建一个值为t的元素,返回void
c.emplace_back(args) 同上
c.push_front(t) c头部创建一个值为t的元素,返回void
c.emplace_front(args) 同上
c.insert(p, t) 在迭代器p指向的元素之前创建一个值是t的元素,返回指向新元素的迭代器
c.emplace(p, args) 同上
c.inset(p, n, t) 在迭代器p指向的元素之前插入n个值为t的元素,返回指向第一个新元素的迭代器;如果n是0,则返回p
c.insert(p, b, e) 将迭代器be范围内的元素,插入到p指向的元素之前;如果范围为空,则返回p
c.insert(p, il) il是一个花括号包围中的元素值列表,将其插入到p指向的元素之前;如果il是空,则返回p
  • push_back将一个元素追加到容器尾部,push_front将元素插入容器头部。
1
2
3
4
// 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将元素插入开始位置。
1
2
3
4
5
6
7
vector<string> svec;
list<string> 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!");
  • 将元素插入到vectordequestring的任何位置都是合法的,但可能会很耗时。

  • 在新标准库中,接受元素个数或范围的insert版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回void。如果范围为空,不插入任何元素,insert会返回第一个参数。

1
2
3
4
list<string> 1st;
auto iter = 1st.begin();
while (cin >> word)
iter = 1st.insert(iter, word); // same as calling push_front
  • 新标准库增加了三个直接构造而不是拷贝元素的操作:emplace_frontemplace_backemplace,其分别对应push_frontpush_backinsert。当调用pushinsert时,元素对象被拷贝到容器中。而调用emplace时,则是将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素。
1
2
3
4
5
6
7
// 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有特殊版本的insertemplace操作,且不支持push_backemplace_backvectorstring不支持push_frontemplace_front

访问元素(Accessing Elements)

  • 每个顺序容器都有一个front成员函数,而除了forward_list之外的顺序容器还有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。

  • 在调用frontback之前,要确保容器非空。

顺序容器的元素访问操作:

操作 解释
c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
c.front() 返回c中头元素的引用。若c为空,函数行为未定义
c[n] 返回c中下标是n的元素的引用,n时候一个无符号证书。若n>=c.size(),则函数行为未定义
c.at(n) 返回下标为n的元素引用。如果下标越界,则抛出out_of_range异常
  • 在容器中访问元素的成员函数都返回引用类型。如果容器是const对象,则返回const引用,否则返回普通引用。

  • 可以快速随机访问的容器(stringvectordequearray)都提供下标运算符。保证下标有效是程序员的责任。如果希望确保下标合法,可以使用at成员函数。at类似下标运算,但如果下标越界,at会抛出out_of_range异常。

1
2
3
vector<string> 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)

顺序容器的元素删除操作:

操作 解释
c.pop_back() 删除c中尾元素,若c为空,则函数行为未定义。函数返回void
c.pop_front() 删除c中首元素,若c为空,则函数行为未定义。函数返回void
c.erase(p) 删除迭代器p指向的元素,返回一个指向被删除元素之后的元素的迭代器,若p本身是尾后迭代器,则函数行为未定义
c.erase(b, e) 删除迭代器be范围内的元素,返回指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则返回尾后迭代器
c.clear() 删除c中所有元素,返回void
  • 删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。删除vectorstring的元素后,指向删除点之后位置的迭代器、引用和指针也都会失效。

  • 删除元素前,程序员必须确保目标元素存在。

  • pop_frontpop_back函数分别删除首元素和尾元素。vectorstring类型不支持pop_frontforward_list类型不支持pop_back

  • erase函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除元素(最后一个)之后位置的迭代器。

1
2
3
// 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中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。

forward_list的插入和删除操作:

操作 解释
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器,此迭代器不能解引用。
lst.cbefore_begin() 同上,但是返回的是常量迭代器。
lst.insert_after(p, t) 在迭代器p之后插入元素。t是一个对象
lst.insert_after(p, n, t) 在迭代器p之后插入元素。t是一个对象,n是数量。若n是0则函数行为未定义
lst.insert_after(p, b, e) 在迭代器p之后插入元素。由迭代器be指定范围。
lst.insert_after(p, il) 在迭代器p之后插入元素。由il指定初始化列表。
emplace_after(p, args) 使用argsp之后的位置,创建一个元素,返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义。
lst.erase_after(p) 删除p指向位置之后的元素,返回一个指向被删元素之后的元素的迭代器,若p指向lst的尾元素或者是一个尾后迭代器,则函数行为未定义。
lst.erase_after(b, e) 类似上面,删除对象换成从be指定的范围。

改变容器大小(Resizing a Container)

顺序容器的大小操作:

操作 解释
c.resize(n) 调整c的大小为n个元素,若n<c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化
c.resize(n, t) 调整c的大小为n个元素,任何新添加的元素都初始化为值t

resize函数接受一个可选的元素值参数,用来初始化添加到容器中的元素,否则新元素进行值初始化。如果容器保存的是类类型元素,且resize向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。

容器操作可能使迭代器失效(Container Operations May Invalidate Iterators)

向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。

  • 向容器中添加元素后:
    • 如果容器是vectorstring类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。
    • 如果容器是deque类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。
    • 如果容器是listforward_list类型,指向容器的迭代器、指针和引用仍然有效。
  • 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效:
    • 如果容器是listforward_list类型,指向容器其他位置的迭代器、指针和引用仍然有效。
    • 如果容器是deque类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。
    • 如果容器是vectorstring类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。

  • 必须保证在每次改变容器后都正确地重新定位迭代器。

  • 不要保存end函数返回的迭代器。

1
2
3
4
5
6
7
8
// 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
}

获取迭代器:

操作 解释
c.begin(), c.end() 返回指向c的首元素和尾元素之后位置的迭代器
c.cbegin(), c.cend() 返回const_iterator
  • c开头的版本是C++11新标准引入的
  • 当不需要写访问时,应该使用cbegincend

反向容器的额外成员

操作 解释
reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 不能修改元素的逆序迭代器
c.rbegin(), c.rend() 返回指向c的尾元素和首元素之前位置的迭代器
c.crbegin(), c.crend() 返回const_reverse_iterator
  • 不支持forward_list

vector对象是如何增长的(How a vector Grows)

vectorstring的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。

容器大小管理操作:

操作 解释
c.shrink_to_fit() capacity()减少到和size()相同大小
c.capacity() 不重新分配内存空间的话,c可以保存多少个元素
c.reverse(n) 分配至少能容纳n个元素的内存空间
  • capacity函数返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。reserve函数告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小。

  • 只有当需要的内存空间超过当前容量时,reserve才会真正改变容器容量,分配不小于需求大小的内存空间。当需求大小小于当前容量时,reserve并不会退回内存空间。因此在调用reserve之后,capacity会大于或等于传递给reserve的参数。

  • 在C++11中可以使用shrink_to_fit函数来要求dequevectorstring退回不需要的内存空间(并不保证退回)。

额外的string操作(Additional string Operations)

构造string的其他方法(Other Ways to Construct strings)

构造string的其他方法:

操作 解释
string s(cp, n) scp指向的数组中前n个字符的拷贝,此数组
string s(s2, pos2) sstring s2从下标pos2开始的字符的拷贝。若pos2 > s2.size(),则构造函数的行为未定义。
string s(s2, pos2, len2) sstring s2从下标pos2开始的len2个字符的拷贝。

从另一个string对象拷贝字符构造string时,如果提供的拷贝开始位置(可选)大于给定string的大小,则构造函数会抛出out_of_range异常。

子字符串操作:

操作 解释
s.substr(pos, n) 返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值是0,n的默认值是s.size() - pos,即拷贝从pos开始的所有字符。
  • 如果传递给substr函数的开始位置超过string的大小,则函数会抛出out_of_range异常。

改变string的其他方法(Other Ways to Change a string)

修改string的操作:

操作 解释
s.insert(pos, args) pos之前插入args指定的字符。pos可以使是下标或者迭代器。接受下标的版本返回指向s的引用;接受迭代器的版本返回指向第一个插入字符的迭代器。
s.erase(pos, len) 删除从pos开始的len个字符,如果len被省略,则删除后面所有字符,返回指向s的引用。
s.assign(args) s中的字符替换成args指定的字符。返回一个指向s的引用。
s.append(args) args指定的字符追加到s,返回一个指向s的引用。
s.replace(range, args) 删除s中范围range中的字符,替换成args指定的字符。返回一个指向s的引用。
  • append函数是在string末尾进行插入操作的简写形式。
1
2
3
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函数是调用eraseinsert函数的简写形式。
1
2
3
4
5
// 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::nposstatic成员。标准库将npos定义为const string::size_type类型,并初始化为-1。

  • 不建议用int或其他带符号类型来保存string搜索函数的返回值。

string搜索操作:

搜索操作 解释
s.find(args) 查找sargs第一次出现的位置
s.rfind(args) 查找sargs最后一次出现的位置
s.find_first_of(args) s中查找args中任何一个字符第一次出现的位置
s.find_last_of(args) s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args) s中查找第一个不在args中的字符
s.find_first_not_of(args) s中查找最后一个不在args中的字符

args必须是一下的形式之一:

args形式 解释
c, pos s中位置pos开始查找字符cpos默认是0
s2, pos s中位置pos开始查找字符串spos默认是0
cp, pos s中位置pos开始查找指针cp指向的以空字符结尾的C风格字符串。pos默认是0
cp, pos, n s中位置pos开始查找指针cp指向的前n个字符。posn无默认值。

compare函数(The compare Functions)

string类型提供了一组compare函数进行字符串比较操作,类似C标准库的strcmp函数。

compare函数的几种参数形式:

参数形式 解释
s2 比较ss2
pos1, n1, s2 比较spos1开始的n1个字符和s2
pos1, n1, s2, pos2, n2 比较spos1开始的n1个字符和s2
cp 比较scp指向的以空字符结尾的字符数组
pos1, n1, cp 比较spos1开始的n1个字符和cp指向的以空字符结尾的字符数组
pos1, n1, cp, n2 比较spos1开始的n1个字符和cp指向的地址开始n2个字符

数值转换(Numeric Conversions)

C++11增加了string和数值之间的转换函数:

转换 解释
to_string(val) 一组重载函数,返回数值valstring表示。val可以使任何算术类型。对每个浮点类型和int或更大的整型,都有相应版本的to_string()。和往常一样,小整型会被提升。
stoi(s, p, b) 返回s起始子串(表示整数内容)的数值,ps中第一个非数值字符的下标,默认是0,b是转换所用的基数。返回int
stol(s, p, b) 返回long
stoul(s, p, b) 返回unsigned long
stoll(s, p, b) 返回long long
stoull(s, p, b) 返回unsigned long long
stof(s, p) 返回s起始子串(表示浮点数内容)的数值,ps中第一个非数值字符的下标,默认是0。返回float
stod(s, p) 返回double
stold(s, p) 返回long double
  • 进行数值转换时,string参数的第一个非空白字符必须是符号(+-)或数字。它可以以0x0X开头来表示十六进制数。对于转换目标是浮点值的函数,string参数也可以以小数点开头,并可以包含eE来表示指数部分。

  • 如果给定的string不能转换为一个数值,则转换函数会抛出invalid_argument异常。如果转换得到的数值无法用任何类型表示,则抛出out_of_range异常。

容器适配器(Container Adaptors)

标准库定义了stackqueuepriority_queue三种容器适配器。容器适配器可以改变已有容器的工作机制。

所有容器适配器都支持的操作和类型:

操作 解释
size_type 一种类型,须以保存当前类型的最大对象的大小
value_type 元素类型
container_type 实现适配器的底层容器类型
A a; 创建一个名为a的空适配器
A a(c) 创建一个名为a的适配器,带有容器c的一个拷贝
关系运算符 每个适配器都支持所有关系运算符:==!=<<=>>=这些运算符返回底层容器的比较结果
a.empty() a包含任何元素,返回false;否则返回true
a.size() 返回a中的元素数目
swap(a, b) 交换ab的内容,ab必须有相同类型,包括底层容器类型也必须相同
a.swap(b) 同上
  • 默认情况下,stackqueue是基于deque实现的,priority_queue是基于vector实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
1
2
3
4
// empty stack implemented on top of vector
stack<string, vector<string>> str_stk;
// str_stk2 is implemented on top of vector and initially holds a copy of svec
stack<string, vector<string>> str_stk2(svec);
  • 所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在array上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用forward_list构造适配器。

栈适配器stack定义在头文件stack中,其支持的操作如下:

操作 解释
s.pop() 删除栈顶元素,不返回。
s.push(item) 创建一个新元素,压入栈顶,该元素通过拷贝或移动item而来
s.emplace(args) 同上,但元素由args来构造。
s.top() 返回栈顶元素,不删除。

队列适配器queuepriority_queue定义在头文件queue中,其支持的操作如下:

操作 解释
q.pop() 删除队首元素,但不返回。
q.front() 返回队首元素的值,不删除。
q.back() 返回队尾元素的值,不删除。只适用于queue
q.top() 返回具有最高优先级的元素值,不删除。
q.push(item) 在队尾压入一个新元素。
q.emplace(args)
  • queue使用先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。

第十章 泛型算法

概述(Overview)

  • 大多数算法都定义在头文件algorithm中,此外标准库还在头文件numeric中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的元素范围进行操作。

  • find函数将范围中的每个元素与给定值进行比较,返回指向第一个等于给定值的元素的迭代器。如果无匹配元素,则返回其第二个参数来表示搜索失败。

1
2
3
4
5
6
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中)用于计算一个序列的和。它接受三个参数,前两个参数指定需要求和的元素范围,第三个参数是和的初值(决定加法运算类型和返回值类型)。
1
2
3
4
5
// 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(), "");
  • 建议在只读算法中使用cbegincend函数。

  • equal函数用于确定两个序列是否保存相同的值。它接受三个迭代器参数,前两个参数指定第一个序列范围,第三个参数指定第二个序列的首元素。equal函数假定第二个序列至少与第一个序列一样长。

1
2
// roster2 should have at least as many elements as roster1
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
  • 只接受单一迭代器表示第二个操作序列的算法都假定第二个序列至少与第一个序列一样长。

写容器元素的算法(Algorithms That Write Container Elements)

  • fill函数接受两个迭代器参数表示序列范围,还接受一个值作为第三个参数,它将给定值赋予范围内的每个元素。
1
2
// reset each element to 0
fill(vec.begin(), vec.end(), 0);
  • fill_n函数接受单个迭代器参数、一个计数值和一个值,它将给定值赋予迭代器指向位置开始的指定个元素。
1
2
// reset all the elements of vec to 0
fill_n(vec.begin(), vec.size(), 0);
  • 向目的位置迭代器写入数据的算法都假定目的位置足够大,能容纳要写入的元素。

  • 插入迭代器(insert iterator)是一种向容器内添加元素的迭代器。通过插入迭代器赋值时,一个与赋值号右侧值相等的元素会被添加到容器中。

  • back_inserter函数(定义在头文件iterator中)接受一个指向容器的引用,返回与该容器绑定的插入迭代器。通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。

1
2
3
4
5
vector<int> 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函数接受三个迭代器参数,前两个参数指定输入序列,第三个参数指定目的序列的起始位置。它将输入序列中的元素拷贝到目的序列中,返回目的位置迭代器(递增后)的值。
1
2
3
4
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函数接受四个参数,前两个迭代器参数指定输入序列,后两个参数指定要搜索的值和替换值。它将序列中所有等于第一个值的元素都替换为第二个值。
1
2
// replace any element with the value 0 with 42
replace(ilst.begin(), ilst.end(), 0, 42);
  • 相对于replacereplace_copy函数可以保留原序列不变。它接受第三个迭代器参数,指定调整后序列的保存位置。
1
2
// 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函数接受两个迭代器参数,指定排序范围。它利用元素类型的<运算符重新排列元素。
1
2
3
4
5
6
7
8
9
10
void elimDups(vector<string> &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函数重排输入序列,消除相邻的重复项,返回指向不重复值范围末尾的迭代器。

定制操作(Customizing Operations)

默认情况下,很多比较算法使用元素类型的<==运算符完成操作。可以为这些算法提供自定义操作来代替默认运算符。

向算法传递函数(Passing a Function to an Algorithm)

谓词(predicate)是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为一元谓词(unary predicate,接受一个参数)和二元谓词(binary predicate,接受两个参数)。接受谓词参数的算法会对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。

1
2
3
4
5
6
7
8
// 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表达式表示一个可调用的代码单元,类似未命名的内联函数,但可以定义在函数内部。其形式如下:

1
[capture list] (parameter list) -> return type { function body }
  • 其中,capture list(捕获列表)是一个由lambda所在函数定义的局部变量的列表(通常为空)。return typeparameter listfunction body与普通函数一样,分别表示返回类型、参数列表和函数体。但与普通函数不同,lambda必须使用尾置返回类型,且不能有默认实参。

  • 定义lambda时可以省略参数列表和返回类型,但必须包含捕获列表和函数体。省略参数列表等价于指定空参数列表。省略返回类型时,若函数体只是一个return语句,则返回类型由返回表达式的类型推断而来。否则返回类型为void

1
2
auto f = [] { return 42; };
cout << f() << endl; // prints 42
  • lambda可以使用其所在函数的局部变量,但必须先将其包含在捕获列表中。捕获列表只能用于局部非static变量,lambda可以直接使用局部static变量和其所在函数之外声明的名字。
1
2
3
// 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函数接受一个输入序列和一个可调用对象,它对输入序列中的每个元素调用此对象。
1
2
3
// 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内对应的值。
1
2
3
4
5
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执行时变量存在。
1
2
3
4
5
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代码隐式捕获函数变量,方法是在捕获列表中写一个&=符号。&为引用捕获,=为值捕获。

  • 可以混合使用显式捕获和隐式捕获。混合使用时,捕获列表中的第一个元素必须是&=符号,用于指定默认捕获方式。显式捕获的变量必须使用与隐式捕获不同的方式。

1
2
3
4
5
6
// 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捕获列表形式:

捕获列表 解释
[] 空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有在捕获变量后才能使用它们。
[names] names是一个逗号分隔的名字列表,这些名字都是在lambda所在函数的局部变量,捕获列表中的变量都被拷贝,名字前如果使用了&,则采用引用捕获方式。
[&] 隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用。
[=] 隐式捕获列表,采用值捕获方式。
[&, identifier_list] identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能使用&
[=, identifier_list] identifier_list中的变量采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且前面必须使用&
  • 默认情况下,对于值方式捕获的变量,lambda不能修改其值。如果希望修改,就必须在参数列表后添加关键字mutable
1
2
3
4
5
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函数接受三个迭代器参数和一个可调用对象。前两个迭代器参数指定输入序列,第三个迭代器参数表示目的位置。它对输入序列中的每个元素调用可调用对象,并将结果写入目的位置。

1
2
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int { if (i < 0) return -i; else return i; });
  • lambda定义返回类型时,必须使用尾置返回类型。

参数绑定(Binding Arguments)

bind函数定义在头文件functional中,相当于一个函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表。一般形式如下:

1
auto newCallable = bind(callable, arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个以逗号分隔的参数列表,对应给定的callable的参数。之后调用newCallable时,newCallable会再调用callable,并传递给它arg_list中的参数。arg_list中可能包含形如_n的名字,其中n是一个整数。这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置:_1newCallable的第一个参数,_2newCallable的第二个参数,依次类推。这些名字都定义在命名空间placeholders中,它又定义在命名空间std中,因此使用时应该进行双重限定。

1
2
3
4
5
6
7
8
9
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函数可以调整给定可调用对象中的参数顺序。
1
2
3
4
// 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引用的类。

1
2
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)

插入器是一种迭代器适配器,它接受一个容器参数,生成一个插入迭代器。通过插入迭代器赋值时,该迭代器调用容器操作向给定容器的指定位置插入一个元素。

插入迭代器操作:

操作 解释
it=t it指定的当前位置插入值t。假定cit绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t)c.push_front(t)c.insert(t, p),其中p是传递给inserter的迭代器位置
*it, ++it, it++ 这些操作虽然存在,但不会对it做任何事情,每个操作都返回it

插入器有三种类型,区别在于元素插入的位置:

  • back_inserter:创建一个调用push_back操作的迭代器。
  • front_inserter:创建一个调用push_front操作的迭代器。
  • inserter:创建一个调用insert操作的迭代器。此函数接受第二个参数,该参数必须是一个指向给定容器的迭代器,元素会被插入到该参数指向的元素之前。
1
2
3
4
5
6
list<int> lst = { 1,2,3,4 };
list<int> lst2, lst3; // empty lists
// after copy completes, lst2 contains 4 3 2 1
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
// after copy completes, lst3 contains 1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));

iostream迭代器(iostream Iterators)

  • istream_iterator从输入流读取数据,ostream_iterator向输出流写入数据。这些迭代器将流当作特定类型的元素序列处理。

  • 创建流迭代器时,必须指定迭代器读写的对象类型。istream_iterator使用>>来读取流,因此istream_iterator要读取的类型必须定义了>>运算符。创建istream_iterator时,可以将其绑定到一个流。如果默认初始化,则创建的是尾后迭代器。

1
2
3
4
istream_iterator<int> int_it(cin);  // reads ints from cin
istream_iterator<int> int_eof; // end iterator value
ifstream in("afile");
istream_iterator<string> str_it(in); // reads strings from "afile"
  • 对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或IO错误,迭代器的值就与尾后迭代器相等。
1
2
3
4
5
6
istream_iterator<int> in_iter(cin);     // read ints from cin
istream_iterator<int> 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++);
  • 可以直接使用流迭代器构造容器。
1
2
istream_iterator<int> in_iter(cin), eof;    // read ints from cin
vector<int> vec(in_iter, eof); // construct vec from an iterator range

istream_iterator操作:

操作 解释
istream_iterator<T> in(is); in从输入流is读取类型为T的值
istream_iterator<T> end; 读取类型是T的值的istream_iterator迭代器,表示尾后位置
in1 == in2 in1in2必须读取相同类型。如果他们都是尾后迭代器,或绑定到相同的输入,则两者相等。
in1 != in2 类似上条
*in 返回从流中读取的值
in->mem *(in).mem含义相同
++in, in++ 使用元素类型所定义的>>运算符从流中读取下一个值。前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值。
  • istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。但可以保证在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。

  • 定义ostream_iterator对象时,必须将其绑定到一个指定的流。不允许定义空的或者表示尾后位置的ostream_iterator

ostream_iterator操作:

操作 解释
ostream_iterator<T> out(os); out将类型为T的值写到输出流os
ostream_iterator<T> out(os, d); out将类型为T的值写到输出流os中,每个值后面都输出一个dd指向一个空字符结尾的字符数组。
out = val <<运算符将val写入到out所绑定的ostream中。val的类型必须和out可写的类型兼容。
*out, ++out, out++ 这些运算符是存在的,但不对out做任何事情。每个运算符都返回out
  • *++运算符实际上不会对ostream_iterator对象做任何操作。但是建议代码写法与其他迭代器保持一致。
1
2
3
4
ostream_iterator<int> 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)

递增反向迭代器会移动到前一个元素,递减会移动到后一个元素。

1
2
3
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());
  • 不能从forward_list或流迭代器创建反向迭代器。

  • 调用反向迭代器的base函数可以获得其对应的普通迭代器。

1
2
3
4
5
6
// 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;
  • 反向迭代器的目的是表示元素范围,而这些范围是不对称的。用普通迭代器初始化反向迭代器,或者给反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同元素。

泛型算法结构(Structure of Generic Algorithms)

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

迭代器类别 解释 支持的操作
输入迭代器 只读,不写;单遍扫描,只能递增 ==,!=,++,*,->
输出迭代器 只写,不读;单遍扫描,只能递增 ++,*
前向迭代器 可读写;多遍扫描,只能递增 ==,!=,++,*,->
双向迭代器 可读写;多遍扫描,可递增递减 ==,!=,++,--,*,->
随机访问迭代器 可读写,多遍扫描,支持全部迭代器运算 ==,!=,<,<=,>,>=,++,--,+,+=,-,-=,*,->,iter[n]==*(iter[n])

5类迭代器(The Five Iterator Categories)

C++标准指定了泛型和数值算法的每个迭代器参数的最小类别。对于迭代器实参来说,其能力必须大于或等于规定的最小类别。向算法传递更低级的迭代器参数会产生错误(大部分编译器不会提示错误)。

迭代器类别:

  • 输入迭代器(input iterator):可以读取序列中的元素,只能用于单遍扫描算法。必须支持以下操作:
    • 用于比较两个迭代器相等性的相等==和不等运算符!=
    • 用于推进迭代器位置的前置和后置递增运算符++
    • 用于读取元素的解引用运算符*;解引用只能出现在赋值运算符右侧。
    • 用于读取元素的箭头运算符->
  • 输出迭代器(output iterator):可以读写序列中的元素,只能用于单遍扫描算法,通常指向目的位置。必须支持以下操作:
    • 用于推进迭代器位置的前置和后置递增运算符++
    • 用于读取元素的解引用运算符*;解引用只能出现在赋值运算符左侧(向已经解引用的输出迭代器赋值,等价于将值写入其指向的元素)。
  • 前向迭代器(forward iterator):可以读写序列中的元素。只能在序列中沿一个方向移动。支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此可以使用前向迭代器对序列进行多遍扫描。
  • 双向迭代器(bidirectional iterator):可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,还支持前置和后置递减运算符--。除forward_list之外的其他标准库容器都提供符合双向迭代器要求的迭代器。
  • 随机访问迭代器(random-access iterator):可以在常量时间内访问序列中的任何元素。除了支持所有双向迭代器的操作之外,还必须支持以下操作:
    • 用于比较两个迭代器相对位置的关系运算符<<=>>=
    • 迭代器和一个整数值的加减法运算++=--=,计算结果是迭代器在序列中前进或后退给定整数个元素后的位置。
    • 用于两个迭代器上的减法运算符-,计算得到两个迭代器的距离。
    • 下标运算符[]

算法形参模式(Algorithm Parameter Patterns)

大多数算法的形参模式是以下四种形式之一:

1
2
3
4
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是算法名称,begend表示算法所操作的输入范围。几乎所有算法都接受一个输入范围,是否有其他参数依赖于算法操作。

  • dest表示输出范围,beg2end2表示第二个输入范围。

  • 向输出迭代器写入数据的算法都假定目标空间足够容纳要写入的数据。

  • 接受单独一个迭代器参数表示第二个输入范围的算法都假定从迭代器参数开始的序列至少与第一个输入范围一样大。

算法命名规范(Algorithm Naming Conventions)

接受谓词参数的算法都有附加的_if后缀。

1
2
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后缀。

1
2
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)

对于listforward_list类型,应该优先使用成员函数版本的算法,而非通用算法。

listforward_list成员函数版本的算法:

操作 解释
lst.merge(lst2) 将来自lst2的元素合并入lst,二者都必须是有序的,元素将从lst2中删除。
lst.merge(lst2, comp) 同上,给定比较操作。
lst.remove(val) 调用erase删除掉与给定值相等(==)的每个元素
lst.remove_if(pred) 调用erase删除掉令一元谓词为真的每个元素
lst.reverse() 反转lst中元素的顺序
lst.sort() 使用<排序元素
lst.sort(comp) 使用给定比较操作排序元素
lst.unique() 调用erase删除同一个值的连续拷贝。使用==
lst.unique(pred) 调用erase删除同一个值的连续拷贝。使用给定的二元谓词。

listforward_listsplice函数可以进行容器合并,其参数如下:

参数 解释
(p, lst2) p是一个指向lst中元素的迭代器,或者一个指向flst首前位置的迭代器。函数将lst2中的所有元素移动到lstp之前的位置或是flstp之后的位置。将元素从lst2中删除。lst2的类型必须和lst相同,而且不能是同一个链表。
(p, lst2, p2) 同上,p2是一个指向lst2中位置的有效的迭代器,将p2指向的元素移动到lst中,或将p2之后的元素移动到flst中。lst2可以是于lstflst相同的链表。
(p, lst2, b, e) be表示lst2中的合法范围。将给定范围中的元素从lst2移动到lstfirst中。lst2lst可以使相同的链表,但p不能指向给定范围中的元素。

链表特有版本的算法操作会改变底层容器。

第十一章 关联容器

关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是mapset

  • map中的元素是一些键值对(key-value):关键字起索引作用,值表示与索引相关联的数据。
  • set中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在set中。

标准库提供了8个关联容器,它们之间的不同体现在三个方面:

  • map还是set类型。
  • 是否允许保存重复的关键字。
  • 是否按顺序保存元素。

允许重复保存关键字的容器名字都包含单词multi;无序保存元素的容器名字都以单词unordered开头。

容器类型 解释
按顺序存储
map 关键数组:保存关键字-值
set 关键字即值,即只保存关键字的容器
multimap 支持同一个键多次出现的map
multiset 支持同一个键多次出现的set
无序集合
unordered_map 用哈希函数组织的map
unordered_set 用哈希函数组织的set
unordered_multimap 哈希组织的map,关键字可以重复出现
unordered_multiset 哈希组织的set,关键字可以重复出现

mapmultimap类型定义在头文件map中;setmultiset类型定义在头文件set中;无序容器定义在头文件unordered_mapunordered_set中。

使用关联容器(Using an Associative Container)

  • map类型通常被称为关联数组(associative array)。

  • map中提取一个元素时,会得到一个pair类型的对象。pair是一个模板类型,保存两个名为firstsecond的公有数据成员。map所使用的pairfirst成员保存关键字,用second成员保存对应的值。

1
2
3
4
5
6
7
8
9
// count the number of times each word occurs in the input
map<string, size_t> 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时,提供的每个键值对用花括号{}包围。

1
2
3
4
5
6
7
8
9
10
map<string, size_t> word_count;   // empty
// list initialization
set<string> exclude = { "the", "but", "and" };
// three elements; authors maps last name to first
map<string, string> authors =
{
{"Joyce", "James"},
{"Austen", "Jane"},
{"Dickens", "Charles"}
};
  • mapset中的关键字必须唯一,multimapmultiset没有此限制。

关键字类型的要求(Requirements on Key Type)

  • 对于有序容器——mapmultimapsetmultiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来进行比较操作。

  • 用来组织容器元素的操作的类型也是该容器类型的一部分。如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型。操作类型在尖括号中紧跟着元素类型给出。

1
2
3
4
5
6
7
8
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<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

pair类型(The pair Type)

  • pair定义在头文件utility中。一个pair可以保存两个数据成员,分别命名为firstsecond
1
2
3
pair<string, string> anon;        // holds two strings
pair<string, size_t> word_count; // holds a string and an size_t
pair<string, vector<int>> line; // holds string and vector<int>
  • pair的默认构造函数对数据成员进行值初始化。

pair支持的操作:

操作 解释
pair<T1, T2> p; p是一个pair,两个类型分别是T1T2的成员都进行了值初始化。
pair<T1, T2> p(v1, v2); firstsecond分别用v1v2进行初始化。
pair<T1, T2>p = {v1, v2}; 等价于p(v1, v2) | |make_pair(v1, v2);|pair的类型从v1v2的类型推断出来。 | |p.first| 返回p的名为first的数据成员。 | |p.second| 返回p的名为second的数据成员。 | |p1 relop p2| 运算关系符按字典序定义。 | |p1 == p2| 必须两对元素两两相等 | |p1 != p2`
  • 在C++11中,如果函数需要返回pair,可以对返回值进行列表初始化。早期C++版本中必须显式构造返回值。
1
2
3
4
5
6
7
8
9
10
pair<string, int> process(vector<string> &v)
{
// process v
if (!v.empty())
// list initialize
return { v.back(), v.back().size() };
else
// explicitly constructed return value
return pair<string, int>();
}

关联容器操作(Operations on Associative Containers)

关联容器定义了类型别名来表示容器关键字和值的类型:

类型别名 解释
key_type 此容器类型的关键字类型
mapped_type 每个关键字关联的类型,只适用于map
value_type 对于map,是pair<const key_type, mapped_type>; 对于set,和key_type相同。
  • 对于set类型,key_typevalue_type是一样的。

  • set中保存的值就是关键字。对于map类型,元素是关键字-值对。即每个元素是一个pair对象,包含一个关键字和一个关联的值。

  • 由于元素关键字不能改变,因此pair的关键字部分是const的。另外,只有map类型(unordered_mapunordered_multimapmultimapmap)才定义了mapped_type

1
2
3
4
5
set<string>::value_type v1;        // v1 is a string
set<string>::key_type v2; // v2 is a string
map<string, int>::value_type v3; // v3 is a pair<const string, int>
map<string, int>::key_type v4; // v4 is a string
map<string, int>::mapped_type v5; // v5 is an int

关联容器迭代器(Associative Container Iterators)

  • 解引用关联容器迭代器时,会得到一个类型为容器的value_type的引用。对map而言,value_typepair类型,其first成员保存const的关键字,second成员保存值。
1
2
3
4
5
6
7
// get an iterator to an element in word_count
auto map_it = word_count.begin();
// *map_it is a reference to a pair<const string, size_t> 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同时定义了iteratorconst_iterator类型,但两种迭代器都只允许只读访问set中的元素。类似mapset中的关键字也是const的。
1
2
3
4
5
6
7
set<int> iset = {0,1,2,3,4,5,6,7,8,9};
set<int>::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
}
  • mapset都支持beginend操作。使用迭代器遍历mapmultimapsetmultiset时,迭代器按关键字升序遍历元素。

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

添加元素(Adding Elements)

  • 使用insert成员可以向关联容器中添加元素。向mapset中添加已存在的元素对容器没有影响。

  • 通常情况下,对于想要添加到map中的数据,并没有现成的pair对象。可以直接在insert的参数列表中创建pair

1
2
3
4
5
// four ways to add word to word_count
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));

关联容器的insert操作:

insert操作 关联容器
c.insert(v) c.emplace(args) vvalue_type类型的对象;args用来构造一个元素。 对于mapset,只有元素的关键字不存在c中才插入或构造元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。对于multimapmultiset则会插入范围中的每个元素。
c.insert(b, e) c.insert(il) be是迭代器,表示一个c::value_type�类型值的范围;il是这种值的花括号列表。函数返回void。对于 mapset,只插入关键字不在c中的元素。
c.insert(p, v) c.emplace(p, args) 类似insert(v),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。

insertemplace的返回值依赖于容器类型和参数:

  • 对于不包含重复关键字的容器,添加单一元素的insertemplace版本返回一个pair,表示操作是否成功。pairfirst成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值。如果关键字已在容器中,则insert直接返回,bool值为false。如果关键字不存在,元素会被添加至容器中,bool值为true
  • 对于允许包含重复关键字的容器,添加单一元素的insertemplace版本返回指向新元素的迭代器。

删除元素(Erasing Elements)

关联容器的删除操作:

操作 解释
c.erase(k) c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量。
c.erase(p) c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end()
c.erase(b, e) 删除迭代器对be所表示范围中的元素。返回e
  • 与顺序容器不同,关联容器提供了一个额外的erase操作。它接受一个key_type参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量。对于不包含重复关键字的容器,erase的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。

map的下标操作(Subscripting a map)

  • map下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。

  • 由于下标运算符可能向容器中添加元素,所以只能对非constmap使用下标操作。

mapunordered_map的下标操作:

操作 解释
c[k] 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其值初始化。
c.at(k) 访问关键字为k的元素,带参数检查;若k不存在在c中,抛出一个out_of_range异常。
  • map进行下标操作时,返回的是mapped_type类型的对象;解引用map迭代器时,返回的是value_type类型的对象。

访问元素(Accessing Elements)

关联容器的查找操作:

操作 解释
c.find(k) 返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
c.count(k) 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1。
c.lower_bound(k) 返回一个迭代器,指向第一个关键字不小于k的元素。
c.upper_bound(k) 返回一个迭代器,指向第一个关键字大于k的元素。
c.equal_range(k) 返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end()
  • 如果multimapmultiset中有多个元素具有相同关键字,则这些元素在容器中会相邻存储。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
multimap<string, string> 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_boundupper_bound操作都接受一个关键字,返回一个迭代器。

  • 如果关键字在容器中,lower_bound返回的迭代器会指向第一个匹配给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配元素之后的位置。

  • 如果关键字不在multimap中,则lower_boundupper_bound会返回相等的迭代器,指向一个不影响排序的关键字插入位置。

  • 因此用相同的关键字调用lower_boundupper_bound会得到一个迭代器范围,表示所有具有该关键字的元素范围。

1
2
3
4
5
6
// 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_boundupper_bound有可能返回尾后迭代器。如果查找的元素具有容器中最大的关键字,则upper_bound返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound也返回尾后迭代器。

  • equal_range操作接受一个关键字,返回一个迭代器pair。若关键字存在,则第一个迭代器指向第一个匹配关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置。若关键字不存在,则两个迭代器都指向一个不影响排序的关键字插入位置。

1
2
3
4
5
// 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)和关键字类型的==运算符组织元素。

  • 无序容器和对应的有序容器通常可以相互替换。但是由于元素未按顺序存储,使用无序容器的程序输出一般会与有序容器的版本不同。

  • 无序容器在存储上组织为一组桶,每个桶保存零或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。

无序容器管理操作:

操作 解释
桶接口
c.bucket_count() 正在使用的桶的数目
c.max_bucket_count() 容器能容纳的最多的桶的数目
c.bucket_size(n) n个桶中有多少个元素
c.bucket(k) 关键字为k的元素在哪个桶中
桶迭代
local_iterator 可以用来访问桶中元素的迭代器类型
const_local_iterator 桶迭代器的const版本
c.begin(n)c.end(n) n的首元素迭代器
c.cbegin(n)c.cend(n) 与前两个函数类似,但返回const_local_iterator
哈希策略
c.load_factor() 每个桶的平均元素数量,返回float值。
c.max_load_factor() c试图维护的平均比桶大小,返回float值。c会在需要时添加新的桶,以使得load_factor<=max_load_factor
c.rehash(n) 重组存储,使得bucket_count>=n,且bucket_count>size/max_load_factor
c.reverse(n) 重组存储,使得c可以保存n个元素且不必rehash
  • 默认情况下,无序容器使用关键字类型的==运算符比较元素,还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型和一些标准库类型提供了hash模板。因此可以直接定义关键字是这些类型的无序容器,而不能直接定义关键字类型为自定义类类型的无序容器,必须先提供对应的hash模板版本。

第十二章 动态内存

程序用堆(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)

  • 智能指针是模板,创建时需要指明指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。
1
2
shared_ptr<string> p1;      // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints

shared_ptrunique_ptr都支持的操作:

操作 解释
shared_ptr<T> sp unique_ptr<T> up 空智能指针,可以指向类型是T的对象
p p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象。
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针,要小心使用,若智能指针释放了对象,返回的指针所指向的对象也就消失了。
swap(p, q) p.swap(q) 交换pq中的指针

shared_ptr独有的操作:

操作 解释
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象。
shared_ptr<T>p(q) pshared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q pq都是shared_ptr,所保存的指针必须能互相转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。
p.unique() p.use_count()是1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试。
  • make_shared函数(定义在头文件memory中)在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
1
2
3
4
5
6
// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 points to an int that is value initialized
shared_ptr<int> p5 = make_shared<int>();
  • 进行拷贝或赋值操作时,每个shared_ptr会记录有多少个其他shared_ptr与其指向相同的对象。
1
2
3
auto p = make_shared<int>(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,它就会自动释放其所管理的对象。
1
2
3
4
5
auto r = make_shared<int>(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)

  • 相对于智能指针,使用newdelete管理内存很容易出错。

  • 默认情况下,动态分配的对象是默认初始化的。所以内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

1
2
string *ps = new string;    // initialized to empty string
int *pi = new int; // pi points to an uninitialized int
  • 可以使用值初始化方式、直接初始化方式、传统构造方式(圆括号())或新标准下的列表初始化方式(花括号{})初始化动态分配的对象。
1
2
3
4
5
6
7
8
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<int> *pv = new vector<int>{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
1
2
3
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_allocnothrow都定义在头文件new中。

1
2
3
// 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的,因此必须使用直接初始化形式。
1
2
shared_ptr<int> p1 = new int(1024);    // error: must use direct initialization
shared_ptr<int> p2(new int(1024)); // ok: uses direct initialization
  • 默认情况下,用来初始化智能指针的内置指针必须指s向动态内存,因为智能指针默认使用delete释放它所管理的对象。如果要将智能指针绑定到一个指向其他类型资源的指针上,就必须提供自定义操作来代替delete
操作 解释
shared_ptr<T> p(q) p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u) punique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d) p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d) pshared_ptr p2的拷贝,唯一的区别是p将可调用对象d来代替delete
p.reset() p是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,则会调用d而不是delete来释放q
p.reset(q) 同上
p.reset(q, d) 同上
  • 不要混合使用内置指针和智能指针。当将shared_ptr绑定到内置指针后,资源管理就应该交由shared_ptr负责。不应该再使用内置指针访问shared_ptr指向的内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ptr is created and initialized when process is called
void process(shared_ptr<int> 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<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x; // undefined: x is a dangling pointer!

shared_ptr<int> 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初始化另一个智能指针或为智能指针赋值。

1
2
3
4
5
6
7
shared_ptr<int> 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<int>(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共享的对象。
1
2
3
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)

  • 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。
1
2
3
4
5
6
7
8
9
10
11
12
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<int> 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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<connection> 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

1
2
3
unique_ptr<int> p1(new int(42));
// C++14
unique_ptr<int> p2 = make_unique<int>(42);
  • 由于unique_ptr独占其指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。

unique_ptr操作:

操作 解释
unique_ptr<T> u1 unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T, D> u2 u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T, D> u(d) unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr 释放u指向的对象,将u置为空。
u.release() u放弃对指针的控制权,返回指针,并将u置空。
u.reset() 释放u指向的对象
u.reset(q) u指向q指向的对象
u.reset(nullptr) u置空
  • release函数返回unique_ptr当前保存的指针并将其置为空。

  • reset函数成员接受一个可选的指针参数,重新设置unique_ptr保存的指针。如果unique_ptr不为空,则它原来指向的对象会被释放。

1
2
3
4
5
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> 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返回的指针,程序就要负责资源的释放。
1
2
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(移动构造、移动赋值)。
1
2
3
4
5
6
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int (p));
// . . .
return ret;
}
  • 老版本的标准库包含了一个名为auto_ptr的类,

  • 类似shared_ptr,默认情况下unique_ptrdelete释放其指向的对象。unique_ptr的删除器同样可以重载,但unique_ptr管理删除器的方式与shared_ptr不同。定义unique_ptr时必须在尖括号中提供删除器类型。创建或reset这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)。

1
2
3
4
5
6
7
8
9
10
11
12
// 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<objT, delT> 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<connection, decltype(end_connection)*> 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指向对象,对象仍然有可能被释放。

  • 操作 解释
    weak_ptr<T> w weak_ptr可以指向类型为T的对象
    weak_ptr<T> w(sp) shared_ptr指向相同对象的weak_ptrT必须能转换为sp指向的类型。
    w = p p可以是shared_ptr或一个weak_ptr。赋值后wp共享对象。
    w.reset() w置为空。
    w.use_count() w共享对象的shared_ptr的数量。
    w.expired() w.use_count()为0,返回true,否则返回false
    w.lock() 如果expiredtrue,则返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr
  • 创建一个weak_ptr时,需要使用shared_ptr来初始化它。

1
2
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged
  • 使用weak_ptr访问对象时,必须先调用lock函数。该函数检查weak_ptr指向的对象是否仍然存在。如果存在,则返回指向共享对象的shared_ptr,否则返回空指针。
1
2
3
4
5
if (shared_ptr<int> 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返回指向第一个对象的指针(元素类型)。
1
2
// 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分配的内存并不是数组类型,因此不能对动态数组调用beginend,也不能用范围for语句处理其中的元素。

  • 默认情况下,new分配的对象是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小后面跟一对空括号()。在新标准中,还可以提供一个元素初始化器的花括号列表。如果初始化器数量大于元素数量,则new表达式失败,不会分配任何内存,并抛出bad_array_new_length异常。

1
2
3
4
5
6
7
8
9
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[]释放动态数组。

1
2
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可以直接管理动态数组,定义时需要在对象类型后添加一对空方括号[]

1
2
3
// up points to an array of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);
up.release(); // automatically uses delete[] to destroy its pointer

指向数组的unique_ptr

操作 解释
unique_ptr<T[]> u u可以指向一个动态分配的数组,整数元素类型为T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
u[i] 返回u拥有的数组中位置i处的对象。u必须指向一个数组。
  • unique_ptr不同,shared_ptr不直接支持动态数组管理。如果想用shared_ptr管理动态数组,必须提供自定义的删除器。
1
2
3
// to use a shared_ptr we must supply a deleter
shared_ptr<int> 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获取内置指针,再用内置指针进行访问。
1
2
3
// 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类是一个模板,定义时必须指定其可以分配的对象类型。

1
2
allocator<string> alloc;    // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings

标准库allocator类及其算法:

操作 解释
allocator<T> a 定义了一个名为aallocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象。
a.deallocate(p, n) 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针。且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args) p必须是一个类型是T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p) pT*类型的指针,此算法对p指向的对象执行析构函数。
  • allocator分配的内存是未构造的,程序需要在此内存中构造对象。新标准库的construct函数接受一个指针和零或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象,必须与对象类型相匹配。
1
2
3
4
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函数接受一个指针,对指向的对象执行析构函数。

1
2
while (q != p)
alloc.destroy(--q); // free the strings we actually allocated
  • deallocate函数用于释放allocator分配的内存空间。传递给deallocate的指针不能为空,它必须指向由allocator分配的内存。而且传递给deallocate的大小参数必须与调用allocator分配内存时提供的大小参数相一致。
1
alloc.deallocate(p, n);

allocator算法:

操作 解释
uninitialized_copy(b, e, b2) 从迭代器be给定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t) 在迭代器be执行的原始内存范围中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
  • 传递给uninitialized_copy的目的位置迭代器必须指向未构造的内存,它直接在给定位置构造元素。返回(递增后的)目的位置迭代器。

第十三章 拷贝控制

一个类通过定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作。

  • 拷贝构造函数(copy constructor)
  • 拷贝赋值运算符(copy-assignment operator)
  • 移动构造函数(move constructor)
  • 移动赋值运算符(move-assignment operator)
  • 析构函数(destructor)

这些操作统称为拷贝控制操作(copy control)。

在定义任何类时,拷贝控制操作都是必要部分。

拷贝、赋值与销毁(Copy,Assign,and Destroy)

拷贝构造函数(The Copy Constructor)

  • 如果一个构造函数的第一个参数是自身类类型的引用(几乎总是const引用),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
1
2
3
4
5
6
7
class Foo
{
public:
Foo(); // default constructor
Foo(const Foo&); // copy constructor
// ...
};
  • 由于拷贝构造函数在一些情况下会被隐式使用,因此通常不会声明为explicit的。

  • 如果类未定义自己的拷贝构造函数,编译器会为类合成一个。一般情况下,合成拷贝构造函数(synthesized copy constructor)会将其参数的非static成员逐个拷贝到正在创建的对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
  • 使用直接初始化时,实际上是要求编译器按照函数匹配规则来选择与实参最匹配的构造函数。使用拷贝初始化时,是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
1
2
3
4
5
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构造函数。

1
2
3
4
5
vector<int> v1(10);     // ok: direct initialization
vector<int> v2 = 10; // error: constructor that takes a size is explicit
void f(vector<int>); // f's parameter is copy initialized
f(10); // error: can't use an explicit constructor to copy an argument
f(vector<int>(10)); // ok: directly construct a temporary vector from an int

拷贝赋值运算符(The Copy-Assignment Operator)

  • 重载运算符(overloaded operator)的参数表示运算符的运算对象。

  • 如果一个运算符是成员函数,则其左侧运算对象会绑定到隐式的this参数上。

  • 赋值运算符通常应该返回一个指向其左侧运算对象的引用。

1
2
3
4
5
6
class Foo
{
public:
Foo& operator=(const Foo&); // assignment operator
// ...
};
  • 标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。

  • 如果类未定义自己的拷贝赋值运算符,编译器会为类合成一个。一般情况下,合成拷贝赋值运算符(synthesized copy-assignment operator)会将其右侧运算对象的非static成员逐个赋值给左侧运算对象的对应成员,之后返回左侧运算对象的引用。

1
2
3
4
5
6
7
8
// 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数据成员。

  • 析构函数的名字由波浪号~接类名构成,它没有返回值,也不接受参数。

1
2
3
4
5
6
class Foo
{
public:
~Foo(); // destructor
// ...
};
  • 由于析构函数不接受参数,所以它不能被重载。

  • 如果类未定义自己的析构函数,编译器会为类合成一个。合成析构函数(synthesized destructor)的函数体为空。

  • 析构函数首先执行函数体,然后再销毁数据成员。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。成员按照初始化顺序的逆序销毁。

  • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

  • 无论何时一个对象被销毁,都会自动调用其析构函数。

  • 当指向一个对象的引用或指针离开作用域时,该对象的析构函数不会执行。

三/五法则(The Rule of Three/Five)

  • 需要析构函数的类一般也需要拷贝和赋值操作。

  • 需要拷贝操作的类一般也需要赋值操作,反之亦然。

使用=default(Using =default)

  • 可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成版本。
1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
};

编写赋值运算符时有两点需要注意:

  • 即使将一个对象赋予它自身,赋值运算符也能正确工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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;
    }
  • 赋值运算符通常结合了拷贝构造函数和析构函数的工作。

    编写赋值运算符时,一个好的方法是先将右侧运算对象拷贝到一个局部临时对象中。拷贝完成后,就可以安全地销毁左侧运算对象的现有成员了。

    1
    2
    3
    4
    5
    6
    7
    8
    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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
};

析构函数释放内存前应该判断是否还有其他对象指向这块内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
8
9
10
11
12
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声明)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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)。这种方式可以正确处理自赋值情况。

1
2
3
4
5
6
7
8
// 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)

  • 某些情况下,一个对象拷贝后就立即被销毁了,此时移动而非拷贝对象会大幅度提高性能。

  • 在旧版本的标准库中,容器所能保存的类型必须是可拷贝的。但在新标准中,可以用容器保存不可拷贝,但可移动的类型。

  • 标准库容器、stringshared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

右值引用(Rvalue Reference)

  • 为了支持移动操作,C++11引入了右值引用类型。右值引用就是必须绑定到右值的引用。可以通过&&来获得右值引用。
1
2
3
4
5
6
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
  • 右值引用只能绑定到即将被销毁,并且没有其他用户的临时对象上。使用右值引用的代码可以自由地接管所引用对象的资源。

  • 变量表达式都是左值,所以不能将一个右值引用直接绑定到一个变量上,即使这个变量的类型是右值引用也不行。

1
2
int &&rr1 = 42;     // ok: literals are rvalues
int &&rr2 = rr1; // error: the expression rr1 is an lvalue!
  • 调用move函数可以获得绑定在左值上的右值引用,此函数定义在头文件utility中。
1
int &&rr3 = std::move(rr1);
  • 调用move函数的代码应该使用std::move而非move,这样做可以避免潜在的名字冲突。

移动构造函数和移动赋值运算符(Move Constructor and Move Assignment)

  • 移动构造函数的第一个参数是该类类型的右值引用,其他任何额外参数都必须有默认值。

  • 除了完成资源移动,移动构造函数还必须确保移后源对象是可以安全销毁的。

  • 在函数的形参列表后面添加关键字noexcept可以指明该函数不会抛出任何异常。

  • 对于构造函数,noexcept位于形参列表和初始化列表开头的冒号之间。在类的头文件声明和定义中(如果定义在类外)都应该指定noexcept

1
2
3
4
5
6
7
8
9
class StrVec
{
public:
StrVec(StrVec&&) noexcept; // move constructor
// other members as before
};

StrVec::StrVec(StrVec &&s) noexcept : /* member initializers */
{ /* constructor body */ }
  • 标准库容器能对异常发生时其自身的行为提供保障。虽然移动操作通常不抛出异常,但抛出异常也是允许的。为了安全起见,除非容器确定元素类型的移动操作不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝而非移动操作。

  • 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

  • 在移动操作之后,移后源对象必须保持有效的、可销毁的状态,但是用户不能使用它的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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数据成员都可以移动时,编译器才会为类合成移动构造函数和移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,则编译器也能移动该成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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函数时也是如此。拷贝赋值运算符和移动赋值运算符的情况类似。

1
2
3
4
5
6
7
8
9
10
11
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
  • 使用非引用参数的单一赋值运算符可以实现拷贝赋值和移动赋值两种功能。依赖于实参的类型,左值被拷贝,右值被移动。
1
2
3
4
5
6
7
8
9
// 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为类型)。
1
2
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

有时可以对右值赋值:

1
2
string s1, s2;
s1 + s2 = "wow!";
  • 在旧标准中,没有办法阻止这种使用方式。为了维持向下兼容性,新标准库仍然允许向右值赋值。但是可以在自己的类中阻止这种行为,规定左侧运算对象(即this指向的对象)必须是一个左值。

  • 在非static成员函数的形参列表后面添加引用限定符(reference qualifier)可以指定this的左值/右值属性。引用限定符可以是&或者&&,分别表示this可以指向一个左值或右值对象。引用限定符必须同时出现在函数的声明和定义中。

1
2
3
4
5
6
7
8
9
10
11
12
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限定符之后。
1
2
3
4
5
6
class Foo
{
public:
Foo someMem() & const; // error: const qualifier must come first
Foo anotherMem() const &; // ok: const qualifier comes first
};
  • 引用限定符也可以区分成员函数的重载版本。
1
2
3
4
5
6
7
8
9
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 &
  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有重载版本都必须有引用限定符。
1
2
3
4
5
6
7
8
9
10
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
};

第十四章 重载运算与类型转换

基本概念(Basic Concepts)

  • 重载的运算符是具有特殊名字的函数,它们的名字由关键字operator和其后要定义的运算符号组成。

  • 重载运算符函数的参数数量和该运算符作用的运算对象数量一样多。对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。

  • 如果一个运算符函数是类的成员函数,则它的第一个运算对象会绑定到隐式的this指针上。因此成员运算符函数的显式参数数量比运算对象的数量少一个。

  • 当运算符作用于内置类型的运算对象时,无法改变该运算符的含义。

  • 只能重载大多数已有的运算符,无权声明新的运算符号。

可以被重载 不可以被重载
+, -, *, /, %, ^ ::, .*, ., ? :,
&, |, ~, !, ,, =
<, >, <=, >=, ++, --
<<, >>, ==, !=, &&, | | |+=,-=,/=,%=,^=,&=| | | \|=,=,<<=,>>=,[],()| | |->,->,new,new[],delete,delete[]`
  • 重载运算符的优先级和结合律与对应的内置运算符一致。

  • 可以像调用普通函数一样直接调用运算符函数。

1
2
3
4
5
// 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
  • 通常情况下,不应该重载逗号,、取地址&、逻辑与&&和逻辑或||运算符。

  • 建议只有当操作的含义对于用户来说清晰明了时才使用重载运算符,重载运算符的返回类型也应该与其内置版本的返回类型兼容。

  • 如果类中含有算术运算符或位运算符,则最好也提供对应的复合赋值运算符。

  • 把运算符定义为成员函数时,它的左侧运算对象必须是运算符所属类型的对象。

1
2
3
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形参。
1
2
3
4
5
6
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 >>)

  • 通常情况下,输入运算符的第一个形参是要读取的流的普通引用,第二个形参是要读入的目的对象的普通引用,返回值是它的第一个形参。
1
2
3
4
5
6
7
8
9
10
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状态。eofbitbadbit等错误最好由IO标准库自己标示。

算术和关系运算符(Arithmetic and Relational Operators)

  • 通常情况下,算术和关系运算符应该定义为非成员函数,以便两侧的运算对象进行转换。其次,由于这些运算符一般不会改变运算对象的状态,所以形参都是常量引用。

  • 算术运算符通常会计算它的两个运算对象并得到一个新值,这个值通常存储在一个局部变量内,操作完成后返回该局部变量的副本作为结果(返回类型建议设置为原对象的const类型)。

1
2
3
4
5
6
7
// 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!=中的一个应该把具体工作委托给另一个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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)

赋值运算符必须定义为成员函数,复合赋值运算符通常也是如此。这两类运算符都应该返回其左侧运算对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
StrVec &StrVec::operator=(initializer_list<string> 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)

下标运算符必须定义为成员函数。

类通常会定义两个版本的下标运算符:一个返回普通引用,另一个是类的常量成员并返回常量引用。

1
2
3
4
5
6
7
8
9
10
11
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)

  • 定义递增和递减运算符的类应该同时定义前置和后置版本,这些运算符通常定义为成员函数。

  • 为了与内置操作保持一致,前置递增或递减运算符应该返回运算后对象的引用。

1
2
3
4
5
6
// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator++()
{
++curr; // advance the current state
return *this;
}
  • 后置递增或递减运算符接受一个额外的(不被使用)int类型形参,该形参的唯一作用就是区分运算符的前置和后置版本。
1
2
3
4
5
6
7
8
9
class StrBlobPtr
{
public:
// increment and decrement
StrBlobPtr& operator++(); // prefix operators
StrBlobPtr& operator--();
StrBlobPtr operator++(int); // postfix operators
StrBlobPtr operator--(int);
};
  • 为了与内置操作保持一致,后置递增或递减运算符应该返回运算前对象的原值(返回类型建议设置为原对象的const类型)。
1
2
3
4
5
6
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
}
  • 如果想通过函数调用的方式使用后置递增或递减运算符,则必须为它的整型参数传递一个值。
1
2
3
StrBlobPtr p(a1);   // p points to the vector inside a1
p.operator++(0); // call postfix operator++
p.operator++(); // call prefix operator++

成员访问运算符(Member Access Operators)

  • 箭头运算符必须定义为成员函数,解引用运算符通常也是如此。

  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的类的对象。

1
2
3
4
5
6
7
8
9
10
11
12
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)

函数调用运算符必须定义为成员函数。一个类可以定义多个不同版本的调用运算符,相互之间必须在参数数量或类型上有所区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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),函数对象常常作为泛型算法的实参。

1
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

lambda是函数对象(Lambdas Are Function Objects)

  • 编写一个lambda后,编译器会将该表达式转换成一个未命名类的未命名对象,类中含有一个重载的函数调用运算符。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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产生的类必须为每个值捕获的变量建立对应的数据成员,并创建构造函数,用捕获变量的值来初始化数据成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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中定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。这些类都被定义为模板的形式,可以为其指定具体的应用类型(即调用运算符的形参类型)。
算术 关系 逻辑
plus<Type> equal_to<Type> logical_and<Type>
minus<Type> not_equal_to<Type> logical_or<Type>
multiplies<Type> greater<Type> logical_not<Type>
divides<Type> greater_equal<Type>
modulus<Type> less<Type>
negate<Type> less_equal<Type>
  • 关系运算符的函数对象类通常被用来替换算法中的默认运算符,这些类对于指针同样适用。
1
2
3
4
5
6
vector<string *> 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<string*>());

可调用对象与function(Callable Objects and function)

  • 调用形式指明了调用返回的类型以及传递给调用的实参类型。不同的可调用对象可能具有相同的调用形式。

  • 标准库function类型是一个模板,定义在头文件functional中,用来表示对象的调用形式。

操作 解释
function<T> f; f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与类型T相同。
function<T> f(nullptr); 显式地构造一个空function
function<T> f(obj) f中存储可调用对象obj的副本
f f作为条件:当f含有一个可调用对象时为真;否则为假。
定义为function<T>的成员的类型
result_type function类型的可调用对象返回的类型
argument_type T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type
first_argument_type 第一个实参的类型
second_argument_type 第二个实参的类型
  • 创建一个具体的function类型时必须提供其所表示的对象的调用形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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<int(int, int)> f1 = add; // function pointer
function<int(int, int)> f2 = div(); // object of a function-object class
function<int(int, int)> 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_functionbinary_function没有关系,后两个类已经被bind函数代替。

重载、类型转换与运算符(Overloading,Conversions,and Operators)

转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversion)。

类型转换运算符(Conversion Operators)

  • 类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型。它不能声明返回类型,形参列表也必须为空,一般形式如下:
1
operator type() const;
  • 类型转换运算符可以面向除了void以外的任意类型(该类型要能作为函数的返回类型)进行定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
};
  • 隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。
1
2
3
4
// 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)。和显式构造函数一样,编译器通常不会将显式类型转换运算符用于隐式类型转换。

1
2
3
4
5
6
7
8
9
10
11
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<int>(si) + 3; // ok: explicitly request the conversion

如果表达式被用作条件,则编译器会隐式地执行显式类型转换。

  • ifwhiledo-while语句的条件部分。
  • for语句头的条件表达式。
  • 条件运算符? :的条件表达式。
  • 逻辑非运算符!、逻辑或运算符||、逻辑与运算符&&的运算对象。

类类型向bool的类型转换通常用在条件部分,因此operator bool一般被定义为显式的。

避免有二义性的类型转换(Avoiding Ambiguous Conversions)

在两种情况下可能产生多重转换路径:

  • A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 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&))
  • 类定义了多个类型转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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)
  • 可以通过显式调用类型转换运算符或转换构造函数解决二义性问题,但不能使用强制类型转换,因为强制类型转换本身也存在二义性。

1
2
A a1 = f(b.operator A());    // ok: use B's conversion operator
A a2 = f(A(b)); // ok: use A's constructor
  • 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标都是算术类型的转换。

  • 使用两个用户定义的类型转换时,如果转换前后存在标准类型转换,则由标准类型转换决定最佳匹配。

  • 如果在调用重载函数时需要使用构造函数或者强制类型转换来改变实参的类型,通常意味着程序设计存在不足。

  • 调用重载函数时,如果需要额外的标准类型转换,则该转换只有在所有可行函数都请求同一个用户定义类型转换时才有用。如果所需的用户定义类型转换不止一个,即使其中一个调用能精确匹配而另一个调用需要额外的标准类型转换,也会产生二义性错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

表达式中运算符的候选函数集既包括成员函数,也包括非成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

如果类既定义了转换目标是算术类型的类型转换,也定义了重载的运算符,则会遇到重载运算符与内置运算符的二义性问题。

第十五章 面向对象程序设计

OOP:概述(OOP:An Overview)

  • 面向对象程序设计(object-oriented programming)的核心思想是数据抽象(封装)、继承和动态绑定(多态)。

  • 通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class),其他类则直接或间接地从基类继承而来,这些继承得到的类叫做派生类(derived class)。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

  • 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类应该将这些函数声明为虚函数(virtual function)。方法是在函数名称前添加virtual关键字。

1
2
3
4
5
6
class Quote
{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
  • 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个或哪些基类继承而来的。类派生列表的形式首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以添加访问说明符。
1
2
3
4
5
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)类型转换,编译器会隐式执行。

1
2
3
4
5
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
  • 每个类控制它自己的成员初始化过程,派生类必须使用基类的构造函数来初始化它的基类部分。派生类的构造函数通过构造函数初始化列表来将实参传递给基类构造函数。
1
2
3
Bulk_quote(const std::string& book, double p, 
std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
  • 除非特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。

  • 派生类初始化时首先初始化基类部分,然后按照声明的顺序依次初始化派生类成员。

  • 派生类可以访问基类的公有成员和受保护成员。

  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。如果某静态成员是可访问的,则既能通过基类也能通过派生类使用它。

  • 已经完整定义的类才能被用作基类。

1
2
3
class Base { /* ... */ } ;
class D1: public Base { /* ... */ };
class D2: public D1 { /* ... */ };

BaseD1的直接基类(direct base),是D2的间接基类(indirect base)。最终的派生类将包含它直接基类的子对象以及每个间接基类的子对象。

  • C++11中,在类名后面添加final关键字可以禁止其他类继承它。
1
2
3
4
5
6
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)则是变量或表达式表示的内存中对象的类型,只有运行时才可知。

  • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

  • 不存在从基类到派生类的隐式类型转换,即使一个基类指针或引用绑定在一个派生类对象上也不行,因为编译器只能通过检查指针或引用的静态类型来判断转换是否合法。

1
2
3
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)了。

1
2
3
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位于函数参数列表之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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关键字来禁止覆盖操作。
1
2
3
4
5
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)
};
  • finaloverride关键字出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。

  • 虚函数也可以有默认实参,每次函数调用的默认实参值由本次调用的静态类型决定。如果通过基类的指针或引用调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。

  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参值最好一致。

  • 使用作用域运算符::可以强制执行虚函数的某个版本,不进行动态绑定。

1
2
// 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)函数。一个纯虚函数无须定义。
1
double net_price(std::size_t) const = 0;
  • 可以为纯虚函数提供定义,但函数体必须定义在类的外部。

  • 含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。

  • 不能创建抽象基类的对象。

  • 派生类构造函数只初始化它的直接基类。

  • 重构(refactoring)负责重新设计类的体系以便将操作或数据从一个类移动到另一个类中。

访问控制与继承(Access Control and Inheritance)

  • 一个类可以使用protected关键字来声明外部代码无法访问,但是派生类对象可以访问的成员。

  • 派生类的成员或友元只能通过派生类对象来访问基类的protected成员。派生类对于一个基类对象中的protected成员没有任何访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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以什么方式继承BD的成员函数和友元都能使用派生类到基类的转换。
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员函数和友元可以使用DB的类型转换;反之,如果D继承B的方式是私有的,则不能使用。

对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类到基类的类型转换也是可访问的。

友元对基类的访问权限由基类自身控制,即使对于派生类中的基类部分也是如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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声明之前的访问说明符决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

  • 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。

  • 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。

  • 派生类定义的成员会隐藏同名的基类成员。

1
2
3
4
5
6
7
8
9
10
11
12
struct Base
{
protected:
int mem;
};

struct Derived : Base
{
int get_mem() { return mem; } // returns Derived::mem
protected:
int mem; // hides mem in the base
};
  • 可以通过作用域运算符::来使用被隐藏的基类成员。
1
2
3
4
5
struct Derived : Base
{
int get_base_mem() { return Base::mem; }
// ...
};
  • 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

  • 和其他函数一样,成员函数无论是否是虚函数都能被重载。

  • 派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对它来说都是可见的,那么它就需要覆盖所有版本,或者一个也不覆盖。

  • 有时一个类仅需覆盖重载集合中的一些而非全部函数,此时如果我们不得不覆盖基类中的每一个版本的话,操作会极其繁琐。为了简化操作,可以为重载成员提供using声明。using声明指定了一个函数名字但不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
};
  • 类内使用using声明改变访问级别的规则同样适用于重载函数的名字。

构造函数与拷贝控制(Constructors and Copy Control)

虚析构函数(Virtual Destructors)

  • 一般来说,如果一个类需要析构函数,那么它也需要拷贝和赋值操作。但基类的析构函数不遵循该规则。

  • 基类通常应该定义一个虚析构函数。

1
2
3
4
5
6
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一个指向派生类对象的基类指针会产生未定义的结果。
1
2
3
4
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)

  • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类成员在内的整个对象。

  • 当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 */
{ /* ... */ }
  • 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝或移动基类部分,则必须在派生类的构造函数初始化列表中显式地使用基类的拷贝或移动构造函数。

  • 派生类的赋值运算符必须显式地为其基类部分赋值。

1
2
3
4
5
6
7
8
// 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;
}
  • 派生类的析构函数只负责销毁派生类自己分配的资源。
1
2
3
4
5
6
class D: public Base
{
public:
// Base::~Base invoked automatically
~D() { /* do what it takes to clean up derived members */ }
};
  • 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

继承的构造函数(Inherited Constructors)

  • C++11新标准允许派生类重用(非常规方式继承)其直接基类定义的构造函数。继承方式是提供一条注明了直接基类名的using声明语句。
1
2
3
4
5
6
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声明不会改变该函数的访问级别,不能指定explicitconstexpr属性。

  • 定义在派生类中的构造函数会替换继承而来的具有相同形参列表的构造函数。

  • 派生类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器会为其合成它们。

  • 当一个基类构造函数含有默认实参时,这些默认值不会被继承。相反,派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认值的形参。

容器与继承(Containers and Inheritance)

  • 因为容器中不能保存不同类型的元素,所以不能把具有继承关系的多种类型的对象直接存储在容器中。

  • 容器不能和存在继承关系的类型兼容。

  • 如果想在容器中存储具有继承关系的对象,则应该存放基类的指针。

第十六章 模板与泛型编程

定义模板(Defining a Template)

函数模板(Function Templates)

  • 函数模板可以用来生成针对特定类型的函数版本。

  • 模板定义以关键字template开始,后跟一个模板参数列表(template parameter list)。模板参数列表以尖括号<>包围,内含用逗号分隔的一个或多个模板参数(template parameter)。

1
2
3
4
5
6
7
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
  • 定义模板时,模板参数列表不能为空。

  • 模板参数表示在类或函数定义中用到的类型或值。当使用模板时,需要显式或隐式地指定模板实参(template argument),并将其绑定到模板参数上。

  • 使用函数模板时,编译器用推断出的模板参数来实例化(instantiate)一个特定版本的函数,这些生成的函数通常被称为模板的实例(instantiation)。

1
2
3
4
5
// instantiates int compare(const int&, const int&)
cout << compare(1, 0) << endl; // T is int
// instantiates int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T is vector<int>
  • 模板类型参数(type parameter)可以用来指定函数的返回类型或参数类型,以及在函数体内用于变量声明和类型转换。类型参数前必须使用关键字classtypename
1
2
3
4
5
6
7
8
9
10
11
12
13
// ok: same type used for the return type and parameter
template <typename T>
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 <typename T, U> T calc(const T&, const U&);
// ok: no distinction between typename and class in a template parameter list
template <typename T, class U> calc (const T&, const U&);
  • 建议使用typename而不是class来指定模板类型参数,这样更加直观。

  • 模板非类型参数(nontype parameter)需要用特定的类型名来指定,表示一个值而非一个类型。非类型参数可以是整型、指向对象或函数类型的指针或左值引用。

1
2
3
4
5
6
7
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}

int compare(const char (&p1)[3], const char (&p2)[4]);
  • 绑定到整型非类型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期,不能用普通局部变量或动态对象作为指针或引用非类型参数的实参。

  • 函数模板也可以声明为inlineconstexpr的,说明符放在模板参数列表之后,返回类型之前。

1
2
3
4
// ok: inline specifier follows the template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of the inline specifier
inline template <typename T> T min(const T&, const T&);
  • 模板程序应该尽量减少对实参类型的要求。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 <typename T>
int compare(const T &v1, const T &v2)
{
if (less<T>()(v1, v2)) return -1;
if (less<T>()(v2, v1)) return 1;
return 0;
}
  • 只有当模板的一个特定版本被实例化时,编译器才会生成代码。此时编译器需要掌握生成代码所需的信息,因此函数模板和类模板成员函数的定义通常放在头文件中。

  • 使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的设计者来保证的。模板设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。

  • 调用者负责保证传递给模板的实参能正确支持模板所要求的操作。

类模板(Class Templates)

使用一个类模板时,必须提供显式模板实参(explicit template argument)列表,编译器使用这些模板实参来实例化出特定的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
class Blob
{
public:
Blob();
Blob(std::initializer_list<T> 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<std::vector<T>> data;
};

Blob<int> ia; // empty Blob<int>
Blob<int> ia2 = { 0, 1, 2, 3, 4 }; // Blob<int> with five elements
// these definitions instantiate two distinct Blob types
Blob<string> names; // Blob that holds strings
Blob<double> prices; // different element type

一个类模板的每个实例都形成一个独立的类,相互之间没有关联。

如果一个类模板中的代码使用了另一个模板,通常不会将一个实际类型(或值)的名字用作其模板实参,而是将模板自己的参数用作被使用模板的实参。

类模板的成员函数具有和类模板相同的模板参数,因此定义在类模板外的成员函数必须以关键字template开始,后跟类模板参数列表。

1
2
template <typename T>
ret-type Blob<T>::member-name(parm-list)

默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。

在类模板自己的作用域内,可以直接使用模板名而不用提供模板实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
class BlobPtr
{
public:
// 类模板作用域内不需要写成BlobPtr<T>形式
BlobPtr& operator++();
}

// 类外定义时需要提供模板实参
template <typename T>
BlobPtr<T>& BlobPtr<T>::operator++()
{
// 进入类模板作用域
BlobPtr Ret = *this;
}

当一个类包含一个友元声明时,类与友元各自是否是模板并无关联。如果一个类模板包含一个非模板友元,则友元可以访问所有类模板实例。如果友元自身是模板,则类可以给所有友元模板实例授予访问权限,也可以只授权给特定实例。

  • 一对一友元关系

    为了引用模板的一个特定实例,必须首先声明模板自身。模板声明包括模板参数列表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // forward declarations needed for friend declarations in Blob
    template <typename> class BlobPtr;
    template <typename> class Blob; // needed for parameters in operator==

    template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);

    template <typename T>
    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<T>;
    friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
    };
  • 通用和特定的模板友元关系

    为了让模板的所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // forward declaration necessary to befriend a specific instantiation of a template
    template <typename T> class Pal;

    class C
    { // C is an ordinary, nontemplate class
    friend class Pal<C>; // 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 <typename T> friend class Pal2;
    };

    template <typename T>
    class C2
    { // C2 is itself a class template
    // each instantiation of C2 has the same instance of Pal as a friend
    friend class Pal<T>; // a template declaration for Pal must be in scope
    // all instances of Pal2 are friends of each instance of C2, prior declaration needed
    template <typename X> 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中,类模板可以将模板类型参数声明为友元。

1
2
3
4
5
6
template <typename Type>
class Bar
{
friend Type; // grants access to the type used to instantiate Bar
// ...
};

C++11允许使用using为类模板定义类型别名。

1
2
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors is a pair<string, string>

类模板可以声明static成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
class Foo
{
public:
static std::size_t count() { return ctr; }

private:
static std::size_t ctr;
};

// instantiates static members Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// all three objects share the same Foo<int>::ctr and Foo<int>::count members
Foo<int> fi, fi2, fi3;

类模板的每个实例都有一个独有的static对象,而每个static成员必须有且只有一个定义。因此与定义模板的成员函数类似,static成员也应该定义成模板。

1
2
template <typename T>
size_t Foo<T>::ctr = 0; // define and initialize ctr

模板参数(Template Parameters)

模板参数遵循普通的作用域规则。与其他任何名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是在模板内不能重用模板参数名。

1
2
3
4
5
6
7
typedef double A;
template <typename A, typename B>
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显式地告知编译器该名字是一个类型。

1
2
3
4
5
6
7
8
template <typename T>
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}

C++11允许为函数和类模板提供默认实参。

1
2
3
4
5
6
7
8
9
// compare has a default template argument, less<T>
// and a default function argument, F()
template <typename T, typename F = less<T>>
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;
}

如果一个类模板为其所有模板参数都提供了默认实参,在使用这些默认实参时,必须在模板名后面跟一个空尖括号对<>

1
2
3
4
5
6
7
8
9
10
11
12
template <class T = int>
class Numbers
{ // by default T is int
public:
Numbers(T v = 0): val(v) { }
// various operations on numbers
private:
T val;
};

Numbers<long double> lots_of_precision;
Numbers<> average_precision; // empty <> says we want the default type

成员模板(Member Templates)

一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数,这种成员被称为成员模板。成员模板不能是虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 <typename T>
void operator()(T *p) const
{
os << "deleting unique_ptr" << std::endl;
delete p;
}

private:
std::ostream &os;
};

在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
class Blob
{
template <typename It>
Blob(It b, It e);
};

template <typename T> // type parameter for the class
template <typename It> // type parameter for the constructor
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>>(b, e))
{ }

为了实例化一个类模板的成员模板,必须同时提供类和函数模板的实参。

控制实例化(Controlling Instantiations)

因为模板在使用时才会进行实例化,所以相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。

在大型程序中,多个文件实例化相同模板的额外开销可能非常严重。C++11允许通过显式实例化(explicit instantiation)来避免这种开销。

显式实例化的形式如下:

1
2
extern template declaration;    // instantiation declaration
template declaration; // instantiation definition

declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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<string>; // instantiates all members of the class template

// Application.cc
// these template types must be instantiated elsewhere in the program
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; // instantiation will appear elsewhere
// Blob<int> and its initializer_list constructor instantiated in this file
Blob<int> a1 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Blob<int> 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引用或指针形参。
  • 如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。数组实参可以转换为指向其首元素的指针。函数实参可以转换为该函数类型的指针。

其他的类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。

一个模板类型参数可以作为多个函数形参的类型。由于允许的类型转换有限,因此传递给这些形参的实参必须具有相同的类型,否则调用失败。

1
2
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)

如果想增强函数的兼容性,可以使用两个类型参数定义函数模板。

1
2
3
4
5
6
7
8
9
10
11
// argument types can differ but must be compatible
template <typename A, typename B>
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)

函数模板中使用普通类型定义的参数可以进行正常的类型转换。

1
2
3
4
5
6
7
8
9
template <typename T>
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)

某些情况下,编译器无法推断出模板实参的类型。

1
2
3
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

显式模板实参(explicit template argument)可以让用户自己控制模板的实例化。提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号<>中指定,位于函数名之后,实参列表之前。

1
2
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)

显式模板实参按照从左到右的顺序与对应的模板参数匹配,只有尾部参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。

1
2
3
4
5
6
7
// poor design: users must explicitly specify all three template parameters
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
// error: can't infer initial template parameters
auto val3 = alternative_sum<long long>(i, lng);
// ok: all three parameters are explicitly specified
auto val2 = alternative_sum<long long, int, long>(i, lng);

对于模板类型参数已经显式指定了的函数实参,可以进行正常的类型转换。

1
2
3
4
long lng;
compare(lng, 1024); // error: template parameters don't match
compare<long>(lng, 1024); // ok: instantiates compare(long, long)
compare<int>(lng, 1024); // ok: instantiates compare(int, int)

尾置返回类型与类型转换(Trailing Return Types and Type Transformation)

由于尾置返回出现在函数列表之后,因此它可以使用函数参数来声明返回类型。

1
2
3
4
5
6
7
// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
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成员就是模板参数类型本身。

Mod<T>,其中Mod是: T是: Mod<T>::type是:
remove_reference X&X&& X
否则 T
add_const X&const X或函数 T
否则 const T
add_lvalue_reference X& T
X&& X&
否则 T&
add_rvalue_reference X&X&& T
否则 T&&
remove_pointer X* X
否则 T
add_pointer X&X&& X*
否则 T*
make_signed unsigned X X
否则 T
make_unsigned 带符号类型 unsigned X
否则 T
remove_extent X[n] X
否则 T
remove_all_extents X[n1][n2]... X
否则 T

使用remove_reference可以获得引用对象的元素类型,如果用一个引用类型实例化remove_reference,则type表示被引用的类型。因为type是一个类的类型成员,所以在模板中必须使用关键字typename来告知编译器其表示一个类型。

1
2
3
4
5
6
7
// must use typename to use a type member of a template parameter
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
// process the range
return *beg; // return a copy of an element from the range
}

函数指针和实参推断(Function Pointers and Argument Deduction)

使用函数模板初始化函数指针或为函数指针赋值时,编译器用指针的类型来推断模板实参。

1
2
3
template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;

如果编译器不能从函数指针类型确定模板实参,则会产生错误。使用显式模板实参可以消除调用歧义。

1
2
3
4
5
6
// 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<int>); // passing compare(const int&, const int&)

模板实参推断和引用(Template Argument Deduction and References)

当一个函数参数是模板类型参数的普通(左值)引用(形如T&)时,只能传递给它一个左值(如一个变量或一个返回引用类型的表达式)。T被推断为实参所引用的类型,如果实参是const的,则T也为const类型。

1
2
3
4
5
template <typename T> 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已经是函数参数类型的一部分了,因此不会再是模板参数类型的一部分。

1
2
3
4
5
6
template <typename T> 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类型是该右值实参的类型。

1
2
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int

模板参数绑定的两个例外规则:

  • 如果将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。

  • 如果间接创建了一个引用的引用(通过类型别名或者模板类型参数间接定义),则这些引用会被“折叠”。右值引用的右值引用会被折叠为右值引用。其他情况下,引用都被折叠为普通左值引用。

    折叠前 折叠后
    T& &T& &&T&& & T&
    T&& && T&&
1
2
3
4
5
6
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&>(int& &&); // when T is int&, function parameter is int& &&
void f3<int&>(int&); // when T is int&, function parameter collapses to int&

模板参数绑定的两个例外规则导致了两个结果:

  • 如果一个函数参数是指向模板类型参数的右值引用,则可以传递给它任意类型的实参。
  • 如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。

当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难。

1
2
3
4
5
6
7
template <typename T>
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
}

实际编程中,模板的右值引用参数通常用于两种情况:模板转发其实参或者模板被重载。函数模板的常用重载形式如下:

1
2
template <typename T> void f(T&&);         // binds to nonconst rvalues
template <typename T> void f(const T&); // lvalues and const rvalues

理解std::move(Understanding std::move)

std::move的定义如下:

1
2
3
4
5
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}

std::move的工作过程:

1
2
3
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_referencestring进行实例化。
    • remove_reference<string>type成员是string
    • move的返回类型是string&&
    • move的函数参数t的类型为string&&
  • std::move(s1)中传递的是左值。
    • 推断出的T类型为string&
    • remove_referencestring&进行实例化。
    • remove_reference<string&>type成员是string
    • move的返回类型是string&&
    • move的函数参数t的类型为string& &&,会折叠成string&

可以使用static_cast显式地将一个左值转换为一个右值引用。

转发(Forwarding)

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在这种情况下,需要保持被转发实参的所有性质,包括实参的const属性以及左值/右值属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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 <typename F, typename T1, typename T2>
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是底层的。

1
2
3
4
5
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}

对于修改后的版本,若调用flip2(f, j, 42),会传递给参数t1一个左值j,但此时推断出的T1类型为int&t1的类型会被折叠为int&,从而解决了flip1的错误。

flip2只能用于接受左值引用的函数,不能用于接受右值引用的函数。函数参数与其他变量一样,都是左值表达式。所以即使是指向模板类型的右值引用参数也只能传递给接受左值引用的函数,不能传递给接受右值引用的函数。

1
2
3
4
5
6
7
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>返回类型T&&

通常情况下,可以使用forward传递定义为指向模板类型参数的右值引用函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性。

1
2
3
4
5
6
template <typename Type>
intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
  • 如果实参是一个右值,则Type是一个普通(非引用)类型,forward<Type>返回类型Type&&
  • 如果实参是一个左值,则通过引用折叠,Type也是一个左值引用类型,forward<Type>返回类型Type&& &,对返回类型进行引用折叠,得到Type&

使用forward编写完善的转发函数。

1
2
3
4
5
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}

std::move一样,对std::forward也不应该使用using声明。

重载与模板(Overloading and Templates)

函数模板可以被另一个模板或普通非模板函数重载。

如果重载涉及函数模板,则函数匹配规则会受到一些影响:

  • 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
  • 候选的函数模板都是可行的,因为模板实参推断会排除任何不可行的模板。
  • 和往常一样,可行函数(模板与非模板)按照类型转换(如果需要的话)来排序。但是可以用于函数模板调用的类型转换非常有限。
  • 和往常一样,如果恰有一个函数提供比其他任何函数都更好的匹配,则选择此函数。但是如果多个函数都提供相同级别的匹配,则:
    • 如果同级别的函数中只有一个是非模板函数,则选择此函数。
    • 如果同级别的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
    • 否则该调用有歧义。

通常,如果使用了一个没有声明的函数,代码将无法编译。但对于重载函数模板的函数而言,如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不再重要了。

1
2
3
4
5
6
7
8
9
10
11
template <typename T> string debug_rep(const T &t);
template <typename T> 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…指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数列表。在函数参数列表中,如果一个参数的类型是模板参数包,则此参数也是函数参数包。

1
2
3
4
5
// 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 <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

对于一个可变参数模板,编译器会推断模板参数类型和参数数量。

可以使用sizeof…运算符获取参数包中的元素数量。类似sizeofsizeof…也返回一个常量表达式,而且不会对其实参求值。

1
2
3
4
5
6
template<typename ... Args>
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)

可变参数函数通常是递归的,第一步调用参数包中的第一个实参,然后用剩余实参调用自身。为了终止递归,还需要定义一个非可变参数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
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 <typename T, typename... Args>
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)。扩展一个包就是将其分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边添加一个省略号来触发扩展操作。

包扩展工作过程:

1
2
3
4
5
6
template <typename T, typename... Args>
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&

    1
    2
    print(cout, i, s, 42);   // two parameters in the pack
    ostream& print(ostream&, const int&, const string&, const int&);
  • 第二个扩展操作扩展函数参数包,模式是函数参数包的名字。扩展结果是一个由包中元素组成、以逗号分隔的列表。

    1
    print(os, s, 42);

    扩展操作中的模式会独立地应用于包中的每个元素。

1
2
3
4
5
6
7
8
9
10
// call debug_rep on each argument in the call to print
template <typename... Args>
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机制来编写函数,实现将其实参不变地传递给其他函数。

1
2
3
4
5
6
7
8
// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template<typename... Args>
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>(args)...);
}

模板特例化(Template Specializations)

在某些情况下,通用模板的定义对特定类型是不合适的,可能编译失败或者操作不正确。如果不希望或不能使用模板版本时,可以定义类或函数模板的特例化版本。一个特例化版本就是模板的一个独立定义,其中的一个或多个模板参数被指定为特定类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// first version; can compare any two types
template <typename T> int compare(const T&, const T&);
// second version to handle string literals
template<size_t N, size_t M>
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)版本本身还是一个模板,用户使用时必须为那些未指定的模板参数提供实参。

只能部分特例化类模板,不能部分特例化函数模板。

由于类模板的部分特例化版本是一个模板,所以需要定义模板参数。对于每个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。在类名之后,需要为特例化的模板参数指定实参,这些实参位于模板名之后的尖括号中,与原始模板中的参数按位置相对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 通用版本
template <typename T>
struct remove_reference
{
typedef T type;
};

// 部分特例化版本
template <typename T>
struct remove_reference<T &> // 左值引用
{
typedef T type;
};

template <typename T>
struct remove_reference<T &&> // 右值引用
{
typedef T type;
};

类模板部分特例化版本的模板参数列表是原始模板参数列表的一个子集或特例化版本。

可以只特例化类模板的指定成员函数,而不用特例化整个模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
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<int>::Bar() // we're specializing the Bar member of Foo<int>
{
// do whatever specialized processing that applies to ints
}

Foo<string> fs; // instantiates Foo<string>::Foo()
fs.Bar(); // instantiates Foo<string>::Bar()
Foo<int> fi; // instantiates Foo<int>::Foo()
fi.Bar(); // uses our specialization of Foo<int>::Bar()

第十七章 标准库特殊设施

tuple类型(The tuple Type)

tuple是类似pair的模板,定义在头文件tuple中。与pair不同,tuple可以有任意数量的成员。如果希望将一些数据组合成单一对象,但又不想定义新数据结构时,可以使用tuple(“快速而随意”的数据结构)。

操作 解释
tuple<T1, T2, ..., Tn> t; t是一个tuple,成员数为n,第i个成员的类型是Ti所有成员都进行值初始化。
tuple<T1, T2, ..., Tn> t(v1, v2, ..., vn); 每个成员用对应的初始值vi进行初始化。此构造函数是explicit的。
make_tuple(v1, v2, ..., vn) 返回一个用给定初始值初始化的tupletuple的类型从初始值的类型推断
t1 == t2 当两个tuple具有相同数量的成员且成员对应相等时,两个tuple相等。
t1 relop t2 tuple的关系运算使用字典序。两个tuple必须具有相同数量的成员。
get<i>(t) 返回t的第i个数据成员的引用:如果t是一个左值,结果是一个左值引用;否则,结果是一个右值引用。tuple的所有成员都是public的。
tuple_size<tupleType>::value 一个类模板,可以通过一个tuple类型来初始化。它有一个名为valuepublic constexpr static数据成员,类型为size_t,表示给定tuple类型中成员的数量。
tuple_element<i, tupleType>::type 一个类模板,可以通过一个整型常量和一个tuple类型来初始化。它有一个名为typepublic成员,表示给定tuple类型中指定成员的类型。

定义和初始化tuple(Defining and Initializing tuples)

定义tuple时需要指定每个成员的类型。创建tuple对象时,可以使用tuple的默认构造函数,它会对每个成员进行值初始化。或者给每个成员提供初始值。包含初始值的构造函数是explicit的,因此必须使用直接初始化语法。

1
2
tuple<size_t, size_t, size_t> threeD = { 1, 2, 3 };   // error
tuple<size_t, size_t, size_t> threeD{ 1, 2, 3 }; // ok

类似make_pairmake_tuple函数可以生成tuple对象。tuple的类型由初始值决定。

1
2
// 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实参后,会返回其指定成员的引用。

1
2
3
4
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_sizetuple_element这两个辅助类模板查询tuple成员的数量和类型。

  • tuple_size通过一个tuple类型来初始化,它有一个名为value的静态公有数据成员,类型为size_t,表示给定tuple中成员的数量。
  • tuple_element通过一个索引值(整型常量)和一个tuple类型来初始化,它有一个名为type的公有数据成员,表示给定tuple中指定成员的类型。

使用decltype可以确定一个对象的类型。

1
2
3
4
5
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<trans>::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时需要指明它包含的二进制位数。

操作 解释
bitset<n> b; bn位;每一位均是0.此构造函数是一个constexpr
bitset<n> b(u); bunsigned long longu的低n位的拷贝。如果n大于unsigned long long的大小,则b中超出unsigned long long的高位被置为0。此构造函数是一个constexpr
bitset<n> b(s, pos, m, zero, one); bstring s从位置pos开始m个字符的拷贝。s只能包含字符zeroone:如果s包含任何其他字符,构造函数会抛出invalid_argument异常。字符在b中分别保存为zeroonepos默认为0,m默认为string::nposzero默认为'0',one默认为'1'。
bitset<n> b(cp, pos, m, zero, one); 和上一个构造函数相同,但从cp指向的字符数组中拷贝字符。如果未提供m,则cp必须指向一个C风格字符串。如果提供了m,则从cp开始必须至少有mzeroone字符。

使用一个整型值初始化bitset时,此值会被转换为unsigned long long类型并被当作位模式处理。bitset中的二进制位就是此模式的副本。如果bitset的大小大于unsigned long long中的二进制位数,剩余的高位会被置为0。如果bitset的大小小于unsigned long long中的二进制位数,则只使用给定值的低位部分。

1
2
3
4
5
6
// 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。

1
2
3
4
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

bitset操作(Operations on bitsets)

bitset操作:

操作 解释
b.any() b中是否存在1。
b.all() b中都是1。
b.none() b中是否没有1。
b.count() b中1的个数。
b.size()
b.test(pos) pos下标是否是1
b.set(pos) pos置1
b.set() 所有都置1
b.reset(pos) 将位置pos处的位复位
b.reset() b中所有位复位
b.flip(pos) 将位置pos处的位取反
b.flip() b中所有位取反
b[pos] 访问b中位置pos处的位;如果bconst的,则当该位置位时,返回true;否则返回false
b.to_ulong() 返回一个unsigned long值,其位模式和b相同。如果b中位模式不能放入指定的结果类型,则抛出一个overflow_error异常。
b.to_ullong() 类似上面,返回一个unsigned long long值。
b.to_string(zero, one) 返回一个string,表示b中位模式。zeroone默认为0和1。
os << b b中二进制位打印为字符10,打印到流os
is >> b is读取字符存入b。当下一个字符不是1或0时,或是已经读入b.size()个位时,读取过程停止。

bitset的下标运算符对const属性进行了重载。const版本的下标运算符在指定位置置位时返回true,否则返回false。非const版本返回bitset定义的一个特殊类型,用来控制指定位置的值。

to_ulongto_ullong操作用来提取bitset的值。只有当bitset的大小不大于对应操作的返回值(to_ulongunsigned longto_ullongunsigned long long)时,才能使用这两个操作。如果bitset中的值不能存入给定类型,则会引发overflow_error异常。

1
2
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中,包含多个组件。

组件 解释
regex 表示一个正则表达式的类
regex_match 将一个字符序列与一个正则表达式匹配
regex_search 寻找第一个与正则表达式匹配的子序列
regex_replace 使用给定格式替换一个正则表达式
sregex_iterator 迭代器适配器,调用regex_searcg来遍历一个string中所有匹配的子串
smatch 容器类,保存在string中搜索的结果
ssub_match string中匹配的子表达式的结果

regex类表示一个正则表达式。

1
2
3
4
5
6
7
8
9
10
11
// 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_matchregex_search函数确定一个给定的字符序列与一个regex是否匹配。如果整个输入序列与表达式匹配,则regex_match函数返回true;如果输入序列中的一个子串与表达式匹配,则regex_search函数返回true。这两个函数的其中一个重载版本接受一个类型为smatch的附加参数。如果匹配成功,函数会将匹配信息保存在给定的smatch对象中。二者的参数形式如下:

操作 解释
(seq, m, r, mft) 在字符序列seq中查找regex对象r中的正则表达式。seq可以是一个string、标识范围的一对迭代器、一个指向空字符结尾的字符数组的指针。
(seq, r, mft) m是一个match对象,用来保存匹配结果的相关细节。mseq必须具有兼容的类型。mft是一个可选的regex_constants::match_flag_type值。

使用正则表达式库(Using the Regular Expression Library)

默认情况下,regex使用的正则表达式语言是ECMAScript。

定义一个regex或者对一个regex调用assign为其赋新值时,可以指定一些标志来影响regex的操作。ECMAScriptbasicextendedawkgrepegrep这六个标志指定编写正则表达式时所使用的语言。这六个标志中必须设置其中之一,且只能设置一个。默认情况下,ECMAScript标志被设置,regex会使用ECMA-262规范,这也是很多Web浏览器使用的正则表达式语言。

操作 解释
regex r(re) regex r(re, f) re表示一个正则表达式,它可以是一个string、一对表示字符范围的迭代器、一个指向空字符结尾的字符数组的指针、一个字符指针和一个计数器、一个花括号包围的字符列表。f是指出对象如何处理的标志。f通过下面列出来的值来设置。如果未指定f,其默认值为ECMAScript
r1 = re r1中的正则表达式替换Wierere表示一个正则表达式,它可以是另一个regex对象、一个string、一个指向空字符结尾的字符数组的指针或是一个花括号包围的字符列表。
r1.assign(re, f) 和使用赋值运算符(=)的效果相同:可选的标志f也和regex的构造函数中对应的参数含义相同。
r.mark_count() r中子表达式的数目
r.flags() 返回r的标志集

定义regex时指定的标志:

操作 解释
icase 在匹配过程中忽略大小写
nosubs 不保存匹配的子表达式
optimize 执行速度优先于构造速度
ECMAScript 使用ECMA-262指定的语法
basic 使用POSIX基本的正则表达式语法
extended 使用POSIX扩展的正则表达式语法
awk 使用POSIX版本的awk语言的语法
grep 使用POSIX版本的grep的语法
egrep 使用POSIX版本的egrep的语法

正则表达式的语法是否正确是在运行期间解析的。如果正则表达式存在错误,标准库会抛出类型为regex_error的异常。除了what操作外,regex_error还有一个名为code的成员,用来返回错误类型对应的数值编码。code返回的值是由具体实现定义的。RE库能抛出的标准错误如下,code返回对应错误的编号(从0开始)。

flag error
error_collate 表达式包含无效的collating元素名字
error_ctype 表达式包含无效的字符类名字
error_escape 表达式包含无效的转义字符或尾部转义(trailing escape)
error_backref 表达式包含无效的反向引用
error_brack 表达式包含不匹配的方括号
error_paren 表达式包含不匹配的圆括号
error_brace 表达式包含不匹配的大括号
error_badbrace 表达式的大括号之间的范围(range)无效
error_range 表达式包含无效的字符范围
error_space 内存不足,无法把表达式转化为有限状态机。
error_badrepeat 表达式中包含重复指示符(即*?+{中的一个)但它前面没有效的正则表达式。
error_complexity 匹配的计算复杂度超出了预设的级别
error_stack 运行栈的内存不足

正则表达式在程序运行时才编译,这是一个非常慢的操作。因此构造一个regex对象或者给一个已经存在的regex赋值是很耗时间的。为了最小化这种开销,应该尽量避免创建不必要的regex。特别是在循环中使用正则表达式时,应该在循环体外部创建regex对象。

RE库为不同的输入序列都定义了对应的类型。使用时RE库类型必须与输入类型匹配。

  • regex类保存char类型的正则表达式;wregex保存wchar_t类型的正则表达式。
  • smatch表示string类型的输入序列;cmatch表示字符数组类型的输入序列;wsmatch表示wstring类型的输入序列;wcmatch表示宽字符数组类型的输入序列。

匹配与Regex迭代器类型(The Match and Regex Iterator Types)

regex迭代器是一种迭代器适配器,它被绑定到一个输入序列和一个regex对象上,每种输入类型都有对应的迭代器类型。

sregex_iterator操作:

操作 解释
sregex_iterator it(b, e, r); 一个sregex_iterator,遍历迭代器be表示的string。它调用sregex_search(b, e, r)it定位到输入中第一个匹配的位置。
sregex_iterator end; sregex_iterator的尾后迭代器
*itit-> 根据最后一个调用regex_search的结果,返回一个smatch对象的引用或一个指向smatch对象的指针。
++itit++ 从输入序列当前匹配位置开始调用regex_search。前置版本返回递增后迭代器;后置版本返回旧值。
it1 == it2 如果两个sregex_iterator都是尾后迭代器,则它们相等。两个非尾后迭代器是从相同的输入序列和regex对象构造,则它们相等。

sregex_iterator为例,将sregex_iterator绑定到一个string和一个regex对象时,迭代器自动定位至给定string中的第一个匹配位置。即,sregex_iterator构造函数对给定stringregex调用regex_search。解引用迭代器时,返回最近一次搜索结果的smatch对象。递增迭代器时,它调用regex_search在输入string中查找下一个匹配位置。

1
2
3
4
5
6
7
8
9
// 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

x的成员,分别返回表示输入序列中当前匹配之前和之后部分的ssub_match对象。一个ssub_match对象有两个名为strlength的成员,分别返回匹配的string和该string`的长度。

1
2
3
4
5
6
7
8
9
10
11
// 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;
}

smatch支持的操作:

操作 解释
m.ready() 如果已经通过调用regex_searchregex_match设置了m,则返回true;否则返回false。如果ready返回false,则对m进行操作是未定义的。
m.size() 如果匹配失败,则返回0,;否则返回最近一次匹配的正则表达式中子表达式的数目。
m.empty() 等价于m.size() == 0
m.prefix() 一个ssub_match对象,标识当前匹配之前的序列
m.suffix() 一个ssub_match对象,标识当前匹配之后的部分
m.format(...)
m.length(n) n个匹配的子表达式的大小
m.position(n) n个子表达式距离序列开始的长度
m.str(n) n个子表达式匹配的string
m[n] 对应第n个子表达式的ssub_match对象
m.begin(), m.end() 表示mssub_match元素范围的迭代器。
m.cbegin(), m.cend() 常量迭代器

使用子表达式(Using Subexpressions)

正则表达式中的模式通常包含一个或多个子表达式。子表达式是模式的一部分,本身也有意义。正则表达式语法通常用括号表示子表达式。

1
2
3
// 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++中的特殊字符,所以在模式中使用\时,需要一个额外的反斜线进行转义。

子匹配操作:

操作 解释
matched 一个public bool数据成员,指出ssub_match是否匹配了
firstsecond public数据成员,指向匹配序列首元素和尾后位置的迭代器。如果未匹配,则firstsecond是相等的。
length() 匹配的大小,如果matchedfalse,则返回0。
str() 返回一个包含输入中匹配部分的string。如果matchedfalse,则返回空string
s = ssub ssub_match对象ssub转化为string对象s。等价于s=ssub.str(),转换运算符不是explicit的。

使用regex_replace(Using regex_replace)

正则表达式替换操作:

操作 解释
m.format(dest, fmt, mft), m.format(fmt, mft) 使用格式字符串fmt生成格式化输出,匹配在m中,可选的match_flag_type标志在mft中。第一个版本写入迭代器dest指向的目的为止,并接受fmt参数,可以是一个string,也可以是一个指向空字符结尾的字符数组的指针。mft的默认值是format_default
rege_replace(dest, seq, r, fmt, mft)regex_replace(seq, r, fmt, mft) 遍历seq,用regex_search查找与regex对象r相匹配的子串,使用格式字符串fmt和可选的match_flag_type标志来生成输出。mft的默认值是match_default

标准库定义了用于在正则表达式替换过程中控制匹配或格式的标志。这些标志可以传递给regex_searchregex_match函数或者smatch类的format成员。匹配和格式化标志的类型为match_flag_type,定义在命名空间regex_constants中。由于regex_constants定义在std中,因此在使用这些名字时,需要同时加上两个命名空间的限定符。

操作 解释
match_default 等价于format_default
match_not_bol 不将首字符作为行首处理
match_not_eol 不将尾字符作为行尾处理
match_not_bow 不将首字符作为单词首处理
match_not_eow 不将尾字符作为单词尾处理
match_any 如果存在多于一个匹配,则可以返回任意一个匹配
match_not_null 不匹配任何空序列
match_continuous 匹配必须从输入的首字符开始
match_prev_avail 输入序列包含第一个匹配之前的内容
format_default ECMAScript规则替换字符串
format_sed POSIX sed规则替换字符串
format_no_copy 不输出输入序列中未匹配的部分
format_first_only 只替换子表达式的第一次出现

默认情况下,regex_replace输出整个输入序列。未与正则表达式匹配的部分会原样输出,匹配的部分按照格式字符串指定的格式输出。使用format_no_copy标志可以只输出匹配部分。

1
2
3
4
// 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)使用引擎类生成指定类型、范围和概率分布的随机数。

C++程序不应该使用rand函数,而应该使用default_random_engine类和恰当的分布类对象。

随机数引擎和分布(Random-Number Engines and Distribution)

随机数引擎是函数对象类,定义了一个不接受参数的调用运算符,返回一个随机unsigned整数。调用一个随机数引擎对象可以生成原始随机数。

1
2
3
4
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类型,此类型一般具有最常用的特性。

随机数引擎操作:

操作 解释
Engine e; 默认构造函数;使用该引擎类型默认的种子
Engine e(s); 使用整型值s作为种子
e.seed(s) 使用种子s重置引擎的状态
e.min()e.max() 此引擎可生成的最小值和最大值
Engine::result_type 此引擎生成的unsigned整型类型
e.discard(u) 将引擎推进u步;u的类型为unsigned long long

大多数情况下,随机数引擎的输出是不能直接使用的,因为生成的随机数范围通常与程序所需要的不符。

使用分布类对象可以得到指定范围的随机数。新标准库的uniform_int_distribution<unsigned>类型生成均匀分布的unsigned值。

1
2
3
4
5
6
7
// uniformly distributed from 0 to 9 inclusive
uniform_int_distribution<unsigned> 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整数在一个系统定义的范围内。一个引擎类型的范围可以通过调用该类型对象的minmax成员来获得。

即使随机数发生器生成的数看起来是随机的,但对于一个给定的发生器,每次运行程序时它都会返回相同的数值序列。

如果函数需要局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static对象,这样随机数发生器就能在函数调用期间保持状态。否则每次调用函数都会生成相同的序列。

1
2
3
4
5
6
7
8
9
10
11
12
// returns a vector of 100 uniformly distributed random numbers
vector<unsigned> 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<unsigned> u(0,9);
vector<unsigned> ret;
for (size_t i = 0; i < 100; ++i)
ret.push_back(u(e));
return ret;
}

通过为引擎提供一个种子(seed),可以让引擎在程序每次运行时生成不同的序列。种子是一个数值,引擎利用它从序列中的一个新位置重新开始生成随机数。

为引擎设置种子有两种方式:

  • 在创建对象时提供种子。
  • 调用引擎的seed成员设置种子。
1
2
3
4
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函数接受单个指针参数,指向用于写入时间的数据结构。如果指针为空,则函数简单地返回时间。

1
default_random_engine e1(time(0));   // a somewhat random seed

由于time函数返回以秒计算的时间,因此用time返回值作为种子的方式只适用于生成种子的间隔为秒级或更长时间的应用。另外如果程序作为一个自动过程的一部分反复运行,这种方式也会无效,可能多次使用的是相同的种子。

其他随机数分布(Other Kinds of Distributions)

rand函数获得随机浮点数的一个常用但不正确的方法是用rand的结果除以RAND_MAX。但因为随机整数的精度通常低于随机浮点数,所以使用这种方法时,有一些浮点值永远不会被生成。

使用新标准库的uniform_real_distribution类型可以获得随机浮点数。

1
2
3
4
5
default_random_engine e;    // generates unsigned random integers
// uniformly distributed from 0 to 1 inclusive
uniform_real_distribution<double> u(0,1);
for (size_t i = 0; i < 10; ++i)
cout << u(e) << " ";

分布类型操作:

操作 解释
Dist d; 默认够赞函数;使d准备好被使用。其他构造函数依赖于Dist的类型;分布类型的构造函数是explicit的。
d(e) 用相同的e连续调用d的话,会根据d的分布式类型生成一个随机数序列;e是一个随机数引擎对象。
d.min(),d.max() 返回d(e)能生成的最小值和最大值。
d.reset() 重建d的状态,是的随后对d的使用不依赖于d已经生成的值。

除了总是生成bool类型的bernouilli_distribution外,其他分布类型都是模板。每个模板都接受单个类型参数,指定分布生成的结果类型。

分布类型限制了可以作为模板类型的参数类型,一些模板只能生成浮点数,而其他模板只能生成整数。分布类型还定义了一个默认模板类型参数,整型分布的默认参数是int,浮点数分布的默认参数是double。使用默认类型时应该在模板名后使用空尖括号。

1
2
// 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操纵符可以输出truefalse,还原格式时使用noboolalpha操纵符。

1
2
3
cout << "default bool values: " << true << " " << false
<< "\nalpha bool values: " << boolalpha
<< true << " " << false << endl;

输出:

1
2
default bool values: 1 0
alpha bool values: true false

默认情况下,整型值的输入输出使用十进制。可以使用hexoctdec操纵符将其改为十六进制、八进制或改回十进制。

1
2
3
4
cout << "default: " << 20 << " " << 1024 << endl;
cout << "octal: " << oct << 20 << " " << 1024 << endl;
cout << "hex: " << hex << 20 << " " << 1024 << endl;
cout << "decimal: " << dec << 20 << " " << 1024 << endl;

输出:

1
2
3
4
default: 20 1024
octal: 24 2000
hex: 14 400
decimal: 20 1024

hexoctdec操纵符只影响整型运算对象,浮点值的表示形式不受影响。

默认情况下,在输出数值时,没有可见的标识指出当前使用的进制模式。如果需要输出八进制或十六进制值,应该使用showbase操纵符。对流应用showbase后,在输出结果中会显示进制,显示模式和指定整型常量进制的规范相同。

  • 前导0x表示十六进制。
  • 前导0表示八进制。
  • 无前导字符表示十进制。

还原格式时使用noshowbase操纵符。

1
2
3
4
5
6
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

输出:

1
2
3
4
default: 20 1024
in octal: 024 02000
in hex: 0x14 0x400
in decimal: 20 1024

默认情况下,十六进制值(包括前导字符)以小写格式输出。使用uppercase操纵符可以输出大写字母。还原格式时使用nouppercase操纵符。

1
2
3
cout << uppercase << showbase << hex
<< "printed in hexadecimal: " << 20 << " " << 1024
<< nouppercase << noshowbase << dec << endl;

输出:

1
printed in hexadecimal: 0X14 0X400

浮点数的输出格式涉及三个方面:

  • 输出精度(即输出多少个数字)。
  • 十六进制、定点十进制或者科学记数法形式输出。
  • 没有小数部分的浮点值是否输出小数点。

默认情况下,浮点值按六位数字精度输出;如果浮点值没有小数部分,则不输出小数点;根据浮点数的值选择输出为定点十进制或科学计数法形式:非常大或非常小的值输出为科学记数法形式,其他值输出为定点十进制形式。

默认情况下,精度控制输出的数字总位数。输出时,浮点值按照当前精度四舍五入而非截断。

调用IO对象的precision成员或者使用setprecision操纵符可以改变精度。

  • precision成员是重载的。一个版本接受一个int值,将精度设置为此值,并返回旧精度值。另一个版本不接受参数,直接返回当前精度值。
  • setprecision操纵符接受一个参数来设置精度。

setprecision操纵符和其他接受参数的操纵符都定义在头文件iomanip中。

1
2
3
4
5
6
7
8
9
10
11
// 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;

输出:

1
2
3
Precision: 6, Value: 1.41421
Precision: 12, Value: 1.41421356237
Precision: 3, Value: 1.41

定义在头文件iostream中的操纵符:

操纵符 解释
boolalpha truefalse输出为字符串
* noboolalpha truefalse输出为1,0
showbase 对整型值输出表示进制的前缀
* noshowbase 不生成表示进制的前缀
showpoint 对浮点值总是显示小数点
* noshowpoint 只有当浮点值包含小数部分时才显示小数点
showpos 对非负数显示+
* noshowpos 对非负数不显示+
uppercase 在十六进制中打印0X,在科学计数法中打印E
* nouppercase 在十六进制中打印0x,在科学计数法中打印e
* dec 整型值显示为十进制
hex 整型值显示为十六进制
oct 整型值显示为八进制
left 在值的右侧添加填充字符
right 在值的左侧添加填充字符
internal 在符号和值之间添加填充字符
fixed 浮点值显示为定点十进制
scientific 浮点值显示为科学计数法
hexfloat 浮点值显示为十六进制(C++11)
defaultfloat 充值浮点数格式为十进制(C++11)
unitbuf 每次输出操作后都刷新缓冲区
1 * nounitbuf
* skipws 输入运算符跳过空白符
noskipws 输入运算符不跳过空白符
flush 刷新ostream缓冲区
ends 插入空字符,然后刷新ostream缓冲区
endl 插入换行,然后刷新ostream缓冲区

操纵符可以强制流使用科学记数法、定点十进制或十六进制形式输出浮点值。

  • scientific使用科学记数法表示浮点值。
  • fixed使用定点十进制表示浮点值。
  • hexfloat(新标准库)使用十六进制表示浮点值。
  • defaultfloat(新标准库)将流恢复到默认状态。

除非程序需要控制浮点数的表示方式,否则最好由标准库来选择计数法。

1
2
3
4
5
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';

输出:

1
2
3
4
5
default format: 141.421
scientific: 1.414214e+002
fixed decimal: 141.421356
hexadecimal: 0x1.1ad7bcp+7
use defaults: 141.421

scientificfixedhexfloat操纵符会改变流的精度含义。执行这些操纵符后,精度控制的将是小数点后面的数字位数,而默认情况下控制的是数字总位数。

默认情况下,当浮点值的小数部分为0时,不显示小数点。使用showpoint操纵符可以强制输出小数点,noshowpoint操纵符还原默认行为。

1
2
3
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,不改变输出流的内部状态,只影响下一次输出的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

输出:

1
2
3
4
5
6
7
8
9
10
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中定义的操纵符:

操作符 作用
dec 设置整数为十进制
hex 设置整数为十六进制
oct 设置整数为八进制
setbase(n) 设置整数为n进制(n=8,10,16)
setfill(n) 设置字符填充,c可以是字符常或字符变量
setprecision(n) 设置浮点数的有效数字为n位
setw(n) 设置字段宽度为n位
setiosflags(ios::fixed) 设置浮点数以固定的小数位数显示
setiosflags(ios::scientific) 设置浮点数以科学计数法表示
setiosflags(ios::left) 输出左对齐
setiosflags(ios::right) 输出右对齐
setiosflags(ios::skipws) 忽略前导空格
setiosflags(ios::uppercase) 在以科学计数法输出E与十六进制输出X以大写输出,否则小写
setiosflags(ios::showpos) 输出正数时显示"+"号
setiosflags(ios::showpoint) 强制显示小数点
resetiosflags() 终止已经设置的输出格式状态,在括号中应指定内容

默认情况下,输入运算符会忽略空白字符(空格符、制表符、换行符、换纸符和回车符)。使用noskipws操纵符可以让输入运算符读取空白符,skipws操纵符还原默认行为。

1
2
3
4
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)。这些操作可以将一个流当作无解释的字节序列来处理。

一些未格式化操作每次处理流的一个字节,它们会读取而不是忽略空白符。

操作 解释
is.get(ch) istream is读取下一个字节存入字符cn中。返回is
os.put(ch) 将字符ch输出到ostream os。返回os
is.get() is的下一个字节作为int返回
is.putback(ch) 将字符ch放回is。返回is
is.unget() is向后移动一个字节。返回is
is.peek() 将下一个字节作为int返回,但不从流中删除它。

使用未格式化IO操作getput可以读取和写入一个字符。

1
2
3
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的常量值,可以用它检测函数返回的值是否是文件尾。

1
2
3
4
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);

一个常见的编程错误是将getpeek函数的返回值赋给char而非int对象,但编译器不能发现这个错误。

1
2
3
4
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操作一次处理大块数据,这些操作可以提高程序执行速度,但需要自己分配并管理用来保存和提取数据的字符数组。

操作 解释
is.get(sink, size, delim) is中读取最多size个字节,并保存在字符数组中,字符数组的起始地址由sink给出。读取过程直到遇到字符delim或读取了size个字节或遇到文件尾时停止。如果遇到了delim,则将其留在输入流中,不读取出来存入sink
is.getline(sink, size, delim) 与接收三个参数的get版本类似,但会读取并丢弃delim
is.read(sink, size) 读取最多size个字节,存入字符数组sink中。返回is
is.gcount() 返回上一个未格式化读取从is读取的字节数
os.write(source, size) 将字符数组source中的size个字节写入os。返回os
is.ignore(size, delim) 读取并忽略最多size个字符,包括delim。与其他未格式化函数不同,ignore有默认参数:size默认值是1,delim的默认值为文件尾。

getgetline函数接受相同的参数,它们的行为类似但不相同。两个函数都一直读取数据,直到遇到下列情况之一:

  • 已经读取了size - 1个字符。
  • 遇到了文件尾(EOF)。
  • 遇到了分隔符。

两个函数的区别在于处理分隔符的方式:get将分隔符留在输入流中作为下一个字符,而getline读取并丢弃分隔符。两个函数都不会将分隔符保存在结果数组中。

读取流数据时的一个常见错误是忘记从流中删除分隔符。

一些操作可能从输入流中读取了未知个数的字节,使用gcount函数可以确定上一次未格式化输入操作读取了多少字符。gcount函数应该在任何后续未格式化输入操作前调用,将字符退回流的操作也属于未格式化输入操作。如果在调用gcount前使用了peekungetputback操作,则gcount的返回值为0。

使用clearignoresync函数可以清空输入流中的数据。读到非法字符时,输入流将处于错误状态。为了继续获取输入数据,先调用clear函数重置流的错误标记。再调用ignore清空流中指定大小的数据,或者调用sync直接清空流中所有数据。numeric_limits<streamsize>::max()返回流的缓冲区大小。

1
2
3
4
5
6
7
8
9
// 重置错误标志
cin.clear();

// 清除流中所有数据
cin.clear();
cin.ignore(numeric_limits<streamsize>::max());

// 清除流中一行数据
cin.ignore(numeric_limits<streamsize>::max(), '\n');

流随机访问(Random Access to a Stream)

随机IO本质上是依赖于操作系统的。

为了支持随机访问,IO类型通过维护一个标记来确定下一次读写操作的位置。seek函数用于移动标记,tell函数用于获取标记。标准库实际上定义了两对seektell函数,一对用于输入流(后缀为g,表示get),一对用于输出流(后缀为p,表示put)。

操作 解释
tellg()tellp 返回一个输入流中(tellg)或输出流中(tellp)标记的当前位置。
seekg(pos)seekp(pos) 在一个输入流或输出流中将标记重定位到给定的绝对地址。pos通常是一个当前teelgtellp返回的值。
seekp(off, from)seekg(off, from) 在一个输入流或输出流中将标记定位到from之前或之后off个字符,from可以是下列值之一:beg,偏移量相对于流开始位置;cur,偏移量相对于流当前位置;end,偏移量相对于流结尾位置。

虽然标准库为所有流类型都定义了seektell函数,但它们是否有意义取决于流绑定到哪个设备。在大多数系统中,绑定到cincoutcerrclog的流不支持随机访问。对这些流可以调用seektell函数,但在运行时会出现错误,流也会被置为无效状态。

从逻辑上考虑,seektell函数的使用范围如下:

  • 可以对istreamifstreamistringstream类型使用g版本。
  • 可以对ostreamofstreamostringstream类型使用p版本。
  • 可以对iostreamfstreamstringstream类型使用gp版本。

一个流中只有一个标记——不存在独立的读标记和写标记。fstreamstringstream类型可以读写同一个流。在这些类型中,有单一的缓冲区用于保存读写的数据,同时标记也只有一个,表示缓冲区中的当前位置。标准库将两个版本的seektell函数都映射到这个标记。

由于流中只有一个标记,因此在切换读写操作时,必须使用seek函数来重定位标记。

seek函数有两个重载版本:一个版本使用绝对地址移动流标记;另一个版本使用指定位置和偏移量移动流标记。

1
2
3
4
5
6
// 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_positionoffset的类型分别是pos_typeoff_type,这两个类型都是机器相关的,定义在头文件istreamostream中。pos_type表示文件位置,而off_type表示距离当前位置的偏移量,偏移量可以是正数也可以是负数。

tellgtellp函数返回一个pos_type值,表示流的当前位置。

第十八章 用于大型程序的工具

异常处理(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的参数类型是引用类型,则该参数是异常对象的一个别名,改变参数就是改变异常对象本身。

  • 在继承体系中,如果

    1
    catch

    的参数类型是基类类型,则可以使用其派生类类型的异常对象对其初始化。

    • catch的参数是基类非引用类型时,异常对象会被切除一部分。
    • catch的参数是基类引用类型时,以常规方式绑定到异常对象。

异常声明的静态类型决定了catch语句所能执行的操作。如果catch的参数是基类类型,则无法使用派生类特有的成员。

通常情况下,如果catch接受的异常与某个继承体系有关,则最好将catch参数定义为引用类型。

查找异常处理代码时,最终结果是第一个与异常匹配的catch语句,但这未必是最佳匹配。因此,越特殊的catch越应该位于整个catch列表的前端。当程序使用具有继承关系的异常时,派生类异常的处理代码应该位于基类异常的处理代码之前。

异常和异常声明的匹配规则比函数参数严格,绝大多数类型转换都不能使用。

  • 允许从非常量到常量的类型转换。
  • 允许从派生类到基类的类型转换。
  • 数组被转换成指向数组元素类型的指针,函数被转换成指向该函数类型的指针。

除此之外,包括标准算术类型转换和类类型转换在内的其他所有转换规则都不能在catch匹配过程中使用。

有时一个单独的catch语句不能完整处理某个异常。执行完一些校正操作后,当前的catch可能会让位于调用链上层的函数继续处理异常。一个catch语句通过重新抛出(rethrowing)的操作将异常传递给另一个catch语句。重新抛出是一条不包含表达式的throw语句。

1
throw;

throw语句只能出现在catchcatch语句调用的函数之内。如果在异常处理代码之外的区域遇到了空throw语句,编译器将调用terminate函数。

重新抛出语句不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。如果catch语句修改了其参数并重新抛出异常,则只有当catch异常声明是引用类型时,程序对参数所做的改变才会被保留并继续传播。

1
2
3
4
5
6
7
8
9
10
11
12
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)的处理代码,可以与任意类型的异常相匹配。

1
2
3
4
5
6
7
8
9
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语句可以同时处理构造函数体和构造函数初始化过程中的异常。

1
2
3
4
5
6
7
8
9
10
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :
data(std::make_shared<std::vector<T>>(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)来指出某个函数不会抛出异常。

1
2
void recoup(int) noexcept;  // won't throw
void alloc(int); // might throw

noexcept说明的出现位置:

  • 关键字noexcept位于函数的参数列表之后,尾置返回类型之前。
  • 对于一个函数来说,noexcept说明必须同时出现在该函数的所有声明和定义语句中。
  • 函数指针的声明和定义也可以指定noexcept
  • typedef或类型别名中不能使用noexcept
  • 在成员函数中,关键字noexcept位于const或引用限定符之后,finaloverride或虚函数的=0之前。

编译器并不会在编译时检查noexcept说明。如果一个函数在指定了noexcept的同时又含有throw语句或其他可能抛出异常的操作,仍然会通过编译(个别编译器可能会提出警告)。

1
2
3
4
5
// 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()的,则也说明该函数不会抛出异常。

1
2
void recoup(int) noexcept;   // recoup doesn't throw
void recoup(int) throw(); // equivalent declaration

noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型。如果实参为true,则函数不会抛出异常;如果实参为false,则函数可能抛出异常。

1
2
void recoup(int) noexcept(true);    // recoup won't throw
void alloc(int) noexcept(false); // alloc can throw

noexcept运算符(noexcept operator)是一个一元运算符,返回bool类型的右值常量表达式,表示给定的运算对象是否会抛出异常。和sizeof类似,noexcept运算符也不会对运算对象求值。

1
noexcept(e)

e调用的函数都含有noexcept说明且e本身不含有throw语句时,上述表达式返回true,否则返回false

noexcept运算符通常在noexcept说明符的实参中使用。

1
void f() noexcept(noexcept(g()));   // f has same exception specifier as g

函数指针与该指针指向的函数必须具有一致的异常说明。如果某个函数指针是noexcept的,则该指针只能指向noexcept函数;如果显式或隐式地说明了函数指针可能抛出异常,则该指针可以指向任何函数。

1
2
3
4
5
6
// 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的。如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许,也可以禁止抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

标准库异常类的继承体系:

exception类型只定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what的虚成员。what函数返回一个const char*,指向一个以NULL结尾的字符数组,并且不会抛出异常。

exceptionbad_castbad_alloc类型定义了默认构造函数。runtime_errorlogic_error类型没有默认构造函数,但是有一个接受C风格字符串或string类型实参的构造函数,该实参通常用于提供错误信息。what函数返回用于初始化异常对象的错误信息。

实际编程中通常会自定义exception(或者exception的标准库派生类)的派生类以扩展其继承体系。这些面向具体应用的异常类表示了与应用相关的异常状态。

命名空间(Namespaces)

大型应用程序通常会使用多个独立开发的库,其中某些名字可能会相互冲突。多个库将名字放置在全局命名空间中会产生命名空间污染(namespace pollution)。

命名空间分割了全局命名空间,其中每个命名空间都是一个作用域。

命名空间定义(Namespace Definitions)

命名空间的定义包含两部分:关键字namespace和随后的命名空间名字。在命名空间名字后面是一系列由花括号包围的声明和定义。能出现在全局作用域中的声明就也能出现在命名空间中。

1
2
3
4
5
6
7
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

命名空间作用域后面不需要分号结束。

和其他名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。命名空间可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。

每个命名空间都是一个作用域,不同命名空间内可以有相同名字的成员。

定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须明确指出所使用的名字属于哪个命名空间。

1
cplusplus_primer::Query q = cplusplus_primer::Query("hello");

命名空间的定义可以是不连续的。

1
2
3
4
namespace nsp
{
// declarations
}

如果之前没有名为nsp的命名空间定义,则上述代码创建一个新的命名空间;否则,上述代码打开已经存在的命名空间定义并为其添加新的成员声明。

利用命名空间不连续的特性可以将几个独立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似管理自定义类和函数的方式。

  • 命名空间的一部分成员用于定义类,以及声明作为类接口的函数和对象。这些成员应该放置在头文件中。
  • 命名空间成员的定义部分放置在另外的源文件中。源文件需要包含对应的头文件。

程序中的某些实体只能定义一次,如非内联函数、静态数据成员等,命名空间中定义的名字也需要满足该要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ---- Sales_data.h----
// #includes should appear before opening the namespace
# include <string>
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不应该出现在命名空间内部。否则头文件中的所有名字都会被定义为该命名空间的成员。

定义多个类型不相关的命名空间时应该使用单独的文件分别表示每个类型。

可以在命名空间的外部定义该命名空间的成员。命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间。

1
2
3
4
5
6
7
// 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);
// ...
}

模板特例化必须定义在原始模板所属的命名空间中。可以在命名空间内部添加模板特例化声明,而在外部对其进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// we must declare the specialization as a member of std
namespace std
{
template <> struct hash<Sales_data>;
}

// having added the declaration for the specialization to std
// we can define the specialization outside the std namespace
template <> struct std::hash<Sales_data>
{
size_t operator()(const Sales_data& s) const
{
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
// other members as before
};

全局作用域中定义的名字被隐式添加到全局命名空间(global namespace)中。全局命名空间以隐式方式声明,在所有程序中都存在。

作用域运算符::可以用于全局命名空间的成员。因为全局命名空间是隐式声明的,所以它并没有名字。

1
::member_name

命名空间可以嵌套。嵌套的命名空间同时也是一个嵌套的作用域,它嵌套在外层命名空间的作用域内。内层命名空间声明的名字会隐藏外层命名空间的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码在访问时需要在名字前添加限定符。

C++11新增了内联命名空间(inline namespace)。和一般的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。定义内联命名空间的方式是在namespace前添加关键字inlineinline必须出现在该命名空间第一次定义的地方。

1
2
3
4
5
6
7
8
9
10
inline namespace FifthEd
{
// namespace for the code from the Primer Fifth Edition
}

namespace FifthEd
{ // implicitly inline
class Query_base { /* ... */ };
// other Query-related declarations
}

当应用程序的代码在两次发布之间发生了改变时,通常会使用内联命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
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后紧跟以花括号包围的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,直到程序结束才销毁。

一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字在每个包含该头文件的文件中对应不同实体。

定义在未命名的命名空间中的名字可以直接使用,不能对其使用作用域运算符。

定义在未命名的命名空间中的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在最外层作用域中,则该命名空间中的名字必须要与全局作用域中的名字有所区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)。别名必须出现在命名空间的定义之后。

1
namespace primer = cplusplus_primer;

一个命名空间可以有多个别名,它们都与命名空间的原名等价。

一条using声明(using declaration)一次只引入命名空间的一个成员。

using声明的有效范围从using声明语句开始,一直到using声明所在的作用域结束为止。在此过程中,外层作用域的同名实体会被隐藏。未加限定的名字只能在using声明所在的作用域及其内层作用域中使用。

using声明可以出现在全局作用域、局部作用域、命名空间作用域和类的作用域中。在类的作用域中使用时,using声明只能指向基类成员。

using声明不同,using指示使某个命名空间中的所有名字都可见。

using指示可以出现在全局作用域、局部作用域和命名空间作用域中,不能出现在类的作用域中。

如果对std等命名空间使用了using指示而未做任何特殊控制的话,会重新引入多个库之间的名字冲突问题。

using指示具有将命名空间成员提升到包含命名空间本身和using指示的最近外层作用域的能力。

1
2
3
4
5
6
7
8
9
10
11
12
// 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
// ...
}

当命名空间被注入到其外层作用域之后,该命名空间中定义的名字可能会与其外层作用域的成员冲突。这种冲突允许存在,但是要想使用冲突的名字,就必须明确指出名字的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

对命名空间内部名字的查找遵循常规查找规则:由内向外依次查找每个外层作用域。只有位于开放的块中且在使用点之前声明的名字才会被考虑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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声明就能被程序使用。

1
2
3
4
5
6
7
8
9
std::string s;
std::cin >> s;
// 等价于
std::operator>>(std::cin, s);

// 若该规则不存在,则必须为>>运算符提供using声明
using std::operator>>;
// 或者显式使用std::operator>>
std::operator>>(std::cin, s);

标准库定义的moveforward模板函数接受一个右值引用形参,可以匹配任何类型。如果应用程序也定义了一个接受单一参数的moveforward函数,则不管形参是什么类型,都会与标准库的版本冲突。对于这两个函数来说,冲突大多是无意的,因此建议使用它们的含有限定语的完整版本(即std::movestd::forward)。

如果一个未声明的类或函数第一次出现在友元声明中,则会被认定是离它最近的外层命名空间的成员。

重载与命名空间(Overloading and Namespaces)

using声明和using指示能将某些函数添加到候选函数集。

确定候选函数集时,会在函数的每个实参类(以及实参类的基类)所属的命名空间中搜索候选函数。这些命名空间中所有与被调用函数同名的函数都会被添加到候选集当中,即使其中某些函数在调用语句处不可见也是如此。

using声明语句声明的是一个名字,而非一个特定的函数。一个using声明囊括了重载函数的所有版本以确保不违反命名空间的接口。

1
2
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指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域中的函数同名,则命名空间的函数会被添加到重载集合中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
class Bear : public ZooAnimal { /* ... */ };
class Panda : public Bear, public Endangered { /* ... */ };

和单继承相同,多重继承的派生列表也只能包含已经被定义过的类,且这些类不能是final的。

多重继承关系中,派生类对象包含每个基类的子对象。

构造一个多重继承的派生类对象将同时构造并初始化它的所有基类子对象。

1
2
3
4
5
6
7
// 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允许派生类从它的一个或多个基类中继承构造函数,但如果从多个基类中继承了相同的构造函数(即形参列表完全相同),程序会产生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Base1
{
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};

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
};

如果一个类从它的多个基类中继承了相同的构造函数,则必须为该构造函数定义其自己的版本。

1
2
3
4
5
6
7
8
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)

多重继承和单继承相同,某个可访问基类的指针或引用可以直接指向派生类对象。

编译器不会在派生类向基类的几种转换中进行比较和选择。

1
2
3
4
5
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,它直接继承自istreamostream,而istreamostream都继承自base_ios,所以iostream继承了base_ios两次。如果iostream对象包含base_ios的两份拷贝,则无法在同一个缓冲区中进行读写操作。

虚继承可以让某个类共享它的基类,其中共享的基类子对象称为虚基类(virtual base class)。在该机制下,不论虚基类在继承体系中出现了多少次,派生类都只包含唯一一个共享的虚基类子对象。

通常情况下,使用虚继承的类层次是由一个人或一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个是虚基类,况且新基类的开发者也无法改变已存在的类体系。

虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。

指定虚基类的方式是在派生列表中添加关键字virtual

1
2
3
// the order of the keywords public and virtual is not significant
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };

如果某个类指定了虚基类,则该类的派生仍按照常规方式进行。

1
class Panda : public Bear, public Raccoon, public Endangered { /* ... */ };

不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。

因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,而且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则也可以直接访问该成员。但如果成员被多个基类覆盖,则一般情况下派生类必须为该成员定义新的版本。例如,假设类B定义了一个名为X的成员,D1D2都从B虚继承得到,D继承了D1D2。则在D的作用域中,X通过D的两个基类都是可见的。如果通过D的对象使用X,则有三种可能性:

  • 如果D1D2中都没有X的定义,则X会被解析为B的成员,此时不存在二义性。
  • 如果D1D2中的某一个定义了X,派生类的X会比共享虚基类BX优先级更高,此时同样没有二义性。
  • 如果D1D2都定义了X,则直接访问X会产生二义性问题。

构造函数与虚继承(Constructors and Virtual Inheritance)

在虚派生中,虚基类是由最低层的派生类初始化的。如果按普通规则处理,虚基类将会在多条继承路径上被重复初始化。

继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。只要能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。即使虚基类不是派生类的直接基类,构造函数也可以进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
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) { }

构造含有虚基类的对象时,首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,之后再按照直接基类在派生列表中出现的顺序依次对其初始化。

一个类可以有多个虚基类,此时这些虚子对象按照它们在派生列表中出现的顺序依次构造。

第十九章 特殊工具与技术

控制内存分配(Controlling Memory Allocation)

重载new和delete(Overloading new and delete)

使用new表达式时,实际执行了三步操作:

  • new表达式调用名为operator new(或operator new[])的标准库函数。该函数分配一块足够大、原始、未命名的内存空间以便存储特定类型的对象(或对象数组)。
  • 编译器调用对应的构造函数构造这些对象并初始化。
  • 对象被分配了空间并构造完成,返回指向该对象的指针。

使用delete表达式时,实际执行了两步操作:

  • 对指针所指向的对象(或对象数组)执行对应的析构函数。
  • 编译器调用名为operator delete(或operator delete[])的标准库函数释放内存空间。

如果程序希望控制内存分配的过程,则需要定义自己的operator newoperator delete函数。编译器会用自定义版本替换标准库版本。

程序可以在全局作用域中定义operator newoperator delete函数,也可以将其定义为成员函数。编译器发现newdelete表达式后,将在程序中查找可供调用的operator函数。如果被分配或释放的对象是类类型,编译器会先在类及其基类的作用域中查找。如果该类含有operator成员,则表达式会调用这些成员。否则编译器会继续在全局作用域查找。如果找到自定义版本,则使用该版本的函数。如果没找到,则使用标准库定义的版本。

可以使用作用域运算符令newdelete表达式忽略定义在类中的函数,直接执行全局作用域版本。

标准库定义了operator newoperator delete函数的8个重载版本,其中前4个版本可能抛出bad_alloc异常,后4个版本不会抛出异常。重载这些运算符时,必须使用关键字noexcept指定其不抛出异常。

1
2
3
4
5
6
7
8
9
10
// 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还定义了一个名为nothrowconst对象,用户可以通过这个对象请求new的非抛出版本。

operator函数定义为类的成员时,它们是隐式静态的,无须显式地声明static。因为operator new用在对象构造之前,operator delete用在对象销毁之后,所以它们必须是静态成员,而且不能操纵类的任何数据成员。

operator newoperator new[]函数的返回类型必须是void*,第一个形参的类型必须是size_t且不能有默认实参。编译器调用operator new时,用存储指定类型对象所需的字节数初始化size_t形参;调用operator new[]时,传入函数的则是存储数组中所有元素所需的空间。

自定义operator new函数时可以为它提供额外的形参,用到这些自定义函数的new表达式必须使用new的定位形式传递参数。下面这种形式的new函数只供标准库使用,不能被用户重定义:

1
void *operator new(size_t, void*);   // this version may not be redefined

operator deleteoperator delete[]函数的返回类型必须是void,第一个形参的类型必须是void*。函数被调用时,编译器会用指向待释放内存的指针来初始化void*形参。

operator deleteoperator delete[]定义为类的成员时,可以包含另一个类型为size_t的形参。该形参的初始值是第一个形参所指向对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete的字节数会因待删除指针所指向对象的动态类型不同而有所区别。实际运行的operator delete函数版本也由对象的动态类型决定。

malloc函数接受一个表示待分配字节数的size_t参数,返回指向分配空间的指针,或者返回0以表示分配失败。free函数接受一个void*参数,它是malloc返回的指针的副本,free将相关内存返回给系统。调用free(0)没有任何意义。

1
2
3
4
5
6
7
8
9
10
11
12
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 newoperator delete函数。它们类似allocator类的allocatedeallocate成员,负责分配或释放内存空间,但不会构造或销毁对象。

不能使用allocator类的construct函数在operator new分配的内存空间中构造对象,而应该使用定位new表达式构造。

1
2
3
4
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运算符的形式如下:

1
2
3
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)

其中type是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,e必须是一个有效指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。在所有形式中,e的类型必须符合以下条件之一:

  • etype的公有派生类。
  • etype的公有基类。
  • etype类型相同。

如果条件符合,则类型转换成功,否则转换失败。转换失败可能有两种结果:

  • 如果dynamic_cast语句的转换目标是指针类型,则结果为0。

    1
    2
    3
    4
    5
    6
    7
    8
    if (Derived *dp = dynamic_cast<Derived*>(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中)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void f(const Base &b)
    {
    try
    {
    const Derived &d = dynamic_cast<const Derived&>(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用于比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同。

1
2
3
4
5
6
7
8
9
10
11
12
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作用于指针时,返回的结果是该指针的静态编译类型。

1
2
3
4
5
// 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可以为具有继承关系的类实现相等运算符。

相等运算符的形参是基类的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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检查两个运算对象的类型是否一致,类型一致才会继续判断每个数据成员的取值是否相同。

1
2
3
4
5
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函数的形参都是基类的引用,但是在比较之前需要先把运算对象转换成自己的类型。

1
2
3
4
5
6
7
8
9
10
11
bool Derived::equal(const Base &rhs) const
{
// we know the types are equal, so the cast won't throw
auto r = dynamic_cast<const Derived&>(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中,并且至少提供以下操作:

操作
t1 == t2 如果两个对象t1和t2类型相同,则返回true;否则返回false
t1 != t2 如果两个对象t1和t2类型不同,则返回true;否则返回false
t.name() 返回类型的C-style字符串,类型名字用系统相关的方法产生
t1.before(t2) 返回指出t1是否出现在t2之前的bool值

type_info类一般是作为一个基类出现,所以它还应该提供一个公有虚析构函数。当编译器希望提供额外的类型信息时,通常在type_info的派生类中完成。

type_info类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义为删除的。创建type_info对象的唯一方式就是使用typeid运算符。

对于某种给定类型来说,name成员的返回值因编译器而异并且不一定与在程序中使用的名字一致。对于name返回值的唯一要求就是类型不同则返回的字符串必须有所区别。

枚举类型(Enumerations)

和类一样,每个枚举类型都定义了一种新的类型。枚举属于字面值常量类型。

C++包含两种枚举:

  • 限定作用域的枚举(scoped enumeration,C++11新增)。定义形式是关键字enum class(或enum struct)后接枚举类型名字以及用花括号包围、以逗号分隔的枚举成员(enumerator)列表。

    1
    2
    3
    4
    5
    6
    enum class open_modes
    {
    input,
    output,
    append
    };
  • 不限定作用域的枚举(unscoped enumeration)。定义时省略关键字class(或struct),枚举类型名字是可选的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    C++// unscoped enumeration
    enum color
    {
    red,
    yellow,
    green
    };
    // unnamed, unscoped enum
    enum
    {
    floatPrec = 6,
    doublePrec = 10,
    double_doublePrec = 10
    };

    如果枚举是未命名的,则只能在定义该枚举时一同定义它的对象。

在限定作用域的枚举类型中,枚举成员的名字遵循常规作用域规则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。

1
2
3
4
5
6
7
8
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。也可以直接为枚举成员指定特定的值。

1
2
3
4
5
enum class intTypes
{
charTyp = 8, shortTyp = 16, intTyp = 16,
longTyp = 32, long_longTyp = 64
};

枚举值可以不唯一。如果没有显式提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。

枚举成员是const的,因此在初始化枚举成员时提供的初始值必须是常量表达式。

可以在任何需要常量表达式的地方使用枚举成员。如:

  • 定义枚举类型的constexpr变量。
  • 将枚举类型对象作为switch语句的表达式,而将枚举值作为case标签。
  • 将枚举类型作为非类型模板形参使用。
  • 在类的定义中初始化枚举类型的静态数据成员。

初始化枚举对象或者给枚举对象赋值时,必须使用该类型的一个枚举成员或者该类型的另一个对象。即使某个整型值恰好与枚举成员的值相等,也不能用其初始化枚举对象。

1
2
open_modes om = 2;        // error: 2 is not of type open_modes
om = open_modes::input; // ok: input is an enumerator of open_modes

不限定作用域的枚举类型对象或枚举成员能自动转换成整型。

1
2
int i = color::red;     // ok: unscoped enumerator implicitly converted to int
int j = peppers::red; // error: scoped enumerations are not implicitly converted

枚举是由某种整数类型表示的。C++11中,可以在枚举名字后面指定用来表示枚举成员的整型类型。

1
2
3
4
enum intValues : unsigned long long
{
/*...*/
};

如果没有指定枚举的潜在类型,则默认情况下限定作用域的枚举成员类型是int。不限定作用域的枚举成员不存在默认类型。

C++11中可以提前声明枚举。枚举的前置声明必须指定(无论隐式或显式)其成员的类型。

1
2
3
// 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的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

初始化或者给成员指针赋值时,需要指定它所指向的成员。

1
pdata = &Screen::contents;

成员指针使用.*->*来获得其指向对象的成员。

1
2
3
4
5
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或引用限定符。

1
2
3
// 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;

如果成员函数存在重载问题,则必须显式声明指针指向的函数类型。

1
2
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;

和普通函数指针不同,在成员函数和指向该成员的指针之间不存在自动转换规则。

1
2
3
// 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

成员函数指针使用.*->*来调用类的成员函数。

1
2
3
4
5
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)

成员指针不是一个可调用对象,不支持函数调用运算符。

1
2
3
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

1
2
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);

定义一个function对象时,必须指定该对象所能表示的函数类型(即可调用对象的形式)。如果可调用对象是一个成员函数,则第一个形参必须表示该成员是在哪个对象上执行的。

使用标准库功能mem_fn(定义在头文件functional中)可以让编译器推断成员的类型。和function一样,mem_fn可以从成员指针生成可调用对象。但mem_fn可以根据成员指针的类型推断可调用对象的类型,无须显式指定。

1
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));

mem_fn生成的可调用对象可以通过对象和指针调用。

1
2
3
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)。嵌套类通常用于定义作为实现部分的类。

外层类的对象和嵌套类的对象是相互独立的。在嵌套类对象中不包含任何外层类定义的成员,在外层类对象中也不包含任何嵌套类定义的成员。

嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。

外层类对嵌套类的成员没有特殊的访问权限,嵌套类对外层类的成员也没有特殊的访问权限。

嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类的访问说明符决定。

嵌套类必须声明在类的内部,但是可以定义在类的内部或外部。在外层类之外定义嵌套类时,必须用外层类的名字限定嵌套类的名字。

1
2
3
4
5
6
7
8
9
10
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中,含有构造函数或析构函数的类类型也可以作为联合的成员类型。

联合可以为其成员指定publicprotectedprivate等保护标记。默认情况下,联合的成员都是公有的。

联合可以定义包括构造函数和析构函数在内的成员函数。但是由于联合既不能继承自其他类,也不能作为基类使用,所以在联合中不能含有虚函数。

定义联合时,首先是关键字union,随后是该联合的名字(可选)以及在花括号内的一组成员声明。

1
2
3
4
5
6
7
8
// 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;
};

默认情况下,联合是未初始化的。可以像显式初始化聚合类一样显式初始化联合,提供的初始值会被用于初始化第一个成员。

1
Token first_token = { 'a' };   // initializes the cval member

可以使用通用的成员访问运算符访问联合对象的成员。

1
2
last_token.cval = 'z';
pt->ival = 42;

匿名联合(anonymous union)是一个未命名的联合,并且在右花括号和分号之间没有任何声明。一旦定义了一个匿名联合,编译器就会自动地为该联合创建一个未命名的对象。在匿名联合的定义所在的作用域内,该联合的成员都是可以直接访问的。

1
2
3
4
5
6
7
8
9
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

匿名联合不能包含protectedprivate成员,也不能定义成员函数。

C++的早期版本规定,在联合中不能含有定义了构造函数或拷贝控制成员的类类型成员。C++11取消了该限制。但是如果联合的成员类型定义了自己的构造函数或拷贝控制成员,该联合的用法会比只含有内置类型成员的联合复杂得多。

  • 当联合只包含内置类型的成员时,可以使用普通的赋值语句改变联合的值。但是如果想将联合的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须构造或析构该类类型的成员。
  • 当联合只包含内置类型的成员时,编译器会按照成员顺序依次合成默认构造函数或拷贝控制成员。但是如果联合含有类类型成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器会为该联合合成对应的版本并将其声明为删除的。

对于联合来说,构造或销毁类类型成员的操作非常复杂。通常情况下,可以把含有类类型成员的联合内嵌在另一个类中,这个类可以管理并控制与联合的类类型成员相关的状态转换。

局部类(Local Classes)

类可以定义在某个函数的内部,这种类被称为局部类。局部类定义的类型只能在定义它的作用域内可见。

局部类的所有成员(包括成员函数)都必须完整定义在类的内部,因此局部类的作用与嵌套类相比相差很远。

局部类中不允许声明静态数据成员。

局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员,不能使用普通局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
};
// . . .
}

常规的访问保护规则对于局部类同样适用。外层函数对局部类的私有成员没有任何访问特权。局部类可以将外层函数声明为友元。

可以在局部类的内部再嵌套一个类。此时嵌套类的定义可以出现在局部类之外,不过嵌套类必须定义在与局部类相同的作用域中。

1
2
3
4
5
6
7
8
9
10
11
12
13
void foo()
{
class Bar
{
public:
class Nested; // declares class Nested
};

// definition of Nested
class Bar::Nested
{
};
}

局部类内的嵌套类也是一个局部类,必须遵循局部类的各种规定。

固有的不可移植的特性(Inherently Nonportable Features)

位域(Bit-fields)

类可以将其非静态数据成员定义成位域,在一个位域中含有一定数量的二进制位。当程序需要向其他程序或硬件设备传递二进制数据时,通常会使用位域。

位域的声明形式是在成员名字之后紧跟一个冒号和一个常量表达式,该表达式用于指定成员所占的二进制位数。

位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以通常情况下使用无符号类型保存位域。位域类型的大小不能小于位域结构的总大小。

1
2
3
4
5
6
7
8
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,防止数据结构错位。

1
2
3
4
5
// 保存原始对齐值,设置新对齐
#pragma pack(push, 1)
// 结构体定义……
// 恢复原始对齐值
#pragma pack(pop)

位域成员按定义顺序在内存中由低地址向高地址排列,具体布局与机器相关。

取地址符&不能作用于位域,因此任何指针都无法指向类的位域。

如果可能的话,类内部连续定义的位域会压缩在同一整数的相邻位,从而提供存储压缩。

访问位域的方式与访问类的其他数据成员的方式类似。操作超过1位的位域时,通常会使用内置的位运算符。

1
2
3
4
5
6
7
8
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类似,都是对类型的额外修饰。二者相互之间并没有影响。

1
2
3
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指针。

1
2
3
4
5
6
7
8
9
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对象,则必须自定义拷贝或移动操作。

1
2
3
4
5
6
7
8
9
10
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++函数所用的语言。

链接指示有单个形式和复合形式,其不能出现在类定义或函数定义的内部。同样的链接指示必须出现在函数的每个声明处。

1
2
3
4
5
6
7
8
9
// illustrative linkage directives that might appear in the C++ header <cstring>
// 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指示被放置在复合链接指示的花括号中时,头文件中的所有函数声明都会被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含自带链接指示的函数,该函数不会受到影响。

1
2
3
4
5
// compound-statement linkage directive
extern "C"
{
#include <string.h> // C functions that manipulate C-style strings
}

C++从C语言继承的标准库函数可以定义成C函数,但并非必须。选择使用C还是C++实现C标准库,是由每个C++实现决定的。

编写函数所使用的语言是函数类型的一部分。因此对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示,而且指向这类函数的指针也必须使用与函数本身一样的链接指示。

1
2
// pf points to a C function that returns void and takes an int
extern "C" void (*pf)(int);

指向C函数的指针与指向C++函数的指针是不同的类型,两者不能相互赋值或初始化(少数C++编译器支持这种赋值操作并将其视为对语言的扩展,但是从严格意义上来说它是非法的)。

1
2
3
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函数的指针,必须使用类型别名。

1
2
3
4
5
6
// 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++函数在其他语言编写的程序中可用。编译器会为该函数生成适合于指定语言的代码。

1
2
// the calc function can be called from C programs
extern "C" double calc(double dparm) { /* ... */ }

如果需要在C和C++中编译同一个源文件,可以在编译C++版本时使用预处理定义__cplusplus

1
2
3
4
5
# ifdef __cplusplus
// ok: we're compiling C++
extern "C"
# endif
int strcmp(const char*, const char*);

链接指示与重载函数的相互作用依赖于目标语言。C语言不支持函数重载,所以一个C链接指示只能用于说明一组重载函数中的某一个。

1
2
3
// error: two extern "C" functions with the same name
extern "C" void print(const char*);
extern "C" void print(int);

相关参考