CPP Primer 知识点笔记
CPP Primer 知识点笔记
第1章 开始
最简单的main函数:
1 | int main() { |
C++包含两种注释,注释界定符/**/通常用于多行注释,而双斜杠//通常用于单行或半行注释。
1 |
|
熟悉编译器
g++:
- 编译:
g++ --std=c++11 ch01.cpp -o main - 运行:
./prog1 - 查看运行状态:
echo $? - 编译多个文件:
g++ ch2.cpp Sales_item.cc -o main
输入 g++ --help,查看编译器选项:
1 | Usage: g++ [options] file... |
输入 g++ -v --help可以看到更完整的指令。
例如还有些常用的:
1 | -h FILENAME, -soname FILENAME: Set internal name of shared library |
获得程序状态:
- windows:
echo %ERRORLEVEL% - UNIX:
echo $?
IO
#include <iostream>std::cout << "hello"std::cin >> v1
记住>>和<<返回的结果都是左操作数,也就是输入流和输出流本身。
endl:这是一个被称为操纵符(manipulator)的特殊值,效果是结束当前行,并将设备关联的缓冲区(buffer)中的内容刷到设备中。
UNIX和Mac下键盘输入文件结束符:ctrl+d,Windows下:ctrl+z
头文件:类的类型一般存储在头文件中,标准库的头文件使用<>,非标准库的头文件使用""。申明写在.h文件,定义实现写在.cpp文件。
避免多次包含同一头文件:
1 |
|
成员函数(类方法):使用.调用。
命名空间(namespace):使用作用域运算符::调用。
注释
- 单行注释:
// - 多行注释:
/**/。编译器将/*和*/之间的内容都作为注释内容忽略。注意不能嵌套。
1 |
|
while语句
循环执行,(直到条件(condition)为假。
for语句
循环头由三部分组成:
- 一个初始化语句(init-statement)
- 一个循环条件(condition)
- 一个表达式(expression)
使用文件重定向
./main <infile >outfile
第2章 变量和基本类型(简)
基本内置类型
基本算数类型:
| 类型 | 含义 | 最小尺寸 |
|---|---|---|
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位有效数字 |
如何选择类型
- 1.当明确知晓数值不可能是负数时,选用无符号类型;
- 2.使用
int执行整数运算。一般long的大小和int一样,而short常常显得太小。除非超过了int的范围,选择long long。 - 3.算术表达式中不要使用
char或bool。 - 4.浮点运算选用
double。
类型转换
- 非布尔型赋给布尔型,初始值为0则结果为false,否则为true。
- 布尔型赋给非布尔型,初始值为false结果为0,初始值为true结果为1。
字面值常量
-
一个形如
42的值被称作字面值常量(literal)。-
整型和浮点型字面值。
-
字符和字符串字面值。
-
使用空格连接,继承自C。
-
字符字面值:单引号,
'a' -
字符串字面值:双引号,
"Hello World"" -
分多行书写字符串。
1
2std:cout<<"wow, a really, really long string"
"literal that spans two lines" <<std::endl;
-
-
转义序列。
\n、\t等。 -
布尔字面值。
true,false。 -
指针字面值。
nullptr
-
字符串型实际上时常量字符构成的数组,结尾处以
'\0'结束,所以字符串类型实际上长度比内容多1。
变量
变量提供一个具名的、可供程序操作的存储空间。 C++中变量和对象一般可以互换使用。
变量定义(define)
- 定义形式:类型说明符(type specifier) + 一个或多个变量名组成的列表。如
int sum = 0, value, units_sold = 0; - 初始化(initialize):对象在创建时获得了一个特定的值。
- 初始化不是赋值!:
- 初始化 = 创建变量 + 赋予初始值
- 赋值 = 擦除对象的当前值 + 用新值代替
- 列表初始化:使用花括号
{},如int units_sold{0}; - 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化。
- 建议初始化每一个内置类型的变量。
变量的声明(declaration) vs 定义(define)
- 为了支持分离式编译,
C++将声明和定义区分开。声明使得名字为程序所知。定义负责创建与名字关联的实体。 - extern:只是说明变量定义在其他地方。
- 只声明而不定义: 在变量名前添加关键字
extern,如extern int i;。但如果包含了初始值,就变成了定义:extern double pi = 3.14; - 变量只能被定义一次,但是可以多次声明。定义只出现在一个文件中,其他文件使用该变量时需要对其声明。
- 名字的作用域(namescope)
{}- 第一次使用变量时再定义它。
- 嵌套的作用域
- 同时存在全局和局部变量时,已定义局部变量的作用域中可用
::reused显式访问全局变量reused。 - 但是用到全局变量时,尽量不适用重名的局部变量。
- 同时存在全局和局部变量时,已定义局部变量的作用域中可用
变量命名规范
- 需体现实际意义
- 变量名用小写字母
- 自定义类名用大写字母开头:Sales_item
- 标识符由多个单词组成,中间须有明确区分:student_loan或studentLoan,不要用studentloan。
左值和右值
- 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
- 右值(r-value)只能出现在赋值语句的右边,比如常量。
复合类型
引用
一般说的引用是指的左值引用
- 引用:引用是一个对象的别名,引用类型引用(refer to)另外一种类型。如
int &refVal = val;。 - 引用必须初始化。
- 引用和它的初始值是绑定bind在一起的,而不是拷贝。一旦定义就不能更改绑定为其他的对象
指针
int *p; //指向int型对象的指针
-
是一种
"指向(point to)"另外一种类型的复合类型。 -
定义指针类型:
int *ip1;,从右向左读有助于阅读,ip1是指向int类型的指针。 -
指针存放某个对象的地址。
-
获取对象的地址:
int i=42; int *p = &i;。&是取地址符。 -
指针的类型与所指向的对象类型必须一致(均为同一类型int、double等)
-
指针的值的四种状态:
-
1.指向一个对象;
-
2.指向紧邻对象的下一个位置;
-
3.空指针;
-
4.无效指针。
-
对无效指针的操作均会引发错误,第二种和第三种虽为有效的,但理论上是不被允许的
-
-
指针访问对象:
cout << *p;输出p指针所指对象的数据,*是解引用符。 -
空指针不指向任何对象。使用
int *p=nullptr;来使用空指针。 -
指针和引用的区别:引用本身并非一个对象,引用定义后就不能绑定到其他的对象了;指针并没有此限制,相当于变量一样使用。
-
赋值语句永远改变的是左侧的对象。
-
void*指针可以存放任意对象的地址。因无类型,仅操作内存空间,对所存对象无法访问。 -
其他指针类型必须要与所指对象严格匹配。
-
两个指针相减的类型是
ptrdiff_t。 -
建议:初始化所有指针。
-
int* p1, p2;//*是对p1的修饰,所以p2还是int型
const限定符
- 动机:希望定义一些不能被改变值的变量。
初始化和const
- const对象必须初始化,且不能被改变。
- const变量默认不能被其他文件访问,非要访问,必须在指定const定义之前加extern。要想在多个文件中使用const变量共享,定义和声明都加const关键字即可。
const的引用
- reference to const(对常量的引用):指向const对象的引用,如
const int ival=1; const int &refVal = ival;,可以读取但不能修改refVal。 - 临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
- 对临时量的引用是非法行为。
指针和const
- pointer to const(指向常量的指针):不能用于改变其所指对象的值, 如
const double pi = 3.14; const double *cptr = π。 - const pointer:指针本身是常量,也就是说指针固定指向该对象,(存放在指针中的地址不变,地址所对应的那个对象值可以修改)如
int i = 0; int *const ptr = &i;
顶层const
顶层const:指针本身是个常量。底层const:指针指向的对象是个常量。拷贝时严格要求相同的底层const资格。
constexpr和常量表达式(▲可选)
- 常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式。
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量的表达式。
处理类型
类型别名
- 传统别名:使用typedef来定义类型的同义词。
typedef double wages; - 新标准别名:别名声明(alias declaration):
using SI = Sales_item;(C++11)
1 | // 对于复合类型(指针等)不能代回原式来进行理解 |
auto类型说明符 c++11
- auto类型说明符:让编译器自动推断类型。
- 一条声明语句只能有一个数据类型,所以一个auto声明多个变量时只能相同的变量类型(包括复杂类型&和*)。
auto sz = 0, pi =3.14//错误 int i = 0, &r = i; auto a = r;推断a的类型是int。- 会忽略
顶层const。 const int ci = 1; const auto f = ci;推断类型是int,如果希望是顶层const需要自己加const
decltype类型指示符
- 从表达式的类型推断出要定义的变量的类型。
- decltype:选择并返回操作数的数据类型。
decltype(f()) sum = x;推断sum的类型是函数f的返回类型。- 不会忽略
顶层const。 - 如果对变量加括号,编译器会将其认为是一个表达式,如int i–>(i),则decltype((i))得到结果为int&引用。
- 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。
C++11
自定义数据结构
struct
尽量不要把类定义和对象定义放在一起。如
struct Student{} xiaoming,xiaofang;
- 类可以以关键字
struct开始,紧跟类名和类体。 - 类数据成员:类体定义类的成员。
C++11:可以为类数据成员提供一个类内初始值(in-class initializer)。
编写自己的头文件
- 头文件通常包含哪些只能被定义一次的实体:类、
const和constexpr变量。
预处理器概述:
- 预处理器(preprocessor):确保头文件多次包含仍能安全工作。
- 当预处理器看到
#include标记时,会用指定的头文件内容代替#include - 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。
#ifdef已定义时为真#ifndef未定义时为真- 头文件保护符的名称需要唯一,且保持全部大写。养成良好习惯,不论是否该头文件被包含,要加保护符。
1 |
|
第2章 变量和基本类型
基本内置类型(Primitive Built-in Types)
算数类型(Arithmetic Types)
算数类型分为两类:整型(integral type)、浮点型(floating-point type)。
bool类型的取值是true或false。
一个char的大小和一个机器字节一样,确保可以存放机器基本字符集中任意字符对应的数字值。wchar_t确保可以存放机器最大扩展字符集中的任意一个字符。
在整型类型大小方面,C++规定short ≤ int ≤ long ≤ long long(long long是C++11定义的类型)。
浮点型可表示单精度(single-precision)、双精度(double-precision)和扩展精度(extended-precision)值,分别对应float、double和long double类型。
除去布尔型和扩展字符型,其他整型可以分为带符号(signed)和无符号(unsigned)两种。带符号类型可以表示正数、负数和0,无符号类型只能表示大于等于0的数值。类型int、short、long和long long都是带符号的,在类型名前面添加unsigned可以得到对应的无符号类型,如unsigned int。
字符型分为char、signed char和unsigned char三种,但是表现形式只有带符号和无符号两种。类型char和signed char并不一样, char的具体形式由编译器(compiler)决定。
如何选择算数类型:
-
当明确知晓数值不可能为负时,应该使用无符号类型。
-
使用
int执行整数运算,如果数值超过了int的表示范围,应该使用long long类型。 -
在算数表达式中不要使用
char和bool类型。如果需要使用一个不大的整数,应该明确指定它的类型是signed char还是unsigned char。 -
执行浮点数运算时建议使用
double类型。
类型转换(Type Conversions)
进行类型转换时,类型所能表示的值的范围决定了转换的过程。
- 把非布尔类型的算术值赋给布尔类型时,初始值为0则结果为
false,否则结果为true。 - 把布尔值赋给非布尔类型时,初始值为
false则结果为0,初始值为true则结果为1。 - 把浮点数赋给整数类型时,进行近似处理,结果值仅保留浮点数中的整数部分。
- 把整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
- 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数(8比特大小的
unsigned char能表示的数值总数是256)取模后的余数。 - 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。
避免无法预知和依赖于实现环境的行为。
无符号数不会小于0这一事实关系到循环的写法。
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 |
不要混用带符号类型和无符号类型。
字面值常量(Literals)
以0开头的整数代表八进制(octal)数,以0x或0X开头的整数代表十六进制(hexadecimal)数。在C++14中,0b或0B开头的整数代表二进制(binary)数。
整型字面值具体的数据类型由它的值和符号决定。
C++14新增了单引号'形式的数字分隔符。数字分隔符不会影响数字的值,但可以通过分隔符将数字分组,使数值读写更容易。
1 | // 按照书写形式,每3位分为一组 |
浮点型字面值默认是一个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 | std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline |
添加特定的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。

使用一个长整型字面值时,最好使用大写字母L进行标记,小写字母l和数字1容易混淆。
变量(Variables)
变量定义(Variable Definitions)
变量定义的基本形式:类型说明符(type specifier)后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。定义时可以为一个或多个变量赋初始值(初始化,initialization)。
初始化不等于赋值(assignment)。初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,再用一个新值来替代。
用花括号初始化变量称为列表初始化(list initialization)。当用于内置类型的变量时,如果使用了列表初始化并且初始值存在丢失信息的风险,则编译器会报错。
1 | long double ld = 3.1415926536; |
如果定义变量时未指定初值,则变量被默认初始化(default initialized)。
对于内置类型,定义于任何函数体之外的变量被初始化为0,函数体内部的变量将不被初始化(uninitialized)。
定义于函数体内的内置类型对象如果没有初始化,则其值未定义,使用该类值是一种错误的编程行为且很难调试。类的对象如果没有显式初始化,则其值由类确定。
建议初始化每一个内置类型的变量。
变量声明和定义的关系(Variable Declarations and Definitions)
声明(declaration)使得名字为程序所知。一个文件如果想使用其他地方定义的名字,则必须先包含对那个名字的声明。
定义(definition)负责创建与名字相关联的实体。
如果想声明一个变量而不定义它,就在变量名前添加关键字extern,并且不要显式地初始化变量。
1 | extern int i; // declares but does not define i |
extern语句如果包含了初始值就不再是声明了,而变成了定义。
变量能且只能被定义一次,但是可以被声明多次。
如果要在多个文件中使用同一个变量,就必须将声明和定义分开。此时变量的定义必须出现且只能出现在一个文件中,其他使用该变量的文件必须对其进行声明,但绝对不能重复定义。
标识符(Identifiers)
C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感。C++为标准库保留了一些名字。用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。
名字的作用域(Scope of a Name)
定义在函数体之外的名字拥有全局作用域(global scope)。声明之后,该名字在整个程序范围内都可使用。
最好在第一次使用变量时再去定义它。这样做更容易找到变量的定义位置,并且也可以赋给它一个比较合理的初始值。
作用域中一旦声明了某个名字,在它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字,此时内层作用域中新定义的名字将屏蔽外层作用域的名字。
可以用作用域操作符::来覆盖默认的作用域规则。因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,会向全局作用域发出请求获取作用域操作符右侧名字对应的变量。
1 |
|
如果函数有可能用到某个全局变量,则不宜再定义一个同名的局部变量。
复合类型(Compound Type)
引用(References)
引用为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是变量名称。
1 | int ival = 1024; |
定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,将无法再令引用重新绑定到另一个对象,因此引用必须初始化。
引用不是对象,它只是为一个已经存在的对象所起的另外一个名字。
声明语句中引用的类型实际上被用于指定它所绑定的对象类型。大部分情况下,引用的类型要和与之绑定的对象严格匹配。
引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起。
指针(Pointer)
与引用类似,指针也实现了对其他对象的间接访问。
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在生命周期内它可以先后指向不同的对象。
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
通过将声明符写成*d的形式来定义指针类型,其中d是变量名称。如果在一条语句中定义了多个指针变量,则每个量前都必须有符号*。
1 | int *ip1, *ip2; // both ip1 and ip2 are pointers to int |
指针存放某个对象的地址,要想获取对象的地址,需要使用取地址符&。
1 | int ival = 42; |
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
声明语句中指针的类型实际上被用于指定它所指向的对象类型。大部分情况下,指针的类型要和它指向的对象严格匹配。
指针的值(即地址)应属于下列状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,即指针没有指向任何对象。
- 无效指针,即上述情况之外的其他值。
试图拷贝或以其他方式访问无效指针的值都会引发错误。
如果指针指向一个对象,可以使用解引用(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 Qualifier)
在变量类型前添加关键字const可以创建值不能被改变的对象。const变量必须被初始化。
1 | const int bufSize = 512; // input buffer size |
默认情况下,const对象被设定成仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
如果想在多个文件间共享const对象:
-
若
const对象的值在编译时已经确定,则应该定义在头文件中。其他源文件包含该头文件时,不会产生重复定义错误。 -
若
const对象的值直到运行时才能确定,则应该在头文件中声明,在源文件中定义。此时const变量的声明和定义前都应该添加extern关键字。1
2
3
4// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize; // same bufSize as defined in file_1.cc
const的引用(References to const)
把引用绑定在const对象上即为对常量的引用(reference to const)。对常量的引用不能被用作修改它所绑定的对象。
1 | const int ci = 1024; |
大部分情况下,引用的类型要和与之绑定的对象严格匹配。但是有两个例外:
-
初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
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 | const double pi = 3.14; // pi is const; its value may not be changed |
定义语句中把*放在const之前用来说明指针本身是一个常量,常量指针(const pointer)必须初始化。
1 | int errNumb = 0; |
指针本身是常量并不代表不能通过指针修改其所指向的对象的值,能否这样做完全依赖于其指向对象的类型。
顶层const(Top-Level const)
顶层const表示指针本身是个常量,底层const(low-level const)表示指针所指的对象是一个常量。指针类型既可以是顶层const也可以是底层const。
1 | int i = 0; |
当执行拷贝操作时,常量是顶层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)指值不会改变并且在编译过程就能得到计算结果的表达式。
一个对象是否为常量表达式由它的数据类型和初始值共同决定。
1 | const int max_files = 20; // max_files is a constant expression |
C++11允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。
1 | constexpr int mf = 20; // 20 is a constant expression |
指针和引用都能定义成constexpr,但是初始值受到严格限制。constexpr指针的初始值必须是0、nullptr或者是存储在某个固定地址中的对象。
函数体内定义的普通变量一般并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反,函数体外定义的变量地址固定不变,可以用来初始化constexpr指针。
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针本身有效,与指针所指的对象无关。constexpr把它所定义的对象置为了顶层const。
1 | constexpr int *p = nullptr; // p是指向int的const指针 |
const和constexpr限定的值都是常量。但constexpr对象的值必须在编译期间确定,而const对象的值可以延迟到运行期间确定。
建议使用constexpr修饰表示数组大小的对象,因为数组的大小必须在编译期间确定且不能改变。
处理类型(Dealing with Types)
类型别名(Type Aliases)
类型别名是某种类型的同义词,传统方法是使用关键字typedef定义类型别名。
1 | typedef double wages; // wages is a synonym for double |
C++11使用关键字using进行别名声明(alias declaration),作用是把等号左侧的名字规定成等号右侧类型的别名。
1 | using SI = Sales_item; // SI is a synonym for Sales_item |
auto类型说明符(The auto Type Specifier)
C++11新增auto类型说明符,能让编译器自动分析表达式所属的类型。auto定义的变量必须有初始值。
1 | // the type of item is deduced from the type of the result of adding val1 and val2 |
编译器推断出来的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 | const int ci = 0, &cj = ci; |
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。如果表达式的内容是解引用操作,则decltype将得到引用类型。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则decltype会得到引用类型,因为变量是一种可以作为赋值语句左值的特殊表达式。
decltype((var))的结果永远是引用,而decltype(var)的结果只有当var本身是一个引用时才会是引用。
自定义数据结构(Defining Our Own Data Structures)
C++11规定可以为类的数据成员(data member)提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员,没有初始值的成员将被默认初始化。
类内初始值不能使用圆括号。
类定义的最后应该加上分号。
头文件(header file)通常包含那些只能被定义一次的实体,如类、const和constexpr变量。
头文件一旦改变,相关的源文件必须重新编译以获取更新之后的声明。
头文件保护符(header guard)依赖于预处理变量(preprocessor variable)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量。#ifdef指令当且仅当变量已定义时为真,#ifndef指令当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。
1 |
|
在高级版本的IDE环境中,可以直接使用#pragma once命令来防止头文件的重复包含。
预处理变量无视C++语言中关于作用域的规则。
整个程序中的预处理变量,包括头文件保护符必须唯一。预处理变量的名字一般均为大写。
头文件即使目前还没有被包含在任何其他头文件中,也应该设置保护符。
第3章 字符串、向量和数组(简)
using声明
- 使用某个命名空间:例如
using std::cin表示使用命名空间std中的名字cin。 - 头文件中不应该包含
using声明。这样使用了该头文件的源码也会使用这个声明,会带来风险。
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):通过括号给对象赋值。
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:读取一整行,包括空白符。
- 执行读操作
s.size()返回的时string::size_type类型,记住是一个无符号类型的值,不要和int混用s1+s2使用时,保证至少一侧是string类型。string s1 = "hello" + "world" // 错误,两侧均为字符串字面值- 字符串字面值和string是不同的类型。
处理string对象中的字符
-
ctype.h vs. cctype:C++修改了c的标准库,名称为去掉
.h,前面加c。如c++版本为
cctype,c版本为ctype.h- 尽量使用c++版本的头文件,即
cctype
- 尽量使用c++版本的头文件,即
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 |
- 遍历字符串:使用范围for(range for)语句:
for (auto c: str),或者for (auto &c: str)使用引用直接改变字符串中的字符。 (C++11) str[x],[]输入参数为string::size_type类型,给出int整型也会自动转化为该类型
vector
- vector是一个容器,也是一个类模板;
#include <vector>然后using std::vector;- 容器:包含其他对象。
- 类模板:本身不是类,但可以实例化instantiation出一个类。
vector是一个模板,vector<int>是一个类型。 - 通过将类型放在类模板名称后面的尖括号中来指定类型,如
vector<int> ivec。
定义和初始化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对象中添加元素
v.push_back(e)在尾部增加元素。
其他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对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能用于添加元素。
迭代器iterator
- 所有标准库容器都可以使用迭代器。
- 类似于指针类型,迭代器也提供了对对象的间接访问。
使用迭代器
vector<int>::iterator iter。auto b = v.begin();返回指向第一个元素的迭代器。auto e = v.end();返回指向最后一个元素的下一个(哨兵,尾后,one past the end)的迭代器(off the end)。- 如果容器为空,
begin()和end()返回的是同一个迭代器,都是尾后迭代器。 - 使用解引用符
*访问迭代器指向的元素。 - 养成使用迭代器和
!=的习惯(泛型编程)。 - 容器:可以包含其他对象;但所有的对象必须类型相同。
- 迭代器(iterator):每种标准容器都有自己的迭代器。
C++倾向于用迭代器而不是下标遍历元素。 - const_iterator:只能读取容器内元素不能改变。
- 箭头运算符: 解引用 + 成员访问,
it->mem等价于(*it).mem - 谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
标准容器迭代器的运算符:
| 运算符 | 解释 |
|---|---|
*iter |
返回迭代器iter所指向的元素的引用 |
iter->mem |
等价于(*iter).mem |
++iter |
令iter指示容器中的下一个元素 |
--iter |
令iter指示容器中的上一个元素 |
iter1 == iter2 |
判断两个迭代器是否相等 |
迭代器运算
vector和string迭代器支持的运算:
| 运算符 | 解释 |
|---|---|
iter + n |
迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。 |
iter - n |
迭代器减去一个证书仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。 |
iter1 += n |
迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1 |
iter1 -= n |
迭代器减法的复合赋值语句,将iter2减n的加过赋给iter1 |
iter1 - iter2 |
两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。 |
>、>=、<、<= |
迭代器的关系运算符,如果某迭代器 |
- difference_type:保证足够大以存储任何两个迭代器对象间的距离,可正可负。
数组
- 相当于vector的低级版,长度固定。
定义和初始化内置数组
- 初始化:
char input_buffer[buffer_size];,长度必须是const表达式,或者不写,让编译器自己推断。 - 数组不允许直接赋值给另一个数组。
访问数组元素
- 数组下标的类型:
size_t。 - 字符数组的特殊性:结尾处有一个空字符,如
char a[] = "hello";。 - 用数组初始化
vector:int a[] = {1,2,3,4,5}; vector<int> v(begin(a), end(a));。
数组和指针
- 使用数组时,编译器一般会把它转换成指针。
- 标准库类型限定使用的下标必须是无符号类型,而内置的下标可以处理负值。
- 指针访问数组:在表达式中使用数组名时,名字会自动转换成指向数组的第一个元素的指针。
C风格字符串
- 从C继承来的字符串。
- 用空字符结束(
\0)。 - 对大多数应用来说,使用标准库
string比使用C风格字符串更安全、更高效。 - 获取
string中的cstring:const char *str = s.c_str();。
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 |
尽量使用vector和迭代器,少用数组
多维数组
- 多维数组的初始化:
int ia[3][4] = {{0,1,2,3}, {4, 5, 6, 7}, {8, 9 ,10, 11}} 1- 使用范围for语句时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
指针vs引用
- 引用总是指向某个对象,定义引用时没有初始化是错的。
- 给引用赋值,修改的是该引用所关联的对象的值,而不是让引用和另一个对象相关联。
指向指针的指针
- 定义:
int **ppi = π - 解引用:
**ppi
动态数组
- 使用
new和delete表达和c中malloc和free类似的功能,即在堆(自由存储区)中分配存储空间。 - 定义:
int *pia = new int[10];10可以被一个变量替代。 - 释放:
delete [] pia;,注意不要忘记[]。
第3章 字符串、向量和数组
命名空间的using声明(Namespace using Declarations)
使用using声明后就无须再通过专门的前缀去获取所需的名字了。
1 | using std::cout; |
程序中使用的每个名字都需要用独立的using声明引入。
头文件中通常不应该包含using声明。
标准库类型string(Library string Type)
标准库类型string表示可变长的字符序列,定义在头文件string中。
定义和初始化string对象(Defining and Initializing strings)
初始化string的方式:
如果使用等号初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,则执行的是直接初始化(direct initialization)。
string对象上的操作(Operations on strings)
string的操作:
在执行读取操作时,string对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
使用getline函数可以读取一整行字符。该函数只要遇到换行符就结束读取并返回结果,如果输入的开始就是一个换行符,则得到空string。触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。
size函数返回string对象的长度,返回值是string::size_type类型,这是一种无符号类型。要使用size_type,必须先指定它是由哪种类型定义的。
如果一个表达式中已经有了size函数就不要再使用int了,这样可以避免混用int和unsigned int可能带来的问题。
当把string对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是string。
1 | string s4 = s1 + ", "; // ok: adding a string and a literal |
为了与C兼容,C++语言中的字符串字面值并不是标准库string的对象。
处理string对象中的字符(Dealing with the Characters in a string)
头文件cctype中的字符操作函数:
建议使用C++版本的C标准库头文件。C语言中名称为name.h的头文件,在C++中则被命名为cname。
C++11提供了范围for(range for)语句,可以遍历给定序列中的每个元素并执行某种操作。
1 | for (declaration : expression) |
expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration部分的变量都会被初始化为expression部分的下一个元素值。
1 | string str("some string"); |
如果想在范围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对象(Defining and Initializing vectors)
初始化vector对象的方法:
初始化vector对象时如果使用圆括号,可以说提供的值是用来构造(construct)vector对象的;如果使用的是花括号,则是在列表初始化(list initialize)该vector对象。
可以只提供vector对象容纳的元素数量而省略初始值,此时会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中的元素类型决定。
向vector对象中添加元素(Adding Elements to a vector)
push_back函数可以把一个值添加到vector的尾端。
1 | vector<int> v2; // empty vector |
范围for语句体内不应该改变其所遍历序列的大小。
其他vector操作(Other vector Operations)
vector支持的操作:

