抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

CodingStudio

努力进步

引言

  • C++ Primer的学习笔记

1 开始

1.2 初始输入输出

  • iostream库为IO库,其中包含两个基本类型istream和ostream,分别表示输入流和输出流
    • 一个流就是一个字符序列,是从IO设备读出或写入IO设备的
  • 标准输入输出对象
    • cin标准输入
    • cout标准输出
    • cerr输出警告和错误信息,标准错误
    • clog输出程序运行时的一般性信息
  • 向流中写入数据
    • 输出运算符<<
      • 接受两个运算对象,左边为ostream对象,右边为要打印的值
      • std::endl为操纵符:结束当前行,并将与设备关联的缓冲区中的内容刷到设备中
        • 缓冲刷新操作可以保证到目前为止程序所产生的所有输出真正写入输出流中,而不是仅停留在内存中等待写入流
  • 从流中读取数据
    • 输入运算符>>

2 变量和基本类型

2.1 基本内置类型

类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8位
wchar_t 宽字符16
char16_t unicode字符16
char32_t unicode字符32
short 短整型16
int 整型16
long 长整型32
long long 长整型64
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字
  • C++规定,一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大
    • long long在C++11中新定义
    • 一个字节为8bit,一个字32或64bit
  • 带符号类型和无符号类型
    • int,short,long和long long都是带符号的,在类型名前添加unsigned就可以得到无符号类型
    • char,signed,unsigned char:char,signed并不一样
  • 类型转换
    • 赋给带符号类型一个超出表示范围的值时,结果是未定
    • 切勿混用带符号类型和无符号类型
  • 字面值常量
    • 以0开头的整数表示八进制数,以0x开头的整数表示十六进制数
    • 浮点数字面值表现位一个小数或以科学计数法表示的指数,其中指数部分用E标识,默认的浮点型字面值为double
    • 单引号括起来的单个字符称为char型字面值,双括号括起来的零个或多个字符则构成字符串型字面值
      • 字符串字面值实际上是由常量字符构成的数组,在末尾处有’\0’
    • 转义字符:以反斜线作为开始
      • 泛化的转义序列,其形式是\x后紧跟1个或多个十六进制,或者紧跟八进制数字,其中数字部分表示的是字符对应的数值
    • 指定字面值的类型:添加前缀和后缀可以改变整型、浮点型和字符型字面值的默认类型
      • 如果后缀中有U,则该字面值属于无符号类型
      • 如果后缀中有L,则字面值的类型至少是long
      • 如果后缀中有LL,则字面值的类型将是long long和unsigned long long 中的一种
    • nullptr是指针字面值

2.2 变量

  • C++是一种静态类型语言,其含义是在编译阶段检查类型
  • 何为对象:对象是指一块能存储数据并具有某种类型的内部空间
  • C++中,初始化和赋值为两个概念
    • C++11,引入列表初始化用花括号来初始化变量
  • 默认初始化:内置类型的变量未被显式初始化,值由定义的位置决定,定义于任何函数体之外的变量被初始化为0
    • 定义在函数体内部的内部的内置类型变量将不被初始化
    • 一个未被初始化的内置类型变量的值是未被定义的
    • 一些类要求每个对象都显式初始化,如果创建了一个该类的对象而未对其做明确的初始化操作,将引发错误
  • 分离式编译
    • C++将声明和定义区分开,声明使得名字为程序所致,定义负责创建实体
      • 变量声明规定了变量的类型和名字,同时申请存储空间
    • 声明变量而不定义,需要在变量名前添加extern,而且不要显式初始化变量
      • extern:标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义
        • 在函数体内部,尝试初始化一个由extern关键字标记的变量将会引发错误
    • 任何显式初始化的声明为定义
    • 变量只能被定义一次,但可以声明多次,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义
  • 标识符
    • C++的标识符由字母、数字和下画线组成,其中必须以字母或下画线开头
    • 标识符的长度没有限制,但是对大小写字母敏感
1
2
3
4
5
6
7
8
9
10
// 定义变量并初始化
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);

// 声明变量而不定义
extern int i;
// 声明并定义j
int j;

2.3 复合类型

2.3.1 引用

  • 引用为对象起了另一个名字,引用类型引用另外一种类型,通过将声明符携程&d的形式来定义引用类型,其中d是声明的变量名
  • 一般在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序会把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用
    • 引用必须初始化(引用无法令引用重新绑定到另外一个对象)
    • 一旦初始化完成,引用将和它的初始值对象一直绑定在一起
    • 定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的

2.3.2 指针

  • 与引用类似,指针也实现了对其他对象的间接访问
  • 指针与引用相比又有很多不同点
    1. 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象
    2. 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值
  • 定义指针类型的方法将声明写出*d的形式,其中d式变量名
    • 在声明语句中指针的类型实际上被用于指定它所指向对象的类型,二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误
    • 获取对象的地址,需要使用取地址符&
  • 指针值的4种状态
    • 指向一个对象
    • 指向紧邻对象所占空间的下一个位置
    • 空指针
    • 无效指针
      • 试图拷贝或以其他方式访问无效指针的值都将引发错误
  • 指针访问对象,需要使用解引用符*访问对象
    • 对指针解引用会得出所指的对象,若给解引用的结果赋值,实际上也就是给指针所指的对象赋值
    • 解引用操作只适用于指向了某个对象的有效指针
  • 空指针
    • C++11引入的新方法,使用nullptr初始化指针
    • nullptr是一种特殊类型的字面值,可以转换成任意其他类型的指针类型
    • NULL的预处理变量来给指针赋值,其值为0,定义在cstdlib中
  • 赋值和指针
    • 指针和引用都能提供对其他对象的间接访问,在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象
      • 一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象
    • 指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象
  • 任何非0指针对应的条件值都是true
  • void* 指针
    • 特殊的指针类型,可以存放任意对象的地址
    • 利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针
      • 不能直接操作void*指针所指的对象
    • 以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象

2.3.3 理解复合类型

  • 定义包括一个基本数据类型和一组声明符
  • 定义多个变量时,基本数据类型是基本数据类型而非基本类型*,*仅仅是修饰了变量名而已
    • 将*或&与变量名连在一起
  • 指向指针的指针
    • **表示指向指针的指针
    • 解引用指向指针的指针会得到一个指针
    • 两次解引用指向指针的指针会得到最原始的对象
  • 指向指针的引用
    • 引用本身不是一个对象,不能定义指向引用的指针,但指针是对象,所以存在对指针的引用
    • *&r,理解时最简单的办法是从右向左阅读r的定义;离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用;声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针;声明的基本数据类型部分指出r引用的是一个int指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int *p;  //定义指向int型对象的指针

int ival = 42;
int *p = &ival; //p存放ival的地址,p式指向变量ival的指针

int* p1,p2; //p1为一个指向int的指针,p2为int类型

int **pp = &p; //pp1表示指向一个int指针的指针

int i = 42;
int *p;
int *&r = p; //r是对指针p的引用

r = &i; //r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0
//要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义
//离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用
//声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针
//最后,声明的基本数据类型部分指出r引用的是一个int指针

2.4 const限定符

  • const限定符对变量的类型加以限制可以防止程序改变变量的值
  • const变量一旦创建其值就不能再改变,故const变量必须初始化
  • 默认状态下,const对象仅在文件内有效
    • 编译器在编译过程中将变量全部替换成对应的值
    • 当多个文件中出现了同名的const变量时,在不同文件中分别定义了独立的变量
    • 文件共享const变量时,对const变量的声明和定义都添加extern关键字

2.4.1 const引用(常量引用)

  • 将引用绑定在const对象上,是对常量的引用
    • const引用与普通引用的不同:对常量的引用不能被用于修改它绑定的对象
  • 引用的类型必须与其所引用对象的类型一致
    • 在初始化常量引用时允许用任意表达式作为初始值,只要其结果可以转换成引用的类型即可
  • 常量引用一个非const的对象:不允许通过常量引用修改对象的值

2.4.2 const和指针

  • const指针(常量指针):指向常量的指针,不可以修改其所指向对象的值
    • 常量指针底层指针,表示指针指向的是常量
    • 常量只可以使用指向常量的指针存放常量的地址
    • 常量指针可以指向其他对象,但不可以通过常量指针修改对象的值
    • 常量指针必须初始化,一旦初始化完成,指针不可以改变
  • 指针常量:把*放在const关键字之前说明指针是一个常量,即不变的是指针本身的值而非指向的那个值
    • 指针常量顶层const,指针本身就是常量
    • 指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型
  • 注意区别指针常量和常量指针

2.4.3 顶层const和底层const

  • 顶层const表示任意的对象是常量
  • 底层const表示指向常量
  • 顶层const拷贝时,顶层const不受影响
  • 底层const拷贝时
    • 拷入和拷出时的对象必须具有相同的底层const
      • 或两个对象的数据类型必须可以转换

2.4.4 constexpr和常量表达式

  • 常量表达式是指指不会改变并且在编译过程中就可以得到计算结果的表达式
  • C++11中,允许变量声明为constexpr类型,以便编译器验证变量的值是否是常量表达式
    • 新标准允许定义一种特殊的constexpr函数(以使得编译时就可以计算其结果,可以用constexpr函数去初始化constexpr变量)
  • 声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化
  • 指针和引用定义成constexpr,constexpr指针初始化值必须是nullptr或0
  • 指针和constexpr
  • 在constexpr声明中定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int i = 42;
int &r1 = i;
const int &r2 = i; //常量引用,但不可以通过其改变i的值

r1 = 0;
r2 = 0; //错误;r2为常量引用

const double pi = 3.14;
double *ptr = &pi; //ptr为普通指针
const double *cptr = &pi; //指向常量的指针

const double *const cptr = &pi; //cptr是一个指向常量对象的常量指针
int *const ptri = &i; //ptri是一个指向int的常量指针

int i = 0;
int *const p1 = &i; //顶层const,不可以改变p1的值
const int ci = 42; //顶层const,不可以改变
const int *p2 = &ci; //底层const,可以改变p2的值
const int *const p3 = p2; //左边的底层const,右边的顶层const
i = ci;
p2 = p3; //p2,p3所指向的对象类型相同,p3顶层const部分不影响

int *p = p3; //错误,p不包含底层const含义
p2 = p3; //正确
p2 = &i; //正确,int*可以转换成const int*
int &r = ci; //错误
const int &r2 = i; //正确
//p3既是顶层const也是底层const
//拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量

const int *p = nullptr; // 为一个指向整型常量的指针
constexpr int *q = nullptr; // 为一个指向整数的常量指针

2.5 处理类型

2.5.1 类型别名

  • 类型别名是一个名字,是某种类型的同义词
  • 传统方法:使用typedef
  • C++11中使用别名声明,这种方法用关键字using作为别名声明的开始,其后紧跟着别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名
    • 类型别名和类型的名字等价
  • 指针,常量和类型别名
    • 在使用类型别名的声明语句时,不可以将别名替换成原来的样子,应将声明部分看成一个整体

2.5.2 auto类型说明符

  • 在C++11中,引入auto修饰符,可以让编译器分析表达式所属的类型,推算变量的类型
  • auto定义的变量必须给初始值
    • ==当引用被用作初始值时,真正参与初始化的其实时引用对象的值
    • auto一般会忽略顶层const,保留底层const,需要顶层const时需要明确指出
    • ==设置一个类型为auto的引用,初始化中的顶层const属性会保留
  • ==一条语句中定义多个变量,&,*只从属于某个声明符,而非基本数据类型的一部分

2.5.3 decltype类型指示符

  • C++11引入的第二种类型说明符,选择并返回操作数的数据类型
  • decltype处理顶层const和引用的方式与auto不同,若decltype使用的表达式是一个变量,则返回该变量的类型(包括顶层const和引用在内)
    • 引用从来都是作为其所指对象的同义词出现,但在decltype中例外
    • 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型
    • 如果表达式内同是解引用操作,则decltype将得到引用类型
    • 如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型
    • 如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式,得到引用类型(变量是一种可以作为赋值语句左值的特殊表达式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef char *pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,其对象是指向char的常量指针

int i = 0, &r = i;
auto a = r; //a为int类型

const int ci = i, &cr = ci;
auto b = ci; //b整型(ci的顶层const属性被忽略)
auto c = cr; //c整型
auto d = &i;
auto e = &ci; //指向整型常量的指针

const auto f = c; // const int

auto &g = 42; //错误
const auto &j = 42; //正确,可以为常量引用绑定字面值

decltype(f()) sum = x; //sum的类型就是函数f的返回类型

int i = 42,*p = &i, &r = i;
decltype(r+0) b; // int
decltype(*p) c; //错误,c时int&,必须初始化
// r是一个引用,因此结果为引用类型
// 但向让结果类型r是所指向的类型,需要使用表达式

decltype((i)) d; //d,int&必须初始化
decltype(i) e; //int

2.6 自定义数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
// 结构体的定义
Struct 结构体名{
成员变量;
成员函数;
};

// 头文件保护符
#ifdef 头文件
#define 头文件
// ...
// 头文件内容
// ...
#endif

3 字符串,向量和数组

3.1 命名空间的using声明

  • using声明可以使用命名空间中的成员
    • 每个名字都需要独立的using声明
    • 头文件不应包含using声明
1
2
// using声明
using namespace::name;

3.2 标准库类型string

  • 标准库类型string表示可变长字符序列,包含在string头文件中
  • 定义和初始化string对象
  • 使用=初始化变量,执行拷贝初始化,不使用=进行直接初始化
  • string操作的注意事项
    • getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了)
      • >>不会保留输入时的空白符,遇空白符停止
    • string::size_type类型
      • 所有用于存放string类的size函数返回值的变量,都是string::size_type类型的
      • 在C++11中,允许编译器通过auto或者decltype来推断变量的类型
        • 避免因为符号数和无符号数产生意外的结果
  • C++11引入范围for语句
    • 遍历给定序列中的每个元素并对序列中的每个值执行某种操作
    • for(declaration:expression)
    • expression部分是一个对象,用于表示一个序列;declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素;每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值
      • auto c可作为declaration部分
string的操作 解释
os<<s 将s写出到输出流os中,返回os
is>>s 从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is,s) 从is中读取一行赋给s,返回is
s.empty() s为空返回true
s.size() 返回s中字符的个数
s[n] 返回s中第n个字符的引用
s1+s2 返回s1,s2连接后的结果
s1=s2 s2的副本替换s1中原来的字符
s1==s2 完全一样,则相等
<,<=,>,>= 对字典序进行比较
cctype头文件中的标准库函数操作 解释
isalnum© 当c为字母或数字时为真
isalpha© 当c为字母时为真
iscntrl© 当c为控制字符时为真
isdigit© 当c为数字时为真
isgraph© 当c不是空格但可打印时为真
islower© 当c是小写字母时为真
isprint© 当c是可打印字符时为真(c为空格或具有可视形式)
ispunct© 当c是标点符号时为真
isspace© 当c是空白时为真
issupper© 当c是大写字母时为真
isxdigit© 当c是十六进制数字时为真
tolower© 输出小写字母
toupper© 输出大写字母
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<string>
using std::string;

string s1; //默认初始化
string s2 = s1; // 拷贝初始化
string s2(s1); // 拷贝初始化
string s3 = "hiya"; //拷贝初始化
string s3("hiya"); //直接初始化
string s4(10,'c'); //cccccccccc

// 读写string对象
int main(){
string s;
cin>>s;
cout<<s<<endl;
return 0;
}

// 读写数量未知的string
int main(){
string word;
while(cin>>word)
cout<<word<<endl;
return 0;
}

// auto推断
auto len = line.size(); //len string::size_type

// 范围for
for(declaration:expression)
statement

3.3 标准库类型vector

  • 标准库类型vector表示对象的集合,其中所有对象的类型都相同,常被称为容器
    • 集合中每个对象都有一个与之对应的索引,索引用于访问对象
    • vector属于类模板
      • 编译器根据模板创建类或函数的过程称为实例化,在模板名后跟着一对尖括号,在括号内放上信息,即可创建模板实例类
    • vector能容纳绝大多数类型的对象作为其元素,但引用不属于对象,不存在包含引用的vector
  • 定义和初始化vector对象
    • 定义一个空vector,然后当运行时获取到元素的值后再逐一添加(在程序运行时高效地往vector对象中添加元素)
    • 在定义对象时指定元素的初始值
    • C++11中可以使用列表初始化,用花括号括起来的元素值赋值给vector对象
    • 用圆括号初始化指定的一般为数量,用花括号初始化的为初始化列表
  • 向vector对象中添加元素
    • 使用push_back向vector对象添加元素,将值作为尾元素压入vector中
      • vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素
      • 添加元素后再分配内存空间,声明定义只是开辟内存头
    • vector对象可以高效地增长,可以快速添加元素
    • 如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环
定义vector对象的方法 解释
vector<T> v1 v1是一个空vector,建在元素是T类型
vector<T> v2(v1) v2中包含有v1所有元素的副本
vector<T> 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中包含初始化个数的元素,每个元素被赋予相同的初始值
vector对象地其他操作 解释
v.empty() v为空返回true
v.size() 返回v中字符的个数
v.push_back() 向v中添加元素
v[n] 返回v中第n个字符的引用
v1=v2 v2的副本替换v1中原来的字符
v1={a,b,c…} 拷贝替换
v1==v2 完全一样,则相等
<,<=,>,>= 对字典序进行比较
1
2
3
4
5
6
7
#include<vector>
usind std::vector;

vector<int> ivec;

// 添加元素
ivec.push_back(1); //向ivec中添加元素

3.4 迭代器介绍

  • 标准库容器可以使用迭代器,迭代器类似指针类型,提供了对对象的间接访问
    • 使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素;有效的迭代器指向某个元素或容器尾元素的下一个位置
  • 使用迭代器
    • 有迭代器的类型同时拥有返回迭代器的成员,这些类型拥有begin和end成员
      • begin成员负责指向第一个元素的迭代器
      • end成员负责指向尾元素的下一个位置,指示的是容器一个不存在的尾后元素,迭代器称为尾后迭代器
      • 由编译器决定迭代器的类型,一般使用auto关键字定义变量
      • 若容器为空,begin和end返回的是一个迭代器,都是尾后迭代器
    • 和指针类似,可通过解引用迭代器来获取它所指示的元素
  • begin和end返回的具体类型由对象是否是常量决定
    • 如果对象是常量,begin和end返回const_iterator
    • 如果对象不是常量,返回iterator
  • 拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器类型
    • const_iterator为常量,可以读取但不能修改它所指向的元素值
      • C++11中引入cbegin和cend获得const_iterator
  • C++语言定义了箭头运算符->, 箭头运算符把解引用和成员访问两个操作结合在一起,it->mem等价于(*it).mem
迭代器运算符 解释
*iter 返回迭代器iter所指元素
iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter 指示容器的下一个元素
–iter 指示容器的上一个元素
iter1 == iter2 判断两个迭代器是否相等
iter1 != iter2 判断两个迭代器是否相等
迭代器支持的运算符 解释
iter+n 迭代器向前移动n个元素
iter-n 迭代器向后移动n个元素
iter+=n 复合赋值语句
iter-=n 复合赋值语句
iter1-iter2 计算两个迭代器之间的距离
1
2
3
4
5
6
7
auto b = v.begin(),e=v.end();

//拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器类型
vector<int>::iterator it; //it可以读写vector<int>的元素
string::iterator it; //it可以读写String的元素
vector<int>::const_iterator it; //it可以读vector<int>的元素,但不可以写
string::const_iterator it; //it可以读String的元素

3.5 数组

  • 数组与vector的区别
    • 与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问
    • 与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素
  • 定义和初始化数组
    • 数组为复合类型,默认情况下,数组的元素被默认初始化
    • 定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型
    • 数组的元素应为对象,不存在引用的数组
    • 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值
  • 访问数组元素
    • 可使用范围for语句
    • 使用数组下标时,通常定义为size_t类型,该类型在cstddef头文件中,是一种机器相关的无符号类型
  • 复杂的数组声明
    • 指针数组:存放指针的数组
    • 数组指针:数组的指针及数组的引用
    • 要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读
  • 复合类型的理解规则:从右到左,从内到外
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned cnt = 42;
constexpr unsigned sz = 42; //常量表达式
int arr[10];
int *parr[sz]; //含有42个整型指针的数组
string bad[cnt]; //cnt不是常量表达式,错误
string strs[get_size()]; //get_size()为常量表达式时正确

int ial[sz] = {0,1,2};
int a2[] = {0,1,2};
int a3[5] = {0,1,2}; //a3[] = {1,2,3,0,0}

char a1[] = {'c','+','+'}; //列表初始化,没有空字符
char a1[] = {'c','+','+','\0'}; //列表初始化,含有显式空字符
char a1[] = "C++"; //列表初始化,含有隐式空字符,表示字符串结束的空字符 size(a1)=4

int *ptrs[10]; //ptrs是含有10个整型指针的数组,指针数组
int (*Parray)[10] = $arr; //Parry指向一个含有10个整数的数组,数组指针
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
int *(&arrRef)[10] = ptrs; //arrRef是一个引用,引用一个指针数组,该数组包含10个指针

3.5.3 指针和数组

  • 使用数组时编译器一般会将数组转换为指针
  • 对数组使用下标运算符得到该数组指定位置的元素
    • 对数组元素使用取地址符能得到指向该元素的指针
    • 数组名为一个指向数组首地址的指针
    • 当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组
      • 使用decltype关键字时不会发生转换
  • 指针作为迭代器
    • 若得到尾后指针,使用时容易出错
    • C++11新标准引入了两个名为begin和end的函数,可得到指向元素的首指针和尾元素下一个位置的指针,定义在iterator头文件中
    • 指针可执行所有迭代器运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致
    • 两指针相减时,得到ptrdiff_t的标准库类型,与size_t一样,是一种定义在cstddef头文件中的机器相关的类型
      • 因为差值可能为负值,所以ptrdiff_t是一种带符号类型
    • 两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,可利用关系运算符进行比较,否则不可以比较
    • 对数组执行下标运算其实是对指向数组元素的指针执行下标运算
      • 只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算
      • 标准库类型限定使用的下标必须是无符号类型,而内置的下标运算可以是有符号类型
      • 内置的下标运算符可以处理负值,结果地址必须指向原来的指针所指同一数组中的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
string nums = {"one","two","three"}
string *p = &nums[0];

string *p2 = nums; //等价于*p2=&num[0]

int ia[] = {0,2,3,4,5,6};
auto ia2(ia); //ia2为一个整型指针,指向ia的第一个元素

decltype(ia) ia3={0,1,2}; //ia3为一个整型数组

int *p = &ia[2]; //p指向索引为2的元素
int j = p[1]; //相当于*(p+1),ia[3]
int k = p[-2]; //p[-2]是ia[0]表示的元素

3.5.4 C风格字符串

  • C风格的字符串为char数组,并以空字符(‘\0’)结束
    • 一般利用指针来操作这些字符串
cstring头文件中,C风格字符串的函数 解释
strlen(p) 返回p的长度,空字符不计算在内
strcmp(p1,p2) 比较相等性,<返回正值
strcat(p1,p2) p2附加在p1后,返回p1
strcpy(p1,p2) p2拷贝给p1,返回p1

3.5.5 与旧代码的接口

  • string的c_str成员函数:将string转换为c风格的字符串
    • 函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样
    • 指针的类型是const char*,从而确保我们不会改变字符数组的内容
  • 不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组
    • 允许使用数组来初始化vector对象
    • 需指明要拷贝区域的首元素地址和尾后地址
  • 尽量使用标准库类型而非数组
1
2
3
int int_arr[] = {0,1,2,3,4,5};
vector<int> ivec(begin(int_arr),end(int_arr));
vector<int> ivec(int_arr+1,int_arr+4); //拷贝3个元素,int_arr[1]~[3]

3.6 多维数组

  • 多维数组初始化时,使用花括号进行初始化,可嵌套使用花括号
  • 可使用范围for语句处理多维数组
    • 使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型
    • 定义多维数组的指针时,多维数组其实是数组的数组
    • 使用auto或者decltype能尽可能地避免在数组前面加上一个指针类型
1
2
3
4
5
6
// 输出is中的每个元素的值,ia为二维数组,每个内层数组各占一行ia[3][4]
for(auto p = ia;p!=ia+3;++p){
for(auto q = *p;q!=*p+4;++q)
cout<<*q<<' ';
cout<<endl;
}

4 表达式

4.1 基础

  • 重载运算符:当运算符作用与类类型的运算对象时,可以自定其含义
    • 定义重载运算符时,其包括的运算对象的类型和返回值的类型都是由运算符定义的;但运算对象的个数,运算符的有限集和结合律都是无法改变的
  • 左值和右值
    • 左值可以位于赋值语句的左侧,右值则不能
    • 当一个对象被用作右值的时候,用的是对象的值(内容)
      • 当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
    • 在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用
      • 当一个左值被当成右值使用时,实际使用的是它的内容(值)
    • 使用关键字decltype时,左值和右值也有所不同
      • 如果表达式的求值结果是左值,decltype作用于该表达式得到一个引用类型

4.2 算数运算符

  • 算数运算符包括
    • + 加
    • - 减
    • * 乘
    • / 除
    • % 取余
  • C++11中,规定商一律向0取整(即直接切除小数部分)

4.3 逻辑和关系运算符

  • 逻辑和关系运算符包括

    • 逻辑非 !
    • 小于 <
    • 大于 >
    • 小于等于 <=
    • 大于等于 >=
    • 相等 ==
    • 不相等 !=
    • 逻辑与 &&
    • 逻辑或 ||
  • 短路求值:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值

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

4.4 赋值运算符

  • 赋值运算符的左侧运算对象必须是一个可修改的左值
  • C++11允许使用花括号括起来的初始值列表跳转至列表初始化作为赋值语句的右侧运算对象
  • 赋值运算符的优先级低于关系运算符的优先级

4.5 递增和递减运算符

  • 前置版本++v
    • 首先将运算对象加1(或减1),然后将改变后的对象作为求值结果
  • 后置版本
    • 将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本
  • 除非必须,否则不用递增递减运算符的后置版本
  • 后置递增运算符的优先级高于解引用运算符

4.6 成员访问运算符

  • 点运算符和箭头运算符都可以访问成员
    • 点运算符获取类对象的一个成员
    • 箭头运算符与点运算符有关,表达式ptr->mem等价于(*ptr).mem
  • 箭头运算符作用于一个指针类型的运算对象,结果是一个左值
  • 点运算符分成两种情况:
    • 如果成员所属的对象是左值,那么结果是左值;
    • 反之,如果成员所属的对象是右值,那么结果是右值
1
2
3
4
string s1 = "a string", *p = &s1;
auto n = s1.size(); //运行string对象s1的size成员
n = (*p).size(); //运行p所指对象的size成员
n = p-size();

4.7 条件运算符

  • 条件运算符 cond?expr1:expr2;
    • 其中cond是判断条件的表达式,而expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式
    • 条件运算符的执行过程是:首先求cond的值,如果条件为真对expr1求值并返回该值,否则对expr2求值并返回该值
  • 条件运算的嵌套最好别超过两到三层

