第一章 开始
std::cin 中的循环流使用
C++ 中可以使用while(std::cin >> value){//Code }
的方式来进行循环数据的读入,直到没有输出为止;示例代码如下:
1 | /* 输入样例: 3 4 5 6 输出 : Sum is :18 */ |
注意:
- 当键盘向程序中输入数据时,对于文件结束;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 |
|
成员函数(类方法):使用.
调用。
命名空间(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
类型的取值是true
或false
。一个
char
的大小和一个机器字节一样,确保可以存放机器基本字符集中任意字符对应的数字值。wchar_t
确保可以存放机器最大扩展字符集中的任意一个字符。在整型类型大小方面,C++规定
short
≤int
≤long
≤long long
(long long
是C++11定义的类型)。浮点型可表示单精度(single-precision)、双精度(double-precision)和扩展精度(extended-precision)值,分别对应
float
、double
和long double
类型。除去布尔型和扩展字符型,其他整型可以分为带符号(signed)和无符号(unsigned)两种。带符号类型可以表示正数、负数和0,无符号类型只能表示大于等于0的数值。类型
int
、short
、long
和long long
都是带符号的,在类型名前面添加unsigned
可以得到对应的无符号类型,如unsigned int
。字符型分为
char
、signed char
和unsigned char
三种,但是表现形式只有带符号和无符号两种。类型char
和signed char
并不一样,char
的具体形式由编译器(compiler)决定。
如何选择类型
- 当明确知晓数值不可能是负数时,选用无符号类型;
- 使用
int
执行整数运算。一般long
的大小和int
一样,而short
常常显得太小。除非超过了int
的范围,选择long long
。 - 算术表达式中不要使用
char
或bool
。 - 浮点运算选用
double
。
类型转换
进行类型转换时,类型所能表示的值的范围决定了转换的过程。
- 把非布尔类型的算术值赋给布尔类型时,初始值为0则结果为
false
,否则结果为true
。 - 把布尔值赋给非布尔类型时,初始值为
false
则结果为0,初始值为true
则结果为1。 - 把浮点数赋给整数类型时,进行近似处理,结果值仅保留浮点数中的整数部分。
- 把整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
- 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数(8比特大小的
unsigned char
能表示的数值总数是256)取模后的余数。 - 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。
避免无法预知和依赖于实现环境的行为。
无符号数不会小于0这一事实关系到循环的写法。
1 | // WRONG: u can never be less than 0; the condition will always succeed |
当u等于0时,--u的结果将会是4294967295。一种解决办法是用while
语句来代替for
语句,前者可以在输出变量前先减去1。
1 | unsigned u = 11; // start the loop one past the first element we want to print |
不要混用带符号类型和无符号类型。
字面值常量
一个形如
42
的值被称作字面值常量(literal)。- 整型和浮点型字面值。
- 字符和字符串字面值。
- 使用空格连接,继承自C。
- 字符字面值:单引号,
'a'
- 字符串字面值:双引号,
"Hello World""
- 转义序列。
\n
、\t
等。 - 布尔字面值。
true
,false
。 - 指针字面值。
nullptr
以
0
开头的整数代表八进制(octal)数,以0x
或0X
开头的整数代表十六进制(hexadecimal)数。在C++14中,0b
或0B
开头的整数代表二进制(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 | std::cout << '\n'; // prints a newline |
泛化转义序列的形式是
\x
后紧跟1个或多个十六进制数字,或者\
后紧跟1个、2个或3个八进制数字,其中数字部分表示字符对应的数值。如果\
后面跟着的八进制数字超过3个,则只有前3个数字与\
构成转义序列。相反,\x
要用到后面跟着的所有数字。1
2std::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
3long 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
2extern int i; // declares but does not define i
int j; // declares and defines jextern
语句如果包含了初始值就不再是声明了,而变成了定义。只声明而不定义: 在变量名前添加关键字
extern
,如extern int i;
。但如果包含了初始值,就变成了定义:extern double pi = 3.14;
变量只能被定义一次,但是可以多次声明。
名字的作用域(namescope)
如果要在多个文件中使用同一个变量,就必须将声明和定义分开。此时变量的定义必须出现且只能出现在一个文件中,其他使用该变量的文件必须对其进行声明,但绝对不能重复定义。
标识符(Identifiers)
C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感。C++为标准库保留了一些名字。用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。
名字的作用域(Scope of a Name)
定义在函数体之外的名字拥有全局作用域(global scope)。声明之后,该名字在整个程序范围内都可使用。
最好在第一次使用变量时再去定义它。这样做更容易找到变量的定义位置,并且也可以赋给它一个比较合理的初始值。
作用域中一旦声明了某个名字,在它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字,此时内层作用域中新定义的名字将屏蔽外层作用域的名字。
可以用作用域操作符
::
来覆盖默认的作用域规则。因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,会向全局作用域发出请求获取作用域操作符右侧名字对应的变量。
1 |
|
如果函数有可能用到某个全局变量,则不宜再定义一个同名的局部变量。
左值和右值
- 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
- 右值(r-value)只能出现在赋值语句的右边,比如常量。
复合类型(Compound Type)
引用(References)
引用:引用是一个对象的别名,引用类型引用(refer to)另外一种类型。如
int &refVal = val;
。1
2
3int 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 | int ival = 42; |
给解引用的结果赋值就是给指针所指向的对象赋值。
解引用操作仅适用于那些确实指向了某个对象的有效指针。
空指针(null pointer)不指向任何对象,在试图使用一个指针前代码可以先检查它是否为空。得到空指针最直接的办法是用字面值
nullptr
来初始化指针。旧版本程序通常使用
NULL
(预处理变量,定义于头文件cstdlib中,值为0)给指针赋值,但在C++11中,最好使用nullptr
初始化空指针。
1 | int *p1 = nullptr; // equivalent to int *p1 = 0; |
建议初始化所有指针。
void*
是一种特殊的指针类型,可以存放任意对象的地址,但不能直接操作void*
指针所指的对象。
理解复合类型的声明(Understanding Compound Type Declarations)
指向指针的指针(Pointers to Pointers):
1 | int ival = 1024; |
指向指针的引用(References to Pointers):
1 | int i = 42; |
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清它的真实含义。
const限定符
- 动机:希望定义一些不能被改变值的变量。
- 在变量类型前添加关键字
const
可以创建值不能被改变的对象。const
变量必须被初始化。 - const变量默认不能被其他文件访问,非要访问,必须在指定const前加extern。
1 | const int bufSize = 512; // input buffer size |
const的引用 (References to const)
reference to const(对常量的引用):指向const对象的引用。
临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
对临时量的引用是非法行为。
把引用绑定在
const
对象上即为对常量的引用(reference to const)。对常量的引用不能被用作修改它所绑定的对象。1
2
3
4const 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
5int 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
2double dval = 3.14;
const int &ri = dval;指针和const(Pointers and const)
指向常量的指针(pointer to const)不能用于修改其所指向的对象。常量对象的地址只能使用指向常量的指针来存放,但是指向常量的指针可以指向一个非常量对象。
1
2
3
4
5
6const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = π // error: ptr is a plain pointer
const double *cptr = π // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr
double dval = 3.14; // dval is a double; its value can be changed
cptr = &dval; // ok: but can't change dval through cptr定义语句中把
*
放在const
之前用来说明指针本身是一个常量,常量指针(const pointer)必须初始化。1
2
3
4int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = π // pip is a const pointer to a const object指针本身是常量并不代表不能通过指针修改其所指向的对象的值,能否这样做完全依赖于其指向对象的类型。
顶层const(Top-Level const)
顶层
const
表示指针本身是个常量,底层const
(low-level const)表示指针所指的对象是一个常量。指针类型既可以是顶层const
也可以是底层const
。1
2
3
4
5
6int 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
2i = 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
5int *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
4const 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 expressionC++11允许将变量声明为
constexpr
类型以便由编译器来验证变量的值是否是一个常量表达式。1
2
3constexpr 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
3constexpr int *p = nullptr; // p是指向int的const指针
constexpr int i = 0;
constexpr const int *cp = &i; // cp是指向const int的const指针const
和constexpr
限定的值都是常量。但constexpr
对象的值必须在编译期间确定,而const
对象的值可以延迟到运行期间确定。建议使用
constexpr
修饰表示数组大小的对象,因为数组的大小必须在编译期间确定且不能改变。
类型别名(Type Aliases)
- 传统别名:使用typedef来定义类型的同义词。
typedef double wages;
- 新标准别名:别名声明(alias declaration):
using SI = Sales_item;
(C++11)
auto类型说明符
auto类型说明符:让编译器自动推断类型。
C++11新增
auto
类型说明符,能让编译器自动分析表达式所属的类型。auto
定义的变量必须有初始值。1
2int i = 0, &r = i;
auto a = r; // a is an int (r is an alias for i, which has type int)
编译器推断出来的auto
类型有时和初始值的类型并不完全一样。
当引用被用作初始值时,编译器以引用对象的类型作为
auto
的类型。1
2int 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
5const 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 | auto &g = ci; // g is a const int& that is bound to ci |
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
4const 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)。- 类内初始值不能使用圆括号。
- 类定义的最后应该加上分号。
编写自己的头文件
头文件通常包含哪些只能被定义一次的实体:类、
const
和constexpr
变量。头文件一旦改变,相关的源文件必须重新编译以获取更新之后的声明。
预处理器(preprocessor):确保头文件多次包含仍能安全工作。
当预处理器看到
#include
标记时,会用指定的头文件内容代替#include
头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。
1
2
3
4
5
6
strct Sale_data{
...
}在高级版本的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) |
s2 是s1 的副本 |
string s2 = s1 |
等价于s2(s1) ,s2 是s1 的副本 |
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 |
返回s1 和s2 连接后的结果 |
s1=s2 |
用s2 的副本代替s1 中原来的字符 |
s1==s2 |
如果s1 和s2 中所含的字符完全一样,则它们相等;string 对象的相等性判断对字母的大小写敏感 |
s1!=s2 |
同上 |
< , <= , > , >= |
利用字符在字典中的顺序进行比较,且对字母的大小写敏感 |
string io:
- 执行读操作
>>
:忽略掉开头的空白(包括空格、换行符和制表符),直到遇到下一处空白为止。 getline
:读取一整行,包括空白符。
- 执行读操作
字符串字面值和string是不同的类型。
在执行读取操作时,
string
对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。使用
getline
函数可以读取一整行字符。该函数只要遇到换行符就结束读取并返回结果,如果输入的开始就是一个换行符,则得到空string
。触发getline
函数返回的那个换行符实际上被丢弃掉了,得到的string
对象中并不包含该换行符。size
函数返回string
对象的长度,返回值是string::size_type
类型,这是一种无符号类型。要使用size_type
,必须先指定它是由哪种类型定义的。如果一个表达式中已经有了
size
函数就不要再使用int
了,这样可以避免混用int
和unsigned int
可能带来的问题。当把
string
对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是string
。1
2
3string 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
2for (declaration : expression)
statementexpression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration部分的变量都会被初始化为expression部分的下一个元素值。
1
2
3
4string 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 |
v1 和v2 相等当且仅当它们的元素数量相同且对应位置的元素值都相同 |
v1 != v2 |
同上 |
< ,<= ,> , >= |
以字典顺序进行比较 |
范围
for
语句内不应该改变其遍历序列的大小。vector
对象(以及string
对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能用于添加元素。size
函数返回vector
对象中元素的个数,返回值是由vector
定义的size_type
类型。vector
对象的类型包含其中元素的类型。1
2vector<int>::size_type // ok
vector::size_type // errorvector
和string
对象的下标运算符只能用来访问已经存在的元素,而不能用来添加元素。### 迭代器iterator1
2
3
4
5
6vector<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):每种标准容器都有自己的迭代器。
C++
倾向于用迭代器而不是下标遍历元素。const_iterator:只能读取容器内元素不能改变。
箭头运算符: 解引用 + 成员访问,
it->mem
等价于(*it).mem
谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。
定义了迭代器的类型都拥有
begin
和end
两个成员函数。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如果容器为空,则
begin
和end
返回的是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符:
运算符 | 解释 |
---|---|
*iter |
返回迭代器iter 所指向的元素的引用 |
iter->mem |
等价于(*iter).mem |
++iter |
令iter 指示容器中的下一个元素 |
--iter |
令iter 指示容器中的上一个元素 |
iter1 == iter2 |
判断两个迭代器是否相等 |
因为
end
返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。在
for
或者其他循环语句的判断条件中,最好使用!=
而不是<
。所有标准库容器的迭代器都定义了==
和!=
,但是只有其中少数同时定义了<
运算符。如果
vector
或string
对象是常量,则只能使用const_iterator
迭代器,该迭代器只能读元素,不能写元素。begin
和end
返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回const_iterator
;如果对象不是常量,则返回iterator
。1
2
3
4vector<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_iteratorC++11新增了
cbegin
和cend
函数,不论vector
或string
对象是否为常量,都返回const_iterator
迭代器。任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。
迭代器运算
vector
和string
迭代器支持的运算:
运算符 | 解释 |
---|---|
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
6const 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
4char 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
4int *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";
。用数组初始化
vector
:int a[] = {1,2,3,4,5}; vector<int> v(begin(a), end(a));
。大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
数组和指针
在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。
一维数组寻址公式: \[ \begin{eqnarray} &&\text { Type array[N]; }\\ &&{ array }[n]:^{\star}(\text { array }+n \times \text { sizeof ( Type ) }) \end{eqnarray} \]1
2
3string 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]
当使用数组作为一个
auto
变量的初始值时,推断得到的类型是指针而非数组。但decltype
关键字不会发生这种转换,直接返回数组类型。1
2
3
4
5
6
7
8int 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 ia3C++11在头文件iterator中定义了两个名为
begin
和end
的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。1
2
3int 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) |
比较p1 和p2 的相等性。如果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
3string 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
3int 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++程序中应该尽量使用
vector
、string
和迭代器,避免使用内置数组、C风格字符串和指针。
多维数组(Multidimensional Arrays)
C++中的多维数组其实就是数组的数组。
当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。
多维数组的初始化:
int ia[3][4] = {{0,1,2,3}, ...}
。使用范围for语句时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
多维数组初始化的几种方式:
1
2
3
4
5
6
7
8
9
10
11
12int 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
3for (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
2for (auto row : ia)
for (auto col : row)使用范围
for
语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。
1
2
3int 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
2int *ip[4]; // array of pointers to int
int (*ip)[4]; // pointer to an array of four ints使用
auto
和decltype
能省略复杂的指针定义。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)。除此之外,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
表达式求值过程中,小整数类型(如
bool
、char
、short
等)通常会被提升(promoted)为较大的整数类型,主要是int
。C++定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自定义其含义,这被称作运算符重载(overloaded operator)。
C++的表达式分为右值(rvalue)和左值(lvalue)。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的地址。需要右值的地方可以用左值代替,反之则不行。
- 赋值运算符需要一个非常量左值作为其左侧运算对象,返回结果也是一个左值。
- 取地址符作用于左值运算对象,返回指向该运算对象的指针,该指针是一个右值。
- 内置解引用运算符、下标运算符、迭代器解引用运算符、
string
和vector
的下标运算符都返回左值。 - 内置类型和迭代器的递增递减运算符作用于左值运算对象。前置版本返回左值,后置版本返回右值。
如果
decltype
作用于一个求值结果是左值的表达式,会得到引用类型。
优先级与结合律(Precedence and Associativity)
复合表达式(compound expression)指含有两个或多个运算符的表达式。
优先级与结合律决定了运算对象的组合方式。
括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
求值顺序(Order of Evaluation)
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
1 | int i = 0; |
处理复合表达式时建议遵循以下两点:
- 不确定求值顺序时,使用括号来强制让表达式的组合关系符合程序逻辑的要求。
- 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。
当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,第二条规则无效。如*++iter
,递增运算符改变了iter的值,而改变后的iter又是解引用运算符的运算对象。类似情况下,求值的顺序不会成为问题。
算术运算符(Logical and Relational Operators)
关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。
溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。
运算符 | 描述 |
---|---|
+ | 把两个操作数相加 |
- | 从第一个操作数中减去第二个操作数 |
* | 把两个操作数相乘 |
/ | 分子除以分母 |
% | 取模运算符,整除后的余数 |
++ | 自增运算符,整数值增加 1 |
-- | 自减运算符,整数值减少 1 |
逻辑和关系运算符(Logical and Relational Operators)
关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
逻辑与(logical AND)运算符
&&
和逻辑或(logical OR)运算符||
都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值
true
和false
作为运算对象。
运算符 | 描述 |
---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 |
赋值运算符(Assignment Operators)
赋值运算符
=
的左侧运算对象必须是一个可修改的左值。C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
1
2vector<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
2int 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 | int i = 0, j; |
除非必须,否则不应该使用递增或递减运算符的后置版本。后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。
在某些语句中混用解引用和递增运算符可以使程序更简洁。
1 | cout << *iter++ << endl; |
成员访问运算符(The Member Access Operators)
点运算符.
和箭头运算符->
都可以用来访问成员,表达式ptr->mem
等价于(*ptr).mem
。
1 | string s1 = "a string", *p = &s1; |
条件运算符(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
运算得到整个数组所占空间的大小。 - 对
string
或vector
对象执行sizeof
运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。
逗号运算符(Comma Operator)
逗号运算符,
含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在for
循环中。
1 | vector<int>::size_type cnt = ivec.size(); |
类型转换(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
2type (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 | // read until we hit end-of-file or find an input equal to sought |
- 使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。
多余的空语句并非总是无害的。
1 | // disaster: extra semicolon: loop body is this null statement |
复合语句(compound statement)是指用花括号括起来的(可能为空)语句和声明的序列。复合语句也叫做块(block),一个块就是一个作用域。在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在(最内层)块的结尾处为止。
语句块不以分号作为结束。
空块的作用等价于空语句。
语句作用域(Statement Scope)
可以在if
、switch
、while
和for
语句的控制结构内定义变量,这些变量只在相应语句的内部可见,一旦语句结束,变量也就超出了其作用范围。
1 | while (int i = get_num()) // i is created and initialized on each iteration |
条件语句(Conditional Statements)
if语句(The if Statement)
if
语句的形式:
1 | if (condition) |
if-else
语句的形式:
1 | if (condition) |
其中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 | case true: |
迭代语句(Iterative Statements)
迭代语句通常称为循环,它重复执行操作直到满足某个条件才停止。while
和for
语句在执行循环体之前检查条件,do-while
语句先执行循环体再检查条件。
while语句(The while Statement)
while
语句的形式:
1 | while (condition) |
只要condition的求值结果为
true
,就一直执行statement(通常是一个块)。condition不能为空,如果condition第一次求值就是false
,statement一次都不会执行。定义在
while
条件部分或者循环体内的变量每次迭代都经历从创建到销毁的过程。在不确定迭代次数,或者想在循环结束后访问循环控制变量时,使用
while
比较合适。
传统的for语句(Traditional for Statement)
for
语句的形式:
1 | for (initializer; condition; expression) |
一般情况下,initializer负责初始化一个值,这个值会随着循环的进行而改变。
condition作为循环控制的条件,只要condition的求值结果为
true
,就执行一次statement。执行后再由expression负责修改initializer初始化的变量,这个变量就是condition检查的对象。如果condition第一次求值就是
false
,statement一次都不会执行。initializer中也可以定义多个对象,但是只能有一条声明语句,因此所有变量的基础类型必须相同。for
语句头中定义的对象只在for
循环体内可见。
范围for语句(Range for Statement)
范围for
语句的形式:
1 | for (declaration : expression) |
其中expression表示一个序列,拥有能返回迭代器的
begin
和end
成员。declaration定义一个变量,序列中的每个元素都应该能转换成该变量的类型(可以使用auto
)。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。每次迭代都会重新定义循环控制变量,并将其初始化为序列中的下一个值,之后才会执行statement。
do-while语句(The do-while Statement)
do-while
语句的形式:
1 | do |
计算condition的值之前会先执行一次statement,condition不能为空。
如果condition的值为
false
,循环终止,否则重复执行statement。因为
do-while
语句先执行语句或块,再判断条件,所以不允许在条件部分定义变量。
跳转语句(Jump Statements)
跳转语句中断当前的执行过程。
break语句(The break Statement)
break
语句只能出现在迭代语句或者switch
语句的内部,负责终止离它最近的while
、do-while
、for
或者switch
语句,并从这些语句之后的第一条语句开始执行。
1 | string buf; |
continue语句(The continue Statement)
continue
语句只能出现在迭代语句的内部,负责终止离它最近的循环的当前一次迭代并立即开始下一次迭代。和break
语句不同的是,只有当switch
语句嵌套在迭代语句内部时,才能在switch
中使用continue
。
continue
语句中断当前迭代后,具体操作视迭代语句类型而定:
- 对于
while
和do-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 | try |
try
语句块中的program-statements组成程序的正常逻辑,其内部声明的变量在块外无法访问,即使在catch
子句中也不行。catch
子句包含关键字catch
、括号内一个对象的声明(异常声明,exception declaration)和一个块。当选中了某个catch
子句处理异常后,执行与之对应的块。catch
一旦完成,程序会跳过剩余的所有catch
子句,继续执行后面的语句。如果最终没能找到与异常相匹配的
catch
子句,程序会执行名为terminate
的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。类似的,如果一段程序没有try
语句块且发生了异常,系统也会调用terminate
函数并终止当前程序的执行。
第六章 函数
函数基础(Function Basics)
典型的函数定义包括返回类型(return type)、函数名字、由0个或多个形式参数(parameter,简称形参)组成的列表和函数体(function body)。函数执行的操作在函数体中指明。
1 | // factorial of val is val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1) |
程序通过调用运算符(call operator)来执行函数。调用运算符的形式之一是一对圆括号()
,作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是一个用逗号隔开的实际参数(argument,简称实参)列表,用来初始化函数形参。调用表达式的类型就是函数的返回类型。
1 | int main() |
函数调用完成两项工作:
- 用实参初始化对应的形参。
- 将控制权从主调函数转移给被调函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
return
语句结束函数的执行过程,完成两项工作:
- 返回
return
语句中的值(可能没有值)。 - 将控制权从被调函数转移回主调函数,函数的返回值用于初始化调用表达式的结果。
实参是形参的初始值,两者的顺序和类型必须一一对应。
函数的形参列表可以为空,但是不能省略。
1 | void f1() { /* ... */ } // implicit void parameter list |
形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。
1 | int f3(int v1, v2) { /* ... */ } // error |
函数的任意两个形参不能同名,函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
形参的名字是可选的,但是无法使用未命名的形参。即使某个形参不被函数使用,也必须为它提供一个实参。
函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。
局部对象(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 | // function that takes a pointer and sets the pointed-to value to zero |
如果想在函数体内访问或修改函数外部的对象,建议使用引用形参代替指针形参。
传引用参数(Passing Arguments by Reference)
通过使用引用形参,函数可以改变实参的值。
1 | // function that takes a reference to an int and sets the given object to zero |
使用引用形参可以避免拷贝操作,拷贝大的类类型对象或容器对象比较低效。另外有的类类型(如IO类型)根本就不支持拷贝操作,这时只能通过引用形参访问该类型的对象。
除了内置类型、函数对象和标准库迭代器外,其他类型的参数建议以引用方式传递。
如果函数无须改变引用形参的值,最好将其声明为常量引用。
一个函数只能返回一个值,但利用引用形参可以使函数返回额外信息。
const形参和实参(const Parameters and Arguments)
当形参有顶层
const
时,传递给它常量对象或非常量对象都是可以的。可以使用非常量对象初始化一个底层
const
形参,但是反过来不行。把函数不会改变的形参定义成普通引用会极大地限制函数所能接受的实参类型,同时也会给别人一种误导,即函数可以修改实参的值。
数组形参(Array Parameters)
因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式。
1 | // each function has a single parameter of type const int* |
因为数组会被转换成指针,所以当我们传递给函数一个数组时,实际上传递的是指向数组首元素的指针。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。
以数组作为形参的函数必须确保使用数组时不会越界。
如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向常量的指针。
形参可以是数组的引用,但此时维度是形参类型的一部分,函数只能作用于指定大小的数组。
将多维数组传递给函数时,数组第二维(以及后面所有维度)的大小是数组类型的一部分,不能省略。
1 | f(int &arr[10]) // error: declares arr as an array of references |
main:处理命令行选项(main:Handling Command-Line Options)
可以在命令行中向main
函数传递参数,形式如下:
1 | 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
6void 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 | if (expected != actual) |
因为
initializer_list
包含begin
和end
成员,所以可以使用范围for
循环处理其中的元素。省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为
varargs
的C标准库功能。通常,省略符形参不应该用于其他目的。省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
返回类型和return语句(Return Types and the return Statement)
return
语句有两种形式,作用是终止当前正在执行的函数并返回到调用该函数的地方。
1 | return; |
无返回值函数(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
11vector<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_SUCCESS
和EXIT_FAILURE
这两个预处理变量,分别表示执行成功和失败。
1 | int main() |
建议使用预处理变量
EXIT_SUCCESS
和EXIT_FAILURE
表示main
函数的执行结果。如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。
1 | // calculate val!, which is 1 * 2 * 3 . . . * val |
在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。
相对于循环迭代,递归的效率较低。但在某些情况下使用递归可以增加代码的可读性。循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继),而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)。
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 | // fcn takes an int argument and returns a pointer to an array of ten ints |
任何函数的定义都能使用尾置返回类型,但是这种形式更适用于返回类型比较复杂的函数。
如果我们知道函数返回的指针将指向哪个数组,就可以使用
decltype
关键字声明返回类型。但decltype
并不会把数组类型转换成指针类型,所以还要在函数声明中添加一个*
符号。
1 | int odd[] = {1,3,5,7,9}; |
函数重载(Overloaded Functions)
同一作用域内的几个名字相同但形参列表不同的函数叫做重载函数。
main
函数不能重载。不允许两个函数除了返回类型以外的其他所有要素都相同。
顶层
const
不影响传入函数的对象,一个拥有顶层const
的形参无法和另一个没有顶层const
的形参区分开来。
1 | Record lookup(Phone); |
- 如果形参是某种类型的指针或引用,则通过区分其指向的对象是常量还是非常量可以实现函数重载,此时的
const
是底层的。当我们传递给重载函数一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
1 | // functions taking const and nonconst references or pointers have different parameters |
const_cast
可以用于函数的重载。当函数的实参不是常量时,将得到普通引用。
1 | // return a reference to the shorter of two strings |
函数匹配(function matching)也叫做重载确定(overload resolution),是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。
调用重载函数时有三种可能的结果:
编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
编译器找不到任何一个函数与实参匹配,发出无匹配(no match)的错误信息。
有一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出二义性调用(ambiguous call)的错误信息。
重载与作用域(Overloading and Scope)
在不同的作用域中无法重载函数名。一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
1 | string read(); |
在C++中,名字查找发生在类型检查之前。
特殊用途语言特性(Features for Specialized Uses)
默认实参(Default Arguments)
- 默认实参作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
1 | typedef string::size_type sz; |
调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
如果想使用默认实参,只要在调用函数的时候省略该实参即可。
虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
1 | // no default for the height or width parameters |
- 默认实参只能出现在函数声明和定义其中一处。通常应该在函数声明中指定默认实参,并将声明放在合适的头文件中。
1 | // 函数声明 |
局部变量不能作为函数的默认实参。
用作默认实参的名字在函数声明所在的作用域内解析,但名字的求值过程发生在函数调用时。
1 | // the declarations of wd, def, and ht must appear outside a function |
内联函数和constexpr函数(Inline and constexpr Functions)
- 内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。定义内联函数时需要在函数的返回类型前添加关键字
inline
。
1 | // inline version: find the shorter of two strings |
在函数声明和定义中都能使用关键字
inline
,但是建议只在函数定义时使用。一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和
switch
语句,否则函数会被编译为普通函数。constexpr
函数是指能用于常量表达式的函数。constexpr
函数的返回类型及所有形参的类型都得是字面值类型。另外C++11标准要求constexpr
函数体中必须有且只有一条return
语句,但是此限制在C++14标准中被删除。
1 | constexpr int new_sz() |
constexpr
函数的返回值可以不是一个常量。
1 | // scale(arg) is a constant expression if arg is 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 | Record lookup(Account&); // function that takes a reference to Account |
函数指针(Pointers to Functions)
- 要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。
1 | // compares lengths of two strings |
- 可以直接使用指向函数的指针来调用函数,无须提前解引用指针。
1 | pf = lengthCompare; // pf now points to the function named lengthCompare |
- 对于重载函数,编译器通过指针类型决定函数版本,指针类型必须与重载函数中的某一个精确匹配。
1 | void ff(int*); |
- 可以把函数的形参定义成指向函数的指针。调用时允许直接把函数名当作实参使用,它会自动转换成指针。
1 | // third parameter is a function type and is automatically treated as a pointer to function |
关键字
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 | struct Sales_data |
- 成员函数通过一个名为
this
的隐式额外参数来访问调用它的对象。this
参数是一个常量指针,被初始化为调用该函数的对象地址。在函数体内可以显式使用this
指针。
1 | total.isbn() |
默认情况下,
this
的类型是指向类类型非常量版本的常量指针。this
也遵循初始化规则,所以默认不能把this
绑定到一个常量对象上,即不能在常量对象上调用普通的成员函数。C++允许在成员函数的参数列表后面添加关键字
const
,表示this
是一个指向常量的指针。使用关键字const
的成员函数被称作常量成员函数(const member function)。
1 | // pseudo-code illustration of how the implicit this pointer is used |
常量对象和指向常量对象的引用或指针都只能调用常量成员函数。
类本身就是一个作用域,成员函数的定义嵌套在类的作用域之内。编译器处理类时,会先编译成员声明,再编译成员函数体(如果有的话),因此成员函数可以随意使用类的其他成员而无须在意这些成员的出现顺序。
在类的外部定义成员函数时,成员函数的定义必须与它的声明相匹配。如果成员函数被声明为常量成员函数,那么它的定义也必须在参数列表后面指定
const
属性。同时,类外部定义的成员名字必须包含它所属的类名。
1 | double Sales_data::avg_price() const |
- 可以定义返回
this
对象的成员函数。
1 | Sales_data& Sales_data::combine(const Sales_data &rhs) |
定义类相关的非成员函数(Defining Nonmember Class-Related Functions)
- 类的作者通常会定义一些辅助函数,尽管这些函数从概念上来说属于类接口的组成部分,但实际上它们并不属于类本身。
1 | // input transactions contain ISBN, number of copies sold, and sales price |
如果非成员函数是类接口的组成部分,则这些函数的声明应该与类放在同一个头文件中。
一般来说,执行输出任务的函数应该尽量减少对格式的控制。
构造函数(Constructors)
类通过一个或几个特殊的成员函数来控制其对象的初始化操作,这些函数被称作构造函数。只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同,没有返回类型,且不能被声明为
const
函数。构造函数在const
对象的构造过程中可以向其写值。
1 | struct Sales_data |
类通过默认构造函数(default constructor)来控制默认初始化过程,默认构造函数无须任何实参。
如果类没有显式地定义构造函数,则编译器会为类隐式地定义一个默认构造函数,该构造函数也被称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,合成的默认构造函数初始化数据成员的规则如下:
如果存在类内初始值,则用它来初始化成员。
否则默认初始化该成员。
某些类不能依赖于合成的默认构造函数。
只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。一旦类定义了其他构造函数,那么除非再显式地定义一个默认的构造函数,否则类将没有默认构造函数。
如果类包含内置类型或者复合类型的成员,则只有当这些成员全部存在类内初始值时,这个类才适合使用合成的默认构造函数。否则用户在创建类的对象时就可能得到未定义的值。
编译器不能为某些类合成默认构造函数。例如类中包含一个其他类类型的成员,且该类型没有默认构造函数,那么编译器将无法初始化该成员。
在C++11中,如果类需要默认的函数行为,可以通过在参数列表后面添加
=default
来要求编译器生成构造函数。其中=default
既可以和函数声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default
在类的内部,则默认构造函数是内联的。
1 | Sales_data() = default; |
- 构造函数初始值列表(constructor initializer list)负责为新创建对象的一个或几个数据成员赋初始值。形式是每个成员名字后面紧跟括号括起来的(或者在花括号内的)成员初始值,不同成员的初始值通过逗号分隔。
1 | Sales_data(const std::string &s): bookNo(s) { } |
- 当某个数据成员被构造函数初始值列表忽略时,它会以与合成默认构造函数相同的方式隐式初始化。
1 | // has the same behavior as the original constructor defined above |
- 构造函数不应该轻易覆盖掉类内初始值,除非新值与原值不同。如果编译器不支持类内初始值,则所有构造函数都应该显式初始化每个内置类型的成员。
拷贝、赋值和析构(Copy、Assignment,and Destruction)
编译器能合成拷贝、赋值和析构函数,但是对于某些类来说合成的版本无法正常工作。特别是当类需要分配类对象之外的资源时,合成的版本通常会失效。
访问控制与封装(Access Control and Encapsulation)
使用访问说明符(access specifier)可以加强类的封装性:
- 定义在
public
说明符之后的成员在整个程序内都可以被访问。public
成员定义类的接口。 - 定义在
private
说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。private
部分封装了类的实现细节。
1 | class Sales_data |
一个类可以包含零或多个访问说明符,每个访问说明符指定了接下来的成员的访问级别,其有效范围到出现下一个访问说明符或类的结尾处为止。
使用关键字
struct
定义类时,定义在第一个访问说明符之前的成员是public
的;而使用关键字class
时,这些成员是private
的。二者唯一的区别就是默认访问权限不同。
友元(Friends)
类可以允许其他类或函数访问它的非公有成员,方法是使用关键字friend
将其他类或函数声明为它的友元。
1 | class Sales_data |
友元声明只能出现在类定义的内部,具体位置不限。友元不是类的成员,也不受它所在区域访问级别的约束。
通常情况下,最好在类定义开始或结束前的位置集中声明友元。
封装的好处:
确保用户代码不会无意间破坏封装对象的状态。
被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
友元声明仅仅指定了访问权限,而并非一个通常意义上的函数声明。如果希望类的用户能调用某个友元函数,就必须在友元声明之外再专门对函数进行一次声明(部分编译器没有该限制)。
为了使友元对类的用户可见,通常会把友元的声明(类的外部)与类本身放在同一个头文件中。
类的其他特性(Additional Class Features)
类成员再探(Class Members Revisited)
由类定义的类型名字和其他成员一样存在访问限制,可以是public
或private
中的一种。
1 | class Screen |
与普通成员不同,用来定义类型的成员必须先定义后使用。类型成员通常位于类起始处。
定义在类内部的成员函数是自动内联的。
如果需要显式声明内联成员函数,建议只在类外部定义的位置说明
inline
。inline
成员函数该与类定义在同一个头文件中。使用关键字
mutable
可以声明可变数据成员(mutable data member)。可变数据成员永远不会是const
的,即使它在const
对象内。因此const
成员函数可以修改可变成员的值。
1 | class Screen |
- 提供类内初始值时,必须使用
=
或花括号形式。
返回this的成员函数(Functions That Return this)
const
成员函数如果以引用形式返回*this
,则返回类型是常量引用。通过区分成员函数是否为
const
的,可以对其进行重载。在常量对象上只能调用const
版本的函数;在非常量对象上,尽管两个版本都能调用,但会选择非常量版本。
1 | class Screen |
类类型(Class Types)
每个类定义了唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型。
可以仅仅声明一个类而暂时不定义它。这种声明被称作前向声明(forward declaration),用于引入类的名字。在类声明之后定义之前都是一个不完全类型(incomplete type)。
1 | class Screen; // declaration of the Screen class |
可以定义指向不完全类型的指针或引用,也可以声明(不能定义)以不完全类型作为参数或返回类型的函数。
只有当类全部完成后才算被定义,所以一个类的成员类型不能是该类本身。但是一旦类的名字出现,就可以被认为是声明过了,因此类可以包含指向它自身类型的引用或指针。
1 | class Link_screen |
友元再探(Friendship Revisited)
- 除了普通函数,类还可以把其他类或其他类的成员函数声明为友元。友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
1 | class Screen |
- 友元函数可以直接定义在类的内部,这种函数是隐式内联的。但是必须在类外部提供相应声明令函数可见。
1 | struct X |
友元关系不存在传递性。
把其他类的成员函数声明为友元时,必须明确指定该函数所属的类名。
1 | class Screen |
- 如果类想把一组重载函数声明为友元,需要对这组函数中的每一个分别声明。
类的作用域(Class Scope)
- 当成员函数定义在类外时,返回类型中使用的名字位于类的作用域之外,此时返回类型必须指明它是哪个类的成员。
1 | class Window_mgr |
名字查找与作用域(Name Lookup and Class Scope)
成员函数体直到整个类可见后才会被处理,因此它能使用类中定义的任何名字。
声明中使用的名字,包括返回类型或参数列表,都必须确保使用前可见。
如果类的成员使用了外层作用域的某个名字,而该名字表示一种类型,则类不能在之后重新定义该名字。
1 | typedef double Money; |
类型名定义通常出现在类起始处,这样能确保所有使用该类型的成员都位于类型名定义之后。
成员函数中名字的解析顺序:
在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才会被考虑。
如果在成员函数内没有找到,则会在类内继续查找,这时会考虑类的所有成员。
如果类内也没有找到,会在成员函数定义之前的作用域查找。
1 | // it is generally a bad idea to use the same name for a parameter and a member |
- 可以通过作用域运算符
::
或显式this
指针来强制访问被隐藏的类成员。
1 | // bad practice: names local to member functions shouldn't hide member names |
构造函数再探(Constructors Revisited)
构造函数初始值列表(Constructor Initializer List)
如果没有在构造函数初始值列表中显式初始化成员,该成员会在构造函数体之前执行默认初始化。
如果成员是
const
、引用,或者是某种未定义默认构造函数的类类型,必须在初始值列表中将其初始化。
1 | class ConstRef |
最好令构造函数初始值的顺序与成员声明的顺序一致,并且尽量避免使用某些成员初始化其他成员。
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数(Delegating Constructors)
C++11扩展了构造函数初始值功能,可以定义委托构造函数。委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。
1 | class Sales_data |
默认构造函数的作用(The Role of the Default Constructor)
当对象被默认初始化或值初始化时会自动执行默认构造函数。
默认初始化的发生情况:
- 在块作用域内不使用初始值定义非静态变量或数组。
- 类本身含有类类型的成员且使用合成默认构造函数。
- 类类型的成员没有在构造函数初始值列表中显式初始化。
值初始化的发生情况:
- 数组初始化时提供的初始值数量少于数组大小。
- 不使用初始值定义局部静态变量。
- 通过
T()
形式(T为类型)的表达式显式地请求值初始化。
类必须包含一个默认构造函数。
如果想定义一个使用默认构造函数进行初始化的对象,应该去掉对象名后的空括号对。
1 | Sales_data obj(); // oops! declares a function, not an object |
隐式的类类型转换(Implicit Class-Type Conversions)
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数被称为转换构造函数(converting constructor)。
1 | string null_book = "9-999-99999-9"; |
- 编译器只会自动执行一步类型转换。
1 | // error: requires two user-defined conversions: |
- 在要求隐式转换的程序上下文中,可以通过将构造函数声明为
explicit
的加以阻止。
1 | class Sales_data |
explicit
关键字只对接受一个实参的构造函数有效。只能在类内声明构造函数时使用
explicit
关键字,在类外定义时不能重复。执行拷贝初始化时(使用
=
)会发生隐式转换,所以explicit
构造函数只能用于直接初始化。
1 | Sales_data item1 (null_book); // ok: direct initialization |
- 可以使用
explicit
构造函数显式地强制转换类型。
1 | // ok: the argument is an explicitly constructed Sales_data object |
聚合类(Aggregate Classes)
聚合类满足如下条件:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类。
- 没有虚函数。
1 | struct Data |
可以使用一个用花括号包围的成员初始值列表初始化聚合类的数据成员。初始值顺序必须与声明顺序一致。如果初始值列表中的元素个数少于类的成员个数,则靠后的成员被值初始化。
1 | // val1.ival = 0; val1.s = string("Anna") |
字面值常量类(Literal Classes)
数据成员都是字面值类型的聚合类是字面值常量类。或者一个类不是聚合类,但符合下列条件,则也是字面值常量类:
- 数据成员都是字面值类型。
- 类至少含有一个
constexpr
构造函数。 - 如果数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式。如果成员属于类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义。
constexpr
构造函数用于生成constexpr
对象以及constexpr
函数的参数或返回类型。
constexpr
构造函数必须初始化所有数据成员,初始值使用constexpr
构造函数或常量表达式。
类的静态成员(static Class Members)
- 使用关键字
static
可以声明类的静态成员。静态成员存在于任何对象之外,对象中不包含与静态成员相关的数据。
1 | class Account |
由于静态成员不与任何对象绑定,因此静态成员函数不能声明为
const
的,也不能在静态成员函数内使用this
指针。用户代码可以使用作用域运算符访问静态成员,也可以通过类对象、引用或指针访问。类的成员函数可以直接访问静态成员。
1 | double r; |
在类外部定义静态成员时,不能重复
static
关键字,其只能用于类内部的声明语句。由于静态数据成员不属于类的任何一个对象,因此它们并不是在创建类对象时被定义的。通常情况下,不应该在类内部初始化静态成员。而必须在类外部定义并初始化每个静态成员。一个静态成员只能被定义一次。一旦它被定义,就会一直存在于程序的整个生命周期中。
1 | // define and initialize a static class member |
建议把静态数据成员的定义与其他非内联函数的定义放在同一个源文件中,这样可以确保对象只被定义一次。
尽管在通常情况下,不应该在类内部初始化静态成员。但是可以为静态成员提供
const
整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr
。初始值必须是常量表达式。
1 | class Account |
- 静态数据成员的类型可以是它所属的类类型。
1 | class Bar |
- 可以使用静态成员作为函数的默认实参。
1 | class Screen |
第八章 IO
部分IO库设施:
istream
:输入流类型,提供输入操作。ostream
:输出流类型,提供输出操作。cin
:istream
对象,从标准输入读取数据。cout
:ostream
对象,向标准输出写入数据。cerr
:ostream
对象,向标准错误写入数据。>>
运算符:从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,wistringstream 从string 读取数据;ostringstream,wostringstream 向string 写入数据;stringstream,wstringstream 读写string |
宽字符版本的IO类型和函数的名字以
w
开始,如wcin
、wcout
和wcerr
分别对应cin
、cout
和cerr
。它们与其对应的普通char
版本都定义在同一个头文件中,如头文件fstream定义了ifstream
和wifstream
类型。可以将派生类的对象当作其基类的对象使用。
IO象无拷贝或赋值(No Copy or Assign for IO Objects)
不能拷贝或对IO对象赋值。
1 | ofstream out1, out2; |
由于IO对象不能拷贝,因此不能将函数形参或返回类型定义为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const
的。
条件状态(Condition States)
IO库条件状态:
状态 | 解释 |
---|---|
strm:iostate |
是一种机器无关的类型,提供了表达条件状态的完整功能 |
strm:badbit |
用来指出流已经崩溃 |
strm:failbit |
用来指出一个IO操作失败了 |
strm:eofbit |
用来指出流到达了文件结束 |
strm:goodbit |
用来指出流未处于错误状态,此值保证为零 |
s.eof() |
若流s 的eofbit 置位,则返回true |
s.fail() |
若流s 的failbit 置位,则返回true |
s.bad() |
若流s 的badbit 置位,则返回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
会被置位,如期望读取数值却读出一个字符。 - 如果到达文件结束位置,
eofbit
和failbit
都会被置位。如果流未发生错误,则goodbit
的值为0。 - 如果
badbit
、failbit
和eofbit
任何一个被置位,检测流状态的条件都会失败。
1 | while (cin >> word) |
good
函数在所有错误均未置位时返回true
。- 而
bad
、fail
和eof
函数在对应错误位被置位时返回true
。 - 此外,在
badbit
被置位时,fail
函数也会返回true
。 - 因此应该使用
good
或fail
函数确定流的总体状态,eof
和bad
只能检测特定错误。
- 流对象的
rdstate
成员返回一个iostate
值,表示流的当前状态。 setstate
成员用于将指定条件置位(叠加原始流状态)。clear
成员的无参版本清除所有错误标志;含参版本接受一个iostate
值,用于设置流的新状态(覆盖原始流状态)。
1 | // remember the current state of cin |
管理输出缓冲(Managing the Output Buffer)
每个输出流都管理一个缓冲区,用于保存程序读写的数据。导致缓冲刷新(即数据真正写入输出设备或文件)的原因有很多:
- 程序正常结束。
- 缓冲区已满。
- 使用操纵符(如
endl
)显式刷新缓冲区。 - 在每个输出操作之后,可以用
unitbuf
操纵符设置流的内部状态,从而清空缓冲区。默认情况下,对cerr
是设置unitbuf
的,因此写到cerr
的内容都是立即刷新的。 - 一个输出流可以被关联到另一个流。这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。默认情况下,
cin
和cerr
都关联到cout
,因此,读cin
或写cerr
都会刷新cout
的缓冲区。
flush
操纵符刷新缓冲区,但不输出任何额外字符。ends
向缓冲区插入一个空字符,然后刷新缓冲区。
1 | cout << "hi!" << endl; // writes hi and a newline, then flushes the buffer |
- 如果想在每次输出操作后都刷新缓冲区,可以使用
unitbuf
操纵符。它令流在接下来的每次写操作后都进行一次flush
操作。而nounitbuf
操纵符则使流恢复使用正常的缓冲区刷新机制。
1 | cout << unitbuf; // all writes will be flushed immediately |
如果程序异常终止,输出缓冲区不会被刷新。
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将
cout
和cin
关联在一起,因此下面的语句会导致cout
的缓冲区被刷新:
1 | cin >> ival; |
交互式系统通常应该关联输入流和输出流。这意味着包括用户提示信息在内的所有输出,都会在读操作之前被打印出来。
使用
tie
函数可以关联两个流。它有两个重载版本:无参版本返回指向输出流的指针。如果本对象已关联到一个输出流,则返回的就是指向这个流的指针,否则返回空指针。tie
的第二个版本接受一个指向ostream
的指针,将本对象关联到此ostream
。
1 | cin.tie(&cout); // illustration only: the library ties cin and cout for us |
- 每个流同时最多关联一个流,但多个流可以同时关联同一个
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 | ifstream in(ifile); // construct an ifstream and open the given 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操作。 |
- 只能对
ofstream
或fstream
对象设定out
模式。 - 只能对
ifstream
或fstream
对象设定in
模式。 - 只有当
out
被设定时才能设定trunc
模式。 - 只要
trunc
没被设定,就能设定app
模式。在app
模式下,即使没有设定out
模式,文件也是以输出方式打开。 - 默认情况下,即使没有设定
trunc
,以out
模式打开的文件也会被截断。如果想保留以out
模式打开的文件内容,就必须同时设定app
模式,这会将数据追加写到文件末尾;或者同时设定in
模式,即同时进行读写操作。 ate
和binary
模式可用于任何类型的文件流对象,并可以和其他任何模式组合使用。- 与
ifstream
对象关联的文件默认以in
模式打开,与ofstream
对象关联的文件默认以out
模式打开,与fstream
对象关联的文件默认以in
和out
模式打开。
默认情况下,打开
ofstream
对象时,文件内容会被丢弃,阻止文件清空的方法是同时指定app
或in
模式。流对象每次打开文件时都可以改变其文件模式。
1 | ofstream out; // no file mode is set |
string流(string Streams)
头文件sstream定义了三个类型来支持内存IO:istringstream
从string
读取数据,ostringstream
向string
写入数据,stringstream
可以同时读写string
的数据。
操作 | 解释 |
---|---|
sstream strm |
定义一个未绑定的stringstream 对象 |
sstream strm(s) |
用s 初始化对象 |
strm.str() |
返回strm 所保存的string 的拷贝 |
strm.str(s) |
将s 拷贝到strm 中,返回void |
使用istringstream(Using an istringstream)
1 | // members are public by default |
使用ostringstream(Using ostringstreams)
1 | for (const auto &entry : people) |
第九章 顺序容器
顺序容器概述(Overview of the Sequential Containers)
顺序容器类型:
类型 | 特性 |
---|---|
vector |
可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素可能很慢 |
deque |
双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 |
list |
双向链表。只支持双向顺序访问。在任何位置插入/删除速度都很快 |
forward_list |
单向链表。只支持单向顺序访问。在任何位置插入/删除速度都很快 |
array |
固定大小数组。支持快速随机访问。不能添加/删除元素 |
string |
类似vector ,但用于保存字符。支持快速随机访问。在尾部插入/删除速度很快 |
forward_list
和array
是C++11新增类型。与内置数组相比,array
更安全易用。forward_list
没有size
操作。
容器选择原则:
- 除非有合适的理由选择其他容器,否则应该使用
vector
。 - 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用
list
或forward_list
。 - 如果程序要求随机访问容器元素,则应该使用
vector
或deque
。 - 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用
deque
。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
vector
追加数据,再调用标准库的sort
函数重排元素,从而避免在中间位置添加元素。 - 如果必须在中间位置插入元素,可以在输入阶段使用
list
。输入完成后将list
中的内容拷贝到vector
中。
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
- 不确定应该使用哪种容器时,可以先只使用
vector
和list
的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vector
或list
都很方便。
容器库概览(Container Library Overview)
每个容器都定义在一个头文件中,文件名与类型名相同。容器均为模板类型。
迭代器(Iterators)
操作 | 解释 |
---|---|
iterator |
此容器类型的迭代器类型 |
const_iterator |
可以读取元素但不能修改元素的迭代器类型 |
size_type |
无符号整数类型,足够保存此种容器类型最大可能的大小 |
difference_type |
带符号整数类型,足够保存两个迭代器之间的距离 |
value_type |
元素类型 |
reference |
元素的左值类型;和value_type & 含义相同 |
const_reference |
元素的const 左值类型,即const value_type & |
forward_list
类型不支持递减运算符--
。一个迭代器范围(iterator range)由一对迭代器表示。这两个迭代器通常被称为
begin
和end
,分别指向同一个容器中的元素或尾后地址。end
迭代器不会指向范围中的最后一个元素,而是指向尾元素之后的位置。这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为[begin,end)
。迭代器begin
和end
必须指向相同的容器,end
可以与begin
指向相同的位置,但不能指向begin
之前的位置(由程序员确保)。
假定begin
和end
构成一个合法的迭代器范围,则:
- 如果
begin
等于end
,则范围为空。 - 如果
begin
不等于end
,则范围内至少包含一个元素,且begin
指向该范围内的第一个元素。 - 可以递增
begin
若干次,令begin
等于end
。
1 | while (begin != end) |
容器类型成员(Container Type Members)
通过类型别名,可以在不了解容器元素类型的情况下使用元素。如果需要元素类型,可以使用容器的value_type
。如果需要元素类型的引用,可以使用reference
或const_reference
。
begin和end成员(begin and end Members)
begin
和end
操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围。begin
和end
操作有多个版本:带r
的版本返回反向迭代器。以c
开头的版本(C++11新增)返回const
迭代器。不以c
开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对const
对象调用时,返回const
迭代器。
1 | list<string> a = {"Milton", "Shakespeare", "Austen"}; |
当
auto
与begin
或end
结合使用时,返回的迭代器类型依赖于容器类型。但调用以c
开头的版本仍然可以获得const
迭代器,与容器是否是常量无关。当程序不需要写操作时,应该使用
cbegin
和cend
。
容器定义和初始化(Defining and Initializing a Container)
容器定义和初始化方式:
操作 | 解释 |
---|---|
C c; |
默认构造函数,构造空容器 |
C c1(c2); 或C c1 = c2; |
构造c2 的拷贝c1 |
C c(b, e) |
构造c ,将迭代器b 和e 指定范围内的所有元素拷贝到c |
C c(a, b, c...) |
列表初始化c |
C c(n) |
只支持顺序容器,且不包括array ,包含n 个元素,这些元素进行了值初始化 |
C c(n, t) |
包含n 个初始值为t 的元素 |
将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。
传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。
1 | // each container has three elements, initialized from the given initializers |
- C++11允许对容器进行列表初始化。
1 | // each container has three elements, initialized from the given initializers |
- 定义和使用
array
类型时,需要同时指定元素类型和容器大小。
1 | array<int, 42> // type is: array that holds 42 ints |
对
array
进行列表初始化时,初始值的数量不能大于array
的大小。如果初始值的数量小于array
的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。可以对
array
进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。
赋值和swap(Assignment and swap)
容器赋值操作:
操作 | 解释 |
---|---|
c1 = c2; |
将c1 中的元素替换成c2 中的元素 |
c1 = {a, b, c...} |
将c1 中的元素替换成列表中的元素(不适用于array ) |
c1.swap(c2) |
交换c1 和c2 的元素 |
swap(c1, c2) |
等价于c1.swap(c2) |
c.assign(b, e) |
将c 中的元素替换成迭代器b 和e 表示范围中的元素,b 和e 不能指向c 中的元素 |
c.assign(il) |
将c 中的元素替换成初始化列表il 中的元素 |
c.assign(n, r) |
将c 中的元素替换为n 个值是t 的元素 |
- 赋值运算符两侧的运算对象必须类型相同。
assign
允许用不同但相容的类型赋值,或者用容器的子序列赋值。
1 | list<string> names; |
由于其旧元素被替换,因此传递给
assign
的迭代器不能指向调用assign
的容器本身。swap
交换两个相同类型容器的内容。除array
外,swap
不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。
1 | vector<string> svec1(10); // vector with ten elements |
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而
swap
操作交换容器内容,不会导致迭代器、引用和指针失效(array
和string
除外)。对于
array
,swap
会真正交换它们的元素。因此在swap
操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。
1 | array<int, 3> a = { 1, 2, 3 }; |
- 对于其他容器类型(除
string
),指针、引用和迭代器在swap
操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。
1 | vector<int> a = { 1, 2, 3 }; |
array
不支持assign
,也不允许用花括号列表进行赋值。
1 | array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9}; |
- 新标准库同时提供了成员和非成员函数版本的
swap
。非成员版本的swap
在泛型编程中非常重要,建议统一使用非成员版本的swap
。
容器大小操作(Container Size Operations)
操作 | 解释 |
---|---|
c.size() |
c 中元素的数目(不支持forward_list ) |
c.max_size() |
c 中可保存的最大元素数目 |
c.empty() |
若c 中存储了元素,返回false ,否则返回true |
size
成员返回容器中元素的数量;empty
当size
为0时返回true
,否则返回false
;max_size
返回一个大于或等于该类型容器所能容纳的最大元素数量的值。forward_list
支持max_size
和empty
,但不支持size
。
关系运算符(Relational Operators)
每个容器类型都支持相等运算符(==
、!=
)。除无序关联容器外,其他容器都支持关系运算符(>
、>=
、<
、<=
)。关系运算符两侧的容器类型和保存元素类型都必须相同。
两个容器的比较实际上是元素的逐对比较,其工作方式与string
的关系运算符类似:
- 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。
- 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
- 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。
1 | vector<int> v1 = { 1, 3, 5, 7, 9, 12 }; |
- 容器的相等运算符实际上是使用元素的
==
运算符实现的,而其他关系运算符则是使用元素的<
运算符。如果元素类型不支持所需运算符,则保存该元素的容器就不能使用相应的关系运算。
顺序容器操作(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) |
将迭代器b 和e 范围内的元素,插入到p 指向的元素之前;如果范围为空,则返回p |
c.insert(p, il) |
il 是一个花括号包围中的元素值列表,将其插入到p 指向的元素之前;如果il 是空,则返回p |
push_back
将一个元素追加到容器尾部,push_front
将元素插入容器头部。
1 | // read from standard input, putting each word onto the end of container |
insert
将元素插入到迭代器指定的位置之前。一些不支持push_front
的容器可以使用insert
将元素插入开始位置。
1 | vector<string> svec; |
将元素插入到
vector
、deque
或string
的任何位置都是合法的,但可能会很耗时。在新标准库中,接受元素个数或范围的
insert
版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回void
。如果范围为空,不插入任何元素,insert
会返回第一个参数。
1 | list<string> 1st; |
- 新标准库增加了三个直接构造而不是拷贝元素的操作:
emplace_front
、emplace_back
和emplace
,其分别对应push_front
、push_back
和insert
。当调用push
或insert
时,元素对象被拷贝到容器中。而调用emplace
时,则是将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素。
1 | // construct a Sales_data object at the end of c |
传递给
emplace
的参数必须与元素类型的构造函数相匹配。forward_list
有特殊版本的insert
和emplace
操作,且不支持push_back
和emplace_back
。vector
和string
不支持push_front
和emplace_front
。
访问元素(Accessing Elements)
每个顺序容器都有一个
front
成员函数,而除了forward_list
之外的顺序容器还有一个back
成员函数。这两个操作分别返回首元素和尾元素的引用。在调用
front
和back
之前,要确保容器非空。
顺序容器的元素访问操作:
操作 | 解释 |
---|---|
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
引用,否则返回普通引用。可以快速随机访问的容器(
string
、vector
、deque
和array
)都提供下标运算符。保证下标有效是程序员的责任。如果希望确保下标合法,可以使用at
成员函数。at
类似下标运算,但如果下标越界,at
会抛出out_of_range
异常。
1 | vector<string> svec; // empty vector |
删除元素(Erasing Elements)
顺序容器的元素删除操作:
操作 | 解释 |
---|---|
c.pop_back() |
删除c 中尾元素,若c 为空,则函数行为未定义。函数返回void |
c.pop_front() |
删除c 中首元素,若c 为空,则函数行为未定义。函数返回void |
c.erase(p) |
删除迭代器p 指向的元素,返回一个指向被删除元素之后的元素的迭代器,若p 本身是尾后迭代器,则函数行为未定义 |
c.erase(b, e) |
删除迭代器b 和e 范围内的元素,返回指向最后一个被删元素之后元素的迭代器,若e 本身就是尾后迭代器,则返回尾后迭代器 |
c.clear() |
删除c 中所有元素,返回void |
删除
deque
中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。删除vector
或string
的元素后,指向删除点之后位置的迭代器、引用和指针也都会失效。删除元素前,程序员必须确保目标元素存在。
pop_front
和pop_back
函数分别删除首元素和尾元素。vector
和string
类型不支持pop_front
,forward_list
类型不支持pop_back
。erase
函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase
都返回指向删除元素(最后一个)之后位置的迭代器。
1 | // delete the range of elements between two iterators |
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 之后插入元素。由迭代器b 和e 指定范围。 |
lst.insert_after(p, il) |
在迭代器p 之后插入元素。由il 指定初始化列表。 |
emplace_after(p, args) |
使用args 在p 之后的位置,创建一个元素,返回一个指向这个新元素的迭代器。若p 为尾后迭代器,则函数行为未定义。 |
lst.erase_after(p) |
删除p 指向位置之后的元素,返回一个指向被删元素之后的元素的迭代器,若p 指向lst 的尾元素或者是一个尾后迭代器,则函数行为未定义。 |
lst.erase_after(b, e) |
类似上面,删除对象换成从b 到e 指定的范围。 |
改变容器大小(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)
向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。
- 向容器中添加元素后:
- 如果容器是
vector
或string
类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。 - 如果容器是
deque
类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。 - 如果容器是
list
或forward_list
类型,指向容器的迭代器、指针和引用仍然有效。
- 如果容器是
- 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效:
- 如果容器是
list
或forward_list
类型,指向容器其他位置的迭代器、指针和引用仍然有效。 - 如果容器是
deque
类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。 - 如果容器是
vector
或string
类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。
- 如果容器是
必须保证在每次改变容器后都正确地重新定位迭代器。
不要保存
end
函数返回的迭代器。
1 | // safer: recalculate end on each trip whenever the loop adds/erases elements |
获取迭代器:
操作 | 解释 |
---|---|
c.begin() , c.end() |
返回指向c 的首元素和尾元素之后位置的迭代器 |
c.cbegin() , c.cend() |
返回const_iterator |
- 以
c
开头的版本是C++11新标准引入的 - 当不需要写访问时,应该使用
cbegin
和cend
。
反向容器的额外成员
操作 | 解释 |
---|---|
reverse_iterator |
按逆序寻址元素的迭代器 |
const_reverse_iterator |
不能修改元素的逆序迭代器 |
c.rbegin() , c.rend() |
返回指向c 的尾元素和首元素之前位置的迭代器 |
c.crbegin() , c.crend() |
返回const_reverse_iterator |
- 不支持
forward_list
vector对象是如何增长的(How a vector Grows)
vector
和string
的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。
容器大小管理操作:
操作 | 解释 |
---|---|
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
函数来要求deque
、vector
和string
退回不需要的内存空间(并不保证退回)。
额外的string操作(Additional string Operations)
构造string的其他方法(Other Ways to Construct strings)
构造string
的其他方法:
操作 | 解释 |
---|---|
string s(cp, n) |
s 是cp 指向的数组中前n 个字符的拷贝,此数组 |
string s(s2, pos2) |
s 是string s2 从下标pos2 开始的字符的拷贝。若pos2 > s2.size() ,则构造函数的行为未定义。 |
string s(s2, pos2, len2) |
s 是string 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 | string s("C++ Primer"), s2 = s; // initialize s and s2 to "C++ Primer" |
replace
函数是调用erase
和insert
函数的简写形式。
1 | // equivalent way to replace "4th" by "5th" |
string搜索操作(string Search Operations)
string
的每个搜索操作都返回一个string::size_type
值,表示匹配位置的下标。如果搜索失败,则返回一个名为string::npos
的static
成员。标准库将npos
定义为const string::size_type
类型,并初始化为-1。不建议用
int
或其他带符号类型来保存string
搜索函数的返回值。
string
搜索操作:
搜索操作 | 解释 |
---|---|
s.find(args) |
查找s 中args 第一次出现的位置 |
s.rfind(args) |
查找s 中args 最后一次出现的位置 |
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 开始查找字符c 。pos 默认是0 |
s2, pos |
从s 中位置pos 开始查找字符串s 。pos 默认是0 |
cp, pos |
从s 中位置pos 开始查找指针cp 指向的以空字符结尾的C风格字符串。pos 默认是0 |
cp, pos, n |
从s 中位置pos 开始查找指针cp 指向的前n 个字符。pos 和n 无默认值。 |
compare函数(The compare Functions)
string
类型提供了一组compare
函数进行字符串比较操作,类似C标准库的strcmp
函数。
compare
函数的几种参数形式:
参数形式 | 解释 |
---|---|
s2 |
比较s 和s2 |
pos1, n1, s2 |
比较s 从pos1 开始的n1 个字符和s2 |
pos1, n1, s2, pos2, n2 |
比较s 从pos1 开始的n1 个字符和s2 |
cp |
比较s 和cp 指向的以空字符结尾的字符数组 |
pos1, n1, cp |
比较s 从pos1 开始的n1 个字符和cp 指向的以空字符结尾的字符数组 |
pos1, n1, cp, n2 |
比较s 从pos1 开始的n1 个字符和cp 指向的地址开始n2 个字符 |
数值转换(Numeric Conversions)
C++11增加了string
和数值之间的转换函数:
转换 | 解释 |
---|---|
to_string(val) |
一组重载函数,返回数值val 的string 表示。val 可以使任何算术类型。对每个浮点类型和int 或更大的整型,都有相应版本的to_string() 。和往常一样,小整型会被提升。 |
stoi(s, p, b) |
返回s 起始子串(表示整数内容)的数值,p 是s 中第一个非数值字符的下标,默认是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 起始子串(表示浮点数内容)的数值,p 是s 中第一个非数值字符的下标,默认是0。返回float |
stod(s, p) |
返回double |
stold(s, p) |
返回long double |
进行数值转换时,
string
参数的第一个非空白字符必须是符号(+
或-
)或数字。它可以以0x
或0X
开头来表示十六进制数。对于转换目标是浮点值的函数,string
参数也可以以小数点开头,并可以包含e
或E
来表示指数部分。如果给定的
string
不能转换为一个数值,则转换函数会抛出invalid_argument
异常。如果转换得到的数值无法用任何类型表示,则抛出out_of_range
异常。
容器适配器(Container Adaptors)
标准库定义了stack
、queue
和priority_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) |
交换a 和b 的内容,a 和b 必须有相同类型,包括底层容器类型也必须相同 |
a.swap(b) |
同上 |
- 默认情况下,
stack
和queue
是基于deque
实现的,priority_queue
是基于vector
实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
1 | // empty stack implemented on top of vector |
- 所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在
array
上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用forward_list
构造适配器。
栈适配器stack
定义在头文件stack中,其支持的操作如下:
操作 | 解释 |
---|---|
s.pop() |
删除栈顶元素,不返回。 |
s.push(item) |
创建一个新元素,压入栈顶,该元素通过拷贝或移动item 而来 |
s.emplace(args) |
同上,但元素由args 来构造。 |
s.top() |
返回栈顶元素,不删除。 |
队列适配器queue
和priority_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 | int val = 42; // value we'll look for |
迭代器参数令算法不依赖于特定容器,但依赖于元素类型操作。
泛型算法本身不会执行容器操作,它们只会运行于迭代器之上,执行迭代器操作。算法可能改变容器中元素的值,或者在容器内移动元素,但不会改变底层容器的大小(当算法操作插入迭代器时,迭代器可以向容器中添加元素,但算法自身不会进行这种操作)。
初识泛型算法(A First Look at the Algorithms)
只读算法(Read-Only Algorithms)
accumulate
函数(定义在头文件numeric中)用于计算一个序列的和。它接受三个参数,前两个参数指定需要求和的元素范围,第三个参数是和的初值(决定加法运算类型和返回值类型)。
1 | // sum the elements in vec starting the summation with the value 0 |
建议在只读算法中使用
cbegin
和cend
函数。equal
函数用于确定两个序列是否保存相同的值。它接受三个迭代器参数,前两个参数指定第一个序列范围,第三个参数指定第二个序列的首元素。equal
函数假定第二个序列至少与第一个序列一样长。
1 | // roster2 should have at least as many elements as roster1 |
- 只接受单一迭代器表示第二个操作序列的算法都假定第二个序列至少与第一个序列一样长。
写容器元素的算法(Algorithms That Write Container Elements)
fill
函数接受两个迭代器参数表示序列范围,还接受一个值作为第三个参数,它将给定值赋予范围内的每个元素。
1 | // reset each element to 0 |
fill_n
函数接受单个迭代器参数、一个计数值和一个值,它将给定值赋予迭代器指向位置开始的指定个元素。
1 | // reset all the elements of vec to 0 |
向目的位置迭代器写入数据的算法都假定目的位置足够大,能容纳要写入的元素。
插入迭代器(insert iterator)是一种向容器内添加元素的迭代器。通过插入迭代器赋值时,一个与赋值号右侧值相等的元素会被添加到容器中。
back_inserter
函数(定义在头文件iterator中)接受一个指向容器的引用,返回与该容器绑定的插入迭代器。通过此迭代器赋值时,赋值运算符会调用push_back
将一个具有给定值的元素添加到容器中。
1 | vector<int> vec; // empty vector |
copy
函数接受三个迭代器参数,前两个参数指定输入序列,第三个参数指定目的序列的起始位置。它将输入序列中的元素拷贝到目的序列中,返回目的位置迭代器(递增后)的值。
1 | int a1[] = { 0,1,2,3,4,5,6,7,8,9 }; |
replace
函数接受四个参数,前两个迭代器参数指定输入序列,后两个参数指定要搜索的值和替换值。它将序列中所有等于第一个值的元素都替换为第二个值。
1 | // replace any element with the value 0 with 42 |
- 相对于
replace
,replace_copy
函数可以保留原序列不变。它接受第三个迭代器参数,指定调整后序列的保存位置。
1 | // use back_inserter to grow destination as needed |
- 很多算法都提供“copy”版本,这些版本不会将新元素放回输入序列,而是创建一个新序列保存结果。
重排容器元素的算法(Algorithms That Reorder Container Elements)
sort
函数接受两个迭代器参数,指定排序范围。它利用元素类型的<
运算符重新排列元素。
1 | void elimDups(vector<string> &words) |
unique
函数重排输入序列,消除相邻的重复项,返回指向不重复值范围末尾的迭代器。
定制操作(Customizing Operations)
默认情况下,很多比较算法使用元素类型的<
或==
运算符完成操作。可以为这些算法提供自定义操作来代替默认运算符。
向算法传递函数(Passing a Function to an Algorithm)
谓词(predicate)是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为一元谓词(unary predicate,接受一个参数)和二元谓词(binary predicate,接受两个参数)。接受谓词参数的算法会对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。
1 | // comparison function to be used to sort by word length |
- 稳定排序函数
stable_sort
可以维持输入序列中相等元素的原有顺序。
lambda表达式(Lambda Expressions)
find_if
函数接受两个迭代器参数和一个谓词参数。迭代器参数用于指定序列范围,之后对序列中的每个元素调用给定谓词,并返回第一个使谓词返回非0值的元素。如果不存在,则返回尾迭代器。对于一个对象或表达式,如果可以对其使用调用运算符
()
,则称它为可调用对象(callable object)。可以向算法传递任何类别的可调用对象。一个
lambda
表达式表示一个可调用的代码单元,类似未命名的内联函数,但可以定义在函数内部。其形式如下:
1 | [capture list] (parameter list) -> return type { function body } |
其中,capture list(捕获列表)是一个由
lambda
所在函数定义的局部变量的列表(通常为空)。return type、parameter list和function body与普通函数一样,分别表示返回类型、参数列表和函数体。但与普通函数不同,lambda
必须使用尾置返回类型,且不能有默认实参。定义
lambda
时可以省略参数列表和返回类型,但必须包含捕获列表和函数体。省略参数列表等价于指定空参数列表。省略返回类型时,若函数体只是一个return
语句,则返回类型由返回表达式的类型推断而来。否则返回类型为void
。
1 | auto f = [] { return 42; }; |
lambda
可以使用其所在函数的局部变量,但必须先将其包含在捕获列表中。捕获列表只能用于局部非static
变量,lambda
可以直接使用局部static
变量和其所在函数之外声明的名字。
1 | // get an iterator to the first element whose size() is >= sz |
for_each
函数接受一个输入序列和一个可调用对象,它对输入序列中的每个元素调用此对象。
1 | // print words of the given size or longer, each one followed by a space |
lambda捕获和返回(Lambda Captures and Returns)
- 被
lambda
捕获的变量的值是在lambda
创建时拷贝,而不是调用时拷贝。在lambda
创建后修改局部变量不会影响lambda
内对应的值。
1 | size_t v1 = 42; // local variable |
lambda
可以以引用方式捕获变量,但必须保证lambda
执行时变量存在。
1 | size_t v1 = 42; // local variable |
可以让编译器根据
lambda
代码隐式捕获函数变量,方法是在捕获列表中写一个&
或=
符号。&
为引用捕获,=
为值捕获。可以混合使用显式捕获和隐式捕获。混合使用时,捕获列表中的第一个元素必须是
&
或=
符号,用于指定默认捕获方式。显式捕获的变量必须使用与隐式捕获不同的方式。
1 | // os implicitly captured by reference; c explicitly captured by value |
lambda
捕获列表形式:
捕获列表 | 解释 |
---|---|
[] |
空捕获列表。lambda 不能使用所在函数中的变量。一个lambda 只有在捕获变量后才能使用它们。 |
[names] |
names 是一个逗号分隔的名字列表,这些名字都是在lambda 所在函数的局部变量,捕获列表中的变量都被拷贝,名字前如果使用了& ,则采用引用捕获方式。 |
[&] |
隐式捕获列表,采用引用捕获方式。lambda 体中所使用的来自所在函数的实体都采用引用方式使用。 |
[=] |
隐式捕获列表,采用值捕获方式。 |
[&, identifier_list] |
identifier_list 是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list 中的名字前面不能使用& |
[=, identifier_list] |
identifier_list 中的变量采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list 中的名字不能包括this ,且前面必须使用& |
- 默认情况下,对于值方式捕获的变量,
lambda
不能修改其值。如果希望修改,就必须在参数列表后添加关键字mutable
。
1 | size_t v1 = 42; // local variable |
对于引用方式捕获的变量,
lambda
是否可以修改依赖于此引用指向的是否是const
类型。transform
函数接受三个迭代器参数和一个可调用对象。前两个迭代器参数指定输入序列,第三个迭代器参数表示目的位置。它对输入序列中的每个元素调用可调用对象,并将结果写入目的位置。
1 | transform(vi.begin(), vi.end(), vi.begin(), |
- 为
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表示生成的可调用对象中参数的位置:_1
为newCallable的第一个参数,_2
为newCallable的第二个参数,依次类推。这些名字都定义在命名空间placeholders中,它又定义在命名空间std中,因此使用时应该进行双重限定。
1 | using std::placeholders::_1; |
bind
函数可以调整给定可调用对象中的参数顺序。
1 | // sort on word length, shortest to longest |
默认情况下,
bind
函数的非占位符参数被拷贝到bind
返回的可调用对象中。但有些类型不支持拷贝操作。如果希望传递给
bind
一个对象而又不拷贝它,则必须使用标准库的ref
函数。ref
函数返回一个对象,包含给定的引用,此对象是可以拷贝的。cref
函数生成保存const
引用的类。
1 | ostream &print(ostream &os, const string &s, char c); |
再探迭代器(Revisiting Iterators)
除了为每种容器定义的迭代器之外,标准库还在头文件iterator中定义了另外几种迭代器。
- 插入迭代器(insert iterator):该类型迭代器被绑定到容器对象上,可用来向容器中插入元素。
- 流迭代器(stream iterator):该类型迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
- 反向迭代器(reverse iterator):该类型迭代器向后而不是向前移动。除了
forward_list
之外的标准库容器都有反向迭代器。 - 移动迭代器(move iterator):该类型迭代器用来移动容器元素。
插入迭代器(Insert Iterators)
插入器是一种迭代器适配器,它接受一个容器参数,生成一个插入迭代器。通过插入迭代器赋值时,该迭代器调用容器操作向给定容器的指定位置插入一个元素。
插入迭代器操作:
操作 | 解释 |
---|---|
it=t |
在it 指定的当前位置插入值t 。假定c 是it 绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用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 | list<int> lst = { 1,2,3,4 }; |
iostream迭代器(iostream Iterators)
istream_iterator
从输入流读取数据,ostream_iterator
向输出流写入数据。这些迭代器将流当作特定类型的元素序列处理。创建流迭代器时,必须指定迭代器读写的对象类型。
istream_iterator
使用>>
来读取流,因此istream_iterator
要读取的类型必须定义了>>
运算符。创建istream_iterator
时,可以将其绑定到一个流。如果默认初始化,则创建的是尾后迭代器。
1 | istream_iterator<int> int_it(cin); // reads ints from cin |
- 对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或IO错误,迭代器的值就与尾后迭代器相等。
1 | istream_iterator<int> in_iter(cin); // read ints from cin |
- 可以直接使用流迭代器构造容器。
1 | istream_iterator<int> in_iter(cin), eof; // read ints from cin |
istream_iterator
操作:
操作 | 解释 |
---|---|
istream_iterator<T> in(is); |
in 从输入流is 读取类型为T 的值 |
istream_iterator<T> end; |
读取类型是T 的值的istream_iterator 迭代器,表示尾后位置 |
in1 == in2 |
in1 和in2 必须读取相同类型。如果他们都是尾后迭代器,或绑定到相同的输入,则两者相等。 |
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 中,每个值后面都输出一个d 。d 指向一个空字符结尾的字符数组。 |
out = val |
用<< 运算符将val 写入到out 所绑定的ostream 中。val 的类型必须和out 可写的类型兼容。 |
*out, ++out, out++ |
这些运算符是存在的,但不对out 做任何事情。每个运算符都返回out 。 |
*
和++
运算符实际上不会对ostream_iterator
对象做任何操作。但是建议代码写法与其他迭代器保持一致。
1 | ostream_iterator<int> out_iter(cout, " "); |
- 可以为任何定义了
<<
运算符的类型创建istream_iterator
对象,为定义了>>
运算符的类型创建ostream_iterator
对象。
反向迭代器(Reverse Iterators)
递增反向迭代器会移动到前一个元素,递减会移动到后一个元素。
1 | sort(vec.begin(), vec.end()); // sorts vec in "normal" order |
不能从
forward_list
或流迭代器创建反向迭代器。调用反向迭代器的
base
函数可以获得其对应的普通迭代器。
1 | // find the last element in a comma-separated list |
- 反向迭代器的目的是表示元素范围,而这些范围是不对称的。用普通迭代器初始化反向迭代器,或者给反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同元素。
泛型算法结构(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 | alg(beg, end, other args); |
其中alg是算法名称,beg和end表示算法所操作的输入范围。几乎所有算法都接受一个输入范围,是否有其他参数依赖于算法操作。
dest表示输出范围,beg2和end2表示第二个输入范围。
向输出迭代器写入数据的算法都假定目标空间足够容纳要写入的数据。
接受单独一个迭代器参数表示第二个输入范围的算法都假定从迭代器参数开始的序列至少与第一个输入范围一样大。
算法命名规范(Algorithm Naming Conventions)
接受谓词参数的算法都有附加的_if
后缀。
1 | find(beg, end, val); // find the first instance of val in the input range |
将执行结果写入额外目的空间的算法都有_copy
后缀。
1 | reverse(beg, end); // reverse the elements in the input range |
一些算法同时提供_copy
和_if
版本。
特定容器算法(Container-Specific Algorithms)
对于list
和forward_list
类型,应该优先使用成员函数版本的算法,而非通用算法。
list
和forward_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 删除同一个值的连续拷贝。使用给定的二元谓词。 |
list
和forward_list
的splice
函数可以进行容器合并,其参数如下:
参数 | 解释 |
---|---|
(p, lst2) |
p 是一个指向lst 中元素的迭代器,或者一个指向flst 首前位置的迭代器。函数将lst2 中的所有元素移动到lst 中p 之前的位置或是flst 中p 之后的位置。将元素从lst2 中删除。lst2 的类型必须和lst 相同,而且不能是同一个链表。 |
(p, lst2, p2) |
同上,p2 是一个指向lst2 中位置的有效的迭代器,将p2 指向的元素移动到lst 中,或将p2 之后的元素移动到flst 中。lst2 可以是于lst 或flst 相同的链表。 |
(p, lst2, b, e) |
b 和e 表示lst2 中的合法范围。将给定范围中的元素从lst2 移动到lst 或first 中。lst2 与lst 可以使相同的链表,但p 不能指向给定范围中的元素。 |
链表特有版本的算法操作会改变底层容器。
第十一章 关联容器
关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是map
和set
。
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 ,关键字可以重复出现 |
map
和multimap
类型定义在头文件map中;set
和multiset
类型定义在头文件set中;无序容器定义在头文件unordered_map和unordered_set中。
使用关联容器(Using an Associative Container)
map
类型通常被称为关联数组(associative array)。从
map
中提取一个元素时,会得到一个pair
类型的对象。pair
是一个模板类型,保存两个名为first
和second
的公有数据成员。map
所使用的pair
用first
成员保存关键字,用second
成员保存对应的值。
1 | // count the number of times each word occurs in the input |
set
类型的find
成员返回一个迭代器。如果给定关键字在set
中,则迭代器指向该关键字,否则返回的是尾后迭代器。
关联容器概述(Overview of the Associative Containers)
定义关联容器(Defining an Associative Container)
定义
map
时,必须指定关键字类型和值类型;定义set
时,只需指定关键字类型。初始化
map
时,提供的每个键值对用花括号{}
包围。
1 | map<string, size_t> word_count; // empty |
map
和set
中的关键字必须唯一,multimap
和multiset
没有此限制。
关键字类型的要求(Requirements on Key Type)
对于有序容器——
map
、multimap
、set
和multiset
,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<
运算符来进行比较操作。用来组织容器元素的操作的类型也是该容器类型的一部分。如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型。操作类型在尖括号中紧跟着元素类型给出。
1 | bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) |
pair类型(The pair Type)
pair
定义在头文件utility中。一个pair
可以保存两个数据成员,分别命名为first
和second
。
1 | pair<string, string> anon; // holds two strings |
pair
的默认构造函数对数据成员进行值初始化。
pair
支持的操作:
操作 | 解释 |
---|---|
pair<T1, T2> p; |
p 是一个pair ,两个类型分别是T1 和T2 的成员都进行了值初始化。 |
pair<T1, T2> p(v1, v2); |
first 和second 分别用v1 和v2 进行初始化。 |
pair<T1, T2>p = {v1, v2}; |
等价于p(v1, v2) | | make_pair(v1, v2);| pair的类型从 v1和 v2的类型推断出来。 | | p.first| 返回 p的名为 first的数据成员。 | | p.second| 返回 p的名为 second的数据成员。 | | p1 relop p2| 运算关系符按字典序定义。 | | p1 == p2| 必须两对元素两两相等 | | p1 != p2` |
- 在C++11中,如果函数需要返回
pair
,可以对返回值进行列表初始化。早期C++版本中必须显式构造返回值。
1 | pair<string, int> process(vector<string> &v) |
关联容器操作(Operations on Associative Containers)
关联容器定义了类型别名来表示容器关键字和值的类型:
类型别名 | 解释 |
---|---|
key_type |
此容器类型的关键字类型 |
mapped_type |
每个关键字关联的类型,只适用于map |
value_type |
对于map ,是pair<const key_type, mapped_type> ; 对于set ,和key_type 相同。 |
对于
set
类型,key_type
和value_type
是一样的。set
中保存的值就是关键字。对于map
类型,元素是关键字-值对。即每个元素是一个pair
对象,包含一个关键字和一个关联的值。由于元素关键字不能改变,因此
pair
的关键字部分是const
的。另外,只有map
类型(unordered_map
、unordered_multimap
、multimap
、map
)才定义了mapped_type
。
1 | set<string>::value_type v1; // v1 is a string |
关联容器迭代器(Associative Container Iterators)
- 解引用关联容器迭代器时,会得到一个类型为容器的
value_type
的引用。对map
而言,value_type
是pair
类型,其first
成员保存const
的关键字,second
成员保存值。
1 | // get an iterator to an element in word_count |
- 虽然
set
同时定义了iterator
和const_iterator
类型,但两种迭代器都只允许只读访问set
中的元素。类似map
,set
中的关键字也是const
的。
1 | set<int> iset = {0,1,2,3,4,5,6,7,8,9}; |
map
和set
都支持begin
和end
操作。使用迭代器遍历map
、multimap
、set
或multiset
时,迭代器按关键字升序遍历元素。通常不对关联容器使用泛型算法。
添加元素(Adding Elements)
使用
insert
成员可以向关联容器中添加元素。向map
和set
中添加已存在的元素对容器没有影响。通常情况下,对于想要添加到
map
中的数据,并没有现成的pair
对象。可以直接在insert
的参数列表中创建pair
。
1 | // four ways to add word to word_count |
关联容器的insert
操作:
insert 操作 |
关联容器 |
---|---|
c.insert(v) c.emplace(args) |
v 是value_type 类型的对象;args 用来构造一个元素。 对于map 和set ,只有元素的关键字不存在c 中才插入或构造元素。函数返回一个pair ,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool 值。对于multimap 和multiset 则会插入范围中的每个元素。 |
c.insert(b, e) c.insert(il) |
b 和e 是迭代器,表示一个c::value_type �类型值的范围;il 是这种值的花括号列表。函数返回void 。对于 map 和set ,只插入关键字不在c 中的元素。 |
c.insert(p, v) c.emplace(p, args) |
类似insert(v) ,但将迭代器p 作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。 |
insert
或emplace
的返回值依赖于容器类型和参数:
- 对于不包含重复关键字的容器,添加单一元素的
insert
和emplace
版本返回一个pair
,表示操作是否成功。pair
的first
成员是一个迭代器,指向具有给定关键字的元素;second
成员是一个bool
值。如果关键字已在容器中,则insert
直接返回,bool
值为false
。如果关键字不存在,元素会被添加至容器中,bool
值为true
。 - 对于允许包含重复关键字的容器,添加单一元素的
insert
和emplace
版本返回指向新元素的迭代器。
删除元素(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) |
删除迭代器对b 和e 所表示范围中的元素。返回e 。 |
- 与顺序容器不同,关联容器提供了一个额外的
erase
操作。它接受一个key_type
参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量。对于不包含重复关键字的容器,erase
的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。
map的下标操作(Subscripting a map)
map
下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。由于下标运算符可能向容器中添加元素,所以只能对非
const
的map
使用下标操作。
map
和unordered_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() 。 |
- 如果
multimap
或multiset
中有多个元素具有相同关键字,则这些元素在容器中会相邻存储。
1 | multimap<string, string> authors; |
lower_bound
和upper_bound
操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,
lower_bound
返回的迭代器会指向第一个匹配给定关键字的元素,而upper_bound
返回的迭代器则指向最后一个匹配元素之后的位置。如果关键字不在
multimap
中,则lower_bound
和upper_bound
会返回相等的迭代器,指向一个不影响排序的关键字插入位置。因此用相同的关键字调用
lower_bound
和upper_bound
会得到一个迭代器范围,表示所有具有该关键字的元素范围。
1 | // definitions of authors and search_item as above |
lower_bound
和upper_bound
有可能返回尾后迭代器。如果查找的元素具有容器中最大的关键字,则upper_bound
返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound
也返回尾后迭代器。equal_range
操作接受一个关键字,返回一个迭代器pair
。若关键字存在,则第一个迭代器指向第一个匹配关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置。若关键字不存在,则两个迭代器都指向一个不影响排序的关键字插入位置。
1 | // definitions of authors and search_item as above |
无序容器(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 | shared_ptr<string> p1; // shared_ptr that can point at a string |
shared_ptr
和unique_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) |
交换p 和q 中的指针 |
shared_ptr
独有的操作:
操作 | 解释 |
---|---|
make_shared<T>(args) |
返回一个shared_ptr ,指向一个动态分配的类型为T 的对象。使用args 初始化此对象。 |
shared_ptr<T>p(q) |
p 是shared_ptr q 的拷贝;此操作会递增q 中的计数器。q 中的指针必须能转换为T* |
p = q |
p 和q 都是shared_ptr ,所保存的指针必须能互相转换。此操作会递减p 的引用计数,递增q 的引用计数;若p 的引用计数变为0,则将其管理的原内存释放。 |
p.unique() |
若p.use_count() 是1,返回true ;否则返回false |
p.use_count() |
返回与p 共享对象的智能指针数量;可能很慢,主要用于调试。 |
make_shared
函数(定义在头文件memory中)在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
。
1 | // shared_ptr that points to an int with value 42 |
- 进行拷贝或赋值操作时,每个
shared_ptr
会记录有多少个其他shared_ptr
与其指向相同的对象。
1 | auto p = make_shared<int>(42); // object to which p points has one user |
- 每个
shared_ptr
都有一个与之关联的计数器,通常称为引用计数(reference count)。拷贝shared_ptr
时引用计数会递增。例如使用一个shared_ptr
初始化另一个shared_ptr
,或将它作为参数传递给函数以及作为函数的返回值返回。给shared_ptr
赋予新值或shared_ptr
被销毁时引用计数会递减。例如一个局部shared_ptr
离开其作用域。一旦一个shared_ptr
的引用计数变为0,它就会自动释放其所管理的对象。
1 | auto r = make_shared<int>(42); // int to which r points has one user |
shared_ptr
的析构函数会递减它所指向对象的引用计数。如果引用计数变为0,shared_ptr
的析构函数会销毁对象并释放空间。如果将
shared_ptr
存放于容器中,而后不再需要全部元素,而只使用其中一部分,应该用erase
删除不再需要的元素。程序使用动态内存通常出于以下三种原因之一:
不确定需要使用多少对象。
不确定所需对象的准确类型。
需要在多个对象间共享数据。
直接管理内存(Managing Memory Directly)
相对于智能指针,使用
new
和delete
管理内存很容易出错。默认情况下,动态分配的对象是默认初始化的。所以内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
1 | string *ps = new string; // initialized to empty string |
- 可以使用值初始化方式、直接初始化方式、传统构造方式(圆括号
()
)或新标准下的列表初始化方式(花括号{}
)初始化动态分配的对象。
1 | int *pi = new int(1024); // object to which pi points has value 1024 |
- 只有当初始化的括号中仅有单一初始化器时才可以使用
auto
。
1 | auto p1 = new auto(obj); // p points to an object of the type of obj |
可以用
new
分配const
对象,返回指向const
类型的指针。动态分配的const
对象必须初始化。默认情况下,如果
new
不能分配所要求的内存空间,会抛出bad_alloc
异常。使用定位new
(placement new)可以阻止其抛出异常。定位new
表达式允许程序向new
传递额外参数。如果将nothrow
传递给new
,则new
在分配失败后会返回空指针。bad_alloc
和nothrow
都定义在头文件new中。
1 | // 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 | shared_ptr<int> p1 = new int(1024); // error: must use direct initialization |
- 默认情况下,用来初始化智能指针的内置指针必须指s向动态内存,因为智能指针默认使用
delete
释放它所管理的对象。如果要将智能指针绑定到一个指向其他类型资源的指针上,就必须提供自定义操作来代替delete
。
操作 | 解释 |
---|---|
shared_ptr<T> p(q) |
p 管理内置指针q 所指向的对象;q 必须指向new 分配的内存,且能够转换为T* 类型 |
shared_ptr<T> p(u) |
p 从unique_ptr u 那里接管了对象的所有权;将u 置为空 |
shared_ptr<T> p(q, d) |
p 接管了内置指针q 所指向的对象的所有权。q 必须能转换为T* 类型。p 将使用可调用对象d 来代替delete 。 |
shared_ptr<T> p(p2, d) |
p 是shared_ptr p2 的拷贝,唯一的区别是p 将可调用对象d 来代替delete 。 |
p.reset() |
若p 是唯一指向其对象的shared_ptr ,reset 会释放此对象。若传递了可选的参数内置指针q ,会令p 指向q ,否则会将p 置空。若还传递了参数d ,则会调用d 而不是delete 来释放q 。 |
p.reset(q) |
同上 |
p.reset(q, d) |
同上 |
- 不要混合使用内置指针和智能指针。当将
shared_ptr
绑定到内置指针后,资源管理就应该交由shared_ptr
负责。不应该再使用内置指针访问shared_ptr
指向的内存。
1 | // ptr is created and initialized when process is called |
智能指针的
get
函数返回一个内置指针,指向智能指针管理的对象。主要用于向不能使用智能指针的代码传递内置指针。使用get
返回指针的代码不能delete
此指针。不要使用
get
初始化另一个智能指针或为智能指针赋值。
1 | shared_ptr<int> p(new int(42)); // reference count is 1 |
- 可以用
reset
函数将新的指针赋予shared_ptr
。与赋值类似,reset
会更新引用计数,如果需要的话,还会释放内存空间。reset
经常与unique
一起使用,来控制多个shared_ptr
共享的对象。
1 | if (!p.unique()) |
智能指针和异常(Smart Pointers and Exceptions)
- 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。
1 | void f() |
- 默认情况下
shared_ptr
假定其指向动态内存,使用delete
释放对象。创建shared_ptr
时可以传递一个(可选)指向删除函数的指针参数,用来代替delete
。
1 | struct destination; // represents what we are connecting to |
智能指针规范:
- 不使用相同的内置指针值初始化或
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 | unique_ptr<int> p1(new 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 | // transfers ownership from p1 (which points to the string Stegosaurus) to p2 |
- 调用
release
会切断unique_ptr
和它原来管理的对象之间的联系。release
返回的指针通常被用来初始化另一个智能指针或给智能指针赋值。如果没有用另一个智能指针保存release
返回的指针,程序就要负责资源的释放。
1 | p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer |
- 不能拷贝
unique_ptr
的规则有一个例外:可以拷贝或赋值一个即将被销毁的unique_ptr
(移动构造、移动赋值)。
1 | unique_ptr<int> clone(int p) |
老版本的标准库包含了一个名为
auto_ptr
的类,类似
shared_ptr
,默认情况下unique_ptr
用delete
释放其指向的对象。unique_ptr
的删除器同样可以重载,但unique_ptr
管理删除器的方式与shared_ptr
不同。定义unique_ptr
时必须在尖括号中提供删除器类型。创建或reset
这种unique_ptr
类型的对象时,必须提供一个指定类型的可调用对象(删除器)。
1 | // p points to an object of type objT and uses an object of type delT to free that object |
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_ptr
。T
必须能转换为sp
指向的类型。w = p
p
可以是shared_ptr
或一个weak_ptr
。赋值后w
和p
共享对象。w.reset()
将 w
置为空。w.use_count()
与 w
共享对象的shared_ptr
的数量。w.expired()
若 w.use_count()
为0,返回true
,否则返回false
w.lock()
如果 expired
为true
,则返回一个空shared_ptr
;否则返回一个指向w
的对象的shared_ptr
。创建一个
weak_ptr
时,需要使用shared_ptr
来初始化它。
1 | auto p = make_shared<int>(42); |
- 使用
weak_ptr
访问对象时,必须先调用lock
函数。该函数检查weak_ptr
指向的对象是否仍然存在。如果存在,则返回指向共享对象的shared_ptr
,否则返回空指针。
1 | if (shared_ptr<int> np = wp.lock()) |
动态数组(Dynamic Arrays)
使用allocator
类可以将内存分配和初始化过程分离,这通常会提供更好的性能和更灵活的内存管理能力。
new和数组(new and Arrays)
- 使用
new
分配对象数组时需要在类型名之后跟一对方括号,在其中指明要分配的对象数量(必须是整型,但不必是常量)。new
返回指向第一个对象的指针(元素类型)。
1 | // call get_size to determine how many ints to allocate |
由于
new
分配的内存并不是数组类型,因此不能对动态数组调用begin
和end
,也不能用范围for
语句处理其中的元素。默认情况下,
new
分配的对象是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小后面跟一对空括号()
。在新标准中,还可以提供一个元素初始化器的花括号列表。如果初始化器数量大于元素数量,则new
表达式失败,不会分配任何内存,并抛出bad_array_new_length
异常。
1 | int *pia = new int[10]; // block of ten uninitialized ints |
虽然可以使用空括号对
new
分配的数组元素进行值初始化,但不能在括号中指定初始化器。这意味着不能用auto
分配数组。动态分配一个空数组是合法的,此时
new
会返回一个合法的非空指针。对于零长度的数组来说,该指针类似尾后指针,不能解引用。使用
delete[]
释放动态数组。
1 | delete p; // p must point to a dynamically allocated object or be null |
如果在
delete
数组指针时忘记添加方括号,或者在delete
单一对象时使用了方括号,编译器很可能不会给出任何警告,程序可能会在执行过程中行为异常。unique_ptr
可以直接管理动态数组,定义时需要在对象类型后添加一对空方括号[]
。
1 | // up points to an array of ten uninitialized ints |
指向数组的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 | // to use a shared_ptr we must supply a deleter |
shared_ptr
未定义下标运算符,智能指针类型也不支持指针算术运算。因此如果想访问shared_ptr
管理的数组元素,必须先用get
获取内置指针,再用内置指针进行访问。
1 | // shared_ptrs don't have subscript operator and don't support pointer arithmetic |
allocator类(The allocator Class)
allocator
类是一个模板,定义时必须指定其可以分配的对象类型。
1 | allocator<string> alloc; // object that can allocate strings |
标准库allocator
类及其算法:
操作 | 解释 |
---|---|
allocator<T> a |
定义了一个名为a 的allocator 对象,它可以为类型为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) |
p 为T* 类型的指针,此算法对p 指向的对象执行析构函数。 |
allocator
分配的内存是未构造的,程序需要在此内存中构造对象。新标准库的construct
函数接受一个指针和零或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象,必须与对象类型相匹配。
1 | auto q = p; // q will point to one past the last constructed element |
直接使用
allocator
返回的未构造内存是错误行为,其结果是未定义的。对象使用完后,必须对每个构造的元素调用
destroy
进行销毁。destroy
函数接受一个指针,对指向的对象执行析构函数。
1 | while (q != p) |
deallocate
函数用于释放allocator
分配的内存空间。传递给deallocate
的指针不能为空,它必须指向由allocator
分配的内存。而且传递给deallocate
的大小参数必须与调用allocator
分配内存时提供的大小参数相一致。
1 | alloc.deallocate(p, n); |
allocator
算法:
操作 | 解释 |
---|---|
uninitialized_copy(b, e, b2) |
从迭代器b 和e 给定的输入范围中拷贝元素到迭代器b2 指定的未构造的原始内存中。b2 指向的内存必须足够大,能够容纳输入序列中元素的拷贝。 |
uninitialized_copy_n(b, n, b2) |
从迭代器b 指向的元素开始,拷贝n 个元素到b2 开始的内存中。 |
uninitialized_fill(b, e, t) |
在迭代器b 和e 执行的原始内存范围中创建对象,对象的值均为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 | class Foo |
由于拷贝构造函数在一些情况下会被隐式使用,因此通常不会声明为
explicit
的。如果类未定义自己的拷贝构造函数,编译器会为类合成一个。一般情况下,合成拷贝构造函数(synthesized copy constructor)会将其参数的非
static
成员逐个拷贝到正在创建的对象中。
1 | class Sales_data |
- 使用直接初始化时,实际上是要求编译器按照函数匹配规则来选择与实参最匹配的构造函数。使用拷贝初始化时,是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
1 | string dots(10, '.'); // direct initialization |
拷贝初始化通常使用拷贝构造函数来完成。但如果一个类拥有移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
发生拷贝初始化的情况:
用
=
定义变量。将对象作为实参传递给非引用类型的形参。
从返回类型为非引用类型的函数返回对象。
用花括号列表初始化数组中的元素或聚合类中的成员。
当传递一个实参或者从函数返回一个值时,不能隐式使用
explicit
构造函数。
1 | vector<int> v1(10); // ok: direct initialization |
拷贝赋值运算符(The Copy-Assignment Operator)
重载运算符(overloaded operator)的参数表示运算符的运算对象。
如果一个运算符是成员函数,则其左侧运算对象会绑定到隐式的
this
参数上。赋值运算符通常应该返回一个指向其左侧运算对象的引用。
1 | class Foo |
标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
如果类未定义自己的拷贝赋值运算符,编译器会为类合成一个。一般情况下,合成拷贝赋值运算符(synthesized copy-assignment operator)会将其右侧运算对象的非
static
成员逐个赋值给左侧运算对象的对应成员,之后返回左侧运算对象的引用。
1 | // equivalent to the synthesized copy-assignment operator |
析构函数(The Destructor)
析构函数负责释放对象使用的资源,并销毁对象的非
static
数据成员。析构函数的名字由波浪号
~
接类名构成,它没有返回值,也不接受参数。
1 | class Foo |
由于析构函数不接受参数,所以它不能被重载。
如果类未定义自己的析构函数,编译器会为类合成一个。合成析构函数(synthesized destructor)的函数体为空。
析构函数首先执行函数体,然后再销毁数据成员。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。成员按照初始化顺序的逆序销毁。
隐式销毁一个内置指针类型的成员不会
delete
它所指向的对象。无论何时一个对象被销毁,都会自动调用其析构函数。
当指向一个对象的引用或指针离开作用域时,该对象的析构函数不会执行。
三/五法则(The Rule of Three/Five)
需要析构函数的类一般也需要拷贝和赋值操作。
需要拷贝操作的类一般也需要赋值操作,反之亦然。
使用=default(Using =default)
- 可以通过将拷贝控制成员定义为
=default
来显式地要求编译器生成合成版本。
1 | class Sales_data |
在类内使用
=default
修饰成员声明时,合成的函数是隐式内联的。如果不希望合成的是内联函数,应该只对成员的类外定义使用=default
。只能对具有合成版本的成员函数使用
=default
。
阻止拷贝(Preventing Copies)
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显式地还是隐式地。
在C++11新标准中,将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function)可以阻止类对象的拷贝。删除的函数是一种虽然进行了声明,但是却不能以任何方式使用的函数。定义删除函数的方式是在函数的形参列表后面添加
=delete
。
1 | struct NoCopy |
=delete
和=default
有两点不同:=delete
可以对任何函数使用;=default
只能对具有合成版本的函数使用。=delete
必须出现在函数第一次声明的地方;=default
既能出现在类内,也能出现在类外。
析构函数不能是删除的函数。对于析构函数被删除的类型,不能定义该类型的变量或者释放指向该类型动态分配对象的指针。
如果一个类中有数据成员不能默认构造、拷贝或销毁,则对应的合成拷贝控制成员将被定义为删除的。
在旧版本的C++标准中,类通过将拷贝构造函数和拷贝赋值运算符声明为
private
成员来阻止类对象的拷贝。在新标准中建议使用=delete
而非private
。
拷贝控制和资源管理(Copy Control and Resource Management)
通常,管理类外资源的类必须定义拷贝控制成员。
行为像值的类(Classes That Act Like Values)
1 | class HasPtr |
编写赋值运算符时有两点需要注意:
即使将一个对象赋予它自身,赋值运算符也能正确工作。
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
8HasPtr& 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 | class HasPtr |
析构函数释放内存前应该判断是否还有其他对象指向这块内存。
1 | HasPtr::~HasPtr() |
交换操作(Swap)
通常,管理类外资源的类会定义swap
函数。如果一个类定义了自己的swap
函数,算法将使用自定义版本,否则将使用标准库定义的swap
。
1 | class HasPtr |
一些算法在交换两个元素时会调用swap
函数,其中每个swap
调用都应该是未加限定的。如果存在类型特定的swap
版本,其匹配程度会优于std中定义的版本(假定作用域中有using
声明)。
1 | void swap(Foo &lhs, Foo &rhs) |
与拷贝控制成员不同,
swap
函数并不是必要的。但是对于分配了资源的类,定义swap
可能是一种重要的优化手段。由于
swap
函数的存在就是为了优化代码,所以一般将其声明为内联函数。定义了
swap
的类通常用swap
来实现赋值运算符。在这种版本的赋值运算符中,右侧运算对象以值方式传递,然后将左侧运算对象与右侧运算对象的副本进行交换(拷贝并交换,copy and swap)。这种方式可以正确处理自赋值情况。
1 | // note rhs is passed by value, which means the HasPtr copy constructor |
拷贝控制示例(A Copy-Control Example)
拷贝赋值运算符通常结合了拷贝构造函数和析构函数的工作。在这种情况下,公共部分应该放在private
的工具函数中完成。
动态内存管理类(Classes That Manage Dynamic Memory)
移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象中。
对象移动(Moving Objects)
某些情况下,一个对象拷贝后就立即被销毁了,此时移动而非拷贝对象会大幅度提高性能。
在旧版本的标准库中,容器所能保存的类型必须是可拷贝的。但在新标准中,可以用容器保存不可拷贝,但可移动的类型。
标准库容器、
string
和shared_ptr
类既支持移动也支持拷贝。IO类和unique_ptr
类可以移动但不能拷贝。
右值引用(Rvalue Reference)
- 为了支持移动操作,C++11引入了右值引用类型。右值引用就是必须绑定到右值的引用。可以通过
&&
来获得右值引用。
1 | int i = 42; |
右值引用只能绑定到即将被销毁,并且没有其他用户的临时对象上。使用右值引用的代码可以自由地接管所引用对象的资源。
变量表达式都是左值,所以不能将一个右值引用直接绑定到一个变量上,即使这个变量的类型是右值引用也不行。
1 | int &&rr1 = 42; // ok: literals are rvalues |
- 调用
move
函数可以获得绑定在左值上的右值引用,此函数定义在头文件utility中。
1 | int &&rr3 = std::move(rr1); |
- 调用
move
函数的代码应该使用std::move
而非move
,这样做可以避免潜在的名字冲突。
移动构造函数和移动赋值运算符(Move Constructor and Move Assignment)
移动构造函数的第一个参数是该类类型的右值引用,其他任何额外参数都必须有默认值。
除了完成资源移动,移动构造函数还必须确保移后源对象是可以安全销毁的。
在函数的形参列表后面添加关键字
noexcept
可以指明该函数不会抛出任何异常。对于构造函数,
noexcept
位于形参列表和初始化列表开头的冒号之间。在类的头文件声明和定义中(如果定义在类外)都应该指定noexcept
。
1 | class StrVec |
标准库容器能对异常发生时其自身的行为提供保障。虽然移动操作通常不抛出异常,但抛出异常也是允许的。为了安全起见,除非容器确定元素类型的移动操作不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝而非移动操作。
不抛出异常的移动构造函数和移动赋值运算符必须标记为
noexcept
。在移动操作之后,移后源对象必须保持有效的、可销毁的状态,但是用户不能使用它的值。
1 | StrVec &StrVec::operator=(StrVec &&rhs) noexcept |
- 只有当一个类没有定义任何拷贝控制成员,且类的每个非
static
数据成员都可以移动时,编译器才会为类合成移动构造函数和移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,则编译器也能移动该成员。
1 | // the compiler will synthesize the move operations for X and hasX |
与拷贝操作不同,移动操作永远不会被隐式定义为删除的函数。但如果显式地要求编译器生成
=default
的移动操作,且编译器不能移动全部成员,则移动操作会被定义为删除的函数。定义了移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则这些成员会被默认地定义为删除的函数。
如果一个类有可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的,即使调用
move
函数时也是如此。拷贝赋值运算符和移动赋值运算符的情况类似。
1 | class Foo |
- 使用非引用参数的单一赋值运算符可以实现拷贝赋值和移动赋值两种功能。依赖于实参的类型,左值被拷贝,右值被移动。
1 | // assignment operator is both the move- and copy-assignment operator |
建议将五个拷贝控制成员当成一个整体来对待。如果一个类需要任何一个拷贝操作,它就应该定义所有五个操作。
移动赋值运算符可以直接检查自赋值情况。
C++11标准库定义了移动迭代器(move iterator)适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。移动迭代器的解引用运算符返回一个右值引用。
调用
make_move_iterator
函数能将一个普通迭代器转换成移动迭代器。原迭代器的所有其他操作在移动迭代器中都照常工作。最好不要在移动构造函数和移动赋值运算符这些类实现代码之外的地方随意使用
move
操作。
右值引用和成员函数(Rvalue References and Member Functions)
- 区分移动和拷贝的重载函数通常有一个版本接受一个
const T&
参数,另一个版本接受一个T&&
参数(T为类型)。
1 | void push_back(const X&); // copy: binds to any kind of X |
有时可以对右值赋值:
1 | string s1, s2; |
在旧标准中,没有办法阻止这种使用方式。为了维持向下兼容性,新标准库仍然允许向右值赋值。但是可以在自己的类中阻止这种行为,规定左侧运算对象(即
this
指向的对象)必须是一个左值。在非
static
成员函数的形参列表后面添加引用限定符(reference qualifier)可以指定this
的左值/右值属性。引用限定符可以是&
或者&&
,分别表示this
可以指向一个左值或右值对象。引用限定符必须同时出现在函数的声明和定义中。
1 | class Foo |
- 一个非
static
成员函数可以同时使用const
和引用限定符,此时引用限定符跟在const
限定符之后。
1 | class Foo |
- 引用限定符也可以区分成员函数的重载版本。
1 | class Foo |
- 如果一个成员函数有引用限定符,则具有相同参数列表的所有重载版本都必须有引用限定符。
1 | class Foo |
第十四章 重载运算与类型转换
基本概念(Basic Concepts)
重载的运算符是具有特殊名字的函数,它们的名字由关键字
operator
和其后要定义的运算符号组成。重载运算符函数的参数数量和该运算符作用的运算对象数量一样多。对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。除了重载的函数调用运算符
operator()
之外,其他重载运算符不能含有默认实参。如果一个运算符函数是类的成员函数,则它的第一个运算对象会绑定到隐式的
this
指针上。因此成员运算符函数的显式参数数量比运算对象的数量少一个。当运算符作用于内置类型的运算对象时,无法改变该运算符的含义。
只能重载大多数已有的运算符,无权声明新的运算符号。
可以被重载 | 不可以被重载 |
+ , - , * , / , % , ^ |
:: , .* , . , ? : , |
& , | , ~ , ! , , , = |
|
< , > , <= , >= , ++ , -- |
|
<< , >> , == , != , && , | | | +=, -=, /=, %=, ^=, &=| | | \|=, =, <<=, >>=, [], ()| | | ->, ->, new, new[], delete, delete[]` |
重载运算符的优先级和结合律与对应的内置运算符一致。
可以像调用普通函数一样直接调用运算符函数。
1 | // equivalent calls to a nonmember operator function |
通常情况下,不应该重载逗号
,
、取地址&
、逻辑与&&
和逻辑或||
运算符。建议只有当操作的含义对于用户来说清晰明了时才使用重载运算符,重载运算符的返回类型也应该与其内置版本的返回类型兼容。
如果类中含有算术运算符或位运算符,则最好也提供对应的复合赋值运算符。
把运算符定义为成员函数时,它的左侧运算对象必须是运算符所属类型的对象。
1 | string s = "world"; |
如何选择将运算符定义为成员函数还是普通函数:
赋值
=
、下标[]
、调用()
和成员访问箭头->
运算符必须是成员函数。复合赋值运算符一般是成员函数,但并非必须。
改变对象状态或者与给定类型密切相关的运算符,如递增、递减、解引用运算符,通常是成员函数。
具有对称性的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符,通常是普通函数。
输入和输出运算符(Input and Output Operators)
重载输出运算符<<(Overloading the Output Operator <<)
- 通常情况下,输出运算符的第一个形参是
ostream
类型的普通引用,第二个形参是要打印类型的常量引用,返回值是它的ostream
形参。
1 | ostream &operator<<(ostream &os, const Sales_data &item) |
输出运算符应该尽量减少格式化操作。
输入输出运算符必须是非成员函数。而由于IO操作通常需要读写类的非公有数据,所以输入输出运算符一般被声明为友元。
重载输入运算符>>(Overloading the Input Operator >>)
- 通常情况下,输入运算符的第一个形参是要读取的流的普通引用,第二个形参是要读入的目的对象的普通引用,返回值是它的第一个形参。
1 | istream &operator>>(istream &is, Sales_data &item) |
输入运算符必须处理输入失败的情况,而输出运算符不需要。
以下情况可能导致读取操作失败:
读取了错误类型的数据。
读取操作到达文件末尾。
遇到输入流的其他错误。
当读取操作发生错误时,输入操作符应该负责从错误状态中恢复。
如果输入的数据不符合规定的格式,即使从技术上看IO操作是成功的,输入运算符也应该设置流的条件状态以标示出失败信息。通常情况下,输入运算符只设置
failbit
状态。eofbit
、badbit
等错误最好由IO标准库自己标示。
算术和关系运算符(Arithmetic and Relational Operators)
通常情况下,算术和关系运算符应该定义为非成员函数,以便两侧的运算对象进行转换。其次,由于这些运算符一般不会改变运算对象的状态,所以形参都是常量引用。
算术运算符通常会计算它的两个运算对象并得到一个新值,这个值通常存储在一个局部变量内,操作完成后返回该局部变量的副本作为结果(返回类型建议设置为原对象的
const
类型)。
1 | // assumes that both objects refer to the same book |
- 如果类定义了算术运算符,则通常也会定义对应的复合赋值运算符,此时最有效的方式是使用复合赋值来实现算术运算符。
相等运算符(Equality Operators)
相等运算符设计准则:
如果类在逻辑上有相等性的含义,则应该定义
operator==
而非一个普通的命名函数。这样做便于使用标准库容器和算法,也更容易记忆。通常情况下,
operator==
应该具有传递性。如果类定义了
operator==
,则也应该定义operator!=
。operator==
和operator!=
中的一个应该把具体工作委托给另一个。1
2
3
4
5
6
7
8
9
10
11bool 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 | StrVec &StrVec::operator=(initializer_list<string> il) |
下标运算符(Subscript Operator)
下标运算符必须定义为成员函数。
类通常会定义两个版本的下标运算符:一个返回普通引用,另一个是类的常量成员并返回常量引用。
1 | class StrVec |
递增和递减运算符(Increment and Decrement Operators)
定义递增和递减运算符的类应该同时定义前置和后置版本,这些运算符通常定义为成员函数。
为了与内置操作保持一致,前置递增或递减运算符应该返回运算后对象的引用。
1 | // prefix: return a reference to the incremented/decremented object |
- 后置递增或递减运算符接受一个额外的(不被使用)
int
类型形参,该形参的唯一作用就是区分运算符的前置和后置版本。
1 | class StrBlobPtr |
- 为了与内置操作保持一致,后置递增或递减运算符应该返回运算前对象的原值(返回类型建议设置为原对象的
const
类型)。
1 | StrBlobPtr StrBlobPtr::operator++(int) |
- 如果想通过函数调用的方式使用后置递增或递减运算符,则必须为它的整型参数传递一个值。
1 | StrBlobPtr p(a1); // p points to the vector inside a1 |
成员访问运算符(Member Access Operators)
箭头运算符必须定义为成员函数,解引用运算符通常也是如此。
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的类的对象。
1 | class StrBlobPtr |
对于形如point->mem
的表达式来说,point必须是指向类对象的指针或者是一个重载了operator->
的类的对象。point类型不同,point->mem
的含义也不同。
- 如果point是指针,则调用内置箭头运算符,表达式等价于
(*point).mem
。 - 如果point是重载了
operator->
的类的对象,则使用point.operator->()
的结果来获取mem,表达式等价于(point.operator->())->mem
。其中,如果该结果是一个指针,则执行内置操作,否则重复调用当前操作。
函数调用运算符(Function-Call Operator)
函数调用运算符必须定义为成员函数。一个类可以定义多个不同版本的调用运算符,相互之间必须在参数数量或类型上有所区别。
1 | class PrintString |
如果类定义了调用运算符,则该类的对象被称作函数对象(function object),函数对象常常作为泛型算法的实参。
1 | for_each(vs.begin(), vs.end(), PrintString(cerr, '\n')); |
lambda是函数对象(Lambdas Are Function Objects)
- 编写一个
lambda
后,编译器会将该表达式转换成一个未命名类的未命名对象,类中含有一个重载的函数调用运算符。
1 | // sort words by size, but maintain alphabetical order for words of the same size |
lambda
默认不能改变它捕获的变量。因此在默认情况下,由lambda
产生的类中的函数调用运算符是一个const
成员函数。如果lambda
被声明为可变的,则调用运算符就不再是const
函数了。lambda
通过引用捕获变量时,由程序负责确保lambda
执行时该引用所绑定的对象确实存在。因此编译器可以直接使用该引用而无须在lambda
产生的类中将其存储为数据成员。相反,通过值捕获的变量被拷贝到lambda
中,此时lambda
产生的类必须为每个值捕获的变量建立对应的数据成员,并创建构造函数,用捕获变量的值来初始化数据成员。
1 | // get an iterator to the first element whose size() is >= sz |
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 | vector<string *> nameTable; // vector of pointers |
可调用对象与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 | // ordinary function |
不能直接将重载函数的名字存入
function
类型的对象中,这样做会产生二义性错误。消除二义性的方法是使用lambda
或者存储函数指针而非函数名字。C++11新标准库中的
function
类与旧版本中的unary_function
和binary_function
没有关系,后两个类已经被bind
函数代替。
重载、类型转换与运算符(Overloading,Conversions,and Operators)
转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversion)。
类型转换运算符(Conversion Operators)
- 类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型。它不能声明返回类型,形参列表也必须为空,一般形式如下:
1 | operator type() const; |
- 类型转换运算符可以面向除了
void
以外的任意类型(该类型要能作为函数的返回类型)进行定义。
1 | class SmallInt |
- 隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。
1 | // the double argument is converted to int using the built-in conversion |
应该避免过度使用类型转换函数。如果在类类型和转换类型之间不存在明显的映射关系,则这样的类型转换可能具有误导性。
C++11引入了显示的类型转换运算符(explicit conversion operator)。和显式构造函数一样,编译器通常不会将显式类型转换运算符用于隐式类型转换。
1 | class SmallInt |
如果表达式被用作条件,则编译器会隐式地执行显式类型转换。
if
、while
、do-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
15struct 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 | A a1 = f(b.operator A()); // ok: use B's conversion operator |
通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标都是算术类型的转换。
使用两个用户定义的类型转换时,如果转换前后存在标准类型转换,则由标准类型转换决定最佳匹配。
如果在调用重载函数时需要使用构造函数或者强制类型转换来改变实参的类型,通常意味着程序设计存在不足。
调用重载函数时,如果需要额外的标准类型转换,则该转换只有在所有可行函数都请求同一个用户定义类型转换时才有用。如果所需的用户定义类型转换不止一个,即使其中一个调用能精确匹配而另一个调用需要额外的标准类型转换,也会产生二义性错误。
1 | struct C |
函数匹配与重载运算符(Function Matching and Overloaded Operators)
表达式中运算符的候选函数集既包括成员函数,也包括非成员函数。
1 | class SmallInt |
如果类既定义了转换目标是算术类型的类型转换,也定义了重载的运算符,则会遇到重载运算符与内置运算符的二义性问题。
第十五章 面向对象程序设计
OOP:概述(OOP:An Overview)
面向对象程序设计(object-oriented programming)的核心思想是数据抽象(封装)、继承和动态绑定(多态)。
通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class),其他类则直接或间接地从基类继承而来,这些继承得到的类叫做派生类(derived class)。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类应该将这些函数声明为虚函数(virtual function)。方法是在函数名称前添加
virtual
关键字。
1 | class Quote |
- 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个或哪些基类继承而来的。类派生列表的形式首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以添加访问说明符。
1 | class Bulk_quote : public Quote |
派生类必须在其内部对所有重新定义的虚函数进行声明。
使用基类的引用或指针调用一个虚函数时将发生动态绑定(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 | Quote item; // object of base type |
- 每个类控制它自己的成员初始化过程,派生类必须使用基类的构造函数来初始化它的基类部分。派生类的构造函数通过构造函数初始化列表来将实参传递给基类构造函数。
1 | Bulk_quote(const std::string& book, double p, |
除非特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。
派生类初始化时首先初始化基类部分,然后按照声明的顺序依次初始化派生类成员。
派生类可以访问基类的公有成员和受保护成员。
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。如果某静态成员是可访问的,则既能通过基类也能通过派生类使用它。
已经完整定义的类才能被用作基类。
1 | class Base { /* ... */ } ; |
Base是D1的直接基类(direct base),是D2的间接基类(indirect base)。最终的派生类将包含它直接基类的子对象以及每个间接基类的子对象。
- C++11中,在类名后面添加
final
关键字可以禁止其他类继承它。
1 | class NoDerived final { /* */ }; // NoDerived can't be a base class |
类型转换与继承(Conversions and Inheritance)
和内置指针一样,智能指针类也支持派生类到基类的类型转换,所以可以将一个派生类对象的指针存储在一个基类的智能指针内。
表达式的静态类型(static type)在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型(dynamic type)则是变量或表达式表示的内存中对象的类型,只有运行时才可知。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
不存在从基类到派生类的隐式类型转换,即使一个基类指针或引用绑定在一个派生类对象上也不行,因为编译器只能通过检查指针或引用的静态类型来判断转换是否合法。
1 | Quote base; |
如果在基类中含有一个或多个虚函数,可以使用
dynamic_cast
运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用,该转换的安全检查将在运行期间执行。如果已知某个基类到派生类的转换是安全的,可以使用
static_cast
强制覆盖掉编译器的检查工作。派生类到基类的自动类型转换只对指针或引用有效,在派生类类型和基类类型之间不存在这种转换。
派生类到基类的转换允许我们给基类的拷贝/移动操作传递一个派生类的对象,这些操作是基类定义的,只会处理基类自己的成员,派生类的部分被切掉(sliced down)了。
1 | Bulk_quote bulk; // object of derived type |
- 用一个派生类对象为一个基类对象初始化或赋值时,只有该对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉。
虚函数(Virtual Functions)
当且仅当通过指针或引用调用虚函数时,才会在运行过程解析该调用,也只有在这种情况下对象的动态类型有可能与静态类型不同。
在派生类中覆盖某个虚函数时,可以再次使用
virtual
关键字说明函数性质,但这并非强制要求。因为一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。在派生类中覆盖某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。
派生类可以定义一个与基类中的虚函数名字相同但形参列表不同的函数,但编译器会认为该函数与基类中原有的函数是相互独立的,此时派生类的函数并没有覆盖掉基类中的版本。
C++11允许派生类使用
override
关键字显式地注明虚函数。如果override
标记了某个函数,但该函数并没有覆盖已存在的虚函数,编译器将报告错误。override
位于函数参数列表之后。
1 | struct B |
- 与禁止类继承类似,函数也可以通过添加
final
关键字来禁止覆盖操作。
1 | struct D2 : B |
final
和override
关键字出现在形参列表(包括任何const
或引用修饰符)以及尾置返回类型之后。虚函数也可以有默认实参,每次函数调用的默认实参值由本次调用的静态类型决定。如果通过基类的指针或引用调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参值最好一致。
使用作用域运算符
::
可以强制执行虚函数的某个版本,不进行动态绑定。
1 | // calls the version from the base class regardless of the dynamic type of baseP |
通常情况下,只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的动态绑定机制。
如果一个派生类虚函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用会被解析为对派生类版本自身的调用,从而导致无限递归。
抽象基类(Abstract Base Classes)
- 在类内部虚函数声明语句的分号前添加
=0
可以将一个虚函数声明为纯虚(pure virtual)函数。一个纯虚函数无须定义。
1 | double net_price(std::size_t) const = 0; |
可以为纯虚函数提供定义,但函数体必须定义在类的外部。
含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。
不能创建抽象基类的对象。
派生类构造函数只初始化它的直接基类。
重构(refactoring)负责重新设计类的体系以便将操作或数据从一个类移动到另一个类中。
访问控制与继承(Access Control and Inheritance)
一个类可以使用
protected
关键字来声明外部代码无法访问,但是派生类对象可以访问的成员。派生类的成员或友元只能通过派生类对象来访问基类的
protected
成员。派生类对于一个基类对象中的protected
成员没有任何访问权限。
1 | class Base |
基类中成员的访问说明符和派生列表中的访问说明符都会影响某个类对其继承成员的访问权限。
派生访问说明符对于派生类的成员及友元能否访问其直接基类的成员没有影响,对基类成员的访问权限只与基类中的访问说明符有关。
派生访问说明符的作用是控制派生类(包括派生类的派生类)用户对于基类成员的访问权限。
- 如果使用公有继承,则基类的公有成员和受保护成员在派生类中属性不发生改变。
- 如果使用受保护继承,则基类的公有成员和受保护成员在派生类中变为受保护成员。
- 如果使用私有继承,则基类的公有成员和受保护成员在派生类中变为私有成员。
派生类到基类转换的可访问性(假定D继承自B):
- 只有当D公有地继承B时,用户代码才能使用派生类到基类的转换。
- 不论D以什么方式继承B,D的成员函数和友元都能使用派生类到基类的转换。
- 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员函数和友元可以使用D到B的类型转换;反之,如果D继承B的方式是私有的,则不能使用。
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类到基类的类型转换也是可访问的。
友元对基类的访问权限由基类自身控制,即使对于派生类中的基类部分也是如此。
1 | class Base |
友元关系不能继承,每个类负责控制各自成员的访问权限。
使用
using
声明可以改变派生类继承的某个名字的访问级别。新的访问级别由该using
声明之前的访问说明符决定。
1 | class Base |
派生类只能为那些它可以访问的名字提供
using
声明。默认情况下,使用
class
关键字定义的派生类是私有继承的,而使用struct
关键字定义的派生类是公有继承的。建议显式地声明派生类的继承方式,不要仅仅依赖于默认设置。
继承中的类作用域(Class Scope under Inheritance)
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
派生类定义的成员会隐藏同名的基类成员。
1 | struct Base |
- 可以通过作用域运算符
::
来使用被隐藏的基类成员。
1 | struct Derived : Base |
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
和其他函数一样,成员函数无论是否是虚函数都能被重载。
派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对它来说都是可见的,那么它就需要覆盖所有版本,或者一个也不覆盖。
有时一个类仅需覆盖重载集合中的一些而非全部函数,此时如果我们不得不覆盖基类中的每一个版本的话,操作会极其繁琐。为了简化操作,可以为重载成员提供
using
声明。using
声明指定了一个函数名字但不指定形参列表,所以一条基类成员函数的using
声明语句就可以把该函数的所有重载实例添加到派生类作用域中。
1 | class Base |
- 类内使用
using
声明改变访问级别的规则同样适用于重载函数的名字。
构造函数与拷贝控制(Constructors and Copy Control)
虚析构函数(Virtual Destructors)
一般来说,如果一个类需要析构函数,那么它也需要拷贝和赋值操作。但基类的析构函数不遵循该规则。
基类通常应该定义一个虚析构函数。
1 | class Quote |
- 如果基类的析构函数不是虚函数,则
delete
一个指向派生类对象的基类指针会产生未定义的结果。
1 | Quote *itemP = new Quote; // same static and dynamic type |
- 虚析构函数会阻止编译器为类合成移动操作。
合成拷贝控制与继承(Synthesized Copy Control and Inheritance)
对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类直接基类的成员。
派生类中删除的拷贝控制与基类的关系:
如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的或者不可访问的函数,则派生类中对应的成员也会是被删除的。因为编译器不能使用基类成员来执行派生类对象中基类部分的构造、赋值或销毁操作。
如果基类的析构函数是被删除的或者不可访问的,则派生类中合成的默认和拷贝构造函数也会是被删除的。因为编译器无法销毁派生类对象中的基类部分。
编译器不会合成一个被删除的移动操作。当我们使用
=default
请求一个移动操作时,如果基类中对应的操作是被删除的或者不可访问的,则派生类中的操作也会是被删除的。因为派生类对象中的基类部分不能移动。同样,如果基类的析构函数是被删除的或者不可访问的,则派生类的移动构造函数也会是被删除的。
在实际编程中,如果基类没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作。
因为基类缺少移动操作会阻止编译器为派生类合成自己的移动操作,所以当我们确实需要执行移动操作时,应该首先在基类中进行定义。
派生类的拷贝控制成员(Derived-Class Copy-Control Members)
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类成员在内的整个对象。
当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分。
1 | class Base { /* ... */ } ; |
在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝或移动基类部分,则必须在派生类的构造函数初始化列表中显式地使用基类的拷贝或移动构造函数。
派生类的赋值运算符必须显式地为其基类部分赋值。
1 | // Base::operator=(const Base&) is not invoked automatically |
- 派生类的析构函数只负责销毁派生类自己分配的资源。
1 | class D: public Base |
- 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
继承的构造函数(Inherited Constructors)
- C++11新标准允许派生类重用(非常规方式继承)其直接基类定义的构造函数。继承方式是提供一条注明了直接基类名的
using
声明语句。
1 | class Bulk_quote : public Disc_quote |
通常情况下,
using
声明语句只是令某个名字在当前作用域内可见。而作用于构造函数时,using
声明将令编译器产生代码。对于基类的每个构造函数,编译器都会生成一个与其形参列表完全相同的派生类构造函数。如果派生类含有自己的数据成员,则这些成员会被默认初始化。构造函数的
using
声明不会改变该函数的访问级别,不能指定explicit
或constexpr
属性。定义在派生类中的构造函数会替换继承而来的具有相同形参列表的构造函数。
派生类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器会为其合成它们。
当一个基类构造函数含有默认实参时,这些默认值不会被继承。相反,派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认值的形参。
容器与继承(Containers and Inheritance)
因为容器中不能保存不同类型的元素,所以不能把具有继承关系的多种类型的对象直接存储在容器中。
容器不能和存在继承关系的类型兼容。
如果想在容器中存储具有继承关系的对象,则应该存放基类的指针。
第十六章 模板与泛型编程
定义模板(Defining a Template)
函数模板(Function Templates)
函数模板可以用来生成针对特定类型的函数版本。
模板定义以关键字
template
开始,后跟一个模板参数列表(template parameter list)。模板参数列表以尖括号<>
包围,内含用逗号分隔的一个或多个模板参数(template parameter)。
1 | template <typename T> |
定义模板时,模板参数列表不能为空。
模板参数表示在类或函数定义中用到的类型或值。当使用模板时,需要显式或隐式地指定模板实参(template argument),并将其绑定到模板参数上。
使用函数模板时,编译器用推断出的模板参数来实例化(instantiate)一个特定版本的函数,这些生成的函数通常被称为模板的实例(instantiation)。
1 | // instantiates int compare(const int&, const int&) |
- 模板类型参数(type parameter)可以用来指定函数的返回类型或参数类型,以及在函数体内用于变量声明和类型转换。类型参数前必须使用关键字
class
或typename
。
1 | // ok: same type used for the return type and parameter |
建议使用
typename
而不是class
来指定模板类型参数,这样更加直观。模板非类型参数(nontype parameter)需要用特定的类型名来指定,表示一个值而非一个类型。非类型参数可以是整型、指向对象或函数类型的指针或左值引用。
1 | template<unsigned N, unsigned M> |
绑定到整型非类型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期,不能用普通局部变量或动态对象作为指针或引用非类型参数的实参。
函数模板也可以声明为
inline
或constexpr
的,说明符放在模板参数列表之后,返回类型之前。
1 | // ok: inline specifier follows the template parameter list |
- 模板程序应该尽量减少对实参类型的要求。
1 | // expected comparison |
只有当模板的一个特定版本被实例化时,编译器才会生成代码。此时编译器需要掌握生成代码所需的信息,因此函数模板和类模板成员函数的定义通常放在头文件中。
使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的设计者来保证的。模板设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。
调用者负责保证传递给模板的实参能正确支持模板所要求的操作。
类模板(Class Templates)
使用一个类模板时,必须提供显式模板实参(explicit template argument)列表,编译器使用这些模板实参来实例化出特定的类。
1 | template <typename T> |
一个类模板的每个实例都形成一个独立的类,相互之间没有关联。
如果一个类模板中的代码使用了另一个模板,通常不会将一个实际类型(或值)的名字用作其模板实参,而是将模板自己的参数用作被使用模板的实参。
类模板的成员函数具有和类模板相同的模板参数,因此定义在类模板外的成员函数必须以关键字template
开始,后跟类模板参数列表。
1 | template <typename T> |
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。
在类模板自己的作用域内,可以直接使用模板名而不用提供模板实参。
1 | template <typename T> |
当一个类包含一个友元声明时,类与友元各自是否是模板并无关联。如果一个类模板包含一个非模板友元,则友元可以访问所有类模板实例。如果友元自身是模板,则类可以给所有友元模板实例授予访问权限,也可以只授权给特定实例。
一对一友元关系
为了引用模板的一个特定实例,必须首先声明模板自身。模板声明包括模板参数列表。
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 | template <typename Type> |
C++11允许使用using
为类模板定义类型别名。
1 | template<typename T> using twin = pair<T, T>; |
类模板可以声明static
成员。
1 | template <typename T> |
类模板的每个实例都有一个独有的static
对象,而每个static
成员必须有且只有一个定义。因此与定义模板的成员函数类似,static
成员也应该定义成模板。
1 | template <typename T> |
模板参数(Template Parameters)
模板参数遵循普通的作用域规则。与其他任何名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是在模板内不能重用模板参数名。
1 | typedef double A; |
由于模板参数名不能重用,所以一个名字在一个特定模板参数列表中只能出现一次。
与函数参数一样,声明中模板参数的名字不必与定义中的相同。
一个特定文件所需要的所有模板声明通常一起放置在文件开始位置,出现在任何使用这些模板的代码之前。
模板中的代码使用作用域运算符::
时,编译器无法确定其访问的名字是类型还是static
成员。
默认情况下,C++假定模板中通过作用域运算符访问的名字是static
成员。因此,如果需要使用一个模板类型参数的类型成员,就必须使用关键字typename
显式地告知编译器该名字是一个类型。
1 | template <typename T> |
C++11允许为函数和类模板提供默认实参。
1 | // compare has a default template argument, less<T> |
如果一个类模板为其所有模板参数都提供了默认实参,在使用这些默认实参时,必须在模板名后面跟一个空尖括号对<>
。
1 | template <class T = int> |
成员模板(Member Templates)
一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数,这种成员被称为成员模板。成员模板不能是虚函数。
1 | class DebugDelete |
在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
1 | template <typename T> |
为了实例化一个类模板的成员模板,必须同时提供类和函数模板的实参。
控制实例化(Controlling Instantiations)
因为模板在使用时才会进行实例化,所以相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。
在大型程序中,多个文件实例化相同模板的额外开销可能非常严重。C++11允许通过显式实例化(explicit instantiation)来避免这种开销。
显式实例化的形式如下:
1 | extern template declaration; // instantiation declaration |
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。当编译器遇到extern
模板声明时,它不会在本文件中生成实例化代码。对于一个给定的实例化版本,可能有多个extern
声明,但必须只有一个定义。
1 | // templateBuild.cc |
当编译器遇到类模板的实例化定义时,它不清楚程序会使用哪些成员函数。和处理类模板的普通实例化不同,编译器会实例化该模板的所有成员,包括内联的成员函数。因此,用来显式实例化类模板的类型必须能用于模板的所有成员。
效率与灵活性(Efficiency and Flexibility)
unique_ptr
在编译时绑定删除器,避免了间接调用删除器的运行时开销。shared_ptr
在运行时绑定删除器,使用户重载删除器的操作更加简便。
模板实参推断(Template Argument Deduction)
对于函数模板,编译器通过调用的函数实参来确定其模板参数。这个过程被称作模板实参推断。
类型转换与模板类型参数(Conversions and Template Type Parameters)
与非模板函数一样,调用函数模板时传递的实参被用来初始化函数的形参。如果一个函数形参的类型使用了模板类型参数,则会采用特殊的初始化规则,只有有限的几种类型转换会自动地应用于这些实参。编译器通常会生成新的模板实例而不是对实参进行类型转换。
有3种类型转换可以在调用中应用于函数模板:
- 顶层
const
会被忽略。 - 可以将一个非
const
对象的引用或指针传递给一个const
引用或指针形参。 - 如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。数组实参可以转换为指向其首元素的指针。函数实参可以转换为该函数类型的指针。
其他的类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
一个模板类型参数可以作为多个函数形参的类型。由于允许的类型转换有限,因此传递给这些形参的实参必须具有相同的类型,否则调用失败。
1 | long lng; |
如果想增强函数的兼容性,可以使用两个类型参数定义函数模板。
1 | // argument types can differ but must be compatible |
函数模板中使用普通类型定义的参数可以进行正常的类型转换。
1 | template <typename T> |
函数模板显式实参(Function-Template Explicit Arguments)
某些情况下,编译器无法推断出模板实参的类型。
1 | // T1 cannot be deduced: it doesn't appear in the function parameter list |
显式模板实参(explicit template argument)可以让用户自己控制模板的实例化。提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号<>
中指定,位于函数名之后,实参列表之前。
1 | // T1 is explicitly specified; T2 and T3 are inferred from the argument types |
显式模板实参按照从左到右的顺序与对应的模板参数匹配,只有尾部参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
1 | // poor design: users must explicitly specify all three template parameters |
对于模板类型参数已经显式指定了的函数实参,可以进行正常的类型转换。
1 | long lng; |
尾置返回类型与类型转换(Trailing Return Types and Type Transformation)
由于尾置返回出现在函数列表之后,因此它可以使用函数参数来声明返回类型。
1 | // a trailing return lets us declare the return type after the parameter list is seen |
标准库在头文件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 | // must use typename to use a type member of a template parameter |
函数指针和实参推断(Function Pointers and Argument Deduction)
使用函数模板初始化函数指针或为函数指针赋值时,编译器用指针的类型来推断模板实参。
1 | template <typename T> int compare(const T&, const T&); |
如果编译器不能从函数指针类型确定模板实参,则会产生错误。使用显式模板实参可以消除调用歧义。
1 | // overloaded versions of func; each takes a different function pointer type |
模板实参推断和引用(Template Argument Deduction and References)
当一个函数参数是模板类型参数的普通(左值)引用(形如T&
)时,只能传递给它一个左值(如一个变量或一个返回引用类型的表达式)。T被推断为实参所引用的类型,如果实参是const
的,则T也为const
类型。
1 | template <typename T> void f1(T&); // argument must be an lvalue |
当一个函数参数是模板类型参数的常量引用(形如const T&
)时,可以传递给它任何类型的实参。函数参数本身是const
时,T的类型推断结果不会是const
类型。const
已经是函数参数类型的一部分了,因此不会再是模板参数类型的一部分。
1 | template <typename T> void f2(const T&); // can take an rvalue |
当一个函数参数是模板类型参数的右值引用(形如T&&
)时,如果传递给它一个右值,类型推断过程类似普通左值引用函数参数的推断过程,推断出的T类型是该右值实参的类型。
1 | template <typename T> void f3(T&&); |
模板参数绑定的两个例外规则:
如果将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。
如果间接创建了一个引用的引用(通过类型别名或者模板类型参数间接定义),则这些引用会被“折叠”。右值引用的右值引用会被折叠为右值引用。其他情况下,引用都被折叠为普通左值引用。
折叠前 折叠后 T& &
、T& &&
、T&& &
T&
T&& &&
T&&
1 | f3(i); // argument is an lvalue; template parameter T is int& |
模板参数绑定的两个例外规则导致了两个结果:
- 如果一个函数参数是指向模板类型参数的右值引用,则可以传递给它任意类型的实参。
- 如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难。
1 | template <typename T> |
实际编程中,模板的右值引用参数通常用于两种情况:模板转发其实参或者模板被重载。函数模板的常用重载形式如下:
1 | template <typename T> void f(T&&); // binds to nonconst rvalues |
理解std::move(Understanding std::move)
std::move
的定义如下:
1 | template <typename T> |
std::move
的工作过程:
1 | string s1("hi!"), s2; |
- 在
std::move(string("bye!"))
中传递的是右值。 - 推断出的T类型为
string
。 remove_reference
用string
进行实例化。remove_reference<string>
的type
成员是string
。move
的返回类型是string&&
。move
的函数参数t的类型为string&&
。
- 推断出的T类型为
- 在
std::move(s1)
中传递的是左值。 - 推断出的T类型为
string&
。 remove_reference
用string&
进行实例化。remove_reference<string&>
的type
成员是string
。move
的返回类型是string&&
。move
的函数参数t的类型为string& &&
,会折叠成string&
。
- 推断出的T类型为
可以使用static_cast
显式地将一个左值转换为一个右值引用。
转发(Forwarding)
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在这种情况下,需要保持被转发实参的所有性质,包括实参的const
属性以及左值/右值属性。
1 | // template that takes a callable and two parameters |
上例中,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 | template <typename F, typename T1, typename T2> |
对于修改后的版本,若调用flip2(f, j, 42)
,会传递给参数t1一个左值j,但此时推断出的T1类型为int&
,t1的类型会被折叠为int&
,从而解决了flip1
的错误。
但flip2
只能用于接受左值引用的函数,不能用于接受右值引用的函数。函数参数与其他变量一样,都是左值表达式。所以即使是指向模板类型的右值引用参数也只能传递给接受左值引用的函数,不能传递给接受右值引用的函数。
1 | void g(int &&i, int& j) |
C++11在头文件utility中定义了forward
。与move
不同,forward
必须通过显式模板实参调用,返回该显式实参类型的右值引用。即forward<T>
返回类型T&&
。
通常情况下,可以使用forward
传递定义为指向模板类型参数的右值引用函数参数。通过其返回类型上的引用折叠,forward
可以保持给定实参的左值/右值属性。
1 | template <typename Type> |
- 如果实参是一个右值,则Type是一个普通(非引用)类型,
forward<Type>
返回类型Type&&
。 - 如果实参是一个左值,则通过引用折叠,Type也是一个左值引用类型,
forward<Type>
返回类型Type&& &
,对返回类型进行引用折叠,得到Type&
。
使用forward
编写完善的转发函数。
1 | template <typename F, typename T1, typename T2> |
与std::move
一样,对std::forward
也不应该使用using
声明。
重载与模板(Overloading and Templates)
函数模板可以被另一个模板或普通非模板函数重载。
如果重载涉及函数模板,则函数匹配规则会受到一些影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板都是可行的,因为模板实参推断会排除任何不可行的模板。
- 和往常一样,可行函数(模板与非模板)按照类型转换(如果需要的话)来排序。但是可以用于函数模板调用的类型转换非常有限。
- 和往常一样,如果恰有一个函数提供比其他任何函数都更好的匹配,则选择此函数。但是如果多个函数都提供相同级别的匹配,则:
- 如果同级别的函数中只有一个是非模板函数,则选择此函数。
- 如果同级别的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
- 否则该调用有歧义。
通常,如果使用了一个没有声明的函数,代码将无法编译。但对于重载函数模板的函数而言,如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不再重要了。
1 | template <typename T> string debug_rep(const T &t); |
在定义任何函数之前,应该声明所有重载的函数版本。这样编译器就不会因为未遇到你希望调用的函数而实例化一个并非你所需要的版本。
可变参数模板(Variadic Templates)
可变参数模板指可以接受可变数量参数的模板函数或模板类。可变数量的参数被称为参数包(parameter pack),分为两种:
- 模板参数包(template parameter pack),表示零个或多个模板参数。
- 函数参数包(function parameter pack),表示零个或多个函数参数。
用一个省略号…
来指出模板参数或函数参数表示一个包。在一个模板参数列表中,class…
或typename…
指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数列表。在函数参数列表中,如果一个参数的类型是模板参数包,则此参数也是函数参数包。
1 | // Args is a template parameter pack; rest is a function parameter pack |
对于一个可变参数模板,编译器会推断模板参数类型和参数数量。
可以使用sizeof…
运算符获取参数包中的元素数量。类似sizeof
,sizeof…
也返回一个常量表达式,而且不会对其实参求值。
1 | template<typename ... Args> |
编写可变参数函数模板(Writing a Variadic Function Template)
可变参数函数通常是递归的,第一步调用参数包中的第一个实参,然后用剩余实参调用自身。为了终止递归,还需要定义一个非可变参数的函数。
1 | // function to end the recursion and print the last element |
Call | t | rest... |
---|---|---|
print(cout, i, s, 42) |
i | s, 42 |
print(cout, s, 42) |
s | 42 |
print(cout, 42) |
包扩展(Pack Expansion)
对于一个参数包,除了获取其大小外,唯一能对它做的事情就是扩展。当扩展一个包时,需要提供用于每个扩展元素的模式(pattern)。扩展一个包就是将其分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边添加一个省略号…
来触发扩展操作。
包扩展工作过程:
1 | template <typename T, typename... Args> |
第一个扩展操作扩展模板参数包,为
print
生成函数参数列表。编译器将模式const Args&
应用到模板参数包Args中的每个元素上。因此该模式的扩展结果是一个以逗号分隔的零个或多个类型的列表,每个类型都形如const type&
。1
2print(cout, i, s, 42); // two parameters in the pack
ostream& print(ostream&, const int&, const string&, const int&);第二个扩展操作扩展函数参数包,模式是函数参数包的名字。扩展结果是一个由包中元素组成、以逗号分隔的列表。
1
print(os, s, 42);
扩展操作中的模式会独立地应用于包中的每个元素。
1 | // call debug_rep on each argument in the call to print |
转发参数包(Forwarding Parameter Packs)
在C++11中,可以组合使用可变参数模板和forward
机制来编写函数,实现将其实参不变地传递给其他函数。
1 | // fun has zero or more parameters each of which is |
模板特例化(Template Specializations)
在某些情况下,通用模板的定义对特定类型是不合适的,可能编译失败或者操作不正确。如果不希望或不能使用模板版本时,可以定义类或函数模板的特例化版本。一个特例化版本就是模板的一个独立定义,其中的一个或多个模板参数被指定为特定类型。
1 | // first version; can compare any two types |
特例化一个函数模板时,必须为模板中的每个模板参数都提供实参。为了指明我们正在实例化一个模板,应该在关键字template
后面添加一个空尖括号对<>
。
特例化版本的参数类型必须与一个先前声明的模板中对应的类型相匹配。
定义特例化函数版本本质上是接管编译器的工作,为模板的一个特殊实例提供了定义。特例化并非重载,因此不影响函数匹配。
将一个特殊版本的函数定义为特例化模板还是独立的非模板函数会影响到重载函数匹配。
模板特例化遵循普通作用域规则。为了特例化一个模板,原模板的声明必须在作用域中。而使用模板实例时,也必须先包含特例化版本的声明。
通常,模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明放在文件开头,后面是这些模板的特例化版本。
类模板也可以特例化。与函数模板不同,类模板的特例化不必为所有模板参数提供实参,可以只指定一部分模板参数。一个类模板的部分特例化(partial specialization)版本本身还是一个模板,用户使用时必须为那些未指定的模板参数提供实参。
只能部分特例化类模板,不能部分特例化函数模板。
由于类模板的部分特例化版本是一个模板,所以需要定义模板参数。对于每个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。在类名之后,需要为特例化的模板参数指定实参,这些实参位于模板名之后的尖括号中,与原始模板中的参数按位置相对应。
1 | // 通用版本 |
类模板部分特例化版本的模板参数列表是原始模板参数列表的一个子集或特例化版本。
可以只特例化类模板的指定成员函数,而不用特例化整个模板。
1 | template <typename T> |
第十七章 标准库特殊设施
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) |
返回一个用给定初始值初始化的tuple 。tuple 的类型从初始值的类型推断。 |
t1 == t2 |
当两个tuple 具有相同数量的成员且成员对应相等时,两个tuple 相等。 |
t1 relop t2 |
tuple 的关系运算使用字典序。两个tuple 必须具有相同数量的成员。 |
get<i>(t) |
返回t 的第i 个数据成员的引用:如果t 是一个左值,结果是一个左值引用;否则,结果是一个右值引用。tuple 的所有成员都是public 的。 |
tuple_size<tupleType>::value |
一个类模板,可以通过一个tuple 类型来初始化。它有一个名为value 的public constexpr static 数据成员,类型为size_t ,表示给定tuple 类型中成员的数量。 |
tuple_element<i, tupleType>::type |
一个类模板,可以通过一个整型常量和一个tuple 类型来初始化。它有一个名为type 的public 成员,表示给定tuple 类型中指定成员的类型。 |
定义和初始化tuple(Defining and Initializing tuples)
定义tuple
时需要指定每个成员的类型。创建tuple
对象时,可以使用tuple
的默认构造函数,它会对每个成员进行值初始化。或者给每个成员提供初始值。包含初始值的构造函数是explicit
的,因此必须使用直接初始化语法。
1 | tuple<size_t, size_t, size_t> threeD = { 1, 2, 3 }; // error |
类似make_pair
,make_tuple
函数可以生成tuple
对象。tuple
的类型由初始值决定。
1 | // tuple that represents a bookstore transaction: ISBN, count, price per book |
可以使用get
访问tuple
的成员。get
是一个函数模板,使用时必须指定一个显式模板实参,表示要访问的成员索引。传递给get
一个tuple
实参后,会返回其指定成员的引用。
1 | auto book = get<0>(item); // returns the first member of item |
可以使用tuple_size
和tuple_element
这两个辅助类模板查询tuple
成员的数量和类型。
tuple_size
通过一个tuple
类型来初始化,它有一个名为value
的静态公有数据成员,类型为size_t
,表示给定tuple
中成员的数量。tuple_element
通过一个索引值(整型常量)和一个tuple
类型来初始化,它有一个名为type
的公有数据成员,表示给定tuple
中指定成员的类型。
使用decltype
可以确定一个对象的类型。
1 | typedef decltype(item) trans; // trans is the type of item |
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; |
b 有n 位;每一位均是0.此构造函数是一个constexpr 。 |
bitset<n> b(u); |
b 是unsigned long long 值u 的低n 位的拷贝。如果n 大于unsigned long long 的大小,则b 中超出unsigned long long 的高位被置为0。此构造函数是一个constexpr 。 |
bitset<n> b(s, pos, m, zero, one); |
b 是string s 从位置pos 开始m 个字符的拷贝。s 只能包含字符zero 或one :如果s 包含任何其他字符,构造函数会抛出invalid_argument 异常。字符在b 中分别保存为zero 和one 。pos 默认为0,m 默认为string::npos ,zero 默认为'0',one 默认为'1'。 |
bitset<n> b(cp, pos, m, zero, one); |
和上一个构造函数相同,但从cp 指向的字符数组中拷贝字符。如果未提供m ,则cp 必须指向一个C 风格字符串。如果提供了m ,则从cp 开始必须至少有m 个zero 或one 字符。 |
使用一个整型值初始化bitset
时,此值会被转换为unsigned long long
类型并被当作位模式处理。bitset
中的二进制位就是此模式的副本。如果bitset
的大小大于unsigned long long
中的二进制位数,剩余的高位会被置为0。如果bitset
的大小小于unsigned long long
中的二进制位数,则只使用给定值的低位部分。
1 | // bitvec1 is smaller than the initializer; high-order bits from the initializer are discarded |
可以使用string
或字符数组指针来初始化bitset
,字符直接表示位模式。使用字符串表示数时,字符串中下标最小的字符对应bitset
的高位。如果string
包含的字符数比bitset
少,则bitset
的高位被置为0。
1 | bitset<32> bitvec4("1100"); // bits 2 and 3 are 1, all others are 0 |
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 处的位;如果b 是const 的,则当该位置位时,返回true ;否则返回false 。 |
b.to_ulong() |
返回一个unsigned long 值,其位模式和b 相同。如果b 中位模式不能放入指定的结果类型,则抛出一个overflow_error 异常。 |
b.to_ullong() |
类似上面,返回一个unsigned long long 值。 |
b.to_string(zero, one) |
返回一个string ,表示b 中位模式。zero 和one 默认为0和1。 |
os << b |
将b 中二进制位打印为字符1 或0 ,打印到流os 。 |
is >> b |
从is 读取字符存入b 。当下一个字符不是1或0时,或是已经读入b.size() 个位时,读取过程停止。 |
bitset
的下标运算符对const
属性进行了重载。const
版本的下标运算符在指定位置置位时返回true
,否则返回false
。非const
版本返回bitset
定义的一个特殊类型,用来控制指定位置的值。
to_ulong
和to_ullong
操作用来提取bitset
的值。只有当bitset
的大小不大于对应操作的返回值(to_ulong
为unsigned long
,to_ullong
为unsigned long long
)时,才能使用这两个操作。如果bitset
中的值不能存入给定类型,则会引发overflow_error
异常。
1 | unsigned long ulong = bitvec3.to_ulong(); |
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 | // find the characters ei that follow a character other than c |
regex_match
和regex_search
函数确定一个给定的字符序列与一个regex
是否匹配。如果整个输入序列与表达式匹配,则regex_match
函数返回true
;如果输入序列中的一个子串与表达式匹配,则regex_search
函数返回true
。这两个函数的其中一个重载版本接受一个类型为smatch
的附加参数。如果匹配成功,函数会将匹配信息保存在给定的smatch
对象中。二者的参数形式如下:
操作 | 解释 |
---|---|
(seq, m, r, mft) |
在字符序列seq 中查找regex 对象r 中的正则表达式。seq 可以是一个string 、标识范围的一对迭代器、一个指向空字符结尾的字符数组的指针。 |
(seq, r, mft) |
m 是一个match 对象,用来保存匹配结果的相关细节。m 和seq 必须具有兼容的类型。mft 是一个可选的regex_constants::match_flag_type 值。 |
使用正则表达式库(Using the Regular Expression Library)
默认情况下,regex
使用的正则表达式语言是ECMAScript。
定义一个regex
或者对一个regex
调用assign
为其赋新值时,可以指定一些标志来影响regex
的操作。ECMAScript
、basic
、extended
、awk
、grep
和egrep
这六个标志指定编写正则表达式时所使用的语言。这六个标志中必须设置其中之一,且只能设置一个。默认情况下,ECMAScript
标志被设置,regex
会使用ECMA-262规范,这也是很多Web浏览器使用的正则表达式语言。
操作 | 解释 |
---|---|
regex r(re) regex r(re, f) |
re 表示一个正则表达式,它可以是一个string 、一对表示字符范围的迭代器、一个指向空字符结尾的字符数组的指针、一个字符指针和一个计数器、一个花括号包围的字符列表。f 是指出对象如何处理的标志。f 通过下面列出来的值来设置。如果未指定f ,其默认值为ECMAScript 。 |
r1 = re |
将r1 中的正则表达式替换Wiere 。re 表示一个正则表达式,它可以是另一个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 ,遍历迭代器b 和e 表示的string 。它调用sregex_search(b, e, r) 将it 定位到输入中第一个匹配的位置。 |
sregex_iterator end; |
sregex_iterator 的尾后迭代器 |
*it , it-> |
根据最后一个调用regex_search 的结果,返回一个smatch 对象的引用或一个指向smatch 对象的指针。 |
++it , it++ |
从输入序列当前匹配位置开始调用regex_search 。前置版本返回递增后迭代器;后置版本返回旧值。 |
it1 == it2 |
如果两个sregex_iterator 都是尾后迭代器,则它们相等。两个非尾后迭代器是从相同的输入序列和regex 对象构造,则它们相等。 |
以sregex_iterator
为例,将sregex_iterator
绑定到一个string
和一个regex
对象时,迭代器自动定位至给定string
中的第一个匹配位置。即,sregex_iterator
构造函数对给定string
和regex
调用regex_search
。解引用迭代器时,返回最近一次搜索结果的smatch
对象。递增迭代器时,它调用regex_search
在输入string
中查找下一个匹配位置。
1 | // find the characters ei that follow a character other than c |
x的成员,分别返回表示输入序列中当前匹配之前和之后部分的
ssub_match对象。一个
ssub_match对象有两个名为
str和
length的成员,分别返回匹配的
string和该
string`的长度。
1 | // same for loop header as before |
smatch
支持的操作:
操作 | 解释 |
---|---|
m.ready() |
如果已经通过调用regex_search 或regex_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() |
表示m 中ssub_match 元素范围的迭代器。 |
m.cbegin(), m.cend() |
常量迭代器 |
使用子表达式(Using Subexpressions)
正则表达式中的模式通常包含一个或多个子表达式。子表达式是模式的一部分,本身也有意义。正则表达式语法通常用括号表示子表达式。
1 | // r has two subexpressions: the first is the part of the file name before the period |
匹配对象除了提供匹配整体的相关信息外,还可以用来访问模式中的每个子表达式。子匹配是按位置来访问的,第一个子匹配位置为0,表示整个模式对应的匹配,随后是每个子表达式对应的匹配。
子表达式的一个常见用途是验证必须匹配特定格式的数据,如电话号码和电子邮箱地址。
ECMAScript正则表达式语言的一些特性:
- 模式
[[:alnum:]]
匹配任意字母。 - 符号
+
表示匹配一个或多个字符。 - 符号
*
表示匹配零个或多个字符。 \{d}
表示单个数字,\{d}{n}
表示一个n个数字的序列。- 在方括号中的字符集合表示匹配这些字符中的任意一个。
- 后接
?
的组件是可选的。 - 类似C++,ECMAScript使用反斜线进行转义。由于模式包含括号,而括号是ECMAScript中的特殊字符,因此需要用
\(
和\)
来表示括号是模式的一部分。
因为反斜线\
是C++中的特殊字符,所以在模式中使用\
时,需要一个额外的反斜线进行转义。
子匹配操作:
操作 | 解释 |
---|---|
matched |
一个public bool 数据成员,指出ssub_match 是否匹配了 |
first , second |
public 数据成员,指向匹配序列首元素和尾后位置的迭代器。如果未匹配,则first 和second 是相等的。 |
length() |
匹配的大小,如果matched 为false ,则返回0。 |
str() |
返回一个包含输入中匹配部分的string 。如果matched 为false ,则返回空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_search
、regex_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 | // generate just the phone numbers: use a new format string |
随机数(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 | default_random_engine e; // generates random unsigned integers |
标准库定义了多个随机数引擎类,区别在于性能和随机性质量。每个编译器都会指定其中一个作为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 | // uniformly distributed from 0 to 9 inclusive |
类似引擎类型,分布类型也是函数对象类。分布类型定义了一个接受一个随机数引擎参数的调用运算符。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布区间。
随机数发生器指分布对象和引擎对象的组合。
rand
函数的生成范围在0到RAND_MAX
之间,随机数引擎生成的unsigned
整数在一个系统定义的范围内。一个引擎类型的范围可以通过调用该类型对象的min
和max
成员来获得。
即使随机数发生器生成的数看起来是随机的,但对于一个给定的发生器,每次运行程序时它都会返回相同的数值序列。
如果函数需要局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static
对象,这样随机数发生器就能在函数调用期间保持状态。否则每次调用函数都会生成相同的序列。
1 | // returns a vector of 100 uniformly distributed random numbers |
通过为引擎提供一个种子(seed),可以让引擎在程序每次运行时生成不同的序列。种子是一个数值,引擎利用它从序列中的一个新位置重新开始生成随机数。
为引擎设置种子有两种方式:
- 在创建对象时提供种子。
- 调用引擎的
seed
成员设置种子。
1 | default_random_engine e1; // uses the default seed |
选择种子的常用方法是调用系统函数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 | default_random_engine e; // generates unsigned random integers |
分布类型操作:
操作 | 解释 |
---|---|
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 | // empty <> signify we want to use the default result type |
bernouilli_distribution
类型是一个普通类,而非模板。该分布返回一个bool
值,其中true
的概率是一个常数,默认为0.5。
由于引擎会返回相同的随机数序列,因此需要在循环中使用引擎时,必须在循环体外定义引擎对象。否则每次循环都会创建新引擎,生成相同序列。同样,分布对象也需要保持运行状态,也必须在循环体外定义。
IO库再探(The IO Library Revisited)
格式化输入与输出(Formatted Input and Output)
除了条件状态外,每个iostream
对象还维护着一个格式状态来控制IO格式化细节。
标准库定义了一组操纵符(manipulator)来修改流的格式状态。操纵符是一个函数或对象,会影响流的状态,并能作为输入和输出运算符的运算对象。类似输入和输出运算符,操纵符也返回它所处理的流对象。
操纵符用于两大类输出控制:控制数值的输出格式,控制补白的数量和位置。
操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。大多数改变格式状态的操纵符都是设置/复原成对的,一个操纵符用于设置新格式,另一个用于恢复正常格式。
默认情况下,bool
值输出为1(true
)或0(false
)。对流使用boolalpha
操纵符可以输出true
或false
,还原格式时使用noboolalpha
操纵符。
1 | cout << "default bool values: " << true << " " << false |
输出:
1 | default bool values: 1 0 |
默认情况下,整型值的输入输出使用十进制。可以使用hex
、oct
和dec
操纵符将其改为十六进制、八进制或改回十进制。
1 | cout << "default: " << 20 << " " << 1024 << endl; |
输出:
1 | default: 20 1024 |
hex
、oct
和dec
操纵符只影响整型运算对象,浮点值的表示形式不受影响。
默认情况下,在输出数值时,没有可见的标识指出当前使用的进制模式。如果需要输出八进制或十六进制值,应该使用showbase
操纵符。对流应用showbase
后,在输出结果中会显示进制,显示模式和指定整型常量进制的规范相同。
- 前导
0x
表示十六进制。 - 前导
0
表示八进制。 - 无前导字符表示十进制。
还原格式时使用noshowbase
操纵符。
1 | cout << showbase; // show the base when printing integral values |
输出:
1 | default: 20 1024 |
默认情况下,十六进制值(包括前导字符)以小写格式输出。使用uppercase
操纵符可以输出大写字母。还原格式时使用nouppercase
操纵符。
1 | cout << uppercase << showbase << hex |
输出:
1 | printed in hexadecimal: 0X14 0X400 |
浮点数的输出格式涉及三个方面:
- 输出精度(即输出多少个数字)。
- 十六进制、定点十进制或者科学记数法形式输出。
- 没有小数部分的浮点值是否输出小数点。
默认情况下,浮点值按六位数字精度输出;如果浮点值没有小数部分,则不输出小数点;根据浮点数的值选择输出为定点十进制或科学计数法形式:非常大或非常小的值输出为科学记数法形式,其他值输出为定点十进制形式。
默认情况下,精度控制输出的数字总位数。输出时,浮点值按照当前精度四舍五入而非截断。
调用IO对象的precision
成员或者使用setprecision
操纵符可以改变精度。
precision
成员是重载的。一个版本接受一个int
值,将精度设置为此值,并返回旧精度值。另一个版本不接受参数,直接返回当前精度值。setprecision
操纵符接受一个参数来设置精度。
setprecision
操纵符和其他接受参数的操纵符都定义在头文件iomanip中。
1 | // cout.precision reports the current precision value |
输出:
1 | Precision: 6, Value: 1.41421 |
定义在头文件iostream中的操纵符:
操纵符 | 解释 |
---|---|
boolalpha |
将true 和false 输出为字符串 |
* noboolalpha |
将true 和false 输出为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 | cout << "default format: " << 100 * sqrt(2.0) << '\n' |
输出:
1 | default format: 141.421 |
scientific
、fixed
和hexfloat
操纵符会改变流的精度含义。执行这些操纵符后,精度控制的将是小数点后面的数字位数,而默认情况下控制的是数字总位数。
默认情况下,当浮点值的小数部分为0时,不显示小数点。使用showpoint
操纵符可以强制输出小数点,noshowpoint
操纵符还原默认行为。
1 | cout << 10.0 << endl; // prints 10 |
按列输出时,通常需要非常精细地控制数据格式。
setw
指定下一个数字或字符串值的最小空间。left
表示左对齐输出。right
表示右对齐输出(默认格式)。internal
控制负数的符号位置,它左对齐符号,右对齐值,中间空间用空格填充。setfill
指定一个字符代替默认的空格进行补白。
setw
类似endl
,不改变输出流的内部状态,只影响下一次输出的大小。
1 | int i = -16; |
输出:
1 | i: -16next 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 | cin >> noskipws; // set cin so that it reads 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操作get
和put
可以读取和写入一个字符。
1 | char ch; |
有时读取完一个字符后才发现目前无法处理该字符,希望将其放回流中。标准库提供了三种方法退回字符。
peek
返回输入流中下一个字符的副本,但不会将其从流中删除。unget
使输入流向后移动,令最后读取的值回到流中。即使不知道最后从流中读取了什么值,也可以调用unget
。putback
是特殊版本的unget
,它退回从流中读取的最后一个值,但它接受一个参数,该参数必须与最后读取的值相同。
一般情况下,在读取下一个值之前,标准库保证程序可以退回最多一个值。
peek
和无参数的get
函数都以int
类型从输入流返回字符。这些函数使用int
的原因是可以返回文件尾标记。char
范围中的每个值都表示一个真实字符,因此没有额外的值可以表示文件尾。返回int
的函数先将要返回的字符转换为unsigned char
,再将结果提升为int
。因此即使字符集中有字符映射到负值,返回的int
也是正值。而标准库使用负值表示文件尾,这样就能保证文件尾与任何合法字符的值都不相同。头文件cstdio定义了一个名为EOF
的常量值,可以用它检测函数返回的值是否是文件尾。
1 | int ch; // use an int, not a char to hold the return from get() |
一个常见的编程错误是将get
或peek
函数的返回值赋给char
而非int
对象,但编译器不能发现这个错误。
1 | char ch; // using a char here invites disaster! |
当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 的默认值为文件尾。 |
get
和getline
函数接受相同的参数,它们的行为类似但不相同。两个函数都一直读取数据,直到遇到下列情况之一:
- 已经读取了size - 1个字符。
- 遇到了文件尾(
EOF
)。 - 遇到了分隔符。
两个函数的区别在于处理分隔符的方式:get
将分隔符留在输入流中作为下一个字符,而getline
读取并丢弃分隔符。两个函数都不会将分隔符保存在结果数组中。
读取流数据时的一个常见错误是忘记从流中删除分隔符。
一些操作可能从输入流中读取了未知个数的字节,使用gcount
函数可以确定上一次未格式化输入操作读取了多少字符。gcount
函数应该在任何后续未格式化输入操作前调用,将字符退回流的操作也属于未格式化输入操作。如果在调用gcount
前使用了peek
、unget
或putback
操作,则gcount
的返回值为0。
使用clear
、ignore
和sync
函数可以清空输入流中的数据。读到非法字符时,输入流将处于错误状态。为了继续获取输入数据,先调用clear
函数重置流的错误标记。再调用ignore
清空流中指定大小的数据,或者调用sync
直接清空流中所有数据。numeric_limits<streamsize>::max()
返回流的缓冲区大小。
1 | // 重置错误标志 |
流随机访问(Random Access to a Stream)
随机IO本质上是依赖于操作系统的。
为了支持随机访问,IO类型通过维护一个标记来确定下一次读写操作的位置。seek
函数用于移动标记,tell
函数用于获取标记。标准库实际上定义了两对seek
和tell
函数,一对用于输入流(后缀为g
,表示get),一对用于输出流(后缀为p
,表示put)。
操作 | 解释 |
---|---|
tellg() ,tellp |
返回一个输入流中(tellg )或输出流中(tellp )标记的当前位置。 |
seekg(pos) ,seekp(pos) |
在一个输入流或输出流中将标记重定位到给定的绝对地址。pos 通常是一个当前teelg 或tellp 返回的值。 |
seekp(off, from) ,seekg(off, from) |
在一个输入流或输出流中将标记定位到from 之前或之后off 个字符,from 可以是下列值之一:beg ,偏移量相对于流开始位置;cur ,偏移量相对于流当前位置;end ,偏移量相对于流结尾位置。 |
虽然标准库为所有流类型都定义了seek
和tell
函数,但它们是否有意义取决于流绑定到哪个设备。在大多数系统中,绑定到cin
、cout
、cerr
和clog
的流不支持随机访问。对这些流可以调用seek
和tell
函数,但在运行时会出现错误,流也会被置为无效状态。
从逻辑上考虑,seek
和tell
函数的使用范围如下:
- 可以对
istream
、ifstream
、istringstream
类型使用g
版本。 - 可以对
ostream
、ofstream
、ostringstream
类型使用p
版本。 - 可以对
iostream
、fstream
、stringstream
类型使用g
和p
版本。
一个流中只有一个标记——不存在独立的读标记和写标记。fstream
和stringstream
类型可以读写同一个流。在这些类型中,有单一的缓冲区用于保存读写的数据,同时标记也只有一个,表示缓冲区中的当前位置。标准库将两个版本的seek
和tell
函数都映射到这个标记。
由于流中只有一个标记,因此在切换读写操作时,必须使用seek
函数来重定位标记。
seek
函数有两个重载版本:一个版本使用绝对地址移动流标记;另一个版本使用指定位置和偏移量移动流标记。
1 | // set the marker to a fixed position |
参数new_position和offset的类型分别是pos_type
和off_type
,这两个类型都是机器相关的,定义在头文件istream和ostream中。pos_type
表示文件位置,而off_type
表示距离当前位置的偏移量,偏移量可以是正数也可以是负数。
tellg
和tellp
函数返回一个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
语句只能出现在catch
或catch
语句调用的函数之内。如果在异常处理代码之外的区域遇到了空throw
语句,编译器将调用terminate
函数。
重新抛出语句不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。如果catch
语句修改了其参数并重新抛出异常,则只有当catch
异常声明是引用类型时,程序对参数所做的改变才会被保留并继续传播。
1 | catch (my_error &eObj) |
使用省略号...
作为异常声明可以一次性捕获所有异常,这种处理代码被称为捕获所有异常(catch-all)的处理代码,可以与任意类型的异常相匹配。
1 | try |
catch(…)
通常与重新抛出语句一起使用。
如果catch(…)
与其他catch
语句一起使用,则catch(…)
必须位于最后,否则catch(…)
后面的catch
语句永远不会被匹配。
函数try语句块与构造函数(Function try Blocks and Constructors)
要想处理构造函数初始值列表抛出的异常,必须将构造函数写成函数try
语句块(function try block)的形式。函数try
语句块使得一组catch
语句可以同时处理构造函数体和构造函数初始化过程中的异常。
1 | template <typename T> |
函数try
语句块的catch
语句会在结尾处隐式地重新抛出异常,通知上层函数对象构造失败。上层函数需要继续处理该异常。
在初始化构造函数参数时发生的异常不属于函数try
语句块处理的范围。
noexcept异常说明(The noexcept Exception Specification)
在C++11中,可以通过提供noexcept
说明(noexcept specification)来指出某个函数不会抛出异常。
1 | void recoup(int) noexcept; // won't throw |
noexcept
说明的出现位置:
- 关键字
noexcept
位于函数的参数列表之后,尾置返回类型之前。 - 对于一个函数来说,
noexcept
说明必须同时出现在该函数的所有声明和定义语句中。 - 函数指针的声明和定义也可以指定
noexcept
。 - 在
typedef
或类型别名中不能使用noexcept
。 - 在成员函数中,关键字
noexcept
位于const
或引用限定符之后,final
、override
或虚函数的=0
之前。
编译器并不会在编译时检查noexcept
说明。如果一个函数在指定了noexcept
的同时又含有throw
语句或其他可能抛出异常的操作,仍然会通过编译(个别编译器可能会提出警告)。
1 | // this function will compile, even though it clearly violates its exception specification |
一旦noexcept
函数抛出异常,程序会调用terminate
函数终止运行(该过程是否执行栈展开未作规定)。因此noexcept
可以用于两种情况:
- 确认函数不会抛出异常。
- 不知道该如何处理函数抛出的异常。
指明某个函数不会抛出异常可以让调用者不必再考虑异常处理操作。
早期的C++版本设计了一套更详细的异常说明方案。函数可以使用一个关键字throw
,后面跟上用括号包围的异常类型列表,用于指定函数可能抛出的异常类型。关键字throw
出现的位置与C++11的noexcept
相同。该方案在C++11中被取消。但如果一个函数被声明为throw()
的,则也说明该函数不会抛出异常。
1 | void recoup(int) noexcept; // recoup doesn't throw |
noexcept
说明符接受一个可选的实参,该实参必须能转换为bool
类型。如果实参为true
,则函数不会抛出异常;如果实参为false
,则函数可能抛出异常。
1 | void recoup(int) noexcept(true); // recoup won't 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 | // both recoup and pf1 promise not to throw |
如果一个虚函数是noexcept
的,则后续派生出来的虚函数必须也是noexcept
的。如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许,也可以禁止抛出异常。
1 | class Base |
编译器合成拷贝控制成员时,也会生成一个异常声明。如果所有的成员和基类操作都含有noexcept
说明,则合成成员也是noexcept
的。
异常类层次(Exception Class Hierarchies)
标准库异常类的继承体系:
exception
类型只定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what
的虚成员。what
函数返回一个const char*
,指向一个以NULL
结尾的字符数组,并且不会抛出异常。
exception
、bad_cast
和bad_alloc
类型定义了默认构造函数。runtime_error
和logic_error
类型没有默认构造函数,但是有一个接受C风格字符串或string
类型实参的构造函数,该实参通常用于提供错误信息。what
函数返回用于初始化异常对象的错误信息。
实际编程中通常会自定义exception
(或者exception
的标准库派生类)的派生类以扩展其继承体系。这些面向具体应用的异常类表示了与应用相关的异常状态。
命名空间(Namespaces)
大型应用程序通常会使用多个独立开发的库,其中某些名字可能会相互冲突。多个库将名字放置在全局命名空间中会产生命名空间污染(namespace pollution)。
命名空间分割了全局命名空间,其中每个命名空间都是一个作用域。
命名空间定义(Namespace Definitions)
命名空间的定义包含两部分:关键字namespace
和随后的命名空间名字。在命名空间名字后面是一系列由花括号包围的声明和定义。能出现在全局作用域中的声明就也能出现在命名空间中。
1 | namespace cplusplus_primer |
命名空间作用域后面不需要分号结束。
和其他名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。命名空间可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。
每个命名空间都是一个作用域,不同命名空间内可以有相同名字的成员。
定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须明确指出所使用的名字属于哪个命名空间。
1 | cplusplus_primer::Query q = cplusplus_primer::Query("hello"); |
命名空间的定义可以是不连续的。
1 | namespace nsp |
如果之前没有名为nsp的命名空间定义,则上述代码创建一个新的命名空间;否则,上述代码打开已经存在的命名空间定义并为其添加新的成员声明。
利用命名空间不连续的特性可以将几个独立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似管理自定义类和函数的方式。
- 命名空间的一部分成员用于定义类,以及声明作为类接口的函数和对象。这些成员应该放置在头文件中。
- 命名空间成员的定义部分放置在另外的源文件中。源文件需要包含对应的头文件。
程序中的某些实体只能定义一次,如非内联函数、静态数据成员等,命名空间中定义的名字也需要满足该要求。
1 | // ---- Sales_data.h---- |
通常情况下,#include
不应该出现在命名空间内部。否则头文件中的所有名字都会被定义为该命名空间的成员。
定义多个类型不相关的命名空间时应该使用单独的文件分别表示每个类型。
可以在命名空间的外部定义该命名空间的成员。命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间。
1 | // namespace members defined outside the namespace must use qualified names |
模板特例化必须定义在原始模板所属的命名空间中。可以在命名空间内部添加模板特例化声明,而在外部对其进行定义。
1 | // we must declare the specialization as a member of std |
全局作用域中定义的名字被隐式添加到全局命名空间(global namespace)中。全局命名空间以隐式方式声明,在所有程序中都存在。
作用域运算符::
可以用于全局命名空间的成员。因为全局命名空间是隐式声明的,所以它并没有名字。
1 | ::member_name |
命名空间可以嵌套。嵌套的命名空间同时也是一个嵌套的作用域,它嵌套在外层命名空间的作用域内。内层命名空间声明的名字会隐藏外层命名空间的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码在访问时需要在名字前添加限定符。
C++11新增了内联命名空间(inline namespace)。和一般的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。定义内联命名空间的方式是在namespace
前添加关键字inline
。inline
必须出现在该命名空间第一次定义的地方。
1 | inline namespace FifthEd |
当应用程序的代码在两次发布之间发生了改变时,通常会使用内联命名空间。
1 | namespace FourthEd |
因为FifthEd是内联的,所以形如cplusplus_primer::
的代码可以直接获得FifthEd的成员。如果想使用早期版本,则必须加上完整的外层命名空间名字。
未命名的命名空间(unnamed namespace)指关键字namespace
后紧跟以花括号包围的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,直到程序结束才销毁。
一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字在每个包含该头文件的文件中对应不同实体。
定义在未命名的命名空间中的名字可以直接使用,不能对其使用作用域运算符。
定义在未命名的命名空间中的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在最外层作用域中,则该命名空间中的名字必须要与全局作用域中的名字有所区别。
1 | int i; // global declaration for i |
在标准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 | // namespace A and function f are defined at global scope |
当命名空间被注入到其外层作用域之后,该命名空间中定义的名字可能会与其外层作用域的成员冲突。这种冲突允许存在,但是要想使用冲突的名字,就必须明确指出名字的版本。
1 | namespace blip |
头文件如果在其顶层作用域中使用using
声明或using
指示,则会将名字注入到包含该头文件的所有文件中。通常,头文件只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用using
声明或using
指示。
相比于使用using
指示,在程序中对命名空间中的每个成员分别使用using
声明效果更好。
- 如果程序使用了多个不同的库,而这些库中的名字通过
using
指示变得可见,则全局命名空间污染问题将重新出现。 using
指示引发的二义性错误只有在使用了冲突名字的地方才会被发现。而using
声明引发的二义性错误在声明处就能发现。
建议在命名空间本身的实现文件中使用using
指示。
类、命名空间与作用域(Classes,Namespaces,and Scope)
对命名空间内部名字的查找遵循常规查找规则:由内向外依次查找每个外层作用域。只有位于开放的块中且在使用点之前声明的名字才会被考虑。
1 | namespace A |
对于位于命名空间中的类来说,名字的常规查找规则依然适用:当成员函数使用某个名字时,首先在该成员中查找,然后在类(包括基类)中查找,接着在外层作用域中查找。
可以从函数的限定名推断出名字查找时检查作用域的顺序,限定名以相反的顺序指出被查找的作用域。
命名空间中名字的隐藏规则有一个例外:传递给函数一个类类型的对象、指向类的引用或指针时,除了在常规作用域查找名字外,还会查找实参类所属的命名空间。该例外允许概念上作为类接口一部分的非成员函数无须单独的using
声明就能被程序使用。
1 | std::string s; |
标准库定义的move
和forward
模板函数接受一个右值引用形参,可以匹配任何类型。如果应用程序也定义了一个接受单一参数的move
和forward
函数,则不管形参是什么类型,都会与标准库的版本冲突。对于这两个函数来说,冲突大多是无意的,因此建议使用它们的含有限定语的完整版本(即std::move
、std::forward
)。
如果一个未声明的类或函数第一次出现在友元声明中,则会被认定是离它最近的外层命名空间的成员。
重载与命名空间(Overloading and Namespaces)
using
声明和using
指示能将某些函数添加到候选函数集。
确定候选函数集时,会在函数的每个实参类(以及实参类的基类)所属的命名空间中搜索候选函数。这些命名空间中所有与被调用函数同名的函数都会被添加到候选集当中,即使其中某些函数在调用语句处不可见也是如此。
using
声明语句声明的是一个名字,而非一个特定的函数。一个using
声明囊括了重载函数的所有版本以确保不违反命名空间的接口。
1 | using NS::print(int); // error: cannot specify a parameter list |
一个using
声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。如果using
声明出现在局部作用域中,则引入的名字会隐藏外层作用域的相关声明。如果using
声明所在的作用域中已经有一个函数与引入的函数同名且形参列表相同,则该using
声明会引发错误。除此之外,using
声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模。
using
指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域中的函数同名,则命名空间的函数会被添加到重载集合中。
1 | namespace libs_R_us |
与using
声明不同,using
指示引入一个与已有函数形参列表完全相同的函数并不会引发错误。但需要明确指出调用的是命名空间中的函数版本还是当前作用域中的版本。
如果存在多个using
指示,则来自每个命名空间的名字都会成为候选函数集的一部分。
多重继承与虚继承(Multiple and Virtual Inheritance)
多重继承(Multiple inheritance)是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。
多重继承(Multiple Inheritance)
派生类的派生列表中可以包含多个基类。每个基类都包含一个可选的访问说明符。和单继承相同,如果访问说明符被省略,则关键字class
对应的默认访问说明符是private
,关键字struct
对应的是public
。
1 | class Bear : public ZooAnimal { /* ... */ }; |
和单继承相同,多重继承的派生列表也只能包含已经被定义过的类,且这些类不能是final
的。
多重继承关系中,派生类对象包含每个基类的子对象。
构造一个多重继承的派生类对象将同时构造并初始化它的所有基类子对象。
1 | // explicitly initialize both base classes |
派生类的构造函数初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类的出现顺序一致,与构造函数初始值列表中基类的顺序无关。
C++11允许派生类从它的一个或多个基类中继承构造函数,但如果从多个基类中继承了相同的构造函数(即形参列表完全相同),程序会产生错误。
1 | struct Base1 |
如果一个类从它的多个基类中继承了相同的构造函数,则必须为该构造函数定义其自己的版本。
1 | struct D2: public Base1, public Base2 |
和单继承相同,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行这些操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动处理其基类部分。在合成版本的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。
类型转换与多个基类(Conversions and Multiple Base Classes)
多重继承和单继承相同,某个可访问基类的指针或引用可以直接指向派生类对象。
编译器不会在派生类向基类的几种转换中进行比较和选择。
1 | void print(const Bear&); |
和单继承相同,对象、指针和引用的静态类型决定了我们可以使用的成员。
多重继承下的类作用域(Class Scope under Multiple Inheritance)
在单继承中,派生类的作用域嵌套在直接基类和间接基类的作用域中。名称查找沿着继承体系自底向上进行,直到找到所需的名字。派生类的名字会隐藏基类的同名成员。在多重继承中,相同的查找过程在所有基类中同时进行。如果名字在多个基类中都被找到,则会产生二义性错误。
派生类可以从多个基类中分别继承名字相同的成员,但是在使用该名字时必须明确指出其版本。避免潜在二义性的最好方法是在派生类中定义新的版本。
虚继承(Virtual Inheritance)
尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再次间接继承该类。
默认情况下,派生类含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中会包含该类的多个子对象。这种默认情况对某些类并不适用。例如iostream
,它直接继承自istream
和ostream
,而istream
和ostream
都继承自base_ios
,所以iostream
继承了base_ios
两次。如果iostream
对象包含base_ios
的两份拷贝,则无法在同一个缓冲区中进行读写操作。
虚继承可以让某个类共享它的基类,其中共享的基类子对象称为虚基类(virtual base class)。在该机制下,不论虚基类在继承体系中出现了多少次,派生类都只包含唯一一个共享的虚基类子对象。
通常情况下,使用虚继承的类层次是由一个人或一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个是虚基类,况且新基类的开发者也无法改变已存在的类体系。
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
指定虚基类的方式是在派生列表中添加关键字virtual
。
1 | // the order of the keywords public and virtual is not significant |
如果某个类指定了虚基类,则该类的派生仍按照常规方式进行。
1 | class Panda : public Bear, public Raccoon, public Endangered { /* ... */ }; |
不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。
因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,而且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则也可以直接访问该成员。但如果成员被多个基类覆盖,则一般情况下派生类必须为该成员定义新的版本。例如,假设类B定义了一个名为X的成员,D1和D2都从B虚继承得到,D继承了D1和D2。则在D的作用域中,X通过D的两个基类都是可见的。如果通过D的对象使用X,则有三种可能性:
- 如果D1和D2中都没有X的定义,则X会被解析为B的成员,此时不存在二义性。
- 如果D1和D2中的某一个定义了X,派生类的X会比共享虚基类B的X优先级更高,此时同样没有二义性。
- 如果D1和D2都定义了X,则直接访问X会产生二义性问题。
构造函数与虚继承(Constructors and Virtual Inheritance)
在虚派生中,虚基类是由最低层的派生类初始化的。如果按普通规则处理,虚基类将会在多条继承路径上被重复初始化。
继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。只要能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。即使虚基类不是派生类的直接基类,构造函数也可以进行初始化。
1 | Bear::Bear(std::string name, bool onExhibit) |
构造含有虚基类的对象时,首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,之后再按照直接基类在派生列表中出现的顺序依次对其初始化。
一个类可以有多个虚基类,此时这些虚子对象按照它们在派生列表中出现的顺序依次构造。
第十九章 特殊工具与技术
控制内存分配(Controlling Memory Allocation)
重载new和delete(Overloading new and delete)
使用new
表达式时,实际执行了三步操作:
new
表达式调用名为operator new
(或operator new[]
)的标准库函数。该函数分配一块足够大、原始、未命名的内存空间以便存储特定类型的对象(或对象数组)。- 编译器调用对应的构造函数构造这些对象并初始化。
- 对象被分配了空间并构造完成,返回指向该对象的指针。
使用delete
表达式时,实际执行了两步操作:
- 对指针所指向的对象(或对象数组)执行对应的析构函数。
- 编译器调用名为
operator delete
(或operator delete[]
)的标准库函数释放内存空间。
如果程序希望控制内存分配的过程,则需要定义自己的operator new
和operator delete
函数。编译器会用自定义版本替换标准库版本。
程序可以在全局作用域中定义operator new
和operator delete
函数,也可以将其定义为成员函数。编译器发现new
或delete
表达式后,将在程序中查找可供调用的operator
函数。如果被分配或释放的对象是类类型,编译器会先在类及其基类的作用域中查找。如果该类含有operator
成员,则表达式会调用这些成员。否则编译器会继续在全局作用域查找。如果找到自定义版本,则使用该版本的函数。如果没找到,则使用标准库定义的版本。
可以使用作用域运算符令new
或delete
表达式忽略定义在类中的函数,直接执行全局作用域版本。
标准库定义了operator new
和operator delete
函数的8个重载版本,其中前4个版本可能抛出bad_alloc
异常,后4个版本不会抛出异常。重载这些运算符时,必须使用关键字noexcept
指定其不抛出异常。
1 | // these versions might throw an exception |
nothrow_t
类型是定义在头文件new中的一个结构体,这个类型不包含任何成员。头文件new还定义了一个名为nothrow
的const
对象,用户可以通过这个对象请求new
的非抛出版本。
将operator
函数定义为类的成员时,它们是隐式静态的,无须显式地声明static
。因为operator new
用在对象构造之前,operator delete
用在对象销毁之后,所以它们必须是静态成员,而且不能操纵类的任何数据成员。
operator new
和operator new[]
函数的返回类型必须是void*
,第一个形参的类型必须是size_t
且不能有默认实参。编译器调用operator new
时,用存储指定类型对象所需的字节数初始化size_t
形参;调用operator new[]
时,传入函数的则是存储数组中所有元素所需的空间。
自定义operator new
函数时可以为它提供额外的形参,用到这些自定义函数的new
表达式必须使用new
的定位形式传递参数。下面这种形式的new
函数只供标准库使用,不能被用户重定义:
1 | void *operator new(size_t, void*); // this version may not be redefined |
operator delete
和operator delete[]
函数的返回类型必须是void
,第一个形参的类型必须是void*
。函数被调用时,编译器会用指向待释放内存的指针来初始化void*
形参。
将operator delete
或operator delete[]
定义为类的成员时,可以包含另一个类型为size_t
的形参。该形参的初始值是第一个形参所指向对象的字节数。size_t
形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete
的字节数会因待删除指针所指向对象的动态类型不同而有所区别。实际运行的operator delete
函数版本也由对象的动态类型决定。
malloc
函数接受一个表示待分配字节数的size_t
参数,返回指向分配空间的指针,或者返回0以表示分配失败。free
函数接受一个void*
参数,它是malloc
返回的指针的副本,free
将相关内存返回给系统。调用free(0)
没有任何意义。
1 | void *operator new(size_t size) |
定位new表达式(Placement new Expressions)
在C++的早期版本中,allocator
类还不是标准库的一部分。如果程序想分开内存分配和初始化过程,需要直接调用operator new
和operator delete
函数。它们类似allocator
类的allocate
和deallocate
成员,负责分配或释放内存空间,但不会构造或销毁对象。
不能使用allocator
类的construct
函数在operator new
分配的内存空间中构造对象,而应该使用定位new
表达式构造。
1 | new (place_address) type |
其中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 | dynamic_cast<type*>(e) |
其中type是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,e必须是一个有效指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。在所有形式中,e的类型必须符合以下条件之一:
- e是type的公有派生类。
- e是type的公有基类。
- e和type类型相同。
如果条件符合,则类型转换成功,否则转换失败。转换失败可能有两种结果:
如果
dynamic_cast
语句的转换目标是指针类型,则结果为0。1
2
3
4
5
6
7
8if (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
12void 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 | Derived *dp = new Derived; |
typeid
应该作用于对象。当typeid
作用于指针时,返回的结果是该指针的静态编译类型。
1 | // test always fails: the type of bp is pointer to Base |
只有当类型含有虚函数时,编译器才会对typeid
的表达式求值以确定返回类型。对于typeid(*p)
,如果指针p所指向的类型不包含虚函数,则p可以是一个无效指针。否则*p
会在运行期间求值,此时p必须是一个有效指针。如果p是空指针,typeid(*p)
会抛出bad_typeid
异常。
使用RTTI(Using RTTI)
使用RTTI可以为具有继承关系的类实现相等运算符。
相等运算符的形参是基类的引用。
1 | class Base |
使用typeid
检查两个运算对象的类型是否一致,类型一致才会继续判断每个数据成员的取值是否相同。
1 | bool operator==(const Base &lhs, const Base &rhs) |
每个类定义的equal
函数负责比较类型自己的数据成员。equal
函数的形参都是基类的引用,但是在比较之前需要先把运算对象转换成自己的类型。
1 | bool Derived::equal(const Base &rhs) const |
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
6enum class open_modes
{
input,
output,
append
};不限定作用域的枚举(unscoped enumeration)。定义时省略关键字
class
(或struct
),枚举类型名字是可选的。1
2
3
4
5
6
7
8
9
10
11
12
13
14C++// unscoped enumeration
enum color
{
red,
yellow,
green
};
// unnamed, unscoped enum
enum
{
floatPrec = 6,
doublePrec = 10,
double_doublePrec = 10
};如果枚举是未命名的,则只能在定义该枚举时一同定义它的对象。
在限定作用域的枚举类型中,枚举成员的名字遵循常规作用域规则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。
1 | enum color { red, yellow, green }; // unscoped enumeration |
默认情况下,枚举值从0开始,依次加1。也可以直接为枚举成员指定特定的值。
1 | enum class intTypes |
枚举值可以不唯一。如果没有显式提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。
枚举成员是const
的,因此在初始化枚举成员时提供的初始值必须是常量表达式。
可以在任何需要常量表达式的地方使用枚举成员。如:
- 定义枚举类型的
constexpr
变量。 - 将枚举类型对象作为
switch
语句的表达式,而将枚举值作为case
标签。 - 将枚举类型作为非类型模板形参使用。
- 在类的定义中初始化枚举类型的静态数据成员。
初始化枚举对象或者给枚举对象赋值时,必须使用该类型的一个枚举成员或者该类型的另一个对象。即使某个整型值恰好与枚举成员的值相等,也不能用其初始化枚举对象。
1 | open_modes om = 2; // error: 2 is not of type open_modes |
不限定作用域的枚举类型对象或枚举成员能自动转换成整型。
1 | int i = color::red; // ok: unscoped enumerator implicitly converted to int |
枚举是由某种整数类型表示的。C++11中,可以在枚举名字后面指定用来表示枚举成员的整型类型。
1 | enum intValues : unsigned long long |
如果没有指定枚举的潜在类型,则默认情况下限定作用域的枚举成员类型是int
。不限定作用域的枚举成员不存在默认类型。
C++11中可以提前声明枚举。枚举的前置声明必须指定(无论隐式或显式)其成员的类型。
1 | // forward declaration of unscoped enum named intValues |
类成员指针(Pointer to Class Member)
成员指针(pointer to member)是指可以指向类的非静态成员的指针。
成员指针的类型包括类的类型和成员的类型。初始化成员指针时,会令其指向类的某个成员,但是不指定该成员所属的对象。直到使用成员指针时,才提供成员所属的对象。
数据成员指针(Pointers to Data Members)
声明成员指针时必须在*
前添加classname::
以表示当前定义的指针可以指向classname的成员。
1 | class Screen |
初始化或者给成员指针赋值时,需要指定它所指向的成员。
1 | pdata = &Screen::contents; |
成员指针使用.*
和->*
来获得其指向对象的成员。
1 | Screen myScreen, *pScreen = &myScreen; |
常规的访问控制规则对成员指针同样有效。数据成员一般是私有的,因此通常不能直接获得数据成员的指针。如果类希望外部代码能访问它的私有数据成员,可以定义一个函数,令其返回指向私有成员的指针。
成员函数指针(Pointers to Member Functions)
类似于其他函数指针,指向成员函数的指针也需要指定目标函数的返回类型和形参列表。如果成员函数是const
成员或引用成员,则指针也必须包含const
或引用限定符。
1 | // pmf is a pointer that can point to a Screen member function that is const |
如果成员函数存在重载问题,则必须显式声明指针指向的函数类型。
1 | char (Screen::*pmf2)(Screen::pos, Screen::pos) const; |
和普通函数指针不同,在成员函数和指向该成员的指针之间不存在自动转换规则。
1 | // pmf points to a Screen member that takes no arguments and returns char |
成员函数指针使用.*
和->*
来调用类的成员函数。
1 | Screen myScreen, *pScreen = &myScreen; |
可以使用类型别名来增强含有成员指针的代码的可读性。
将成员函数用作可调用对象(Using Member Functions as Callable Objects)
成员指针不是一个可调用对象,不支持函数调用运算符。
1 | auto fp = &string::empty; // fp points to the string empty function |
从成员函数指针获取可调用对象的一种方法是使用标准库模板function
。
1 | function<bool (const string&)> fcn = &string::empty; |
定义一个function
对象时,必须指定该对象所能表示的函数类型(即可调用对象的形式)。如果可调用对象是一个成员函数,则第一个形参必须表示该成员是在哪个对象上执行的。
使用标准库功能mem_fn
(定义在头文件functional中)可以让编译器推断成员的类型。和function
一样,mem_fn
可以从成员指针生成可调用对象。但mem_fn
可以根据成员指针的类型推断可调用对象的类型,无须显式指定。
1 | find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); |
mem_fn
生成的可调用对象可以通过对象和指针调用。
1 | auto f = mem_fn(&string::empty); // f takes a string or a string* |
嵌套类(Nested Classes)
一个类可以定义在另一个类的内部,前者被称为嵌套类或嵌套类型(nested type)。嵌套类通常用于定义作为实现部分的类。
外层类的对象和嵌套类的对象是相互独立的。在嵌套类对象中不包含任何外层类定义的成员,在外层类对象中也不包含任何嵌套类定义的成员。
嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。
外层类对嵌套类的成员没有特殊的访问权限,嵌套类对外层类的成员也没有特殊的访问权限。
嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类的访问说明符决定。
嵌套类必须声明在类的内部,但是可以定义在类的内部或外部。在外层类之外定义嵌套类时,必须用外层类的名字限定嵌套类的名字。
1 | class TextQuery |
在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型。
union:一种节省空间的类(union: A Space-Saving Class)
联合(union)是一种特殊的类。一个联合可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。给联合的某个成员赋值之后,其他成员会变为未定义状态。分配给联合对象的存储空间至少要能容纳它的最大数据成员。
联合不能包含引用类型的成员。在C++11中,含有构造函数或析构函数的类类型也可以作为联合的成员类型。
联合可以为其成员指定public
、protected
和private
等保护标记。默认情况下,联合的成员都是公有的。
联合可以定义包括构造函数和析构函数在内的成员函数。但是由于联合既不能继承自其他类,也不能作为基类使用,所以在联合中不能含有虚函数。
定义联合时,首先是关键字union
,随后是该联合的名字(可选)以及在花括号内的一组成员声明。
1 | // objects of type Token have a single member, which could be of any of the listed types |
默认情况下,联合是未初始化的。可以像显式初始化聚合类一样显式初始化联合,提供的初始值会被用于初始化第一个成员。
1 | Token first_token = { 'a' }; // initializes the cval member |
可以使用通用的成员访问运算符访问联合对象的成员。
1 | last_token.cval = 'z'; |
匿名联合(anonymous union)是一个未命名的联合,并且在右花括号和分号之间没有任何声明。一旦定义了一个匿名联合,编译器就会自动地为该联合创建一个未命名的对象。在匿名联合的定义所在的作用域内,该联合的成员都是可以直接访问的。
1 | union |
匿名联合不能包含protected
和private
成员,也不能定义成员函数。
C++的早期版本规定,在联合中不能含有定义了构造函数或拷贝控制成员的类类型成员。C++11取消了该限制。但是如果联合的成员类型定义了自己的构造函数或拷贝控制成员,该联合的用法会比只含有内置类型成员的联合复杂得多。
- 当联合只包含内置类型的成员时,可以使用普通的赋值语句改变联合的值。但是如果想将联合的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须构造或析构该类类型的成员。
- 当联合只包含内置类型的成员时,编译器会按照成员顺序依次合成默认构造函数或拷贝控制成员。但是如果联合含有类类型成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器会为该联合合成对应的版本并将其声明为删除的。
对于联合来说,构造或销毁类类型成员的操作非常复杂。通常情况下,可以把含有类类型成员的联合内嵌在另一个类中,这个类可以管理并控制与联合的类类型成员相关的状态转换。
局部类(Local Classes)
类可以定义在某个函数的内部,这种类被称为局部类。局部类定义的类型只能在定义它的作用域内可见。
局部类的所有成员(包括成员函数)都必须完整定义在类的内部,因此局部类的作用与嵌套类相比相差很远。
局部类中不允许声明静态数据成员。
局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员,不能使用普通局部变量。
1 | int a, val; |
常规的访问保护规则对于局部类同样适用。外层函数对局部类的私有成员没有任何访问特权。局部类可以将外层函数声明为友元。
可以在局部类的内部再嵌套一个类。此时嵌套类的定义可以出现在局部类之外,不过嵌套类必须定义在与局部类相同的作用域中。
1 | void foo() |
局部类内的嵌套类也是一个局部类,必须遵循局部类的各种规定。
固有的不可移植的特性(Inherently Nonportable Features)
位域(Bit-fields)
类可以将其非静态数据成员定义成位域,在一个位域中含有一定数量的二进制位。当程序需要向其他程序或硬件设备传递二进制数据时,通常会使用位域。
位域的声明形式是在成员名字之后紧跟一个冒号和一个常量表达式,该表达式用于指定成员所占的二进制位数。
位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以通常情况下使用无符号类型保存位域。位域类型的大小不能小于位域结构的总大小。
1 | struct Descriptor |
定义位域时建议结合#pragma pack
指令将结构体对齐值修改为1,防止数据结构错位。
1 | // 保存原始对齐值,设置新对齐 |
位域成员按定义顺序在内存中由低地址向高地址排列,具体布局与机器相关。
取地址符&
不能作用于位域,因此任何指针都无法指向类的位域。
如果可能的话,类内部连续定义的位域会压缩在同一整数的相邻位,从而提供存储压缩。
访问位域的方式与访问类的其他数据成员的方式类似。操作超过1位的位域时,通常会使用内置的位运算符。
1 | File &File::open(File::modes m) |
volatile限定符(volatile Qualifier)
当对象的值可能在程序的控制或检测之外被改变时(如子线程),应该将该对象声明为volatile
。关键字volatile
的作用是告知编译器不要优化这样的对象。
volatile
的确切含义与机器有关,只能通过查阅编译器文档来理解。要想让一个使用了volatile
的程序在移植到新机器或新编译器后仍然有效,通常需要对该程序进行一些修改。
volatile
的用法和const
类似,都是对类型的额外修饰。二者相互之间并没有影响。
1 | volatile int display_register; // int value that might change |
类可以将成员函数定义为volatile
的。volatile
对象只能调用volatile
成员函数。
volatile
和指针的关系类似const
。可以声明volatile
指针、指向volatile
对象的指针和指向volatile
对象的volatile
指针。
1 | volatile int v; // v is a volatile int |
不能使用合成的拷贝/移动构造函数和赋值运算符初始化volatile
对象或者给volatile
对象赋值。合成的成员接受的形参类型是非volatile
常量引用,不能把非volatile
引用绑定到volatile
对象上。
如果类需要拷贝、移动或赋值它的volatile
对象,则必须自定义拷贝或移动操作。
1 | class Foo |
链接指示:extern "C"(Linkage Directives:extern "C")
C++程序有时需要调用使用其他语言编写的函数,最常见的是调用C语言函数。其他语言中的函数名字也必须在C++中进行声明。对于这些函数,编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别。C++使用链接指示指出任意非C++函数所用的语言。
链接指示有单个形式和复合形式,其不能出现在类定义或函数定义的内部。同样的链接指示必须出现在函数的每个声明处。
1 | // illustrative linkage directives that might appear in the C++ header <cstring> |
链接指示包含关键字extern
、字符串字面值常量和一个函数声明。其中的字符串字面值常量指出了编写函数所用的语言。
复合形式的链接指示可以应用于整个头文件。当一个#include
指示被放置在复合链接指示的花括号中时,头文件中的所有函数声明都会被认为是由链接指示的语言编写的。链接指示可以嵌套,因此如果头文件包含自带链接指示的函数,该函数不会受到影响。
1 | // compound-statement linkage directive |
C++从C语言继承的标准库函数可以定义成C函数,但并非必须。选择使用C还是C++实现C标准库,是由每个C++实现决定的。
编写函数所使用的语言是函数类型的一部分。因此对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示,而且指向这类函数的指针也必须使用与函数本身一样的链接指示。
1 | // pf points to a C function that returns void and takes an int |
指向C函数的指针与指向C++函数的指针是不同的类型,两者不能相互赋值或初始化(少数C++编译器支持这种赋值操作并将其视为对语言的扩展,但是从严格意义上来说它是非法的)。
1 | void (*pf1)(int); // points to a C++ function |
链接指示不仅对函数本身有效,对作为返回类型或形参类型的函数指针也有效。所以如果希望给C++函数传入指向C函数的指针,必须使用类型别名。
1 | // f1 is a C function; its parameter is a pointer to a C function |
通过链接指示定义函数,可以令C++函数在其他语言编写的程序中可用。编译器会为该函数生成适合于指定语言的代码。
1 | // the calc function can be called from C programs |
如果需要在C和C++中编译同一个源文件,可以在编译C++版本时使用预处理定义__cplusplus
。
1 |
|
链接指示与重载函数的相互作用依赖于目标语言。C语言不支持函数重载,所以一个C链接指示只能用于说明一组重载函数中的某一个。
1 | // error: two extern "C" functions with the same name |