size函数返回vector对象中元素的个数,返回值是由vector定义的size_type类型。vector对象的类型包含其中元素的类型。
1 | vector<int>::size_type // ok |
vector和string对象的下标运算符只能用来访问已经存在的元素,而不能用来添加元素。
1 | vector<int> ivec; // empty vector |
迭代器介绍(Introducing Iterators)
迭代器的作用和下标类似,但是更加通用。所有标准库容器都可以使用迭代器,但是其中只有少数几种同时支持下标运算符。
使用迭代器(Using Iterators)
定义了迭代器的类型都拥有begin和end两个成员函数。begin函数返回指向第一个元素的迭代器,end函数返回指向容器“尾元素的下一位置(one past the end)”的迭代器,通常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。尾后迭代器仅是个标记,表示程序已经处理完了容器中的所有元素。迭代器一般为iterator类型。
1 | // b denotes the first element and e denotes one past the last element in ivec |
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符:

因为end返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。
在for或者其他循环语句的判断条件中,最好使用!=而不是<。所有标准库容器的迭代器都定义了==和!=,但是只有其中少数同时定义了<运算符。
如果vector或string对象是常量,则只能使用const_iterator迭代器,该迭代器只能读元素,不能写元素。
begin和end返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回const_iterator;如果对象不是常量,则返回iterator。
1 | vector<int> v; |
C++11新增了cbegin和cend函数,不论vector或string对象是否为常量,都返回const_iterator迭代器。
任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。
迭代器运算(Iterator Arithmetic)
vector和string迭代器支持的操作:
difference_type类型用来表示两个迭代器间的距离,这是一种带符号整数类型。
数组(Arrays)
数组类似vector,但数组的大小确定不变,不能随意向数组中添加元素。
如果不清楚元素的确切个数,应该使用vector。
定义和初始化内置数组(Defining and Initializing Built-in Arrays)
数组是一种复合类型,声明形式为a[d],其中a是数组名称,d是数组维度(dimension)。维度必须是一个常量表达式。
默认情况下,数组的元素被默认初始化。
定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值列表推断类型。
如果定义数组时提供了元素的初始化列表,则允许省略数组维度,编译器会根据初始值的数量计算维度。但如果显式指明了维度,那么初始值的数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化。
1 | const unsigned sz = 3; |
可以用字符串字面值初始化字符数组,但字符串字面值结尾处的空字符也会一起被拷贝到字符数组中。
1 | char a1[] = {'C', '+', '+'}; // list initialization, no null |
不能用一个数组初始化或直接赋值给另一个数组。
从数组的名字开始由内向外阅读有助于理解复杂数组声明的含义。
1 | int *ptrs[10]; // ptrs is an array of ten pointers to int |
访问数组元素(Accessing the Elements of an Array)
数组下标通常被定义成size_t类型,这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。size_t定义在头文件cstddef中。
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
指针和数组(Pointers and Arrays)
在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。
1 | string nums[] = {"one", "two", "three"}; // array of strings |
一维数组寻址公式:
当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。但decltype关键字不会发生这种转换,直接返回数组类型。
1 | int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints |
C++11在头文件iterator中定义了两个名为begin和end的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。
1 | int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints |
两个指针相减的结果类型是ptrdiff_t,这是一种定义在头文件cstddef中的带符号类型。
标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。
C风格字符串(C-Style Character Strings)
C风格字符串是将字符串存放在字符数组中,并以空字符结束(null terminated)。这不是一种类型,而是一种为了表达和使用字符串而形成的书写方法。
C++标准支持C风格字符串,但是最好不要在C++程序中使用它们。对大多数程序来说,使用标准库string要比使用C风格字符串更加安全和高效。
C风格字符串的函数:
C风格字符串函数不负责验证其参数的正确性,传入此类函数的指针必须指向以空字符作为结尾的数组。
与旧代码的接口(Interfacing to Older Code)
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:
- 允许使用以空字符结束的字符数组来初始化
string对象或为string对象赋值。 - 在
string对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。 - 在
string对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧运算对象。
不能用string对象直接初始化指向字符的指针。为了实现该功能,string提供了一个名为c_str的成员函数,返回const char*类型的指针,指向一个以空字符结束的字符数组,数组的数据和string对象一样。
1 | string s("Hello World"); // s holds Hello World |
针对string对象的后续操作有可能会让c_str函数之前返回的数组失去作用,如果程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
可以使用数组来初始化vector对象,但是需要指明要拷贝区域的首元素地址和尾后地址。
1 | int int_arr[] = {0, 1, 2, 3, 4, 5}; |
在新版本的C++程序中应该尽量使用vector、string和迭代器,避免使用内置数组、C风格字符串和指针。
多维数组(Multidimensional Arrays)
C++中的多维数组其实就是数组的数组。当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。
多维数组初始化的几种方式:
1 | int ia[3][4] = |
可以使用下标访问多维数组的元素,数组的每个维度对应一个下标运算符。如果表达式中下标运算符的数量和数组维度一样多,则表达式的结果是给定类型的元素。如果下标运算符数量比数组维度小,则表达式的结果是给定索引处的一个内层数组。
1 | // assigns the first element of arr to the last element in the last row of ia |
多维数组寻址公式:
使用范围for语句处理多维数组时,为了避免数组被自动转换成指针,语句中的外层循环控制变量必须声明成引用类型。
1 | for (const auto &row : ia) // for every element in the outer array |
如果row不是引用类型,编译器初始化row时会自动将数组形式的元素转换成指向该数组内首元素的指针,这样得到的row就是int*类型,而之后的内层循环则试图在一个int*内遍历,程序将无法通过编译。
1 | for (auto row : ia) |
使用范围for语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。
因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。
1 | int ia[3][4]; // array of size 3; each element is an array of ints of size 4 |
声明指向数组类型的指针时,必须带有圆括号。
1 | int *ip[4]; // array of pointers to int |
使用auto和decltype能省略复杂的指针定义。
1 | // print the value of each element in ia, with each inner array on its own line |
第4章 表达式(简)
表达式基础
- 运算对象转换:小整数类型会被提升为较大的整数类型
- 重载运算符:当运算符作用在类类型的运算对象时,用户可以自行定义其含义。
- 左值和右值:
- C中原意:左值可以在表达式左边,右值不能。
C++:当一个对象被用作右值的时候,用的是对象的值(内容);- 被用做左值时,用的是对象的身份(在内存中的位置)。
- 求值顺序:
int i = f1() + f2()- 先计算
f1() + f2(),再计算int i = f1() + f2()。但是f1和f2的计算先后不确定 - 但是,如果f1、f2都对同一对象进行了修改,因为顺序不确定,所以会编译出错,显示未定义
- 先计算
算术运算符
-
溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。
-
bool类型不应该参与计算
1
2
3
4bool b=true;
bool b2=-b; //仍然为true
//b为true,提升为对应int=1,-b=-1
//b2=-1≠0,所以b2仍未true -
取余运算 m%n ,结果符号与 m 相同
逻辑运算符
- 短路求值:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。先左再右
- 小技巧,声明为引用类型可以避免对元素的拷贝,如下,如string特别大时可以节省大量时间。
1 | vector<string> text; |
赋值运算符
- 赋值运算的返回结果时它的左侧运算对象,且是一个左值。类型也就是左侧对象的类型。
- 如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
- 赋值运算符满足右结合律,这点和其他二元运算符不一样。
ival = jval = 0;等价于ival = (jval = 0); - 赋值运算优先级比较低,使用其当条件时应该加括号。
- 复合赋值运算符,复合运算符只求值一次,普通运算符求值两次。(对性能有一点点点点影响)
任意复合运算符op等价于a = a op b;
递增递减运算符
- 前置版本
j = ++i,先加一后赋值 - 后置版本
j = i++,先赋值后加一
优先使用前置版本,后置多一步储存原始值。(除非需要变化前的值)
混用解引用和递增运算符
*iter++等价于*(iter++),递增优先级较高
1 | auto iter = vi.begin(); |
简洁是一种美德,追求简洁能降低程序出错可能性
成员访问运算符
ptr->mem等价于(*ptr).mem
注意.运算符优先级大于*,所以记得加括号
条件运算符
-
条件运算符(
?:)允许我们把简单的if-else逻辑嵌入到单个表达式中去,按照如下形式:cond? expr1: expr2 -
可以嵌套使用,右结合律,从右向左顺序组合
-
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass"; //等价于 finalgrade = (grade > 90) ? "high pass" : ((grade < 60) ? "fail" : "pass");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 输出表达式使用条件运算符记得加括号,条件运算符优先级太低。
### 位运算符
用于检查和设置二进制位的功能。
- 位运算符是作用于**整数类型**的运算对象。
- 二进制位向左移(`<<`)或者向右移(`>>`),移出边界外的位就被舍弃掉了。
- 位取反(`~`)(逐位求反)、与(`&`)、或(`|`)、异或(`^`)
有符号数负值可能移位后变号,所以强烈建议**位运算符仅用于无符号数**。
应用:
```c++
unsigned long quiz1 = 0; // 每一位代表一个学生是否通过考试
1UL << 12; // 代表第12个学生通过
quiz1 |= (1UL << 12); // 将第12个学生置为已通过
quiz1 &= ~(1UL << 12); // 将第12个学生修改为未通过
bool stu12 = quiz1 & (1UL << 12); // 判断第12个学生是否通过
-
位运算符使用较少,但是重载cout、cin大家都用过
位运算符满足左结合律,优先级介于中间,使用时尽量加括号。
sizeof运算符
- 返回一条表达式或一个类型名字所占的字节数。
- 返回的类型是
size_t的常量表达式。 sizeof并不实际计算其运算对象的值。- 两种形式:
sizeof (type),给出类型名sizeof expr,给出表达式
- 可用sizeof返回数组的大小
1 | int ia[10]; |
逗号运算符
从左向右依次求值。
左侧求值结果丢弃,逗号运算符结果是右侧表达式的值。
类型转换
隐式类型转换
设计为尽可能避免损失精度,即转换为更精细类型。
- 比
int类型小的整数值先提升为较大的整数类型。 - 条件中,非布尔转换成布尔。
- 初始化中,初始值转换成变量的类型。
- 算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。
- 函数调用时也会有转换。
算术转换
整型提升
- 常见的char、bool、short能存在int就会转换成int,否则提升为
unsigned int wchar_t,char16_t,char32_t提升为整型中int,long,long long ……最小的,且能容纳原类型所有可能值的类型。
其他转换
p143
显式类型转换(尽量避免)
-
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:通常为运算对象的位模式提供低层次上的重新解释。
旧式强制类型转换
type expr
运算符优先级表
p147
第4章 表达式
基础(Fundamentals)
表达式(expression)由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。
基础概念(Basic Concepts)
C++定义了一元运算符(unary operator)和二元运算符(binary operator)。除此之外,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
表达式求值过程中,小整数类型(如bool、char、short等)通常会被提升(promoted)为较大的整数类型,主要是int。
C++定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自定义其含义,这被称作运算符重载(overloaded operator)。
C++的表达式分为右值(rvalue)和左值(lvalue)。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的地址。需要右值的地方可以用左值代替,反之则不行。
- 赋值运算符需要一个非常量左值作为其左侧运算对象,返回结果也是一个左值。
- 取地址符作用于左值运算对象,返回指向该运算对象的指针,该指针是一个右值。
- 内置解引用运算符、下标运算符、迭代器解引用运算符、
string和vector的下标运算符都返回左值。 - 内置类型和迭代器的递增递减运算符作用于左值运算对象。前置版本返回左值,后置版本返回右值。
如果decltype作用于一个求值结果是左值的表达式,会得到引用类型。
优先级与结合律(Precedence and Associativity)
复合表达式(compound expression)指含有两个或多个运算符的表达式。优先级与结合律决定了运算对象的组合方式。
括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
求值顺序(Order of Evaluation)
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
1 | int i = 0; |
处理复合表达式时建议遵循以下两点:
- 不确定求值顺序时,使用括号来强制让表达式的组合关系符合程序逻辑的要求。
- 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。
当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,第二条规则无效。如*++iter,递增运算符改变了iter的值,而改变后的iter又是解引用运算符的运算对象。类似情况下,求值的顺序不会成为问题。
算术运算符(Arithmetic Operators)
算术运算符(左结合律):

在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。
逻辑和关系运算符(Logical and Relational Operators)
关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。

逻辑与(logical AND)运算符&&和逻辑或(logical OR)运算符||都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。
- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
赋值运算符(Assignment Operators)
赋值运算符=的左侧运算对象必须是一个可修改的左值。
C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
1 | vector<int> vi; // initially empty |
赋值运算符满足右结合律。
1 | int ival, jval; |
因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
不要混淆相等运算符==和赋值运算符=。
复合赋值运算符包括+=、-=、*=、/=、%=、<<=、>>=、&=、^=和|=。任意一种复合运算都完全等价于a = a op b。
递增和递减运算符(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的二进制位,如何选择视具体环境而定。
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)。
算术转换(Integral Promotions)
把一种算术类型转换成另一种算术类型叫做算术转换。
整型提升(integral promotions)负责把小整数类型转换成较大的整数类型。
其他隐式类型转换(Other Implicit Conversions)
在大多数表达式中,数组名字自动转换成指向数组首元素的指针。
常量整数值0或字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。
任意一种算术类型或指针类型都能转换成布尔类型。如果指针或算术类型的值为0,转换结果是false,否则是true。
指向非常量类型的指针能转换成指向相应的常量类型的指针。
显式转换(Explicit Conversions)
显式类型转换也叫做强制类型转换(cast)。虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。
命名的强制类型转换(named cast)形式如下:
1 | cast-name<type>(expression); |
其中type是转换的目标类型,expression是要转换的值。如果type是引用类型,则转换结果是左值。cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种,用来指定转换的方式。
dynamic_cast支持运行时类型识别。- 任何具有明确定义的类型转换,只要不包含底层
const,都能使用static_cast。 const_cast只能改变运算对象的底层const,不能改变表达式的类型。同时也只有const_cast能改变表达式的常量属性。const_cast常常用于函数重载。reinterpret_cast通常为运算对象的位模式提供底层上的重新解释。
早期版本的C++语言中,显式类型转换包含两种形式:
1 | type (expression); // function-style cast notation |
第5章 语句(简)
简单语句
- 表达式语句:一个表达式末尾加上分号,就变成了表达式语句。
- 空语句:只有一个单独的分号。
- 复合语句(块):用花括号
{}包裹起来的语句和声明的序列。一个块就是一个作用域。
条件语句
- 悬垂else(dangling else):用来描述在嵌套的
if else语句中,如果if比else多时如何处理的问题。C++使用的方法是else匹配最近没有配对的if。
迭代语句
- while:当不确定到底要迭代多少次时,使用
while循环比较合适,比如读取输入的内容。 - for:
for语句可以省略掉init-statement,condition和expression的任何一个;甚至全部。 - 范围for:
for (declaration: expression) statement
跳转语句
- break:
break语句负责终止离它最近的while、do while、for或者switch语句,并从这些语句之后的第一条语句开始继续执行。 - continue:终止最近的循环中的当前迭代并立即开始下一次迭代。只能在
while、do while、for循环的内部。
try语句块和异常处理
- throw表达式:异常检测部分使用
throw表达式来表示它遇到了无法处理的问题。我们说throw引发raise了异常。 - try语句块:以
try关键词开始,以一个或多个catch字句结束。try语句块中的代码抛出的异常通常会被某个catch捕获并处理。catch子句也被称为异常处理代码。 - 异常类:用于在
throw表达式和相关的catch子句之间传递异常的具体信息。
第5章 语句
简单语句(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函数并终止当前程序的执行。
标准异常(Standard Exceptions)
异常类分别定义在4个头文件中:
-
头文件
exception定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。 -
头文件
stdexcept定义了几种常用的异常类。
-
头文件
new定义了bad_alloc异常类。 -
头文件
type_info定义了bad_cast异常类。
标准库异常类的继承体系:
只能以默认初始化的方式初始化exception、bad_alloc和bad_cast对象,不允许为这些对象提供初始值。其他异常类的对象在初始化时必须提供一个string或一个C风格字符串,通常表示异常信息。what成员函数可以返回该字符串的string副本。
第6章 函数(简)
函数基础
- 函数定义:包括返回类型、函数名字和0个或者多个形参(parameter)组成的列表和函数体。
- 调用运算符:调用运算符的形式是一对圆括号
(),作用于一个表达式,该表达式是函数或者指向函数的指针。 - 圆括号内是用逗号隔开的实参(argument)列表。
- 函数调用过程:
- 1.主调函数(calling function)的执行被中断。
- 2.被调函数(called function)开始执行。
- 形参和实参:形参和实参的个数和类型必须匹配上。
- 返回类型:
void表示函数不返回任何值。函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。 - 名字:名字的作用于是程序文本的一部分,名字在其中可见。
局部对象
- 生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
- 局部变量(local variable):形参和函数体内部定义的变量统称为局部变量。它对函数而言是局部的,对函数外部而言是隐藏的。
- 自动对象:只存在于块执行期间的对象。当块的执行结束后,它的值就变成未定义的了。
- 局部静态对象:
static类型的局部变量,生命周期贯穿函数调用前后。
函数声明
- 函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型。
- 在头文件中进行函数声明:建议变量在头文件中声明;在源文件中定义。
- 分离编译:
CC a.cc b.cc直接编译生成可执行文件;CC -c a.cc b.cc编译生成对象代码a.o b.o;CC a.o b.o编译生成可执行文件。
参数传递
- 形参初始化的机理和变量初始化一样。
- 引用传递(passed by reference):又称传引用调用(called by reference),指形参是引用类型,引用形参是它对应的实参的别名。
- 值传递(passed by value):又称传值调用(called by value),指实参的值是通过拷贝传递给形参。
传值参数
- 当初始化一个非引用类型的变量时,初始值被拷贝给变量。
- 函数对形参做的所有操作都不会影响实参。
- 指针形参:常用在C中,
C++建议使用引用类型的形参代替指针。
传引用参数
- 通过使用引用形参,允许函数改变一个或多个实参的值。
- 引用形参直接关联到绑定的对象,而非对象的副本。
- 使用引用形参可以用于返回额外的信息。
- 经常用引用形参来避免不必要的复制。
void swap(int &v1, int &v2)- 如果无需改变引用形参的值,最好将其声明为常量引用。
const形参和实参
- 形参的顶层
const被忽略。void func(const int i);调用时既可以传入const int也可以传入int。 - 我们可以使用非常量初始化一个底层
const对象,但是反过来不行。 - 在函数中,不能改变实参的局部副本。
- 尽量使用常量引用。
数组形参
- 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
- 要注意数组的实际长度,不能越界。
main处理命令行选项
int main(int argc, char *argv[]){...}- 第一个形参代表参数的个数;第二个形参是参数C风格字符串数组。
可变形参
initializer_list提供的操作(C++11):
| 操作 | 解释 |
|---|---|
initializer_list<T> lst; |
默认初始化;T类型元素的空列表 |
initializer_list<T> lst{a,b,c...}; |
lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const。 |
lst2(lst) |
拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。 |
lst2 = lst |
同上 |
lst.size() |
列表中的元素数量 |
lst.begin() |
返回指向lst中首元素的指针 |
lst.end() |
返回指向lst中微元素下一位置的指针 |
initializer_list使用demo:
1 | void err_msg(ErrCode e, initializer_list<string> il){ |
- 所有实参类型相同,可以使用
initializer_list的标准库类型。 - 实参类型不同,可以使用
可变参数模板。 - 省略形参符:
...,便于C++访问某些C代码,这些C代码使用了varargs的C标准功能。
返回类型和return语句
无返回值函数
没有返回值的 return语句只能用在返回类型是 void的函数中,返回 void的函数不要求非得有 return语句。
有返回值函数
return语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。- 值的返回:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
- 不要返回局部对象的引用或指针。
- 引用返回左值:函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型得到右值。
- 列表初始化返回值:函数可以返回花括号包围的值的列表。(
C++11) - 主函数main的返回值:如果结尾没有
return,编译器将隐式地插入一条返回0的return语句。返回0代表执行成功。
返回数组指针
Type (*function (parameter_list))[dimension]- 使用类型别名:
typedef int arrT[10];或者using arrT = int[10;],然后arrT* func() {...} - 使用
decltype:decltype(odd) *arrPtr(int i) {...} - 尾置返回类型: 在形参列表后面以一个
->开始:auto func(int i) -> int(*)[10](C++11)
函数重载
- 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。
main函数不能重载。- 重载和const形参:
- 一个有顶层const的形参和没有它的函数无法区分。
Record lookup(Phone* const)和Record lookup(Phone*)无法区分。 - 相反,是否有某个底层const形参可以区分。
Record lookup(Account*)和Record lookup(const Account*)可以区分。
- 一个有顶层const的形参和没有它的函数无法区分。
- 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。
特殊用途语言特性
默认实参
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');- 一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。
内联(inline)函数
- 普通函数的缺点:调用函数比求解等价表达式要慢得多。
inline函数可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数。inline函数应该在头文件中定义。
constexpr函数
- 指能用于常量表达式的函数。
constexpr int new_sz() {return 42;}- 函数的返回类型及所有形参类型都要是字面值类型。
constexpr函数应该在头文件中定义。
调试帮助
assert预处理宏(preprocessor macro):assert(expr);
开关调试状态:
CC -D NDEBUG main.c可以定义这个变量NDEBUG。
1 | void print(){ |
函数匹配
- 重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
- 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
- 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
- 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。
函数指针
- 函数指针:是指向函数的指针。
bool (*pf)(const string &, const string &);注:两端的括号不可少。- 函数指针形参:
- 形参中使用函数定义或者函数指针定义效果一样。
- 使用类型别名或者
decltype。
- 返回指向函数的指针:1.类型别名;2.尾置返回类型。
第6章 函数
函数基础(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。
在Visual Studio中可以设置main函数调试参数:

含有可变形参的函数(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对象不会拷贝列表中的元素。拷贝后,原始列表和副本共享元素。
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 | // disaster: this function returns a reference to a local object |
如果函数返回指针、引用或类的对象,则可以使用函数调用的结果访问结果对象的成员。
调用一个返回引用的函数会得到左值,其他返回类型得到右值。
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作用于函数时,返回的是函数类型,而不是函数指针类型。
函数可以返回指向函数的指针。但返回类型不会像函数类型的形参一样自动地转换成指针,必须显式地将其指定为指针类型。
第7章 类 (Class)(简)
定义抽象数据类型
- 类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation)。
- 数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。
类成员 (Member)
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。
类的成员函数
- 成员函数的声明必须在类的内部。
- 成员函数的定义既可以在类的内部也可以在外部。
- 使用点运算符
.调用成员函数。 - 必须对任何
const或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。 ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }- 默认实参:
Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { } *this:- 每个成员函数都有一个额外的,隐含的形参
this。 this总是指向当前对象,因此this是一个常量指针。- 形参表后面的
const,改变了隐含的this形参的类型,如bool same_isbn(const Sales_item &rhs) const,这种函数称为“常量成员函数”(this指向的当前对象是常量)。 return *this;可以让成员函数连续调用。- 普通的非
const成员函数:this是指向类类型的const指针(可以改变this所指向的值,不能改变this保存的地址)。 const成员函数:this是指向const类类型的const指针(既不能改变this所指向的值,也不能改变this保存的地址)。
- 每个成员函数都有一个额外的,隐含的形参
非成员函数
- 和类相关的非成员函数,定义和声明都应该在类的外部。
类的构造函数
- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
- 构造函数是特殊的成员函数。
- 构造函数放在类的
public部分。 - 与类同名的成员函数。
Sales_item(): units_sold(0), revenue(0.0) { }=default要求编译器合成默认的构造函数。(C++11)- 初始化列表:冒号和花括号之间的代码:
Sales_item(): units_sold(0), revenue(0.0) { }
访问控制与封装
- 访问说明符(access specifiers):
public:定义在public后面的成员在整个程序内可以被访问;public成员定义类的接口。private:定义在private后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private隐藏了类的实现细节。
- 使用
class或者struct:都可以被用于定义一个类。唯一的却别在于访问权限。- 使用
class:在第一个访问说明符之前的成员是priavte的。 - 使用
struct:在第一个访问说明符之前的成员是public的。
- 使用
友元
- 允许特定的非成员函数访问一个类的私有成员.
- 友元的声明以关键字
friend开始。friend Sales_data add(const Sales_data&, const Sales_data&);表示非成员函数add可以访问类的非公有成员。 - 通常将友元声明成组地放在类定义的开始或者结尾。
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
封装的益处
- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
类的其他特性
- 成员函数作为内联函数
inline:- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明时显式地加上
inline。
- 可变数据成员 (mutable data member):
mutable size_t access_ctr;- 永远不会是
const,即使它是const对象的成员。
- 类类型:
- 每个类定义了唯一的类型。
类的作用域
- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。
构造函数再探
- 构造函数初始值列表:
- 类似
python使用赋值的方式有时候不行,比如const或者引用类型的数据,只能初始化,不能赋值。(注意初始化和赋值的区别) - 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
- 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。
- 类似
委托构造函数 (delegating constructor, C++11)
- 委托构造函数将自己的职责委托给了其他构造函数。
Sale_data(): Sale_data("", 0, 0) {}
隐式的类型转换
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。
- 编译器只会自动地执行
仅一步类型转换。 - 抑制构造函数定义的隐式转换:
- 将构造函数声明为
explicit加以阻止。 explicit构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
- 将构造函数声明为
聚合类 (aggregate class)
- 满足以下所有条件:
- 所有成员都是
public的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual函数。
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
字面值常量类
constexpr函数的参数和返回值必须是字面值。- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个
constexpr构造函数。 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
类的静态成员
- 非
static数据成员存在于类类型的每个对象中。 static数据成员独立于该类的任意对象而存在。- 每个
static数据成员是与类关联的对象,并不与该类的对象相关联。 - 声明:
- 声明之前加上关键词
static。
- 声明之前加上关键词
- 使用:
- 使用作用域运算符
::直接访问静态成员:r = Account::rate(); - 也可以使用对象访问:
r = ac.rate();
- 使用作用域运算符
- 定义:
- 在类外部定义时不用加
static。
- 在类外部定义时不用加
- 初始化:
- 通常不在类的内部初始化,而是在定义时进行初始化,如
double Account::interestRate = initRate(); - 如果一定要在类内部定义,则要求必须是字面值常量类型的
constexpr。
- 通常不在类的内部初始化,而是在定义时进行初始化,如
第7章 类
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程及设计技术。类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及其他私有函数。
定义抽象数据类型(Defining Abstract Data Types)
设计Sales_data类(Designing the Sales_data Class)
类的用户是程序员,而非应用程序的最终使用者。
定义改进的Sales_data类(Defining the Revised Sales_data Class)
成员函数(member function)的声明必须在类的内部,定义则既可以在类的内部也可以在类的外部。定义在类内部的函数是隐式的内联函数。
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 |
第8章 IO库(简)
前面章节已经在用的IO库设施
- istream:输入流类型,提供输入操作。
- ostream:输出流类型,提供输出操作
- cin:一个
istream对象,从标准输入读取数据。 - cout:一个
ostream对象,向标准输出写入数据。 - cerr:一个
ostream对象,向标准错误写入消息。 - >>运算符:用来从一个
istream对象中读取输入数据。 - <<运算符:用来向一个
ostream对象中写入输出数据。 - getline函数:从一个给定的
istream对象中读取一行数据,存入到一个给定的string对象中。
IO类
标准库定义的IO类型
iostream头文件:从标准流中读写数据,istream、ostream等。fstream头文件:从文件中读写数据,ifstream、ofstream等。sstream头文件:从字符串中读写数据,istringstream、ostringstream
IO对象不可复制或赋值
- 1.IO对象不能存在容器里.
- 2.形参和返回类型也不能是流类型。
- 3.形参和返回类型一般是流的引用。
- 4.读写一个IO对象会改变其状态,因此传递和返回的引用不能是
const的。
条件状态
| 状态 | 解释 |
|---|---|
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 |
上表中,strm是一种IO类型,(如istream), s是一个流对象。
管理输出缓冲
- 每个输出流都管理一个缓冲区,执行输出的代码,文本串可能立即打印出来,也可能被操作系统保存在缓冲区内,随后再打印。
- 刷新缓冲区,可以使用如下IO操纵符:
endl:输出一个换行符并刷新缓冲区。flush:刷新流,单不添加任何字符。ends:在缓冲区插入空字符null,然后刷新。unitbuf:告诉流接下来每次操作之后都要进行一次flush操作。nounitbuf:回到正常的缓冲方式。
文件输入输出
- 头文件
fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据。ofstream向一个给定文件写入数据。fstream可以读写给定文件。
- 文件流:需要读写文件时,必须定义自己的文件流对象,并绑定在需要的文件上。
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关联的文件是否成功打开且尚未关闭 |
上表中,fstream是头文件fstream中定义的一个类型,fstrm是一个文件流对象。
文件模式
| 文件模式 | 解释 |
|---|---|
in |
以读的方式打开 |
out |
以写的方式打开 |
app |
每次写操作前均定位到文件末尾 |
ate |
打开文件后立即定位到文件末尾 |
trunc |
截断文件 |
binary |
以二进制方式进行IO操作。 |
string流
- 头文件
sstream定义了三个类型来支持内存IO:istringstream从string读取数据。ostringstream向string写入数据。stringstream可以读写给定string。
stringstream特有的操作
| 操作 | 解释 |
|---|---|
sstream strm |
定义一个未绑定的stringstream对象 |
sstream strm(s) |
用s初始化对象 |
strm.str() |
返回strm所保存的string的拷贝 |
strm.str(s) |
将s拷贝到strm中,返回void |
上表中sstream是头文件sstream中任意一个类型。s是一个string。
第8章 IO库
部分IO库设施:
istream:输入流类型,提供输入操作。ostream:输出流类型,提供输出操作。cin:istream对象,从标准输入读取数据。cout:ostream对象,向标准输出写入数据。cerr:ostream对象,向标准错误写入数据。>>运算符:从istream对象读取输入数据。<<运算符:向ostream对象写入输出数据。getline函数:从istream对象读取一行数据,写入string对象。
IO类(The IO Classes)
头文件iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存中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库条件状态:
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可以同时读写指定文件。
使用文件流对象(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)
每个流都有一个关联的文件模式,用来指出如何使用文件。
- 只能对
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的数据。
使用istringstream(Using an istringstream)
1 | // members are public by default |
使用ostringstream(Using ostringstreams)
1 | for (const auto &entry : people) |
第9章 顺序容器(简)
顺序容器概述
- 顺序容器(sequential container):为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
顺序容器类型
| 容器类型 | 介绍 |
|---|---|
vector |
可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。 |
deque |
双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。 |
list |
双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快。 |
forward_list |
单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。 |
array |
固定大小数组。支持快速随机访问。不能添加或者删除元素。 |
string |
与vector相似的容器,但专门用于保存字符。随机访问块。在尾部插入/删除速度快。 |
- 除了固定大小的
array外,其他容器都提供高效、灵活的内存管理。 forward_list和array是新C++标准增加的类型。- 通常使用
vector是最好的选择,除非你有很好的理由选择其他容器。 - 新标准库的容器比旧版的快得多。
容器操作
类型
| 操作 | 解释 |
|---|---|
iterator |
此容器类型的迭代器类型 |
const_iterator |
可以读取元素但不能修改元素的迭代器类型 |
size_type |
无符号整数类型,足够保存此种容器类型最大可能的大小 |
difference_type |
带符号整数类型,足够保存两个迭代器之间的距离 |
value_type |
元素类型 |
reference |
元素的左值类型;和value_type &含义相同 |
const_reference |
元素的const左值类型,即const value_type & |
构造函数
| 操作 | 解释 |
|---|---|
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的元素 |
- 只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
array具有固定大小。- 和其他容器不同,默认构造的
array是非空的。 - 直接复制:将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须相同。
- 使用迭代器复制:不要求容器类型相同,容器内的元素类型也可以不同。
赋值和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的元素 |
- 使用非成员版本的
swap是一个好习惯。 assign操作不适用于关联容器和array
大小
| 操作 | 解释 |
|---|---|
c.size() |
c中元素的数目(不支持forward_list) |
c.max_size() |
c中可保存的最大元素数目 |
c.empty() |
若c中存储了元素,返回false,否则返回true |
添加元素
| 操作 | 解释 |
|---|---|
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.insert(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 |
- 因为这些操作会改变大小,因此不适用于
array。 forward_list有自己专有版本的insert和emplace。forward_list不支持push_back和emplace_back。- 当我们用一个对象去初始化容器或者将对象插入到容器时,实际上放入的是对象的拷贝。
emplace开头的函数是新标准引入的,这些操作是构造而不是拷贝元素。- 传递给
emplace的参数必须和元素类型的构造函数相匹配。
访问元素
| 操作 | 解释 |
|---|---|
c.back() |
返回c中尾元素的引用。若c为空,函数行为未定义 |
c.front() |
返回c中头元素的引用。若c为空,函数行为未定义 |
c[n] |
返回c中下标是n的元素的引用,n时候一个无符号证书。若n>=c.size(),则函数行为未定义 |
c.at(n) |
返回下标为n的元素引用。如果下标越界,则抛出out_of_range异常 |
- 访问成员函数返回的是引用。
at和下标操作只适用于string、vector、deque、array。back不适用于forward_list。- 如果希望下标是合法的,可以使用
at函数。
删除元素
| 操作 | 解释 |
|---|---|
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 |
- 会改变容器大小,不适用于
array。 forward_list有特殊版本的eraseforward_list不支持pop_backvector和string不支持pop_front
特殊的forwad_list操作
- 链表在删除元素时需要修改前置节点的内容,双向链表会前驱的指针,但是单向链表没有保存,因此需要增加获取前置节点的方法。
forward_list定义了before_begin,即首前(off-the-begining)迭代器,允许我们再在首元素之前添加或删除元素。
| 操作 | 解释 |
|---|---|
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指定的范围。 |
改变容器大小
| 操作 | 解释 |
|---|---|
c.resize(n) |
调整c的大小为n个元素,若n<c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化 |
c.resize(n, t) |
调整c的大小为n个元素,任何新添加的元素都初始化为值t |
获取迭代器
| 操作 | 解释 |
|---|---|
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
迭代器
- 迭代器范围:
begin到end,即第一个元素到最后一个元素的后面一个位置。 - 左闭合区间:
[begin, end) - 左闭合范围蕴含的编程设定:
- 如果
begin和end相等,则范围为空。 - 如果二者不等,则范围至少包含一个元素,且
begin指向该范围中的第一个元素。 - 可以对
begin递增若干次,使得begin == end。
- 如果
容器操作可能使迭代器失效
- 在向容器添加元素后:
- 如果容器是
vector或string,且存储空间被重新分配,则指向容器的迭代器、指针、引用都会失效。 - 对于
deque,插入到除首尾位置之外的任何位置都会导致指向容器的迭代器、指针、引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在元素的引用和指针不会失效。 - 对于
list和forward_list,指向容器的迭代器、指针和引用依然有效。
- 如果容器是
- 在从一个容器中删除元素后:
- 对于
list和forward_list,指向容器其他位置的迭代器、引用和指针仍然有效。 - 对于
deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、指针、引用都会失效;如果是删除deque的尾元素,则尾后迭代器会失效,但其他不受影响;如果删除的是deque的头元素,这些也不会受影响。 - 对于
vector和string,指向被删元素之前的迭代器、引用、指针仍然有效。 - 注意:当我们删除元素时,尾后迭代器总是会失效。
- 注意:使用失效的迭代器、指针、引用是严重的运行时错误!
- 建议:将要求迭代器必须保持有效的程序片段最小化。
- 建议:不要保存
end返回的迭代器。
- 对于
容器内元素的类型约束
- 元素类型必须支持赋值运算;
- 元素类型的对象必须可以复制。
- 除了输入输出标准库类型外,其他所有标准库类型都是有效的容器元素类型。
vector对象是如何增长的
vector和string在内存中是连续保存的,如果原先分配的内存位置已经使用完,则需要重新分配新空间,将已有元素从就位置移动到新空间中,然后添加新元素。
管理容量的成员函数
| 操作 | 解释 |
|---|---|
c.shrink_to_fit() |
将capacity()减少到和size()相同大小 |
c.capacity() |
不重新分配内存空间的话,c可以保存多少个元素 |
c.reverse(n) |
分配至少能容纳n个元素的内存空间 |
shrink_to_fit只适用于vector、string和dequecapacity和reverse只适用于vector和string。
额外的string操作
构造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个字符的拷贝。 |
n,len2,pos2都是无符号值。
substr操作
| 操作 | 解释 |
|---|---|
s.substr(pos, n) |
返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值是0,n的默认值是s.size() - pos,即拷贝从pos开始的所有字符。 |
改变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的引用。 |
string搜索操作
string类提供了6个不同的搜索函数,每个函数都有4个重载版本。- 每个搜索操作都返回一个
string::size_type值,表示匹配发生位置的下标。如果搜索失败则返回一个名为string::npos的static成员(类型是string::size_type,初始化值是-1,也就是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无默认值。 |
s.compare的几种参数形式
逻辑类似于C标准库的strcmp函数,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、正数或负数。
| 参数形式 | 解释 |
|---|---|
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个字符 |
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 |
容器适配器(adapter)
- 适配器是使一事物的行为类似于另一事物的行为的一种机制,例如
stack可以使任何一种顺序容器以栈的方式工作。 - 初始化
deque<int> deq; stack<int> stk(deq);从deq拷贝元素到stk。 - 创建适配器时,指定一个顺序容器,可以覆盖默认的基础容器:
stack<string, vector<string> > str_stk;。
适配器的通用操作和类型
| 操作 | 解释 |
|---|---|
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
| 操作 | 解释 |
|---|---|
s.pop() |
删除栈顶元素,不返回。 |
s.push(item) |
创建一个新元素,压入栈顶,该元素通过拷贝或移动item而来 |
s.emplace(args) |
同上,但元素由args来构造。 |
s.top() |
返回栈顶元素,不删除。 |
- 定义在
stack头文件中。 stack默认基于deque实现,也可以在list或vector之上实现。
queue和priority_queue
| 操作 | 解释 |
|---|---|
q.pop() |
删除队首元素,但不返回。 |
q.front() |
返回队首元素的值,不删除。 |
q.back() |
返回队尾元素的值,不删除。只适用于queue |
q.top() |
返回具有最高优先级的元素值,不删除。 |
q.push(item) |
在队尾压入一个新元素。 |
q.emplace(args) |
- 定义在
queue头文件中。 queue默认基于deque实现,priority_queue默认基于vector实现。queue可以在list或vector之上实现,priority_queue也可以用deque实现。
第9章 顺序容器
顺序容器概述(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)
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)
容器定义和初始化方式:
将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。
传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。
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)
容器赋值操作:
赋值运算符两侧的运算对象必须类型相同。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)
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外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。
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之前,要确保容器非空。
顺序容器的元素访问操作:
在容器中访问元素的成员函数都返回引用类型。如果容器是const对象,则返回const引用,否则返回普通引用。
可以快速随机访问的容器(string、vector、deque和array)都提供下标运算符。保证下标有效是程序员的责任。如果希望确保下标合法,可以使用at成员函数。at类似下标运算,但如果下标越界,at会抛出out_of_range异常。
1 | vector<string> svec; // empty vector |
删除元素(Erasing Elements)
顺序容器的元素删除操作:
删除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的插入和删除操作:
改变容器大小(Resizing a Container)
顺序容器的大小操作:
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 |
vector对象是如何增长的(How a vector Grows)
vector和string的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。
容器大小管理操作:
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对象拷贝字符构造string时,如果提供的拷贝开始位置(可选)大于给定string的大小,则构造函数会抛出out_of_range异常。
子字符串操作:
如果传递给substr函数的开始位置超过string的大小,则函数会抛出out_of_range异常。
改变string的其他方法(Other Ways to Change a string)
修改string的操作:
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搜索操作:
compare函数(The compare Functions)
string类型提供了一组compare函数进行字符串比较操作,类似C标准库的strcmp函数。
compare函数的几种参数形式:
数值转换(Numeric Conversions)
C++11增加了string和数值之间的转换函数:
进行数值转换时,string参数的第一个非空白字符必须是符号(+或-)或数字。它可以以0x或0X开头来表示十六进制数。对于转换目标是浮点值的函数,string参数也可以以小数点开头,并可以包含e或E来表示指数部分。
如果给定的string不能转换为一个数值,则转换函数会抛出invalid_argument异常。如果转换得到的数值无法用任何类型表示,则抛出out_of_range异常。
容器适配器(Container Adaptors)
标准库定义了stack、queue和priority_queue三种容器适配器。容器适配器可以改变已有容器的工作机制。
所有容器适配器都支持的操作和类型:
默认情况下,stack和queue是基于deque实现的,priority_queue是基于vector实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
1 | // empty stack implemented on top of vector |
所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在array上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用forward_list构造适配器。
栈适配器stack定义在头文件stack中,其支持的操作如下:
队列适配器queue和priority_queue定义在头文件queue中,其支持的操作如下:
queue使用先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。
第10章 泛型算法(简)
泛型算法
- 因为它们实现共同的操作,所以称之为“算法”;而“泛型”、指的是它们可以操作在多种容器类型上。
- 泛型算法本身不执行容器操作,只是单独依赖迭代器和迭代器操作实现。
- 头文件:
#include <algorithm>或者#include <numeric>(算数相关) - 大多数算法是通过遍历两个迭代器标记的一段元素来实现其功能。
- 必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但不能直接添加或者删除元素。
find
vector<int>::const_iterator result = find(vec.begin(), vec.end(), search_value);- 输入:两个标记范围的迭代器和目标查找值。返回:如果找到,返回对应的迭代器,否则返回第二个参数,即标记结尾的迭代器。
初识泛型算法
- 标准库提供了超过100个算法,但这些算法有一致的结构。
- 理解算法的最基本的方法是了解它们是否读取元素、改变元素、重排元素顺序。
只读算法
- 只读取范围中的元素,不改变元素。
- 如
find和accumulate(在numeric中定义,求和)。 find_first_of,输入:两对迭代器标记两段范围,在第一段中找第二段中任意元素,返回第一个匹配的元素,找不到返回第一段的end迭代器。- 通常最好使用
cbegin和cend。 equal:确定两个序列是否保存相同的值。
写容器元素的算法
- 一些算法将新值赋予序列中的元素。
- 算法不检查写操作。
fill:fill(vec.begin(), vec.end(), 0);将每个元素重置为0fill_n:fill_n(vec.begin(), 10, 0);- 插入迭代器
back_inserter:- 用来确保算法有足够的空间存储数据。
#include <iterator>back_inserter(vec)
- 拷贝算法
copy: - 输入:前两个参数指定输入范围,第三个指向目标序列。
copy (ilst.begin(), ilst.end(), back_inserter(ivec));copy时必须保证目标目的序列至少要包含与输入序列一样多的元素。
重排容器元素的算法
- 这些算法会重排容器中元素的顺序。
- 排序算法
sort:- 接受两个迭代器,表示要排序的元素范围。
- 消除重复
unique:- 之前要先调用
sort - 返回的迭代器指向最后一个不重复元素之后的位置。
- 顺序会变,重复的元素被“删除”。
- 并没有真正删除,真正删除必须使用容器操作。
- 之前要先调用
定制操作
向算法传递函数:
-
谓词(
predicate):- 是一个可调用的表达式,返回结果是一个能用作条件的值
- 一元谓词:接受一个参数
- 二元谓词:接受两个参数
-
例子:
stable_sort:- 保留相等元素的原始相对位置。
stable_sort(words.begin(), words.end(), isShorter);
lambda表达式
-
有时可能希望操作可以接受更多的参数。
-
lambda表达式表示一个可调用的代码单元,可以理解成是一个未命名的内联函数。 -
形式:
[capture list](parameter list) -> return type {function body}。- 其中
capture list捕获列表是一个lambda所在函数定义的局部变量的列表(通常为空)。不可忽略。 return type是返回类型。可忽略。parameter是参数列表。可忽略。function body是函数体。不可忽略。auto f = [] {return 42;}
- 其中
-
例子:
find_if:- 接受一对表示范围的迭代器和一个谓词,用来查找第一个满足特定要求的元素。返回第一个使谓词返回非0值的元素。
auto wc = find_if(words.begin(), words.end(), [sz](const string &a){return a.size() >= sz;});
for_each:- 接受一个可调用对象,并对序列中每个元素调用此对象。
for_each(wc, words.end(), [](const string &s){cout << s << " ";})
lambda捕获和返回
- 定义
lambda时会生成一个新的类类型和该类型的一个对象。 - 默认情况下,从
lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员,在lambda对象创建时被初始化。 - 值捕获:前提是变量可以拷贝,
size_t v1 = 42; auto f = [v1] {return v1;};。 - 引用捕获:必须保证在
lambda执行时,变量是存在的,auto f2 = [&v1] {return v1;}; - 尽量减少捕获的数据量,尽可能避免捕获指针或引用。
- 隐式捕获:让编译器推断捕获列表,在捕获列表中写一个
&(引用方式)或=(值方式)。auto f3 = [=] {return v1;}
lambda捕获列表:
| 捕获列表 | 解释 |
|---|---|
[] |
空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有在捕获变量后才能使用它们。 |
[names] |
names是一个逗号分隔的名字列表,这些名字都是在lambda所在函数的局部变量,捕获列表中的变量都被拷贝,名字前如果使用了&,则采用引用捕获方式。 |
[&] |
隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用。 |
[=] |
隐式捕获列表,采用值捕获方式。 |
[&, identifier_list] |
identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能使用& |
[=, identifier_list] |
identifier_list中的变量采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且前面必须使用& |
参数绑定
lambda表达式更适合在一两个地方使用的简单操作。- 如果是很多地方使用相同的操作,还是需要定义函数。
- 函数如何包装成一元谓词?使用参数绑定。
- 标准库
bind函数:- 定义在头文件
functional中,可以看做为一个通用的函数适配器。 auto newCallable = bind(callable, arg_list);- 我们再调用
newCallable的时候,newCallable会调用callable并传递给它arg_list中的参数。 _n代表第n个位置的参数。定义在placeholders的命名空间中。using std::placeholder::_1;auto g = bind(f, a, b, _2, c, _1);,调用g(_1, _2)实际上调用f(a, b, _2, c, _1)- 非占位符的参数要使用引用传参,必须使用标准库
ref函数或者cref函数。
- 定义在头文件
再探迭代器
插入迭代器
- 插入器是一种迭代器适配器,接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
- 三种类型:
back_inserter:创建一个使用push_back的迭代器。front_inserter创建一个使用push_front的迭代器。inserter创建一个使用insert的迭代器。接受第二个参数,即一个指向给定容器的迭代器,元素会被查到迭代器所指向的元素之前。
插入迭代器操作:
| 操作 | 解释 |
|---|---|
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 |
iostream迭代器
- 迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流。
- 通过使用流迭代器,我们可以用泛型算法从流对象中读取数据以及向其写入数据。
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++ |
使用元素类型所定义的>>运算符从流中读取下一个值。前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值。 |
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。 |
反向迭代器
- 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
- 对于反向迭代器,递增和递减的操作含义会颠倒。
- 实现向后遍历,配合
rbegin和rend。
泛型算法结构
5类迭代器
| 迭代器类别 | 解释 | 支持的操作 |
|---|---|---|
| 输入迭代器 | 只读,不写;单遍扫描,只能递增 | ==,!=,++,*,-> |
| 输出迭代器 | 只写,不读;单遍扫描,只能递增 | ++,* |
| 前向迭代器 | 可读写;多遍扫描,只能递增 | ==,!=,++,*,-> |
| 双向迭代器 | 可读写;多遍扫描,可递增递减 | ==,!=,++,--,*,-> |
| 随机访问迭代器 | 可读写,多遍扫描,支持全部迭代器运算 | ==,!=,<,<=,>,>=,++,--,+,+=,-,-=,*,->,iter[n]==*(iter[n]) |
算法的形参模式
alg(beg, end, other args);alg(beg, end, dest, other args);alg(beg, end, beg2, other args);alg(beg, end, beg2, end2, other args);
其中,alg是算法名称,beg和end表示算法所操作的输入范围。dest、beg2、end2都是迭代器参数,是否使用要依赖于执行的操作。
算法命名规范
- 一些算法使用重载形式传递一个谓词。
- 接受一个元素值的算法通常有一个不同名的版本:加
_if,接受一个谓词代替元素值。 - 区分拷贝元素的版本和不拷贝的版本:拷贝版本通常加
_copy。
特定容器算法
- 对于
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删除同一个值的连续拷贝。使用给定的二元谓词。 |
- 上面的操作都返回
void
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不能指向给定范围中的元素。 |
- 使用
lst.splice(args)或flst.splice_after(args)
第10章 泛型算法
概述(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不能修改其值。如果希望修改,就必须在参数列表后添加关键字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)
插入器是一种迭代器适配器,它接受一个容器参数,生成一个插入迭代器。通过插入迭代器赋值时,该迭代器调用容器操作向给定容器的指定位置插入一个元素。
插入迭代器操作:
插入器有三种类型,区别在于元素插入的位置:
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绑定到一个流时,标准库并不保证迭代器立即从流读取数据。但可以保证在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。
定义ostream_iterator对象时,必须将其绑定到一个指定的流。不允许定义空的或者表示尾后位置的ostream_iterator。
ostream_iterator操作:
*和++运算符实际上不会对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):
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成员函数版本的算法:
list和forward_list的splice函数可以进行容器合并,其参数如下:
链表特有版本的算法操作会改变底层容器。
第11章 关联容器(简)
- 关联容器和顺序容器的不同:关联容器中的元素时按照关键字来保存和访问的。
- 关联容器支持通过关键字来高效地查找和读取元素,基本的关联容器类型是
map和set。
关联容器类型:
| 容器类型 | 解释 |
|---|---|
| 按顺序存储 | |
map |
关键数组:保存关键字-值对 |
set |
关键字即值,即只保存关键字的容器 |
multimap |
支持同一个键多次出现的map |
multiset |
支持同一个键多次出现的set |
| 无序集合 | |
unordered_map |
用哈希函数组织的map |
unordered_set |
用哈希函数组织的set |
unordered_multimap |
哈希组织的map,关键字可以重复出现 |
unordered_multiset |
哈希组织的set,关键字可以重复出现 |
关联容器概述
定义关联容器
- 需要指定元素类型。
- 列表初始化:
map:map<string, int> word_count = {{"a", 1}, {"b", 2}};set:set<string> exclude = {"the", "a"};
关键字类型的要求
- 对于有序容器,关键字类型必须定义元素比较的方法。默认是
<。 - 如果想传递一个比较的函数,可以这样定义:
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
pair
- 在
utility头文件中定义。 - 一个
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 |
同上 |
关联容器操作
关联容器额外的类型别名:
| 类型别名 | 解释 |
|---|---|
key_type |
此容器类型的关键字类型 |
mapped_type |
每个关键字关联的类型,只适用于map |
value_type |
对于map,是pair<const key_type, mapped_type>; 对于set,和key_type相同。 |
关联容器迭代器
- 解引用一个关联容器迭代器时,会得到一个类型为容器的
value_type的值的引用。 set的迭代器是const的。- 遍历关联容器:使用
begin和end,遍历map、multimap、set、multiset时,迭代器按关键字升序遍历元素。
添加元素
关联容器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作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。 |
向map添加元素:
word_count.insert({word, 1});word_count.insert(make_pair(word, 1));word_count.insert(pair<string, size_t>(word, 1));word_count.insert(map<string, size_t>::value_type (word, 1));
删除元素
从关联容器中删除元素:
| 操作 | 解释 |
|---|---|
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。 |
下标操作
map和unordered_map的下标操作:
| 操作 | 解释 |
|---|---|
c[k] |
返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其值初始化。 |
c.at(k) |
访问关键字为k的元素,带参数检查;若k不存在在c中,抛出一个out_of_range异常。 |
查找元素
在一个关联容器中查找元素:
| 操作 | 解释 |
|---|---|
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()。 |
lower_bound和upper_bound不适用于无序容器。- 下标和
at操作只适用于非const的map和unordered_map。
无序容器
- 有序容器使用比较运算符来组织元素;无序容器使用哈希函数和关键字类型的
==运算符。 - 理论上哈希技术可以获得更好的性能。
- 无序容器在存储上组织为一组桶(bucket),每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。
无序容器管理操作:
| 操作 | 解释 |
|---|---|
| 桶接口 | |
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。 |
第11章 关联容器
关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是map和set。
map中的元素是一些键值对(key-value):关键字起索引作用,值表示与索引相关联的数据。set中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在set中。
标准库提供了8个关联容器,它们之间的不同体现在三个方面:
- 是
map还是set类型。 - 是否允许保存重复的关键字。
- 是否按顺序保存元素。
允许重复保存关键字的容器名字都包含单词multi;无序保存元素的容器名字都以单词unordered开头。
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支持的操作:
在C++11中,如果函数需要返回pair,可以对返回值进行列表初始化。早期C++版本中必须显式构造返回值。
1 | pair<string, int> process(vector<string> &v) |
关联容器操作(Operations on Associative Containers)
关联容器定义了类型别名来表示容器关键字和值的类型:
对于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或emplace的返回值依赖于容器类型和参数:
- 对于不包含重复关键字的容器,添加单一元素的
insert和emplace版本返回一个pair,表示操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值。如果关键字已在容器中,则insert直接返回,bool值为false。如果关键字不存在,元素会被添加至容器中,bool值为true。 - 对于允许包含重复关键字的容器,添加单一元素的
insert和emplace版本返回指向新元素的迭代器。
删除元素(Erasing Elements)
关联容器的删除操作:
与顺序容器不同,关联容器提供了一个额外的erase操作。它接受一个key_type参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量。对于不包含重复关键字的容器,erase的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。
map的下标操作(Subscripting a map)
map下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。
由于下标运算符可能向容器中添加元素,所以只能对非const的map使用下标操作。
map和unordered_map的下标操作:
对map进行下标操作时,返回的是mapped_type类型的对象;解引用map迭代器时,返回的是value_type类型的对象。
访问元素(Accessing Elements)
关联容器的查找操作:
如果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)和关键字类型的==运算符组织元素。
无序容器和对应的有序容器通常可以相互替换。但是由于元素未按顺序存储,使用无序容器的程序输出一般会与有序容器的版本不同。
无序容器在存储上组织为一组桶,每个桶保存零或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。
无序容器管理操作:
默认情况下,无序容器使用关键字类型的==运算符比较元素,还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型和一些标准库类型提供了hash模板。因此可以直接定义关键字是这些类型的无序容器,而不能直接定义关键字类型为自定义类类型的无序容器,必须先提供对应的hash模板版本。
第12章 动态内存(简)
-
对象的生命周期:
- 全局对象在程序启动时分配,结束时销毁。
- 局部对象在进入程序块时创建,离开块时销毁。
- 局部
static对象在第一次使用前分配,在程序结束时销毁。 - 动态分配对象:只能显式地被释放。
-
对象的内存位置:
- 静态内存用来保存局部
static对象、类static对象、定义在任何函数之外的变量。 - 栈内存用来保存定义在函数内的非
static对象。 - 堆内存,又称自由空间,用来存储动态分配的对象。
- 静态内存用来保存局部
动态内存与智能指针
- 动态内存管理:
new:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
- 智能指针:
- 管理动态对象。
- 行为类似常规指针。
- 负责自动释放所指向的对象。
- 智能指针也是模板。
shared_ptr类
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共享对象的智能指针数量;可能很慢,主要用于调试。 |
- 使用动态内存的三种原因:
- 程序不知道自己需要使用多少对象(比如容器类)。
- 程序不知道所需要对象的准确类型。
- 程序需要在多个对象间共享数据。
直接管理内存
- 用
new动态分配和初始化对象。new无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。int *pi = new int(123);- 一旦内存耗尽,会抛出类型是
bad_alloc的异常。
- 用
delete将动态内存归还给系统。- 接受一个指针,指向要释放的对象。
delete后的指针称为空悬指针(dangling pointer)。
- 使用
new和delete管理动态内存存在三个常见问题:- 1.忘记
delete内存。 - 2.使用已经释放掉的对象。
- 3.同一块内存释放两次。
- 1.忘记
- 坚持只使用智能指针可以避免上述所有问题。
shared_ptr和new结合使用
定义和改变shared_ptr的其他方法:
| 操作 | 解释 |
|---|---|
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) |
同上 |
智能指针和异常
- 如果使用智能指针,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
- 智能指针陷阱:
- 不用相同的内置指针初始化(或
reset)多个智能指针 - 不
delete get()返回的指针。 - 如果你使用
get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。 - 如果你使用智能指针管理的资源不是
new分配的内存,记住传递给它一个删除器。
- 不用相同的内置指针初始化(或
unique_ptr
- 某一个时刻只能有一个
unique_ptr指向一个给定的对象。 - 不支持拷贝或者赋值操作。
- 向后兼容:
auto_ptr:老版本,具有unique_ptr的部分特性。特别是,不能在容器中保存auto_ptr,也不能从函数返回auto_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置空 |
weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针。- 指向一个由
shared_ptr管理的对象,不改变shared_ptr的引用计数。 - 一旦最后一个指向对象的
shared_ptr被销毁,对象就会被释放,不管有没有weak_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。 |
动态数组
new和数组
-
new一个动态数组:- 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
- 返回指向第一个对象的指针。
int *p = new int[size];
-
delete一个动态数组:delete [] p;
-
unique_ptr和数组:- 指向数组的
unique_ptr不支持成员访问运算符(点和箭头)。
- 指向数组的
| 操作 | 解释 |
|---|---|
unique_ptr<T[]> u |
u可以指向一个动态分配的数组,整数元素类型为T |
unique_ptr<T[]> u(p) |
u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*。 |
u[i] |
返回u拥有的数组中位置i处的对象。u必须指向一个数组。 |
allocator类
- 标准库
allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开。 - 分配的是原始的、未构造的内存。
allocator是一个模板。allocator<string> alloc;
标准库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伴随算法:
| 操作 | 解释 |
|---|---|
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必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。 |
- 定义在头文件
memory中。 - 在给定目的位置创建元素,而不是由系统分配内存给他们。
第12章 动态内存
程序用堆(heap)来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序控制。
动态内存与智能指针(Dynamic Memory and Smart Pointers)
C++中的动态内存管理通过一对运算符完成:new在动态内存中为对象分配空间并返回指向该对象的指针,可以选择对对象进行初始化;delete接受一个动态对象的指针,销毁该对象并释放与之关联的内存。
新标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,但它自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr独占所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在头文件memory中。
shared_ptr类(The shared_ptr Class)
智能指针是模板,创建时需要指明指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。
1 | shared_ptr<string> p1; // shared_ptr that can point at a string |
shared_ptr和unique_ptr都支持的操作:
shared_ptr独有的操作:
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 |
默认情况下,用来初始化智能指针的内置指针必须指向动态内存,因为智能指针默认使用delete释放它所管理的对象。如果要将智能指针绑定到一个指向其他类型资源的指针上,就必须提供自定义操作来代替delete。
不要混合使用内置指针和智能指针。当将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操作:
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时,需要使用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不同,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分配的内存是未构造的,程序需要在此内存中构造对象。新标准库的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的目的位置迭代器必须指向未构造的内存,它直接在给定位置构造元素。返回(递增后的)目的位置迭代器。
第13章 拷贝控制(简)
拷贝控制操作(copy control):
- 拷贝构造函数(copy constructor)
- 拷贝赋值运算符(copy-assignment operator)
- 移动构造函数(move constructor)
- 移动赋值函数(move-assignement operator)
- 析构函数(destructor)
拷贝、赋值和销毁
拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo{ public: Foo(const Foo&); }- 合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
- 拷贝初始化:
- 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
- 通常使用拷贝构造函数完成。
string book = "9-99";- 出现场景:
- 用
=定义变量时。 - 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
- 用
拷贝赋值运算符
- 重载赋值运算符:
- 重写一个名为
operator=的函数. - 通常返回一个指向其左侧运算对象的引用。
Foo& operator=(const Foo&);
- 重写一个名为
- 合成拷贝赋值运算符:
- 将右侧运算对象的每个非
static成员赋予左侧运算对象的对应成员。
- 将右侧运算对象的每个非
析构函数
- 释放对象所使用的资源,并销毁对象的非
static数据成员。 - 名字由波浪号接类名构成。没有返回值,也不接受参数。
~Foo();- 调用时机:
- 变量在离开其作用域时。
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,其元素被销毁。
- 动态分配的对象,当对指向它的指针应用
delete运算符时。 - 对于临时对象,当创建它的完整表达式结束时。
- 合成析构函数:
- 空函数体执行完后,成员会被自动销毁。
- 注意:析构函数体本身并不直接销毁成员。
三/五法则
- 需要析构函数的类也需要拷贝和赋值操作。
- 需要拷贝操作的类也需要赋值操作,反之亦然。
使用=default
- 可以通过将拷贝控制成员定义为
=default来显式地要求编译器生成合成的版本。 - 合成的函数将隐式地声明为内联的。
阻止拷贝
- 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
- 定义删除的函数:
=delete。 - 虽然声明了它们,但是不能以任何方式使用它们。
- 析构函数不能是删除的成员。
- 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
- 老版本使用
private声明来阻止拷贝。
拷贝控制和资源管理
- 类的行为可以像一个值,也可以像一个指针。
- 行为像值:对象有自己的状态,副本和原对象是完全独立的。
- 行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
交换操作
- 管理资源的类通常还定义一个名为
swap的函数。 - 经常用于重排元素顺序的算法。
- 用
swap而不是std::swap。
对象移动
- 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
- 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
- 标准库容器、
string和shared_ptr类既可以支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
右值引用
- 新标准引入右值引用以支持移动操作。
- 通过
&&获得右值引用。 - 只能绑定到一个将要销毁的对象。
- 常规引用可以称之为左值引用。
- 左值持久,右值短暂。
move函数:
int &&rr2 = std::move(rr1);move告诉编译器,我们有一个左值,但我希望像右值一样处理它。- 调用
move意味着:除了对rr1赋值或者销毁它外,我们将不再使用它。
移动构造函数和移动赋值运算符
- 移动构造函数:
- 第一个参数是该类类型的一个引用,关键是,这个引用参数是一个右值引用。
StrVec::StrVec(StrVec &&s) noexcept{}- 不分配任何新内存,只是接管给定的内存。
- 移动赋值运算符:
StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
- 移动右值,拷贝左值。
- 如果没有移动构造函数,右值也被拷贝。
- 更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
- 移动迭代器:
make_move_iterator函数讲一个普通迭代器转换为一个移动迭代器。
- 建议:小心地使用移动操作,以获得性能提升。
右值引用和成员函数
- 区分移动和拷贝的重载函数通常有一个版本接受一个
const T&,而另一个版本接受一个T&&。 - 引用限定符:
- 在参数列表后面防止一个
&,限定只能向可修改的左值赋值而不能向右值赋值。
- 在参数列表后面防止一个
第13章 拷贝控制
一个类通过定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作。
- 拷贝构造函数(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 |
第14章 重载运算与类型转换(简)
基本概念
- 重载运算符是具有特殊名字的函数:由关键字
operator和其后要定义的运算符号共同组成。 - 当一个重载的运算符是成员函数时,
this绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个。 - 只能重载大多数的运算符,而不能发明新的运算符号。
- 重载运算符的优先级和结合律跟对应的内置运算符保持一致。
- 调用方式:
data1 + data2;operator+(data1, data2);
- 是否是成员函数:
- 赋值(
=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。 - 复合赋值运算符一般来说是成员。
- 改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。
- 具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。
- 赋值(
运算符:
| 可以被重载 | 不可以被重载 |
|---|---|
+, -, *, /, %, ^ |
::, .*, ., ? :, |
&, |, ~, !, ,, = |
|
<, >, <=, >=, ++, -- |
|
<<, >>, ==, !=, &&, || |
|
+=, -=, /=, %=, ^=, &= |
|
|=, *=, <<=, >>=, [], () |
|
->, ->*, new, new[], delete, delete[] |
输入和输出运算符
重载输出运算符<<
- 第一个形参通常是一个非常量的
ostream对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream对象。 - 输入输出运算符必须是非成员函数。
重载输入运算符>>
- 第一个形参通常是运算符将要读取的流的因不用,第二个形参是将要读取到的(非常量)对象的引用。
- 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
算数和关系运算符(+、-、*、/)
- 如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。
相等运算符==
- 如果定义了
operator==,则这个类也应该定义operator!=。 - 相等运算符和不等运算符的一个应该把工作委托给另一个。
- 相等运算符应该具有传递性。
- 如果某个类在逻辑上有相等性的含义,则该类应该定义
operator==,这样做可以使用户更容易使用标准库算法来处理这个类。
关系运算符
- 如果存在唯一一种逻辑可靠的
<定义,则应该考虑为这个类定义<运算符。如果同时还包含==,则当且晋档<的定义和++产生的结果一直时才定义<运算符。
赋值运算符=
- 我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
- 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。
下标运算符[]
- 下标运算符必须是成员函数。
- 一般会定义两个版本:
- 1.返回普通引用。
- 2.类的常量成员,并返回常量引用。
递增和递减运算符(++、–)
- 定义递增和递减运算符的类应该同时定义前置版本和后置版本。
- 通常应该被定义成类的成员。
- 为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
- 同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
- 后置版本接受一个额外的,不被使用的
int类型的形参。因为不会用到,所以无需命名。
成员访问运算符(*、->)
- 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
- 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
- 解引用和乘法的区别是一个是一元运算符,一个是二元运算符。
函数调用运算符
- 可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
- 函数调用运算符必须是成员函数。
- 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
- 如果累定义了调用运算符,则该类的对象称作函数对象。
lambda是函数对象
lambda捕获变量:lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。
标准库定义的函数对象
标准库函数对象:
| 算术 | 关系 | 逻辑 |
|---|---|---|
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> |
- 可以在算法中使用标准库函数对象。
可调用对象与function
标准库function类型:
| 操作 | 解释 |
|---|---|
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类型,它可以表示接受两个int,返回一个int的可调用对象。function<int(int, int)>
重载、类型转换、运算符
类型转换运算符
- 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:
operator type() const; - 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是
const。 - 避免过度使用类型转换函数。
- C++11引入了显式的类型转换运算符。
- 向
bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。
避免有二义性的类型转换
- 通常,不要为类第几个亿相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。
- 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。
函数匹配与重载运算符
- 如果
a是一种类型,则表达式a sym b可能是:a.operatorsym(b);operatorsym(a,b);
- 如果我们队同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
第14章 重载运算与类型转换
基本概念(Basic Concepts)
重载的运算符是具有特殊名字的函数,它们的名字由关键字operator和其后要定义的运算符号组成。
重载运算符函数的参数数量和该运算符作用的运算对象数量一样多。对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
如果一个运算符函数是类的成员函数,则它的第一个运算对象会绑定到隐式的this指针上。因此成员运算符函数的显式参数数量比运算对象的数量少一个。
当运算符作用于内置类型的运算对象时,无法改变该运算符的含义。
只能重载大多数已有的运算符,无权声明新的运算符号。
重载运算符的优先级和结合律与对应的内置运算符一致。
可以像调用普通函数一样直接调用运算符函数。
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中定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。这些类都被定义为模板的形式,可以为其指定具体的应用类型(即调用运算符的形参类型)。
关系运算符的函数对象类通常被用来替换算法中的默认运算符,这些类对于指针同样适用。
1 | vector<string *> nameTable; // vector of pointers |
可调用对象与function(Callable Objects and function)
调用形式指明了调用返回的类型以及传递给调用的实参类型。不同的可调用对象可能具有相同的调用形式。
标准库function类型是一个模板,定义在头文件functional中,用来表示对象的调用形式。
创建一个具体的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 |
如果类既定义了转换目标是算术类型的类型转换,也定义了重载的运算符,则会遇到重载运算符与内置运算符的二义性问题。
第15章 面向对象程序设计(简)
OOP:概述
- 面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。
- 继承(inheritance):
- 通过继承联系在一起的类构成一种层次关系。
- 通常在层次关系的根部有一个基类(base class)。
- 其他类直接或者简介从基类继承而来,这些继承得到的类成为派生类(derived class)。
- 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
- 对于某些函数,基类希望它的派生类个自定义适合自己的版本,此时基类就将这些函数声明成虚函数(virtual function)。
- 派生类必须通过使用类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前都可以有访问说明符。
class Bulk_quote : public Quote{}; - 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual关键字,也可以不加。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override关键字。
- 动态绑定(dynamic binding,又称运行时绑定):
- 使用同一段代码可以分别处理基类和派生类的对象。
- 函数的运行版本由实参决定,即在运行时选择函数的版本。
定义基类和派生类
定义基类
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
- 基类通过在其成员函数的声明语句前加上关键字
virtual使得该函数执行动态绑定。 - 如果成员函数没有被声明为虚函数,则解析过程发生在编译时而非运行时。
- 访问控制:
protected: 基类和和其派生类还有友元可以访问。private: 只有基类本身和友元可以访问。
定义派生类
- 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有一下三种访问说明符的一个:
public、protected、private。 - C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override关键字。 - 派生类构造函数:派生类必须使用基类的构造函数去初始化它的基类部分。
- 静态成员:如果基类定义了一个基类成员,则在整个继承体系中只存在该成员的唯一定义。
- 派生类的声明:声明中不包含它的派生列表。
- C++11新标准提供了一种防止继承的方法,在类名后面跟一个关键字
final。
类型转换与继承
- 理解基类和派生类之间的类型抓换是理解C++语言面向对象编程的关键所在。
- 可以将基类的指针或引用绑定到派生类对象上。
- 不存在从基类向派生类的隐式类型转换。
- 派生类向基类的自动类型转换只对指针或引用类型有效,对象之间不存在类型转换。
虚函数
- 使用虚函数可以执行动态绑定。
- OOP的核心思想是多态性(polymorphism)。
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
- 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual关键字,也可以不加。 - C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override关键字。 - 如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上
override可以明确程序员的意图,让编译器帮忙确认参数列表是否出错。 - 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
- 通常,只有成员函数(或友元)中的代码才需要使用作用域运算符(
::)来回避虚函数的机制。
抽象基类
- 纯虚函数(pure virtual):清晰地告诉用户当前的函数是没有实际意义的。纯虚函数无需定义,只用在函数体的位置前书写
=0就可以将一个虚函数说明为纯虚函数。 - 含有纯虚函数的类是抽象基类(abstract base class)。不能创建抽象基类的对象。
访问控制与继承
- 受保护的成员:
protected说明符可以看做是public和private中的产物。- 类似于私有成员,受保护的成员对类的用户来说是不可访问的。
- 类似于公有成员,受保护的成员对于派生类的成员和友元来说是可访问的。
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
- 派生访问说明符:
- 对于派生类的成员(及友元)能否访问其直接积累的成员没什么影响。
- 派生访问说明符的目的是:控制派生类用户对于基类成员的访问权限。比如
struct Priv_Drev: private Base{}意味着在派生类Priv_Drev中,从Base继承而来的部分都是private的。
- 友元关系不能继承。
- 改变个别成员的可访问性:使用
using。 - 默认情况下,使用
class关键字定义的派生类是私有继承的;使用struct关键字定义的派生类是公有继承的。
继承中的类作用域
- 每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
- 派生类的成员将隐藏同名的基类成员。
- 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
构造函数与拷贝控制
虚析构函数
- 基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
- 如果基类的析构函数不是虚函数,则
delete一个指向派生类对象的基类指针将产生未定义的行为。 - 虚析构函数将阻止合成移动操作。
合成拷贝控制与继承
- 基类或派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似:他们对类本身的成员依次进行初始化、赋值或销毁的操作。
派生类的拷贝控制成员
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
- 派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。
继承的构造函数
- C++11新标准中,派生类可以重用其直接基类定义的构造函数。
- 如
using Disc_quote::Disc_quote;,注明了要继承Disc_quote的构造函数。
容器与继承
- 当我们使用容器存放继承体系中的对象时,通常必须采用间接存储的方式。
- 派生类对象直接赋值给积累对象,其中的派生类部分会被切掉。
- 在容器中放置(智能)指针而非对象。
- 对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程。相反,我们必须使用指针和引用。因为指针会增加程序的复杂性,所以经常定义一些辅助的类来处理这些复杂的情况。
文本查询程序再探
- 使系统支持:单词查询、逻辑非查询、逻辑或查询、逻辑与查询。
面向对象的解决方案
- 将几种不同的查询建模成相互独立的类,这些类共享一个公共基类:
WordQueryNotQueryOrQueryAndQuery
- 这些类包含两个操作:
eval:接受一个TextQuery对象并返回一个QueryResult。rep:返回基础查询的string表示形式。
- 继承和组合:
- 当我们令一个类公有地继承另一个类时,派生类应当反映与基类的“是一种(Is A)”的关系。
- 类型之间另一种常见的关系是“有一个(Has A)”的关系。
- 对于面向对象编程的新手来说,想要理解一个程序,最困难的部分往往是理解程序的设计思路。一旦掌握了设计思路,接下来的实现也就水到渠成了。
Query程序设计:
| 操作 | 解释 |
|---|---|
Query程序接口类和操作 |
|
TextQuery |
该类读入给定的文件并构建一个查找图。包含一个query操作,它接受一个string实参,返回一个QueryResult对象;该QueryResult对象表示string出现的行。 |
QueryResult |
该类保存一个query操作的结果。 |
Query |
是一个接口类,指向Query_base派生类的对象。 |
Query q(s) |
将Query对象q绑定到一个存放着string s的新WordQuery对象上。 |
q1 & q2 |
返回一个Query对象,该Query绑定到一个存放q1和q2的新AndQuery对象上。 |
q1 | q2 |
返回一个Query对象,该Query绑定到一个存放q1和q2的新OrQuery对象上。 |
~q |
返回一个Query对象,该Query绑定到一个存放q的新NotQuery对象上。 |
Query程序实现类 |
|
Query_base |
查询类的抽象基类 |
WordQuery |
Query_base的派生类,用于查找一个给定的单词 |
NotQuery |
Query_base的派生类,用于查找一个给定的单词 |
BinaryQuery |
Query_base的派生类,查询结果是Query运算对象没有出现的行的集合 |
OrQuery |
Query_base的派生类,返回它的两个运算对象分别出现的行的并集 |
AndQuery |
Query_base的派生类,返回它的两个运算对象分别出现的行的交集 |
第15章 面向对象程序设计
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)
因为容器中不能保存不同类型的元素,所以不能把具有继承关系的多种类型的对象直接存储在容器中。
容器不能和存在继承关系的类型兼容。
如果想在容器中存储具有继承关系的对象,则应该存放基类的指针。
第16章 模板和泛型编程(简)
- 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。
- OOP能处理类型在程序运行之前都未知的情况;
- 泛型编程中,在编译时就可以获知类型。
定义模板
- 模板:模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。
函数模板
template <typename T> int compare(const T &v1, const T &v2){}- 模板定义以关键字
template开始,后接模板形参表,模板形参表是用尖括号<>括住的一个或多个模板形参的列表,用逗号分隔,不能为空。 - 使用模板时,我们显式或隐式地指定模板实参,将其绑定到模板参数上。
- 模板类型参数:类型参数前必须使用关键字
class或者typename,这两个关键字含义相同,可以互换使用。旧的程序只能使用class。 - 非类型模板参数:表示一个值而非一个类型。实参必须是常量表达式。
template <class T, size_t N> void array_init(T (&parm)[N]){} - 内联函数模板:
template <typename T> inline T min(const T&, const T&); - 模板程序应该尽量减少对实参类型的要求。
- 函数模板和类模板成员函数的定义通常放在头文件中。
类模板
- 类模板用于生成类的蓝图。
- 不同于函数模板,编译器不能推断模板参数类型。
- 定义类模板:
template <class Type> class Queue {};
- 实例化类模板:提供显式模板实参列表,来实例化出特定的类。
- 一个类模板中所有的实例都形成一个独立的类。
- 模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
- 类模板的成员函数:
template <typename T> ret-type Blob::member-name(parm-list)
- 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
- 新标准允许模板将自己的类型参数成为友元。
template <typename T> class Bar{friend T;};。 - 模板类型别名:因为模板不是一个类型,因此无法定义一个
typedef引用一个模板,但是新标准允许我们为类模板定义一个类型别名:template<typename T> using twin = pair<T, T>;
模板参数
- 模板参数与作用域:一个模板参数名的可用范围是在声明之后,至模板声明或定义结束前。
- 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置。
- 当我们希望通知编译器一个名字表示类型时,必须使用关键字
typename,而不能使用class。 - 默认模板实参:
template <class T = int> class Numbers{}
成员模板
- 成员模板(member template):本身是模板的函数成员。
- 普通(非模板)类的成员模板。
- 类模板的成员模板。
控制实例化
- 动机:在多个文件中实例化相同模板的额外开销可能非常严重。
- 显式实例化:
extern template declaration; // 实例化声明template declaration; // 实例化定义
效率与灵活性
模板实参推断
- 对函数模板,编译器利用调用中的函数实参来确定其模板参数,这个过程叫模板实参推断。
类型转换与模板类型参数
- 能够自动转换类型的只有:
- 和其他函数一样,顶层
const会被忽略。 - 数组实参或函数实参转换为指针。
- 和其他函数一样,顶层
函数模板显式实参
- 某些情况下,编译器无法推断出模板实参的类型。
- 定义:
template <typename T1, typename T2, typename T3> T1 sum(T2, T3); - 使用函数显式实参调用:
auto val3 = sum<long long>(i, lng); // T1是显式指定,T2和T3都是从函数实参类型推断而来 - 注意:正常类型转换可以应用于显式指定的实参。
尾置返回类型与类型转换
- 使用场景:并不清楚返回结果的准确类型,但知道所需类型是和参数相关的。
template <typename It> auto fcn(It beg, It end) -> decltype(*beg)- 尾置返回允许我们在参数列表之后声明返回类型。
标准库的类型转换模板:
- 定义在头文件
type_traits中。
对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 |
函数指针和实参推断
- 当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
模板实参推断和引用
- 从左值引用函数推断类型:若形如
T&,则只能传递给它一个左值。但如果是const T&,则可以接受一个右值。 - 从右值引用函数推断类型:若形如
T&&,则只能传递给它一个右值。 - 引用折叠和右值引用参数:
- 规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
T&&),编译器会推断模板类型参数为实参的左值引用类型。 - 规则2:如果我们间接创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。对于一个给定类型
X:X& &、X& &&和X&& &都折叠成类型X&。- 类型
X&& &&折叠成X&&。
- 上面两个例外规则导致两个重要结果:
- 1.如果一个函数参数是一个指向模板类型参数的右值引用(如
T&&),则它可以被绑定到一个左值上; - 2.如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数(
T&)。
- 1.如果一个函数参数是一个指向模板类型参数的右值引用(如
- 规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
理解std::move
- 标准库
move函数是使用右值引用的模板的一个很好的例子。 - 从一个左值
static_cast到一个右值引用是允许的。
1 | template <typename T> |
转发
- 使用一个名为
forward的新标准库设施来传递参数,它能够保持原始实参的类型。 - 定义在头文件
utility中。 - 必须通过显式模板实参来调用。
forward返回显式实参类型的右值引用。即,forward<T>的返回类型是T&&。
重载与模板
- 多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
- 非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
可变参数模板
可变参数模板就是一个接受可变数目参数的模板函数或模板类。
- 可变数目的参数被称为参数包。
- 模板参数包:标识另个或多个模板参数。
- 函数参数包:标识另个或者多个函数参数。
- 用一个省略号来指出一个模板参数或函数参数,表示一个包。
template <typename T, typename... Args>,Args第一个模板参数包。void foo(const T &t, const Args& ... rest);,rest是一个函数参数包。sizeof...运算符,返回参数的数目。
编写可变参数函数模板
- 可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
包扩展
- 对于一个参数包,除了获取它的大小,唯一能做的事情就是扩展(expand)。
- 扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。
转发参数包
- 新标准下可以组合使用可变参数模板和
forward机制,实现将实参不变地传递给其他函数。
模板特例化(Specializations)
- 定义函数模板特例化:关键字
template后面跟一个空尖括号对(<>)。 - 特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
- 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
- 我们可以部分特例化类模板,但不能部分特例化函数模板。
第16章 模板与泛型编程
定义模板(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成员就是模板参数类型本身。
使用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&&。
- 推断出的
-
在
std::move(s1)中传递的是左值。- 推断出的
T类型为string&。 remove_reference用string&进行实例化。remove_reference<string&>的type成员是string。move的返回类型是string&&。move的函数参数t的类型为string& &&,会折叠成string&。
- 推断出的
可以使用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> |