4.8 位运算符

  • 位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合
  • 一种名为bitset的标准库类型也可以表示任意大小的二进制位集合
  • 位运算符包括
    • 位求反 `
    • 左移 <<
    • 右移 >>
    • 位与 &
    • 位异或 ^
    • 位或 |
  • 强烈建议仅将位运算符用于处理无符号类型

4.9 sizeof运算符

  • sizeof运算符返回一条表达式或一个类型名字所占的字节数
  • sizeof运算符满足右结合律,其所得的值是一个size_t类型,是一个常量表达式
  • 形式1:sizeof(type)
  • 形式2:sizeof expr
    • sizeof返回的是表达式结果类型的大小,sizeof并不实际计算其运算对象的值
  • 执行sizeof运算能得到整个数组的大小,可以用数组的大小除以单个元素的大小得到数组中元素的个数
1
2
3
// 返回数组中的元素数量
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz];

4.10 逗号运算符

  • 逗号运算符含有两个运算对象,按照从左向右的顺序依次求值
  • 对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值

4.11 类型转换

  • 隐式转换
    • 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型
    • 在条件中,非布尔值转换成布尔类型
    • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
    • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
    • 函数调用时也会发生类型转换
  • 算术转换:把一种算术类型转换成另外一种算术类型,其中运算符的运算对象将转换成最宽的类型
  • 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针
  • 指针的转换
    • 常量整数值0或者字面值nullptr能转换成任意指针类型
    • 指向任意非常量的指针能转换成void*
    • 指向任意对象的指针能转换成const void*
    • 在有继承关系的类型间还有另外一种指针转换的方式
  • 转换成布尔类型
    • 存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true
  • 转换成常量
    • 允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,就能将指向T的指针或引用分别转换成指向const T的指针或引用
  • 类类型定义的转换

4.11.3 显式转换

  • 显式转换为强制类型转换
  • 命名的强制类型转换 cast-name<type>(expression);
    • type是转换的目标类型而expression是要转换的值
    • 如果type是引用类型,则结果是左值
    • cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种
  • static_cast
    • 任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
    • static_cast可以将一个较大的算术类型赋值给较小的类型
    • static_cast对于编译器无法自动执行的类型转换也非常有用
    • 把指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,应该确保指针的值保持不变;强制转换的结果将与原始的地址值相等
    • 对右值引用的特许规则:虽然不能隐式地将一个左值转换成右值引用,但static_cast可以显式地将一个左值转换成一个右值
  • const_cast
    • “去掉const性质”:将常量对象转换成非常量对象的行为
    • 只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误
  • reinterpret_cat
    • 为运算对象的位模式提供较低层次上的重新解释
1
2
3
4
5
void * p = &d;
double *dp = static_cast<double*>(p);

int *ip;
char *pc = reinterpret_cast<char*>(ip);

5 语句

5.3 条件语句

  • C++语言提供了两种按条件执行的语句
    • 一种是if语句,它根据条件决定控制流
    • 另外一种是switch语句,它计算一个整型表达式的值,然后根据这个值从几条执行路径中选择一条
  • 悬垂else
    • 当一个if语句嵌套在另一个if语句内部时,很可能if分支会多于else分支
    • C++规定else与离它最近的尚未匹配的if匹配
  • switch-case-default

5.4 迭代语句

  • 迭代语句通常称为循环,它重复执行操作直到满足某个条件才停下来
    • while和for语句在执行循环体之前检查条件
    • do while语句先执行循环体,然后再检查条件
  • 范围for语句for(declaration : expression) statement
    • expression表示的必须是一个序列
    • declaration定义一个变量,序列中的每个元素都得能转换成该变量的类型;确保类型相容最简单的办法是使用auto类型说明符
      • 需要对序列中的元素执行写操作,循环变量必须声明成引用类型
    • 每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行statement

5.5 跳转语句

  • C++语言提供了4种跳转语句:break,continue,goto和return
  • break语句负责终止离它最近的while、do while、for或switch语句,并从这些语句之后的第一条语句开始继续执行
    • break语句只能出现在迭代语句或者switch语句内部
  • continue语句终止最近的循环中的当前迭代并立即开始下一次迭代
    • continue语句只能出现在for、while和do while循环的内部,或者嵌套在此类循环里的语句或块的内部
  • goto语句的作用是从goto语句无条件跳转到同一函数内的另一条语句
    • 不要在程序中使用goto语句,因为它使得程序既难理解又难修改
    • goto label;其中,label是用于标识一条语句的标示符
    • 带标签语句是一种特殊的语句,语句之前有一个标示符以及一个冒号:
  • return

5.6 try语句块和异常处理

  • C++的异常处理包括
    • throw表达式,异常检查部分使用throw表达式表示遇到了无法处理的问题
    • try语句块,异常处理部分使用try语句块处理异常,try-catch语句块
    • 一套异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息

5.6.1 throw表达式

  • throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型
1
2
3
if(item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to same ISBN"); // throw表达式
cout << item1 + item2 << endl;

5.6.2 try语句块

  • try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问
1
2
3
4
5
6
7
8
// try语句块通用语法结构
try{
program-statements
}catch(exception-declaration){
handler-statements
}catch(exception-declaration){
handler-statements
}

5.6.3 标准异常

  • C++标准库定义了异常类
    • exception头文件定义了最通用的异常类exception;只报告异常的发生,不提供任何额外信息
    • stdexcept头文件定义了几种常用的异常类,详细信息在表5.1中列出
    • new头文件定义了bad_alloc异常类型
    • type_info头文件定义了bad_cast异常类型
  • 标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值
  • 只能以默认初始化的方式初始化exception、bad_alloc和bad_cast对象,不允许为这些对象提供初始值
    • 其他异常类型不允许使用默认初始化的方式:应该使用string对象或者C风格字符串初始化这些类型的对象;当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息
  • 异常类型只定义了一个名为what的成员函数,该函数没有任何参数,目的是提供关于异常的一些文本信息,返回值是一个指向C风格字符串的const char*
<stdexcept>定义的异常类 解释
exception 最常见的问题
runtime_error 只有在运行时才能检测出的问题
range_error 运行时错误:生成的结果超出了有意义的值域范围
overflow_error 运行时错误:计算上溢
underflow_error 运行时错误:计算下溢
logic_error 程序逻辑错误
domain_error 逻辑错误:参数对应的结果值不存在
invalid_error 逻辑错误:无效参数
length_error 逻辑错误:试图创建一个超出该类型最大长度的对象
out_of_range 逻辑错误:使用一个超出有效范围的值

6 函数

6.1 函数基础

  • 典型的函数包括:返回类型,函数名字,由0个或多个形参组成的列表以及函数体
  • 通过调用运算符来执行函数,调用运算符的形式是一对圆括号,作用于一个表达式,该表达式是函数或者指向函数的指针
  • 实参和形参
    • 实参是形参的初始值
    • 实参的类型必须与形参类型匹配
    • 任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字
    • 是否设置未命名的形参并不影响调用时提供的实参数量
      • 即使某个形参不被函数使用,也必须为它提供一个实参
  • 函数的返回类型不能是数组或函数类型,但可以是指向数组或函数的指针

6.1.1 局部对象

  • 形参和函数体内部定义的变量称为局部变量
  • 对函数而言是局部的,仅在函数的作用域内可见,同时局部变量会隐藏在外层作用域中同名的其他所有声明
  • 存在于块执行器件的对象称为自动对象,当块的执行结束后,块中创建的自动对象的值就为未定义的
    • 形参是一种自动对象,函数开始时为形参申请存储空间,函数终止,形参被销毁
  • 局部静态对象
    • static类型:局部变量的生命周期贯穿函数调用及之后的时间
    • 局部静态对象在程序执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在函数结束执行也不会对它有影响

6.1.2 函数声明

  • 函数声明与函数定义类似,唯一的区别就是函数声明无需函数体,使用一个分号代替
  • 函数的三要素(返回类型,函数名,形参类型)描述了函数接口,说明了调用该函数所需的全部信息
  • 函数声明也叫做函数原型
  • 建议在头文件中进行函数声明
    • 含有函数声明的头文件应该被包含到定义函数的源文件中

6.1.3 分离式编译

  • 分离式遍历-链接形成可执行文件

6.2 参数传递

  • 当形参是引用类型,对应的实参被引用传递或函数被传引用调用
  • 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象,实参被值传递或函数被传值调用

6.2.1 传值参数

  • 初始化一个非引用类型变量时,初始值被拷贝给变量
    • 对变量的改动不会影响初始值
    • 函数对形参的所有操作都不会影响实参
  • 指针参数
    • 指针的行为和其他非引用类型一样
    • 当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针
    • 可以使用指针修改它所指向的对象的值(指针可以间接访问所指的对象)
  • 在C++中,建议使用引用类型的形参代替指针访问函数外部的对象

6.2.2 传引用参数

  • 引用的操纵实际上是作用在引用所引的对象上
  • 通过使用引用形参,允许函数改变一个或多个实参的值
  • 使用引用可以避免拷贝
  • 函数无需修改引用形参的值,最好将其声明为常量引用
  • 传递引用形参可以帮助返回额外信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int n = 0, i = 42;
int &r = n;

//传递指针
void reset(int *ip){
*ip = 0; //改变了指针ip所指对象的值
ip = 0; //改变了ip的局部拷贝,实参未被改变
}
reset(&i); //改变i的值,而非i的地址

//传递引用
void reset(int &i) //i是传递给reset函数的对象的另一个名字
{
i = 0;
}

int j = 42;
reset(j); //传引用,j的值会发生改变

6.2.3 const形参和实参

  • 顶层const和底层const
  • 形参是const时,若为顶层const,初始化时会忽略顶层const属性
    • 底层const可以使用非常量进行初始化,反之不成立
    • C++允许用字面值初始化常量引用
  • 尽量使用常量引用
    • 把函数不会改变的形参定义成普通的引用会给函数的调用者产生误导,即函数可以修改他的实参值
    • 使用引用而非常量引用也会极大地限制函数所能接受的实参类型,不能把const对象,字面值或需要类型转换的对象传递给普通的引用形参
    • 函数嵌套调用时,假如其他函数(正确地)将它们的形参定义成常量引用,那么内层函数(普通引用)无法正常使用

6.2.4 数组形参

  • 数组的特殊性
    • 不允许拷贝数组
    • 使用数组时会将其转换成指针
  • 数组进行传递时,传递的是指向数组首元素的指针,不可以进行值传递
    • 可以将形参携程类似数组的形式
    • 数组是以指针的形式传递给函数的,但需要指明数组的确切尺寸
      • 使用标记指定数组长度
      • 使用标准库规范,传递指向数组首元素和尾后元素的指针
      • 显式传递一个表示数组大小的形参
  • 数组形参和const
    • 当函数不需要对数组元素执行写操作时,数组形参应该是指向const的指针,只有需要改变元素值是才定义为非常量的指针
  • 数组引用形参
    • 形参可以是数组的引用,引用形参绑定到对应的数组上
  • 传递多维数组
    • 处理的是数组的数组,首元素本身就是一个数组,指针就是一个指向数组的指针
    • 数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略
1
2
3
4
5
6
7
8
9
10
11
// const int ia[]等价于const int* ia
void print(const int ia[], size_t size)

// 形参为数组引用,维度是类型的一部分
void print(int (&arr)[10])

// 二维数组
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10],int rowSize)
// 等价于
void print(int matrix[][10], int rowSize)

6.2.5 main:处理命令行选项

  • main函数传递实参可以通过命令行选项通过两个形参传递给main函数
  • int main(int argv, char *argv[])
    • 第一个形参argc表示数组中字符串的数量
    • 第二个形参argv是一个数组,它的元素是指向C风格字符串的指针

6.2.6 含有可变形参的函数

  • 编写处理不同数量实参的函数,两种方法
    • 如果实参类型相同,传递一个名为initializer_list的标准库类型
    • 如果实参的类型不同,编写可变参数模板函数
    • 利用省略符,传递可变数量的实参,一般只用于与C函数交互的接口程序
  • 使用initializer_list类型的形参表示函数的实参数量未知但是全部实参的类型都相同
    • initializer_list是一种标准库类型,用于表示某种特定类型的值的数组
    • initializer_list类型定义在同名的头文件中
    • 和vector一样,initializer_list也是一种模板类型
      • 定义initializer_list对象时,必须说明列表中所含元素的类型
    • 和vector不一样的是,initializer_list对象中的元素永远是常量值,无法改变initializer_list对象中元素的值
  • 省略符形参
    • 为了C++程序访问某些特殊的C代码而设置
    • 省略符形参应该仅仅用于C和C++通用的类型
      • 特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝
    • 省略符形参只出现在形参列表的最后一个位置
initializer_list提供的操作 解释
initializer_list<T> lst; 默认初始化;T类型元素的空列表
initializer_list<T> lst{a,b,c…}; lst的元素数量和初始值一样多;lst元素是对应初始值的副本
lst2(lst) 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst2=lst 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst.size() 列表中的元素数量
lst.begin() 首元素的指针
lst.end() 尾元素下一位置的指针
1
2
3
4
5
6
7
8
9
10
initializer_list<string> ls;
initializer_list<int> li;

void error_msg(initializer_list<string> il){
for(auto beg = il.begin();beg != il.end();++beg)
cout << *beg << " ";
cout << endl;
}

void foo(parm_list, ...) //省略符形参

6.3 返回类型和return语句

6.3.1 无返回值函数

  • 没有返回值的return语句只能用在返回类型是void的函数中
  • 返回void的函数不要求非得有return语句(会隐式地执行return)

6.3.2 有返回值的函数

  • return语句提供了函数的结果
    • 只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值
    • return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型
  • 值是如何被返回的
    • 返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果
    • 若函数返回引用,则该引用仅是它所引对象的一个别名
  • 不要返回局部变量的引用或指针
    • 函数完成后,局部变量所占的存储空间被释放,随之指针和引用不再有效
  • 引用返回左值
    • 调用一个返回引用的函数得到左值,其他返回类型得到右值
    • 可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值
  • 列表初始化返回值
    • C++11规定,函数可以返回花括号包围的值的列表
    • 类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化
      • 如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定
  • 主函数main的返回值
    • 允许main函数没有return语句直接结束,编译器会隐式插入一条返回0的return语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char &get_val(string &str,string::size_type ix){
return str[ix];
}

int main(){
string s("a value");
get_val(s,0) = 'A'; //将s[0]值改为A
}
// 返回值是引用,因此调用是个左值,和其他左值一样它也能出现在赋值运算符的左侧

vector<string> process(){
//..
if(expected.empty())
return {};
else if(expected == actual)
return {"functionX","okay"};
else
return {"functionX",expected, actual};
}

6.3.3 返回数组指针

  • 声明一个返回数组指针的函数
    • 定义一个返回数组指针的函数,数组的维度必须跟在函数名字之后
    • 函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度
    • Type (*function(parameter_list))[dimension]
  • 尾置返回类型
    • 在C++11中引入尾置返回类型
    • 任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效
    • 尾置返回类型跟在形参列表后面并以一个->符号开头
    • 为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个auto
  • 使用decltype
    • 知道函数返回的指针指向哪个数组就可以使用decltype关键字声明返回类型
    • decltype并不负责把数组类型转换成对应的指针,结果是个数组
    • 要想返回指针还必须在函数声明时加一个*符号
1
2
3
4
5
6
7
8
9
10
// 尾置返回类型
// 返回一个指针,该指针指向含有10个整数的数组
auto func(int) -> int (*)[10];

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};

decltype(odd) *arrPtr(int i){
return (i%2):&odd:&even;
}

6.4 函数重载

  • 重载函数:同一个作用域内的几个函数名字相同但形参列表不同的函数
    • main函数不能重载
  • 定义重载函数
    • 重载函数在形参数量或形参类型上不同
    • 不允许两个参数除了返回类型外其他所有要素都相同
  • 重载和const形参
    • 顶层const不影响传入函数的对象
    • 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开
    • 形参是某种类型的指针或引用,指向的是常量对象和指向非常量对象的函数可以重载,此时的const是底层的
  • const_cast和重载
    • const_cast在重载函数的情景中最有用const_cast
  • 函数匹配:把函数调用与一组重载函数中的某一个关联起来。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数
  • 在内层作用域中声明名字,将隐藏外层作用域中声明的同名实体
    • 在不同的作用域中无法重载函数名
1
2
3
4
5
6
7
8
9
const string &shorterString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1 : s2;
}

string &shorterString(string &s1, string &s2){
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}

6.5 特殊用途语言特性

6.5.1 默认实参

  • 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参
  • 默认实参初始化
    • 局部变量不能作为默认实参
    • 只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参
    • 函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值
    • 用作默认实参的名字在函数声明所在的作用域内解析,名字的求值过程发生在函数调用时

6.5.2 内联函数和constexpr函数

  • 内联函数:通常就是将它在每个调用点上内联地展开消除在运行时的开销
  • 在返回类型前面加关键字inline就可以声明为内联函数
    • 一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数
  • constexpr函数是指能用于常量表达式的函数
    • 定义constexpr函数:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句
    • 执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值
    • 为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数
    • constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行
      • 例如,constexpr函数中可以有空语句、类型别名以及using声明
    • constexpr函数不一定返回常量表达式
  • 内联函数和constexpr函数经常放在头文件中
1
2
3
4
5
6
7
// 内联版本
inline const string &shorterString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1 : s2;
}

constexpr int new_sz(){return 42;}
constexpr int foo = new_sz; //正确

6.5.3 调试帮助

  • 屏蔽调试代码的方法
    • assert
    • NDEBUG
  • assert预处理宏
    • assert是一种预处理宏(预处理变量),类似于内联函数,定义在cassert文件中
    • 预处理名字由预处理器而非编译器管理,可以直接使用预处理名字而无须提供using声明
    • assert宏使用一个表达式作为它的条件: assert(expr);
      • 首先对expr求值,如果表达式为假,assert输出信息并终止程序的执行;为真,assert什么也不做
    • assert宏常用于检查“不能发生”的条件
  • NDEBUG
    • assert的行为依赖于一个名为NDEBUG的预处理变量的状态
    • 定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查
      • 可以把assert当成调试程序的一种辅助手段
    • 预处理器定义了对于程序调试很有用的名字
      • _ FILE _ 存放文件名的字符串字面值
      • _ LINE _ 存放当前行号的整型字面值
      • _ TIME _ 存放文件编译时间的字符串字面值
      • _ DATE _ 存放文件编译日期的字符串字面值
      • _ func _当前调试的函数的名字
1
2
3
4
5
6
7
8
9
10
11
// 使用#define语句定义NDEBUG从而关闭调试状态
$ CC -D NDEBUG main.c
// 等价于在main.c文件的一开始写#define NDEBUG

// 可以使用NDEBUG编写自己的条件调试代码
// 如果NDEBUG未定义,将执行#ifndef和#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略掉
void print(const int ia[], size_t size){
#ifndef NDEBUG
cerr << __func__ << size << endl;
#endif
}

6.6 函数匹配

  • 函数匹配步骤
    • 第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数
      • 候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见
    • 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数
      • 可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型
      • 如果函数含有默认实参,则在调用该函数时传入的实参数量可能少于它实际使用的实参数量
    • 第三步是从可行函数中选择与本次调用最匹配的函数
    • 如果有且只有一个函数满足下列条件,则匹配成功:
      • 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
      • 至少有一个实参的匹配优于其他可行函数提供的匹配

6.6.1 实参类型转换

  • 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
    1. 精确匹配,包括以下情况:
      • 实参类型和形参类型相同。
      • 实参从数组类型或函数类型转换成对应的指针类型
      • 向实参添加顶层const或者从实参中删除顶层const。
    2. 通过const转换实现的匹配
    3. 通过类型提升实现的匹配
    4. 通过算术类型转换或指针转换实现的匹配

6.7 函数指针

  • 函数指针指向的是函数而非对象
    • 函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关
    • 声明函数指针:在函数声明中,只需要用指针替换函数名即可
1
2
3
4
5
// 定义pf函数指针
bool (*pf)(const string &, const string &); //未初始化
// *pf两端的括号必不可少
// 如果不写这对括号,则pf是一个返回值为bool指针的函数
bool *pf(const string &, const string &); //未初始化
  • 使用函数指针
    • 把函数名作为一个值使用时,该函数自动地转换成指针
    • 直接使用指向函数的指针调用该函数,无须提前解引用指针
    • 可以为函数指针赋一个nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数
  • 重载函数指针:如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// pf函数指针
pf = lengthCompare; //pf指向lengthCompare函数
pf = &lengthCompare; //等价赋值语句

bool b1 = pf("hello","goodbye"); //调用lengthCompare函数

string::size_type sumLength(const string&,const string&);
bool cstringCompare(const char*, const char*);
pf = 0; //正确,不指向任何函数
pf = sumLength; //错误,返回类型不匹配

pf = cstringCompare; //错误,形参类型不匹配
pf = lengthCompare; //正确

// 重载函数指针
void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned)
  • 函数指针形参
    • 和数组类似,虽然不能定义函数类型的形参,但形参可以是指向函数的指针
    • 形参看起来是函数类型,但实际上当作指针使用
      • 直接把函数作为实参,会自动转换成指针
  • 返回指向函数的指针
    • 必须把返回类型写出指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理
    • 要想声明一个返回函数指针的函数,最简单的办法是使用类型别名
      • 和函数类型的形参不一样,返回类型不会自动地转换成指针;需要显式地将返回类型指定为指针
    • 可以使用尾置返回类型的方式声明一个返回函数指针的函数
  • 将auto和decltype用于函数指针类型
    • 如果明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程
    • 将decltype作用于某个函数时,返回函数类型而非指针类型
      • 需要显式地加上*以表明我们需要返回指针,而非函数本身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void useBigger(const string &s1, const string &s2,
bool pf(const string &, const string &));
// 等价显式定义为指向函数的指针
void useBigger(const string &s1, const string &s2,
bool (*pf)(const string &, const string &));

// 自动将函数转换为函数指针
useBigger(s1,s2,lengthCompare);

// 返回指向函数的指针
using F = int(int *, int); //F是函数类型,不是指针
using PF = (int*)(int *,int); //PF是指针类型

PF f1(int); //正确;PF是指向函数的指针,f1返回指向函数的指针
F *f1(int); //正确;显式指定返回类型是指向函数的指针

//等价声明
int (*f1(int))(int*,int);
//照由内向外的顺序阅读这条声明语句:我们看到f1有形参列表,所以f1是个函数
// f1前面有*,所以f1返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int

auto f1(int) -> int (*) (int*,int);

7 类

  • 类的基本思想是数据抽象封装
    • 数据抽象是一种依赖于接口和实现分离的编程技术
      • 类的接口包括用户所能执行的操作;
      • 类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数
    • 封装实现了类的接口和实现的分离
      • 封装后的类隐藏了它的实现细节

7.1 定义抽象数据类型

7.1.1 定义改进的类

  • 成员函数的声明必须在类的内部,定义既可以在类的内部也可以在类的外部
    • 定义在类内部的函数是隐式的inline函数
  • 作为接口组成部分的非成员函数,定义和声明在类的外部
  • this参数
    • 成员函数通过一个名为this的额外的隐式参数来访问调用它的哪个对象
    • this是一个常量指针,总是指向这个对象
    • 在成员函数内部,可以直接使用调用该函数的对象的成员,而无须成员访问运算符,
      • 因为this所指的正是这个对象,任何对类成员的直接访问都被看作this的隐式引用
  • 引入const成员函数(常量成员函数)
    • 默认情况下,this的类型是指向类类型非常量版本的指针常量
      • 因此在默认情况下,不能把this绑定在常量对象上,不能在常量对象上调用普通的成员函数
    • 修改为指向常量的指针常量,需要在参数列表后添加const关键词,表示this是指向常量的指针常量
    • 常量对象,以及常量对象的引用或指针都只能调用常量成员函数
  • 在类的外部定义成员函数,定义需要与声明匹配,类外部定义的成员的名字必须包含所属的类名,使用::作用符
  • 定义返回this对象的函数
    • 函数的返回类型为类,return语句返回*this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 常量成员函数
std::string isbn() const
{
return bookNo;
}

// 等价于
std::string Sales_data::isbn(const Sales_data *const this)
{
return this->isbn;
}

// 定义返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs){
units_sold += rhs.units_dold;
revenue += tho.revenue;
return *this;
}

total.combine(trans); // 更新total的值
// total的地址被绑定到隐式的this参数上,而rhs绑定到了trans上。
// 效果等同于求total.units_sold和trans.unit_sold的和,然后把结果保存到total.units_sold中

// 无须使用隐式的this指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问:
return *this;
// 其中,return语句解引用this指针以获得执行该函数的对象,调用返回total的引用

7.1.3 定义类相关的非成员函数

  • 如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内
    • 在这种方式下,用户使用接口的任何部分都只需要引入一个文件

7.1.4 构造函数

  • 构造函数:初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行函数
    • 构造函数的名字和类名相同,但构造函数没有返回类型
    • 构造函数不能声明为const,直到构造函数完成初始化过程,对象才能真正取得其常量属性,构造函数在const对象的构造过程中可以向其写值
  • 合成的默认构造函数
    • 类通过一个特殊的构造函数来控制默认初始化过程
    • 如果存在类内的初始值,用它来初始化成员,否则默认初始化该成员
  • 不能合成默认构造函数的情况
    • 类没有声明任何构造函数,编译器会自动合成默认构造函数
    • 类包含有内置类型或复合类型的成员,则只有当这些成员全都被赋予了类内的初始值,这个类才适合使用默认的构造函数
  • =default
    • 在C++11中,需要默认的行为,可以在参数列表后写上 =default 来要求编译器生成构造函数
    • =default 既可以和声明一起出现在类的内部(默认构造函数为内联函数),可以作为定义出现在类外部
  • 构造函数初始化列表
    • 构造函数冒号后的部分为构造初始化列表,构造函数初始值是成员名字的一个列表,每个名字后面紧跟着括号括起来的成员初始值,不同成员的初始化通过逗号分隔开
    • 负责新创建的对象的一个或几个数据成员赋初值
  • 类的外部定义构造函数:使用 类名::类名 的结构

7.1.5 拷贝,赋值和析构

  • 编译器可以合成拷贝,赋值和析构函数,但生成的版本可能无法正常工作

7.2 访问控制与封装

  • 使用访问说明符加强类的封装性
    • 定义在public说明符之后的成员在整个程序内可被访问
      • public成员定义类的接口
    • 定义在private说明符之后的成员可被类的成员函数防备,但不能在使用类的代码访问
      • private部分封装了类的实现细节
  • 使用class或struct关键字
    • 使用class关键字定义类,成员默认是private的
    • 使用struct关键字定义类,成员默认是public的

7.2.1 友元

  • 友元:类可以允许其他类或者函数访问它的非共有成员,方法是令其他类或函数成为它的友元
    • 只需要增加一条以friend关键字开始的函数声明语句即可
    • 友元声明只能出现在类定义的内部,但类出现的具体位置不限
    • 友元不是类的成员也不受它所在区域访问控制级别的约束,最好再类定义开始或结束前的位置声明友元
  • 封装的好处
    • 确保用户代码不会无意间破坏封装对象的状态
    • 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码
    • 防止由于用户的原因造成数据被破坏
  • 友元的声明
    • 如果希望类的用户能够调用某个友元函数,那么就必须在友元声明之外再专门对函数进行一次声明
      • 为了使友元对类的用户可见,通常把友元的声明与类本身放置在同一个头文件中(类的外部)
      • 需要调用的友元函数,在类的外部需再次声明

7.3 类的其他特性

7.3.1 类成员再探

  • 类内部的成员函数自动是inline的
    • 可以在类内部把inline显式声明,但不推荐;最好在类外部定义的地方声明inline
  • 成员函数可以被重载
  • 可变数据成员
    • 使用mutable关键字
    • 一个可变数据成员永远不会是const,即使是const对象的成员
    • 一个const成员函数可以改变一个可变成员的值
  • 在C++11中将默认值声明成类内初始值,可以在默认初始化时拥有默认初始值
    • 类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式
1
2
3
4
5
6
7
8
9
10
11
12
13
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr; //即使在const对象内也能被修改
}

void Screen::some_member() const
{
++access_ctr;
}
// 尽管some_member是一个const成员函数,它仍然能够改变access_ctr的值
// 该成员是个可变成员,因此任何成员函数,包括const函数在内都能改变它的值

7.3.2 返回*this的成员函数

  • 定义返回this对象的函数
  • 从const成员函数返回*this
    • 一个const成员函数如果以引用的形式返回*this,那返回类型将是常量引用
    • 函数声明中返回的是const类型的类引用
  • 基于const的重载
    • 区分成员函数是否是const可以对函数进行重载
    • 非常量版本的函数对常量对象是不可用的,需要在一个常量对象上调用const成员函数;但可以在非常量对象上调用const成员函数,但建议显式匹配

7.3.3 类类型

  • 每个类定义了唯一的类型,即使两个类的成员列表完全一致,也是不同的类型
  • 可以把类名作为类型的名字使用,从而直接指向类类型
    • 或者,也可以把类名跟在关键字class或struct后面
  • 类的声明:声明类而暂时不定义
    • class 类名; 声明称为前向声明,向程序中引入类,但声明类之后类是一个不完全类型,不清楚包括哪些成员

7.3.4 友元再探

  • 类之间的友元关系
    • 友元类的成员函数可以访问此类包括非公有成员在内的所有成员
    • 友元关系不存在传递性,每个类负责控制自己的友元类或友元函数
  • 令成员函数作为友元
    • 当把一个成员函数声明为友元时,必须知名成员函数属于哪个类
  • 函数重载和友元
    • 一个类想把一组重载函数声明为它的友元,需要对这组函数中的每一个分别声明
  • 友元声明和作用域
    • 类和非成员函数的声明不是必须在友元声明之前
    • 在类的内部定义该函数,必须在类的外部提供相应的声明从而使得函数可见
      • 即使仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Screen{
// windos_mgr::clear必须在Screen类之前被声明
friend void Window_mgr::clear(ScreenIndex);
// Screen类的剩余部分
}
// 必须按照如下方式设计程序:
// · 首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen
// · 接下来定义Screen,包括对于clear的友元声明
// · 最后定义clear,此时它才可以使用Screen的成员

struct X{
friend void f(){}
X(){f();}
void g();
void h();
}

void X::g(){return f();} //错误f()未声明
void f();
void X::h(){return f();} //正确

7.4 类的作用域

  • 在类的作用域之外,普通的数据和函数成员只能由对象,引用或指针使用成员访问运算符进行访问
  • 对于类类型成员使用作用域运算符::来访问
  • 当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外时,返回类型必须指明它是哪个类的成员

7.4.1 名字查找与类的作用域

  • 内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过
  • 在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字
  • 在类中,成员函数中定义的成员会隐藏类中的同名成员,但可以使用类名或this指针强制访问成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef double Money;
class Accout{
public:
Money balance(){
return bal;
}
private:
typedef double Money; //错误,不能重新定义
}

class Screen{
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height){
cursor = width * height; //是那个参数
}
private:
pos cursor = 0;
pos height = 0, width = 0;
}

// 不建议的写法,成员函数中的名字不应该隐藏同名的成员
void Screen::dummy_fcn(pos height){
cursor = width * this->height; //成员height
// 或
cursor = width * Screen::height;
}

// 建议的写法:不要把成员名字作为参数或其他局部变量使用
void Screen::dummy_fcn(pos ht){
cursor = width * height; //成员height
}

// 不建议的写法,不应该隐藏外层作用域中可能用到的名字
void Screen::dummy_fcn(pos height){
cursor = width * ::height; //全局的height
}

7.5 构造函数再探

7.5.1 构造函数初始值列表

  • 初始化const或引用类型的唯一机会就是通过构造函数初始值
    • 如果成员是const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值
  • 建议:使用构造函数初始值
    • 在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值
    • 除了效率问题外更重要的是,一些数据成员必须被初始化
  • 构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序
    • 但一个成员是用于另一个成员初始化时,位置关系较为重要
    • 最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且若可能的话,尽量避免使用某些成员初始化其他成员
1
2
3
4
5
6
7
8
9
10
11
12
class ConstRef{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
}

// 构造函数体一开始执行,初始化就完成了
// 显式初始化引用和const成员
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i){}

7.5.2 委托构造函数

  • C++11中扩展了构造函数初始值的功能,可以定义委托构造函数
    • 委托构造函数使用它所属类的其他构造函数执行自己的初始化过程,它把自己的一些职责委托给了其他构造函数
    • 委托构造函数具有成员初始化列表和一个函数体,成员初始化列表只有类名本身一个入口
    • 成员初始值:类名后面紧跟着圆括号形成的参数列表,参数列表必须和类中另外一个构造函数匹配
    • 先执行该构造函数的函数体代码再执行被委托构造函数的函数体
1
2
3
4
5
6
7
8
9
class Sales_data{
public:
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price){}
// 其他构造函数委托给另一个构造函数
Sales_data() : Sales_data("",0,0){}
Sales_data(std::string s) : Sales_data(s,0,0){}
Sales_data(std::istream &is) : Sales_data{read(is,*this);}
};

7.5.3 默认构造函数的作用

  • 当对象被默认初始化或值初始化时自动执行默认构造函数
  • 默认初始化在以下情况下发生:
    • 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组
    • 当一个类本身含有类类型的成员且使用合成的默认构造函数
    • 当类类型的成员没有在构造函数初始值列表中显式地初始化时显式初始化
  • 值初始化在以下情况下发生
    • 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时
    • 当我们不使用初始值定义一个局部静态变量时,局部对象
    • 当我们通过书写形如T( )的表达式显式地请求值初始化时,其中T是类型名(vector的一个构造函数只接受一个实参用于说明vector大小,它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化)

7.5.4 隐式的类类型转换

  • 类定义隐式转换规则:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,这种构造函数称作转换构造函数
    • 在需要该类对象的地方,可以使用其他可转换的对象类型作为替代
    • 只允许一步类型转换,编译器指挥自动地执行一步类型转换
  • 可以将构造函数声明为explicit抑制构造函数定义的隐式转换
    • explicit只对一个实参的构造函数有效
  • explicit构造函数只能用于直接初始化
    • 发生隐式转换的一种情况是当执行拷贝形式(=)的初始化时
    • 以直接初始化的形式可以使用explicit构造函数抑制自动转换
1
2
3
4
5
6
7
8
9
10
11
12
string null_book = "9-999-99999-9";
// 构造一个临时的Sales_data对象
item.combine(null_book);

// 错误,两步转换
item.combine("9-999-99999-9");
// 正确
item.combine(string("9-999-99999-9"));

// 显式构造
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));

7.5.5 聚合类

  • 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式
  • 当一个类满足如下条件时,为聚合的:
    • 所有成员都是public的
    • 没有定义任何构造函数
    • 没有类内初始值,参考
    • 没有基类,也没有virtual函数
  • 可以使用花括号的成员初始值列表初始化聚合类的数据成员
    • 初始值的列表必须和声明的顺序一致
    • 初始值列表中的元素个数少于类的成员数量,靠后的成员被值初始化
1
2
3
4
5
6
struct Data{
int ival;
string s;
};

Data vall = {0, "Anna};

7.5.6 字面值常量类

  • constexpr,其中提到constexpr函数的参数和返回值都必须是字面值类型
  • 字面值类型的类可能含有constexpr函数成员
    • 成员必须符合constexpr函数的所有要求,是隐式const的
  • 数据成员都是字面值类型的聚合类是字面值常量类
  • 如果一个类不是聚合类,但符合下述要求,则也是一个字面值常量类:
    • 数据成员都必须是字面值类型
    • 类必须至少含有一个constexpr构造函数
    • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
    • 类必须使用析构函数的默认定义
  • constexpr构造函数
    • 字面值常量类的构造函数可以是constexpr函数
    • constexpr构造函数可以声明成=default的形式(或者是删除函数的形式)
      • 否则,constexpr构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又符合constexpr函数的要求(意味着它能拥有的唯一可执行语句就是返回语句
    • 由上述可知,constexpr构造函数体一般来说应该是空的
    • constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式
      • constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 字面值常量类
class Debug{
public:
constexpr Debug(bool b = true): hw(b),io(b),other(b){}
constexpr Debug(bool h, bool i, bool o): hw(h),io(i),other(o){}
constexpr bool any(){return hw||io||other;}
void set_io(bool b){io=b;}
void set_hw(bool b){hw=b;}
void set_other(bool b){hw=b;}
private:
bool hw;
bool io;
bool other;
}

// constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型
constexpr Debug io_sub(false,true,false); //调试io
// 等价于
if(io_sub.any())
cerr << "print error" <<endl;

constexpr Debug prod(false); //无调试
// 等价于
if(prod.any())
cerr << "print error" <<endl;

7.6 类的静态成员

  • 声明静态成员
    • 声明成员为static使得其与类关联在一起
    • 可以是public或private,可以是常量,引用,指针,类类型等
  • 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
    • 静态成员函数不会与任何对象绑定在一起,不包含this指针
    • 静态成员函数不能声明称const,且不能在静态成员函数中使用this指针
  • 通过**作用域运算符::**访问静态成员,可以使用类的对象,引用或指针来访问静态成员。
    • 成员函数不用通过作用域运算符就可以直接访问静态成员
  • 定义静态成员
    • 可以在类的内部或外部定义静态成员函数
    • 在类的外部定义静态成员时,不能重复static关键字,该static关键字只能出现在类内部的声明中
    • 静态数据成员不属于类的任何一个对象,不是由类的构造函数初始化的,因此需要在类的外部定义和初始化每个静态成员,静态数据只能定义一次,且存在于程序的整个声明周期
  • 静态成员的类内初始化
    • 通常情况下,类的静态成员不应该在类的内部初始化
    • 然而,可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr
    • 即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员
  • 静态数据可以作为默认实参,非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义并初始化一个静态成员
double Account::interestRate = initRate();
// 该对象是类Account的静态成员,其类型是double
// 从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了
// 因此,可以直接使用initRate函数
// 注意,虽然initRate是私有的,也能用它初始化interestRate

// 静态成员的类内初始化
class Account{
public:
static double rate() {return interestRate;}
static void rate(double);
private:
static constexpr int period = 30; //常量表达式
double daily_tbl[period];
}

8 IO库

  • io库的基础设施
    • istream
    • ostream
    • cin
    • cout
    • cerr
    • >>
    • <<
    • getline()

8.1 IO类

IO库类型和头文件 类型
iostream istream,wistream从流读取数据
ostream,wistream向流写入数据
iostream,wiostream读写流
fstream ifstream,wifstream从文件读取数据
ofstream,wofstream向文件写入数据
fstream,wfstream读写文件
sstream istringstream,wistringstream从string读取数据
ostringstream,wostringstream向string写入数据
stringstream,wstringstream读写string
  • 宽字符版本的类型和函数的名字以w开始
    • wcin,wcout和wcerr对应cin,cout,cerr
  • IO类型间的关系
    • 概念上,设备类型和字符大小都不会影响要执行的IO操作
    • 标准库使我们可以忽略不同类型的流之间的差异,是通过继承机制实现的

8.1.1 IO对象无拷贝或赋值

  • 不能拷贝或对IO对象赋值,因此不能将形参或返回类型设置为流类型
    • 进行IO操作的函数通常以引用方式传递和返回流,读写一个IO对象会改变其状态,传递和返回引用不能是const

8.1.2 条件状态

IO库条件状态 -
strm::iostate strm是一种IO类型,iostate是一种机器相关的类型,提供了表达条件状态的完整功能
strm::badbit 指出流已崩溃
strm::failbit 指出一个IO操作失败
strm::eofbit 指出流到达了文件结束
strm::goodbit 指出流未处于错误状态
s.eof() 若流s的eofbit置位,则返回true
s.fail() 若流s的failbit或badbit置位,则返回true
s.bad() 若流s的badbit置位,则返回true
s.good() 若流s处于有效状态,则返回true
s.clear() 若流s中所有条件状态位复位,将流的状态设置位有效,返回void
s.clear(flags) 根据给定的flags标志位,将流s中对应条件状态位复位。flags的类型位strm::iostate。返回void
s.setstate(flags) 根据给定的flags标志位,将流s中对应条件状态位置位。flags的类型位strm::iostate。返回void
s.rdstate() 返回流s的当前条件状态,返回类型位strm::iostate
  • IO库定义了一个与机器无关的iostate类型,提供了表达流状态的完整功能,这个类型作为一个位集合使用
    • IO库定义了4个iostate类型的constexpr值,表示特定的位状态
    • badbit表示系统级错误,通常情况下,一旦badbit被置位,流就无法再使用了
    • 在发生可恢复错误后,failbit被置位,通常是可以修正的,流还可以继续使用
    • 如果到达文件结束位置,eofbit和failbit都会被置位
    • goodbit的值为0,表示流未发生错误
  • 管理条件状态
    • 流对象的rdstate成员返回一个iostate值,对应流的当前状态
1
2
3
4
5
6
7
8
9
// 记住cin当前状态
// clear不接受参数的版本清除(复位)所有错误标志位
auto old_state = cin.rdstate();
cin.clear();
process_input(cin); //使用cin
cin.setstate(old_state); //置位会原有状态
// 带参数的clear版本接受一个iostate值,表示流的新状态
// 复位failbit和badbit,其他位保持不变
cin.clear(cin.rdstate & ~cin.failbit & ~cin.badbit);

8.1.3 管理输出缓冲

  • 每个输出流都管理一个缓冲区,用来保存程序读写的数据
  • 导致缓冲刷新的原因
    • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行
    • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区
    • 可以使用操纵符如endl来显式刷新缓冲区
    • 在每个输出操作之后,可以用操纵符unitbuf设置流的内部状态,来清空缓冲区
      • 默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的
    • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新
      • 默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新
  • 刷新输出缓冲区
    • endl
    • IO库中的flush和ends
      • flush刷新缓冲区,但不输出任何额外的字符
      • ends向缓冲区插入一个空字符,然后刷新缓冲区
  • unitbu操作符
    • 可控制不每次输出操作后都刷新缓冲区
    • nounitbu重置流,使其恢复正常使用
  • 程序崩溃,输出缓冲区不会被刷新
  • 关联输入和输出
    • 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流
    • 标准库将cout和cin关联在一起
    • tie有两个重载的版本
      • 不带参数:返回指向输出流的指针(如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针)
      • 接受一个指向ostream的指针:将自己关联到此ostream( x.tie(&o)将x关联到输出流o)
1
2
3
4
5
6
7
8
9
10
11
12
cout<<"hi"<<endl;
cout<<"hi"<<flush;
cout<<"hi"<<ends;

cout << unitbuf;
cout << nounitbuf;

cin.tie(&cout);

ostream *old_tie = cin.tie(nullptr);
cin.tie(&cerr);
cin.tie(old_tie);

8.2 文件输入输出

  • 头文件fstream定义了三个类型来支持文件IO:
    • ifstream从一个给定文件读取数据
    • ofstream向一个给定文件写入数据
    • fstream可以读写给定文件
  • 使用IO运算符读写文件,可以用getline从一个ifstream读取数据
fstream特有的操作 -
fstream fstrm 创建一个未绑定的文件流。fstream时头文件fstream中定义的一个类型
fstream fstrm(s) 创建一个未绑定的文件流,并打开名为s的文件
fstream fstrm(s,mode) 创建一个未绑定的文件流,以mode打开名为s的文件
fstrm.open(s) 打开名为s的文件,并将文件与fstrm绑定
fstrm.close() 关闭于fstrm绑定的文件,返回void
fstrm.is_open() 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭

8.2.1 使用文件流对象

  • 想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来,每个文件流类都定义了一个名为open的成员函数,完成一些系统相关的操作,用来定位给定的文件,并视情况打开为读或写模式
  • 用fstream代替iostream&
    • 在要求使用基类型对象的地方,可以用继承类型的对象来替代
    • 接受一个iostream类型引用(或指针)参数的函数,可以用一个对应的fstream(或sstream)类型来调用
      • 如果有一个函数接受一个ostream&参数,在调用这个函数时,可以传递给它一个ofstream对象,对istream&和ifstream也是类似的
  • 成员函数open和close
    • 定义一个文件流对下昂,随后可以调用open将文件关联起来(调用open失败,failbit会被置位)
  • 自动析构:当一个fstream对象被销毁时,close会自动被调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ifstream in(ifile); //构建一个ifstream并代开给定文件
ofstream out; //输出文件流未关联到任何文件

ifstream input(argv[1]); //打开销售记录文件
ofstream output(argv[2]);
Sales_data total;
if(read(input,total)){
Sales_data trans;
while(read(input,trans)){
if(total.isbn()==trans.isbn())
total.combine(trans);
else{
print(output,total)<<endl;
total = trans;
}
}
print(out,total) <<endl;
} else
cerr<<"No data?!"<<endl;

// open文件流
ifstream in(ifile);
ofstream out;
out.open(ifile + ".copy");

if(out) //检测open是否成功

in.close;
in.open(ifile + "2");

8.2.2 文件模式

文件模式 -
in 以读方式打开
out 以写方式打开
app 每次写操作前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式进行IO
  • 只可以对ofstream或fstream对象设定out模式
  • 只可以对ifstream或fstream对象设定in模式
  • 只有当out也被设定时才可设定trunc模式
  • 只要trunc没被设定,就可以设定app模式。在app模式下,即使没有显式指定out模式,文件也总是以输出方式被打开
  • 默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,即打开文件同时进行读写操作
  • ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用
  • 每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模
    • 与ifstream关联的文件默认以in模式打开
    • 与ofstream关联的文件默认以out模式打开
    • 与fstream关联的文件默认以in和out模式打开
  • 在每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值

8.3 string流

  • istringstream从string读取数据
  • ostringstream向string写入数据
  • stringstream既可从string读数据也可向string写数据
stringstream特有地操作 -
sstream strm; strm是一个未绑定的stringstream对象。sstream是头文件sstream中定义的一个类型
sstream strm(s); strm是一个sstream对象。保存string s的一个拷贝。此构造函数时explicit的
strm.str() 返回strm所保存的string的拷贝
strm.str(s) 将string s拷贝strm中。返回void
  • 使用istringstream:当某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,通常可以使用istringstream
  • 使用ostringstream:当逐步构造输出,希望最后一起打印时,ostringstream是很有用的

9 序容器

  • 顺序容器为程序员提供了控制元素存储和访问顺序的能力

9.1 顺序容器概述

顺序容器类型 -
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素会很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快
forward_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度很快
array 固定大小数组。支持快速随机访问。不能添加或删除元素
string 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快
  • 除了固定大小的array外,其他容器都提供高效,灵活的内存管理
    • 可以添加和删除元素,扩张和收缩容器的大小
    • string和vector将元素保存在连续的内存空间中
      • 可以由元素的下标来计算其地址是非常快速的,两个容器的中间位置添加和删除元素会非常耗时
    • list和forward_list两个容器的设计目的是令容器任何位置的添加和删除都很快捷,但不支持元素的随机访问(为了访问元素只能遍历整个容器),其额外内存开销也很大
    • deque中间的位置添加或删除元素的代价很高,但在两端添加或删除元素很快,与list和forward_list添加删除元素的速度相当
  • C++11中新添加的类型,forward_list和array
    • 与内置数组类似,array对象的大小是固定的
      • array不支持添加和删除元素以及改变容器大小的操作
    • forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能
      • 因此forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销
      • 对其他容器而言,size保证是一个快速的常量时间的操作
  • 选择容器的基本原则
    • 除非你有很好的理由选择其他容器,否则应使用vector
    • 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list
    • 如果程序要求随机访问元素,应使用vector或deque
    • 如果程序要求在容器的中间插入或删除元素,应使用list或forward_list
    • 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque
    • 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则
      • 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素
      • 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中

9.2 容器库概览

  • 每个容器都定义在一个头文件中,文件名与类型名相同,容器均为模板类
  • 对容器可保存的元素类型的限制
    • 顺序容器可以保存任意类型的元素,特别是,可以定义一个容器,其元素类型是另一个容器
    • 可以为不支持特定操作需求的类型定义容器(但只能使用那些没有特殊需求的容器操作)
容器操作 -
类型别名 -
iterator 此容器类型的迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
size_type 无符号整数类型,足够保存此种容器类型最大可能容器的大小
difference_type 带符号整数类型,足够保存两个迭代器之间的距离
value_type 元素类型
reference 元素的左值类型,与value_type&含义相同
const_reference 元素的const左值类型,与const value_type&含义相同
构造函数 -
C c 默认构造函数,构造空容器
C c1(c2) 构造c2的拷贝c1
C c(b,e) 构造c,将迭代器b和e指定的范围内的元素拷贝到c
C c{a,b,c…} 列表初始化c
赋值与swap -
c1=c2 将c1中的元素替换成c2中元素
c1={…} 将c1中的元素替换成列表中元素
a.swap(b) 交换a和b的元素
swap(a,b) 交换a和b的元素
大小 -
c.size() c中的元素数目(不支持forward_list)
c.max_size() c中可保存的最大元素数目
c.empty() c保存了元素返回false
添加/删除元素(不适用于array) -
c.insert(args) 将args中的元素拷贝进c
c.emplace(inits) 使用inits构造c中的一个元素
c.erase(args) 删除args指定的元素
c.clear() 删除c中的所有元素
关系运算符 -
==,!= 所有容器都支持相等运算符
<,>=,<=,>= 关系运算符(无序容器不支持)
获取迭代器 -
c.begin() c.end() 返回迭代器
c.cbegin() c.cend() 返回const_iterator
反向容器的额外成员(不支持forward_list -
reverse_iterator 逆序寻址元素的迭代器
const_reverse_iterator 不能修改元素的逆序迭代器
c.rbegin() c.rend() 返回reverse_iterator
c.crbegin() c.crend() 返回const_reverse_iterator

9.2.1 迭代器

  • 与容器一样,迭代器有公共的接口:迭代器提供的操作的实现方式都是相同的
    • forward_list迭代器不支持递减运算符
  • 迭代器范围
    • 一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一容器中的元素(begin,first)或者尾元素(end,last,尾元素的下一个元素)之间的元素
    • 元素范围为左闭合区间
  • 构成范围的迭代器的要求
    • 两个迭代器begin和end构成一个迭代器
      • 指向同一容器的元素,或者是容器最后一个元素之后的位置,且可以通过递增begin到达end

9.2.3 begin和end成员

  • begin和end操作生成指向容器第一个元素和尾元素之后位置的迭代器,形成一个包含容器内所有元素的迭代器范围
  • 带r的反向迭代器
  • 带c的返回const迭代器的版本
    • C++11新引入的,用于支持auto与begin和end函数相结合
    • 当不需要写访问时,应该使用cbegin和cend

9.2.4 容器定义和初始化

容器定义和初始化 -
C c 默认构造函数,构造空容器,若C是array,则c中元素按默认方式初始化
C c1(c2) 构造c2的拷贝c1
C c1 = c2 相同的容器类型,且保存的是相同的元素类型;对于array还应具有相同的大小
C c{a,b,c…} 列表初始化c
C c = {a,b,c…} 列表初始化c
C c(b,e) 构造c,将迭代器b和e指定的范围内的元素拷贝到c
只有顺序容器(不包括array)的构造函数才可以接受大小参数 -
C seq(n) seq包含n个元素,这些元素进行值初始化;此构造函数是explicit
C seq(n,t) seq包含n个初始值为t的元素
  • 将一个容器初始化为另一个容器的拷贝
    • 可以直接拷贝整个容器
      • 两个容器的类型及其元素类型必须匹配
    • 拷贝由一个迭代器对指定元素范围
      • 新容器和原容器中元素类型可以不同,只要能将拷贝元素转换为要初始化的容器的元素类型即可
      • 构造函数接受两个迭代器参数,为第一个元素和尾元素之后的元素
  • 列表初始化
    • C++11中可以对容器进行列表初始化,显式指定容器中每个元素的值
  • 与顺序容器大小相关的构造函数
    • 接受一个容器大小和一个可选的元素初始值
    • 只有顺序容器的构造函数才接受大小参数,关联容器并不支持
  • 标准库array由固定大小
    • 定义array时不仅要指定元素类型,也要指定大小
    • 不支持普通的容器构造函数,一个默认构造的array非空,包含于大小一样多的默认初始化的元素
1
2
3
4
5
6
7
8
9
10
11
12
list<string> authors = {"Milton","Shakespeare","Austen"};
vector<const char*> articles = {"a","an","the"};

list<string> list2(authors); //正确
deque<string> authList(authors); //错误,容器类型不匹配
forward_list<string> words(articles.begin(),articles.end()); //正确,可以将const char*元素转换为string

//拷贝元素,直到(不包括)it指向的元素
deque<string> authList(authors.begin(),it);

array<int,42> //保存42个int的数组
array<int,10>::size_type i; //数组类型包括元素类型和大小

9.2.5 赋值和swap

赋值与swap -
c1=c2 将c1中的元素替换成c2中元素
c1={…} 将c1中的元素替换成列表中元素
a.swap(b) 交换a和b的元素
swap(a,b) 交换a和b的元素
assign操作不适用于关联容器和array -
seq.assign(b,e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素
seq.assign(il) 将seq中的元素替换为初始化列表il中的元素
seq.assign(n,t) 将seq中的元素替换为n个值为t的元素
  • 赋值相关操作会导致指向坐标容器内部的迭代器,引用和指针失效
  • swap会将容器内容交换,不会导致指向容器的迭代器,引用和指针失效(容器类型为array和string除外)
  • 使用assign(仅顺序容器)
    • 允许从一个不同但相容的类型赋值,或者从容器的一个子序列赋值
    • 由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器
  • 使用swap
    • 除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成

9.2.7 关系运算符

  • 每个容器类型都支持相等运算符
  • 除了无序关联容器外的所有容器都支持关系运算符
  • 比较两个容器实际上是进行元素的逐对比较
    • 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等
    • 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器
    • 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果
  • 只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器
    • 容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符

9.3 顺序容器操作

9.3.1 向顺序容器添加元素

  • 除array外,所有的标准库容器提供灵活的内存管理,在运行时可以动态添加或删除元素来改变容器大小
    • forward_list有自己版本的insert和emplace
    • forward_list不支持push_back和emplace_back
    • vector和string不支持push_front和emplace_front
向顺序容器添加元素的操作 -
c.push_back(t) 在c的尾部创建一个值为t或由args创建的元素
c.emplace_back(t) 在c的尾部创建一个值为t或由args创建的元素
c.push_front(t) 在c的头部创建一个值为t或由args创建的元素
c.emplace_front(t) 在c的头部创建一个值为t或由args创建的元素
c.insert(p,t) 在迭代器p指向的元素之前创建一个值为t的元素。返回指向新添加元素的迭代器
c.emplace(p,args) 在迭代器p指向的元素之前创建一个值为t的元素。返回指向新添加元素的迭代器
c.insert(p,n,t) 在迭代器p指向的元素之前创建n个值为t的元素。返回指向新添加的第一个元素的迭代器
c.insert(p,b,e) 将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前
c.insert(p,il) il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回p
  • 向一个vector,string或deque插入元素会使所有指向容器的迭代器,引用和指针失效
  • 使用push_back
    • 除了array和forward_list之外,每个顺序容器都支持push_back
    • 当用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身
  • 使用push_front
    • list,forward_list和deque还支持push_front,将元素插入到容器头部
    • deque提供了随机访问元素的能力,且支持push_front,但vector具有随机访问的能力不支持push_front,插入元素非常耗时
  • 在容器特定位置添加元素:insert成员,vector、deque、list和string都支持insert成员,但vector、deque和string插入元素耗时
  • 插入范围内元素:insert带参数的版本
  • 使用emplace操作
    • emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素,分别对应push_front、insert和push_back,允许将元素放置在容器头部、一个指定位置之前或容器尾部
    • 当调用push或insert成员函数时,将元素类型的对象传递给它们,这些对象被拷贝到容器中
    • 而当调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数,emplace成员使用这些参数在容器管理的内存空间中直接构造元素(传递给emplace函数的参数必须和构造函数相匹配)
1
2
3
4
5
6
// 在c的末尾构造一个Sales_data对象
// 使用三个参数的Sales_data构造函数
c.emplace_back("978-0590353403",25,15.99);

// 创建一个临时的对象传递给push_back
c.push_back(Sales_data("978-0590353403",25,15.99));

9.3.2 访问元素

  • 包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数,分别返回首元素和尾元素的引用
    • 间接的方法:通过解引用begin返回的迭代器来获取首元素的引用,以及通过递减然后解引用end返回的迭代器来获得尾元素的引用
  • at和下标操作只适用于string,vector,deque和array
    • back不适用于forward_list
在顺序容器中访问元素的操作 -
c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
c.front() 返回c中首元素的引用。若c为空,函数行为未定义
c[n] 返回c中下标未n的元素的引用
c.at(n) 返回下标未n的元素引用
  • 访问成员函数返回的是引用:在容器中访问元素的成员函数返回的都是引用
    • 如果容器是一个const对象,则返回值是const的引用
    • 如果使用auto变量来保存函数的返回值,使用此变量来改变元素的值,应将变量定义为引用类型
  • 安全的随机访问:使用at函数,类似下标运算符,若下标越界则抛出out_of_range异常

9.3.3 删除元素

  • forward_list由特殊版本的erase
  • forward_list不支持pop_back;vector和string不支持pop_front
  • 删除deque中除首尾位置之外的任何元素都会使所有迭代器,引用和指针失效
    • 指向vector或string中删除点之后位置的迭代器,引用和指针都会失效
  • 删除元素的成员函数并不检查其参数,在删除之前必须确保它们使存在的
顺序容器的删除操作 -
c.pop_back() 删除c中尾元素
c.pop_front() 删除c中首元素
c.erase(p) 删除迭代器p所指的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器
c.erase(b,e) 删除迭代器b和e所指范围内的元素
c.clear() 删除c中的所有元素,返回void

9.3.4 特殊的forward_list操作

在forward_list中插入或删除元素的操作 -
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用
lst.cbefore_begin() -
lst.insert_after(p,t) 在迭代器p之后的位置插入元素。t是一个对象
lst.insert_after(p,n,t) 在迭代器p之后的位置插入元素。t是一个对象,n是数量
lst.insert_after(p,b,e) b,e表示范围的一对迭代器
lst.insert_after(p,il) il是一个花括号列表
emplace_after(p,args) 使用args在p指定的位置之后创建一个元素
lst.erase_after(p) 删除p指向的位置之后的元素
lst.erase_after(b,e) 删除b之后直到e之间的元素

9.3.5 改变容器大小

  • resize来增大或缩小容器(array不支持resize)
    • 如果当前大小大于所要求的大小,容器后部的元素会被删除
    • 如果当前大小小于新大小,会将新元素添加到容器后部
顺序容器大小操作 -
c.resize(n) 调整c的大小为n个元素
c.resize(n,t) 调整c的大小为n个元素,任何新添加的元素都初始化为值t

9.3.6 容器操作可能是迭代器失效

  • 向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题
  • 在向容器添加元素后:
    • 如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效
    • 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效
    • 对于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
      当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了。当我们删除一个元素后:
      • 对于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效
      • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响
      • 对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效。注意:当我们删除元素时,尾后迭代器总是会失效
  • 不要保存end返回的迭代器

9.4 vector对象是如何增长的

  • vector和string的实现通常会分配比新的空间需求更大的内存空间,容器预留了这些文件作为备用,用来保存更多的新元素
    • 这种策略比每次添加新元素时重新分配内存空间高效,实际性能表现的足够好
  • 管理容器的成员函数
    • vector和string类型提供了一些成员函数,允许我们与它的实现中内存分配部分互动
    • capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素
    • reserve操作允许我们通知容器它应该准备保存多少个元素
  • shrink_to_fit只适用于vector,string和deque
    • capacity和reserve只适用于vector和string
    • reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间
      • 如果需求大小大于当前容量,reserve至少分配与需求一样大的内存空间(可能更大)
      • 如果需求大小小于或等于当前容量,reserve什么也不做(且容器不会退回内存空间)
  • 在C++11中,可以调用shrink_to_fit来要求deque、vector或string退回不需要的内存空间
    • 此函数指出不再需要任何多余的内存空间
    • 但具体的实现可以选择忽略此请求(调用shrink_to_fit也并不保证一定退回内存空间)
容器大小管理操作 -
c.shrink_to_fit() 将capacity()减少为于size()相同大小
c.capacity() 不重新分配内存空间的话,c可以保存多少元素
c.reserve(n) 分配至少能容纳n个元素的内存空间
  • capacity和size
    • size:指它已经保存的元素的数目
    • capacity:在不分配新的内存空间的前提下最多可以保存多少元素
  • 每个vector实现都可以选择自己的内存分配策略
    • 但必须遵守的一条原则:只有当迫不得已时才可以分配新的内存空间
    • 只有在执行insert操作时size与capacity相等,或者调用resize或reserve时给定的大小超过当前capacity,vector才可能重新分配内存空间
  • 虽然不同的实现可以采用不同的分配策略,但所有实现都应遵循一个原则:
    • 确保用push_back向vector添加元素的操作有高效率
    • 从技术角度说,就是通过在一个初始为空的vector上调用n次push_back来创建一个n个元素的vector,所花费的时间不能超过n的常数倍

9.5 额外的string操作

9.5.1 构造string的其他方法

容器定义和初始化方法

string支持的其他构造函数 -
string s(cp,n) s是cp指向的数组中前n个字符的拷贝,此数组至少应该包括n个字符
string s(s2,pos2) s是string s2从下标pos2开始的字符的拷贝
string s(s2,pos2,len2) s是string s2从下标pos2开始len2个字符的拷贝
  • 构造函数接受一个string或一个const char*参数
    • 传递一个string时可以给定一个下标来指出从哪里开始拷贝
    • 传递const char*时指针指向的数组必须以空字符结尾,拷贝操作时遇到空字符时停止
    • 传递构造函数一个计数值,数组就不必以空字符结尾
  • substr操作
    • s.substr(pos,n) 返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0.n的默认值为s.size()-pos,即拷贝从pos开始的所有字符

9.5.2 改变string的其他方法

修改string的操作 -
s.insert(pos,args) 在pos之前插入args指定的字符(pos可以是下标或一个迭代器),接受下标的版本返回一个指向s的引用;接受迭代器的版本返回指向第一个插入字符的迭代器
s.erase(pos,len) 删除从位置pos开始的len个字符。若len被省略则删除所有字符。返回一个指向s的引用
s.assign(args) 将s中的字符替换args指定的字符。返回一个指向s的引用
s.append(args) 将args追加到s,返回一个指向s的引用
s.replace(range,args) 删除s中范围range内的字符,替换为args指定的字符。range或者是一个下标和一个长度,或者是一对指向s的迭代器。返回一个指向s的引用
args可以是下列形式之一; append和assign可以使用所有形式
str不能与s相同,迭代器版本b和e不能指向e -
str 字符串str
str,pos,len str中从pos开始最多len个字
cp,len 从cp指向的字符数组的前len个字符
cp cp指向的以空字符结尾的字符数组
n,c n个字符c
b,e 迭代器b和e指定的范围内的字符
初始化列表 花括号包围的,以逗号分隔的字符列表

9.5.3 string搜索操作

  • 每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标
    • 如果搜索失败,则返回一个名为string::npos的static成员
    • 标准库将npos定义为一个const string::size_type类型,并初始化为值-1
      • 由于npos是一个unsigned类型,此初始值意味着npos等于任何string最大的可能大小
string搜索操作 -
s.find(args) 查找s中args第一次出现的位置
s.rfind(args) 查找s中args最后一次出现的位置
s.find_first_of(args) 查找s中args中任何一个字符第一次出现的位置
s.find_last_of(args) 查找s中args中任何一个字符最后一次出现的位置
s.find_first_not_of(args) 查找s中第一个不在args中的字符
s.find_last_not_of(args) 查找s中最后一个不在args中的字符
args可以是下列形式之一 -
c,pos 从s中位置pos开始查找字符c。pos默认值为0
s2,pos 从s中位置pos开始查找字符串s2。pos默认值为0
cp,pos 从s中位置pos开始查找指针cp指向的以空字符结尾的C风格字符串。pos默认值为0
cp,pos,n 从s中位置pos开始查找指针cp指向的数组的前n个字符。pos默认值为0
1
2
3
4
5
6
string::size_type pos = 0;
// 每步循环查找name中下一个数
while((pos=name.find_first_of(numbers,pos))!=string::npos){
cout<<pos<<name[pos]<<endl;
++pos;
}

9.5.4 compare函数

s.compare的几种参数形式 -
s2 比较s和s2
pos1,n1,s2 将s中从pos1开始的n1个字符与s2进行比较
pos1,n1,s2,pos2,n2 将s中从pos1开始的n1个字符与s2中从pos2开始的n2个字符进行比较
cp 比较s与cp指向的以空字符结尾的字符串数组
pos1,n1,cp 将s中从pos1开始n1的字符与cp指向的以空字符结尾的字符数组进行比较
pos1,n1,cp,n2 将s中从pos1开始n1的字符与cp指向的地址开始的n2个字符进行比较

9.5.5 数值转换

  • 如果string不能转换为一个数值,这些函数抛出一个invalid_argument异常
  • 如果转换得到的数值无法用任何类型来表示,则抛出一个out_of_range异常
string和数值之间的转换 -
to_string 一组重载函数,返回数值val的string表示
stoi(s,p,b) 返回s的起始子串(表示整数内容),返回值类型为int
stol(s,p,b) 返回s的起始子串(表示整数内容),返回值类型为long
stoul(s,p,b) 返回s的起始子串(表示整数内容),返回值类型为unsigned long
stoll(s,p,b) 返回s的起始子串(表示整数内容),返回值类型为long long
stoull(s,p,b) 返回s的起始子串(表示整数内容),返回值类型为unsigned long long
stof(s,p) 返回s的起始子串(表示浮点数内容)的数值,返回值类型为float
stod(s,p) 返回s的起始子串(表示浮点数内容)的数值,返回值类型为double
stold(s,p) 返回s的起始子串(表示浮点数内容)的数值,返回值类型为long double

9.6 容器适配器

  • 顺序容器适配器:stack,queue和priority_queue
  • 适配器是标准库中一个通用概念(容器,迭代器和函数都有适配器)
    • 一个内容适配器接受一个已有的容器类型,使其行为看起来像一种不同的类型
所有容器适配器都支持的操作和类型 -
to_string 一组重载函数,返回数值val的string表示
value_type 元素类型
container_type 实现适配器的底层容器类型
A a 创建一个名为a的空适配器
A a© 创建一个名为a的适配器,带有容器c的一个拷贝
关系运算符 运算符返回底层容器的比较结果
a.empty() 若a包含任何元素,返回false
a.size() 返回a中的元素数目
swap(a,b) 交换a和b的内容,a和b必须由相同类型,包括底层容器类型也必须相同
a.swap(b) 交换a和b的内容,a和b必须由相同类型,包括底层容器类型也必须相同
  • 定义一个适配器:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器
    • 默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的
  • 对于一个给定的适配器,可以使用哪些容器是有限制的
    • 所有适配器都要求容器具有添加和删除元素的能力(适配器不能构造在array上)
      • 类似的,也不能用forward_list来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力
    • stack只要求push_back、pop_back和back操作,因此可以使用除array和forward_list之外的任何容器类型来构造stack
    • queue适配器要求back、push_back、front和push_front,因此它可以构造于list或deque之上,但不能基于vector构造
    • priority_queue除了front、push_back和pop_back操作之外还要求随机访问能力,因此它可以构造于vector或deque之上,但不能基于list构造
1
2
3
4
5
6
stack<int> stk(deq);    //从deq拷贝元素到stk

// 在vector上实现的空栈
stack<string,vector<string>> str_stk;
// str_stk2在vector上实现,初始化时保存svec的拷贝
stack<string,vector<string>> str_stk2(svec);
  • 栈适配器
    • stack类型定义在stack头文件中
    • 栈默认基于deque实现,也可以在list或vector上实现的
未列出的栈操作 -
s.pop() 删除栈顶元素,但不返回该元素值
s.push(item) 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来
s.emplace(args) 创建一个新元素压入栈顶,该元素由args构造
s.top() 返回栈顶元素,但不将元素弹出栈
1
2
3
4
5
6
7
8
9
10
stack<int> intStack;

// 填满栈
for(size_t ix = 0;ix!=10;++ix)
intStack.push(ix); //intStack保存0到9十个数
while(!intStack.empty()){
int value = intStack.top();
// 使用栈顶值的代码
intStack.pop(); //弹出栈顶元素,继续循环
}
  • 队列适配器
    • queue和priority_queue适配器定义在queue头文件中
    • 标准库queue使用一种先进先出(FIFO)的存储和访问策略
    • priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前
    • queue默认基于deque实现,priority_queue默认基于vector实现
      • queue也可以用list或vector实现,priority_queue可以用deque实现
未列出的queue和priority_queue操作 -
q.pop() 返回queue的首元素或priority_queue的最高优先级的元素
q.front() 返回首元素或尾元素,但不删除此元素
q.back() 只适用于queue
q.top() 返回最高优先级元素,但不删除该元素
- 只适用于priority_queue
s.push(item) 在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item
s.emplace(args) 在queue末尾或priority_queue中恰当的位置创建一个元素,其值由args构造

10 泛型算法

10.1 概述

  • 大多数的算法都定义在头文件algorithm中,还在头文件numeric中定义了一组数值泛型算法
    • 一般情况下,算法不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作(通常情况下,算法遍历范围,对其中每个元素进行一些处理)
  • 迭代器令算符不依赖于容器,但算法依赖于元素类型的操作
  • 算法永远不会执行容器的操作
    • 泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作
    • 泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小
    • 算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素

10.2 初始泛型算法

10.2.1 只读算法

  • 一些算法只会读取输入范围内的元素,而不改变元素
  • 对于读取而不改变元素的算法,通常最好使用cbegin()和cend(),但若需要使用算法返回迭代器来改变元素的值,需要使用begin()和end()的结果作为参数

10.2.2 写容器元素的算法

  • 一些算法将新值赋予序列中的元素,使用这些算法时,必须注意确保序列原大小至少不小于要求算法写入的元素数目
    • 算法不会执行容器操作,因此自身不可能改变大小
1
2
3
4
5
// 算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数
// fill将给定的这个值赋予输入序列的每个元素
fill(vec.begin(),vec.end(),0) //将每个元素重置为0
// 将容器的一个子序列设置为10
fill(vec.begin(),vec.begin()+vec.size()/2,10);
  • 迭代器参数
    • 一些算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器
    • 操作两个序列的算法之间的区别在于如何传递第二个序列
      • 如equal,接受三个迭代器,前两个表示第一个序列的范围,第三个表示第二个序列的首元素
        • 用一个单一的迭代器表示第二个序列的算法都是假定第二个序列至少与第一个一样长,确保算法不会试图访问第二个序列中不存在的元素
      • 其他算法接受四个迭代器,前两个表示第一个序列的元素范围,后两个表示第二个序列的范围
  • 算法不检查写操作
    • 一些算法接受一个迭代器来指出一个单独的目的位置,算法将新值赋予一个序列的元素,该序列从目的位置迭代器指向的元素开始
    • 向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
// 可以用fill_n将一个新值赋予vector中的元素
vector<int> vec; //空vector
// 使用vec,赋予它不同值
fill_n(vec.begin(), vec.size(), 0); //将所有元素重置为0

// 函数fill_n假定写入指定个元素是安全的
fill_n(dest, n, val)
// fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素

// 在空容器上调用fill_n
vector<int> vec; //空向量
// 灾难:修改vec中的10个不存在元素
fill_n(vec.begin(), 10, 0);
  • 介绍back_inserter
    • 一种保证算法有足够元素空间来容纳数据的方法是使用插入迭代器
    • 插入迭代器是一种向容器中添加元素的迭代器,通常情况通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中
    • iterator函数中back_insert函数:接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器
      • 通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中
1
2
3
4
5
6
7
vector<int> vec;  //空向量
auto it = back_insert(vec);
*it = 42;

// 使用back_insert创建迭代器,作为算法的目的位置来使用
vector<int> vec; //空向量
fill_n(back_insert(vec),10,0); //添加10个元素到vec
  • 拷贝算法
    • 是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法
    • 接受三个迭代器:前两个表示一个输入范围,第三个表示目的序列的起始位置;将输入范围内的元素拷贝到目的系列中
    • copy算法
    • replace算法:读入一个序列,并将其中所有等于给定值的元素都改为另一个值,接收4个参数,前两个是迭代器,表示输入序列,后两个一个是搜索的值,另一个是新值
    • replace_copy:保留原序列不变,需要支持额外的第三个迭代器参数,指出保存后序列的位置

10.2.3 重排容器元素的算法

  • sort算法:重排输入序列中的元素,利用元素类型的<运算符来实现排序
  • unique算法:重排输入序列,将相邻的重复项消除,并返回一个指向不重复值范围末尾的迭代器
  • erase算法:删除无用元素

10.3 定制操作

  • 很多算法会比较输入序列的元素:默认情况下,这类算法使用元素类型的<或==运算符完成比较;

10.3.1 向算法传递函数

  • 按长度重排vector,将使用sort的重载版本,接收三个参数,此参数是一个谓词
    • 谓词:是一个可调用的表达式,其返回结果是一个能用作条件的值
      • 接收谓词参数的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型
    • 接收二元谓词的sort版本用谓词代替<来比较元素
  • stable_sort算法:保持等长元素间的字典序,重排输入序列中的元素
1
2
3
4
5
6
7
// 比较函数,用来按长度排序单词
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}

// 按长度由短至长排序
sort(words.begin(),words.end(),isShorter);

10.3.2 lambda表达式

  • 可以向一个算法传递任何类别的可调用对象
    • 对于一个对象或一个表达式,如果可以对其调用运算符,则称它为可调用的,e为一个可调用表达式,则可编写e(args),其中args是一个逗号分割的一个或多个参数列表
    • ==可调用对象:函数,函数指针,重载了函数调用运算符的类,lambda表达式
  • lambda表达式:表示一个可调用单元源代码,可理解为一个未命名的内联函数(具有返回类,参数列表和函数体),可定义在函数内部
    • lambda表达式的形式:[capture list](parameter list) -> return type{function body}
      • capture list是一个lambda所在函数中定义的局部变量的列表;
      • 可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
      • 忽略参数列表等价于一个空参数列表,若忽略返回类型,lambda根据函数体中的代码推断出返回类型;若函数体只是一个return语句,则返回类型从返回表达式的类型推断而来
  • 向lambda传递参数
    • 调用lambda时给定的实参会被用来初始化lambda的形参,但不能带默认参数(因此lambda调用的实参数目永远与形参数目相等)
    • 空捕获列表表示lambda不使用它所在函数中的任何局部变量
  • 使用捕获列表
    • 捕获列表指引lambda在其内部包含访问局部变量所需的信息
    • 一个lambda通过将局部变量包含在其捕获列表中指出将会使用这些变量
    • 一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量
    • 捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字
  • for_each算法
    • 使用for_each接收一个可调用对象,并将输入序列中每个元素调用此对象
1
2
3
4
5
6
7
8
stable_sort(words.begin(),words.end(),
[](const string &a, const string &b) {return a.size() < b.size();})

[sz](const string &a) {return a.size() >= sz;}

for_each(wc,words.end(),
[](const string &s) {cout << s << " ";})
cout << endl;

10.3.3 lambda捕获和返回

  • 当定义一个lambda时,编译器生成一个与lambda对应的新的类类型
    • 当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象(当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象)
    • 默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员
  • 值捕获
    • 被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝
  • 引用捕获
    • 一个以引用方式捕获的变量与其他任何类型的引用的行为类似
    • 在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象
    • 采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的
  • 建议:尽量保持lambda的变量捕获简单
    • 捕获一个普通变量,如int、string或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有所需的值就可以了
    • 如果捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值
  • 隐式捕获
    • 让编译器根据lambda体中的代码来推断要使用变量
    • 应在捕获列表中写一个&或=
      • &告诉编译器采用捕获引用方式
      • =表示采用值捕获方式
    • 混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=(指定了默认捕获方式为引用或值),且显式捕获和隐式捕获必须采用不同的方式
  • 可变lambda
    • 默认情况下,对于一个值被拷贝的变量,lambda不会改变其值,若希望能改变一个被捕获的变量的值,必须在参数列表首加上关键字mutable
      • 可变lambda能省略参数列表
    • 引用可以修改依赖于此引用指向的是一个const类型还是非const类型
  • 指定lambda返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void fcn1(){
size_t v1 = 42;
auto f = [v1] {return v1;};
v1 = 0;
auto j = f(); //j值为42
// 由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值
}

void fcn2(){
size_t v1 = 42;
auto f = [&v1] {return v1;};
v1 = 0;
auto j = f(); //j值为0
}

wc = find_if(words.begin(),words.end(),
[=](const string &s) {return s.size() >= sz;})

// 可变lambda
void fcn3(){
size_t v1 = 42;
auto f = [v1]() mutable {return ++v1;};
v1 = 0;
auto j = f(); //j值为43
}

transform(vi.begin(),vi.end(),vi.begin(),
[](int i) -> int
{if(i<0) return -i; else return i;});

10.3.4 参数绑定

  • 对于捕获局部变量的地方,建议使用lambda
  • 标准库bind函数
    • 定义在头文件functional中
    • 可将bind函数看作一个通用的函数适配器,接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表
    • 调用的一般形式: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为第二个参数,依此类推
  • 使用placeholders名字
    • 定义在头文件functional中
    • 名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中
    • 为了使用这些名字,两个命名空间都要写上
  • 用bind重排参数顺序
  • 绑定引用
    • 默认情况下,bind的不是占位符的参数被拷贝进bind返回的可调用对象中,但有时对有些绑定的参数希望以引用方式传递或时要绑定的参数的类型无法拷贝
    • ref和cref定义在functional中
    • ref函数:返回一个对象,包含给定的引用,此对象是可以拷贝的
    • cref函数:生成一个保存const引用的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
bool check_size(const string &s, string::size_type sz){
return s.size() >= sz;
}

// 将使用bind生成一个调用check_size的对象
auto check6 = bind(check_size,_1,6);
// 此bind调用只有一个占位符,表示check6只接受单一参数

string s = "hello";
bool bl = check6(s);

// 替换示例
auto wc = find_if(words.begin(),words.end()
[sz](const string &a));
// 替换
auto wc = find_if(words.begin(),words.end()
bind(check_size,_1,sz));
// 此bind调用生成一个可调用对象,将check_size的第二个参数绑定到sz的值
// 当find_if对words中的string调用这个对象时,这些对象会调用check_size,将给定的string和sz传递给它

// 使用_1
using std::placeholders::_1;
// 或声明全部placeholders
using namespace std::placeholders;

// 由短到长排序
sort(words.begin(),words.end(),isShorter);
// 由长到短排序
sort(words.begin(),words.end(),bind(isShorter,_2,_1));

ostream &print(ostream &os, const string &s, char c){
return os<<s<<c;
}
for_each(words.begin(),words.end(),
bind(print,ref(os),_1,' '));

10.4 再探迭代器

  • 迭代器的四种类型:
    • 插入迭代器:迭代器被绑定到一个容器上,可用来向容器插入元素
    • 流迭代器:迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流
    • 反向迭代器:迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器
    • 移动迭代器:专用的迭代器不是拷贝其中的元素,而是移动它们

10.4.1 插入迭代器

  • 插入迭代器是一种迭代器适配器,接受一个容器,生成一个迭代器,能实现向给定容器添加元素
    • 通过插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定尾置插入一个元素
  • 插入迭代器操作
    • it=t 在it指定的当前位置插入值t
    • *it,it,it
  • 插入器的三种类型,差异在于元素插入的位置
    • back_inserter 创建一个使用push_back的迭代器
    • front_inserter 创建一个使用push_front的迭代器
      • 当调用front_inserter©时,得到一个插入迭代器,接下来会调用push_front
      • 当每个元素被插入到容器c中时,它变为c的新的首元素
      • 因此,front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserter和back_inserter则不会
    • inserter 创建一个使用insert的迭代器(接受第二个参数,指向给定容器的迭代器,元素将被插入到给定迭代器表示的元素之前)
  • 只有在容器支持push_front的情况下,才可以使用front_inserter。类似的,只有在容器支持push_back的情况下,才能使用back_inserter
1
2
3
4
5
6
7
8
9
10
11
12
13
*it = val;
//等效代码
it = c.insert(it,val);
++it;

//front_insert
list<int>lst = {1,2,3,4};
list<int> lst2,lst3;

// 拷贝完成后lst2包含 4 3 2 1
copy(lst.cbegin(),lst.cend(),front_inserter(lst2));
// 拷贝完成后lst3包含 1 2 3 4
copy(lst.cbegin(),lst.cend(),insert(lst3,lst3.begin())

10.4.2 iostream迭代器

  • 标准库定义了用于IO类型对象的迭代器
  • istream_iterator读取输入流,ostream_iterator向一个输出流写数据
    • 迭代器将对应的流当作一个特定类型的元素序列来处理,通过使用流迭代器,可以用泛型算法从流对象读取数据以及向其写入数据
  • istream_iterator操作
    • 当创建一个流迭代器时,必须指定迭代器将要读写的对象类型
    • 一个istream_iterator使用>>读取流
    • 创建一个istream_iterator时,可以将它绑定到一个流
      • 还可以默认初始化迭代器,创建一个可以当作尾后值使用的迭代器
  • 使用算法操作流迭代器:由于算法使用迭代器操作处理数据,而流迭代器又至少支持某些迭代器操作,因此至少可以用某些算法来操作流迭代器
  • istream_iterator允许使用懒惰求值
    • 当讲istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据
    • 具体实现可以推迟从流中读取数据,直到使用迭代器时才真正读取(该操作办证第一次解引用迭代器之前,从流中读取数据的操作已经完成)
istream_iterator操作 -
istream_iterator<T> in(is) in从输入流is读取类型为T的值
istream_iterator<T> end 读取类型为T的值的istream_iterator迭代器,表示尾后位置
in1 == in2 in1和in2必须读取相同类型;如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等
*in 返回从流中的读取的值
in->men 返回从流中的读取的值
in,in 使用元素类型所定义的>>运算符从输入流中读取下一个值(前置版本返回一致指向递增后迭代器的引用,后置版本返回旧值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
istream_iterator<int> int_it(cin);  //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器
ifstream in("afile");
istream_iterator<string> str_in(in);

// 用istream_iterator从标准输入读取数据,存入vector
istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while(in_iter != eof)
vec.push_back(*in_iter++);

// 重写
istream_iterator<int> in_iter(cin),eof;
vector<int> vec(in_in_iter,eof);
// 用一对表示元素范围的迭代器来构造vec
// 两个迭代器是istream_iterator,这意味着元素范围是通过从关联的流中读取数据获得的
// 这个构造函数从cin中读取数据,直至遇到文件尾或者遇到一个不是int的数据为止
// 从流中读取的数据被用来构造vec

// 利用一对istream_iterator调用accumulate
istream_iterator<int> in(cin),eof;
cout<<accumulate(in,eof,0)<<endl;
  • ostream_iterator操作
    • 可以对任何具有输出运算符(<<)的类型定义ostream_iterator
    • 创建一个ostream_iterator时,可以提供第二参数,在输出每个元素后都会打印此字符串
ostream_iterator操作 -
ostream_iterator<T> out(os) out将类型为T的值写出到输出流os中
ostream_iterator<T> out(os,d) out将类型为T的值写出到输出流os中,每个值后面都输出一个d。d指向一个空字符结尾的字符数组
out = val 用<<运算符将val写入到out所绑定的ostream中
*out,out,out 不对out做任何事情。每个运算符都返回out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
*out_iter++ = e; //赋值语句实际上将元素写到cout
cout << endl;

// 运算符*和++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响
// 但是,推荐第一种形式。在这种写法中,流迭代器的使用与其他迭代器的使用保持一致
for(auto e:vec)
out_iter = e; //赋值语句实际上将元素写到cout
cout << endl;

// 可以调用copy
copy(vec.begin(),vec.end(),out_iter);

  • 使用流迭代器处理类类型
    • 可以为任何定义了输入运算符的类型创建istream_iterator,可以为任何定义了输出运算符的类型创建ostream_iterator

10.4.3 反向迭代器

  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器
    • 对于反向迭代器,递增,递减操作的含义会颠倒过来
      • 递增一个反向迭代器(++it)会移动到前一个元素
      • 递减一个迭代器(–it)会移动到下一个元素
    • 除了forward_list或一个流迭代器之外,其他容器都支持反向迭代器
      • 除了forward_list之外,标准容器上的其他迭代器都既支持递增运算又支持递减运算
      • 流迭代器不支持递减运算,因为不可能在一个流中反向移动
    • 可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器
      • 这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器
      • 与普通迭代器一样,反向迭代器也有const和非const版本

10.5 泛型算法结构

迭代器类别 -
输入迭代器 只读,不写;单遍扫描,只能递增
输出迭代器 只写,不读;单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写;多遍扫描,可递增递减
随机访问迭代器 可读写;多遍扫描,支持全部迭代器运算

10.5.1 5类迭代器

  • 输入迭代器:可以读取序列中的元素
    • 用于比较两个迭代器的相等和不相等运算符(==、!=)
    • 用于推进迭代器的前置和后置递增运算(++)
    • 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧
    • 箭头运算符(->),等价于(*it).member,即,解引用迭代器,并提取对象的成员
    • 输入迭代器只用于顺序访问
      • 对于一个输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的迭代器失效。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素
  • 输出迭代器:可以看作输入迭代器功能上的补集——只写而不读元素
    • 用于推进迭代器的前置和后置递增运算(++)
    • 解引用运算符(*),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)
  • 前向迭代器:可以读写元素。这类迭代器只能在序列中沿一个方向移动。前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此,我们可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。
  • 双向迭代器:可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(–)
  • 随机访问迭代器:提供在常量时间内访问序列中任意元素的能力
    • 用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=)
    • 迭代器和一个整数值的加减运算(+、+=、-和-=),计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置
    • 用于两个迭代器上的减法运算符(-),得到两个迭代器的距离
    • 下标运算符(iter[n]),与*(iter[n])等价

10.5.3 算法命名规范

  • 一些算法使用重载形式传递一个谓词
    • 接受谓词参数来代替<或==运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数
      • 函数的一个版本用元素类型的运算符来比较元素
      • 另一个版本接受一个额外谓词参数,来代替<或==
  • _if版本的算法
    • 接受一个元素值的算法通常有另一个不同名的(不是重载的)版本,该版本接受一个谓词代替元素值
    • 接受谓词参数的算法都有附加的_if前缀
  • 区分拷贝元素的版本和不拷贝的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
unique(beg,end);    //使用==运算符比较元素
unique(beg,end,comp); //使用comp比较元素

find(beg,end,val); //查找输入范围中val第一次出现的位置
find_if(beg,end,pred); //查找第一个令pred为真的元素
// 两个算法提供了命名上差异的版本,而非重载版本,因为两个版本的算法都接受相同数目的参数
// 因此可能产生重载歧义,虽然很罕见,但为了避免任何可能的歧义,标准库选择提供不同名字的版本而不是重载

// 从v1中删除奇数元素
remove_if(v1.begin(),v1.end()
[](int i) {return i%2;});
// 将偶数元素从v1拷贝到v2;v1不变
remove_if(v1.begin(),v1.end(),back_inserter(v2),
[](int i) {return i%2;});

10.6 特定容器算法

  • 对于list和forward_list,应该优先使用成员函数版本的算法而不是通用算法
  • 链表特有的操作会改变容器
    • 多数链表特有的算法都与其通用版本很相似,但不完全相同
    • 链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器
    • 类似的,merge和splice会销毁其参数
list和forward_list成员函数版本的算法(返回void) -
lst.merge(lst2) 将来自lst2的元素合并如lst
lst.merge(lst2,comp) 元素将从lst2中删除(在合并之后,lst2变成空)
lst.remove(val) 调用erase删除掉与给定值相等或令一元谓词为真的每个元素
lst.remove_if(pred) 调用erase删除掉与给定值相等或令一元谓词为真的每个元素
lst.reverse() 反转lst中元素的顺序
lst.sort() 使用<或给定比较操作排序元素
lst.sort(comp) 使用<或给定比较操作排序元素
lst.unique() 调用erase删除同一值得连续拷贝.第一个版本使用=,第二个版本使用给定的二元谓词
lst.unique(pred) 调用erase删除同一值得连续拷贝.第一个版本使用=,第二个版本使用给定的二元谓词

11 关联容器

  • 关联容器中的元素时按关键字来保存和访问的,支持高效的关键字查找和访问
  • 两个主要的关联容器:mapset
    • map中的元素是一些关键字-值对:关键字起到索引的作用,值则表示与索引相关联的数据
    • set中每个元素只包含一个关键字;set支持高效的关键字查询操作(检查一个给定关键字是否在set中)
  • 头文件
    • 类型map和multimap定义在头文件map中
    • set和multiset定义在头文件set中
    • 无序容器则定义在头文件unordered_map和unordered_set中
关联容器类型 -
按关键字有序保存元素 -
map 关联数组;保存关键字-值对
set 关键字即值
multimap 关键字可重复出现的map
multiset 关键字可重复出现的set
无序集合 -
unordered_map 用哈希函数组织的map
unordered_set 用哈希函数组织的set
unordered_multimap 哈希组织的map,关键字可重复出现
unordered_multiset 哈希组织的set,关键字可重复出现

11.1 使用关联容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用关联数组进行单词计数
// 统计每个单词在输入中出现的次数
map<string,size_t> word_count;
string word;
while(cin>>word)
++word_count[word];
for(const auto &w:word_count)
cout<<w.first<<" occurs "<<w.second<<((w.second >1)? "times" : " time")<<endl;

// 统计输入中每个单词出现的次数
// 可忽略部分单词
map<string,size_t> word_count;
set<string> exclude = {"The","But"};

string word;
while(cin>>word)
if(exclude.find(word) == exclude.end() )
++word_count[word];

11.2 关联容器概述

  • 关联容器支持普通容器操作,但不支持顺序容器的位置相关的操作
    • 关联容器中元素时根据关键字存储的,这些操作对关联容器没有意义,而且关联容器不支持构造函数或插入操作这些接受一个元素值和一个数量值得操作

11.2.1 定义关联容器

  • 每个关联容器都定义一个默认构造函数,创建了一个指定类型的空容器
    • 定义map,必须既指明关键字类型又指明值类型
      • 初始化时,必须提供关键字类型和值类型{key,value}
    • 定义set,只需指明关键字类型
      • 元素类型就是关键字类型
  • 初始化multimap和multiset
    • 允许多个元素具有相同的关键字
1
2
3
4
5
6
7
8
9
// 将创建一个名为ivec的保存int的vector,它包含20个元素:0到9每个整数有两个拷贝
// 将使用此vector初始化一个set和一个multiset
vector<int> ivec;
for(vector<int>::size_type i= 0;i!=10;++i){
ivec.push_back(i);
ivec.push_back(i);
}
set<int> iset(ivec.cbegin(), ivec.cend());
multiset<int> miset(ivec.cbegin,ivec.cend());

11.2.2 关键字类型的要求

  • 关键容器对其关键字类型有一定限制
    • 对于有序容器map,multimap,set,multiset,关键字类型必须定义元素比较的方法
    • 默认情况下,标准库使用关键字类型的<运算符比较两个关键字
  • 有序容器的关键字类型
    • 可以向算法提供自定义的比较操作,也可以提供自定义的操作代替关键字上的<运算符
    • 所提供的操作必须在关键字类型上定义一个严格弱序(可看作小于等于)
  • 使用关键字类型的比较函数
    • 用来组织一个容器中元素的操作的类型也是该容器类型的一部分
    • 为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型
    • 在尖括号中出现的每个类型,就仅仅是一个类型而已
      • 当创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合)
  • 为了使用自定义的操作,在定义multiset时必须提供两个类型:关键字类型,以及比较操作类型(函数指针类型)
1
2
3
4
5
6
7
// 严格弱序函数
bool compareIsbn(const Sales_data % lhs, const Sales_data &rhs){
return lhs.isbn() < rhs.isbn();
}

multiset<Sales_data,decltype(compareIsbn)*> bookstore(compareIsbn);
// 用compareIsbn来初始化bookstore对象,这表示当我们向bookstore添加元素时,通过调用compareIsbn来为这些元素排序

11.2.3 pair类型

  • pair的标准库类型,定义在头文件utility中
    • 一个pair保存两个数据成员(创建时需要提供两个类型名)
    • 与其他标准库类型不同,pair的数据成员是public的,两个成员分别命名为first和second
1
2
3
4
5
pair<string,string> anon;
pair<string,size_t> word_count;
pair<string,vector<int>> line;

pair<string,string> author{"James","Joyce"};
pair上的操作 -
pair<T1,T2> p; p是pair,对两个成员进行了值初始化
pair<T1,T2> p(v1,v2); p是pair,对两个成员v1,v2进行了值初始化
pair<T1,T2> p={v1,v2}; 同上
make_pair(v1,v2); 返回一个用v1和v2初始化的pair
p.first 返回p的first数据成员
p.second 返回p的second数据成员
p1 relop p2 关系运算符(relop为关系运算符)运算
p1 == p2 相等性判断
p1 != p2 相等性判断

11.3 关联容器操作

关联容器额外的类型别名 -
key_type 此容器类型的关键字类型
mapped_type 每个关键字关联的类型,只适用于map
value_type 对于set与key_type相同;对于map为pair<const key_type,mapped_type>

11.3.1 关联容器迭代器

  • 解引用一个关联容器迭代器时,得到一个类型为容器的value_type的值的引用
  • 一个map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字成员的值
  • set的迭代器是const的:虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素
  • 遍历关联容器:map和set类型都支持begin和end操作,可以用这些函数获取迭代器,然后用迭代器来遍历容器
  • 关联容器和算法
    • 通常不对关联容器使用泛型算法
      • 关键词是const意味着不能将关联容器传递给修改或重排容器元素的算法
    • 关联容器只可用于只读取元素的算法
1
2
3
4
5
6
7
8
9
10
auto map_it = word_cout.begin();
map_it->first = "new key"; //错误,关键字是const
++map_it->second; //可通过迭代器改变元素

set<int> iset = {0,1,2,3,4,5,6,7,8,9};
set<int>::iterator set_it = iset.begin();
if(set_it != iset.end()){
*set_it = 42; //错误,set中关键字只读
cout << *set_it <<endl; //正确,可以读关键字
}

11.3.2 添加元素

  • 关联容器的insert成员向容器中添加一个元素或元素范围
    • insert有两个版本,分别接受一对迭代器,或是一个初始化器列表
  • 向map添加元素
    • 元素类型是pair,可以在insert的参数列表中创建一个pair
关联容器insert操作 -
c.insert(v) v是value_type类型的对象
c.emplace(args) 构造一个元素
c.insert(b,e) b,e是迭代器,表示一个c::value_type类型值的范围
c.insert(il) il是花括号列表
c.insert(p,v) 将迭代器p作为指示从哪里开始搜索新元素应存储的位置
c.insert(p,args) 将迭代器p作为指示从哪里开始搜索新元素应存储的位置
  • 检测insert的返回值
    • 对于map和set,只有当元素的关键字不在c中才插入元素,函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值
      • 若关键字已在容器中,bool部分为false
    • 对于multimap和multiset,总会插入给定元素,并返回一个指向新元素的迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 统计每个单词在输入中出现次数的繁琐方法
map<string,size_t> word_count;
string sord;
while(cin>>word){
auto ret = word_cout.insert({word,1});
if(!ref.second)
++ret.first->second;
}

//ret 保存insert返回的值,是一个pair
// ret.first是pair的第一个成员,是一个map迭代器,指向具有给定关键字的元素
// ret.first-> 解引用此迭代器,提取map中的元素,元素也是一个pair
// ret.first->second map中元素的值部分
// ++ret.first->second 递增此值

// ret的实际类型
pair<map<string,size_t>::iterator,bool>

11.3.3 删除元素

从关联容器删除元素 -
c.erase(k) 从c中删除每个关键字为k的元素.返回一个size_type值(指出删除元素的数量)
c.erase(p) 从c中删除迭代器p指定的元素.p必须指向c中的一个真实元素。返回一个指向p之后元素的迭代器
c.erase(b,e) 删除迭代器对b和e所表示的范围中的元素,返回e

11.3.4 map的下标操作

  • map和unordered_map容器提供了下标运算符和一个对应的at函数
    • 不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联
    • map下标运算符接受一个索引,获取与此关键字相关联的值
    • 但如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化
  • set类型不支持下标,因为set中没有与关键字相关联的“值”
map和unordered_map的下标操作 -
c[k] 返回关键字为k的元素;若k不在c中,添加元素并进行初始化
c.at(k) 访问关键字为k的元素,带参数检查,若不存在,则抛出一个out_of_range异常
  • 当对一个map进行下标操作时,会获得一个mapped_type对象
  • 当解引用一个map迭代器时,会得到一个value_type对象

11.3.5 访问元素

  • 关心一个特定元素是否已在容器中,find函数是最佳选择
    • 对于允许重复关键字的容器,count还会统计有多少个元素有相同的关键字
  • 对map使用find代替下标操作
  • 在multimap或multiset中查找元素:如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储
  • 不同的面向迭代器查找元素
    • 用lower_bound和upper_bound来解决此问题
    • 两个操作都接受一个关键字,返回一个迭代器
      • 如果关键字在容器中,lower_bound返回的迭代器将指向第一个具有给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置
      • 如果元素不在multimap中,则lower_bound和upper_bound会返回相等的迭代器
      • 如果lower_bound和upper_bound返回相同的迭代器,则给定关键字不在容器中
  • equal_range函数
    • 函数接受一个关键字,返回一个迭代器pair
    • 若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置
    • 若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置
在一个关联容器中查找元素的操作 -
- lower_bound和upper_bound不适用于无序容器
- 下标和at操作只适用于非const的map和unordered_map
c.find(k) 返回一个迭代器,指向第一个关键字为k的元素;若无k,则返回尾后迭代器
c.count(k) 返回关键字等于k的数量
c.lower_bound(k) 返回一个迭代器,指向第一个关键字不小于k的元素
c.upper_bound(k) 返回一个迭代器,指向第一个关键字大于k的元素
c.equal_range(k) 返回一个迭代器pair,表示关键字等于k的元素的范围;若不存在,两个成员为c.end()

11.4 无序容器

  • 新标准定义了4个无序关联容器,使用哈希函数和关键字类型的==运算符
  • 使用无序容器
    • 除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find,insert等)
    • 用于map和set的操作也能用于unordered_map和unordered_set
    • 通常可用一个无序容器替换对应的有序容器
1
2
3
4
5
6
7
// 用unordered_map重写单词计数程序
unordered_map<string,size_t> word_count;
string word;
while(cin>>word)
++word_count[word];
for(const auto &w:word_count)
cout<<w.first<<" occurs " << w.second << ((w.second >1)? " times":" time")<<endl;
  • 管理桶
    • 无序容器在存储上组织为一组桶(使用一个哈希函数将元素映射到桶),每个桶保存零个或多个元素
    • 为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶,容器将具有一个特定哈希值的所有元素都保存在相同的桶中,如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中
    • 因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小
    • 对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的
无序容器管理操作 -
桶接口 -
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.reserve(n) 重组存储,使得c可以保存n个元素且不必rehash
  • 无序容器对关键字类型的要求
    • 默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型的对象来生成每个元素的哈希值
    • 标准库为内置类型(包括指针)提供了hash模板,还为一些标准库类型,包括string和智能指针类型定义了hash
    • 因此,可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针类型的无序容器
1
2
3
4
5
6
7
8
9
10
11
12
13
size_t hasher(const Sales_data &sd){
return hash<string>() (sd.isbn());
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs){
return lhs.isbn() == rhs.isbn();
}

// 我们的hasher函数使用一个标准库hash类型对象来计算ISBN成员的哈希值,该hash类型建立在string类型之上
// 类似的,eqOp函数通过比较ISBN号来比较两个Sales_data

using SD_multiset = unordered_multiset<Sales_data,dacltype(hasher)*,decltype(eqOp)*>;
// 参数使桶大小,哈希函数指针和相等性判断运算符指针
SD_multiset bookstore(42,hasher,eqOp);

12 动态内存

  • 目前为止只使用过静态内存或栈内存(分配在静态或栈内存中的对象由编译器自动创建和销毁)
    • 静态内存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量
    • 栈内存用来保存定义在函数内的非static对象
      • 对于栈对象,仅在其定义的程序块运行时才存在
      • static对象在使用之前分配,在程序结束时销毁
  • 除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆
    • 程序用堆来存储动态分配的对象

12.1 动态内存与智能指针

  • 在C++中,动态内存使通过一对运算符来完成的:
    • new,在动态内存中位对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化
    • delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存
  • 在新标准中,提供两种智能指针来管理动态对象,与普通指针的区别是可以自动释放所指向的对象
    • memory头文件
    • shared_ptr:允许多个对象指向同一个对象
    • unique_ptr:独占所指向的对象
    • weak_ptr:伴随类,弱引用,指向shared_ptr所管理的对象

12.1.1 shared_ptr类

  • 智能指针是模板,创建时必须提供额外的信息(指针可以指向的类型,在<>内给出类型)
    • 默认初始化的智能指针中保存着一个空指针
    • 解引用一个智能指针返回它所指向的对象
1
2
3
4
5
shared_ptr<string> p1;
shared_ptr<list<int>> p2;

if(p1 && p1->empty())
*p1 = "hi";
shared_ptr和unique_ptr都支持的操作 -
shared_ptr<T> sp 空智能指针,可以指向类型为T的对象
unique_ptr<T> up 空智能指针,可以指向类型为T的对象
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它所指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针
swap(p,q) 交换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中的指针必须能转换成T*
p=q 所保存的指针必须能相互转换;此操作会递减p的引用计数,递增q的引用计数
p.unique() 若p.use_count()为1,返回true
p.use_count() 返回与p共享对象的智能指针数量(用于调试)
  • make_shared函数
    • 最安全的分配和使用动态内存的方法:调用make_shared函数
      • 此函数在动态内存中分配一个对象并初始化,返回指向此对象的shared_ptr
    • 类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象
    • 可以使用auto定义一个对象保存make_shared的结果
  • shared_ptr的拷贝和赋值
    • 当进行拷贝或赋值操作时,每个shared_ptr会记录有多少个其他shared_ptr指向相同的对象
    • 每个shared_ptr都有一个关联的计数器,通常称其为引用计数,拷贝shared_ptr,计数器都会递增
1
2
3
4
5
6
7
8
9
10
11
12
13
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p3 = make_shared<string>(10,'9');

auto p6 = make_shared<vector<string>>();

auto p = make_shared<int>(42);
auto q(p); //p和q指向相同对象,此对象有两个引用者

auto r = make_shared<int>(42);
r = q;
//递增r所指向的对象的引用计数;
// 递减r原来指向对象的引用计数;
// r原来指向的对象已经没有引用者,会自动释放
  • shared_ptr自动销毁所管理的对象以及相关联的内存
    • 当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象
    • 通过析构函数完成销毁工作,析构函数一般用来释放对象所分配的资源
      • shared_ptr的析构函数会递减所指向的对象的引用计数
    • 当动态对象不再被使用时,shared_ptr类会自动释放动态对象
    • 由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留非常重要
      • 如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> facory(T arg){
return make_shared<Foo>(arg);
}

void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
}//p离开作用域,所指向的内存会被自动释放掉

void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
// 使用p
return p; //当返回p时,引用计数进行了递增操作
}//p离开了作用域,但它指向的内存不会被释放掉
  • 使用了动态生存期的资源的类
    • 程序使用动态内存出现三种原因之一:
      1. 程序不知道自己需要使用多少对象
      2. 程序不知道所需对象的准确类型
      3. 程序需要在多个对象间共享数据
        • 一般而言,如果两个对象共享底层数据,当某个对象被销毁,不能单方面地销毁底层数据
    • 使用动态内存的一个常见原因是允许多个对象共享相同的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const {return data->size();}
bool empty() const {return data->empty();}

void push_back(const std::string &t) {data->push_back(t);}
void pop_back();
std::string& front();
std::string& back();
private:
std::shared_ptr<str::vector<std::string>> data;
void check(size_type i, const std::string &msg) const;
};

StrBlob::StrBlob(): data(make_shared<vector<string>>) {}
strBlob::StrBolb(initializer_list<string> il):
data(make_shared<vector<string>>()) {}

void StrBlob::check(size_type i, const string &msg) const{
if(i >= data->size())
throw out_of_range(msg);
}

string& StrBlob::front(){
check(0,"front on empty StrBlob");
return data->front();
}

string& StrBlob::back(){
check(0,"back on empty StrBlob");
return data->back();
}

string& StrBlob::pop_bakc(){
check(0,"pop_back on empty StrBlob");
return data->pop_back();
}

// 类似Sales_data类,StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作
// 默认情况下,这些操作拷贝、赋值和销毁类的数据成员
// StrBlob类只有一个数据成员,它是shared_ptr类型
// 因此,当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁

12.1.2 直接管理内存

  • 运算符new分配内存,delete释放nre分配的内存
  • 使用new动态分配和初始化对象
    • 在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针
    • 默认情况下,动态分配的对象是默认初始化的,意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将用默认构造进行初始化
      • 可以使用直接初始化方式来初始化动态分配的对象,或使用列表初始化(使用花括号)
      • 可以对动态分配的对象进行值初始化(在类型名之后跟一对空括号)
        • 如果使用括号包围的仅有单一初始化器,可以使用auto推断元素类型
  • 动态分配的const对象
    • 动态分配的const对象必须进行初始化(定义了默认构造函数的类类型,可以隐式初始化)
  • 内存耗尽
    • 一旦一个程序用光了所有的可用内存,new表达式会报错,抛出bad_alloc异常
    • 定位new:允许向new传递额外的参数,(传递nothrow意图不能抛出异常,返回空指针)
      • nothrow和bad_alloc定义在new头文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int *pi = new int;
string *ps = new string;

int *pi = new int(1024);
string *ps = new string(10,'9');
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

string *ps = new string(); //值初始化为空

auto p1 = new auto(obj);
auto p2 = new auto{a,b,c}; //错误,括号中只能由单个初始化器

const int *pci = new const int(1024)
const string *pcs = new const string;

int *p1 = new int;
int *p2 = new (nothrow) int; //分配失败,new返回空指针
  • 释放动态内存
    • 使用delete表达式来讲动态内存归还给系统
      • 两个动作:销毁给定指针指向的对象,释放对应的内存
    • delete表达式接受一个指针,指向要释放的对象
  • 指针和delete
    • delete必须指向动态分配的内存或空指针(释放一块非new分配的内存,该行为未定义)
    • const对象的值不能被修改,但本身是可以被销毁的
  • 动态对象的生存期直到被释放时为止
    • 由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放
    • 通过内置指针类型来管理的内存,直到被显式释放之前它都是存在的
      • 返回动态内存的指针的函数需要记得释放内存
      • 当一个指针离开其作用域时,它所指向的对象什么都不会发生;如果这个指针指向的时动态内存,内存将不会被自动释放
  • 小心:动态内存的管理非常容易出错
    • 使用new和delete管理动态内存存在三个常见问题:
      1. 忘记delete内存
      2. 使用已经释放掉的对象
      3. 同一块内存释放两次
  • delete之后重置指针值
    • delete一个指针后,指针为空悬指针
      • 不再使用该指针:在指针即将离开其作用域之前释放它所关联的内存
      • 需要使用该指针:将指针赋值为nullptr,指明指针不再指向任何对象
    • 多个指针指向相同内存,delete一个指针后,另一个指针需要置为nullptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
delete p;

// 释放const对象
const int *pci = new const int(1024);
delete pci;

// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg){
return new Foo(arg); //调用者负责释放此内存
}

void use_factory(T arg){
Foo *p = factory(arg);
//使用p但不delete
} //p离开作用域,但所指向的内存没有释放

// 记得释放内存
void use_factory(T arg){
Foo *p = factory(arg);
//使用p
delete p;
}

int *p(new int(42));
auto q = p;
delete p;
p = nullptr;

12.1.3 shared_ptr和new结合使用

  • 接受指针参数的智能指针构造函数是explicit的
    • 不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
    • 不能进行内置指针到智能指针间的隐式转换
  • 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象
    • 可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可以用new返回的指针来初始化智能指针
shared_ptr<double> p1;
shared_ptr<int> p2(new int(42));

// 不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1 = new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); //正确:使用直接初始化形式

shared_ptr<int> clone(int p){
return new int(p); //错误,存在隐式转换
}

shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p));
}
定义和改变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会释放此对象
p.reset(q) 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空
p.reset(q,d) 若传递了参数d,将会调用d而不是delete来释放q
  • 不要混合使用普通指针和智能指针
    • shared_ptr可以协调对象的析构,但仅限于其自身的拷贝之间(推荐make_shared而不是new的原因)
    • 在分配对象的同时就将shared_ptr与之绑定,从而避免无意中将同一块内存绑定到多个独立创建的shared_ptr上
    • 将一个shared_ptr绑定到一个普通指针时,就将内存的管理责任交给了这个shared_ptr
      • 一旦这样做了,就不应该再使用内置指针来访问shared_ptr所指向的内存了
    • 使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为无法知道对象何时会被销毁
  • 不要使用get初始化另一个智能指针或为智能指针赋值
    • 智能指针类型定义了一个get函数,返回一个内置指针,指向智能指针管理的对象
      • 使用场景:需要向不支持使用智能指针的代码传递指针
    • 使用get返回的指针不能delete
  • 用reset将新的指针赋予一个shared_ptr:
    • 与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象
    • reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 在函数被调用时ptr被创建并初始化
void process(shared_ptr<int> ptr){
//使用ptr
} //ptr离开作用域,被销毁

// process的参数是传值方式传递
// 拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2
// 当process结束时,ptr的引用计数会递减,但不会变为0
// 因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放
shared_ptr<int> p(new int(42)); //引用计数为1
process(p); //拷贝p会递增它的引用计数;在process中引用计数值为2
int i = *p; //正确;引用计数值为1
// 虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的

// 虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的
int *x(new int(1024));
process(x); //错误:不能将int*转换为一个shared_ptr<int>
process(shared_ptr<int>(x)); //合法的,但内存会被释放
int j = *x; //未定义的:x是一个空悬指针
// 将一个临时shared_ptr传递给process
// 当这个调用所在的表达式结束时,这个临时对象就被销毁了
// 但x继续指向(已经释放的)内存,从而变成一个空悬指针

// 将另一个智能指针绑定到get返回的指针上是错误的
shared_ptr<int> p(new int(42));
int *q = p.get();
{
// 新程序块
shared_ptr<int>(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义:p指向的内存已被释放

p = new int(1024); //错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024)); //正确:p指向一个新对象

// 在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝
if(!p.unique())
p.reset(new string(*p));
*p += newVal;

12.1.4 智能指针和异常

  • 可以使用智能指针确保资源被释放
    • 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放
  • 使用自定义的释放操作
    • 删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作
  • 正确使用智能指针,必须坚持一些基本规范:
    • 不使用相同的内置指针值初始化(或reset)多个智能指针
    • 不delete get()返回的指针
    • 不使用get()初始化或reset另一个智能指针
    • 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,指针就变为无效了
    • 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 网络库代码(示例)
struct destination;
struct connection;
connection connect(destination*);
void disconnect(connect);
void f(destination &d){
//获得一个连接
connection c = connect(&d);
// 使用连接
// 如果在f推出之 前忘记调用disconnect,就无法关闭c
}

// 释放操作,删除器
void end_connection(connection *p) {disconnect(*p);}

void f(destination &d){
//获得一个连接
connection c = connect(&d);
shared_ptr<connection> p(&c,end_connection)
// 使用连接
// 如果在f推出之 前忘记调用disconnect,就无法关闭c
}

12.1.5 unique_ptr

  • 一个unique_ptr拥有它所指向的对象
    • 与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象
    • 当unique_ptr被销毁时,它所指向的对象也被销毁
    • 当定义一个unique_ptr时,需要将其绑定到一个new返回的指针上
      • 类似shared_ptr,初始化unique_ptr必须采用直接初始化形式
    • unique_ptr不支持普通的拷贝或赋值操作
  • 可调用release或reset将指针的所有权转移
    • release成员返回unique_ptr当前保存的指针并将其置为空
      • release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值
      • 调用release会切断unique_ptr和它原来管理的对象间的联系
    • reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针
  • 传递unique_ptr参数和返回unique_ptr
    • 可以拷贝或赋值一个将要被销毁的unique_ptr
    • auto_ptr(标准库的较早版本,具有unique_ptr的部分特性,但不是全部)
      • 不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr
      • 虽然auto_ptr仍是标准库的一部分,但编写程序时应该使用unique_ptr
  • 向unique_ptr传递删除器
    • 重载一个unique_ptr中默认的删除器
    • 必须在尖括号中unique_ptr指向类型之后提供删除器类型
    • 在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)
unique_ptr操作 -
unique_ptr<T> u1 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针
unique_ptr<T,D> u2 u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T,D> u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u=nullptr 释放u指向的对象,将u置为空
u.release() u放弃对指针的控制权,返回指针,并将u置为空
u.reset() 释放u指向的对象
u.reset(q) 如果提供了内置指针q,令u指向这个对象
u.reset(nullptr) 如果提供了内置指针q,令u指向这个对象
1
2
3
4
5
6
7
8
9
10
11
12
unique_ptr<int> p2(new int(42));

unique_ptr<string> p1(new string("Stegosaurus"));

// 从函数返回一个unique_ptr
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}

// p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
// 调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn)

12.1.6 weak_ptr

  • weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象
    • 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
    • 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放(即使有weak_ptr指向对象)
  • 由于对象可能不存在,不能使用weak_ptr直接访问对象,而必须调用lock
    • 此函数检查weak_ptr指向的对象是否仍存在
    • 如果存在,lock返回一个指向共享对象的shared_ptr
weak_ptr操作 -
weak_ptr<T> w 空weak_ptr,可以指向类型为T的对象
weak_ptr<T> w(sp) 与shared_ptr sp指向相同对象的weak_ptr
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
w.lock() 若expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

12.2 动态数组

  • C++语言和标准库提供了两种一次分配一个对象数组的方法
  • C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组
  • 标准库中包含一个名为allocator的类,允许我们将分配和初始化分离

12.2.1 new和数组

  • 为了new分配一个对象数组,需要在类型名之后跟一对方括号,在其中指明要分配的对象的数目
  • new分配一个数组时,得到的是一个数组元素类型的指针
    • 分配的内存不是一个数组类型,不能对动态数组调用begin和end,不能用范围for语句
  • 初始化动态分配对象的数组
    • 可以对数组中的元素进行值初始化,在大小后跟着一对空括号
      • 不能在括号中给出初始化器,意味着不能用auto分配数组
    • 可以使用元素初始化器的花括号列表
  • 使用new[0]时:动态数组正确,对于零长度的数组来说,此指针就像尾后指针一样,但指针不能解引用
  • 释放动态数组
    • 特殊形式的delete:在指针前加一个空方括号
    • 如果在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告。程序可能在执行过程中在没有任何警告的情况下行为异常
  • 智能指针和动态数组
    • 可以管理new分配的数组的unique_ptr版本,必须在对象类型后面跟一对空方括号
      • 当一个unique_ptr指向一个数组时,不能使用点和箭头成员运算符(毕竟unique_ptr指向的是一个数组而不是单个对象),可以使用下标访问成员
    • 使用shared_ptr管理一个动态数组,必须提供自定义的删除器
      • shared_ptr不直接支持动态数组管理这一特性会影响访问数组中的元素
        • 为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素
指向数组的unique_ptr -
unique_ptr<T[]> u u可以指向一个动态分配的数组
unique_ptr<T[]> u(p) u可以指向内置指针p所指向的动态分配的数组;p必须能转换成类型T*
u[i] 返回u拥有的数组中位置i处的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int *pia = new int[get_size()];

int *pia = new int[10](); //10个值初始化为0的int
int *pia1 = new int[10]{0,1,2,3,4,5,6,7,8,9};

delete [] pa;

unique_ptr<int[]> up(new int[10]);
up.release(); //自动用delete[]销毁其指针

//为了使用shared_ptr,必须提供删除器
shared_ptr<int> sp(new int[10],[](int *p) {delete[] p;});
sp.reset(); //使用提供的lambda释放数组

// 使用get获取一个内置指针
for(size_t i = 0; i!=10;++i){
*(sp.get()+i) = i;
}

12.2.2 allocator类

  • new灵活性上的限制:希望将内存分配和对象初始化组合在一起
  • allocator类
    • 在头文件memory中
    • 帮助将内存分配和对象构造分离开,提供类型感知的内存分配方法,分配的内存时原始的,未构造的
    • 必须指明allocator可以分配的对象类型(当一个allocator对象分配内存时,会根据给定的对象类型来确定恰当的内存大小和对齐位置)
allocator类及其算法 -
allocator<T[]> a 定义了一个名为a的allocator对象,可为类型T的对象分配内存
a.allocator(n) 分配一段原始的,未构造的内存,保存n个类型为T的对象
a.deallocator(p,n) 释放从T*指针p中地址开始的内存;p必须是一个先前有allocate返回的指针,且n必须是p创建时所要求的大小;在调用之前必须对这块内存中的对象调用destory
a.construct(p,args) p必须时一个类型为T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destroy(p) 对p指向的对象执行析构函数
  • allocator分配未构造的内存
    • construct成员函数接受一个指针和额外参数,初始化构造对象(额外参数必须时与构造的对象的类型相匹配的合法初始化器)
    • 一旦元素被销毁后,就可以重新使用这部分内存来保存其他对象,也可以将其归还给系统
      • 释放内存通过调用deallocate来完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
allocator<string> alloc;  //可以分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string

auto q = p; //q指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++,10,'c'); //*q为cccccccccc
alloc.construct(q++,"hi"); //*q为hi

// 销毁对象
while(q != p)
alloc.destroy(--q); //释放真正构造的string

// 释放内存
alloc.deallocate(p,n);
allocator算法 -
uninitialized_copy(b,e,b2) 从迭代器b和e指出的输入范围中拷贝元素到迭代器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个对象

13 拷贝控制

  • 一个类通过定义5种特殊的成员函数来控制显式地或隐式地指定在此类型地的对象拷贝,移动或销毁时做什么
    • 拷贝构造函数
    • 拷贝赋值运算符
    • 移动构造函数
    • 移动赋值运算符
    • 析构函数
  • 拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么
  • 拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么
  • 析构函数定义了当此类型对象销毁时做什么

13.1 拷贝,赋值与销毁

13.1.1 拷贝构造函数

  • 拷贝构造函数:构造函数的第一个参数是自身的引用,且任何额外参数都有默认值
    • 拷贝构造函数通常不应该是explicit的
  • 合成拷贝构造函数
    • 即使定义了其他构造函数,编译器也会合成一个拷贝构造函数
      • 对某些类,合成拷贝构造函数用来阻止拷贝该类类型的对象
      • 一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中(拷贝每个非static成员)
        • 对于类类型,会使用其拷贝构造函数拷贝
        • 内置类型的成员则直接拷贝
  • 拷贝初始化
    • 使用直接初始化,实际上是要求编译器使用普通的函数匹配来选择与提供的参数最匹配的构造函数
    • 使用拷贝初始化,要求编译器将右侧运算对象拷贝到正在创建的对象中(必要时还要进行类型转换)
    • 如果一个类由一个移动构造函数,则拷贝初始化有时候会使用移动构造函数而非拷贝构造函数完成
      • 拷贝初始化在用=定义变量时会发生
      • 将一个对象作为实参传递给一个非引用类型的形参会发生
      • 从一个返回类型为非引用类型的函数返回一个对象会发生
      • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员会发生
  • 参数和返回值
    • 在函数调用过程中,具体非引用类型的参数要进行拷贝初始化
      • 当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果
    • 拷贝构造函数用来初始化非引用类类型参数(若其参数不是引用类型则调用不成功)
  • 拷贝初始化的限制:若初始化过程需要通过一个explicit的构造函数来进行类型转换,使用拷贝初始化还是直接初始化就都可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Sales_data{
public:
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
}

Sales_data::Sales_data(const Sakes_data& orig):
bookNo(orig.bookNo),units_sold(orig.units_sold),revenue(orig.revenue)
{}

string dots(10,' '); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string null_book = "9-999-99999-9"; //拷贝初始化
string nines = string(100,'9'); //拷贝初始化

vector<int> v1(10);
vector<int> v2 = 10; //错误,接收大小参数的构造函数是explicit

13.1.2 拷贝赋值运算符

  • 类控制其对象如何让赋值
    • 类未定义拷贝赋值运算符时,编译器会合成拷贝赋值运算符
  • 重载赋值运算符
    • 重载运算符本质上时函数,其名字由operator关键字后接表示要定义的运算符的符号组成
      • 重载赋值运算符就是operator=的函数
    • 重载运算符的参数必须表示运算符的运算对象,某些运算符必须定义为成员函数
      • 若运算符时一个成员函数,其左侧运算符对象就绑定到隐式的this参数
    • 为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用
  • 合成拷贝赋值运算符
    • 类似拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值
    • 拷贝赋值运算符会将右侧运算对象的每个非static成员赋给左侧运算对象对象的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 拷贝赋值运算符
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
}

// 等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sakes_data& orig)
{
bookNo = orig.bookNo;
units_sold = orig.units_sold;
revenue = orig.revenue;
return *this;
}

13.1.3 析构函数

  • 析构函数释放对象使用的资源,并销毁对象的非static数据成员
    • 析构函数是类的成员函数,名字由波浪号接类名构成,没有返回值也不接受参数
  • 析构函数,首先执行函数体,然后销毁成员,成员按初始化顺序销毁使用
    • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象
    • 智能指针成员在析构时会被自动销毁
    • 当一个对象的引用或指针离开作用域时,析构函数不会执行
  • 合成析构函数:合成函数用来阻止该类型的对象被销毁,如果不是这种情况,合成析构函数的函数体为空

13.1.4 三/五法则

  • 三个基本操作可以控制类的拷贝操作:拷贝构造函数,拷贝赋值运算符,析构函数
    • 新标准中,可以定义移动构造函数和移动赋值运算符
  • 需要析构函数的类也需要拷贝和赋值操作
  • 需要拷贝构造函数的类也需要拷贝赋值运算符,反之亦然

13.1.5 使用=default

  • 可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成地版本
    • 在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的
    • 只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)
    • 如果不希望合成的成员是内联函数,应该只对成员的类外定义使用=default
1
2
3
4
5
6
7
8
9
class Sales_data{
public:
// 拷贝控制成员;使用default
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data&);
~Sales_data() = default;
}
Sales_data& Sales_data::operator=(const Sales_data&) = default;

13.1.6 阻止拷贝

  • 对于拷贝构造函数和拷贝赋值运算符没有意义地类,定义时必须采用某种机制阻止拷贝或赋值
    • 新标准下,可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝
    • 删除的函数:虽然声明了,但不能以任何方式使用,在函数的参数列表后面加上=delete来指明希望定义为删除的
      • =delete必须出现在函数第一次声明时
  • 析构函数不能是删除的函数(避免无法销毁此类型的对象)
    • 删除了析构函数的类型编译器将不允许定义该类型的变量或创建该类的临时对象
  • 合成的拷贝控制成员可能是删除的
    • 如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的,编译器将这些合成的成员定义为删除的函数
    • 一个成员由删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的
    • 对于具有引用成员或无法默认构造的const成员的类,编译器不会为其合成默认构造函数
      • 如果一个类有const成员,则不能使用合成的拷贝赋值运算符
      • 对于有引用成员的类,合成拷贝赋值运算符被定义为删除的
        • 将一个新值赋予一个引用成员,为这样的类合成拷贝赋值运算符,则赋值后,左侧运算对象仍然指向赋值前一样的对象,而不会与右侧运算对象指向相同的对象
  • private拷贝控制
    • 在新标准之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private阻止拷贝
    • 现在不推荐使用
1
2
3
4
5
6
7
8
9
10
11
12
13
struct NoCopy{
NoCopy() = default();
NoCopy(const Nocopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete; //阻止赋值
}

struct NoDtor{
NoDtor() = default;
~NoDtor() = delete;
}

NoDtor *p = new NoDtor(); //正确,但不能delete p
delete p; //错误,析构函数是删除的

13.2 拷贝控制和资源管理

  • 管理类外资源的类必须定义拷贝控制成员
    • 这种类需要通过析构函数来释放对象所分配的资源(一个类需要析构函数必须肯定需要拷贝构造函数和拷贝赋值运算符)
    • 为了定义这些成员,首先必须确定此类型对象的拷贝语义,有两种选择:定义拷贝操作,使类的行为看起来像一个值或像一个指针
      • 类的行为像一个值,意味着应该有自己的状态;拷贝一个像值的对象时,副本和原对象使完全独立的,改变副本不会对原对象产生任何影响
      • 行为像指针的类则共享状态;拷贝一个类使,副本和原对象使用相同的底层数据;改变副本会改变原对象,反之亦然

13.2.1 行为像值的类

  • 为了提供类值得行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝
  • 类值拷贝赋值运算符
    • 赋值运算符通常组合了析构函数和构造函数的操作
      • 赋值运算符会从右侧运算对象拷贝数据
      • 赋值运算符会销毁左侧运算对象的资源
    • 即使是将一个对象赋予它自身,也要能正确工作,一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 意味着对于ps指向string,每个HasPtr对象都必须有自己的拷贝
// 为了实现类值,需要定义一个拷贝构造函数,析构函数,拷贝赋值运算符
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0){}
//对ps指向的string,每个HasPtr对象都有自己的拷贝
HasPtr(const HasPtr &p):
ps(new std::string(*p.ps)),i(p.i){}
HasPtr& operator=(const HasPtr&);
~HasPtr(){delete ps;}
private:
std::string *ps;
int i;
}

// 本例中,通过先拷贝右侧运算对象,可以处理自赋值情况,并且能保证在异常发生时代码安全
// 完成拷贝后,释放左侧运算对象的资源,并更新指针指向新分配的string
HasPtr& HasPtr::operator=(const HasPtr& rhs){
auto newp = new string(*rhs.ps);
delete ps;

ps = newp;
i = rhs.i;
return *this;
}

13.2.2 定义行为像指针的类

  • 对于行为类似指针的类,需要定义拷贝构造函数和拷贝赋值运算符
    • 类展现类似指针的行为的最好办法是使用shared_ptr来管理类中的资源
      • 拷贝(或赋值)一个shared_ptr会拷贝(赋值)shared_ptr所指向的指针,shared_ptr类自己记录共享所指向对象的数量;没有用户使用对象时,shared_ptr类负责释放资源
    • 直接管理资源,使用引用计数
      • 引用计数的工作方式
        • 除了初始化对象外,每个构造函数要创建一个引用计数,用来记录有多少个对象与正在创建的对象共享状态
        • 构造函数不分配新的计数器,而是拷贝给定对象的数据成员
        • 析构函数递减计数器,指出共享状态的用户减少一个
        • 拷贝赋值运算符递减右侧运算对象的计数器,递减左侧运算对象的计数器
    • 在动态内存中保存计数器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 定义一个使用引用计数的类
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0),use(new std::size_t(1)) {}
HasPtr(const HasPtr &p):
ps(p.ps),i(p.i),use(p.use) {++*use;}
HasPtr& operator=(const HasPtr&);
~HasPtr();
private:
std::string *ps;
int i;
std::size_t *use; //用来记录有多少个对象共享*ps的成员
}

HasPtr::~HasPtr(){
if(--*use == 0){
delete ps;
delete use;
}
}

HasPtr& HasPtr::operator=(const HasPtr& rhs){
++*rhs.use;
if(--*use == 0){
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}

13.3 交换操作

  • 除了定义拷贝控制成员,管理资源的类还定义了一个swap的函数
    • swap函数应该调用swap,而不是std::swap
  • 在赋值运算符中使用swap
    • 定义swap的类通常用swap来定义赋值运算符,使用拷贝并交换的技术
    • 使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class HasPtr{
// 将swap定义为friend,以便能访问HasPtr的(private的)数据成员
// 由于swap的存在就是为了优化代码,将其声明为inline函数
friend void swap(HasPtr&, HasPtr&);
};
inline void swap(HasPtr &lhs,HasPtr &rhs){
using std::swap;
swap(lhs.ps,rhs.ps);
swap(lhs.i,rhs.i);
}

// 注意rhs是按值传递的,意味着HasPtr的拷贝构造函数
// 将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs){
swap(*this,rhs); //rhs现在指向本对象曾经使用的内存
return *this; // rhs被销毁,从而delete了rhs中的指针
}

13.5 *动态内存管理类

  • 某些类需要在运行时分配可变大小的内存空间,可以使用标准库容器来保存它们的数据
    • 某些类需要自己进行内存分配,这些类必须定义自己的拷贝控制成员来管理所分配的内存
  • 将实现标准库vector类的一个简化版本,只针对于string,被命名为StrVec
  • StrVec类的设计
    • 在StrVec类中使用allocator类获得原始内存
    • 使用destroy成员来销毁元素
    • alloc_n_copy会分配内存,并拷贝一个给定范围中的元素
    • free会销毁构造的元素并释放内存
    • chk_n_alloc保证StrVec至少有容纳一个新元素的空间
    • reallocate在内存用完时为StrVec分配新内存
  • 使用construct
    • 函数push_back调用chk_n_alloc确保空间容纳新元素
  • 在重新分配内存的过程中移动而不是拷贝元素
    • 移动拷贝函数
    • std::move;move的标准库函数,位于utility文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class StrVec{
public:
StrVec(): //allocator成员进行默认初始化
elements(nullptr),first_free(nullptr),cap(nullptr){}
StrVec(const StrVec&); //拷贝构造函数
StrVec &operator=(const StrVec&); //拷贝赋值运算符
~StrVec(); //析构函数
void push_back(const std::string&); //拷贝元素
size_t size() const {return first_free - elements;}
size_t capacity() const {return cap - elements;}
std::string *begin() const {return elements;}
std::string *end() const {return first_free;}
// ...
private:
Static std::allocator<std::string> alloc; //分配元素
void chk_n_alloc(){
if(size() == capacity())
reallocate();
}
// 工具函数,被拷贝构造函数,赋值运算符和析构函数所使用
std::pair<std::string*,std::string*> alloc_n_copy
(const std::string*,const std::string*);
void free();
void reallocate();
std::string *elements;
std::string *first_free;
std::string *cap;
}

13.6 对象移动

  • 新标准的一个最主要特征是:可以移动而非拷贝对象的能力
    • 标准库容器,string和shared_ptr类既支持移动也支持拷贝.IO类和unique_ptr类可以移动但不能拷贝

13.6.1 右值引用

  • 左值与右值的介绍
  • 为了支持移动操作,新标准引入了新的引用类型——右值引用
    • 右值引用就是必须绑定到右值的引用,使用&&获得右值引用,且只能绑定到一个将要销毁的对象
    • 左值引用不能将其绑定到要求转换的表达式,字面常量或返回右值的表达式
      • 返回左值引用的函数,连同赋值,下标,解引用和前置递增运算符都是返回左值,可以用左值引用绑定
    • 右值引用可以绑定到这类表达式上
      • 返回非引用类型的函数,连通算数,关系,位以及后置递增运算符都生成右值,可以用const左值引用或右值绑定
  • 左值持久,右值短暂
    • 左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时对象
  • 变量是左值:变量可以看作只有一个运算对象而没有运算符的表达式
    • 不能将一个右值引用绑定到一个右值引用类型的变量上
    • 变量是左值,不能将一个右值引用直接绑定到一个变量(包括右值引用类型的变量)上
  • 标准库move函数
    • 可以显式地将一个左值转换位对应地右值引用
    • 调用move就承诺:除了对左值赋值或销毁它之外,将不再使用它,且调用move之后,不能对移动后原对象地值做任何假设
      • 可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的
      • 使用move的代码应该使用std::move而不是move,可以避免潜在的名字冲突
1
2
3
4
int &&rr1 = 42;
int &&rr2 = rr1; //错误:表达式rr1是左值

int &&rr3 = std::move(rr1);

13.6.2 移动构造函数和移动赋值运算符

  • 移动拷贝函数的第一个参数是该类类型的一个引用
    • 该引用为一个右值引用
    • 任何额外的参数都必须有默认实参
  • 需要保证移后源对象处于销毁无害的状态,且一旦资源完成移动,源对象必须不再指向被移动的资源
    • 动构造函数不分配任何新内存,而是接管给定的内存
    • 接管内存后,将对象中的指针都置为nullptr
  • noexcept
    • 承诺函数不抛出异常的一种方法,在参数列表后指定noexcept
    • 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
1
2
3
4
5
6
7
// StrVec类定义移动构造函数
StrVec::StrVec(StrVec &&s) noexcept //移动操作不应抛出任何异常
: elements(s.elements),first_free(s.first_free),cap(s.cap)
{
// 对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
  • 移动赋值运算符
    • 移动赋值运算符执行于析构函数和移动构造函数相同的工作
    • 以移动构造函数一样,若移动赋值运算符不跑出任何异常,就应该标记为noexcept
1
2
3
4
5
6
7
8
9
10
StrVec &StrVec::operator=(StrVec &&rhs) noexcept{
if(this!=&rhs){
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
  • 移后源对象必须可析构
    • 从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁
      • 因此,当编写一个移动操作时,必须确保移后源对象进入一个可析构的状态
      • 可通过将移后源对象的指针成员置为nullptr来实现的。
    • 除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的
      • 一般来说,对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值
      • 另一方面,移动操作对移后源对象中留下的值没有任何要求
      • 因此程序不应该依赖于移后源对象中的数据
  • 合成的移动操作
    • 若一个类定义了拷贝构造函数,拷贝赋值运算符或析构函数,编译器就不会合成移动构造函数和移动赋值运算符
    • 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符
      • 编译器可以移动内置类型的成员
      • 如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员
    • 移动操作永远不会隐式定义为删除的函数
      • 可以显式要求生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数
      • 移动构造函数被定义为删除的函数的条件是:
        • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似
        • 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
        • 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
        • 类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的
      • 移动操作和合成的拷贝控制成员间还有最后一个相互作用关系:一个类是否定义了自己的移动操作对拷贝操作如何合成有影响。如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的,需要定义自己的拷贝操作
  • 如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动赋值运算符的情况类似。
  • 拷贝并交换赋值运算符和移动操作
    • 拷贝并交换赋值运算符:是函数匹配和移动操作间相互关系的一个很好的示例
  • 所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。如前所述,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HasPtr{
public:
//添加的移动构造函数
HasPtr(HasPtr &&p) noexcept :ps(p.ps),i(p.i){p.ps = 0;}
// 赋值运算符既是移动赋值运算符,也是拷贝赋值运算符
HasPtr& operator=(HasPtr rhs){
swap(*this,rhs);
return *this;
}
}
// 为类添加了一个移动构造函数,它接管了给定实参的值
// 构造函数体将给定的HasPtr的指针置为0,从而确保销毁移后源对象是安全的

hp = hp2; //hp2是左值使用拷贝构造函数拷贝
hp = std::move(hp2); //移动构造函数移动hp2
  • 移动迭代器
    • 一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器,移动迭代器的解引用运算符生成一个右值引用
    • 调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器
  • 建议:不要随意使用移动操作
    • 由于一个移后源对象具有不确定的状态,对其调用std::move是危险的
    • 调用move时,必须绝对确认移后源对象没有其他用户

13.6.3 右值引用和成员函数

  • 除了构造函数和赋值运算符之外,一个成员函数同时提供拷贝和移动版本,一个版本接收一个指向const的左值引用,第二个版本接收一个指向非const的右值引用
    • 区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&
    • push_bakc的标准库容器提供两个版本
1
2
void push_back(const X&); //拷贝,绑定到任意类型的X
void push_back(X&&); //移动,只能绑定到类型X的可修改的右值
  • 右值和左值引用成员函数
    • 在一个对象上调用成员函数,而不管该对象是一个左值还是右值
    • 新标准中仍然允许向右值赋值,若希望阻止这种用法,希望强制左侧运算对象(即this指向的对象)是一个左值
    • this的左右值属性与定义const成员函数相同,即在参数列表后放置一个引用限定符
      • 引用限定符可以是&或&&,分别指出this可以指向一个左值或右值
      • 类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中
      • 一个函数可以同时用const和引用限定。但引用限定符必须跟随在const限定符之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string s1 = "a value",s2 = "another";
auto n = (s1+s2).find('a');

s1 + s2 = "wow"; //正确,但应阻止这种用法

class Foo{
public:
Foo &operator=(const Foo&) &; //只能向可修改的左值放置
};
Foo &Foo::operator=(const Foo &rhs) &{
//do something works;
return *this;
}

class Foo{
Foo anotherMem() const &; //正确,const限定符在前
}
  • 重载和引用函数
    • 可以综合引用限定符和const来区分一个成员函数的重载版本
    • 当定义const成员函数时,可以定义两个版本,唯一的差别是一个版本有const限定而另一个没有
    • 引用限定的函数则不一样:如果定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加
      • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符

14 重载运算与类型转换

14.1 基本概念

  • 重载的运算符具有特殊名字的函数:名字由operator和其后要定义的运算符号共同组成,也包含返回类型,参数列表以及函数体
    • 如果运算符函数是成员函数,则第一个左侧运算对象绑定到隐式的this指针上(成员运算符函数的显式参数数量比运算符的运算对象总数少一个)
    • 对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数,意味着当运算符作用于内置类型的运算对象时,无法改变该运算符的含义
  • 不能被重载的运算符
    • ::
    • .*
    • .
    • ? :
  • 直接调用一个重载的运算符函数
    • 将运算符作用于类型正确的实参,从而以这种简介调用重载的运算符函数
    • 可以像调用普通函数一样直接调用运算符函数,先指定函数名字,然后再传入数量正确,类型适当的实参
    • 可以显式调用成员运算符的函数:首先指定运算函数的对象的名字,然后使用点运算符访问希望调用的函数
  • 某些运算符不应该被重载
    • 某些运算符指定了运算对象求值的顺序,若重载则运算对象求值顺序的规则不发应用到重载的运算符上
      • 特使是:逻辑与,逻辑或和逗号运算符
    • C++对逗号运算符和取地址运算符定义了特殊含义,一般不会被重载
  • 使用于内置类型一致的含义
    • 某些操作在逻辑上与运算符相关,则适合于定义成重载的运算符
    • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容,逻辑运算符和关系运算符应该返回bool,算数运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符应该返回左侧运算对象的引用
  • 选择作为成员或者非成员
    • 定义重载的运算符时,必须首先决定将其声明为类的成员函数还是声明为一个普通的非成员函数
      • 赋值(=),下标([ ]),调用(( ))和成员访问箭头(->)运算符必须是成员
      • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同
      • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
      • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误:不饿能为int重定义内置的运算符
int operator+(int,int);

// 一个非成员运算符函数的等价调用
data1 + data2;
operator+(data1,data2);
// 调用非成员函数operator+,传入data1,data2作为实参

data1 += data2;
data1.operator+=(data2);

string s = "world";
string t = s + "!";
string u = "hi" + s; //如果+时string的成员则产生错误
// 如果operator+时string类的成员,则上面的第一个加法等价于s.operator+("!")
// "hi"+s等价于"hi".operator+(s)

14.2 输入和输出运算符

  • IO标准库分别使用>>和<<执行输入和输出操作
    • 对两个运算符来说,IO库定义了用其读写内置类型的版本,而类需要自定义适合其对象的新版本以支持IO操作

14.2.1 重载输出运算符<<

  • 通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用时因为无法直接复制一个ostream对象
    • 第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型,第二个形参是引用的原因是我们希望避免复制实参
    • 为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参
  • 输出运算符尽量减少格式化操作
    • 用于内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符
    • 如果运算符打印了换行符,则用户就无法在对象的同一行内接着打印一些描述性的文本,相反,令输出运算符尽量减少格式化操作可以使用户有权控制输出的细节
    • 通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
  • 输入输出运算符必须使非成员函数
    • 与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数,否则左侧运算对象是类的一个对象
    • 假设输入输出运算符是某个类的成员,则它们也必须是istream或ostream的成员,然而,这两个类属于标准库,并且无法给标准库中的类添加任何成员
      • 如果希望为类自定义IO运算符,则必须将其定义成非成员函数
1
2
3
4
5
6
7
8
// Sales_data的输出运算符
ostream& operator<<(ostream &os, const Sales_data &item){
os << item.isbn() <<" " << item.units_sold <<" "<< item.revenus << " " << item.avg_price();
return os;
}

Sales_data data;
data<<cout; //如果operator<<是Sales_data的成员

14.2.2 重载输入运算符>>

  • 输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的对象的引用,该运算符通常会返回某个给定流的引用
    • 第二个形参之所以必须是非常量是因为输入运算符本身的目的就是将数据读入到这个对象中
  • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要
  • 输入时的错误
    • 当流含有错误类型的数据时读取操作可能会失败
    • 当读取操作到达文件末尾或遇到输入流的其他错误时也会失败
    • 通常将对象置于合法的状态,能略微保护使用者免于受到输入错误的影响,当读取操作发生错误时,输入运算符应该负责从错误中恢复
1
2
3
4
5
6
7
8
9
10
// Sales_data的输入运算符
istream &operator>>(istream &is,Sales_data &item){
double price;
is >> item.bookNo >> item.units_sold >>price;
if(is)
item.revenue = item.units_sold * price;
else
item = Sales_data();
return is;
}

14.3 算术和关系运算符

  • 通常情况下,把算数和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换
    • 这些运算符一般不需要改变运算对象的状态,形参都是常量的引用
  • 算数运算符通常会计算它的两个运算对象并得到一个新值,操作完成后返回该局部变量的副本作为其结果
    • 如果类定义了算数运算符,则一般也会定义一个对应的复合赋值运算符
  • 如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符
1
2
3
4
5
6
7
// 最有效的方法是使用复合赋值来定义算数运算符
// 假设两个对象指向同一本书
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs){
Sales_data sum = lhs;
sum += rhs;
return sum;
}

14.3.1 相等运算符

  • C++中的类通过定义相等运算符来检测两个对象是否相等
1
2
3
4
5
6
7
8
bool operator==(const Sales_data &lhs, const Sales_data &rhs){
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator !=(const Sales_data &lhs, const Sales_data &rhs){
return !(lhs == rhs);
}
  • 设计准则:
    • 如果一个类含有判断两个对象是否相等的操作,则它显然应该把函数定义成operator==而非一个普通的命名函数
    • 如果类定义了operator==,则该运算符应该能判断一组给定的对象中是否含有重复数据
    • 通常情况下,相等运算符应该具有传递性
    • 如果类定义了operator==,则这个类也应该定义operator!=
    • 相等运算符和不相等运算符中的一个应该把工作委托给另外一个,这意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符则只是调用那个真正工作的运算符

14.3.2 关系运算符

  • 定义了相等运算符的类常常包含关系运算符,特别是,因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用
  • 通常情况下关系运算符应该
    • 定义顺序关系,令其与关联容器中对关键字的要求一致;
    • 如果类同时含有运算符的话,则定义一种关系令其与保持一致
  • 如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符
  • 如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符

14.4 赋值运算符

  • 拷贝赋值和移动赋值运算符可以把类的一个对象赋值给该类的另一个对象
    • 类可以定义其他赋值运算符以使用别的类型作为右侧运算对象
  • 和拷贝赋值及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,再创建一个新空间
    • 不同之处是运算符无须检查对象向自身的赋值
  • 可以重载赋值运算符,不论形参的类型是什么,赋值运算符都必须定义为成员函数
  • 复合赋值运算符
    • 复合赋值运算符不非得是类的成员,但还是倾向于把包括复合赋值在内的所有赋值运算符都定义在类的内部
    • 为了与内置类型的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧运算对象的引用
  • 赋值运算符必须定义成类的成员,复合赋值运算符也通常定义为类的成员(两类运算符都应该返回左侧运算对象的引用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 标准库vector定义了第三种赋值运算符,接受花括号内的元素列表作为参数
vector<string> v;
v = {"a","an","the"};

// 同样也可以把这个运算符添加到StrVec类中
class StrVec{
public:
StrVec &operator=(std::initializer_list<std::string>);
};

// 为了与内置类型的赋值运算符保持一致,新的赋值运算符将返回其左侧运算对象的引用
StrVec &StrVec::operator=(initializer_list<string> il){
auto data = alloc_n_copy(il.begin(),il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}

// Sales_data类中复合赋值运算符的定义
Sales_data& Sales_data::operator+=(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

14.5 下标运算符

  • 表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[]
  • 下标运算符必须是成员函数
    • 下标运算符通常以所访问元素的引用作为返回值
    • 最好定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用确保不会给返回的对象赋值
1
2
3
4
5
6
7
8
9
10
11
class StrVec{
public:
std::string& operator[](std::size_t n){
return elements[n];
}
const std::string& operator[](std::size_t n) const{
return elements[n];
}
private:
std::string *elements; //指向数组首元素的指针
}

14.6 递增和递减运算符

  • 对于内置类型来说,递增和递减运算符既有前置版本还有后置版本
    • 应该为类定义两个版本递增和递减运算符
  • 为了与内置版本保持一致前置运算符应该返回递增或递减后对象的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class StrBlobPtr{
public:
StrBlobPtr& operator++();
StrBlobPtr& operator--();
}

// 递增和递减运算符的工作机理非常相似
// 前置版本:返回递增/递减对象的引用
StrBlobPtr& StrBlobPtr::operator++(){
check(curr,"increment past end of StrBlobPtr");
++curr;
return *this;
}

StrBlobPtr& StrBlobPtr::operator--(){
--curr;
check(curr,"decrement past begin of StrBlobPtr");
return *this;
}
  • 区分前置和后置运算符
    • 要想同时定义前置和后置运算符,需要解决同一个符号的重载问题
      • 后置版本接收一个额外的int类型的形参
        • 当使用后置运算符时,编译器为形参提供了值为0的实参
        • 形参的唯一作用就是区分前置版本和后置版本的函数,而不是真的在实现后置版本时参与运算
    • 为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用
  • 显式地调用后置运算符
    • 可以显式地调用一个重载的运算符,其效果与在表达式中以运算符号的形式使用它完全一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 添加后置运算符
class StrBlobPtr{
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
}

// 在递增对象之前需要首先记录对象的状态
StrBlobStr StrBlobStr::operator++(int){
StrBlobPtr ret = *this;
++*this;
return ret;
}
StrBlobStr StrBlobStr::operator--(int){
StrBlobPtr ret = *this;
--*this;
return ret;
}

// 用函数调用地方式调用后置版本
StrBlobPtr p(a1);
p.operator++(0);
p.operator++();

14.7 成员访问运算符

  • 在迭代器类及智能指针类中常常用到解引用运算符和箭头运算符
    • 箭头运算符必须是类的成员,解引用运算符通常也是类的成员
  • 对箭头运算符返回值的限定
    • 令operator*完成任何指定操作,可以让operator*返回一个固定值或打印对象的内容或其他
    • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class StrBlobPtr{
public:
std::string& operator*() const{
auto p = check(curr,"dereference past end");
return (*p)[curr];
}
std::string* operator->() const{
return & this->operator*();
}
}
// 解引用运算符首先检查curr是否仍在作用范围内,如果是,则返回curr所指元素的一个引用
// 箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果元素的地址
// 值得注意的是,我们将这两个运算符定义成了const成员,这是因为与递增和递减运算符不一样,获取一个元素并不会改变StrBlobPtr对象的状态
// 同时,它们的返回值分别是非常量string的引用或指针,因为一个StrBlobPtr只能绑定到非常量的StrBlob对象

(*point).mem; //point是一个内置的指针类型
point.operator()->mem; //point是类的一个对象

14.8 函数调用运算符

  • 如果类重载了函数调用运算符,可以像使用函数一样使用该类的对象
    • 因为这样的类同时能存储状态,所以与普通函数相比更加灵活
  • 函数调用运算符必须是成员函数
    • 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别
    • 如果类定义了调用运算符,则该类的对象称作函数对象;因为可以调用这种对象,所以说这些对象的“行为像函数一样”
1
2
3
4
5
6
7
8
9
10
struct absInt{
int operator() (int val) const{
return val<0? -val:val;
}
}

// 定义了一种操作:函数调用运算符,它负责接受一个int类型的实参,然后返回该实参的绝对值
int i = -42;
absInt abdObj;
int ui = absObj(i); //将i传递给absObj.operator()
  • 含有状态的函数对象类
    • 和其他类一样,函数对象除了operator()之外可以包含其他成员
    • 函数对象类通常含有一些数据成员,成员被用于定制调用运算符中的操作
1
2
3
4
5
6
7
8
9
10
class PrintString{
public:
PrintString(ostream &o = cout, char c = ' '):
os(o),sep(c){}
void operator() (const string &s) const {os << s << seq;}
private:
ostream &os;
char seq;
}
// 类有一个构造函数,它接受一个输出流的引用以及一个用于分隔的字符,这两个形参的默认实参分别是cout和空格。之后的函数调用运算符使用这些成员协助其打印给定的string

14.8.1 lambda是函数对象

  • 在lambda表达式产生的类中含有一个重载的函数调用运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 传递给stable_sort作为最后一个实参的lambda表达式
// 根据单词长度第七进行排序,对于长度相同的字母按照字母表顺序排序
stable sort(words.begin(),words.end()),
[](const string &a, const string &b)
{return a.size() < b.size();}

// 行为类似一个类的未命名对象
class ShorterString{
public:
bool operator() (const string &s1, const string &s2) const
{return s1.size() < s2.size();}
};

// 在默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。如果lambda被声明为可变的,则调用运算符就不是const的了

// 用这个类代替lambda
stable sort(words.begin(),words.end(),ShorterString());
// 第三个实参是新构建的ShorterString对象,当stable_sort内部的代码每次比较两个string时就会“调用”这一对象,此时该对象将调用运算符的函数体,判断第一个string的大小小于第二个时返回true
  • 表示lambda及相应捕获行为的类
    • 当lambda表达式通过引用捕获变量时,需要确保lambda执行时引用所引用的对象确实存在
    • 通过值捕获的变量被拷贝到lambda中
      • lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用比获得变量的值来初始化数据成员
  • lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 找到第一个长度不小于给定值的string对象
auto wc = find_if(words.begin(),words.end(),
[sz](const string &a){return a.size()>=sz;});

// lambda表达式产生的类将形成
class SizeComp{
sizeComp(size_t n):sz(n){}
bool operator() (const string &s) const
{return s.size() >= sz;}
private:
size_t sz;
}

// 获得第一个指向满足条件元素的迭代器,该元素满足size() is >= sz
auto wc = find_if(words.begin(),words.end(),SizeComp(sz));

14.8.2 标准库定义的函数对象

  • 标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符
    • 定义在functional头文件中
算术 关系 逻辑
plus<Type> equal_to<Type> logical_and<Type>
minus<Type> no_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>

14.8.3 可调用对象与function

  • C++中可调用的对象
    • 函数
    • 函数指针
    • lambda表达式
    • bind创建的对象
    • 重载了函数调用运算符的类
  • 两个不同类型的可调用对象可以共享同一种调用形式
    • 调用形式指明了调用返回类型以及传递给调用的实参类型
    • 一种调用形式对应一个函数类型
  • 不同类型可能具有相同的调用形式
  • 对于几个可调用对象共享同一种调用形式的情况,有时希望把它们看成具有相同的类型
  • 函数表:用于存储指向可调用对象的指针,当程序需要执行某个特定的操作时,从表中查找该调用函数
    • 函数表可以使用map实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int(int, int)
// 函数类型,接收两个int,返回一个int

// 考虑下列不同类型的可调用对象
int add(int i,int j){return i+j;}
auto mod = [](int i, int j){return i%j;}

// 函数对象类
struct divide{
int operator()(int denominator,int divisor){
return denominator/divisor;
}
};

// 共享一种调用形式
int(int, int)

// map实现函数表
map<string,int(*)(int,int)> binops;
binops.insert({"+"},add); //{"+",add}是一个pair
function的操作 -
function<T> f; f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同
function<T> f(nullptr); 显式地构造一个空function
function<T> f(obj); 在f中存储可调用对象obj的副本
f 将f作为条件;当f含有一个可调用对象时为真;否则为假
f(args) 调用f中的对象,参数是args
定义为function<T>的成员的类型 -
result_type 该function类型的可调用对象返回的类型
argument_type 当T有一个或两个实参时定义的类型
first_argument_type 第一个实参类型
second_argument_type 第二个实参类型
  • function是一个模板,在创建时需要提供额外的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 调用形式
function<int(int,int)>

function<int(int,int)> f1 = add;
function<int(int,int)> f2 = divide();
function<int(int,int)> f3 = [](int i,int j){return i*j;}

// 重新定义map
map<string,function<int(int,int)>> binops;

map<string,function<int(int,int)>> binops = {
{"+",add},
{"-",std::minus<int>()},
{"/",devide()},
}
  • 重载的函数与function
    • 不能直接将重载函数的名字存入function类型的对象中
    • 解决二义性的一条途径:存储函数指针而非函数的名字
    • 第二条途经:使用lambda
  • 新版本标准库中的function类与旧版本中的unary_function和binary_function没有关联,后两个类已经被更通用的bind函数替代了
1
2
3
4
int (*fp)(int,int) = add;
binops.insert({"+",fp});

binops.insert({"+",[](int a,int b){return add(a,b);}});

14.9 重载,类型转换与运算符

  • 跳转:由一个实参调用的非显式构造函数定义了一种隐式的类型转换,这种构造函数将实参类型的对象转换成类类型
  • 可以定义对于类类型的类型转换,通过定义类型转换运算符可以做到,转换构造函数和类型转换运算符共同定义了类类型转换

14.9.1 类型转换运算符

  • 类型转换运算符:类的一种特殊成员函数,负责将一个类类型的值转换成其他类型
    • 一般形式:operator type() const
    • 类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数
    • 类型转换运算符通常不应该改变待转换对象的内容,因此类型转换运算符一般被定义成const成员
  • 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const
  • 类型转换运算符是隐式执行的,无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参
    • 同时,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义含有类型转换运算符的类
class SmallInt{
public:
SmallInt(int i=0):val(i){
if(i<0||i>255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const {return val;}
private:
std::size_t val;
}

// 我们的SmallInt类既定义了向类类型的转换,也定义了从类类型向其他类型的转换
// 其中,构造函数将算术类型的值转换成SmallInt对象,而类型转换运算符将SmallInt对象转换成int
SmallInt si;
si = 4;
si +3; //首先将si隐式转换成int,然后执行整数加法

class SamllInt;
operator int(SmallInt&); //错误,不是成员函数
class SmallInt{
public:
int operator int() const; //错误,制定了返回类型
operator int(int =0) const; //错误,参数列表不为空
operator int*() const {return 42;} //错误,42不是指针
}
  • 显式的类型转化运算符:C++11新标准引入的
    • 编译器不会将一个显式的类型转换运算符用于隐式类型转换
      • 当表达式出现在下列位置时,显式的类型转换将被隐式地执行
        • if、while及do语句的条件部分
        • for语句头的条件表达式
        • 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象
        • 条件运算符(? :)的条件表达式
    • 在C++11新标准下,IO标准库通过定义一个向bool的显式类型转换实现向void*转换
      • 向bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的
1
2
3
4
5
6
7
8
9
10
11
12
13
// 显式的类型转换
class SmallInt{
public:
ecplicit operator int() const {return val;};
};

SmallInt si =3;
si + 3; //错误发生了隐式转换

// 无论我们什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。例如:
while(std::cin>>value)
// while语句的条件执行输入运算符,它负责将数据读入到value并返回cin。为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换
// 如果cin的条件状态是good,则该函数返回为真;否则该函数返回为假

14.9.2 避免有二义性的类型转换

  • 如果类中包含一个或多个类型转换,则必须确保在类类型和目标之间只有唯一的转换方式
    • 多重转换路径:
      • 第一种情况是两个类提供相同的类型转换
      • 第二种情况是定义了多个类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义了两种将B转换成A的方法:一种使用B的类型转换运算符、另一种使用A的以B为参数的构造函数
struct B;
struct A{
A() = default;
A(const B&); //将B转换成A
};
struct B{
operator A() const
};

A f(const A&);
B b;
A a = f(b); //二义性错误:含义是f(B::operator A())还是f(A::A(const B&))

// 需要显式调用
A a1 = f(b.operator A());
A a2 = f(A(b));
  • 以下的经验规则可能避免类型转换产生二义性有帮助:

    • 不要令两个类执行相同的类型转换:如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中再定义转换目标是Foo类的类型转换运算符
    • 避免转换目标是内置算术类型的类型转换。特别是当你已经定义了一个转换成算术类型的类型转换时,接下来
      • 不要再定义接受算术类型的重载运算符。如果用户需要使用这样的运算符,则类型转换操作将转换你的类型的对象,然后使用内置的运算符
      • 不要定义转换到多种算术类型的类型转换。让标准类型转换完成向其他算术类型转换的工作
    • 一言以蔽之:除了显式地向bool类型的转换之外,我们应该尽量避免定义类型转换函数并尽可能地限制那些“显然正确”的非显式构造函数
  • 重载函数与转换构造函数:如果在调用重载函数时我们需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序的设计存在不足。在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性

14.9.3 函数匹配与重载运算符

  • 和普通函数调用不同,不能通过调用的形式来区分当前调用的是成员函数还是非成员函数
    • 当使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内
    • 当我们一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载
      • 因为用来调用命名函数的语法形式对于成员函数和非成员函数来说是不相同的
      • 当通过类类型的对象(或者该对象的指针及引用)进行函数调用时,只考虑该类的成员函数
      • 而当在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,因此二者都应该在考虑的范围内
  • 如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题

15 面向对象程序设计

15.1 OOP概述

  • 面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定
    • 通过数据抽象,可以将类的接口与实现分离
    • 使用继承,可以定义相似的类型并对其相似关系建模
    • 使用动态绑定,可以在一定程度上忽略相似类型的区别,以同一的方式使用它们的对象
  • 继承
    • 通过继承联系在一起的类构成一种层次关系
    • 通常在层次关系的根部有一个基类,其他类直接或简介地从基类继承而来,继承得到的类称为派生类
    • 基类负责定义层次关系中所有类共同拥有的成员,每个派生类定义各自特有的成员
    • C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待
      • 对于某些函数,基类希望派生类各自定义适合自身的版本,此时基类将这些函数声明为虚函数
      • 派生类必须使用类派生列表指出是由哪个基类继承而来
1
2
3
4
5
6
7
8
9
10
11
class Quote{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};

// 类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符
class Bulk_quote:public Quote{
public:
double net_price(std::size_t) const override;
}
  • 动态绑定
    • 通过使用动态绑定,能用同一段代码分类处理两个类中的对象
    • 在运行时选择函数的版本,动态绑定又称为运行时绑定
    • 当使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
1
2
3
4
5
6
7
8
9
10
11
12
// 计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os,const Quote &item, size_t n){
// 根据传入item形参的对象类型调用Quote::net_price
// 或者Bulk_quote::net_price
double ret = item.net_price(n);
os << item.isbn() << n << ret <<endl;
return ret;
}

// basic的类型是Quote,bulk的类型是Bulk_quote
print_total(cout,basic,20); //调用Quote的net_price
print_total(cout,bulk,20); //调用Bulk_quote的net_price

15.2 定义基类和派生类

15.2.1 定义基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 基类的定义
class Quote{
public:
Quote() = default;
Quote(const std::string &book,double sales_price):
bookNo(book),price(sales_price){}
std::string isbn() const {return bookNo;}
virtual double net_price(std::size_t n) const{
return n * price;
}
virtual ~Quote() = default; //对析构函数进行动态绑定
private:
std::string bookNo;
protected:
double price = 0.0;
};
  • 基类都应该定义一个虚析构函数,即使该函数不执行任何操作
  • 成员函数与继承
    • 派生类可以继承其基类的成员,派生类需要对部分操作提供自己的新定义以覆盖(override)从基类继承来的旧定义
  • 基类必须将两种成员函数分开:
    • 一类是基类希望其派生类进行覆盖的函数(虚函数)
      • 当使用指针或引用调用虚函数时,该调用被动态绑定(执行基类版本或派生类版本)
      • 在成员函数语句之前加virtual使得函数执行动态绑定
        • 任何构造函数之外的函数都可以是虚函数
        • virtual智能出现在类内部的声明语句中
    • 一类是基类希望派生类直接继承而不要改变的函数
  • 访问控制与继承:基类允许派生类访问基类的成员,但禁止其他用户访问,使用受保护的(protected)访问运算

15.2.2 定义派生类

  • 派生类必须通过使用类派生列表明确指出是从哪个基类继承而来的
    • 类派生列表的形式是,首先一个冒号,后面紧跟以逗号分割的基类列表,其中每个基类前都可以有三类访问说明符的一个public,protected,private
  • 如果一个派生是公共的,则基类的公共成员也是派生类接口的组成部分
  • 派生类中的虚函数
    • 派生类经常覆盖它继承的虚函数,如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本
      • 派生类可以在它覆盖的函数前使用virtual关键字,但不是非要这么做
      • C++11新标准允许派生类显式注明它在使用某个成员函数覆盖了它继承的虚函数
  • 派生类对象及派生类向基类的类型转换
    • 一个派生类对象包含多个组成部分:一个派生类对象包含多个组成部分,一个含有派生类自定义的非静态成员的子对象,以及一个与该派生类继承的基类对应的子对象
    • 在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的
    • 派生类对象中含有与基类对应的组成部分,可以把派生类的对象当作基类对象来使用,而且也能将基类的指针或引用绑定到派生类对象中的基类部分
    • 派生类到基类的类型转换,和其他类型转换一样,编译器会隐式地执行派生类到基类的转换
      • 隐式特性意味着可以把派生类对象或派生类对象的引用用在需要基类引用的地方;同时可以把派生类对象的指针用在需要基类指针的地方
  • 派生类构造函数:派生类可以访问基类的公共成员和受保护成员
  • 继承与静态成员
    • 基类定义了一个静态成员,则在整个继承中只存在该成员的唯一定义
    • 若基类中的成员是private,派生类无权访问
  • 被用作基类的类
    • 一个类是基类,同时也可以是派生类
    • 直接基类出现在派生列表中,而简介基类有派生类通过其直接基类继承而来
    • 每个类都会继承直接基类的所有成员
  • 防止继承的发生
    • 在类名后跟final关键字可以防止继承发生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Bulk_quote:public Quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double,std::size_t,double);
// 覆盖基类的函数版本以实现基于大量购买折扣政策
double net_price(dts::size_t) const override;
private:
std::size_t min_qty = 0;
double discount = 0.0;
}

Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p指向Quote对象
p = &bulk; //p指向bulk的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分

class NoDerived final(); //NoDerived不能作为基类

15.2.3 类型转换与继承

  • 可以将基类的指针绑定到派生类对象上
    • 当使用基类的引用时,实际上不清楚引用所绑定的真实类型,该对象可能是基类的对象,也可能是派生类的对象
    • 和内置指针一样,智能指针类也支持派生类向基类的类型转换(可以将派生类对象的指针存储在一个基类的智能指针内)
  • 静态类型与动态内存
    • 当使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示的对象的动态类型区分开
      • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致
      • 基类的指针或引用的静态类型可能与其动态类型不一致
  • 不存在从基类向派生类的隐式类型转换
    • 之所以存在派生类向基类的类型转换时因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上
    • 一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在
    • 如果基类对象不是派生类对象的一部分,则它只含有基类定义的成员
    • 不存在从基类向派生类的自动类型转换
    • 即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换
1
2
3
4
5
6
7
8
9
10
// 一个基类的对象可能是派生类对象的一部分,也可能不是
// 不存在从基类向派生类的自动类型转换
Quote base;
Bulk_quote* bulkP = &base; //错误,不能将基类转换成派生类
Bulk_quote& bulkP = base; //错误,不能将基类转换成派生类

// 即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换
Bulk_quote bulk;
Quote *itemP = &bulk;
Bulk_quote *bulkP = itemP; //错误,不能将基类转换成派生类
  • 在对象之间不存在类型转化
    • 派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这种转换
    • 当用派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略掉
  • 存在继承关系的类型之间的转换规则
    • 从派生类向基类的类型转换只对指针或引用类型有效
    • 基类向派生类不存在隐式类型转换
    • 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问数显而变得不可行

15.3 虚函数

  • 使用基类的引用或指针调用一个虚成员函数时会执行动态绑定
    • 必须为所有虚函数都提供定义
  • 对虚函数的调用可能在运行时才被解析
    • 当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数
    • 被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的一个
  • 动态绑定只有当通过指针或引用调用虚函数时才会发生
    • 通过一个具有普通类型的表达式调用虚函数时,在编译时就能将调用的版本确认下来
  • C++的多态性
    • 把具有继承关系的多个类型称为多态类型
    • 引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在
    • 当使用基类的引用或指针调用基类中定义的一个函数时,并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型
    • 另一方面,对非虚函数的调用在编译时进行绑定
      • 类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上
  • 派生类中的虚函数
    • 当在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质(一旦被声明为虚函数,在所有的派生类中都是虚函数)
    • 基类中的虚函数在派生类中隐含的也是一个虚函数,派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配
  • final和override说明符
    • 在新标准中,使用override关键字说明派生类中的虚函数
    • 某个函数指定为final,之后任何尝试覆盖该函数的操作都将引发错误
    • final和override说明符出现在形参列表以及尾置返回类型之后
  • 虚函数与默认形参:虚函数也可以拥有默认实参
    • 如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定
      • 如果通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此
  • 回避虚函数机制
    • 希望对虚函数的调用不要进行动态绑定,而是强迫执行虚函数的某个特定版本,可以使用作用域运算符
    • 如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域作用符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归
1
2
3
4
5
6
7
8
// 到底调用net_price的哪个版本完全依赖于运行时绑定到item的实参的实际(动态)类型
Quote base("0-201-82470-1",50);
print_total(cout,base,10); //调用Quote::net_price
Bulk_total derived("0-201-82470-1",50,5,.19);
print_total(cout,derived,10); //调用Bulk_quote::net_price

// 回避虚函数机制
double undiscounted = baseP->Quote::net_price(42);

15.4 抽象基类

  • 纯虚函数
    • 纯虚函数无需定义,通过在函数体的位置书写=0可以将一个虚函数说明为纯虚函数,=0智能出现在类内部的虚函数声明语句处
      • 可以为纯虚函数提供定义,但函数体必须定义在类的外部(不能在类的内部为一个=0的函数提供函数体)
  • 含有纯虚函数的类是抽象基类
    • 抽象基类负责定义接口,而后续的其他类可以覆盖该接口
    • 不能直接创建一个抽象基类的对象,可定义派生类的对象(前提是提前覆盖率纯虚函数)
  • 派生类构造函数只初始化它的直接基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Dis_quote:public Quote{
public:
Disc_quote() = default;
Disc_quote(const std::string& book,double price,std::size_t qty, double disc):
Quote(book,price),quantity(qty),discount(disc){}
double net_price(std::size_t) const =0;
protected:
std::size_t quantity = 0;
double discount = 0.0;
}

class Bulk_quote:public Disc_quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book,double price, std::size_t qty, double disc):
Disc_quote(book,price,qty,disc){}
// 覆盖基类中的函数版本以实现一种新的折扣策略
double net_price(std::size_t) const override;
}

15.5 访问控制与继承

  • 每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制其成员对于派生类是否可访问
  • 受保护的成员
    • 一个类使用protect关键字来声明那些希望与派生类分享但不想被其他公共访问使用的成员
    • protect说明符可以看作是public和private中和后的产物
      • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
      • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的
      • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权
  • 公有,私有和受保护继承
    • 某个类对其继承而来的成员的访问权限受到两个因素影响
      • 在基类中该成员的访问说明符
      • 在派生类的派生列表中的访问说明符
    • 派生访问说明符的目的是控制派生类用户(包括派生类的派生类)对于基类成员的访问权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base{
protect:
int prot_mem;
};
class Sneaky: public Base{
friend void clobber(Sneaky&); //能访问Sneaky::prot_mem
friend void clobber(Base&); //不能访问Base::prot_mem
int j;
}
void clobber(Sneaky &s) {s.j = s.prot_mem = 0;} //正确可以访问
void clobber(Base &b) {b.prot_mem = 0;} //错误,不能访问

class Base{
public:
void pub_mem()
protected:
int prot_mem;
private:
char priv_mem;
};
struct Pub_Derv : public Base{
int f() {return prot_mem;}
char g() {return priv_mem;} //错误,不可访问
}
struct Priv_Derv : private Base{
// private不影响派生类的访问权限
int f() {return prot_mem;}
}
  • 派生类向基类转换的可访问性
    • 派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响
    • 假定D继承自B
      • 只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换
      • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的
      • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用
    • 对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的,反之则不行
  • 类的设计与受保护的成员
    • 普通用户:编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员
    • 实现者:负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有(实现)部分
    • 派生类:基类把它希望派生类能够使用的部分声明成受保护的。普通用户不能访问受保护的成员,而派生类及其友元仍旧不能访问私有成员
    • 和其他类一样,基类应该将其接口成员声明为公有的;同时将属于其实现的部分分成两组:一组可供派生类访问,另一组只能由基类及基类的友元访问。对于前者应该声明为受保护的,这样派生类就能在实现自己的功能时使用基类的这些操作和数据;对于后者应该声明为私有的
  • 友元与继承
    • 友元关系不能继承
    • 基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员
    • 当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效;对于原来的类,其友元的基类或派生类不具有特殊的访问能力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base{
// 添加friend声明,其他成员与之前的版本一致
friend class Pal; //Pal在访问Base的派生类时不具有特殊性
};
class Pal{
public:
int f(Base b) {return b.prot_mem;} //正确,Pal时Base的友元
int f2(Sneaky s) {return s.j;} //错误,Pal不是Sneaky的友元
// 对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此
int f3(Sneaky s) {return s_prot.mem;} //正确,Pal是Base的友元
// Pal是Base的友元,所以Pal能够访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况
}

// D2对Base的protected和privated成员不具有特殊的访问能力
class D2: public Pal{
public:
int mem(Base b) {return b.prot_mem;} //错误:友元关系不能继承
}
  • 改变个别成员的可访问性
    • 需要改变派生类继承的某个名字的访问级别,通过使用using声明可达到这一目的
      • using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定
      • 如果一条using声明语句出现在类的private部分,则该名字只能被类的成员和友元访问
      • 如果using声明语句位于public部分,则类的所有用户都能访问它
      • 如果using声明语句位于protected部分,则该名字对于成员、友元和派生类是可访问的
    • 派生类只能为那些它可以访问的名字提供using声明
  • 默认的继承保护机制
    • 默认派生运算符也由定义派生类所用的关键字来决定
    • 使用class关键字定义的派生类默认是私有继承的
    • 使用struct关键字定义的派生类是公有继承的
    • 建议显式声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base{
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
}
class Derived: private Base{
public:
// 保持对象尺寸相关的成员的访问级别
using Base::size;
protected:
using Base::n;
}
// 使用using声明语句改变了这些成员的可访问性。改变之后,Derived的用户将可以使用size成员,而Derived的派生类将能使用n

15.6 继承中的类作用域

  • 在编译时进行名字查找
    • 一个对象,引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致,但是能使用哪些成员还是由静态类型决定的
1
2
3
4
5
6
7
8
9
10
11
12
13
// 给Disc_quote添加一个新成员,该成员返回一个存有最小(或最大)数量及折扣价格的pair
class Disc_quote:public Quote{
public:
std::pair<size_t, double> discount_policy() const
{ return {quantity,discount};}
}

//只能通过Disc_quto及其派生类的对象,引用或指针使用discount_policy
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; //静态类型与动态类型一致
Quote *itemP = &bulk; //静态类型与动态类型不一致
bulkP->discount_policy(); //正确:bulkP的类型是Bulk_quote*
itemP->discount_policy(); //错误:itemP的类型是Quote*
  • 名字冲突与继承
    • 和其他作用域一致,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域的名字将隐藏定义在外层作用域的名字
    • 派生类的成员将隐藏同名的基类成员
  • 通过作用域运算符来使用隐藏的成员
    • 可以通过使用作用域运算符来使用一个被隐藏的基类成员
    • 除了覆盖继承而来的虚函数之外,派生类最好不要宠用其他定义在基类中的名字
  • 名字查找优先于类型检查
    • 定义派生类中的函数也不会重载其基类中的成员
    • 如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员(即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉)
  • 虚函数与作用域
    • 假如基类与派生类的虚函数接受的实参不同,则无法通过基类的引用或指针调用派生类的虚函数
  • 覆盖重载的函数
    • 和其他函数一样,成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例
    • 如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖
      • 一种好的解决方案是为重载的成员提供一条using声明语句,这样就无须覆盖基类中的每一个重载版本
      • using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义
      • 类内using声明的一般规则同样适用于重载函数的名字;基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Base{
Base():mem(0){}
protected:
int mem;
}
struct Derived: Base{
Derived(int i):mem(i){}
int get_mem() {return mem;}

// 使用隐藏的成员
int get_mem() {return Base::mem;}
protected:
int mem; //隐藏基类中的mem
}

Derived d(42);
d.get_mem(); //42

class Base{
public:
virtual int fcn();
};
class D1:public Base{
public:
int fcn(int); //形参列表与Base中的fcn不一致
virtual void f2(); //是一个新的虚函数,在Base中不存在
}
class D2:public D1{
public:
int fcn(int); //是一个非虚函数,隐藏了D1::fcn(int)
int fcn(); //覆盖了Base的虚函数fcn
void f2(); //覆盖了D1的虚函数f2
}

// 使用隐藏的虚函数的方法
Base bobj;D1 d1obj;D2 d2obj;

Base *bp1 = &bobj,*bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //虚调用,将在运行时调用Base::fcn
bp2->fcn(); //虚调用,将在运行时调用Base::fcn
bp3->fcn(); //虚调用,将在运行时调用D2::fcn

D1 *d1p = &d1obj;
D2 *d2p = &d2obj;
bp2->f2(); //错误,Base没有名为f2的成员
d1p->f2(); //虚调用,将在运行时调用D1::f2()
d2p->f2(); //虚调用,将在运行时调用D2::f2()

15.7 构造函数与拷贝控制

15.7.1 虚析构函数

  • 继承关系对基类拷贝控制最直接的影响时基类通常应该定义一个虚析构函数,能动态分配继承体系中的对象
    • 通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本
    • 虚析构函数的虚属性也会被继承
  • 虚析构函数将阻止合成移动操作:如果一个类定义了析构函数,即使通过=default的形式使用了合成版本,编译器也不会为这个类使用合成移动操作
1
2
3
4
5
6
7
8
9
class Quote{
public:
virtual ~Quote() = default; //动态绑定析构函数
}

Quote *itemP = new Quote;
delete itemP; //调用Quote的析构函数
itemP = new Bulk_quote;
delete itemP; //调用Bulk_quote的析构函数

15.7.2 合成拷贝控制与继承

  • 基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数,赋值运算符或析构函数类似
    • 对类本身的成员依次进行初始化,赋值或销毁的操作,此外合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化,赋值或销毁操作
  • 对于派生类的析构函数来说,除了销毁派生类自己的成员外,还负责销毁派生类的直接基类
  • 派生类中删除的拷贝控制与基类的关系
    • 基类或派生类能出于同样的原因将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数
    • 某些定义基类的方式也可能导致派生类成员称为被删除的函数
      • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作
      • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分
      • 和过去一样,编译器将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的
  • 移动操作与继承
    • 大多数基类都会定义一个虚析构函数,默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中没有合成的移动操作
    • 基类缺少移动操作会阻止派生类拥有自己的合成操作
    • 一旦基类定义了自己的移动操作,那么必须同时显示地定义拷贝操作

15.7.3 派生类的拷贝控制成员

  • 派生类的拷贝和移动构造函数在拷贝和移动自由成员的同时,也要拷贝和移动基类部分的成员
    • 析构函数只负责销毁派生类自己分配的资源,派生类对象的基类部分是被自动销毁的
  • 定义派生类的拷贝或移动拷贝函数
    • 使用基类的构造函数初始化对象的基类部分
    • 在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果需要拷贝或移动基类部分,则必须在派生类的构造函数初始化列表中显式地使用基类的拷贝或移动构造函数
  • 派生类赋值运算符
    • 派生类的赋值运算符必须显式的为其基类部分赋值
  • 在构造函数和析构函数中调用虚函数
    • 派生类对象的基类部分首先被构建,当执行基类的构造函数时,该对象的派生类部分还是未初始化的状态
    • 为了处理该未完成状态,当构建一个对象时,需要把对象的类和构造函数的类看作是同一个;对虚函数的调用绑定正好符合这种把对象的类和构造函数的类看成同一个的要求;
  • 如果构造函数或析构函数调用某个虚函数,应该执行与构造函数或析构函数所属类型对应的虚函数版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base{//....};
class D:public Base{
public:
D(const D& d):Base(d) //拷贝基类成员
D(D&& d):Base(std::move(d)) //移动基类成员
}
// 初始值Base(d)将一个D对象传递给基类构造函数
// 尽管从道理上来说,Base可以包含一个参数类型为D的构造函数,但是在实际编程过程中通常不会这么做
// 相反,Base(d)一般会匹配Base的拷贝构造函数。D类型的对象d将被绑定到该构造函数的Base&形参上。Base的拷贝构造函数负责将d的基类部分拷贝给要创建的对象

// 假如没有提供基类的初始值
D(const D& d) //成员初始值,但没有提供基类初始值
// Base的默认构造函数将被用来初始化D对象的基类部分。假定D的构造函数从d中拷贝了派生类成员,则这个新构建的对象的配置将非常奇怪:它的Base成员被赋予了默认值,而D成员的值则是从其他对象拷贝得来的

// Base::operator=(const BAse&)不会被自动调用
D &D::operator=(const D &rhs){
Base::operatr=(rhs); //为其基类部分赋值
return *this;
}

15.7.4 继承的构造函数

  • 派生类能重用其直接基类定义的构造函数
    • 一个类只能继承其直接基类的构造函数,类不能继承默认,拷贝和移动构造函数,若派生类未定义这些构造函数,则编译器将合成
    • 派生类继承基类构造函数使用using语句
  • 继承的构造函数的特点
    • 一个构造函数的using声明不会改变该构造函数的访问等级
    • 一个using声明语句不能指定explicit或constexpr
      • 若基类的构造函数时explicit或constexpr,则继承的构造函数仍保持相同属性
    • 当一个基类构造函数含有默认实参,则实参不会被继承,派生类获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参
1
2
3
4
5
class Bulk_quote:public Disc_quote{
public:
using Disc_quote::Disc_quote; //继承基类的构造函数
double new_price(std::size_t) const;
}

15.8 容器与继承

  • 使用容器存放继承体系中的对象,必须使用间接存储的方式
    • 因为不允许在容器中保存不同类型的元素,故不能将具有继承关系的多种类型的对象直接存放在容器中
  • 在容器中放置(智能)指针而非对象
    • 希望在容器中存放具有继承关系的对象时,实际上存放的通常是基类指针
      • 指针的动态类型指向的可能是基类或者派生类
1
2
3
4
5
6
7
8
9
10
vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1",50));
//正确,但只能把对象的Quote部分拷贝给basket
basket.push_back(Bulk_quote("0-201-82470-1",50,10,.25));

//basket是Quote对象,向其中添加Bulk_quote对象是,派生类部分会被忽略

vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-021-82470-1",50));
basket.push_back(make_shared<Bulk_quote>("0-021-82470-1",50,10,.25));

16 模板与泛型编程

16.1 定义模板

16.1.1 函数模板

  • 定义一个通用的函数模板,而不是为每个类型都定义一个新函数
    • 模板的定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分割的一个或多个模板参数的列表,用<>包围起来
    • 模板参数列表不能为空
    • 模板参数表示在类或函数定义中用到的类型或值,使用模板时,(隐式地或显示地)指定模板实参
  • 实例化函数模板
    • 调用函数模板时,编译器用函数实参来推断模板实参(编译器使用实参的类型来确定绑定到模板参数T的类型)
    • 编译器用推断出的模板参数来实例化一个特殊版本的函数
  • 模板类型参数
    • 可以将类型参数看作类型说明符
    • 类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换
  • 非类型模板参数
    • 通过特定的类型名而非关键字class或typename来指定非类型参数
    • 可以在模板中定义非类型参数,一个非类型参数表示一个值而非一个类型,非类型参数必须是常量表达式
    • 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用
      • 绑定到非类型整型参数的实参必须是一个常量表达式
      • 绑定到指针或引用非类型参数的实参必须具有静态的生存期
      • 不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参
      • 指针参数也可以用nullptr或一个值为0的常量表达式来实例化
  • inline和constexpr的函数模板
    • 函数模板可以声明称inline和constexpr,说明符放在模板参数列表之后,返回类型之前
  • 编写泛型代码的两个重要原则
    • 模板中的函数参数是const的引用
    • 函数体中的条件判断仅使用<比较运算符
  • 模板编译
    • 只有当实例化出模板的一个特定版本时,编译器才会生成代码
    • 函数模板和类模板成员函数的定义通常放在头文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// compare的模板版本
template<typename T>
int compare(const T &v1, const T &v2){
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}

cout << compare(1,0)<<endl; //T为int

vector<int> vec1{1,2,3};
vector<int> vec2{4,5,6};
cout << compare(vec1,vec2)<<endl; //T为vector<int>

template<typename T> T foo(T* p){
T tmp = *p;
//...
return tmp;
}

// 错误,U前面必须假声class或typename
template<typename T,U> T calc(const T&, const U&);
// 正确
template<typename T,class U> T calc(const T&, const U&);

template<unsigned N, unsigned M>
int compare(onst char(&p1)[N], const char (&p2)[M]){
return strcmp(p1,p2);
}
// 调用时会用字面常量的大小来代替N,M

template<typename T> inline T min(const T&,const T&);

16.1.2 类模板

  • 类模板是用来生成类的蓝图
    • 与函数模板不同之处是,编译器不能为类模板推断模板参数类型
  • 定义类模板
    • 类似函数模板,类模板以关键字template开始,后跟模板参数列表
    • 在类模板的定义中,将模板参数当作替身,代替使用模板时用户需要提供的类型或值
  • 实例化类模板
    • 使用类模板时,必须提供额外信息,额外信息是显式模板实参列表,其被绑定到模板参数使用模板实例化出特定的类
    • 一个类模板的每个实例都形成一个独立的类,两个独立的类不会关联,且不会对其他任何实例类的成员有特殊访问权限
  • 在模板作用域中引用模板类型
    • 类模板用来实例化类型,而一个实例化的类型总是包含模板参数
    • 一个类模板中的代码如果使用了另一个模板,通常不将一个实际类型的名字用作其模板实参。相反,通过将模板自己的参数当作被使用模板的实参
  • 类模板的成员函数
    • 既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数
    • 类模板的成员函数本身是一个函数,但类模板每个实例都有其自己版本的成员函数,因此类模板的成员函数具有和模板相同的模板参数
    • 定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表
  • 构造函数
    • 与其他任何定义在类模板外的成员一样,构造函数的定义要以模板参数开始
  • 类模板成员函数的实例化
    • 默认情况下,一个类模板的成员函数只有当程序用到它时才能进行实例化
    • 对于一个实例化的类模板,其成员只有在使用时才被实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
template<typename T> class Blob{
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;

Blob();
Blob(std::initializer<T> il);

size_type size() const {return data->size();}
bool empty() const {return data->empty();}

void push_back(const T &t) const {data->push_back(t);}
void push_back(T &&t) const {data->push_back(std::move(t));}
void pop_back;

T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data;
void check(size_type i, const std::string &msg) const;
}

// data使用两个模板,vector和shared_ptr
std::shared_ptr<std::vector<T>> data;

// 对于StrBlob的一个给定的成员函数
ret-type StrBlob::member-name(parm-list);
// 对应的Blob成员:
template<typename T> ret-type Blob<T>::member-name(parm-list)

// check成员,检查一个给定的索引
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
if(i>=data->size())
throw std::out_of_range(msg);
}

// 模板版本则返回一个引用,指向用来实例化Blob的类型
template <typename T>
T& Blob<T>::back()
{
check(0,"back on empty Blob");
return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
check(i,"subscipt out of range");
return (*data)[i];
}

// 模板版本则返回一个引用,指向用来实例化Blob的类型
template <typename T>
void Blob<T>::pop_back()
{
check(0,"pop_back on empty Blob");
return data->pop_back();
}

// 构造函数的定义要以模板参数开始
template <typename T> Blob<T>::Blob() :data(std::make_shared<std::vector<T>>()){}
  • 在类代码内简化模板类名的使用
    • 使用一个类模板类型时,必须提供模板实参
    • 在类模板自己的作用域中,可以使用模板名而不提供实参
    • 处于一个类模板的作用域中时,编译器处理模板自身引用时就好像已经提供了与模板参数匹配的实参一样
  • 在类模板外使用类模板名
    • 在类模板外定义其成员时,必须记住,并不在类的作用域中,直到遇到类名才表示进入类的作用域
    • 在一个类模板的作用域内,可以直接使用模板名而不必指定模板实参
  • 类模板和友元
    • 当一个类包含一个友元声明时,类与友元各自是否是模板相互无关
    • 如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例;如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例
  • 一对一友好关系
    • 类模板与另一个模板间友好关系的最常见形式是建立对应实例及其友元间的友好关系
  • 通用和特定的模板友好关系
    • 一个类也可以将另一个模板的每个实例都声明为自己的友元或限定特定的实例为友元
    • 为了让所有实例称为友元,友元声明中必须使用与类模板本身不同的模板参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 若试图访问一个不存在的元素,BlobPtr抛出一个异常
template<typename T> class BlobPtr{
public:
BlobPtr() : curr(0) {}
BlobPtr(Blob<T> &a, size_t sz = 0):
wptr(a.data),curr(sz) {}
T& operator*() const
{
auto p = check(curr, "deference past end");
return (*p)[curr];
}
// 处于一个类模板的作用域中时,编译器处理模板自身引用时就好像已经提供了与模板参数匹配的实参一样
// 等效于BlobPtr<T>& operator++();
BlobPtr& operator++();
BlobPtr& operator--();
private:
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;
}

// 后置:递增/递减对象但返回原址
template<typename T>
BlobPtr<T> BlobPtr<T>::operator++(int){
BlobPtr ret = *this;
++*this;
return ret;
}
// 在函数体内,已经进入类的作用域,在定义ret时无需重复模板参数
// 不提供模板参数,则编译器将假定使用的类型与成员实例化所用类型一致

// 一对一友好关系
// 前置声明,在Blob中声明友元所需要的
template<typename> class BlobPtr;
template<typename> class Blob;
template<typename T>
bool operator==(const Blob<T>&, const Blob<T>&);
template<typename T> class Blob{
// 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator==<T>
(const Blob<T>&, const Blob<T>&);
}

// 友元的声明用Blob的模板形参作为自己的模板实参
// 友好关系被限定在用相同实例化的Blob和BlobPtr相等的运算符中
Blob<char> ca;
Blob<int> ia;
// BlobPtr<char>的成员可以访问ca的非public部分
// 但ca对ia没有特殊访问权限

// 通用和特定的模板友好关系
// 前置声明,在将模板的一个特定实例声明为友元时会用到
template<typename T> class Pal;
class C{
firend class Pal<C>; //用类C实例化的Pal时C的一个友元
template <typename T> friend class Pal2; //Pal2的所有实例都是C的友元,无需前置声明
};
template<template T> class C2{
friend class Pal<T>; //Pal的模板声明必须在作用域之内
template<typename X> friend class Pal2; //Pal2的所有实例都是C2每个实例的友元,无需前置声明
friend class Pal3; //Pal3是一个非模板类,它是C2所有实例的友元
}
  • 令模板自己的类型参数成为友元
    • 在新标准中,可以将模板类型参数声明为友元
  • 模板类型别名
    • 新标准允许为类模板定义一个类型别名
  • 类模板的static成员
    • 类模板可以声明static成员,static成员函数只有在使用时才会实例化
    • 类模板的每个不同的实例都有其自己的static成员实例,类模板的相同类型的实例共享相同的成员实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 模板参数类型声明为友元
template<typename Type> class Bar{
friend Type;
//...
}
// 对于某个类型Foo,Foo将成为Bar<Foo>的友元

// 使用typedef定义模板类型别名
typedef Blob<string> StrBlob;

// 使用using定义模板类型别名
template<typename T> using twin = pair<T,T>;
twin<string> authors;

template<typename T> using partNo = pair<T,unsigned>;
partNo<string> books; //pair<string unsigned>

// 声明static成员
template<typename T> class Foo{
public:
static std::size_t count() {return ctr;}
private:
static std::size_t ctr;
};

16.1.3 模板参数

  • 模板参数的名字没有内在含义
  • 模板参数名的可用范围时在其声明之后,至模板声明或定义结束之前
  • 模板参数会隐藏外层作用域中声明的相同的名字,但在模板内部不能重用模板参数名
  • 模板声明
    • 与函数参数相同,声明中的模板参数的名字不必与定义中相同
  • 使用类的类型成员
    • 使用一个模板类型参数的类型参数,就必须显式告诉编译器该名字是一个类型
    • 通过使用关键字typename来实现,不能使用class
  • 默认模板实参
    • 在新标准中,可以为类模板和函数提供默认模板实参
  • 模板默认实参与类模板
    • 无论何时使用一个类模板,都必须在模板名之后接上尖括号,尖括号指出类必须从一个模板实例化而来
    • 特别是,如果一个类模板为其所有模板参数都提供了默认实参,且希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template<typename T> int compare(const T&, const T&);
template<typename T> class Blob;
// 声明但不定义compare和Blob
template<typename T> T calc(const T&, const T&);
template<typename U> U calc(const U&, const U&);
// 模板的定义
template<typename Type>
Type clac(const Type& a, const Type& b) {//...}

// 使用类的类型成员
template<typename T>
typename T::value_type top(const T& c){
if(!c.empty())
return c.back();
else
return typename T::value_type();
}

// 重写compare
template<typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F()){
if(f(v1,v2)) return -1;
if(f(v2,v1)) return 1;
return 0;
}
// 调用该模板时,可以自定义比较操作

template<class T = int> class Numbers{
public:
Numbers(T v = 0):val(v){}
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; //<>表示希望使用默认类型

16.1.4 成员模板

  • 一个类可以包含本身是模板的成员函数,这种成员成为成员模板
    • 成员模板不能是虚函数
  • 普通(非模板)类的成员函数
  • 类模板的成员模板
    • 可以为类模板定义成员模板,类和成员各自有自己独立的模板参数
    • 与类模板的普通成员函数不同,成员模板是函数模板
      • 在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表,类模板的参数列表在前,成员模板参数列表在后
  • 实例化与成员模板
    • 为了实例化一个类模板的成员模板,必须同时提供类和函数模板的实参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 作为普通类包含成员模板的例子,定义一个类,类似unique_ptr所使用的默认删除器类型
// 类似默认删除器,类将包含一个重载的函数调用运算符,它接受一个指针并对此指针执行delete
// 与默认删除器不同,类还将在删除器被执行时打印一条信息。由于希望删除器适用于任何类型,所以将调用运算符定义为一个模板

// 函数对象类,对给定指针执行delete
class DebugDelete{
public:
DebugDelete(std::ostream &s = std::cerr): os(s) { }
// 与任何函数模板相同,T的类型由编译器推断
template<typename T> void operator()(T *p) const
{
os << "deleting unique_ptr" << std::endl;
delete p;
}
private:
std::osream &os;
}

double *p = new double;
DebugDelete d;
d(p); //调用DebugDelete::operator()(double*),释放p
int* ip = new int;
DebugDelete()(ip); //在一个临时DebugDelete对象上调用operator()(int*)

// 将DebugDelete用作unique_ptr的删除器
// 为了重载unique_ptr的删除器,在尖括号内给出删除器类型,并提供一个这种类型的对象给unique_ptr的构造函数
// 销毁p指向的对象
unique_ptr<int, DebugDelete> p(new int, DebugDelete());

// 类模板的成员模板
template <typename T> class Blob{
template <typename It> Blob(It b, It e);
}

template<typename T> template<typename It> Blob<T>::Blob(It b,Ite ):
data(std::make_shared<std::vector<T>>(b,e)) {}

16.1.5 控制实例化

  • 当模板被使用时才会进行实例化,这意味着相同的实例可能出现在多个对象文件中,独立编译的源文件使用了相同的模板,模板参数相同时会有多个模板的实例
  • 可以使用显式实例化来避免多余的开销
    • 将一个实例化声明为extern表示承诺在程序其他位置有该实例化的一个非extern声明,但必须只有一个定义
    • 对于每个实例化声明,在程序中某个位置必须有其显式的实例化定义
1
2
3
4
5
6
extern template declaration;  //实例化声明
template declaration; //实例化定义
// declaration是一个类或函数声明,其中所有模板参数已被替换成模板实参

extern template class Blob<string>;
template int compare(const int&, const int&);

16.2 模板实参推断

  • 从函数实参来确定模板实参的过程称为模板实参推断

16.2.1 类型转换与模板类型参数

  • 与非模板函数一样,在一次调用中传递给函数模板的实参被用来初始化函数的形参
  • 如果函数形参的类型使用了模板类型参数,则采用特殊的初始化规则
  • 顶层const无论在形参中还是实参中,都会被忽略
  • 在其他类型转换中,能在调用中应用于函数模板的包括:
    • const转换:可以将一个非const对象的引用传递给一个const的引用
    • 数组和函数指针转换:若函数形参不是引用类型,则可以对数组或函数类型的实参引用正常的指针转换
    • 将实参传递给模板类型的函数形参是,能自动应用类型转换的只有const转换及数组或函数到指针的转换
1
2
3
4
5
6
7
8
9
10
11
12
// 考虑函数fobj和fref的调用
// fobj函数拷贝它的参数,而fref的参数是引用类型
template<typename T> T fobj(T, T); //实参被拷贝
template<typename T> T fref(const T&, const T&);
string s1("a value");
const string s2("another value");
fobj(s1,s2); //调用fobj(string,string);const被呼呼额
fref(s1,s2); //调用fobj(const string&,const string&);
//将s1转换为const是允许的
int a[10],b[42];
fobj(a,b); //调用f(int*,int*)
fref(a,b); //错误,数组类型不匹配
  • 使用相同模板参数类型的函数形参
    • 一个模板类型参数可以用作多个函数形参的类型
    • 由于至允许有限的集中类型转换,因此传递给形参的实参必须具有相同的类型
    • 若允许对函数实参进行正常的初始化,可以将函数模板定义成两个类型的参数
      • 且必须定义能比较这些类型的<运算符
  • 正常类型转换应用于普通函数实参
    • 函数模板可以有用普通类型定义的参数
      • 不涉及模板类型参数的类型,这种函数实参不进行特殊处理,正常转换为对应形参的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// compare函数接收两个const T&参数,其实参类型必须具有相同类型
compare(long, 1024); //不能实例化,参数类型不同

// 若允许对函数实参进行正常的初始化,可以将函数模板定义成两个类型的参数
template<typename A, typename B>
int compare(const A& v1, const B& v2)
{
if (v1<v2)
{
return -1;
}
if (v2<v1)
{
return 1;
}
return 1;
}

16.2.2 函数模板显式实参

  • 当函数返回类型与参数列表中任何类型都不相同是,编译器无法推断出模板实参的类型
  • 指定显式模板实参
    • 在调用时必须提供一个显式模板实参:显式实参在尖括号中给出,位于函数名之后,实参列表之前
    • 显式模板实参按由左至右的顺序与对应的模板参数匹配
  • 正常类型转换应用于显式指定的实参
    • 对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换
1
2
3
4
5
// 编译器无法推断
template<typename T1, typename T2, typename T3> T1 sum(T2, T3);

// 提供显式模板实参,显式指明T1,但T2,T3由实参类型推断
auto val3 = sum<long long>(i,lng);

16.2.3 尾置返回类型与类型转换

  • 显式模板实参表示模板函数的返回类型是很有效的,但在其他情况下,要求显式指定模板实参会添加额外的负担
  • 使用尾置返回类型,由于尾置返回类型出现在参数列表之后,可以使用函数的参数
  • 进行类型转换的标准库模板类
    • 对于传递的参数的类型,但直到唯一可以使用的操作是迭代器操作,但所有迭代器操作都不会生成元素,只能使用元素的引用
    • 为了获得元素类型,使用标准库的类型转换模板
      • 定义在头文件type_traits中,通常用于所谓的模板源程序设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用
template <typename It>
??? &fcn(It beg, It end){
//处理序列
return *beg;
}

// 并不知道返回结果的类型,但知道所需类型是所处理的序列的元素类型
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi","bye"};
auto &i = fcn(vi.begin(),vi.end()); //应该返回int&
auto &s = fcn(ca.begin(),ca.end()); //应该返回string&

// 尾置返回允许在参数列表之后声明返回类型
template <typename It>
auto &fcn(It beg, It end) -> decltype(*beg)
{
//处理序列
return *beg;
}

// 使用remove_reference来获得元素类型
remove_reference<decltype(*beg)>::type

// 组合使用
template <typename It>
auto &fcn(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
//处理序列
return *beg;
}
// type是一个类的成员,而该类依赖于一个模板参数,必须在返回类型的声明中使用typename来告知编译器,type表示一个类型
对Mod<T>,其中Mod为 T为 Mod<T>::type为
remove_reference X&或X&&,否则 X,否则为T
add_const X&,const X,或函数,否则 T,否则为const T
add_lvalue_reference X&,X&&,否则 T,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_extent X[n1][n2],否则 X,否则为T

16.2.4 函数指针和实参推断

  • 用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参
1
2
3
4
5
6
7
8
9
10
11
12
13
// 假定有一个函数指针,指向的函数返回int,接受两个参数,每个参数都是指向const int的引用
// 可以使用该指针指向compare的一个实例
template<typename T> int compare(const T&, const T&);
// pf1指向实例int compare(const int&,const int&)
int (*pf1)(const int&,const int&) = compare;

// 如果不能从函数指针类型确定模板实参,则产生错误
// func的重载版本:每个版本接受不同的函数指针类型
void func(int(*) (const string&, const string&));
void func(int(*) (const int&, const int&));
func(compare); //错误,使用混乱

func(compare<int>); //传递compare(const int&, const int&)

16.2.5 模板实参推断和引用

  • 从左值引用函数参数推断类型
    • 当一个函数参数是模板类型参数的普通(左值)引用时,只能传递一个左值,实参可以时const类型,也可以不是
      • 如果实参是const的,则T将被推断称const类型
      • 如果函数参数的类型是const T&,正常可以传递任何类型的实参(对象,临时对象或字面值)
      • 当函数参数本身是const时,T的类型推断结果不会是一个const类型
  • 从右值引用函数参数推断类型
    • 函数参数时右值引用时,可以传递右值,类型推断推断的应为右值
  • 引用折叠和右值引用参数
    • 例外规则,允许非正常的绑定
      • 第一条是:当将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型
      • 如果间接创建一个引用的引用,则引用形成折叠:引用会折叠称一个普通的左值引用类型
      • 只有右值引用的右值引用会折叠称右值引用
      • 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
      • 若函数参数是指向模板参数类型的右值引用,则可以传递任意类型的实参;若将左值传递,则函数实参被实例化成一个普通的左值引用
  • 右值引用常用于两种情况:模板转发其实参或模板被重载
  • 虽然不能隐式地将一个左值转换为右值引用,但可以用static_cast显式地将一个左值转换为一个右值引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T> void f1(T&);    //实参必须时一个左值
f1(i); //i是int,则T为int
f1(ci); //i是const int,则T为const int
f1(5); //错误,传递的实参必须是左值

template <typename T> void f2(const T&); //可以接受右值
f2(i); //i是int,则T为const int
f2(ci); //i是const int,则T为int
f2(5); //T为int

template <typename T> void f3(T&&);
f3(42); //T为int
f3(i); //实参为左值,T为int&
f3(ci); //实参为左值,T为const int&

// 编写接受右值引用参数的模板函数
template<typename T> void f3(T&& val){
T t = val;
t = fcn(t);
if(val == t){//...}
}

16.2.7 转发

  • 某些函数需要将其一个或多个实参连同类型不变地转发给其他函数
    • 需要保持被转发实参的所有性质,包括实参类型是否是const以及实参是左值还是右值
  • 定义能保持类型信息的函数参数
    • 通过将一个函数参数定义为一个指向模板类型参数的右值引用,可以保持其对应实参的所有类型信息
    • 使用引用参数,使得可以保持const属性,因为在引用类型中的const是底层的
    • 将函数参数定义为T1&&和T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性
    • 如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持
  • 在调用中使用std::forward保持类型信息
    • 使用名为forward的新标准库设施来传递flip2的参数,能保俶原始实参的类型
    • 定义在utility中,必须通过显式模板实参来调用,forward返回该显式实参类型的右值引用
    • 使用forward传递那些定义为模板类型参数的右值引用的函数参数
      • 通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 接受一个可调用对象和另外两个参数的模板
// 对翻转的参数调用给定的可调用对象
// flip1是一个不完整的实现:顶层const和引用丢失了
template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2){
f(t2,t1);
}

// 调用一个接受引用参数的函数时会出现问题
void f(int v1, int &v2){
cout<<v1<<" "<< ++v2<<endl;
// 改变了绑定到v2的值
}

f(42,i); //f改变了实参i
flip1(f,j,42); //通过flip1调用f不会改变j
// 问题在于j被传递给flip1的参数t1,此参数是一个普通的,非引用类型的int,而非int&
// 被实例化为
void flip1(void(*fcn)(int,int&),int t1,int t2);

// 定义能保持类型信息的函数参数
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2){
f(t2,t1);
}
// 解决一半问题,但不能接受右值引用参数的函数
void g(int &&i,int& j){
cout<<i<<" "<< j<<endl;
}
// 试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。即使传递一个右值给flip2
flip2(g,i,42); //错误,不能从一个左值实例化int&&
// 函数参数与其他任何变量一样,都是左值表达式
// 因此,flip2中对g的调用将传递给g的右值引用参数一个左值

// forward可以保持给定实参的左值/右值属性
template<typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2){
f(std::forward<T2>(t2),std::forward<T1>(t1));
}

16.3 重载与模板

  • 函数模板可以被另一个模板或一个普通非模板函数重载(名字相同的函数必须具有不同数量或类型的参数)
  • 当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本
  • 对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 编写重载模板
// 打印任何不能处理的类型
template <typename T> string debug_rep(const T &t){
ostringstream ret;
ret<<t;
return ret.str();
}
// 此函数可以用来生成一个对象对应的string表示,该对象可以是任意具备输出运算符的类型

// 定义打印指针的debug_rep版本
// 打印指针的值,后跟指针指向的对象
// 不能用于char*
// IO库为char*值定义了一个<<版本。此<<版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值
template<typename T> string debug_rep(T *p){
ostringstream ret;
ret <<"pointer: "<<p;
if(p)
ret<< " " <<debug_rep(*p);
else
ret<< " null pointer";
return ret.str; //返回ret绑定的string的一个副本
}

// 以下调用
const string *sp = &s;

// 两个模板都是可行的,而且两个都是精确匹配:
// debug_rep(const string*&),由第一个版本的debug_rep实例化而来,T被绑定到string*
// debug_rep(const string*),由第二个版本的debug_rep实例化而来,T被绑定到const string

// 解析为debug_rep()T*),即第二个版本
// 设计这条规则的原因是,没有它,将无法对一个const的指针调用指针版本的debug_rep
// 问题在于模板debug_rep(const T&)本质上可以用于任何类型,包括指针类型。此模板比debug_rep(T*)更通用,后者只能用于指针类型。没有这条规则,传递const的指针的调用永远是有歧义的

// C语言风格字符串指针和字符串字面常量
cout <<debug_rep("hi world!")<<endl; //调用debug_rep(T*)
// 对给定实参来说,两个模板都提供精确匹配
// 第二个模板需要进行一次(许可的)数组到指针的转换,而对于函数匹配来说,这种转换被认为是精确匹配
// 非模板版本是可行的,但需要进行一次用户定义的类型转换,因此它没有精确匹配那么好,所以两个模板成为可能调用的函数。与之前一样,T*版本更加特例化,编译器会选择它

16.4 可变参数模板

  • 一个可变参数模板就是一个接受可变数目参数的模板函数或模板类
    • 可变数目的参数被称为参数包
    • 存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数模板
    • 用省略号来指出一个模板参数或函数参数表示一个包
      • 在一个模板参数列表中class…或typename…指出接下来的参数表示零个或多个类型的列表;
      • 一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数列表
      • 在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包
  • sizeof运算符
    • 需要直到包中有多少元素时,可以使用sizeof…运算符,sizeof…返回一个常量表达式,且不会对其实参求值
1
2
3
4
5
6
7
8
// Args是一个模板参数包,rest是一个函数参数包
template<typename T, typename...Args>
void foo(const T &t, const Args &...rest);

template<typename ...Args> void g(Args ...args){
cout << sizeof...(Args) <<endl;
cout << sizeof...(args) <<endl;
}

16.4.1 编写可变参数函数模板

  • 可变参数函数通常是递归的
    • 第一步调用处理包中的第一个实参,然后用剩余实参调用自身
  • 当定义可变参数版本的函数时,非可变参数版本的声明必须在作用域中。否则可变参数版本会无限递归
1
2
3
4
5
6
7
8
9
10
11
12
// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数模板的print定义之前声明
template<typename T>
ostream &print(ostream &os, const T &t){
return os << t; //包中最后一个元素之后不打印分隔符
}
// 包中除了最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename ...Args>
ostream &print(ostream &os, const T &t, const Args&... rest){
os << t << ", ";
return print(os,rest); //递归调用,打印其他实参
}

16.4.2 包扩展

  • 对于一个参数包,除了获取其大小外,对它唯一能做的是扩展
    • 当扩展一个包是,还需要提供用于每个扩展元素的模式
    • 扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表
    • 在模式右边放一个省略号来触发扩展操作
  • 扩展中的模式会独立地应用于包中的每个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 对print做扩展操作
template <typename T, typename ...Args>
ostream &print(ostream &os, const T &t, const Args&... rest) //扩展Args
{
os << t << ", ";
return print(os,rest...); //扩展rest
}

// 在print调用中对每个实参调用debug_rep
template <typename ...Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
return print(os,debug_rep(rest)...);
}

// 问题在于debug_rep调用中扩展了rest,等价于
print(cerr,debug_rep(fcnName,code.num(),otherData,"otherData",item));
// 试图用了一个五个实参的列表来调用debug_rep,但并不存在与此调用匹配的debug_rep版本
// debug_rep函数不是可变参数的,且没有哪个debg_rep接受五个参数

16.4.3 转发参数包

  • 组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 将为StrVec类添加一个emplace_back成员
// 标准库容器的emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造一个元素
// 保持类型信息是一个两阶段的过程
// 首先为了保持实参中的类型信息,必须将emplace_back的函数定义为模板类型参数的右值引用
class StrVec
{
public:
template<class... Args> void emplace_back(Args&&...);
}
// 模板参数包扩展中的模式是&&,意味着每个函数参数将是一个指向其对应实参的右值引用
// 当emplace_back将实参传递给constrct时,必须使用forward来保持实参的原始类型
template<class... Args>
inline
void StrVec::emplace_back(Args&&... args){
chk_n_alloc(); //如果需要的话重新分配StrVec内存空间
alloc.construct(first_free++, std::forward<Args>(args)...);
}

// 假定svec是一个StrVec,如果调用
svec.emplace_back(10,'c'); //将cccccccccc添加新的尾元素
// construct调用中的模式会扩展出
std::forward<int>(10),std::forward<char>(c)

// 如果用右值调用emplace_back,则construct会得到一个右值
svec.emplace_back(s1+s2);
// 传递右值
std::forward<string>(string("the end"))
// forward<string>的结果类型是string&&,因此construct将得到一个右值引用实参
// construct会继续将此实参传递给string的移动构造函数来创建新元素

// 转发和可变参数模板
// func有零个或多个参数,每个参数都是一个模板参数类型的右值引用
template<typename... Args>
void fun(Args&&... args) //将Args扩展为一个右值引用
{
// work的实参即扩展Args又扩展args
work(std::forward<Args>(args)...);
}
// 由于fun的参数是右值引用,因此可以传递给它任意类型的实参
// 由于使用std::forward传递这些实参,因此它们的所有类型信息在调用work时都会得到保持

16.5 模板特例化

  • 编写单一模板,使之对任何可能的模板实参都是最合适的,都能实例化
    • 在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确
    • 当不能使用模板版本时,可定义类或函数模板得一个特例化版本
  • 定义函数模板特例化
    • 特例化一个函数模板时,必须为原模版中每个模板参数都提供实参
    • 为了指出正在实例化一个模板,应该使用关键字template后跟一对空尖括号,空尖括号指出将为原模版的所有模板参数提供实参
  • 函数重载与模板特例化
    • 特例化的本质是实例化一个模板,而非重载它。特例化不影响函数匹配
    • 将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配,
      • 函数匹配会选择疼特例化的版本
      • 当一个非模板函数提供与函数模板同样匹配好的情况下,编译器会选择非模板版本
  • 类模板特例化
    • 可以特例化类模板,必须在原模版定义所在的命名空间中特例化
  • 类模板部分特例化
    • 可指定一部分而非所有模板参数,或是参数的一部分而非全部特性
    • 一个类模板的部分特例化本身是一个模板,使用时用户必须为哪些在特例化版本中为指定的模板参数提供实参
    • 只能特例化类模板,不能部分特例化函数模板
  • 特例化成员而不是类:可以只特例化成员函数而不是特例化整个模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// compare函数的例子
// 第一个版本:可以比较任意两个类型
template<typename T> int cmpare(const T&, const T&);
// 第二个版本:处理字符串字面常量
template<size_t N, size_t M> int cmpare(const char (&)[N], const char (&)[M]);
// 只有当传递compare一个字符串字面常量或一个数组时,编译器才会调用接受两个非类型模板参数的模板
const char *p1 = "hi",*p2 = "mom";
compare(p1, p2); //调用第一个模板
compare("hi","mom"); //调用第二个模板
// 无法将一个指针转换为一个数组的引用
// 为了处理字符指针,可以为第一个版本的compare定义一个模板特例化版本
// 一个特例化版本就是模板的一个独立地定义,在其中一个或多个模板被指定为特定的类型

// compare的特殊版本,处理字符数组的指针
template<>
int compare(const char* const &p1, const char* const &p2){
return strcmp(p1,p2);
}
// 当定义一个特殊化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配
// 希望定义此函数的一个特例化版本,其中T为const char*
// 函数要求一个指向此类型const版本的引用。一个指针类型的const版本是一个常量指针而不是指向const类型的指针
// 需要在特例化版本中使用的类型是const char * const &,即一个指向const char的const指针的引用

// 打开std命名空间,以便特例化std::hash
namespace std
{

} //关闭std命名空间;注意右花括号之后没有分号

// 打开std命名空间,以便特例化std::hash
namespace std
{
template <> //特例化版本,模板参数为Sales_data
struct hash<Sales_data>{
typedef size_t resule_type;
typedef Sales_data argument_type;
size_t operator() (const Sales_data& s) const;
};
size_t hash<Sales_data>::operator() (const Sales_data& s) const
{
return hash<string>() (s.bookNo) ^
hash<unsigned>() (s.units_sold) ^
hash<double>() (s.revenue);
}
} //关闭std命名空间;注意右花括号之后没有分号

template <class T> class std::hash; //友元声明所需要的
class Sales_data{
friend class std::hash<Sales_data>;
}
// 指出特殊实例hash<Sales_data>是Sales_data的友元
// 由于此实例定义在std命名空间中,必须在友元声明中使用std::hash

// 特例化成员
template <typename T> struct Foo{
Foo(const T &t = T()) : mem(t){}
void Bar() {//...}
T mem;
};
template<> //正在特例化模板
void Foo<int>::Bar(){
// 特例化Foo<int>的成员Bar
// 进行应用于int的特例化处理
}

17 标准库特殊设施

17.1 tuple类型

  • tuple类似pair的模板,一个tuple可以有任意数量的成员,每个确定的tuple类型的成员数目是固定的,但一个tuple类型的成员数目可以与另一个tuple类型不同
  • 当希望将一些数据组合成成单一对象,但不想重新定义一个数据结构来表示数据时,tuple为较好的选择
    • 定义在tuple头文件中
tuple支持的操作 -
tuple<T1,T2,…,Tn> t t为n个不同成员类型的tuple
tuple<T1,T2,…,Tn> t(v1,v2,…vn) t为n个不同成员类型初始化了的tuple,次构造函数时explicit
make_tuple(v1,v2,…vn) 返回一个初始值初始化的tuple
t1 == t2 当两个tuple具有相同数量的成员且成员对应相等时,两个相等
t1 != t2 当两个tuple具有相同数量的成员且成员对应相等时,两个相等
t1 relop t2 关系运算符,使用字典序进行
get<i>(t) 返回t第i个成员的引用,若t是一个左值,结果为左值,否则为右值
tuple_size<tupleType>::value 一个类模板,通过tuple类型初始化
tuple_element<i,tupleType>::type 一个类模板,通过一个整型常量和一个tuple类型来初始化,有一个type的成员,表示给定tuple类型中指定成员的类型

17.1.1 定义和初始化tuple

  • 定义tuple时,需要指出每个成员的类型
    • 构造函数是explicit,必须使用直接初始化语法
1
2
3
4
5
6
7
8
tuple<size_t,size_t,size_t> threeD;
tuple<string, vector<double>, int, list<int>> = someVal("constants",{3.14,2.718},42,{0,1,2,3,4,5})

// 直接初始化
tuple<size_t,size_t,size_t> threeD{1,2,3};

// make_tuple
auto item = make_tuple("0-99-78345-X",3,20.00);
  • 访问tuple成员
    • 使用get标准库函数模板,并指定显式模板实参(传递tuple对象,返回指定成员的引用)
    • 尖括号的值必须是整型常量表达式
    • ==不知道tuple准确的类型细节时,使用辅助类进行查询
1
2
3
4
5
6
auto book = get<0>(item);
get<2>(item) *= 0.8;

typedef decltype(item) trans; //trans是item的类型
size_t sz = tuple_size<trans>::value; //返回trans类型对象中成员的数量
tuple_element<1,trans>::type cnt = get<1>(item);

17.1.2 使用tuple返回多个值

  • 最常见的用途是从一个函数返回多个值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// matches有三个成员:一家书店的索引和两个指向书店vector中元素的迭代器
typedef tuple<vector<Sales_data>::size_type,
vector<Sales_data>::const_iterator,
vector<Sales_data>::const_iterator> matches;
// files保存每家书店的销售记录
// findBook返回一个vector,每家销售了给定书籍的书店在其中都有一项
vector<matches>
findBook(const vector<vector<Sales_data>> &files, const string &book){
vector<matches> ret;
for(auto it = files.cbegin();it!=files.cend();++it){
auto found = equal_range(it->cbegin(),it->cend(),book,compareIsbn);
if(found.first != found.second)
ret.push_back(make_tuple(it - files.cbegin(),found.first,found.second));
}
return ret;
}
// equal_range的标准库算法:前两个实参是表示输入序列的迭代器(参见10.1节,第336页),第三个参数是一个值。默认情况下,equal_range使用<运算符来比较元素,Sales_data没有<运算符,则需要传递函数指针
// 返回迭代器表示pair表示元素范围

// 打印汇总销售信息
void reportResults(istream &in, ostream &os, const vector<vector<Sales_data>> &files){
string s;
while(in>>s){
auto trans = findBook(files,s);
if(trans.empty()){
cout << s << " not found in any stores" << endl;
continue;
}
for(const auto &store : trans)
os << "store " << get<0>(store) << " sales: "<< accumualte(get<1>(store),get<2>(store),Sales_data(s))<<endl;
}
}

17.2 bitset类型

  • bitset类,可以使位运算更加容易,且能处理超过最长整数类型大小的位集合
    • 定义在bitset头文件中

17.2.1 定义和初始化bitset

初始化bitset的方法 -
bitset<n> b; b有n位,每一位均为0,此构造函数时constexpr
bitset<n> b(u); b有unsigned long long值u的低n位的拷贝
bitset<n> b(s,pos,m,zero,one); b是string s从pos开始m个字符的拷贝;s只能包含字符zero或one
bitset<n> b(cp,pos,m,zero,one); 同上,cp指向字符数组中拷贝字符
- 接受一个string或字符指针的构造函数时explicit
1
2
3
4
5
6
7
8
9
10
// unsigned初始化
bitset<13> bitvec1(oxbeef);

// string初始化
bitset<32> bitvec2("1100");

// string的子串进行初始化
string str("111111110000000011001101");
bitset<32> bitset5(str,5,4);
bitset<32> bitset5(str,str.size()-4);

17.2.2 bitset操作

bitset操作 -
b.any() b中是否含有置位的二进制位
b.all() b中所有位都置位了吗
b.none() b中不存在置位的二进制位吗
b.count() b中置位的位数
b.size() b中的位数,constexpr函数
b.test(pos) 判断pos位是否置位
b.set(pos,v) 将pos处的位设置位bool值v
b.set() 将b中所有位置位
b.reset(pos) 将位置pos处的位复位
b.reset() 将b中所有的位复位
b.flip(pos) 改变pos处的位状态
b.flip() 改变每一位的状态
b[pos] 访问b中位置pos处的位
b.to_ulong() 返回一个unsigned long或unsigned long long值,其位模式与b相同
b.to_ullong() 返回一个unsigned long或unsigned long long值,其位模式与b相同
b.to_string(zero,one) 返回一个string,表示b中的位模式,zero和one的默认值分为0和1表示b中的0和1
os << b 将b中二进制位打印为字符1或0,打印到流os
is >> b 从is读取字符并存入b

17.4 正则表达式

  • 是一种描述字符序列的方法,是一种强大的计算工具
  • 使用C++正则表达式库(RE库),定义在头文件regex中,包含多个组件
正则表达式库组件 -
regex 表示有一个正则表达式的类
regex_match 将一个字符序列与一个正则表达式匹配
regex_search 寻找第一个与正则表达式匹配的子序列
regex_replace 使用给定格式替换一个正则表达式
sregex_iterator 迭代器适配器,使用regex_search遍历一个string中所有匹配的子串
smatch 容器类,保存在string中的搜索结果
ssub_match string中匹配的子串表达式的结果
  • regex类表示一个正则表达式
    • 函数regex_match和regexsearch确定了一个给定字符序列与一个给定regex是否匹配
    • regex_search和regex_match的参数
      • (seq,m,r,mft) /(seq,r,mft)
      • 在字符序列seq中查找regex对象r中的正则表达式,seq可以是一个string,表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针,m是一个match对象,用来报讯匹配结果的相关细节,m和seq必须具有兼容的类型
      • mft是一个可选的regex_constants::match_flag_type值

17.3.1 使用正则表达式库

1
2
3
4
5
6
7
8
9
10
11
12
13
// 查找拼写规则,i除非在c之后,否则必须在e之前
// 查找不在字符c之后的字符串ei
string pattern("[^c]ei");
// 包含pattern的整个单词
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern); //构造一个用于查找模式的regex
smatch result; //定义一个对象保存搜索结果
// 定义一个string保存与模式匹配和不匹配的文本
string test_str = "receipt friend theif receive";
// 用r在test_str中查找与pattern匹配的子串
if(regex_search(test_str,result,r))
cout << result.str() << endl;

regex(和wregex)选项 -
regex r(re) re时一个正则表达式
regex r(re,f) re时一个正则表达式,f是指出对象如何处理的标志
r1 = re 将r1中的正则表达式替换成re
r1.assign(re,f) 与=效果相同
r1.mark_count() r中子表达式的数目
r1.flags() 返回r的标志集
定义regex时指定的标志 定义在regex和regex_constans::syntax_option_type中
icase 在匹配过程中忽略大小写
nosubs 不保存匹配的子表达式
optimize 执行速度有限
ECMAScript 使用ECMAScipt指定的语法
basic 使用POSIX基本的正则表达式语法
extened 使用POSIX扩展的正则表达式语法
awk 使用POSIX版本的awk正则表达式语法
grep 使用POSIX版本的grep正则表达式语法
egrep 使用POSIX版本的egrep正则表达式语法
  • 指定或使用正则表达式时的错误
    • 正则表达式是在运行时,当一个regex对象被初始化或被赋予了一个新模式时,才会被编译
    • 正则表达式在运行时解析是否正确
    • 错误时,抛出regex_error的异常,what操作描述什么错误,code成员返回错误类型对应的数值编码
正则表达式库类 -
输入类型 使用正则表达式类
string regex,smatch,ssub_match和sregex_iterator
const char* regex,cmatch,csub_match和cregex_iterator
wstring wregex,wsmatch,wssub_match和wsregex_iterator
const wchar_t* wregex,wcmatch,wcsub_match和wcregex_iterator

17.3.2 匹配与Regex迭代器类型

sregex_iterator操作 同样适用cregex_iterator,wsregex_iterator,wcregex_iterator
sregex_iterator 一个sregex_iterator,遍历迭代器b和e表示的string
it(b,e,r) 调用sregex_search(b,e,r)将it定位到输入中第一个匹配的位置
sregex_iterator end; 尾后迭代器
*it 返回一个smatch的对象的引用或指向stmatch对象的指针
it-> 返回一个smatch的对象的引用或指向stmatch对象的指针
++it 前置返回递增后迭代器
it++ 后置返回旧值
it1==it2 相等比较
it1!=it2 相等比较
  • 当将一个sregex_iterator绑定到一个string和一个regex对象时,迭代器自动定位到给定string中第一个匹配位置
    • 即,sregex_iterator构造函数对给定string和regex调用regex_search
  • 当解引用迭代器时,会得到一个对应最近一次搜索结果的smatch对象
  • 当递增迭代器时,它调用regex_search在输入string中查找下一个匹配
smatch操作 -
m.ready() 若已经通过调用regex_search或regex_match设置了m,则返回true
m.size() 匹配失败返回0,否则返回最近一次匹配的正则表达式中子表达式的数目
m.empty() 若m.size()为0,返回true
m.prefix() ssub_match对象,表示当前匹配之前的序列
m.suffix() ssub_match对象,表示当前匹配之后的序列
m.format() -
m.length(n) 第n个匹配的子表达式的大小
m.position(n) 第n个子表达式距离序列开始的距离
m.str(n) 第n个子表达式匹配的string
m[n] 对应第n个子表达式的ssub_match地想
m.begin(),m.end() 迭代器
m.cbegin(),m.cend() 迭代器

17.3.3 使用子表达式

  • 正则表达式中的模式通常包含一个或多个子表达式
    • 一个子表达式是模式的一部分,本身也具有意义。正则表达式语法通常用括号表示子表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 电话号码匹配
string phone = "(\\()?(\\d{3})(\\))?([-.])?(\\d{3})([-.]?)(\\d{4})";
// 逐个剥离(括号包围的)子表达式:1.(\\()?表示区号部分可选的左括号;2.(\\d{3})表示区号;3.(\\))?表示区号部分可选的右括号;4.([-.])?表示区号部分可选的分隔符;5.(\\d{3})表示号码的下三位数字;6.([-.])?表示可选的分隔符;7.(\\d{4})表示号码的最后四位数字
regex r(phone);
smatch m;
string s;
while(getline(cin,s)){
for(sregex_iterator it(s.begin(),s.end(),r),end_it;it!end_it;++it)
if(valid(*it))
cout << "valid: " << it->str() << endl;
else
cout << "not valid: " << it->str() << endl;
}

// pattern有七个子表达式,每个smatch对象会包含八个ssub_match元素。位置[0]的元素表示整个匹配;元素[1]…[7]表示每个对应的子表达式
// 已经有一个完整的匹配,但不知道每个可选的子表达式是否是匹配的一部分
// 如果一个子表达式是完整匹配的一部分,则其对应的ssub_match对象的matched成员为true

// 在一个合法的电话号码中,区号要么是完整括号包围的,要么完全没有括号。因此,valid要做什么工作依赖于号码是否以一个括号开始
bool valid(const smatch& m){
// 如果区号前有一个左括号
if(m[1].matched)
// 则区号后必须有一个右括号,之后紧跟剩余号码或一个空格
return m[3].matched && (m[4].matched == 0 || m[4].str() == " ");
else
return !m[3].matched && m[4].str() == m[6].str();
}

17.3.4 使用regex_replace

  • 希望在输入序列中查找并替换一个正则表达式时,使用regex_replace
    • 接受一个输入序列,一个regex对象,和一个输出形式的字符串
正则表达式替换操作 -
m.format(dest,fmt,mft) 使用格式字符串 fmt 生成格式化输出,匹配在m中,可选的match_flag_type标志在mft中.第一个版本写入迭代器dest指向的目的位置并接受fmt参数,可以是string,也可以是表示字符数组中范围的一对指针
m.format(fmt,mft) 第二个版本返回string,保存输出,并接受fmt参数,可以是 一个string, 也可以是指向空字符结尾的字符数组的指针.mft的默认值为format_default
regex_replace(dest,seq,r,fmt,mft) 遍历seq,用regex_search查找与regex对象r匹配的子串
regex_replace(seq,r,fmt,mft) 遍历seq,用regex_search查找与regex对象r匹配的子串
匹配标志 定义在regex_constans::match_flag_type中
match_default 等价于format_default
match_not_bol 不将首字符作为行首处理
match_not_eol 不将尾字符作为行首处理
match_not_bow 不将首字符作为单词首处理
match_not_eow 不将尾字符作为单词首处理
match_any 如果存在对于一个匹配,则可以返回任意一个匹配
match_not_null 不匹配任何空字符
match_continuous 匹配必须从输入的首字符开始
match_prev_avail 输入序列包含第一个匹配之前的内容
format_default 用ECMAScript规则替换字符串
format_sed 用POSIX sed规则替换字符串
format_no_copy 不输出输入序列中未匹配的部分
format_first_only 只替换子表达式的第一次出现

17.4 随机数

  • 头文件random中的随机数库通过一组协作式的类来解决随机数的范围,类型或分布问题
    • 引入随机数引擎类和随机数分布类
    • 一个引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的在给定范围内的服从特定概率分布的随机数
    • C++中应该使用default_random_engine类和恰当的分布类对象

17.4.1 随机数引擎和分布

  • 随机数引擎时函数对象类,定义了一个调用运算符,不接受参数返回一个随机unsigned整数
  • 每次程序运行都会生成不同的随机结果,可以通过种子来实现
    • 种子时一个数值,引擎可以利用它从序列中一个新位置重新开始生成随机数
    • 创建种子的两种方式
      • 在创建引擎对象时提供种子
      • 调用引擎的seed成员
随机数引擎操作 -
Engine e 默认构造函数,使用该引擎类型默认的种子
Engine e(s); 使用整数值s作为种子
e.seed(s); 使用种子s重置引擎的状态
e.min(); 引擎可生成的最小值
e.max(); 引擎可生成的最大值
Engine::result_type 此引擎生成的unsigned整数类型
e.discard(u) 此引擎推进u步;u的类型尾unsigned long long
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
default_random_engine e;  //生成随机无符号数
for(size_t i = 0;i<10;++i)
cout << e() << " ";

// 生成0到9之间均匀分布的随机数
uniform_int_distribution<unsigned> u(0,9);
default_random_engine e;
for(size_t i = 0;i<10;++i>)
cout << u(e) << " ";

default_random_engine e1; //默认种子
default_random_engine e2(2147483646); //使用给定的种子
// e3和e4将生成相同的序列,使用相同的种子
default_random_engine e3;
e3.seed(32767);
default_random_engine e4(32767);
for(size_t i = 0; i!=100;++i){
if(e1() == e2)
cout << i<<endl;
if(e3() == e4)
cout << i<<endl;
}

17.4.2 其他随机数分布

分布类型的操作 -
Dist d 默认构造函数,分布类型的构造函数时explicit
d(e) 使用相同的e连续调用d扽话,会根据d的分布式类型生成一个随机数序列;e随机数引擎对象
d.min() 最小值
d.max() 最大值
d.reset() 重建d的状态
  • 一个分布不接受模板参数,即bernoulli_distribution,因为它是一个普通类,而非模板
    • 此分布总是返回一个bool值。它返回true的概率是一个常数,此概率的默认值是0.5
  • 由于引擎返回相同的随机数序列,所以必须在循环外声明引擎对象
    • 否则,每步循环都会创建一个新引擎,从而每步循环都会生成相同的值
    • 类似的,分布对象也要保持状态,因此也应该在循环外定义
1
2
// 定义0-1的随机均匀分布
uniform_real_distribution<double> u(0,1);

17.5 IO库再探

17.5.1 格式化输入与输出

  • 格式状态控制格式化的某些方面,如整数值是几进制,浮点值得精度,一个输出元素的宽度
  • 操纵符来修改流的格式状态
    • 一个操纵符是一个函数或是一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象
  • 当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效
  • 改变布尔值的格式
    • boolalpha操纵符覆盖格式,输出true和false
    • noboolalpha取消cout格式状态的改变
  • 指定整数值的进制
    • 使用hex,oct和dec将其改成十六进制,八进制或十进制输出
    • 浮点数不受影响
  • 在输出中指出进制
    • 需要打印八进制或十六进制,使用showbase操纵符,遵循与整数常量中指定进制相同的规范
    • 使用noshowbase恢复,不再显示整数值的进制
  • 控制浮点数格式
    • 三种格式:
      • 以多高精度(多少个数字)打印浮点值
      • 数值是打印为十六进制、定点十进制还是科学记数法形式
      • 对于没有小数部分的浮点值是否打印小数点
    • 默认情况下是六位数精度打印,
  • 指定打印精度
    • 调用IO对象的precision成员使用setprecision操纵符改变精度
      • 定义在iomanip中
      • precision成员是重载的,接受一个int值将其设为精度,并返回旧精度,不接受参数返回当前精度值
      • setprecision接受一个参数,用来设置精度
  • 指定浮点数计数法
    • scientific改变流的状态使用科学计数法
    • fixed改变流的状态使用定点十进制
    • 新标准库中的函数
      • hexfloat强制浮点数使用十六进制
      • defaultfloat恢复至默认状态
    • 精度值控制的是小数点后的数字位数,默认情况下控制的是数字总位数
  • 打印小数点
    • showpoint操纵符强制打印小数点
    • noshowpoint恢复默认行为
  • 输出补白
    • setw 指定下一个数字或字符串值得最小空间
    • left 表示左对齐输出
    • right 表示右对齐输出(默认格式
    • internal 控制负号符号得位置
    • setfill 允许指定一个字符代替默认的空格来补白输出
  • 定义在iomanip中的操纵符
    • setfill(ch) 用ch填充空白
    • setprecision(n) 将浮点精度设置为n
    • setw(w) 读或写值得宽度为w个字符
    • setbase(b) 将整数输出为b进制
  • 控制输入格式
    • noskipws 输入序列读取空白符,而不是跳过
    • skipws 恢复

17.5.2 未格式化的输入/输出操作

  • 低层操作,支持未格式化IO,允许将流仿作一个无解释的字节序列处理
  • 三种方法退回字符,有着细微的差别:
    • peek返回输入流中下一个字符的副本,但不会将它从流中删除,peek返回的值仍然留在流中
    • unget使得输入流向后移动,从而最后读取的值又回到流中。即使不知道最后从流中读取什么值,仍然可以调用unget
    • putback是更特殊版本的unget:它退回从流中读取的最后一个值,但它接受一个参数,此参数必须与最后读取的值相同
  • 函数peek和无参的get版本都以int类型从输入流返回一个字符
    • 函数返回一个int的原因是:可以返回文件尾标记,使用char范围中的每个值来表示一个真实字符,因此,取值范围中没有额外的值可以用来表示文件尾
    • 返回int的函数将它们要返回的字符先转换为unsigned char,然后再将结果提升到int。因此,即使字符集中有字符映射到负值,这些操作返回的int也是正值。而标准库使用负值表示文件尾可以保证与任何合法字符的值都不同
    • 头文件cstdio定义了一个名为EOF的const,可以用它来检测从get返回的值是否是文件尾,而不必记忆表示文件尾的实际数值
  • get将分隔符留作istream中的下一个字符,而getline则读取并丢弃分隔符
    • 无论哪个函数都不会将分隔符保存在sink中
  • 确定读取了多少个字符
    • 某些操作从输入读取未知个数的字节。可以调用gcount来确定最后一个未格式化输入操作读取了多少个字符。应该在任何后续未格式化输入操作之前调用gcount
    • 特别是,将字符退回流的单字符操作也属于未格式化输入操作。如果在调用gcount之前调用了peek、unget或putback,则gcount的返回值为0
单字节低层IO操作 -
is.get(ch) 从istream is读取下一个字节存入字符ch中返回is
os.put(ch) 将字符ch输出到ostream os,返回os
is.get() 将is的下一个字节作为int返回
is.putback(ch) 将字符ch放回is,返回is
is.unget() 将is向后移动一个字节,返回is
is.peek() 将下一个字节作为int返回,但不从流中删除
多字节低层IO操作 -
is.get(sink,size,delim) 从is中读取最多size个字节,保存在起始地址为sink的字符数组中,若读取到字符delim或读取了size个字节或文件尾时停止,若遇到了delim,则将其留在输入流中,不读取出来存入sink
is.getline(sink,size,delim) 与get相似,但会读取并舍弃delim
is.read(sink,size) 读取最多size个字节,存入字符数组sink中,返回is
is.gcount() 返回上一个未格式化读取操作从is读取的字节数
os.write(source, size) 将字符数组source中size个字节写入os,返回os
is.ignore(size, delim) 读取并忽略最多size个字符,包括delim

17.5.3 流随机访问

  • 为了支持随机访问,提供了两个函数,一个函数通过将标记seek到一个给定位置来重定位它;另一个函数tell我们标记的当前位置
  • 从逻辑上讲,只能对istream和派生自istream的类型ifstream和istringstream使用g版本,同样只能对ostream和派生自ostream的类型ofstream和ostringstream使用p版本;一个iostream、fstream或stringstream既能读又能写关联的流,因此对这些类型的对象既能使用g版本又能使用p版本
  • 由于只有单一的标记,因此只要在读写操作间切换,就必须进行seek操作来重定位标记
seek和tell函数 -
tellg() 返回一个输入流标记的当前位置
tellp() 返回一个输出流标记的当前位置
seekg(pos) 在输入流将标记重新定位到给定的绝对位置,pos通常是前一个tellg或tellp返回的值
seekp(pos) 在输出流将标记重新定位到给定的绝对位置,pos通常是前一个tellg或tellp返回的值
seekg(off, from) 在一个输入流中将标记当味道from之前或之后off个字符
seekp(off, from) 在一个输出流中将标记当味道from之前或之后off个字符

18 用于大型程序的工具

18.1 异常处理

18.1.1 抛出异常

  • C++语言中,通过抛出一条表达式来引发一个异常
    • 被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码被用来处理该异常
    • 被选中的处理代码是在调用链中与对象类型匹配的最近的处理代码
  • 栈展开
    • 栈展开过程:
      • 当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。当throw出现在一个try语句块内时,检查与该try块关联的catch子句
      • 如果找到了匹配的catch,就使用该catch处理异常
      • 如果这一步没找到匹配的catch且该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句
      • 如果还是找不到匹配的catch,则退出当前的函数,在调用当前函数的外层函数中继续寻找
    • 栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句为止;或者没有皮皮额的catch语句,程序调用标准库函数terminate,终止程序执行
  • 栈展开过程中对象被自动销毁
    • 在栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正常地销毁
    • 如果异常发生在构造函数中,则需要确保已构造地成员能被正常地销毁
  • 析构函数与异常
    • 若使用类来控制资源地分配,就能确保无论函数正常结束还是异常时,资源都能被正确释放
    • 析构函数不应该抛出自身不能处理地异常
      • 析构函数需要执行某个可能抛出异常地操作,则操作应该被放置在一个try语句块中,并且在析构函数内部得到处理
    • 在栈展开的过程中,运行类类型的局部对象的析构函数
      • 析构函数自动执行,不应该抛出异常。一旦析构函数抛出异常,且析构函数自身没能捕捉到该异常,则程序将被终止
  • 异常对象
    • 异常对象位于编译器管理的空间中,当异常处理完毕后,异常对象被销毁
    • 抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型
      • 如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出

18.1.2 捕获异常

  • 当进入一个catch语句之后,通过异常对象初始化异常声明中的参数,通过异常初始化异常声明中的参数
    • 和函数的参数类似,若catch的参数类型是非引用类型,则该参数是异常对象的一个副本,在catch语句内改变该参数实际上改变的是局部副本而非异常对象本身
    • 如果catch的参数是基类类型,可以使用其派生类类型的异常对象对其进行初始化
      • 若catch的参数是非引用,则异常对象将被且一部分
    • 异常声明的静态类型将决定catch语句所能执行的操作,若catch的参数是基类类型,则catch无法使用派生类特有的任何成员
  • 查找匹配的处理代码
    • 搜寻catch语句时,找到的catch语句是第一个异常匹配的语句
    • 当程序使用具有继承关系的多个异常时必须对catch语句的顺序进行组织和管理,使得派生类异常的处理代码出现在基类异常的处理代码之前
    • 在匹配时允许的转换
      • 允许从非常量向常量的类型转换,也就是说,一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句
      • 允许从派生类向基类的类型转换
      • 数组被转换成指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针
    • 应该把继承链最底层的类放在前面,而将继承链最顶端的类放在后面
  • 重新抛出
    • 一条catch语句通过重新抛出的操作将异常传递给另一个catch语句
    • 重新抛出使用throw语句,但不包含任何表达式
      • 一个重新抛出语句并不指定新的表达式,而是将当前异常对象沿调用链向上传递
  • 捕获所有异常的处理代码
    • 一次性捕获所有异常,使用省略号作为异常声明,这样的处理代码称为捕获所有异常的代码(catch…)
    • 一条捕获所有异常的语句可以与任意类型的异常匹配
    • 如果catch(…)与其他几个catch语句一起出现,则catch(…)必须在最后的位置
      • 出现在捕获所有异常语句后面的catch语句将永远不会被匹配
1
2
3
4
5
6
7
8
void mainp(){
try{

}catch(...){
// 处理异常的某些特殊操作
throw;
}
}

18.1.3 函数try语句块与构造函数

  • 构造函数在进入其函数体之前首先初始化列表,在初始化列表抛出异常时构造函数体内的try语句块未生效
    • 构造函数体内的catch语句无法处理构造函数初始化列表抛出的异常
    • 将构造函数写成函数try语句块
      • 函数try语句块使得一组catch语句既能处理构造函数体,也能处理构造函数的初始化过程
1
2
3
4
5
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) try:
data(std::make_shared<std::vector<T>> il){
//空函数体
}catch(const std::bad_alloc &e) {handle_out_of_memory(e);}

18.1.4 noexcept异常说明

  • 通过提供noexcept说明指定某个函数不会抛出异常
    • 关键字noexcept紧跟在函数的参数列表后面,表示函数不会抛出异常
  • 违反异常说明
    • 编译器并不会在编译时检查noexcept说明
      • 实际上若一个函数说明了noexcept的同时又含有throw语句也能顺利编译
    • noexcept可接受一个可选的实参,该实参必须转换成bool类型,若为true函数不会抛出异常
  • noexcept运算符
    • noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常
    • 不会对其运算对象求值
  • 异常说明与指针,虚函数和拷贝控制
    • 函数指针及该指针所指的函数必须具有一致的异常说明
    • 虚函数承诺了不会抛出异常,则派生的虚函数也必须不能抛出任何异常
    • 若基类的虚函数允许抛出异常,则派生类的对应函数既可以抛出异常,也可以不允许抛出异常
1
2
3
4
noexcept(recoup(i));  //若recoup不跑出异常结果为true

noexcept(e);
// 当e调用的所有函数都做了不抛出说明且e本身不含有throw语句时,表达式为true

18.1.5 异常类层次

  • exception
    • bad_cast
    • runtime_error
      • overflow_error
      • underflow_error
      • range_error
    • logic_error
      • domain_error
      • invalid_argument
      • out_of_range
      • length_error
    • bad_alloc

18.2 命名空间

  • 命名空间为了防止名字冲突而提供了可控的机制,分割了全局命名空间,其中每个命名空间是一个作用域

18.2.1 命名空间的定义

  • 命名空间的定义包含
    • 首先是关键字namespace
    • 随后是命名空间的名字
    • 在命名空间后市一系列由花括号括起来的声明和定义
    • 命名空间后无需分号
  • 每一个命名空间都是一个作用域
    • 命名空间中每个名字必须表示该空间的唯一实体
    • 定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问
    • 位于命名空间之外的代码必须使用作用域说明符(::)指出所用的名字属于哪个命名空间
  • 命名空间的定义可以不连续的特性
    • 使得可以将几个独立的接口和实现文件组成一个命名空间
    • 命名空间的管理
      • 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中
      • 命名空间成员的定义部分则置于另外的源文件中
  • 定义命名空间成员
    • 命名空间之外定义的成员必须使用含有前缀的名字
  • 模板特例化
    • 模板特例化必须定义在原始模板所属的空间
    • 只要在命名空间中声明了特例化,就能在命名空间外部定义
  • 全局命名空间
    • 全局命名空间为隐式的方式声明的,全局作用域中定义的名字被隐式地添加到全局命名空间中
  • 嵌套的命名空间:是定义在其他命名空间中的命名空间
    • 调用时嵌套调用
  • 内联命名空间
    • 内联命名空间中的名字可被外层命名空间直接使用
    • 无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它
    • 关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间时可不写inline
    • 应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到内联命名空间
  • 未命名的命名空间
    • namespace后紧跟着花括号的一系列声明
    • 未命名的命名空间中定义的变量拥有静态声明周期:再第一次使用前创建,并且直到程序结束才销毁
    • 未命名的命名空间不能跨越文件
    • 未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同
      • 如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别
    • 未命名的命名空间可以嵌套在其他命名空间中,使用外层命名空间的名字来访问
  • 可使用未命名的命名空间代替文件中的静态声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace cplusplus_pimer{
class Sales_data{//...};
Sales_data operator+(const Sales_data&,const Sales_data&);
class Query(//...);
class Query_base{//..}
}

// 声明模板特例化
namespace std{
template<> struct hash<Sales_data>
}

// 在std中添加了模板特例化声明后,就可以在命名空间的外部定义
template<> struct std::hash<Sales_data>{

}

18.2.2 使用命名空间成员

  • 命名空间的别名以关键字nameapace开始,后接别名,=,命名空间原来的名字和分号
  • using声明
    • 有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止
  • 头文件与using声明
    • 头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中

18.2.3 类,命名空间与作用域

  • 对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域。外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止
  • 查找与std::move和std::forward
    • 通常情况下,如果在应用程序中定义了一个标准库中已有的名字,则将出现以下两种情况中的一种:要么根据一般的重载规则确定某次调用应该执行函数的哪个版本;要么应用程序根本就不会执行函数的标准库版本
    • 在函数模板中,右值引用形参可以匹配任何类型。如果应用程序也定义了一个接受单一形参的move函数,则不管该形参是什么类型,应用程序的move函数都将与标准库的版本冲突。forward函数也是如此
  • 友元声明与实参相关的查找
    • 当类声明了一个友元时,该友元声明并没有使得友元本身可见。然而,一个另外的未声明的类或函数如果第一次出现在友元声明中,则认为它是最近的外层命名空间的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace A{
class C{
friend void f2(); //除非另有声明,否则不会被找到
friend void f(const C&); //根据实参相关的查找规则可被找到
}
}

int main(){
A::C cobj;
f(cobj); //通过再A::C中的友元声明找到
f2(cobj); //错误,未声明
}
// 因为f接受一个类类型的实参,而且f在C所属的命名空间进行了隐式的声明,所以f能被找到。相反,因为f2没有形参,所以它无法被找到

18.2.4 重载与命名空间

  • 与实参相关的查找与重载
    • 对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行
    • 这条规则对于如何确定候选函数集同样也有影响:将在每个实参类(以及实参类的基类)所属的命名空间中搜寻候选函数;在这些命名空间中所有与被调用函数同名的函数都将被添加到候选集当中,即使其中某些函数在调用语句处不可见也是如此
  • 重载与using声明
    • using声明语句声明的是一个名字,而非一个特定的函数
    • 一个using声明囊括了重载函数的所有版本以确保不违反命名空间的接口。库的作者为某项任务提供了好几个不同的函数,允许用户选择性地忽略重载函数中的一部分但不是全部有可能导致意想不到的程序行为
    • 一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数
      • 如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明
      • 如果using声明所在的作用域中已经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误
      • 除此之外,using声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模
  • 重载与using指示
    • using指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中
  • 跨越多个using指示的重载
    • 如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数集的一部分

18.3 多重继承与虚继承

  • 多重继承:从多个直接基类中产生派生类的能力
    • 继承了所有父类的属性

18.3.1 多重继承

  • 在派生类的派生列表中可以包含多个基类
    • 每个基类包含一个可选的说明符,若忽略,则class为private,struct为public
    • 多继承的派生列表只能包含已经被定义过的类,而且类不能是final的
    • 在某个给定的派生列表中,同一个基类只能出现一次
  • 多重继承的派生类从每个基类中继承状态
  • 派生类构造函数初始化所有基类
    • 构造一个派生类对象将同时构造并初始化它的所有基类子对象
    • 多重继承的派生类的构造函数初始值只能初始化它的直接基类
  • 继承的构造函数与多重继承
    • 允许派生类从一个或几个基类中继承构造函数,但从不能从多个基类继承相同的构造函数,需为这个类自定义构造函数
  • 多重继承的派生类的拷贝与移动操作
    • 多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作
    • 只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作
    • 在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作
1
2
3
class Bear: public ZooAnimal{
lass Panda:public Bear, public Endangered{//....};
}

18.3.2 类型转换与多个基类

  • 可以使用可访问基类的指针或引用直接指向派生类的对象
  • 基于指针类型或引用类型的查找
    • 对象,指针和引用的静态类型决定了能使用哪些成员

18.3.3 多重继承下的类作用域

  • 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义

18.3.4 虚继承

  • 实际上派生类可以多次继承同一类,派生类可以通过它的两个直接基类分别继承同一个简介积累,也可以直接继承某个基类,然后通过某一个基类再一次间接继承该类
    • 在默认情况下,派生类中含有继承链上每个类对应的子部分
    • 如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象
  • 虚继承机制:令某个类做出声明,承诺愿意共享它的基类,其中共享的基类子对象称为虚基类
    • 无论虚基类在继承体系中出现多少次,在派生类中都只含有唯一一个共享的虚基类
    • 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,不影响派生类本身
  • 使用虚基类
    • 指定虚基类的方式是在派生列表中添加virtual
    • virtual说明符表示在后续的派生类当中共享虚基类的同一份实例
  • 虚基类成员的可见性
    • 在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问
1
2
3
4
5
6
7
8
9
// 关键字public和virtual的顺序随意
class Raccon : public virtual ZooAnimal
class Bear : virtual public ZooAnimal

// 某个类指定了虚基类,该类的派生按常规方式进行
class Panda:public Bear,public Raccoon,public Endangered{
//...
}
// Panda通过Raccoon和Bear继承了ZooAnimal,因为Raccoon和Bear继承ZooAnimal的方式都是虚继承,所以在Panda中只有一个ZooAnimal基类部分

18.3.5 构造函数与虚继承

  • 在虚派生中,虚基类是最低层的派生类初始化的
  • 虚继承的对象的构造方式
    • 含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化
    • 虚基类总是先于非虚基类的构造
  • 构造函数与析构函数的次序
    • 一个类可以由多个虚基类,虚的子对象按照在派生列表中出现的顺序从左向右一次构造

19 特殊工具与技术

19.1 控制内存分配

  • 重载new运算符和delete运算符控制内存分配的过程

19.1.1 重载new和delete

  • 使用new表达式的实际操作
    • new盗用了operator new的标准库函数,分配一块足够大的原始的未命名的内存空间一遍存储特定类型的对象
    • 编译器运行相应的构造函数构造这些对象,并传入初始值
    • 对象被分配了空间并构造完成,返回一个指向该对象的指针
  • 使用delete的实际操作
    • 对指向的对象或数组执行对象的析构函数
    • 编译器调用operator delete的标准库函数释放内存空间
  • 当自定义了全局的operator new函数和operator delete函数后,我们就担负起了控制动态内存分配的职责。这两个函数必须是正确的:因为它们是程序整个处理过程中至关重要的一部分
  • operator new接口和operator delete接口
    • 可自定义上面函数版本的任意一个,前提是自定义的版本必须位于全局作用域或类作用域中
      • 使用上面的运算符函数定义成类的成员时时隐式静态的,无需显示声明为static
      • 因为operator new用在对象构造之前而operator delete用在对象销毁之后,所以这两个成员(new和delete)必须是静态的,而且它们不能操纵类的任何数据成员
  • 想要自定义operator new函数,则可以为它定义额外的形参
    • 此时用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参
  • malloc和free函数
    • C++从C中继承了这些函数,并将其定义在cstdlib头文件中
      • malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针或者返回0以表示分配失败
      • free函数接受一个void*,是malloc返回的指针的副本,free将相关内存返回给系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 标准库中的重载版本
// 可能会抛出异常
void *operator new(size_t); //分配一个对象
void *operator new[](size_t); //分配一个数组
void *operator delete(void*) noexcept; //释放一个对象
void *operator delete[](void*) noexcept; //释放一个数组

// 承诺不会抛出异常
void *operator new(size_t,nothrow_t&) noexcept; //分配一个对象
void *operator new[](size_t,nothrow_t&) noexcept; //分配一个数组
void *operator delete(void*, nothrow_t&) noexcept; //释放一个对象
void *operator delete[](void*, nothrow_t&) noexcept; //释放一个数组

void *operator new(size_t, void*); //不允许重新定义这个版本
// 该版本只供标准库使用,不能被用户重新定义

// 定义operator new和operator delete的简单方式
void *operator new(size_t size){
if(void *mem = malloc(size))
return mem;
else
throw bad_alloc();
}
void operator delete(void *mem) noexcept{
free(mem);
}

19.1.2 定位new表达式

  • 与allocator不同的是,对于operator new分配的内存空间来说无法使用construct函数构造对象
  • 应该使用new的定位new形式构造对象,为分配函数提供了额外的信息
    • 定位new允许在一个特定的,预先分配的内存地址上构造对象
  • 定位new与allocator的construct成员相似,但一个重要区别
    • 传给construct的指针必须指向同一个allocator对象分配的空间,但是传给定位new的指针无需指向operator new分配的内存
  • 显式的析构函数调用
    • 定位new与使用allocate类似一样,对析构函数的显式调用也与使用destroy类似
    • 既可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数
    • 和调用destroy类似,调用析构函数可以清除给定的对象但是不会释放该对象所在的空间
    • 需要的话可以重新使用该空间
      • 调用析构函数会销毁对象,但是不会释放内存
1
2
3
4
5
6
7
8
9
10
// 定位new的形式
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list}

// place_address必须是一个指针,同时在initializers中提供一个以逗号分割的初始值列表,该初始值列表用于构造新分配的对象

string *sp = new string("a value");
sp->~string();

19.2 运行时类型识别

  • 运行时类型识别的功能由两个运算符实现
    • typeid运算符用于返回表达式的类型
    • dynamic_cast运算符用于将基类的指针或引用安全地转换成派生类的指针或引用
  • 将两个运算符用于某种类型的指针或引用,并且该类型含有虚函数是,运算符将使用指针或引用所绑定对象的动态类型
  • 两个运算符特别适用的情况
    • 想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数
    • 一般来说,只要有可能应该尽量使用虚函数
      • 当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本
    • 假设我们使用虚函数,则可以使用一个RTTI运算符
      • 另一方面,与虚成员函数相比,使用RTTI运算符蕴含着更多潜在的风险:程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行

19.2.1 dynamic_cast运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// dynamic_cast运算符的使用形式
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
// 其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数
// e的类型必须符合以下三个条件中的任意一个:
// e的类型是目标type的公有派生类、e的类型是目标type的公有基类或者e的类型就是目标type的类型
// 如果符合,则类型转换可以成功。否则,转换失败

// 假定Base类至少含有一个虚函数,Derived是Base的公有派生类
// 如果有一个指向Base的指针bp,则可以在运行时将它转换成指向Derived的指针
if(Derived *dp = dynamic_cast<Derived*>(bp)){
//使用dp指向的Derived对象
}else{ //bp指向一个Base对象
//使用bp指向的Base对象
}

19.2.2 typeid运算符

  • typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字
    • typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型
    • 定义在typeinfo头文件
    • 可以作用于任意类型的表达式,顶层const被忽略
    • 当typeid作用于引用返回该引用所引对象的类型
    • 当typeid作用于数组或函数时,并不会执行向指针的标准类型转换
  • 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型
  • 当typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型
    • typeid是否需要运行时检查决定了表达式是否会被求值
      • 只有当类型含有虚函数时,编译器才会对表达式求值。反之,如果类型不含有虚函数,则typeid返回表达式的静态类型;编译器无须对表达式求值也能知道表达式的静态类型
    • 如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型
      • 这条规则适用于typeid(*p)的情况。如果指针p所指的类型不含有虚函数,则p不必非得是一个有效的指针。否则,*p将在运行时求值,此时p必须是一个有效的指针
      • 如果p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常
1
2
3
4
5
6
7
8
9
10
Derived *dp = new Derived;
Base *bp = dp; //两个指针都指向Derived对象

if(typeid(*bp)==typeid(*dp)){
//bp和dp指向同一类型的对象
}
// 检查运行时类型是否是某种指定的类型
if(typeid(*bp) == typeid(Derived)){
//bp实际指向Derived对象
}

19.2.3 使用RTTI

  • 在某些情况下RTTI非常有用
    • 比如想为具有继承关系的类实现相等运算符时,对于两个对象来说,如果它们的类型相同并且对应的数据成员取值相同,则说这两个对象是相等的
    • 在类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算符必须把派生类的新成员考虑进来
  • 定义比较操作
    • 如果参与比较的两个对象类型不同,则比较结果为false
      • 例如,如果试图比较一个基类对象和一个派生类对象,则==运算符应该返回false
    • 基于上述推论,可以使用RTTI解决问题了
      • 定义的相等运算符的形参是基类的引用,然后使用typeid检查两个运算对象的类型是否一致
      • 如果运算对象的类型不一致,则==返回false;类型一致才调用equal函数。每个类定义的equal函数负责比较类型自己的成员。这些运算符接受Base&形参,但是在进行比较操作前先把运算对象转换成运算符所属的类类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 定义两个示例类
class Base{
friend bool operator==(const Base&, const Base&);
public:
//Base的接口成员
protected:
virtual bool equal(const Base&) const;
//Base的数据成员和其他用于实现的成员
};
class Derived: public Base{
public:
//Derived的其他接口成员
protected:
bool equal(const Base&) const;
// Derived的数据成员和其他用于实现的成员
};

// 定义整体相等运算符
bool operator==(const Base &lhs, const Base &rhs){
//如果typeid不相同,返回false;否则虚调用equal
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

// 虚equal函数
// 继承体系中的每个类必须定义自己的equal函数
// 派生类的所有函数要做的第一件事都是相同的,那就是将实参的类型转换为派生类类型
bool Derived::equal(const Base &rhs) const{
//清除这两个类型是相等的,所以转换过程不会抛出异常
auto r = dynamic_cast<const Derivd&>(rhs);
// 执行比较两个Derived对象的操作并返回结果
}

// 基类equal函数
bool Base::equal(const Base &rhs) const{
//执行比较Base对象的操作
}

19.2.4 type_info类

type_info的操作 -
t1 == t2 如果type_info对象t1和t2表示同一种类型,返回true
t1 != t2 如果type_info对象t1和t2表示不同类型,返回true
t.name() 返回C风格字符串,表示类型名字的可打印形式。类似名字的生成方式因系统而异
t1.before(t2) 返回一个bool值,表示t1是否位于t2之前。before所采用的顺序关系是依赖于编译器

19.3 枚举类型

  • 枚举类型将一组整型常量组织在一起
    • 和类一样,每个枚举类型定义了一种新的类型,枚举属于字面值常量类型
  • 限定作用域的枚举类型:首先是关键字enum class随后是枚举类型名字以及花括号括起来的以逗号分割的枚举成员,最后是一个分号
  • 不限定作用域的枚举类型:省略class
  • 枚举成员
    • 在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的
    • 在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同
    • 默认情况下,枚举值从0开始,依次加1
  • 初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象
  • 一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型
  • 指定enum的大小
    • 实际上enum是由某种整数类型表示的
    • 在enum后面加上冒号以及想要在enum中使用的类型
  • 枚举类型的前置声明
    • 可以提前声明enum,enum的前置声明必须指定其成员的大小
  • 形参匹配与枚举类型
    • 要想初始化一个enum对象,必须使用该enum类型的另一个对象或者它的一个枚举成员
    • 即使某个整型值恰好与枚举成员的值相等也不能作为函数的enum实参使用
    • 尽管不能直接将整型值传给enum形参,但是可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 定义限定作用域的枚举类型
enum class open_modes {input, output, append};
// 定义不限定作用域的枚举类型
enum color {red, yellow, green};

enum class intTypes{
charTyp = 8,shortTyp = 16, intTyp = 16, longTyp = 32, long_longTyp = 64
};

constexpr intTypes charbits = intTypes::charTyp;

// 初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象
om = open_mode::input; //input是open_modes的一个枚举成员

// 不限定作用域的枚举类型intValues的前置声明
enum intValues : unsigned long long; //不限定作用于的必须指定成员类型
enum class open_modes; //限定作用域的枚举类型可以使用默认成员类型int

// 不限定作用域的枚举类型,潜在类型因机器而已
enum Tokens {INLINE = 18,VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main(){
Tokens curTok = INLINE;
ff(128);
ff(INLINE);
ff(curTok);
return 0;
}

19.4 类成员指针

  • 成员指针是指可以指向类的非静态成员的指针
    • 一般情况下,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象
    • 类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别
    • 初始化这样的指针时,令其指向类的某个成员,但是不指定该对象所属的对象,直到使用成员指针时,才提供成员所属的对象
1
2
3
4
5
6
7
8
9
10
11
12
// Screen类
class Screen{
public:
typedef std::string::size_type pos;
char get_curosor() const { return contents[cursor];}
char get() const;
char get(pos ht, pos wd) const;
private:
std::string contents;
pos cursor;
pos height, width;
}

19.4.1 数据成员函数

  • 和其他指针一样,在声明成员指针时可以使用*来表示当前声明对象的名字是一个指针,成员指针必须包含成员所属的类
    • 在*之前添加classname::以表示当前定义的指针可以指向classname的成员
    • 初始化一个成员指针时,需要指定所指向的成员
    • 新标准中声明成员指针简答的方法是使用auto或decltype
  • 使用数据成员指针
    • 当初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据
    • 成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息
    • 与成员访问运算符.和->类似,也有两种成员指针访问运算符:.*和->*,这两个运算符使得我们可以解引用指针并获得该对象的成员
  • 返回数据成员指针的函数
    • 常规的访问控制规则对成员指针同样有效
    • 因为数据成员一般情况下是私有的,所以通常不直接获得数据成员的指针
1
2
3
4
5
6
7
8
9
const string Screen::*pdata;

pdata = &Screen::contents;

auto pdata = &Screen::contents;

Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*pdata;
s = pScreen->*pdata;

19.4.2 成员函数指针

  • 和指向数据成员的指针一样,使用classname::*的形式声明一个指向成员函数的指针
    • 类似于任何其他函数指针,指向成员函数的指针也需要指定目标函数的返回类型和形参列表
    • 如果成员函数是const成员或者引用成员,则必须将const限定符或引用限定符包含进来
  • 和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则
  • 使用成员函数指针
    • 和使用指向数据成员的指针一样,使用. *或者->*运算符作用于指向成员函数的指针,以调用类的成员函数
  • 使用成员指针的别名
    • 使用类型别名或typedef可以让成员指针更容易理解
    • 和其他函数指针类似,可以将指向成员函数的指针作为某个函数的返回类型或形参类型
      • 其中,指向成员的指针形参也可以拥有默认实参
  • 成员指针函数表
    • 对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中。如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个

19.4.3 将成员函数用作可调用对象

  • 要想通过一个指向成员函数的指针进行函数调用,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上
  • 因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符
  • 因为成员指针不是可调用对象,所以不能直接将一个指向成员函数的指针传递给算法
  • 使用function生成一个可调用对象
    • 从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function
    • 使用function必须提供成员的调用形式
      • 使用mem_fn可以让编译器负责推断成员的类型,可以从成员指针生成一个可调用对象
        • mem_fn可根据成员指针的类型推断出可调用对象的类型,而无需用户显式地指定
  • 使用bind生成一个可调用对象
    • 出于完整性的考虑使用bind从成员函数生成一个可调用对象
    • 和function类似的地方是,当使用bind时,必须将函数中用于表示执行对象的隐式形参转换成显式的
    • 和mem_fn类似的地方是,bind生成的可调用对象的第一个实参既可以是string的指针,也可以是string的引用

19.5 嵌套类

  • 一个类定义在另一个类的内部,前者称为嵌套类或嵌套类型
    • 嵌套类常用于定义作为实现部分的类
    • 嵌套类是一个独立的类,与外层类基本没什么关系
      • 特别是,外层类的对象和嵌套类的对象是相互独立的
    • 在嵌套类的对象中不包含任何外层类定义的成员
      • 类似的,在外层类的对象中也不包含任何嵌套类定义的成员
    • 嵌套类中成员的种类与非嵌套类是一样的
      • 和其他类类似,嵌套类也使用访问限定符来控制外界对其成员的访问权限
      • 外层类对嵌套类的成员没有特殊的访问权限,同样,嵌套类对外层类的成员也没有特殊的访问权限
    • 嵌套类在其外层类中定义了一个类型成员
      • 和其他成员类似,该类型的访问权限由外层类决定
      • 位于外层类public部分的嵌套类实际上定义了一种可以随处访问的类型
      • 位于外层类protected部分的嵌套类定义的类型只能被外层类及其友元和派生类访问
      • 位于外层类private部分的嵌套类定义的类型只能被外层类的成员和友元访问
  • 在外层类之外定义一个嵌套类
    • 在嵌套类在其外层类之外完成真正的定义之前,它都是一个不完全类型
  • 定义嵌套类的成员
    • 为其定义构造函数,必须指明QueryResult是嵌套在TextQuery的作用域之内的
    • 具体的做法是使用外层类的名字限定嵌套类的名字
  • 嵌套类作用域中的名字查找规则
    • 名字查找的一般规则在嵌套类中同样适用
    • 嵌套类本身是一个嵌套作用域,所以还必须查找嵌套类的外层作用域
      • 可以说明为什么我们不在QueryResult的嵌套版本中定义line_no
      • 原来的QueryResult类定义了该成员,从而使其成员可以避免使用TextQuery::line_no的形式。然而QueryResult的嵌套类版本本身就是定义在TextQuery中的,所以我们不需要再使用typedef。嵌套的QueryResult无须说明line_no属于TextQuery就可以直接使用它
    • 嵌套类是其外层类的一个类型成员,外层类的成员可以像使用任何其他类型成员一样使用嵌套类的名字
  • 嵌套类和外层类型是相互独立的
    • 尽管嵌套类定义在其外层类的作用域中,但是外层类的对象和嵌套类的对象没有任何关系
    • 嵌套类的对象只包含嵌套类定义的成员;同样,外层类的对象只包含外层类定义的成员,在外层类对象中不会有任何嵌套类的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 声明嵌套类
class TextQuery{
public:
class QueryResult; //嵌套类稍后定义
};

// 在外层类之外定义一个嵌套类
// QueryResult的定义
class TextQuery::QueryResult{
// 在类的作用域内,不必对QueryResult形参进行限定
friend std::ostream&
print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string,std::shared_ptr<std::set<line_no>>,std::shared_ptr<std::vector<std::string>>);
};

TextQuery::QueryResult::QueryResult(string s,shhared_ptr<set<line_no>> p, shared_ptr<vector<string>> f):
sought(s),lines(p),file(f){}

// 嵌套类的静态成员定义
int textQuery::QueryResult::static_mem = 1024;

19.6 union:一种节省空间的类

  • 联合是一种特殊的类,可以有多个数据成员,但在任意时刻只有一个数据成员可以有值
    • 当给union的某个成员赋值之后,该union的其他成员就变成未定义的状态了
    • 分配给一个union对象的存储空间至少要能容纳它的最大的数据成员
  • union不能含有引用类型的成员,可以为成员指定public,protect和private等保护标记
    • 默认情况下,union的成员都是共有的
    • union不能继承自其他类,也不能作为基类使用,不能含有虚函数
  • 匿名union是一个未命名的union,并且在右花括号和分号之间没有任何声明
    • 在匿名union的定义所在的作用域内该union的成员都是可以直接访问的
  • 含有类类型成员的union
    • 当union包含的是内置类型的成员时,可以使用普通的赋值语句改变union保存的值
      • 但是对于含有特殊类类型成员的union就没这么简单了。如果我们想将union的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须分别构造或析构该类类型的成员:当将union的值改为类类型成员对应的值时,必须运行该类型的构造函数;反之,当将类类型成员的值改为一个其他值时,必须运行该类型的析构函数
    • 当union包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员
      • 但是如果union含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的
  • 使用类管理union成员
    • 把含有类类型成员的union内嵌在另一个类中,可以管理并控制与union的类类型成员有关的状态转换
    • 为了追踪union中存储什么类型的值,通常会定义一个union的判别式(独立的对象)
      • 可以使用判别式辨别union存储的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义union
union Token{
char cval;
int ival;
double dval;
};

// 使用union类型
Token first_token = {'a'}; //初始化
Token last_token;
Token *pt = new Token; //指向一个未初始化的Token对象的指针

last_token.cval = 'z';
pt->ival = 42;

19.7 局部类

  • 局部类:类可以定义在某个函数的内部
    • 定义只在作用域内可见,成员受到严格的限制
    • 局部类中不允许声明静态数据成员
  • 局部类不能使用函数作用域中的变量
    • 局部类对其外层作用域中的名字的访问权限受到很多限制:局部类只能访问外层作用域定义的类型名,静态变量,枚举成员
    • 如果局部类定义在某个函数内部,该函数的普通局部变量不能被局部类使用
  • 常规的访问保护规则对局部类同样适用
    • 外层函数对局部类的私有成员没有任何访问特权
    • 当然,局部类可以将外层函数声明为友元;或者更常见的情况是局部类将其成员声明成公有的
    • 在程序中有权访问局部类的代码非常有限。局部类已经封装在函数作用域中,通过信息隐藏进一步封装就显得没什么必要了
    • 外层函数对局部类的私有成员没有任何访问特权。当然,局部类可以将外层函数声明为友元;或者更常见的情况是局部类将其成员声明成公有的。在程序中有权访问局部类的代码非常有限。局部类已经封装在函数作用域中,通过信息隐藏进一步封装就显得没什么必要了
  • 局部类中的名字查找
    • 局部类内部的名字查找次序与其他类相似
    • 在声明类的成员时,必须先确保用到的名字位于作用域中,然后再使用该名字
    • 定义成员时用到的名字可以出现在类的任意位置
      • 如果某个名字不是局部类的成员,则继续在外层函数作用域中查找;如果还没有找到,则在外层函数所在的作用域中查找
  • 嵌套的局部类
    • 可以在局部类的内部再嵌套一个类
    • 此时,嵌套类的定义可以出现在局部类之外。不过,嵌套类必须定义在与局部类相同的作用域中

19.8 固有的不可移植的特性

19.8.1 位域

  • 类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位
    • 当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域

19.8.2 volatile限定符

  • 直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制
  • 当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile
    • 关键字volatile告诉编译器不应对这样的对象进行优化

19.8.3 链接指示:extern “C”

  • 使用链接指示指出任意非C++函数所用的语言

评论