- 第一章 开始
- 第二章 变量和基本类型
- 第三章 字符串、向量和数组
- 第四章 表达式(expression)
- 第五章 语句
- 第六章 函数
- 第七章 类
- 第八章 IO库
- 第九章 顺序容器
- 第十章 泛型算法(generic algorithm)
- 第十一章 关联容器(associative-container)
- 第十二章 动态内存(dynamic memory)
- 第十三章 拷贝控制
- 第十四章 重载运算与类型转换
- 第十五章 面向对象程序设计
- 第十八章 用于大型程序的工具
- 第十六章 模板与泛型编程
- 第十七章 标准库特殊设施
- 第十九章 特殊工具与技术
《C++ Primer 5th》读书笔记
第一章 开始
- 初识输入输出
- iostream库包含两个基础类型istream和ostream,分别表示输入流和输出流
- 标准库定义了四个IO对象:cin(标准输入),cout(标准输出),cerr(标准错误),clog
- 写入endl的效果是结束当前行,并刷新与设备关联的缓冲区;刷新缓冲区的操作可以保证到目前为止程序所产生的所有输出真正地写入输出流中,而不是停留在内存中
- 在调试时添加的打印语句应该保证一直刷新流,才能保证输出的及时
- 注释简介
- 单行注释:
//
,可以嵌套 - 界定符对注释:
/**/
,不可以嵌套
- 单行注释:
- 控制流
- while语句
- for语句
- 当使用istream对象作为条件时,其效果是检测流的状态,流的状态分为有效和无效
- 当流遇到错误、文件结束符(EOF)、无效的输入时,istream对象的状态变为无效
- if语句
- 类的简介
第二章 变量和基本类型
- C++是一门静态数据类型(staticall typed)语言,其含义是在编译阶段进行类型检查(type checking),因此我们必须先声明后使用变量,这样编译器才知道变量与其上的操作是否合法
- 基本内置类型
- 表2.1列出了C++标准规定的算术类型尺寸的最小值
- 扩展字符型:
wchar_t
用于确保能够存放机器最大扩展字符集中的任一字符;char16_t
和char32_t
用于Unicode字符集 - 浮点型可分为单精度(float),双精度(double),扩展精度(long double)
- 除了布尔类型和扩展字符型,其他整型可以分为signed和unsigned两种
- 字符型有三种:char, signed char, unsigned char,char对应signed char还是unsigned char取决于编译器
- C++并没有规定带符号类型应该如何表示(比如使用补码),但是约定在表示范围内正值和负值量应该平衡
- 选择类型的经验准则
- 当明确知道类型不可能为负时使用无符号类型
- 只有在存放字符时才使用char,存放布尔值时使用bool;如果需要存放一个不大的整数必须明确使用signed char或者unsigned char
- 执行浮点运算时使用double,因为float的精度较低并且计算代价和double相差无几;一般不需要long double的精度
- 类型转换
- 类型所能表示的范围决定了转换的过程
- 把非bool类型的变量赋给bool类型变量,非0表示true,0表示false
- 把bool类型变量赋给非bool类型变量,true表示1,false表示0
- 把浮点数赋给整数,结果值仅保留浮点数中整数部分,没有进行四舍五入
- 把整数赋给浮点数,小数部分为0,但是可能会丢失精度
- 赋给无符号类型超过表示范围的值,结果是初值对无符号类型表示数值的总个数(比如8位无符号数为256)取余,超过表示范围的值包括负数
- 赋给有符号类型超过表示范围的值,结果是未定义的
- 切勿混用带符号类型和无符号类型
- 字面值常量
- 十进制字面值的类型是能够容纳当前值的int、long和long long中尺寸最小的那个
- 八进制和十六进制字面值类型是能够容纳当前值的int、unsigned int、long、unsigned long、long long和unsigned long long中尺寸最小的那个
- 如果一个字面值超过了与之关联的最大数据类型的表示范围将产生错误
- 浮点型字面值默认是double类型
- 编译器自动在字符串的结尾处添加空字符(
'\0'
) - 如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,那么它们实际上是一个整体
- 通过添加表2.2中的前缀和后缀可以改变字面值的默认类型以及是否带符号
- 十进制字面值默认是带符号数
- 八进制和十六进制字面值默认既可能带符号也可能不带符号,因此在选择类型时就有上述第一二条规则
- 布尔类型的字面值为true和false
- 指针字面值为nullptr
- 使用NULL或者0在重载函数时会产生二义性
- C++将NULL定义为0,C将NULL定义为(void *)0
- 变量
- 初始化(initialized):当对象在创建时获得了一个特定的值
- 在同一条定义中,可以用先定义的变量初始化后定义的变量
- 初始化不是赋值,赋值的含义是将对象的当前值擦除,以一个新值来代替
- 列表初始化(list initialization)
- 在初始化对象或者为对象赋新值时可以使用一组由花括号括起来的初始值
- 对于内置类型的变量,如果使用列表初始化且初始值存在丢失信息的风险,则编译器报错
- 默认初始化(default initialized)
- 如果定义变量时没有指定初始值,那么变量被默认初始化
- 定义于函数体内的内置类型变量如果没有初始化,则其值未定义;类对象如果没有显示初始化,则其值由类确定
- 变量声明和定义的关系
- 声明规定了变量的类型和名字,定义还申请存储空间,也可能为变量赋初始值
- 变量能且只能被定义一次,但是可以被声明多次
- 在函数体内部试图初始化一个extern关键字标记的变量将引发错误
- 任何包含了显式初始化的声明都成为定义
- 标识符
- C++标识符由字母、数字和下划线组成,只能以下划线和字母开头,没有长度限制,对大小写敏感
- 用户定义的标识符不能连续出现两个下划线,不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头
- 变量命名规范
- 标识符要体现实际意义
- 变量名一般用小写字母
- 用户自定义的类名一般以大写字母开头
- 如果标识符由多个单词组成,单词间应该有明显区分,例如下划线命名法或者驼峰命名法
- 名字的作用域(scope)
- 全局作用域(global scope)
- 当作用域操作符左侧为空时指代全局作用域,例如
::val
- 块作用域(block scope)
- 嵌套作用域
- 被包含的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)
- 内层作用域可以访问外层作用域声明的变量,也可以重新定义外层作用域中的变量
- 初始化(initialized):当对象在创建时获得了一个特定的值
- 复合类型(compound type)
- 引用(reference)
- 这里谈的是左值引用(lvalue reference)
- 定义引用时必须初始化,一旦初始化完成,引用和初始值一直绑定在一起,无法令引用重新绑定到另一个对象
- 引用并非对象,它只是为已经存在的对象所起的另外一个名字
- 定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是把初始值拷贝给引用
- 不能够定义引用的引用,但是可以将引用作为另一个引用的初始值
- 引用类型要和与之绑定对象的类型严格匹配,并且只能绑定到对象上,不能是某个字面值或者某个表达式的计算结果
- 指针(pointer)
- 与引用的区别
- 指针本身就是一个对象,允许对指针赋值和拷贝,在指针的生命期内可以指向不同的对象
- 指针无须在定义时赋初始值
- 因为引用不是对象,因此不能定义指向引用的指针
- 指针的操作
- 可以通过解引用符(
*
)来访问实际的对象 - 将指针用在条件表达式中,如果指针为0则表示false,否则表示true
- 可以使用
==
或!=
比较两个指针,如果两个指针指向的地址相同则为true,否则为false
- 可以通过解引用符(
void *
可以用于存放任意类型对象的地址- 面对一条复杂的指针或者引用的声明语句,从右向左阅读有助于弄清楚它的真实含义
- 引用(reference)
- const限定符
- 因为const对象一旦创建之后其值就不能够修改,因此必须在定义时初始化
- const对象默认仅在文件内有效
- 编译器将在所有用到const对象的地方将其替换成对应的值
- 如果想在多个文件之间共享const对象,必须在定义之前添加extern关键字
- 对常量的引用(reference to const)
- 与普通引用不同的是,对常量的引用不能够用作修改它所绑定的对象
- 在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型即可
- 对const的引用可能引用一个非const的对象
- 指针和const
- 指向常量的指针(pointer to const)
- 不能用于改变所指对象的值
- 只能使用指向常量的指针存放常量对象的地址
- 允许一个指向常量的指针指向非常量对象的地址
- 常量指针(const pointer)
- 常量指针必须初始化,一旦初始化完成,它的值就不能再改变了
- 顶层const
- 顶层const(top-level const):表示指针本身就是个常量
- 底层const(low-level const):表示指针所指的对象是一个常量
- 当执行对象的拷贝操作时,顶层const不受什么影响,但是底层const却不能够忽视
- 拷入拷出的对象必须具有相同的底层const,或者两者的数据类型能够转换,非常量可以转换为常量
- 对于引用,初始值的顶层const不能够忽略
- 对顶层const取地址变为底层const
- constexpr和常量表达式
- 常量表达式(const expression)是指值不会改变并且在编译过程中就能得到计算结果的表达式
- 在C++语言中有几种情况要用到常量表达式,C++ 11标准规定可以使用constexpr类型变量来由编译器验证变量是否是一个常量表达式
- 算术类型、引用和指针可以被声明为constexpr,但是较复杂的类型则不能声明为constexpr
- 声明为constexpr的引用和指针的初始值受到严格限制,指针只能被初始化为nullptr、0或者存储于某个固定地址中的对象;引用只能使用常量表达式初始化
- 声明为constexpr的指针具有顶层const
- 处理类型
- 类型别名(type alias)
- 使用传统方法typedef
- C++ 11可以使用别名声明(alias declaration):
using SI=Sales_item;
- 当类型别名指代的是复合类型时,在使用时要小心
typedef char *pstring; const pstring p;//注意p等价于char *const p
- auto类型说明符
- auto让编译器通过初始值推断变量的类型,因此auto定义的变量必须要有初始值
- 使用auto在一条语句中声明多个变量,所有变量的基本类型必须一致
- 编译器推断auto类型的一些规则
- 当使用引用作为初始值时,编译器以被引用对象的类型作为auto的类型
- auto一般会忽略顶层const,保留底层const,如果希望保留顶层const必须显式声明
- 对于auto类型的引用,初始值的顶层const被保留,常量的初始值不具有顶层const(
auto &h=42;//错误
)
- decltype类型指示符
- 如果希望从表达式的类型推断出类型但是不想用该表达式初始化变量(auto就是那么干的),可以使用decltype
- 编译器分析表达式并得到它的类型,但是不会实际计算表达式
- 编译器分析表达式的一些规则
- 如果decltype使用的是一个变量,那么返回变量的类型(包括顶层const和引用,引用从来都作为被引用对象的同义词出现,除了decltype)
- 如果decltype使用的是表达式,那么返回表达式结果对应的类型,如果表达式结果的对象能够作为左值,那么返回引用
- 如果表达式的内容是解引用操作,那么得到的是引用类型
- decltype((variable))的结果永远是引用,decltype(variable)的结果只有当variable的类型为引用时才是引用
- 类型别名(type alias)
- 自定义数据结构
- 不要忘了在类定义的最后加上分号
- C++ 11规定可以使用类内初始值(in-class initializer)初始化类成员,没有初始化的成员进行默认初始化
- 类内初始值或者放在等号的右边或者放在花括号内,记住不能使用圆括号
第三章 字符串、向量和数组
- 命名空间的using声明(using declaration)
using namespace::name;
- 头文件不应该包含using声明
- 标准库类型string
#include<string> using std::string;
- 初始化string对象的方式
string s1;//默认初始化 string s2(s1);//直接初始化 string s2=s1;//拷贝初始化 string s3("value");//最后的空字符没有拷贝 string s3="value";//最后的空字符没有拷贝 string s4(n, 'c'); //使用等号初始化一个变量执行的是拷贝初始化(copy initialization);不使用等号执行的是直接初始化(direct initialization)
- string对象上的操作
- 使用cin读取时,string会自动忽略开头的空白(空格符,制表符、换行符等),从第一个真正的字符开始读起直到遇见下一处空白为止
- 使用getline读取一整行
- 不会忽略空白字符
- getline从给定的输入流中读入内容直到遇到换行符(读入),然后把读入的内容存到string对象中(不存换行符);因此如果一开始就遇到换行符,那么string就是一个空字符串
- getline返回输入流,因此可以将getline的结果作为条件
- empty和size操作
string::size_type
类型- size函数的返回值
- 为无符号类型,因此不要和有符号数混用
- 与机器无关
- 比较string对象
- 为string对象赋值
- 两个string对象相加
- 字面值和string对象相加
- 必须确保每个加号两侧的运算对象至少有一个是string对象
- 为了和C语言兼容,C++中的字符串字面值并不是string对象
- 处理string对象中的字符
- cctype头文件包含处理字符相关的函数
- C++程序应该使用cname的头文件而不是name.h形式,标准库中的名字总能在命名空间std中找到
- 范围for(range for)
for(declaration:expression)
- expression表示一个序列对象,declaration负责定义一个变量
- 下标运算符
[]
- 接收的参数为
string::size_type
类型 - C++标准不要求标准库检查下标的合法性,一旦使用一个超出范围的下标,就会产生不可预知的结果
- 接收的参数为
- 标准库类型vector
#include<vector> using std::vector;
- vector是模板而不是类型,
vector<int>
才是类型 - vector不能接受引用作为其元素类型
- 某些编译器需要老式的声明语句来处理元素为vector的vector对象,例如
vector<vector<int> >
- 定义和初始化vector对象
vector<T> v1;//默认初始化,创建空的vector vector<T> v2(v1); vector<T> v2=v1; vector<T> v3(n, val); vector<T> v4(n);//每个元素执行默认初始化 vector<T> v5{a,b,c...};//如果提供的值不能用来进行列表初始化,编译器会使用这样的值来构造vector对象,相当于将花括号变为圆括号 vector<T> v5={a,b,c...};
- 向vector对象中添加元素
push_back
成员函数
- 比较有效的方法是先定义一个空的vector,在运行时向其添加具体值,在定义时指定容量性能可能更差
- 如果循环体内部包含有向vector对象添加元素的语句,那么不能使用范围for循环
- 范围for循环内不应该改变其所遍历序列的大小
- 其他vector操作
- 见表3.5
- size函数的返回值类型为
vector<T>::size_type
- 只有当两个vector所含元素个数相同并且对应位置的元素相等我们才说两个vector相等
- 只有当元素可比较时,vector对象才能被比较
- 下标运算符接受的类型为相应的
size_type
类型 - 不能用下标形式添加元素
- vector对象(包括string对象)的下标运算符只能用于访问已存在的元素,不能用于添加元素
- 迭代器(iterator)介绍
- 所有标准库容器都支持迭代器,但是只有少数几种才同时支持下标运算符
- string支持迭代器,但是严格意义上string不属于容器
- 使用迭代器
- 通过begin返回指向第一个元素(或第一个字符)的迭代器,end返回尾元素下一位置的迭代器,常被称为尾后迭代器(
off-the-end iterator
) - 如果容器为空,则begin和end都返回尾后迭代器
- 通过begin返回指向第一个元素(或第一个字符)的迭代器,end返回尾元素下一位置的迭代器,常被称为尾后迭代器(
- 标准容器迭代器的运算符见表3.6
- 如果两个迭代器指向的元素相同或者都是同一个迭代器的尾后迭代器,那么它们相等,否则不相等
- 因为所有标准库容器迭代器都定义了
==
和!=
,但是它们大多数没有定义<
,因此应该在循环中养成使用!=
的习惯 - 迭代器的类型
- 一般来说无须知道迭代器的精确类型,可以使用auto
- 对于常量容器对象,迭代器类型只能是
const_iterator
,否则迭代器类型可以是iterator或者const_iterator
- begin和end返回的迭代器类型由对象是否是常量决定
- cbegin和cend只会返回只读的迭代器
- 结合解引用和成员访问操作必须加括号:
(*iter).mem
- 可以使用箭头运算符一步到位:
iter->mem
- 可以使用箭头运算符一步到位:
- 但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素
- 迭代器运算(iterator arithmetic)
- vector和string支持的迭代器运算见表3.7
- 两个迭代器相减的结果类型为
difference_type
,为带符号类型 - 移动迭代器不会使迭代器超出范围
- 数组
- 定义和初始化内置数组
- 同指针和引用,数组也是一种复合类型
- 数组的维度必须是一个常量表达式
- 数组元素会被默认初始化,同内置类型
- 不允许使用auto来推断数组的类型
- 不允许定义引用类型的数组
- 可以使用列表初始化的方式显式初始化数组
- 对于字符数组可以使用字符串字面值的形式初始化,但是要预留空间给空字符
- 数组之间不允许直接拷贝和赋值
- 在某些编译器上可能支持,这就是所谓的编译器扩展(compiler extension)
- 理解复杂的数组声明
int *ptrs[10]; int &refs[10];//Wrong int (*Parray)[10]=&arr; int (&arrRef)[10]=arr;
- 访问数组元素
- 数组下标的类型通常被定义为
size_t
- 定义在cstddef头文件中
- 与机器相关的无符号类型
- 数组下标的类型通常被定义为
- 指针和数组
- 在用到数组名的地方,编译器会自动将数组名替换为指向数组首元素的指针
- auto会推断数组名为指针,decltype会推断数组名为数组
- 标准库函数begin和end
- 定义在iterator头文件中
- begin返回数组的首地址,end返回数组的尾后指针
- 可以通过
&arr[len]
的方式获得尾后指针
- 可以通过
- 指针运算
- 不像迭代器,移动指针可能会超过范围
- 两个指针相减的结果类型为
ptrdiff_t
,定义在cstddef头文件中,为带符号类型 - 允许给空指针加上或者减去结果为0的整型常量表达式
- 两个空指针相减的结果为0
- 下标和指针
- 对数组使用下标运算时,编译器会自动转换为指针的偏移操作
- 标准库使用的下标运算符接受的是无符号类型,内置的下标运算符可以接受带符号类型
- C风格字符串(C-style character string)
- 尽管C++支持C风格的字符串,但是最好不要使用它们,不方便且极易引发漏洞
- C标准库String函数
- 定义在cstring头文件中
- C风格字符串函数见表3.8
- 传入此类函数的指针必须指向以空字符作为结束的数组
- 与旧代码的接口
- 混用string对象和C风格字符串
- 允许使用空字符结束的字符数组来初始化string对象或为string对象赋值
- 允许使用空字符结束的字符数组作为string对象加法运算中的一个运算对象,或者在复合赋值运算中作为右侧的运算对象
- string可以通过
c_str
函数返回C风格的字符串- 返回值类型为
const char *
,防止被修改
- 返回值类型为
- 如果后续操作改变了string的值可能导致返回的数组失效
- 使用数组初始化vector对象
- 调用vector的某个构造函数,需要传入数组的首地址和尾后地址
- 现代C++程序应该使用vector和iterator代替内置数组和指针,使用string代替C风格字符串
- 定义和初始化内置数组
- 多维数组
- 使用rang for处理多维数组时,除了最内层的循环,其他所有循环的控制变量都应该是引用类型
- 因为auto会将数组名转换为指针
- 指针和多维数组
for(auto i=begin(arr);i!=end(arr);++i){ for(auto j=begin(*i);j!=end(*i);++j){ cout<<*j<<" "; } }
- 使用类型别名简化多维数组的指针
- 使用using或者typedef
- 使用rang for处理多维数组时,除了最内层的循环,其他所有循环的控制变量都应该是引用类型
第四章 表达式(expression)
- 表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result),把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式
- 基础
- 基本概念
- 重载运算符(overloaded operator)只能改变运算对象和返回值的类型,但是运算对象的个数、运算符的优先级和结合律都是无法改变的
- 左值(lvalue)和右值(rvalue)
- C++表达式不是左值就是右值,左值表达式能够位于等号的左侧,而右值不行
- 当一个对象被用作右值时,用的是对象的内容;当一个对象被用作左值时,用的是对象在内存中的位置
- 在需要右值的地方可以使用左值来代替,但是不能把右值当成左值使用
- 对于decltype,如果表达式的求值结果是左值,那么将得到引用类型
- 优先级和结合律
- 复合表达式(compound expression)是指含有两个或多个运算符的表达式
- 4.12节列出了全部的运算符
- 求值顺序
- 优先级和结合律规定了对象的组合方式,但是没有说明运算对象按照什么顺序求值
- 考虑形如
f()+g()*h()+j()
表达式:优先级规定g()和h()的返回值相乘,结合律规定按照从左到右的顺序计算加法,但是关于函数的调用顺序没有明确规定 - C++之所以没有明确运算符的求值顺序,是为了给编译器优化留下余地
- 考虑形如
- 以下运算符明确规定了求值顺序
- 逻辑与,逻辑或为短路求值(short-circuit evaluation)
- 条件运算符
- 逗号运算符
- 优先级和结合律规定了对象的组合方式,但是没有说明运算对象按照什么顺序求值
- 书写复合表达式的经验
- 不能明确确定优先级的时候使用括号来强制优先级
- 如果改变了某个对象的值,在表达式的其他地方就不要再使用这个运算对象了,除非改变运算对象的子表达式是另外一个子表达式的运算对象,它们之间就有明确的顺序了
- 基本概念
- 算术运算符
- 运算对象和求值结果都是右值
- 在表达式求值之前,小整数类型的运算对象被提升为较大整数类型,所有运算对象最终会转换成同一类型
- 对于大多数运算符,布尔类型的运算对象被提升为int类型
bool b=true; b=-b;//b仍然为true
- 对于大多数运算符,布尔类型的运算对象被提升为int类型
- 参与取余运算的运算对象必须是整数类型
- C++ 11规定对于整数除法,商一律向0取整(即直接丢弃小数部分)
- 除了由于负号导致溢出的情况,
m%(-n)==m%n
,(-m)%n==-(m%n)
- 逻辑和关系运算符
- 运算对象和求值结果都是右值,求值结果为布尔类型
- 进行比较运算时除非比较的对象是bool类型,否则不要使用布尔字面值true和false作为运算对象
- 赋值运算符
- 左侧运算对象必须是一个左值,运算结果是左侧的运算对象,并且是一个左值
- 如果左右两侧运算对象类型不同,右侧运算对象将转换成左侧运算对象的类型,虽然有时会转换失败
- C++11标准允许使用花括号表示的初始值列表作为赋值语句的右侧运算对象
- 对于左侧运算对象为内置类型,初始值列表最多只能包含一个值,如果该转换丢失信息的话那么编译器将报错
- 对于左侧运算对象为类类型,赋值运算的细节由类本身决定
- 无论左侧运算对象为何种类型,初始值列表都可以为空,编译器将创建一个值初始化的临时量将其赋给左侧运算对象
- 赋值运算满足右结合律
- 对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同或者可由右边对象类型转换得到
- 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号
- 复合赋值运算符保证只求值一次,使用普通运算符则求值两次,求值两次在宏中使用会带来副作用
- 递增和递减运算符
- 这两种运算符必须作用于左值,前置版本将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回
- 除非必须,否则不使用递增递减运算符的后置版本
- 后置版本会返回先前的值,如果不需要先前的值就会造成浪费
- 运算对象可按任意顺序求值,在复合表达式中要小心使用这两个运算符
*beg=toupper(*beg++);//行为是未定义的
- 成员访问运算符
- 包括点运算符和箭头运算符
- 解引用运算符的优先级低于两个成员访问运算符,因此需要适当地加上括号
(*p).size();
- 箭头运算符作用于指针类型的运算对象结果是一个左值,对于点运算符
- 如果成员所属对象是左值,那么结果就是左值
- 如果成员所属对象是右值,那么结果就是右值
- 条件运算符
- 形式为
cond ? expr1 : expr2;
- 当条件运算符两个表达式都是左值或者都能转换成同一种左值类型时,运算结果是左值;否则运算结果是右值
- 嵌套条件运算符的层次太多影响代码的可读性
- 条件运算符的优先级非常低,通常需要在它的两端加上括号
- 考虑在输出表达式中使用条件运算符
- 形式为
- 位运算
- 位运算适用于整数类型和bitset类型
- “小整数”类型的运算对象会被自动提升为较大的整数类型
- 没有明确规定如何处理有符号类型,因此建议将位运算用于处理无符号类型
- 对于G++编译器,有符号数进行的是符号右移
- 移位运算符
- 将经过移动(可能还进行了提升)的左侧运算对象的拷贝作为求值结果,为右值
- 右侧运算对象一定不能为负,并且值要严格小于结果的位数,否则会产生未定义的行为
- 对于G++编译器,对超过范围的移位数会对总位数取余
- 移位运算符(又叫IO运算符)满足左结合律
- 移位运算符的优先级比算术运算符的优先级低,但是比关系运算符、赋值运算符和条件运算符的优先级高,因此需要在适当的时候加上括号
- 位运算适用于整数类型和bitset类型
- sizeof运算符
- sizeof运算符返回一条表达式或一个类型名字所占的字节数
- 形式为
sizeof expr
,sizeof(type)
- 结果值为
size_t
类型的常量表达式
- 形式为
- sizeof的一些规则
- 对于引用类型得到的是被引用对象所占空间的大小
- 对于指针执行得到的是指针本身所占空间的大小
- 对于解引用指针得到的是指针指向对象所占空间的大小
- 这里体现了sizeof的右结合性
- sizeof并不实际计算运算对象的值,因此指针不需要有效
- 对于数组返回的是整个数组的大小,sizeof不会把数组当成指针处理
- 对string对象或者vector对象返回的是该类型固定部分的大小,而不是元素占用的空间
- sizeof运算符返回一条表达式或一个类型名字所占的字节数
- 逗号运算符(comma operator)
- 优先级最低
- 规定了求值顺序:按照从左向右的顺序依次求值
- 逗号运算符的最终结果是右侧表达式的值,如果右侧表达式的值是左值,那么最终的求值结果也是左值
- 经常在for循环中使用逗号运算符
- 类型转换
- 隐式转换(implicit conversion)自动执行,不需要人工介入
- 发生隐式转换的时机
- 在大多数表达式中,比int类型小的整数值首先提升为较大的整数类型
- 在条件中,将非布尔类型转换成布尔类型
- 在初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧对象的类型
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
- 函数调用时也会发生类型转换
- 算术转换(arithmetic conversion):将一种算术类型转换为另外一种算术类型
- 整型提升(integral promotion):负责把小整数类型提升为大整数类型
- 对于bool、char、signed char、unsigned char、short和unsigned short类型,只要它们的值能够存放在int里,就会被提升为int类型,否则提升为unsigned int
- 对于
wchar_t
,char16_t
,char32_t
提升为int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型 - 前提是转换后的类型能够容纳原类型所有可能的值
- 如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型
- 执行整型提升,如果结果类型匹配,无需进一步转换
- 如果运算对象要么都是带符号的,要么都是无符号的,则小类型运算对象转换成较大的类型
- 如果运算对象一个带符号,另一个无符号,如果无符号类型不小于带符号类型,将带符号类型转换为无符号类型(规则为取余);否则将无符号类型转换为带符号类型(转换结果依赖于机器)
- 整型提升(integral promotion):负责把小整数类型提升为大整数类型
- 其他类型隐式转换
- 数组名自动转换成指向数组首元素的指针
- 当数组用于decltype、取地址(&)、sizeof及typeid等运算符时,上述转换不会发生
- 指针转换
- 常数值0和字面值nullptr能够转换成任意指针类型
- 指向任意非常量的指针能够转换成
void *
- 指向任意类型的指针能够转换成
const void *
- 在有继承关系的类型间的转换
- 转换成布尔类型
- 如果指针或者算术类型非0转换为true,否则转换为false
- 转换成常量
- 允许将指向非常量的指针或者引用转换为指向相应常量类型的指针或者引用
- 类类型自定义转换
- 类类型能够定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换
- 数组名自动转换成指向数组首元素的指针
- 显式转换
- 虽然有时不得不使用强制类型转换(cast),但是这种方法本质上是非常危险的
- 命名的强制类型转换
- 形式为
cast-name<type>(expression);
static_cast
:任何具有明确定义的类型转换,只要不包含底层const都可以使用static_cast
const_cast
:只能改变运算对象的底层const,不能改变表达式的类型reinterpret_cast
:为运算对象的位模式提供较低层次上的重新解释,static_cast
只能用于明确定义的类型转换dynamic_cast
:支持运行时的类型识别
- 形式为
- 旧式的强制类型转换
- 函数形式的强制类型转换:
type (expr);
- C语言风格的强制类型转换:
(type) expr;
- 函数形式的强制类型转换:
- 强制类型转换干扰了正常的类型检查,因此建议不要使用强制类型转换
- 运算符优先级表
第五章 语句
- 简单语句
- 表达式语句(expression statement):执行表达式并丢弃掉求值结果
- 空语句(null statement):空语句只含有一个单独的分号
- 使用空语句时应该加上注释,从而令读者知道该语句是有意省略的
- 多余的空语句并非总是无害的
- 复合语句(compound statement)
- 使用花括号括起来的语句和声明序列,可能为空
- 复合语句也称为块(block),块不以分号结束
- 语句作用域
- 可以在if、switch、while和for语句的控制结构定义变量,在do while结构中不允许定义变量
- 定义在控制结构当中的变量只能在相应的语句的内部可见
- 条件语句
- 悬垂else(danging else):不同语言有不同的解决方法,C++将else匹配最近的if
- 最好在switch的最后一个case标签加上break,便于以后添加新的case标签
- 即使不需要处理default情况,定义一个default标签能够告诉程序的阅读者已经考虑到了默认的情况
- 标签不应该孤零零地出现,其后必须跟上一条语句或者另外一个case语句
- switch内部的变量定义
- 不允许跨过变量的初始化语句直接跳转到该变量作用域的另一个位置,这意味着如果变量只是定义而没有初始化那么是合法的
- 如果需要为某个case分支定义变量,应该把变量定义在块内
- 迭代语句
- 定义在while条件部分或循环体内的变量每次迭代都经历从创建到销毁的过程
- 范围for语句(range for)
- 形式为
for(declaration:expression) statement
- declaration部分定义变量,一般使用auto类型,为了改变列表的元素,需要定义为引用类型
- expression表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者vector、string等对象,这些对象的共同特点是都有返回迭代器的begin和end函数
- 每次迭代都会重新定义循环控制变量
- 在循环体内不能够改变expression表示序列的长度
- 形式为
- 跳转语句
- break statement负责终止离它最近的while、do while、for或switch语句
- continue statement终止离它最近的while、do while或for语句并立即开始下一次迭代
- goto statement从goto语句无条件跳转到同一函数内的另一条语句
- 不要在程序中使用goto语句,因为其降低了程序的可读性和可维护性
- 标签标示符独立于变量或其他标识符的名字,因此名字可以重用
- goto语句不能将程序的控制权从变量的作用域之外转移到作用域之内,也就是不能跳过变量的初始化定义;但是反过来是可以的;这一点类似于switch
- return语句在第六章中介绍
- try语句块和异常处理
- 异常是指存在于运行时的反常行为,这些行为超过了函数正常功能的范围
- throw表达式
- throw表达式由关键字throw和紧随其后的表达式组成,其中表达式的类型就是抛出的异常类型,throw表达式经常紧跟一个分号,构成表达式语句
- 例子:
throw runtime_error("error");
try-catch
语句- 在try语句块中声明的变量无法在外部使用,在catch语句块中也不能使用
- 当选中某个catch块执行后,程序跳转到try语句块的最后一个catch子句之后继续执行
- 当抛出异常时,寻找处理代码的过程与函数调用链相反
- 首先在抛出异常的函数搜索匹配的catch子句,如果没有则终止该函数
- 继续搜索调用它的函数,直到找到适当类型的catch子句为止
- 如果最终没能找到匹配的catch子句,程序转到名为terminate的标准库函数,该函数的行为与系统有关,一般会导致程序非正常退出
- 没有try语句块也意味着没有匹配的catch子句
- 对于那些确实要处理异常并继续执行的程序,编写异常安全的程序非常困难
- 标准异常
- 标准异常是C++标准库用于报告标准库函数遇到的问题,这些标准异常也可以用在用户的程序中
- exception头文件定义了exception异常类型,它只报告异常的发生不提供额外的信息
- stdexcept头文件定义了常用的异常类型,见表5.1
- new头文件定义了
bad_alloc
异常类型 - type_info头文件定义了
bad_cast
异常类型
- 对于exception、
bad_alloc
和bad_cast
只能以默认初始化的方式初始化对象- 对于其他的异常类型应该使用string对象或者C语言风格字符串初始化,不允许使用默认初始化
- 异常类型只定义了what成员函数,如果该异常类型有字符串初始值,则what返回该字符串;否则what返回的内容由编译器决定
- 标准异常是C++标准库用于报告标准库函数遇到的问题,这些标准异常也可以用在用户的程序中
- C++程序不会catch到异常,除非程序显式throw出异常
第六章 函数
- 函数基础
- 尽管实参和形参有对应关系,但是并没有规定实参的求值顺序,编译器能够以任意可行的顺序对实参求值
- 声明空形参列表
void func();//隐式声明
void func(void);//与C语言兼容的方式,显式声明
- 不仅函数声明时的形参名是可选的,函数定义时也可以不给形参命名,但是不影响调用函数需要的参数个数
- 函数的返回类型不可以是数组或者函数,但可以是对应类型的指针
- 局部对象
- 自动对象:就是普通的局部对象,执行的是默认初始化
- 局部静态对象:将局部对象定义为static类型可获得此种对象,执行的是值初始化
- 函数声明(函数原型,function prototype)
- 一个函数可以只有声明而没有定义,如果没有使用该函数的话
- 写上形参的名字可以帮助使用者更好地理解函数的功能
- 在函数头文件中声明函数原型,含有函数声明的头文件应该被包含到函数定义的源文件中,编译器负责验证函数定义和声明是否匹配(g++貌似不进行验证)
- 参数传递
- 在C++语言中,建议使用引用类型的形参代替指针
- 使用引用避免拷贝,如果函数无须改变引用对象的值,最好将其声明为常量引用
- 尽量使用常量引用的另一个原因是不能把const对象、字面值或者需要类型转换的对象传递给普通引用形参
- 某些类型不能拷贝,例如IO类
- 同其他初始化过程一样,形参的顶层const被忽略,因此可以传递常量或者非常量给形参
- 正因为顶层const被忽略,所以顶层const对函数重载没有影响
- 数组形参
- 当函数传递一个数组时,实际上传递的是指向数组首元素的指针;数组还有另外一个特性是不能直接复制
- 形参类型
const int *, const int[], const int[10]
是等价的,数组的大小对函数调用没有影响 - 管理指针形参常用的技术
- 使用标记指定数组长度,例如C风格字符串以空字符结尾
- 使用标准库规范,使用begin和end
- 显式传递一个表示数组大小的形参
- 形参类型
- 数组引用形参:维度也是类型的一部分,需要维度匹配才能调用,通过模板技术可以传递任意大小的数组
- 传递多维数组时,可以忽略第一维的大小,但是后面维的大小作为类型的一部分不能忽略
- 当函数传递一个数组时,实际上传递的是指向数组首元素的指针;数组还有另外一个特性是不能直接复制
- 对于main函数,argv第一个指针指向程序的名字或者空字符串,最后一个指针之后的元素值保证为0
- 含有可变形参的函数
- 所有实参类型相同,使用
initializer_list
initializer_list
提供的操作见表6.1initializer_list
也是一种模板类型,与vector不同的是,其中的元素是常量值,无法修改- 可以使用范围for遍历
- 实参类型不同,使用可变参数模板
- 省略符形参一般只用于于C函数交互的接口程序
- 省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象无法正常使用
- 只能出现在形参列表最后的位置,无须进行类型检查
- 所有实参类型相同,使用
- 返回类型和return语句
- 在含有return语句的循环后面应该也有return语句,如果没有程序就是错误的,很多编译器无法发现此类错误
- 有返回值函数
- 值是如何返回的:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果(不知道与不要返回局部对象的引用或指针有无关系)
- 不要返回局部对象的引用或指针
- 返回字符串字面值会转换成一个局部临时string对象,细节见P202
- 调用运算符的优先级和点运算符、箭头运算符相同,满足左结合律,因此,如果函数返回指针、引用或者类的对象,可以直接使用函数调用的结果访问对象成员
- 返回引用的函数可以得到左值,其他返回类型得到右值,特别地,能为返回类型为非常量引用的函数的结果赋值
- 列表初始化返回值
- 如果列表为空,临时量执行值初始化
- 否则返回值由函数返回类型决定
- 对于内置类型,花括号包围的列表最多包含一个值,并且不能出现精度丢失的情况
- 对于类类型,由类本身定义如何使用初始值
- 对于main函数,如果控制到达了函数的结尾处而且没有return语句,编译器隐式插入一条返回0的return语句
- 为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量表示成功或者失败
EXIT_SUCCESS
EXIT_FAILURE
- 为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量表示成功或者失败
- main函数不能递归调用自己
- 返回数组指针
- 使用类型别名typedef和using
- 传统声明:
Type (*func(paramlist))[dimension]
- C++ 11规定可以使用尾置返回类型(trailing return type)表示任何的函数定义
auto func(paramlist) -> return type
- 使用decltype
- 函数重载(overloaded)
- 如果同一个作用域内的几个函数名字相同但形参列表不同,称之为重载函数
- 函数重载在一定程度上可以减轻程序员起名字和记名字的负担
- main函数不能够重载
- 定义函数重载
- 函数重载不关心返回值是否相同
- 顶层const不能实现重载,对于指针和引用,底层const可以实现重载
- 当传递一个非常量的对象或者指向非常量对象指针时,编译器优先选择非常量版本
- 函数重载虽然能够减少函数名的数量,但是有时给函数起不同的名字能使得程序更加容易理解
const_cast
在重载的情景中最有用- 调用重载函数时的三种可能结果
- 编译器找到最佳匹配(best match),生成调用该函数的代码
- 编译器找不到匹配(no match),发出错误消息
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也产生错误,称为二义性调用(ambiguous call)
- 重载与作用域
- 一般来说,将函数声明置于局部作用域内是一个不明智的选择
- 在不同作用域内无法重载函数名,内层作用域中声明的名字将隐藏外层作用域中的同名实体
- 在C++中,名字查找发生于类型检查之前
- 如果同一个作用域内的几个函数名字相同但形参列表不同,称之为重载函数
- 特殊用途的语言特性
- 默认实参(default argument)
- 调用含有默认实参的函数时,可以包含该实参,也可以忽略该实参;一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值
- 在给定作用域中一个形参只能被赋予一次默认实参
- 函数后续声明只能为之前没有默认值的形参添加默认实参,而且该形参右侧所有形参必须都有默认值
- 局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参
- 用作默认实参的名字在函数声明的作用域内解析,而这些名字的求值过程发生在函数调用时
- 内联(inline)函数和constexpr函数
- 内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求
- 内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器不支持内联递归
- constexpr函数是指能用于常量表达式的函数
- 函数的返回类型和所有形参的类型都必须是字面值类型,而且函数体内必须有且只有一条语句,该语句为return
- 字面值类型包括算术类型、指针、引用、枚举以及字面值常量类
- constexpr函数被隐式指定为inline函数
- constexpr函数不一定返回常量表达式
- 函数的返回类型和所有形参的类型都必须是字面值类型,而且函数体内必须有且只有一条语句,该语句为return
- inline函数和constexpr函数定义在头文件中
- 编译器想要展开函数,仅有函数的声明是不够的,这两个函数不同于一般的函数,可以多次定义(因此可以放在头文件中),但是每个定义都必须一致
- 内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求
- 调试帮助
- assert预处理宏(preprocessor marco)
- 定义在cassert头文件中
assert(expr)
,首先对expr求值,如果为假(0)输出信息并终止程序执行,为真(非0)什么也不做
- NDEBUG预处理变量
- 如果定义了NDEBUG,assert什么也不做,默认情况下未定义NDEBUG,assert进行运行时检查
- assert应该仅用于那些确实不可能发生的事情,不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查
- 可以使用NDEBUG编写自己的条件调试代码,C++编译器定义了几个对调试有用的名字
__FILE__
__LINE__
__func__
__TIME__
:文件编译时间__DATE__
:文件编译日期
- assert预处理宏(preprocessor marco)
- 默认实参(default argument)
- 函数匹配
- 确定候选函数(candidate function),候选函数满足两个条件
- 与调用函数同名
- 在调用点处可见
- 确定可行函数(viable function),可行函数满足两个条件
- 形参数量和提供的实参数量相等,要考虑默认实参的情况
- 每个实参的类型与对应形参类型相同或者能够转换
- 如果没找到可行函数,编译器报无匹配函数错误
- 寻找最佳匹配,如果有且只有一个函数满足下列条件,则匹配成功
- 该函数每个实参的匹配都不劣于其他可行函数的匹配
- 至少有一个实参的匹配优于其他可行函数提供的匹配
- 如果没找到最佳匹配,编译器报二义性调用错误
- 调用重载函数时应该尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,说明设计的形参不合理
- 实参类型转换
- 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为以下等级
- 精确匹配
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换为对应的指针类型
- 向实参添加顶层const或删除顶层const
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或者指针转换实现的匹配
- 通过类类型转换实现的匹配
- 精确匹配
- 需要类型提升的匹配,小整数类型会提升为int类型或更大的整数类型
- 假设有两个函数,一个接受int,另一个接受short,只有当提供的实参为short类型时才会调用接受short的函数
- 所有算术类型转换的级别都相同
- double转换为float不会比转换为long级别更高
- 如果重载函数的区别在于它们的引用类型的形参是否引用了const或者指针类型的形参是否指向了const,编译器通过实参是否是常量来决定选择哪个函数
- 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为以下等级
- 确定候选函数(candidate function),候选函数满足两个条件
- 函数指针
- 函数类型由返回类型和形参类型共同决定,不同类型的函数指针之间不存在转换规则,值为0的整型常量表达式或nullptr可以赋给任何类型的函数指针
- 使用函数指针
pf=f;
pf=&f;
pf();
(*pf)();
- 对于重载函数指针,指针的类型必须与重载函数的某一个精确匹配
- 函数指针形参
- 函数类型作为形参时,编译器会自动转换为指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
- 类似于数组,decltype返回函数类型,不会将函数类型自动转换为指针类型
- 函数类型作为形参时,编译器会自动转换为指向函数的指针
- 返回指向函数的指针
- 必须把返回类型写成指针形式,编译器不会自动将函数类型转换为指向函数的指针
- 直接声明:
int (*f(int))(int, int);
- 使用类型别名:
using pf=int(*)(int, int);
- 尾置返回类型:
auto f(int)->int(*)(int, int);
第七章 类
- 定义抽象数据类型(abstract data type)
- 尽管所有的成员函数都必须在类内声明,但是成员函数体可以定义在类内或者类外
- 定义在类内的函数是隐式inline函数
- 定义在类外的函数必须包含它所属的类名
- this是一个指针常量,不允许改变this中保存的地址
- 常量成员函数(const member function):紧跟在参数列表后面的const表示this是一个指向常量的指针
- 常量成员函数不能改变调用它的对象上的内容
- 常量对象以及常量对象的指针和引用只能调用常量成员函数
- 编译器分两步处理类,首先编译成员的声明,然后才轮到成员函数,因此成员函数可以随意使用类中的其他成员而无需在意这些成员出现的次序
- 如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件中
- 构造函数(constructor)
- 构造函数不能被声明为const
- 当创建类的const对象时,直到构造函数完成初始化,对象才真正获得“常量”属性
- 合成的默认构造函数(synthesized default constructor)
- 初始化类成员的规则:如果存在类内初始值,用它来初始化成员,否则默认初始化
- 某些类不能够依赖默认的构造函数
- 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数
- 如果类包含有内置类型或者复合类型的成员,只有当这些成员全部被赋予了类内初始值时,这个类才适合使用合成的默认构造函数
- 如果一个类包含一个其他类型的成员且这个成员的类型没有默认的构造函数,那么编译器无法初始化该成员
Classname()=default;
:显式要求编译器生成默认的构造函数- 如果出现在类内,则默认构造函数是内联函数
- 否则默认构造函数默认不内联
- 构造函数初始值列表(constructor initialize list)
- 没有出现在此列表的成员按照合成的默认构造函数进行初始化
- 在初始值列表中可以使用表达式
- 构造函数不能被声明为const
- 拷贝、赋值和析构
- 如果我们不自定义这些操作,编译器将替我们合成它们,编译器生成的版本将对每个成员执行拷贝、赋值和销毁
- 合成的版本对某些类无法正常工作,特别是当类需要分配类对象之外的资源时
- 如果类包含vector或者string对象,则其拷贝、赋值和销毁的合成版本能够正常工作
- 尽管所有的成员函数都必须在类内声明,但是成员函数体可以定义在类内或者类外
- 访问控制和封装
- 访问说明符(access specifiers)
- public定义类的接口,private隐藏类的实现
- 没有严格限定访问说明符出现的次数
- 每个访问说明符的有效范围直到出现下个访问说明符或者到达类的结尾
- 使用class和struct定义类的唯一区别就是默认的访问权限
- 友元(friend)
- 将其它类或者函数声明为友元,可以访问类的非public成员
- 友元的声明只能出现在类内,友元不是类的成员不受所在区域的访问说明符的影响
- 一般来说,最好在类定义的开始或者结束前的位置集中声明友元
- 友元的声明仅仅指定了访问权限,而非通常意义上的函数声明
- 如果希望类的用户能够调用某个友元函数,必须在友元声明之外再专门对函数进行一次声明
- 通常把友元的声明与类本身放置在同一个头文件中
- 许多编译器并未强制限定友元函数必须在使用之前在类的外部声明(本地g++编译器有此要求)
- 类的其它特性
- 定义类型成员
- 受到访问说明符的限制
- 类型成员必须先定义后使用,因此通常在类开始的地方定义
- inline成员函数
- 定义在类内部的成员函数自动inline
- 可以在类内声明成员函数时使用inline,或者可以在类外定义成员函数时使用inline
- 最好在类外使用inline,使得类容易理解些
- inline成员函数必须和类的定义在同一个头文件中
- 重载成员函数
- 可变数据成员(mutable data member)
- 通过mutable关键字声明
- 一个可变数据成员永远不会是const,即使它是const对象的成员
- 常量成员函数可以改变可变数据成员
- 提供类内初始值时,只能以等号或者花括号的形式提供
- 一个const成员函数如果以引用形式返回
*this
,那么它的返回类型将是常量引用 - 基于const重载:对于常量对象调用const版本成员函数,对于非常量对象调用非const版本成员函数
- 类的声明(前向声明,forward declaration)
- 在声明之后定义之前,类是一个不完全类型(incomplete type)
- 不完全类型的使用场景受限
- 可以定义指向不完全类型的指针或引用,但是只有定义类之后,才可以使用指针或者引用访问成员
- 可以声明但是不能定义以不完全类型作为参数或者返回类型的函数
- 一个类的成员类型不能是类本身,但是类允许包含指向它自身类型的引用或者指针,一旦一个类的名字出现后,它就被认为是声明过了
- 当类成员为引用类型时,不能使用默认构造函数,必须在构造函数的初始值列表中初始化
- 友元再探
- 如果一个类指定了友元类,则友元类的所有成员函数都可以访问此类包括非公有成员在内的所有成员
- 友元关系不能传递,每个类负责控制自己的友元函数或者友元类
- 令成员函数作为友元,需要组织程序的结构以满足声明和定义的依赖关系
- 如果一个类想把一组重载函数声明为它的友元,需要对这组函数的每一个分别声明
- 友元声明和作用域
- 友元声明的作用是影响访问权限,本身并非普通意义上的声明
- 类和非成员函数的声明不是必须在它们的友元声明之前,当一个名字第一次出现在友元的声明中,隐式地假定该名字在当前作用域中可见,然而友元本身不一定真的声明在当前作用域中
- 就算在类内部定义友元函数,也必须在类外部提供相应的声明使得函数可见
- 如果一个类指定了友元类,则友元类的所有成员函数都可以访问此类包括非公有成员在内的所有成员
- 定义类型成员
- 访问说明符(access specifiers)
- 类的作用域
- 作用域和定义在类外部的成员
- 参数列表和函数体在类的作用域之内,可以直接使用类的其他成员
- 返回类型在类的作用域之外,必须指明它是哪个类的成员
- 一般作用域名字查找(name lookup)过程
- 首先,在名字所在的块中寻找声明语句,只考虑在名字使用之前出现的声明
- 如果没找到,继续查找外层作用域
- 如果最终没有找到匹配的声明,则程序报错
- 类的定义分两步处理
- 首先编译成员的声明
- 直到类全部成员可见后才编译函数体
- 用于类成员声明的名字查找
- 两阶段的处理方式只适用于成员函数体中使用的名字,对于声明中使用的名字(包括返回类型,参数列表使用的类型)都必须在使用前可见
- 使用一般作用域名字查找过程
- 类型名要特殊处理
- 一般来说,内层作用域可以重新定义外层作用域的名字,即使该名字已经在内层作用域使用过
- 如果成员使用了外层作用域中的名字并且该名字表示类型,则类不能在之后重新定义该名字
- 类型名的定义通常出现在类定义的开始处,这样就能确保所有成员使用同样的类型
- 成员定义中的普通块作用域的名字查找
- 首先,在成员函数内查找该名字的声明,只考虑在名字使用之前出现的声明
- 如果在成员函数内没有找到,则在类内继续查找,这时类内所有成员都可以被考虑(因为类的两阶段处理)
- 如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找
- 尽管外层对象被隐藏掉了,仍然可以使用作用域运算符访问它
- 成员函数中的名字本来不应该隐藏同名的成员,但即使隐藏了,也可以使用类名作用域或者this指针来强制访问成员
- 使用全局作用域符在全局中查找名字
- 作用域和定义在类外部的成员
- 构造函数再探
- 构造函数初始值列表
- 如果没有在构造函数初始值列表显式初始化成员,则该成员将在构造函数体之前执行默认初始化(如果支持类内初始化,先进行类内初始化)
- 初始化和赋值事关底层效率问题,前者直接初始化,后者先初始化再赋值,因此建议使用初始化
- 如果成员是const、引用或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表(或者类内初始化)为这些成员提供初始值
- 成员初始化的顺序与成员在类中定义的顺序相同,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序
- 最好令构造函数初始值的顺序与成员声明的顺序保持一致,如果可能的话,尽量避免使用某些成员初始化其他成员
- 如果一个构造函数为所有参数都提供了默认值,则它实际上也定义了默认构造函数
- 如果没有在构造函数初始值列表显式初始化成员,则该成员将在构造函数体之前执行默认初始化(如果支持类内初始化,先进行类内初始化)
- 委托构造函数(delegating constructor)
- 使用它所属类的其他构造函数执行它自己的初始化过程
- 一个委托构造函数有一个初始值列表和一个函数体,初始值列表唯一的入口就是类名本身,在函数体中可以有自己的操作
- 默认构造函数的作用
- 当对象被默认初始化或值初始化时自动执行默认构造函数,在这两种情况中,类必须提供默认构造函数
- 默认初始化在以下情况发生
- 在块作用域中不使用任何初始值定义一个非静态变量或者数组时
- 当一个类本身含有类类型成员且使用合成的默认构造函数时
- 当类类型成员没有在构造函数的初始值列表中显式初始化
- 值初始化在以下情况发生
- 在数组初始化过程中提供的初始值数量少于数组的大小时
- 当不使用初始值定义一个局部静态变量时
- 通过书写形如
T()
显式请求值初始化
- 使用默认构造函数
Class obj();//定义了一个函数而非对象
Class obj;//使用默认构造函数定义了一个对象
- 隐式类类型转换
- 发生隐式转换的时机见之前讨论
- 转换构造函数(converting constructor):通过一个实参调用的构造函数定义了一条从构造函数参数类型向类类型隐式转换的规则
- 只允许一步类类型转换,这一步就是编译器拿着给定的实参类型去匹配形参类型的构造函数
- 使用explicit抑制构造函数定义的隐式转换
- 关键字explicit只对只有一个实参的构造函数有效
- 只能在类内部声明时使用explicit,不能在类外部定义时使用
- explicit构造函数只能用于直接初始化
- 可以使用explicit构造函数进行显式强制类型转换
- 聚合类(aggregate class)
- 使得用户可以直接访问其成员,并且具有特殊的初始化语法形式
- 聚合类的条件
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类也没有virtual函数
- 可以提供一个花括号括起来的成员初始值列表,用它来初始化聚合类的数据成员
- 初始值的顺序必须与声明的顺序一致
- 如果初始值的数量少于数据成员的数量,靠后的成员被值初始化,但是初始值数量不能超过数据成员数量
- 显式初始化类的成员存在的缺点
- 要求类的所有成员都是public
- 将正确初始化每个对象的每个成员的任务交给了类的用户,类的用户不太可靠
- 添加或删除一个成员之后,所有初始化语句都需要更新
- 字面值常量类
- 除了算术类型、引用和指针外,某些类也是字面值类型
- 数据成员都是字面值类型的聚合类是字面值常量类;如果一个类不是聚合类,但是满足以下要求,则它也是一个字面值常量类
- 数据成员都必须是字面值类型
- 类必须至少包含一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr函数
- 类必须使用析构函数默认定义,该成员负责销毁类的对象
- constexpr构造函数
- constexpr构造函数可以声明为default形式或者删除函数形式,constexpr构造函数体一般为空
- constexpr构造函数必须初始化所有数据成员,初始值可以使用constexpr构造函数或者常量表达式
- constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型
- 类的constexpr函数是隐式const的
- 构造函数初始值列表
- 类的静态成员
- 静态成员函数不与任何对象绑定在一起,因此不包含this指针,没了this指针就不能被声明为const,不能使用其他非静态的成员
- 定义静态成员
- static关键字只能出现在类内
- 静态成员函数可以在类内定义也可以在类外定义
- 静态数据成员必须在类外定义和初始化
- 为了确保静态数据成员只定义一次,可以把静态数据成员的定义和其他非内联函数的定义放在同一个文件中
- 对于constexpr static只能在类内初始化,但是const static可以在类外初始化
- 如果某个静态成员的应用场景仅限于编译器可以替换的它的值的情况,则一个初始化的const static或constexpr static不需要在类外定义,但是通常情况也应该在类外定义,但是不能再提供初始值
- 静态成员适用但是普通成员不能使用的场合
- 静态数据成员可以是不完全类型,特别地,静态数据成员的类型可以是它所属的类类型
- 静态数据成员可以作为默认参数,函数声明可以出现在静态数据成员之前
第八章 IO库
- IO库
- IO类
- IO库类型和头文件见表8.1
- IO对象无拷贝或赋值
- 形参和返回值类型需要设置为流类型的引用,读写一个IO对象会改变其状态,因此不能是常引用
- 条件状态(condition state)
- IO库条件状态见表8.2
- 一个流一旦发生错误,其上后续的IO操作都会失败,应该在使用一个流之前检查它是否处于良好状态
- 查询流状态:使用fail和good查询流总体状态;将流对象用于条件判断
- 管理输出缓冲
- 导致缓冲区刷新的原因
- 程序正常结束,作为main函数return操作的一部分
- 缓冲区满时,需要刷新缓冲,新的数据才能写入缓冲区
- 使用操作符显式刷新缓冲区
- endl:输出换行并刷新
- ends:输出空字符并刷新
- flush:刷新缓冲区不输出额外字符
- unitbuf告诉流接下来的每次写操作之后都进行一次flush操作,nounitbuf则重置流
- cerr是设置unitbuf的
- 输出流可以被关联到另一个输入输出流,当读写被关联流时,输出流会被刷新
- cin和cerr默认被关联到cout,因此当读写cin和cerr时,会导致cout的缓冲区被刷新
- 交互式系统应该关联输入流和输出流,这样能够确保在读入数据前提示信息已经输出
- 通过tie函数查看或者关联输出流
- 每个流最多同时关联一个ostream,但是多个流可以同时关联到一个ostream
- 如果程序异常终止,输出缓冲区不会被刷新,因此在调试程序时需要注意
- 导致缓冲区刷新的原因
- IO类
- 文件输入输出
- 使用文件流对象
- 根据里氏替换原则,可以使用fstream代替iostream
- 成员函数open和close
- 如果open失败,failbit会被置位;需要通过查询流的状态判断open是否成功
- 对一个已经打开的文件流调用open会失败,导致failbit被置位,随后试图使用文件流的操作都会失败
- 为了将文件流关联到另一个文件,需要先关闭已经关联的文件
- 如果open成功,则open会重新设置流的状态,使得
good()
为true
- 当一个fstream对象被销毁时,close会自动调用
- 文件模式(file mode)
- 文件模式见表8.4
- 指定文件模式有如下限制
- 只可以对ofstream或者fstream设置out
- 只可以对ifstream或者fstream设置in
- 只有当out被设置时才可以设置trunc
- 只要trunc没被设置,就可以设置app。在app模式下,即使没有显式指定out,文件也总是以输出模式打开
- 即使没有指定trunc,以out方式打开的文件也会自动截断,为了保留文件内容,可以
- 同时指定app
- 同时指定in
- ate和binary可以与其他任何文件模式结合使用
- 默认文件模式
- 与ifstream关联的文件以in模式打开
- 与ofstream关联的文件以out模式打开
- 与fstream关联的文件以in和out模式打开
- 使用文件流对象
- string流
- stringstream的操作见表8.5
第九章 顺序容器
- 一个容器就是一些特定类型对象的集合。顺序容器(sequential container)为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应
- 顺序容器概述
- 顺序容器的类型见表9.1
- vector和string将元素保存在连续的内存中,支持随机访问,但是在尾部之外的位置插入删除元素很慢,添加元素可能需要分配额外的存储空间
- list(双向链表)和
forward_list
(单向链表)在任何位置添加或删除元素都很快,不支持随机访问,会带来额外的开销 - deque支持随机访问,在除了两端的位置添加或删除元素可能很慢
- array的大小固定,比内置数据更加安全、更容易使用;
forward_list
达到与最好的手写单向链表相当的性能,但是没有size操作,其他容器的size操作都是常数时间
- 现代C++程序应该使用标准库容器,而不是原始的数据结构,比如内置数据
- 选择顺序容器的基本原则
- 除非有很好的理由选择其他容器,否则使用vector
- 如果程序有很多小元素,并且空间的额外开销很重要,不要使用list或
forward_list
- 如果程序要求随机访问元素,使用vector或deque
- 如果程序要求在容器中间插入或删除元素,使用list或
forward_list
- 如果程序需要在头尾位置插入或删除元素,但不会在中间位置插入或删除,使用deque
- 如果程序只有在读取数据时才需要在容器中间插入元素,随后需要随机访问元素,则在输入阶段使用list,一旦完成后,将list拷贝到vector
- 如果程序既需要随机访问元素,又需要在容器中间插入元素,结果取决于list(
forward_list
)访问元素和vector(deque)插入元素的相对性能,一般应用中占主导地位的操作决定了容器的类型 - 如果不确定应该使用哪种容器,可以在程序中使用vector和list公有的操作迭代器,不要使用下标操作,这样在必要时候使用vector和list都很方便
- 顺序容器的类型见表9.1
- 容器库概览
- 在容器中可以保存几乎任何类型,但某些容器操作对元素类型有特殊要求,例如
vector<noDefault> v(10, init); //对于没有默认构造函数的类型必须提供初始化器
- 容器操作见表9.2
- 迭代器
- 表3.6列出了迭代器支持的所有操作,
forward_list
不支持递减运算符;表3.7列出了迭代器支持的算术运算,只能应用于string、vector、deque和array的迭代器 - 迭代器范围(iterator range)由一对迭代器表示,它们表示的范围为左闭合区间(left-inclusive interval)
- 表3.6列出了迭代器支持的所有操作,
- begin和end成员
- 不以c开头的函数(begin, end, rbegin, rend)都是被重载过的,意味着当常量对象调用这些方法时,返回的是
const_iterator
类型;否则返回iterator类型 - 以c开头的函数是新标准引入的,无论是否为常量对象,都返回
const_iterator
类型
- 不以c开头的函数(begin, end, rbegin, rend)都是被重载过的,意味着当常量对象调用这些方法时,返回的是
- 容器定义和初始化
- 见表9.3
- 将一个容器初始化为另一个容器的拷贝
C c1(c2);
,C c1=c2;
:要求两个容器的类型和元素的类型匹配C c(b, e);
:仅仅要求元素的类型相容,容器类型和元素类型可以不同
- 标准库array具有固定大小
- 标准库array的大小是类型的一部分
array<int, 5> arr;
- 对于默认构造的array,元素进行默认初始化
- 对于初始值列表初始化的array,剩余的元素使用值初始化
- array支持拷贝和赋值操作
- 标准库array的大小是类型的一部分
- 赋值和swap
- 容器赋值运算见表9.4
- 赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效;swap则不会(除了容器类型为array和string的情况)
- 使用赋值运算符=
- 左边元素的数量将和右边的一样
- 运算对象必须具有相同的类型
- 使用assign函数(仅顺序容器)
- 仅仅要求元素的类型相容,容器类型和元素类型可以不同
- 传递给assign的迭代器不能指向调用assign的容器
- assgin的第二个版本接受一个整型值和一个元素值
- 使用swap
- 要求两个容器类型相同
- 除了array,swap只是交换两个容器的内部结构,因此可以在常数时间内完成
- 统一使用非成员版本的swap是一个好习惯
- 容器大小操作
- size函数返回容器元素的数目(
forward_list
不支持) - empty函数
max_size
函数返回一个大于或等于该类型容器所能容纳的最大元素数
- size函数返回容器元素的数目(
- 关系运算符
- 每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>,>=,<,<=)
- 只有当元素类型也定义了相应的关系运算符,才可以使用关系运算符比较两个容器
- 相等运算符通过使用元素的==运算符实现,其他关系运算符使用元素的<运算符
- 关系运算符的运算对象必须是相同的容器类型并且保存相同类型的元素
- 关系运算符的工作方式可以类比string
- 每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>,>=,<,<=)
- 在容器中可以保存几乎任何类型,但某些容器操作对元素类型有特殊要求,例如
- 顺序容器操作
- 向顺序容器添加元素
- 见表9.5
- 向vector、string和deque插入元素会使所有指向容器的迭代器、引用和指针失效
- 使用
push_back
- array和
free_list
不支持 - 当用一个对象来初始化容器或将一个对象插入到容器时,实际上放入到容器中的是对象值的拷贝
- array和
- 使用
push_front
- array、vector和string不支持
- 在容器特定位置添加元素
- insert将元素插入到迭代器之前的位置
- 对不支持
push_front
操作的容器,可以使用insert实现类似的功能- 将元素插入到vector、string和deque的任意位置都是合法的,但是可能很耗时
- 在新标准中,insert函数返回第一个新加入元素的迭代器
- 使用emplace操作
emplace_front
,emplace
,emplace_back
分别对应push_front
,insert
,push_back
操作- emplace在容器中直接构造元素而不是拷贝元素,传递给emplace函数的参数必须与元素类型的构造函数相匹配
- 访问元素
- 见表9.6
- 每个顺序容器都有front函数,除了
forward_list
都有back函数 - 访问成员函数返回的是引用
- 访问成员函数包括front, back, 下标和at
- 对于常量容器返回常引用
- 下标操作和安全的随机访问
- 如果下标越界,at成员函数会抛出越界异常,其他访问成员函数不会进行越界检查
- 在调用访问成员函数之前需要使用empty函数判断容器是否为空
- 删除元素
- 见表9.7
- 删除deque中除首尾位置之外的元素都会使所有的迭代器、引用和指针失效;指向vector或string删除点之后位置的迭代器、引用和指针都会失效
- 删除元素的成员函数并不检查其参数,程序员必须确保它们是存在的
pop_front
和pop_back
成员函数- 与访问成员函数类似,不能对一个空容器执行弹出操作
- 这些操作返回void,如果需要必须在执行弹出操作之前保存它
- 从容器内部删除元素
- erase返回删除元素之后位置的迭代器
- clear可以删除整个容器的所有元素
- 特殊的
forward_list
操作- 因为
forward_list
实现为单向链表,没有简单的方法来获取一个元素的前驱,因此定义了insert_after
,emplace_after
,erase_after
来插入或删除指定迭代器之后的元素 before_begin
函数返回首前(off-the-beginning)迭代器,允许在首部进行插入删除操作
- 因为
- 改变容器大小
- 如果没有提供初始值,resize对新增的元素进行值初始化
- 如果resize缩小容器大小,指向被删除元素的迭代器、引用和指针都会失效;对vector、string和deque进行resize可能导致迭代器、引用和指针失效
- resize只能改变容器元素的数量,不能改变容量
- 容器操作可能使迭代器失效
- 使用失效的迭代器、引用或指针是严重的运行时错误
- 向容器添加元素导致失效
- 对于vector和string,如果存储空间重新分配,则指向容器的迭代器、引用和指针都会失效;如果存储空间未重新分配,插入位置之前的迭代器、引用和指针仍有效,之后的无效
- 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、引用和指针失效;如果在首尾位置添加元素,迭代器会失效,但是指向存在元素的引用和指针不会失效
- 对于list和
forward_list
,指向容器的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效
- 删除容器中的元素,指向被删除元素的迭代器、引用和指针都会失效
- 对于list和
forward_list
,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效 - 对于deque,删除除首尾位置之外的任何位置都会导致迭代器、引用和指针失效;如果删除尾元素,则尾后迭代器会失效,但其他迭代器、引用和指针仍有效;如果删除首元素则没有影响
- 对于vector和string,指向被删除元素之前元素的迭代器、引用和指针仍有效
- 对于list和
- 向容器添加元素导致失效
- 如果在一个循环中插入或删除deque、string或vector中的元素,不要缓存end返回的迭代器
- 使用失效的迭代器、引用或指针是严重的运行时错误
- vector对象是如何增长的
- 如果没有空间容纳新元素,vector分配新的内存空间,将原来的元素移动到新内存空间,添加新元素,释放旧内存空间,vector和string实现通常会分配比实际需求更大的内存空间来提高效率
- 管理容量的成员函数
- 见表9.10
- 只有当reserve请求的内存空间超过当前容量时,才会改变容器容量,至少分配与reserve请求一样大的内存空间
- resize只会改变容器元素的数量不会改变容器容量
- size返回当前容器存储元素数量,capacity返回容器总共能够容纳的元素数量
shrink_to_fit
请求将capacity减少为与size同等大小,退回不需要的内存空间,但不保证一定退回
- 每个vector实现可以选择自己的内存分配策略,但必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间
- 额外的string操作
- 构造string的其他方法
- 见表9.11
- substr操作
- 改变string的其他方法
- 见表9.13
- append和replace操作
- 构造string的其他方法
- string搜索操作
- 见表9.14
- compare函数
- 见表9.15
- 数值转换
- 见表9.16
- 向顺序容器添加元素
- 容器适配器(adaptor)
- 适配器是标准库中的通用概念,容器、迭代器和函数都有适配器。一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样
- 所有适配器都支持的操作和类型见表9.17
- 定义一个适配器
- 默认情况下,stack和queue基于deque实现的,
priority_queue
基于vector实现 - stack可以接受vector、deque和list作为容器
- queue可以接受deque和list作为容器
priority_queue
可以接受vector和deque作为容器
- 默认情况下,stack和queue基于deque实现的,
- 栈适配器
- 栈操作见表9.18
- 队列适配器
- queue和
priority_queue
定义在queue头文件中 - 队列/优先队列操作见表9.19
- queue和
第十章 泛型算法(generic algorithm)
- 概述
- 大多数算法定义在头文件algorithm中, 头文件numeric中定义了一组数值泛型算法
- 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作
- 泛型算法本身不会执行容器操作,它们只会运行于迭代器之上,执行迭代器操作,因此有一个编程假定:算法永远不会改变底层容器的大小。但是可以通过插入迭代器来实现
- 初识泛型算法
- 只读算法
- find
- count
- accumulate
- 第三个参数的类型决定了使用哪个加法符号以及返回值的类型
- equal
- 可以比较不同类型容器的元素,元素的类型也可以不同
- 只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长
- 写容器元素的算法
- 向目的位置迭代器写入数据的算法假定目的位置足够大,能够容纳要写入的元素
- fill和
fill_n
- fill和
- 介绍
back_inserter
- 定义在头文件iterator中
- 对插入迭代器(insert iterator)赋值,使得与赋值号右侧值相等的元素被添加到容器中
- 对
back_inserter
返回的插入迭代器赋值相当于调用push_back
- 拷贝算法
- copy
- “拷贝”版本的算法,例如
replace_copy
- 向目的位置迭代器写入数据的算法假定目的位置足够大,能够容纳要写入的元素
- 重排容器元素的算法
- sort:利用元素类型的<运算符实现排序,不稳定排序
- unique:“消除”相邻重复出现的元素,该算法不会改变容器的大小
- 只读算法
- 定制操作
- 向算法传递函数
- 谓词(predicate):是一个可调用的表达式,返回结果是一个能用作条件的值,标准库使用的谓词可以分为
- 一元谓词(unary predicate):只接受一个参数
- 二元谓词(binary predicate):接受两个参数
- 谓词(predicate):是一个可调用的表达式,返回结果是一个能用作条件的值,标准库使用的谓词可以分为
- lambda表达式(lambda expression)
- 可调用对象(callable object):对其可以使用调用运算符, 可以向算法传递以下类别的可调用对象
- 函数
- 函数指针
- 重载了函数调用运算符的类
- lambda表达式
- bind创建的对象
- 一个lambda表达式表示一个可调用的代码单元,可以将其理解为未命名的内联函数
- lambda表达式的形式
[capture list](parameter list)->return type{function body}
- 可以忽略参数列表和返回类型,如果函数体内部只有一条return语句,则返回类型从return语句推断而来
- 捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它之前声明的全局变量
- lambda表达式不能有默认参数
find_if
算法for_each
算法
- 可调用对象(callable object):对其可以使用调用运算符, 可以向算法传递以下类别的可调用对象
- lambda捕获和返回
- lambda捕获列表形式见表10.1
- 当定义一个lambda时,编译器生成一个与lambda对应的未命名的类类型,该类包含对应该lambda所捕获变量的数据成员
- 值捕获
- 被捕获的变量在lambda创建时拷贝,意味着随后的修改不会影响lambda内对应的值
- 引用捕获
- 必须保证在lambda执行时引用的变量是存在的
- 因为标准IO对象不能拷贝,只能使用引用捕获
- 尽量避免捕获迭代器、引用和指针
- 隐式捕获:在捕获列表中写一个
&
或=
,让编译器根据lambda中的代码推断需要捕获的变量- 可以混合使用隐式捕获和显式捕获
- 可变lambda
- 默认情况下,对于值捕获的变量,lambda不能改变其值
- 为了改变值捕获的变量,必须在参数列表后加上mutable,因此不能省略参数列表
- lambda使用尾置返回类型
- 参数绑定
- 标准库bind函数
- 定义在头文件functional中
- 接受一个可调用对象生成一个新的可调用对象来“适应”原对象的参数列表
- 使用placeholders名字
- 名字
_n
定义在placeholders命名空间中using namespace std::placeholders;
- 名字
- 绑定引用参数
- bind需要借助ref函数传递引用
- ref和cref函数定义在头文件functional中
- 标准库bind函数
- 向算法传递函数
- 再探迭代器
- 插入迭代器(insert iterator)是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素
back_inserter
:创建一个使用push_back
的迭代器front_inserter
:创建一个使用push_front
的迭代器inserter
:创建一个使用insert的迭代器,此函数接受第二个参数,元素将被插入到第二个参数表示的迭代器之前的位置*it=val;//it为inserter创建的迭代器,等价于以下代码 it=insert(it, val); ++it;//恢复it为原来的位置
front_inserter
会颠倒插入序列的顺序,back_inserter
和inseter不会
- iostream迭代器
istream_iterator
操作- 见表10.3
istream_iterator
要绑定的类型必须定义了输入运算符- 默认初始化的迭代器可以作为尾后迭代器使用
- 直接从输入流构造容器
- 允许使用惰性求值:标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取
ostream_iterator
操作- 见表10.4
ostream_iterator
要绑定的类型必须定义了输入运算符- 必须将
ostream_iterator
绑定一个指定的流,不允许空的迭代器 - 可以结合copy来打印容器中的元素
- 虽然运算符
*
和++
对iostream迭代器不做任何事情,但还是建议加上以保持与一般迭代器的一致性
- 反向迭代器
- 反向迭代器只能从支持
++
和--
的迭代器定义,因此forward_list
和流迭代器不支持反向迭代器 - 反向迭代器和其他迭代器间的关系
- 通过调用反向迭代器的base成员函数将反向迭代器转换为普通迭代器
- 普通迭代器和反向迭代器并不是指向同一个位置,它们都要满足左闭合区间的要求,详见图10.2
- 反向迭代器只能从支持
- 插入迭代器(insert iterator)是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素
- 泛型算法结构
- 5类迭代器
- 迭代器类别见表10.5
- 除了输出迭代器,高层类别的迭代器支持低层类别迭代器的所有操作
- C++标准指明了泛型和数值算法的每个迭代器参数需要的最小类别
- 传递的迭代器能力必须与规定的最小类别相当,传递一个能力更差的迭代器会产生错误,但是很多编译器不会给出提醒
- 迭代器的类别
- 输入迭代器(input iterator)
istream_iterator
- 输出迭代器(output iterator)
ostream_iterator
- 前向迭代器(forward iterator)
forward_list
的迭代器
- 双向迭代器(bidirectional iterator)
- 除了
forward_list
,其他标准库容器都提供双向迭代器
- 除了
- 随机访问迭代器(random-access iterator)
- array、deque、string和vector的迭代器,用于访问内置数组的指针都是随机访问迭代器
- 输入迭代器(input iterator)
- 算法形参模式
- 接受单个目标迭代器的算法
- 向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据
- 接受第二个输入序列的算法
- 接受单独beg2的算法都假定从beg2开始的序列与beg和end表示的范围至少一样大
- 接受单个目标迭代器的算法
- 算法的命名规范
- 一些算法使用重载方式传递一个谓词
- 接受谓词参数来代替
<
或==
运算符
- 接受谓词参数来代替
_if
版本的算法- 接受一个元素值的算法通常有另外一个版本,该版本接受一个谓词来代替元素值
- 例如find和
find_if
- 区分拷贝元素的版本和不拷贝的版本
- 重排元素的算法通常还提供将元素写到一个指定目的位置的版本
- 写到额外目的位置的算法名字后面附加一个
_copy
- 一些算法使用重载方式传递一个谓词
- 5类迭代器
- 特定容器算法
- list和
forward_list
成员函数版本的算法见表10.6- 对于list和
forward_list
应该优先使用成员函数版本的算法而不是通用的算法,因为前者通常拥有更好的性能
- 对于list和
- splice拼接算法,此算法是链表数据结构特有的,见表10.7
- 链表特有操作会改变容器
- remove和unique的链表版本会改变底层容器
- merge和splice会销毁其参数
- list和
第十一章 关联容器(associative-container)
- 关联容器的类型见表11.1
- 8个容器的不同体现在3个维度
- 或者是一个set,或者是一个map
- 或者要求不重复的关键字,或者允许重复的关键字(multi)
- 或者按顺序保存元素,或者无序保存(unordered)
- map和multimap定义在头文件map中,set和multiset定义在头文件set中;无序容器定义在
unordered_map
和unordered_set
中
- 8个容器的不同体现在3个维度
- 使用关联容器
- 关联容器概述
- 关联容器的迭代器都是双向迭代器
- 定义关联容器
- 新标准下,关联容器可以进行值初始化
- 关键字类型的要求
- 关键字类型必须定义元素比较的方法
- 默认情况下,标准库使用关键字类型的
<
运算符进行比较 - 可以提供自己定义的比较函数,但是有一些要求,见P378
- 默认情况下,标准库使用关键字类型的
- 使用关键字类型的比较函数
- 在尖括号出现的每个类型仅仅是一个类型而已,当创建一个容器时,才会以构造函数参数的形式提供真正的比较操作
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);//注意当作为类成员时需要将圆括号改为花括号
- 在尖括号出现的每个类型仅仅是一个类型而已,当创建一个容器时,才会以构造函数参数的形式提供真正的比较操作
- 关键字类型必须定义元素比较的方法
- pair类型
- 定义在头文件utility中
- pair的默认构造函数对数据成员进行值初始化
- pair上的操作见表11.2
- 关联容器的操作
- 关联容器额外的类型别名见表11.3
- 关联容器迭代器
- 解引用一个关联容器的迭代器时,得到的类型为容器的
value_type
- 对于map,
value_type
为pair类型,其first成员保存const的关键字,second成员保存值 - 对于set,其迭代器都是const的
- 对于map,
- 当使用一个迭代器遍历map、multimap、set或multiset时,迭代器按关键字升序遍历元素
- 关联容器和算法
- 通常不对关联容器使用泛型算法
- 解引用一个关联容器的迭代器时,得到的类型为容器的
- 添加元素(insert, emplace)
- 关联容器的insert操作见表11.4
- 对于map进行insert操作时,要记住元素类型是pair
- 检测insert(emplace)的返回值
- 对于不包含重复关键字的容器,添加单一元素的版本返回值为pair类型;pair的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,指出元素是插入成功还是已经存在于容器中
- 对于包含重复关键字的容器,添加单一元素的版本返回值为指向新元素的迭代器
- 删除元素(erase)
- 从关联容器删除元素见表11.5
- map的下标操作
- map和
unordered_map
提供了下标运算符和对应的at函数 - 如果关键字不在map中,下标运算符会为它创建一个元素并插入到map中,关联值进行值初始化;但是对于at函数,则会抛出
out_of_range
异常 - 只可以对非const的map使用下标操作
- 与vector和string不同,map的下标运算符返回的类型与解引用迭代器得到的类型不同
- map和
- 访问元素
- 在一个关联容器中查找元素的操作见表11.7
- find
- count
- lower_bound
- upper_bound
- equal_range
- 对map使用find代替下标操作
- 在multimap或multiset中查找元素
- 使用find和count结合
- 使用
lower_bound
和upper_bound
,这两个函数返回的迭代器构成一个迭代器范围 - 使用
equal_range
函数
- 在一个关联容器中查找元素的操作见表11.7
- 无序容器(unordered associative container)
- 无序关联容器不是使用比较运算符来组织元素,而是使用一个hash函数和关键字类型的
==
运算符 - 如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器
- 无序容器提供了与有序容器相同的操作,因此两者通常可以互换,但是使用无序容器输出的结果可能与有序容器不同
- 无序容器的管理操作见表11.8
- 无序容器对关键字的要求
- 无序容器使用关键字类型的
==
运算符比较元素,使用hash<key_type>
类型对象生成每个元素的哈希值 - 标准库为内置类型提供了hash模板,对于自定义的类型需要提供自己的hash模板
- 可以提供函数代替
==
运算符和哈希值计算函数
- 无序容器使用关键字类型的
- 无序关联容器不是使用比较运算符来组织元素,而是使用一个hash函数和关键字类型的
- 小结
- 无论在有序容器还是无序容器中,具有相同关键字的元素都是相邻存储的,区别见Ordered v Unordered Associative Containers
第十二章 动态内存(dynamic memory)
- 动态内存与智能指针(smart pointer)
- 智能指针定义在头文件memory中
shared_ptr
类- 默认初始化的智能指针保存一个空指针
shared_ptr
支持的操作见表12.1,12.2make_shared
函数- 类似顺序容器的emplace成员,
make_shared
用其参数来构造给定类型的对象,如果不传递任何参数,对象就会进行值初始化
- 类似顺序容器的emplace成员,
shared_ptr
的拷贝和赋值- 可以认为每个
shared_ptr
都有一个引用计数器(reference count),一旦计数器变为0,就会自动释放管理的对象
- 可以认为每个
shared_ptr
自动销毁所管理的对象和释放相关联的内存shared_ptr
的析构函数会递减它指向对象的引用计数器,如果引用计数变为0,就会调用管理对象的析构函数- 由于在最后一个
shared_ptr
销毁之前内存都不会释放,因此保证shared_ptr
在无用之后不再保留就非常重要了- 记得使用erase删除容器中不再需要的
shared_ptr
- 记得使用erase删除容器中不再需要的
- 使用动态内存的原因
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象之间共享数据
- 直接管理内存
- 使用new动态分配和初始化对象
- 默认情况下,动态分配的对象进行默认初始化
- 可以使用直接初始化方式初始化动态分配的对象
- 使用圆括号
- 对于内置类型来说,使用圆括号与不使用圆括号的差别很大;但是对于类类型,都是调用默认构造函数(圆括号为空)
- 使用花括号(列表初始化)
- 使用圆括号
- 当括号中只有单一初始化器时可以使用auto来让编译器推断要分配的类型
auto p = new auto(1);//p的类型为int*
- 动态分配的const对象
- 对于定义了默认构造函数的类类型可以进行隐式初始化,但是其他类型的对象就必须进行显式初始化
- 内存耗尽
- 默认情况下,如果new不能分配所要求的空间,就会抛出
bad_alloc
类型的异常 - 可以通过向定位new(placement new)表达式传递nothrow对象来阻止抛出异常,而只是返回空指针
auto p = new(nothrow) int;
bad_alloc
和nothrow定义在头文件new中
- 默认情况下,如果new不能分配所要求的空间,就会抛出
- 释放动态内存
- delete表达式接受一个指针,该指针必须指向动态分配的内存或者空指针
- delete表达式执行两个动作
- 调用指针指向对象的析构函数销毁对象
- 释放对应内存
- 指针值和delete
- 释放一块非动态分配的内存或者释放多次同一块内存,其行为是未定义的
- const对象可以被释放
- 动态对象的生存期直到被释放时为止
- 动态内存管理常见的错误
- 忘记delete内存
- 重复使用释放掉的对象
- 同一块内存释放两次
- 动态内存管理常见的错误
- delete之后重置指针值,只是提供了有限的保护
- 可能还有其他指向同一块内存的指针没有被重置
- 使用new动态分配和初始化对象
shared_ptr
和new结合使用- 定义和改变
shared_ptr
的其他方法见表12.3- 接受指针参数的构造函数是explicit的,必须使用直接初始化形式来初始化一个智能指针,不能在return语句中试图隐式转换普通指针
- 智能指针默认使用delete释放它管理的对象,但是可以提供一个可调用对象来替代delete
- 不要混合使用普通指针和智能指针
- 当将一个智能指针绑定到一个普通指针时,我们就将内存管理的责任交给了智能指针,就不应该通过内置指针访问智能指针所指向的内存了
- 使用一个内置指针来访问一个智能指针负责的对象是很危险的,因为我们无法知道对象何时会被销毁
- get方法用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或为另一个智能指针赋值
- 其他
shared_ptr
操作- reset成员经常与unique一起使用,在改变底层对象之前,检查自己是否是当前对象仅有的用户
- 定义和改变
- 智能指针和异常
- 智能指针能够确保在异常发生后资源被正确地释放
- 通过提供自己的删除器来管理那些不具有良好定义的析构函数的类
- 当提供自己的删除器时,智能指针不会对自己保存的指针调用delete,而是调用提供的删除器,因此在删除器中要执行delete操作
- 删除器接受一个指针参数
- 智能指针的陷阱
- 不使用相同的内置指针初始化(或reset)多个智能指针
- 不delete get返回的指针
- 不使用get初始化或reset另一个智能指针
- 如果使用了get返回的指针,当最后一个智能指针销毁后,你的指针就变为无效的了
- 如果使用智能指针管理的资源不是new分配的内存,记住要传递一个自己的删除器
unique_ptr
unique_ptr
独有操作见表12.4- 与
shared_ptr
不同,每个时刻只能有一个unique_ptr
指向一个给定的对象;当unique_ptr
被销毁时,它所指向的对象也被销毁 - 没有类似
make_shared
的函数,只能通过直接初始化方式绑定一个指针 - 由于一个
unique_ptr
独占它的对象,因此不支持普通的拷贝和赋值,但是可以通过release和reset转移指针所有权 - 需要负责释放release返回指针对应的资源
- 与
- 传递
unique_ptr
参数和返回unique_ptr
- 不能拷贝
unique_ptr
的规则有一个例外,就是可以拷贝或赋值一个将要被销毁的unique_ptr
- 不能拷贝
- 虽然标准库较早版本的
auto_ptr
拥有unique_ptr
的某些特性,但是编写程序时应该使用unique_ptr
- 向
unique_ptr
传递删除器- 默认情况下使用delete释放它指向的对象
- 管理删除器的方式与
shared_ptr
不同,与重载关联容器的比较操作类似unique_ptr<objT, delT> p(new objT, fcn);
- 在创建或reset时,必须提供一个指定类型的可调用对象作为删除器
weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr
管理的对象- 将
weak_ptr
绑定到shared_ptr
不会改变shared_ptr
引用计数,一旦最后一个指向shared_ptr
的对象被销毁,即使有weak_ptr
指向对象,对象还是会被释放
- 将
weak_ptr
操作见表12.5- 不能直接使用
weak_ptr
直接访问对象,必须调用lock返回共享对象的shared_ptr
,通过shared_ptr
进行访问
- 不能直接使用
- 动态数组
- 大多数应用程序应该使用容器而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能
- 使用容器的类可以使用默认版本的拷贝、赋值和析构操作,分配动态数组的类必须定义自己的上述操作,管理所关联的内存
- new和数组
- 分配数组会得到一个元素类型的指针
- 由于分配的内存不是数组类型,因此不可以调用使用数组维度的函数
- begin和end函数
- 范围for语句
- 要记住我们所说的动态数组不是数组类型
- 由于分配的内存不是数组类型,因此不可以调用使用数组维度的函数
- 初始化动态分配对象的数组
- 默认情况下,new分配的对象都是进行默认初始化
- 进行值初始化
- 圆括号,但不能在括号内给出初始化器,只能是空的圆括号
- 花括号,括号内可以提供初始化器;如果初始化器数目大于元素数目,new表达式失败;否则会对剩余元素进行值初始化
- 动态分配一个空数组是合法的
- new返回一个合法的非空指针,对于零长度的动态数组而言,此指针就像尾后指针
- 释放动态数组
- 数组中的元素按逆序销毁
- 如果在delete一个动态数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告,程序会在没有任何警告的情况下行为异常
- 智能指针和动态数组
- 标准库提供了管理动态数组的
unique_ptr
版本- 使用下标运算符访问数组中的元素,具体操作见表12.6
shared_ptr
不支持直接管理动态数组,需要自己提供删除器- 不能使用下标运算符,通过使用get返回的内置指针来访问元素
- 标准库提供了管理动态数组的
- 分配数组会得到一个元素类型的指针
- allocator类
- 标准库allocator类定义在头文件memory中,将内存分配和对象构造分离,而new则是将两者结合在一起,可能会导致不必要的浪费
- 标准库allocator类及其算法见表12.7
- allocator分配未构造的(unconstructed)内存
- 为了使用allocate返回的内存,必须用construct构造对象,使用未构造的内存,其行为是未定义的
- 当用完对象后,必须对每个构造的元素调用destroy来销毁它们,函数destroy对指向对象调用析构函数
- 一旦元素被销毁后,就可以重新使用这部分内存或者等全部元素被destroy后,调用deallocate释放内存
- 拷贝和填充未初始化内存的算法
- 见表12.8
- 大多数应用程序应该使用容器而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能
- 使用标准库:文本查询程序
第十三章 拷贝控制
- 拷贝控制操作(copy control):拷贝构造函数(copy constructor),拷贝赋值运算符(copy-assignment operator),移动构造函数(move constructor),移动赋值运算符(move-assignment operator)和析构函数(destructor)
- 编译器定义的版本的行为可能不是我们所想的
- 拷贝、赋值与销毁
- 拷贝构造函数
- 如果构造函数的第一个参数是自身类型的引用,且任何其他的参数都有默认值,那么此构造函数就是拷贝构造函数
- 第一个参数可以不是const的引用,当通常定义为const引用
- 拷贝构造函数在几种情况下都会隐式使用,因此不能定义为explicit
- 合成的拷贝构造函数(synthesized copy constructor)
- 不同于合成的默认构造函数,即使定义了其他构造函数,编译器也会合成一个拷贝构造函数
- 对某些类来说,合成的拷贝构造函数用来阻止拷贝该类类型的对象
- 编译器从给定对象中将每个非static成员拷贝到正在创建的对象中
- 对于类类型成员,会使用其拷贝构造函数
- 对于内置类型成员,直接进行拷贝;特别地,会逐元素地拷贝数组类型的成员
- 拷贝初始化(copy initialization)
- 直接初始化和拷贝初始化
- 使用直接初始化时,实际上是要求编译器使用普通的函数匹配来选择与提供的参数最匹配的构造函数
- 使用拷贝初始化,要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换
- 直接初始化也可以调用拷贝构造函数
- 拷贝初始化依靠拷贝构造函数或移动构造函数来完成
- 拷贝初始化发生的情况
- 使用
=
定义变量 - 将一个对象作为实参传递给一个非引用的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型会对它们分配的对象进行拷贝初始化;例如调用容器的insert方法
- 使用
- 参数和返回值
- 拷贝构造函数用来初始化非引用类类型的参数,这解释了拷贝构造函数的第一个参数为什么一定是引用类型
class C::C(C c);
这样的构造函数是非法的,因为根据函数最佳匹配的原则不会匹配拷贝构造函数而是匹配给出的这个构造函数,但是这个构造函数会导致无限递归
- 拷贝初始化的限制
- 如果给定的初始值要求通过一个explicit的构造函数进行类型转换,那么就不能使用拷贝初始化了
- 编译器可以绕过拷贝构造函数
- 作为一个优化策略,编译器可以绕过拷贝或移动构造函数,但是拷贝或移动构造函数必须是存在且可访问的
- 直接初始化和拷贝初始化
- 如果构造函数的第一个参数是自身类型的引用,且任何其他的参数都有默认值,那么此构造函数就是拷贝构造函数
- 拷贝赋值运算符
- 如果类未定义拷贝赋值运算符,编译器也会合成一个
- 重载赋值运算符
- 某些运算符,包括赋值运算符,必须定义为成员函数。如果一个运算符是成员函数,那么其左侧运算对象绑定到隐式的this参数,右侧运算对象作为参数传入
- 赋值运算符通常应该返回一个指向其左侧运算对象的引用
- 标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用
- 合成拷贝赋值运算符(synthesized copy-assignment operator)
- 对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值
- 除了上述的情况,一般来说合成拷贝运算符将右侧运算对象的每个非static成员赋予左侧运算对象对应成员;对于数组成员逐个赋值数组元素
- 返回一个指向其左侧运算对象的引用
- 析构函数
- 析构函数没有返回值,也不接受参数,因此不能被重载,对于一个类,只有一个析构函数
- 析构函数完成的工作
- 在构造函数中,成员的初始化是在函数体执行之前完成的,按照它们在类中出现的顺序进行初始化;在析构函数中,首先执行函数体,然后按照初始化的逆序销毁成员
- 销毁成员时,对于类类型成员调用它们的析构函数;对于内置类型成员什么也不做
- 销毁一个内置指针类型成员不会delete它指向的对象;智能指针成员在析构阶段会自动销毁
- 什么时候会调用析构函数:无论何时一个对象被销毁,就会自动调用其析构函数
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
- delete一个动态分配的对象
- 对于临时对象,当创建它的完整表达式结束时被销毁
- 当指向一个对象的指针或者引用离开作用域时,析构函数不会执行
- 合成析构函数
- 当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数(synthesized destructor)
- 对于某些类,合成析构函数用来阻止该类型的对象被销毁,如果不是这种情况,合成析构函数体为空
- 合成析构函数体之所以可以为空,是因为成员是在析构函数体之后隐含的析构阶段中被销毁的
- 三/五法则
- 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数
- 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然
- 析构函数不能是删除的
- 如果一个类有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的
- 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作
- 使用=default
- 只能对具有合成版本的成员函数使用=default,(即默认构造函数以及拷贝控制成员)
- 在类内使用=default,合成的函数被隐式声明为inline;如果不希望合成的函数是内联的,应该在类外使用=default
- 阻止拷贝
- 定义删除的函数(deleted function)
- 与=default不同的是,=delete必须出现在函数第一次声明的时候
- 与=default不同的是,可以对任何函数使用=delete
- 析构函数不能是删除的成员
- 对于析构函数是删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针
- 合成的拷贝控制成员可能是删除的
- 一些规则
- 如果类的某个成员的析构函数是删除的或不可访问的,则类的合成析构函数被定义为删除的
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的;如果类的某个成员的析构函数是删除的或不可访问的,则类合成的拷贝构造函数被定义为删除的
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个const或引用的成员,则类的合成拷贝赋值运算符被定义为删除的
- 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器,或是类有一个const成员,它没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的
- 本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
- 一些规则
- private拷贝控制
- 在新标准发布之前,通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝
- 为了阻止友元和成员函数进行拷贝,需要将拷贝控制成员声明为private并且不定义它们
- 希望阻止拷贝的类应该使用=delete
- 定义删除的函数(deleted function)
- 拷贝控制和资源管理
- 为了定义拷贝控制成员,必须确定此类型对象的拷贝语义:使类的行为看起来像一个值或者像一个指针
- 行为像值的类
- 为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝
- 类值拷贝赋值运算符
- 如果将一个对象赋予它自身,赋值运算符必须能正确工作
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
- 在销毁左侧运算对象资源之前拷贝右侧运算对象
- 定义行为像指针的类
- 使用
shared_ptr
管理资源 - 使用引用计数(reference count)
- 引用计数使用动态分配的方式
- 使用
- 拷贝构造函数
- 交换操作
- 与拷贝控制成员不同,swap并不是必要的。但是对于分配了资源的类,定义swap可能是一种重要的优化手段
- swap函数应该调用swap而不是
std::swap
- 在赋值运算符中使用swap
- 定义swap的类通常用swap来定义它们的赋值运算符。这些运算符使用了名为拷贝并交换(copy and swap)的技术
- 使用拷贝和交换的赋值运算符自动就是异常安全的,并且能正确处理自赋值
- The copy and swap is an elegant way when working with dynamicly allocated memory
- 拷贝控制示例
- 动态内存管理类
- 使用allocator
- 对象移动
- 在新标准中,容器可以保存不可拷贝类型,只要它们是可移动的
- 标准库容器、string和
shared_ptr
类支持拷贝和移动,IO类和unique_ptr
类支持移动不支持拷贝
- 标准库容器、string和
- 右值引用(rvalue reference)
- 可以将一个const的左值引用或右值引用绑定到右值表达式
- 左值持久,右值短暂,右值引用指向将要被销毁的对象
- 变量是左值,因此不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行
- 标准库move函数
- 定义在头文件utility
- 可以通过显式类型转换的方式获得右值引用类型,也可以通过std::move函数获得
- 可以销毁一个移后源(moved-from)对象,也可以对它赋新值,但是不能使用一个移后源对象的值,不能对移后源对象的值做任何假设
- 使用move的代码应该使用std::move而不是move,这样做可以避免名字冲突
- 移动构造函数和移动赋值运算符
- 移动操作、标准库容器和异常
- noexcept的使用
- 出现在参数列表之后,初始值列表之前
- 在声明和定义中都要指定noexcept
- 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
- 标准库容器对异常发生时自身的行为提供保障
- 如果希望vector在重新分配内存时对自定义类型对象进行移动而不是拷贝,就必须显式地告诉标准库移动操作不会抛出异常
- noexcept的使用
- 移动赋值运算符
- 移动赋值运算符执行与析构函数和移动构造函数相同的工作
- 直接检测自赋值的情况
- 移后源对象必须可析构
- 在移动操作后,移后源对象必须保持有效、可析构的状态,但是用户不能对其值进行任何假设
- 合成的移动操作
- 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值,编译器才会为它合成移动构造函数或移动赋值运算符
- 如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替
- 只有在显式要求编译器生成default的移动操作,并且编译器不能移动所有成员时,移动操作会定义为delete函数;以下情况编译器不能移动所有成员(还是要结合具体编译器的实现,例如第一条在本机的g++不成立)
- 有类成员定义了拷贝构造函数但是没有定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数但是编译器不能为其合成移动构造函数;移动赋值运算符有类似原则
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
- 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
- 类似拷贝赋值运算符,如果有类成员是const或引用,则类的移动赋值运算符被定义为删除的
- 如果类定义了一个移动构造函数和/或移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的
- 移动右值,拷贝左值,但如果没有移动构造函数,右值也被拷贝
- 赋值运算符也类似
- 拷贝并交换赋值运算符和移动操作
- 添加一个移动构造函数,实际上也会获得一个移动赋值运算符
- 参考练习13.53如何提升效率
- 移动迭代器(move iterator)
- 通过调用标准库的
make_move_iterator
函数将一个普通迭代器转换为一个移动迭代器 - 移动迭代器的解引用运算符生成一个右值引用,原迭代器的所有其他操作都照常工作
- 可以在
uninitialized_copy
使用移动迭代器,但是标准库不保证那些算法适用移动迭代器,哪些不适用 - 建议不要随便使用移动操作
- 由于一个移后源对象具有不确定状态,当我们调用std::move时,必须绝对确认移后源对象没有其他用户
- 通过在类代码中使用move可以大幅度提升性能;但是普通用户代码中使用move可能导致莫名其妙的错误
- 通过调用标准库的
- 移动操作、标准库容器和异常
- 右值引用和成员函数
- 区分移动和拷贝的重载函数通常有一个版本接受一个
const T&
,而另一个版本接受一个T &&
- 右值和左值引用成员函数
- 通过在参数列表后放置一个引用限定符(reference qualifier)来指出this的左值/右值属性,引用限定符可以是
&
或&&
- 引用限定符只能作用于非static的成员函数,且必须同时出现在函数声明和定义处
- 对于
&
限定的函数,只能用于左值;对于&&
限定的函数,只能用于右值 - 引用限定符必须跟在const限定符之后,因为引用没有顶层const
- 通过在参数列表后放置一个引用限定符(reference qualifier)来指出this的左值/右值属性,引用限定符可以是
- 重载和引用函数
- 如果通过引用限定符重载成员函数,那么所有重载成员函数都必须有引用限定符,不能通过有没有引用限定符区别重载函数
- 区分移动和拷贝的重载函数通常有一个版本接受一个
- 在新标准中,容器可以保存不可拷贝类型,只要它们是可移动的
第十四章 重载运算与类型转换
- 基本概念
- 除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参
- 当一个重载的运算符是成员函数时,this绑定到左侧运算对象;成员运算符函数显式参数数量比运算对象的数量少一个
- 对一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数
- 表14.1指明了哪些运算符可以重载,哪些不能重载
- 直接调用一个重载的运算符函数
- 运算符函数的函数名为
operator+运算符
- 运算符函数的函数名为
- 通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符
- 上述运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载
- C++语言已经定义了逗号和取地址运算符作用与类类型的特殊含义
- 使用与内置类型一致的含义
- 只有当操作的含义对于用户来说清晰明了时才使用运算符
- 如果用户对运算符可能有集中不同的理解,则使用这样的运算符将产生二义性
- 选择作为成员还是非成员
- 赋值
(=)
,下标([])
,调用(())
和成员访问箭头(->)
运算符必须是成员 - 复合赋值运算符一般来说应该是成员,但并非必须
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数
- 赋值
- 输入和输出运算符
- 重载输出运算符
<<
- 通常情况下,输出运算符的第一个形参是一个非常量的ostream对象的引用,第二个形参是一个常量的引用,一般要返回它的ostream形参
- 输出运算符尽量减少格式化操作
- 输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
- 输入输出运算符必须是非成员函数
- IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元
- 重载输入运算符
>>
- 输入运算符必须处理输入可能失败的情况,而输出运算符不需要
- 当读取操作发生错误时,输入运算符应该负责从错误中恢复
- 重载输出运算符
- 算术和关系运算符
- 如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符
- 相等运算符
- 设计准则
- 如果一个类含有判断两个对象是否相等的操作,则它应该把函数定义为
operator==
而非一个普通命名函数 - 如果类定义了
operator==
,则运算符应该能判断一组给定对象中是否含有重复数据 - 通常情况下,相等运算符应该具有传递性
- 如果类定义了
operator==
,则也应该定义operator!=
- 相等运算符和不相等运算符中的一个应该把工作委托给另外一个
- 如果一个类含有判断两个对象是否相等的操作,则它应该把函数定义为
- 设计准则
- 关系运算符
- 通常情况下,关系运算符应该
- 定义顺序关系,令其与关联容器对关键字的要求一致
- 如果类同时定义了相等运算符,则定义一种关系令其与相等运算符保持一致;特别是,如果两个对象是不相等的,那么一个对象应该小于另外一个
- 如果存在唯一一种逻辑可靠的
<
定义,则应该考虑为这个类定义<
运算符 - 如果类同时还包含
==
,则当且仅当<
的定义和==
产生的结果一致时才定义<
运算符
- 通常情况下,关系运算符应该
- 赋值运算符
- 不论形参类型是什么,赋值运算符都必须定义为成员函数
- 下标运算符
- 下标运算符必须是成员函数
- 如果一个类包含下标运算符,则它通常会定义两个版本,一个返回普通引用,另一个是类的常量成员并且返回常量引用
- 递增和递减运算符
- 定义递增和递减运算符的类应该同时定义前置版本和后置版本,这些运算符通常应该定义成类的成员
- 区分前置和后置运算符
- 后置版本接受一个额外的不被使用的int类型的形参,编译器为这个形参提供一个值为0的实参
- 成员访问运算符
- 箭头运算符必须是类的成员,解引用运算符通常也是类的成员,尽管并非必须如此
- 对箭头运算符返回值的限定
- 我们能令
operator*
完成任何指定的操作,箭头运算符则不是这样,当重载箭头时,可以改变的是箭头从哪个对象当中获取成员,而箭头获取成员这一事实则永远不变 - 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象
- 如果返回指针,则使用内置的箭头运算符
- 如果返回自定义了箭头运算符的某个类的对象,则使用类的箭头运算符
- 我们能令
- 函数调用运算符
- 函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别
- 如果类定义了调用运算符,则该类的对象称作函数对象(function object)
- lambda是函数对象
- 编译器将lambda表达式翻译成一个未命名类的未命名对象,在产生的类中有一个重载的函数调用运算符
- lambda表达式产生的类不含默认构造函数、赋值运算符以及默认析构函数;它是否含有默认的拷贝/移动构造函数通常要视捕获的数据成员类型而定
- 标准库定义的函数对象
- 标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符
- 表14.2所列的类型定义在functional头文件中
- 在算法中使用标准库函数对象
sort(svec.begin(), svec.end(), greater<string>());
- 标准库规定其函数对象对于指针同样适用
- 直接比较两个无关指针将产生未定义的行为,但可以使用标准库函数对象来实现
- 可调用对象与function
- 调用形式(call signature)指明了调用返回的类型以及传递给调用的实参类型,一种调用形式对应一个函数类型
- 不同类型可能具有相同的调用形式
- 因为它们的类型不同,所以需要借助function类型来统一管理
- 标准库function类型
- 定义在functional头文件中
- 表14.3列出了function定义的操作
- 重载的函数与function
- 不能直接将重载函数的名字存入function类型的对象中,会存在二义性
- 可以通过函数指针或lambda表达式来解决二义性问题
- 不能直接将重载函数的名字存入function类型的对象中,会存在二义性
- 重载、类型转换与运算符
- 转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversions),这样的转换有时也被称为用户定义的类型转换(user-defined conversions)
- 类型转换运算符(conversion operator)
- 形如
operator type()const;
- type不能是void、数组类型和函数类型
- 一个类型转换函数必须是类的成员函数,它不能声明返回类型,形参列表也必须为void,通常为常量函数
- 尽管编译器一次只能执行一个用户定义的类型转换,但是用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用
- 尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值
- 避免过度使用类型转换函数:如果在类类型和转换类型之间不存在明显的映射关系,则这样的类型转换可能具有误导性
- 类型转换运算符可能产生意外结果
- 对于类来说,定义向bool的类型转换是比较普遍的现象,然而因为bool是一种算术类型,所以类类型的对象转换成bool后能被用于任何需要算术类型的上下文中,这样的类型转换可能引发意想不到的结果,如下所示
cin<<42;//在早期的C++中,cin被隐式转换为bool类型,表达式的结果实际上是将bool值左移42位
- 对于类来说,定义向bool的类型转换是比较普遍的现象,然而因为bool是一种算术类型,所以类类型的对象转换成bool后能被用于任何需要算术类型的上下文中,这样的类型转换可能引发意想不到的结果,如下所示
- 显式的类型转换运算符
- 为了防止上述的异常情况发生,新标准引入了显式类型转换运算符(explicit conversion operator)
- 当类型转换运算符是explicit时
- 可以进行显式强制类型转换
- 编译器通常不会将其用于隐式类型转换,但如果表达式被用作条件,则编译器会将显式的类型转换(只能是转换为bool)自动应用于它
- if、while、for以及do语句的条件部分
- 逻辑非、逻辑或、逻辑与的运算对象
- 条件运算符的条件表达式
- 转换为bool
- 当我们为类定义bool的类型转换时,应该将其定义为explicit
- 形如
- 避免有二义性的类型转换
- 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个以及两个以上转换源或转换目标为算术类型的转换
- 相同的类型转换:A类定义一个接受B类的转换构造函数,B类定义一个转换目标为A类的类型转换运算符
- 实参匹配和相同的类型转换
- 最好不要定义相同的类型转换,编译器会因为产生二义性无法进行转换而报错(在本机的g++编译器没有报错)
- 二义性与转换目标为内置类型的多重类型转换
- 最好不要创建两个转换源都是算术类型的类型转换
- 最好不要创建两个转换对象都是算术类型的类型转换
- 当使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个
- 总之,除了显式地向bool类型的转换之外,应该尽量避免定义类型转换函数
- 重载函数与转换构造函数
- 当调用重载函数时,如果两个或多个类型的转换构造函数都提供了同一种可行的匹配,将产生二义性
- 如果在调用重载函数时需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序的设计存在不足
- 重载函数与用户定义的类型转换
- 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用;如果所需的用户定义的类型转换不止一个,则该调用具有二义性
- 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个以及两个以上转换源或转换目标为算术类型的转换
- 函数匹配与重载运算符
- 当使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本;除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内
- 如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题
第十五章 面向对象程序设计
- OOP:概述
- 面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定
- 数据抽象使得类的接口和实现分离
- 使用继承可以定义相似的类型并对其相似关系进行建模
- 使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象
- 继承(inheritance)
- 基类(base class),派生类(derived class)
- 虚函数(virtual function)
- 类派生列表(class derivation list)
- 动态绑定(dynamic binding)
- 在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
- 面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定
- 定义基类和派生类
- 定义基类
- 基类通常定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
- 成员函数与继承
- 任何构造函数之外的非静态函数都可以是虚函数
- 关键字virtual只能出现在类内的声明语句而不能用于类外部的函数定义
- 基类中的虚函数在派生类中隐式地也是虚函数
- 访问控制与继承
- protected:派生类有权访问而禁止其他用户访问
- 定义派生类
- 派生类中的虚函数
- 如果派生类没有覆盖基类中的虚函数,则派生类会直接继承其在基类中的版本
- 新标准允许派生类使用override注明它使用某个成员函数覆盖它继承的虚函数,override必须放在类内声明的最后
- 派生类对象及派生类向基类的类型转换
- 编译器会隐式地执行派生类到基类的(derived-to-base)类型转换
- 派生类构造函数
- 每个类控制它自己的成员初始化过程
- 首先初始化基类的部分,然后按照声明顺序依次初始化派生类的成员
- 派生类使用基类的成员
- 派生类可以访问基类的公有成员和受保护成员
- 遵循基类的接口
- 每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此
- 派生类应该遵循基类的接口,通过调用基类的构造函数来初始化那些从基类中继承而来的成员
- 继承与静态成员
- 在整个继承体系中,只存在静态成员的唯一定义
- 静态成员遵循通用的访问控制规则
- 派生类的声明
- 一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体:是一个类,一个函数还是一个变量等等
- 派生类的声明中不能包含它的派生列表
- 一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体:是一个类,一个函数还是一个变量等等
- 被用作基类的类
- 如果想将某个类用作基类,则该类必须已经定义而非仅仅声明
- 因此一个类不能派生它本身
- 对于一个最终的派生类来说,它会继承其直接基类的成员;该直接基类的成员又含有其基类的成员;依此类推直至继承链的顶端
- 如果想将某个类用作基类,则该类必须已经定义而非仅仅声明
- 防止继承的发生
- 在新标准中,可以在类名后跟关键字final来防止继承的发生
- 派生类中的虚函数
- 类型转换与继承
- 把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,存在两种例外情况
- 对象的类型含有一个可接受的const类型转换规则
- 将基类的指针或引用绑定到派生类对象上
- 和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着可以将一个派生类对象的指针存储在一个基类的智能指针内
- 静态类型(static type)与动态类型(dynamic type)
- 静态类型是变量声明时的类型或表达式生成的类型;动态类型是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知
- 如果表达式既不是指针也不是引用,那么它的动态类型永远与静态类型一致
- 不存在从基类向派生类的隐式类型转换
- 即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的隐式转换,因为编译器只能通过检查静态类型来推断转换是否合法
- 如果基类中含有一个或多个虚函数,可以使用
dynamic_cast
请求类型转换,该转换的安全检查将在运行时执行 - 如果已知基类向派生类的转换是安全的,可以使用
static_cast
来强制覆盖掉编译器的检查工作
- 在对象之间不存在类型转换
- 派生类向基类的自动类型转换只对指针或者引用类型有效,在派生类类型和基类类型之间不存在这样的转换
- 当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉
- 存在继承关系的类型之间的转换规则
- 从派生类向基类的类型转换只对指针或引用类型有效
- 基类向派生类不存在隐式类型转换
- 派生类向基类的类型转换可能由于访问受限变得不可行
- 把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,存在两种例外情况
- 定义基类
- 虚函数
- 通常情况下,如果不使用某个函数,则无须为该函数提供定义,但是必须为每一个虚函数都提供定义而不管它是否被用到了,因为连编译器也无法确定到底会使用哪个虚函数
- 对虚函数的调用可能在运行时才被解析
- 当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数
- 当通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来
- 当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数
- C++的多态性
- 引用或指针的静态类型与动态类型不同是C++语言支持多态性的根本所在
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
- 派生类中的虚函数
- 一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数
- 当派生类重载某个虚函数时,该函数在派生类中的形参必须与在基类中的形参严格匹配,返回类型也必须匹配
- 当类的虚函数返回类型是类本身的指针或引用时,返回类型可以不严格匹配;也就是说,如果D由B派生得到,则基类的虚函数可以返回B*而派生类对应的函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的
- final和override说明符
- 如果使用override标记某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错
- 如果使用final标记某个函数,则之后任何尝试覆盖该函数的操作都将引发错误
- final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后
- 虚函数与默认实参
- 虚函数可以拥有默认参数,该实参值由本次调用的静态类型决定
- 如果虚函数使用默认参数,则基类和派生类中定义的默认参数最好一致
- 回避虚函数机制
- 使用作用域运算符可以强迫执行虚函数的某个特定版本
- 通常情况下,只有成员函数中的代码才需要使用作用域运算符来回避虚函数机制
- 基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作
- 抽象基类
- 纯虚函数(pure virtual function)
- 通过在声明语句分号之前书写
=0
就可以将一个虚函数说明为纯虚函数,=0
只能出现在类内部虚函数声明语句处 - 可以为纯虚函数提供定义,不过函数体必须定义在类的外部;即使为纯虚函数提供定义,也不能实例化含有纯虚函数的抽象类
- 含有纯虚函数的类是抽象基类(abstract base class)
- 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类
- 不能创建抽象基类的对象
- 派生类构造函数只初始化它的直接基类
- 重构(refactoring)
- 重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中,对于面向对象的应用程序来说,重构是一种很普遍的现象
- 通过在声明语句分号之前书写
- 纯虚函数(pure virtual function)
- 访问控制和继承
- 每个类分别控制着自己的成员的初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问(accessible)
- 受保护的成员
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
- 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可以访问的
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,派生类对于一个基类对象中的受保护成员没有任何访问特权
- 公有、私有和受保护继承
- 某个类对其继承而来的成员的访问权限受到两个因素影响
- 在基类中该成员的访问说明符
- 在派生类的派生列表中的访问说明符
- 派生类的成员以及友元对基类成员的访问权限只与基类中的访问说明符有关;派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限
- 某个类对其继承而来的成员的访问权限受到两个因素影响
- 派生类向基类转换的可访问性
- 假定D继承于B
- 只有当D public继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是protected或private,则用户代码不能使用该转换
- 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的
- 如果D继承B的方式是public或protected,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用
- 对于代码中的某个给定节点来说,如果能够通过派生类访问基类的公有成员,则派生类向基类的类型转换也是可访问的,反之则不行
- 类的设计与受保护的成员
- 基类应该将接口成员声明为公有的(public);同时将属于其实现的部分分为两组:一组可供派生类访问(protected),另一组只能由基类及基类的友元访问(private)
- 假定D继承于B
- 友元与继承
- 就像友元关系不能传递一样,友元关系同样也不能继承
- 基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员
- 每个类负责控制自己的成员的访问权限,这种可访问性包括了基类对象内嵌在其派生类对象中的情况
- 就像友元关系不能传递一样,友元关系同样也不能继承
- 改变个别成员的可访问性
- 通过在类内部使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员标记出来,标记名字的访问权限由using声明语句之前的访问说明符决定
- 派生类只能为那些它可以访问的名字提供using声明
- 默认的继承保护级别
- 使用class和struct关键字定义类唯一的差别就是默认成员访问说明符和默认派生访问说明符
- class默认为private
- struct默认为public
- 尽量不要依赖默认设置,使用显式声明不至于产生误会
- 使用class和struct关键字定义类唯一的差别就是默认成员访问说明符和默认派生访问说明符
- 继承中的类作用域
- 每个类定义自己的作用域,当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内;如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义
- 在编译时进行名字查找
- 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型与动态类型可能不一样,但是我们能使用哪些成员仍然是由静态类型决定的
- 名字冲突与继承
- 派生类能够重用定义在其直接基类或间接基类的名字,此时定义在内层作用域(派生类)的名字将隐藏定义在外层作用域(基类)的名字
- 通过作用域运算符来使用隐藏的成员
- 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
- 假定调用p->mem()或者obj.mem(),则依次执行以下步骤
- 首先确定p或obj的静态类型
- 在该静态类型对应的类中查找mem,如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端,如果最终还是找不到,则编译器报错
- 一旦找到了mem,就进行常规的类型检查,确认当前找到的mem是否合法
- 假设调用合法,则编译器将根据调用的是否是虚函数产生不同的代码
- 如果mem是虚函数并且是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本
- 如果mem不是不是虚函数或者是通过对象进行调用,则编译器将产生一个常规函数调用
- 一如往常,名字查找先于类型检查
- 声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义在派生类中的函数不会重载其基类中的成员,而是隐藏该基类成员,即使派生类成员和基类成员的形参列表不同
- 为什么是隐藏呢,因为当编译器查找到名字后就停止查找了,进行类型检查,如果类型检查失败就报错了,不会接着在基类中查找名字
- 声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义在派生类中的函数不会重载其基类中的成员,而是隐藏该基类成员,即使派生类成员和基类成员的形参列表不同
- 虚函数与作用域
- 基类和派生类中的虚函数必须有相同的形参列表
- 覆盖重载的函数
- 和其他函数一样,成员函数无论是否是虚函数都能被重载;如果派生类希望所有的重载版本对于它来说是可见的,那么它就需要覆盖所有的版本或者一个也不覆盖
- 有时派生类仅需覆盖重载集合中的一些而非全部函数,如果不得不覆盖基类中的每一个版本的话,显然操作将及其繁琐
- 一种好的解决方案是为重载的成员提供一条using声明语句,using声明语句指定一个名字而不指定形参列表,这样就将基类中所有重载的函数添加到派生类的作用域中,此时派生类只需要定义其特有的函数就可以了
- 类内using声明的一般规则同样适用于重载函数的名字,对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问
- 构造函数与拷贝控制
- 虚析构函数
- 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为
- 如果一个类需要析构函数,那么它同样需要拷贝和赋值操作,对于基类的析构函数来说并不遵循这条准则
- 虚析构函数将阻止合成移动操作
- 如果一个类定义了析构函数,即使它通过
=default
的形式使用了合成的版本,编译器也不会为这个类合成移动操作
- 如果一个类定义了析构函数,即使它通过
- 合成拷贝控制与继承
- 合成的拷贝控制除了对类本身的成员进行初始化、赋值或销毁的操作外,还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作
- 无论基类的成员是合成的版本还是自定义的版本都没有太大影响(自定义操作默认都是调用直接基类的默认构造函数),唯一的要求是相应的成员应该可访问并且不是一个被删除的函数
- 基类因为定义了析构函数而不能拥有合成的移动操作,意味着它的派生类也没有合成的移动操作
- 派生类中删除的拷贝控制与基类的关系
- 某些定义基类的方式可能导致有的派生类成员成为被删除的函数
- 如果基类中的默认构造函数,拷贝构造函数、拷贝赋值运算符或析构函数是被删除的或不可访问的,则派生类中对应的成员将是被删除的
- 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的
- 编译器不会合成一个删除掉的移动操作;当使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的;如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的
- 某些定义基类的方式可能导致有的派生类成员成为被删除的函数
- 移动操作与继承
- 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实需要移动操作时应该首先在基类中进行定义
- 一旦基类定义了移动操作,编译器不会为其合成拷贝构造函数和拷贝赋值运算符
- 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实需要移动操作时应该首先在基类中进行定义
- 合成的拷贝控制除了对类本身的成员进行初始化、赋值或销毁的操作外,还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作
- 派生类的拷贝控制成员
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象
- 定义派生类的拷贝或移动构造函数
- 默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝或移动构造函数
- 派生类赋值运算符
- 与拷贝和移动函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值
- 需要注意自赋值的情况
- 派生类的析构函数
- 在析构函数体执行完成后,对象的成员会被隐式销毁,因此对象的基类部分也是隐式销毁的(调用基类的析构函数);所以和构造函数以及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源
- 在构造函数和析构函数中调用虚函数
- 当构造或析构一个对象时,需要把对象的类型和构造函数或析构函数的类看做同一个
- 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本
- 继承的构造函数
- 派生类继承基类构造函数的方式是提供一条注明了直接基类名的using声明语句
- 一个类只初始化它的直接基类,因此一个类也只能继承其直接基类的构造函数
- 不能继承默认、拷贝和移动构造函数,如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们
- 通常情况下,using声明语句只是令某个名字在当前作用域内可见,而当作用于构造函数时,using声明语句将令编译器产生代码;对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数
derived(params):base(args){}
- 如果派生类含有自己的数据成员,这些成员将被默认初始化
- 继承构造函数的特点
- 和普通成员的using不同,一个构造函数的using声明不会改变该构造函数的访问级别
- 不管using声明出现在哪里,基类构造函数的访问级别不会改变
- 如果基类的构造函数是explicit或constexpr的,则继承的构造函数也拥有相同的属性
- 当一个基类构造函数含有默认实参,这些实参并不会被继承,派生类将获得多个继承的构造函数
- 如果基类含有几个构造函数,大多数时候派生类会继承所有这些构造函数,除了两种例外情况
- 派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本
- 如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承
- 默认、拷贝和移动构造函数不会被继承,这些构造函数按照正常规则被合成
- 继承的构造函数不会被作为用户定义的构造函数使用,因此,如果一个类只有继承的构造函数,则它也将拥有一个合成的默认构造函数
- 派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本
- 和普通成员的using不同,一个构造函数的using声明不会改变该构造函数的访问级别
- 派生类继承基类构造函数的方式是提供一条注明了直接基类名的using声明语句
- 虚析构函数
- 容器与继承
- 在容器中放置(智能)指针而非对象
- 当希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针)
- 在容器中放置(智能)指针而非对象
- 文本查询程序再探
第十八章 用于大型程序的工具
- 异常处理(exception handling)
- 抛出异常
- 栈展开(stack unwinding)
- 栈展开过程沿着嵌套函数的调用链不断查找,直到找到与异常匹配的catch子句为止;或者最后没找到匹配的catch,程序将调用标准库函数terminate终止执行
- 假设找到一个匹配的catch子句,程序进入该子句并执行其中的代码,当执行这个catch子句后,找到与try块关联的最后一个catch子句之后的点,并从这里继续执行
- 栈展开的过程中对象被自动销毁
- 如果在栈展开的过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正确地销毁
- 如果某个局部对象的类型是类类型,则该对象的析构函数将被调用
- 编译器在销毁内置类型的对象时不需要做任何事情
- 如果在栈展开的过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正确地销毁
- 析构函数与异常
- 析构函数总是会被执行,但是如果在负责释放这些资源的代码前面发生了异常,则释放资源的代码将不会被执行;另一方面,类对象分配的资源将由类的析构函数负责释放,无论析构函数是正常结束还是遭遇异常,资源都能被正确释放
- 在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出异常,并且析构函数没能捕获到该异常,则程序将被终止
- 异常对象(exception object)
- 编译器使用异常抛出表达式来对异常对象进行拷贝初始化,因此在throw语句中的表达式必须拥有完全类型
- 如果该表达式是类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数
- 如果该表达式是数组类型或函数类型,则表达式将被转换成与之对应的指针类型
- 异常对象位于编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间,当异常处理完毕后,异常对象被销毁
- 抛出一个局部对象的指针几乎肯定是一种错误的行为,出于同样的原因,从函数中返回指向局部对象的指针也是错误的
- 当抛出一条表达式时,该表达式的静态编译类型决定了异常对象的类型
- 如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将只有基类部分
- 编译器使用异常抛出表达式来对异常对象进行拷贝初始化,因此在throw语句中的表达式必须拥有完全类型
- 栈展开(stack unwinding)
- 捕获异常
- 如果catch无须访问抛出的表达式,则可以省略捕获形参的名字
- catch子句声明的类型决定了处理代码所能捕获的异常类型
- 这个类型必须是完全类型,可以是左值引用,但不能是右值引用
- catch的参数支持多态
- 异常声明的静态类型将决定catch语句所能执行的操作;如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员
- 通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义为引用类型
- 查找匹配的处理代码
- 最终找到的catch未必是异常的最佳匹配,挑选出来的应该是第一个与异常匹配的catch语句
- 除了一些极细小的差别外,要求异常的类型和catch声明的类型是精确匹配的
- 允许从非常量向常量的类型转换,也就是说,一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句
- 允许从派生类向基类的类型转换
- 数组被转换成指向数组类型的指针,函数被转换成指向该函数类型的指针
- 包括标准算术类型转换和类类型转换在内的其他所有转换规则都不能在匹配catch的过程中使用
- 如果在多个catch语句的类型之间存在着继承关系,则应该把继承链最低端的类(most derived type)放在前面,而将继承链最顶端(least derived type)放在后面
- 重新抛出(rethrowing)
- 重新抛出就是一条空的throw语句
throw;
- 一条catch语句通过重新抛出操作将异常传递给调用链更上一层的函数接着处理异常
- 空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内,如果在其他地方遇到空的throw语句,编译器将调用terminate
- 一条重新抛出语句将当前的异常对象沿着调用链向上传递
- 只有当catch异常声明是引用类型时我们对参数所做的修改才会被保留并继续传播
- 重新抛出就是一条空的throw语句
- 捕获所有异常的(
catch-all
)处理代码- 为了一次性捕获所有异常,我们使用省略号作为异常声明
catch(...)
通常与重新抛出语句一起使用,其中catch执行当前局部能完成的工作catch(...)
必须放在最后的位置;出现在捕获所有异常语句后面的catch语句将永远不会被匹配
- 函数try语句块(function try block)与构造函数
- 构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常
- 函数try语句块(也称为函数测试块)既能处理构造函数体(或析构函数体)的异常,也能处理构造函数的初始化过程(或析构函数的析构过程)
- 关键字try出现在表示构造函数初始值列表的冒号以及表示构造函数体的花括号之前
- 在初始化构造函数的参数时也可能发生异常,这样的异常不属于函数try语句块的一部分
- 如果在参数初始化的过程中发生了异常,则该异常属于调用表达式的一部分,并将在调用者所在的上下文中处理
- noexcept异常说明
- 对于用户以及编译器来说,预先知道某个函数不会抛出异常显然大有裨益
- 用户可以简化调用该函数的代码
- 编译器能执行某些特殊的优化操作
- 在新标准中,通过noexcept说明(noexcept specification)指定某个函数不会抛出异常
- noexcept应该出现在函数所有声明语句和定义语句中,应该在函数的尾置返回类型之前
- 函数的声明和定义可以指定noexcept,在typedef和using类型别名中不能出现noexcept
- 在成员函数中,noexcept说明符应该出现在const及引用限定符之后,而在final、override或虚函数的
=0
之前
- 违反异常说明
- 通常情况下,编译器不能也不必在编译时验证异常说明
- 一旦一个noexcept函数抛出异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺
- noexcept可以用在两种情况
- 确认函数不会抛出异常
- 根本不知道该如何处理异常
- 指明某个函数不会抛出异常可以令该函数的调用者不必再考虑如何处理异常:无论该函数确实不抛出异常还是程序被终止,调用者都无须为此负责
- 为了向后兼容,以下两种异常说明都是合法的
void recoup(int)noexcept;
void recoup(int)throw();
- 通常情况下,编译器不能也不必在编译时验证异常说明
- 异常说明的实参
- noexcept说明符接受一个可选的实参,该实参必须能够转换为bool类型
- 如果实参为true,则函数不会抛出异常;否则函数可能会抛出异常
- noexcept说明符接受一个可选的实参,该实参必须能够转换为bool类型
- noexcept运算符
- noexcept运算符是一个一元运算符,返回值是一个bool类型的右值常量表达式,和sizeof类似,noexcept不会求其运算符对象的值
- 对于
noexcept(e)
来说,当e调用的所有函数都做了noexcept说明且e本身不含有throw语句时,表达式为true,否则返回false(对于本机的g++编译器,只要当e做了noexcept说明就返回true,否则返回false)
- 异常说明与指针、虚函数和拷贝控制
- 函数指针和该指针所指的函数必须具有一致的异常说明
- 如果指针做了不抛出异常的声明,则该指针只能指向不抛出异常的函数(对于本机的g++编译器,不满足这条规定)
- 如果指针显式或隐式地说明可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以
- 如果一个虚函数承诺它不会抛出异常,则后续派生出来的虚函数也必须做同样的承诺
- 如果基类的虚函数允许抛出异常,则派生类对应函数既可以允许抛出异常,也可以不允许抛出异常
- 当编译器合成拷贝控制成员时,同时也生成一个异常说明
- 如果对所有成员和基类所有操作都承诺不会抛出异常,则合成的成员是noexcept的
- 如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)
- 如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个,合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致
- 函数指针和该指针所指的函数必须具有一致的异常说明
- 对于用户以及编译器来说,预先知道某个函数不会抛出异常显然大有裨益
- 异常类层次
- 异常类定义了拷贝构造函数、拷贝赋值运算符、虚析构函数和一个名为what的虚成员函数
- 类exception、
bad_cast
和bad_alloc
定义了默认构造函数,类runtime_error
和logic_error
定义了接受一个字符串实参的构造函数- 由what返回初始化异常对象的信息
- 抛出异常
- 命名空间
- 多个库将名字放置在全局命名空间将引发命名空间污染(namespace pollution)
- 命名空间定义
- 关键字namespace,只要能出现在全局作用域中的声明就能置于命名空间内
- 和其他名字一样,命名空间的名字必须在定义它的作用域内保持唯一
- 命名空间可以定义在全局作用域中或者其他命名空间中,但是不能定义在函数或类的内部
- 命名空间作用域后面无须分号
- 每个命名空间都是一个作用域
- 命名空间中的每个名字都必须唯一,不同命名空间内可以有相同名字的成员
- 定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问;位于命名空间之外的代码必须明确指出所有的名字属于哪个作用域
- 命名空间可以是不连续的
- 命名空间可以定义在几个不同的部分,如果之前没有该命名空间,则创建一个新的命名空间,否则为命名空间添加一些新成员的声明
- 命名空间定义的不连续性使得可以将几个独立的接口和实现文件组成一个命名空间
- 命名空间的一部分成员的作用是定义类以及声明作为类接口的函数及对象,这些成员应该置于头文件中
- 命名空间成员的定义部分应该置于另外的源文件中
- 定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型
- 通常情况下,不把#include放在命名空间中,否则头文件中所有的名字定义成该命名空间的成员
- 定义命名空间的成员
- 假定作用域中存在合适的声明语句,则命名空间中的代码可以使用同一命名空间定义的名字的简写形式,无须前缀
- 也可以在命名空间定义的外部定义该命名空间的成员,该名字的定义需要明确指出其所属的命名空间
- 可以在全局作用域中定义,但不能在一个不相关的作用域中定义
- 模板特例化
- 模板特例化必须定义在原始模板所属的命名空间中,只要在命名空间中声明了特例化,就能在命名空间外部定义它了
- 全局命名空间(global namespace)
- 全局命名空间以隐式的方式声明,并且在所有程序中都存在
- 全局作用域中定义的名字被隐式地添加到全局命名空间中
::member_name
表示全局命名空间的一个成员
- 嵌套的命名空间
- 嵌套的命名空间是指定义在其他命名空间中的命名空间
- 嵌套的命名空间同时是一个嵌套的作用域
- 内层命名空间声明的名字将隐藏外层命名空间声明的同名成员
- 外层命名空间中的代码要访问内层命名空间的名字必须加限定符
- 内联命名空间(inline namespace)
- 内联命名空间的名字可以直接被外层命名空间直接使用,无须在内联命名空间的名字前添加该命名空间的前缀
- 定义方式:
inline namespace ns{}
- 关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间时可以写inline也可以不写
- 未命名的命名空间(unnamed namespace)
- 未命名空间中定义的变量拥有静态生命周期,它们在第一次使用前创建,直到程序结束时才销毁
- 和其他命名空间不同,未命名的命名空间仅在特定的文件内部有效,其作用范围不会横跨多个不同的文件
- 未命名空间中定义的名字的作用域与该命名空间所在作用域相同,因此不能与外层命名空间中的名字冲突
- 一个未命名空间也能嵌套在其他命名空间当中,此时未命名空间中的成员通过外层命名空间的名字来访问
- 在文件中进行静态声明的做法已经被C++标准库取消了,现在的做法是使用未命名的命名空间
- 使用命名空间成员
- 命名空间的别名(namespace alias)
namespace primer=cplusplus_primer;
- 一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价
- using声明(using declaration):扼要概述
- using声明引入名字的有效范围从using声明的地方开始,直到using声明所在的作用域结束为止,在此过程中,外层作用域的同名实体将被隐藏
- 一条using声明语句可以出现在全局作用域、局部作用域、命名空间作用域以及类作用域中
- 在类作用域中,这样的声明语句只能指向基类成员
- using指示(using directive)
- using指示以关键字using开始,后面是关键字namespace以及命名空间的名字
- using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类作用域中
- 如果我们提供了一个对std等命名空间的using指示而未做任何特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题
- using指示与作用域
- using指示引入的名字的作用域远比using声明引入的名字的作用域复杂
- using声明的名字的作用域与using声明本身的作用域一致,从效果上看,好像using声明语句为命名空间的成员在当前作用域内创建了一个别名一样
- using指示具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力
- using指示一般被看作是出现在最近的外层作用域中
- using指示引入的名字的作用域远比using声明引入的名字的作用域复杂
- 头文件与using声明或指示
- 头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中
- 头文件最多只能在它的函数或命名空间内使用using指示或using声明
- 避免使用using指示
- 相比于使用using指示,在程序中对命名空间的每个成员分别使用using声明效果更好,这么做可以减少注入到命名空间中的名字数量
- using指示也并非一无是处,例如在命名空间本身的实现文件中就可以使用using指示
- 头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中
- 命名空间的别名(namespace alias)
- 类、命名空间与作用域
- 对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域
- 只有位于开放的块中并且在使用点之前声明的名字才被考虑
- 对于位于命名空间的类来说,常规的查找规则仍然适用:当成员函数使用某个名字时,首先在该成员中进行查找,然后在类中查找(包括基类),接着在外层作用域中查找
- 除了类内部出现的成员函数定义之外,总是向上查找作用域
- 可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域
- 实参相关的查找与类类型形参
- 对于命名空间中名字的隐藏规则来说有一个重要的例外
- 当给函数传递一个类类型的对象时,除了在常规的作用域中查找外还会查找实参类所属的命名空间,这一例外对于传递类的引用或指针的调用同样有效
- 查找规则的这个例外允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用
- 对于命名空间中名字的隐藏规则来说有一个重要的例外
- 查找与std::move和std::forward
- 在函数模板中,右值引用的形参可以匹配任何类型,std::move和std::forward就是这样的函数
- 如果我们定义接受一个形参的move或forward函数,无论形参的类型是什么都会与std::move或std::forward冲突
- 因此建议书写std::move和std::forward显式调用标准库版本
- 在函数模板中,右值引用的形参可以匹配任何类型,std::move和std::forward就是这样的函数
- 友元声明与实参相关的查找
- 一个另外的未声明的类或函数如果第一次出现在友元声明中,则认为它是最近的外层命名空间的成员,这条规则与实参相关的查找规则结合在一起将产生意向不到的效果
namespace ns{ class C{ friend void f2(); friend void f(const C&); }; } int main(){ ns::C cobj; f(cobj); //f2(); }
- 一个另外的未声明的类或函数如果第一次出现在友元声明中,则认为它是最近的外层命名空间的成员,这条规则与实参相关的查找规则结合在一起将产生意向不到的效果
- 对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域
- 重载与命名空间
- 与实参相关的查找与重载
- 对于接受类类型实参的函数来说,将在每个实参类(以及实参类的基类)所属的命名空间中搜寻候选函数,即使其中某些函数在调用语句处不可见也是如此
- 重载与using声明
- using声明语句声明的是一个名字,而非一个特定的函数
- 一个using声明囊括了重载函数的所有版本以确保不违反命名空间的接口
- 一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数
- 如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明
- 如果using声明所在的作用域中已经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误
- using声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模
- using声明语句声明的是一个名字,而非一个特定的函数
- 重载与using指示
- using指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中
- 与using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误
- 只要指明调用的是命名空间中的函数版本还是当前作用域的版本即可
- 跨越多个using指示的重载
- 如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数集的一部分
- 与实参相关的查找与重载
- 多重继承(multiple inheritance)与虚继承
- 多重继承
- 多重继承的派生列表只能包含已经被定义过的类,而且这些类不能是final的
- 对于派生类能够继承的基类个数,C++没有进行特殊规定,但是在给定的派生列表中,同一个基类只能出现一次
- 多重继承的派生类从每个基类中继承状态
- 派生类构造函数初始化所有基类
- 多重继承的派生类的构造函数初始值只能初始化它的直接基类
- 基类的构造顺序与派生类列表中基类的出现顺序保持一致,而与派生类构造函数的初始值列表中基类的顺序无关
- 多重继承的派生类的构造函数初始值只能初始化它的直接基类
- 继承的构造函数与多重继承
- 如果使用某个基类的构造函数,对于其他基类进行默认初始化
- 如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本
- 析构函数与多重继承
- 派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的
- 析构函数的调用顺序正好与构造函数相反
- 多重继承的派生类的拷贝与移动操作
- 多重继承的派生类如果定义了自己的拷贝/移动构造函数和赋值运算符,则必须在包括基类的完整对象上执行拷贝、移动或赋值操作
- 只有派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作
- 多重继承的派生类如果定义了自己的拷贝/移动构造函数和赋值运算符,则必须在包括基类的完整对象上执行拷贝、移动或赋值操作
- 类型转换与多个基类
- 可以令某个可访问基类的指针或引用直接指向一个派生类对象
- 编译器不会在派生类向基类的几种转换中进行比较和选择,在编译器看来,转换到任意一种基类都一样好
- 以基类类型进行的函数重载可能会发生错误
- 基于指针类型或引用类型的查找
- 对象、指针和引用的静态类型决定了能够使用哪些成员
- 多重继承下的类作用域
- 如果在派生类中查找不到名字,将并行地在多个基类中查找名字
- 名字查找先于类型检查,当编译器在多个作用域中同时发现相同的名字时,直接报告一个调用二义性错误
- 即使名字在当前作用域中不可用
- 名字查找先于类型检查,当编译器在多个作用域中同时发现相同的名字时,直接报告一个调用二义性错误
- 对一个派生类来说,从它的几个基类中分别继承名字相同的成员是完全合法的,只不过在使用这个名字时必须使用前缀限定符
- 要想避免潜在的二义性,最好的方法就是在派生类为该函数定义一个新版本
- 如果在派生类中查找不到名字,将并行地在多个基类中查找名字
- 虚继承(virtual inheritance)
- 虚继承的目的是令某个类做出声明,承诺愿意共享它的基类,共享的基类子对象称为虚基类(virtual base class)
- 在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象
- 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身
- 使用虚基类
- 指定虚基类的方式是在派生列表中添加关键字virtual,与访问限定符出现的先后顺序随意
- 支持向基类的常规类型转换
- 不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作
- 虚基类成员的可见性
- 因为每个共享的虚基类中只有唯一一个共享的子对象,所以该虚基类的成员可以被直接访问
- 如果虚基类的成员被一条派生路径覆盖,则仍然可以直接访问这个被覆盖的成员,只不过访问的是基类的成员
- 如果成员被多于一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本
- 虚继承的目的是令某个类做出声明,承诺愿意共享它的基类,共享的基类子对象称为虚基类(virtual base class)
- 构造函数与虚继承
- 在虚派生中,虚基类是由最低层的派生类初始化的
- 即使虚基类不是派生类的直接基类,派生类也可以通过构造函数初始化虚基类
- 虚继承的对象的构造方式
- 含有虚基类的对象的构造顺序与一般的顺序稍有区别
- 首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化
- 如果最低层派生类没有显式初始化虚基类,将调用虚基类的默认构造函数进行初始化
- 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关
- 含有虚基类的对象的构造顺序与一般的顺序稍有区别
- 构造函数与析构函数的次序
- 编译器按照直接基类的声明顺序对其依次进行检查,以确定其中是否含有虚基类:如果含有虚基类,则先构造虚基类,然后按照声明的顺序逐一构造其他非虚基类
- 合成的拷贝和移动构造函数按照完全相同的顺序执行
- 合成的赋值运算符中的成员按照该顺序赋值
- 对象销毁的顺序与构造的顺序相反
- 在虚派生中,虚基类是由最低层的派生类初始化的
- 多重继承