前言
把《C++ Primer》读薄系列笔记全集。
目录
第I部分:C++基础
- 开始学习C++
- 变量和基本类型
- 字符串、向量和数组
- 表达式
- 语句
- 函数
- 类
第II部分:C++标准库
- IO库
- 顺序容器
- 范型算法
- 关联容器
- 动态内存
第III部分:类设计者的工具
- 拷贝控制
- 重载运算与类型转换
- 面向对象程序设计
- 模版与泛型编程
题解
修订版课后题解见GitHub仓库:cpp-primer-workbook。
开始学习C++
- main的返回值:0为成功状态,非0为系统定义的错误类型
- 输入输出:计算结果为左侧运算对象,IO操作读写缓冲与程序中的动作无关
- 输入流istream对象:cin(标准输入);流状态有效则cin为真,遇到EOF或无效输入cin为假
- 键盘输入EOF:Windows下ctrl+d->return,Unix下ctrl+d
- 输出流ostream对象:cout(标准输出)、cerr(警告错误、不缓冲)、clog(一般性信息、缓冲)
- 操纵符endl:结束当前行,并将缓冲区内容刷到设备中,调试时的打印应保证一直刷新流
- 缓冲刷新:默认情况下,cin和cerr会刷cout缓冲,程序异常终止cout不会被刷新
- 命名空间:避免相同名字导致的冲突,调用标准库需显式说明作用域std::
- 文件重定向:将标准输入输出与文件关联,运行命令
prog1 <infile >outfile
- 编译:
编译器 | 系统环境 | 编译 | 运行 | 获取返回值 |
---|---|---|---|---|
VS | Windows | c1 /EHsc /W4 prog1.cc |
prog1 |
echo %ERRORLEVEL% |
GNU | Unix | g++ -std=c++11 -Wall prog1.cc |
./a.out |
echo $? |
变量和基本类型
- 空类型(void):无值无操作,不能定义void类型变量
- 字符类型:char为UTF-8编码,wchar_t是确保机器可以存储及其最大扩展字符集中的任何字符的宽字符,char16_t和char32_t对应Unicode字符集
- 内置类型的机器实现:地址表示比特串开始位置,类型决定了数据所占的比特位数及如何解释其内容
- 浮点数:float和double分别有7和16个有效位,通常选用double,避免float的低精度及long double的低效率
- 未定义(undefined):引发难以追踪的运行时的错误、安全问题、移植性问题
- 赋值超范围:对无符号类型是初始值对无符号类型表示数值范围取模后的余数,而带符号类型结果是未定义
- 有无符号类型混用:表达式中同时包含带符号和无符号类型,带符号数会自动转为无符号数
- 整型字面值:十进制字面值默认为带符号数,是int/long/long long 中能容纳其值且尺寸最小。八进制(0开头)和十六进制(0x开头)则是int/unsigned int/unsigned long/long long/unsigned long long中尺寸最小者
- 浮点型字面值:小数或科学计数法形式的指数,默认类型double
- 字符串字面值:编译器在每个字符串结尾添加空字符'\0’
- 字符前缀与类型:u(char16_t),U(char32_t),L(wchar_t),u8(char)
- 泛化的转义序列:\x后跟十六进制数字,或\后跟1到3位八进制数字,可像普通字符一样使用
- 列表初始化:使用花括号初始化,当存在丢失信息风险时编译器将报错,而使用()可以执行但可能发生信息丢失
- 初始化:定义在任何函数之外的变量被初始化为0,每个类决定各自的初始化对象方式。而定义在函数体内部的内置变量类型将不初始化,试图访问未初始化的值将引发未定义行为
- 定义:负责创建与名字关联的实体,并申请存储空间和赋初始值
- 声明:规定变量的类型和名字,只声明不定义可在变量名前添加extern并不显式初始化,包含显式初始化即为定义
- 引用:为对象起的别名,定义时必须初始化,不是对象,不可定义引用的引用
- 指针:指向某个对象,定义时无需赋值,本身就是对象,对指针使用解引用符*可访问该对象
- 指针值状态:指向一个对象、指向对象的下一个紧邻空间位置、空指针、无效指针(访问或拷贝都会出错)
- 空指针(nullptr):不指向任何对象,可以转换成任何其他的指针类型
- 初始化指针:建议用已定义对象或nullptr或0初始化所有指针,把任何int型变量(即使值为0)赋值给指针是错误的
- void*指针:可存放任意对象的地址,但不能直接操作其所指对象
- 引用/指针类型匹配:除两种特例(const可绑定非const,基类可绑定派生类)外,指针和引用的类型都需要与之绑定的对象严格匹配
- 复合类型判断:从右向左阅读复杂的指针或引用的声明语句,离变量名越近的符号对变量的类型有越直接的影响
- 多文件共享const:不管声明还是定义都要添加关键字extern
- 初始化对const的引用:允许任何表达式作为初始值,只要该表达式结果可以转化为引用类型的临时量对象
- 指向常量的指针:自觉不去改变所指对象的值,而该对象若不是常量对象则其值通过其他方式改变
- const指针:必须初始化且不能修改,书写上直接在变量名之前,表示不变的是指针本身而非指针指向的对象
- 顶层/底层const:顶层const表示本身是常量,底层const表示绑定的对象是常量;执行拷贝操作时,拷入拷出对象必须具有相同的底层const资格,或能够强制转换
- 常量表达式:数据类型和初始值都需要是常量类型,值不会改变并在编译过程就能得到计算结果
- constexpr变量:一定是常量,必须用常量表达式(字面值类型,包括算术类型、引用、指针)或constexpr函数(足够简单编译时可计算结果)初始化
- constexpr指针:初始值是nullptr或0,或存储于某个固定地址中的对象
- 指针、常量与类型别名:
typedef char *pstring; const pstring cstr = 0;
,与const char *cstr
不等价,前者的cstr是指向char的常量指针,后者中cstr是指向常量char的指针 - 类型说明符auto:让编译器通过初始值推算变量类型,并赋诸该值;忽略顶层const,保留底层const
- 类型指示符decltype:让编译器通过初始值推算变量类型,但不用该初始值赋值;包含顶层const,解引用、(())、赋值产生的引用都会判为引用类型
- 预处理器:在编译前执行的一段程序,功能有替换#include的头文件,以及头文件保护符避免重复定义实体
- 预处理变量:#define将一个名字设置为预处理变量,#ifndef及#ifdef则判断名字是否已被定义过,无视作用域规则
字符串、向量和数组
- using声明:每个using声明引入命名空间的一个成员;头文件中的代码一般不应使用using声明
- string初始化:用数字和字符初始化,则string对象内容是将给定字符连续重复给定次数得到的序列
- getline:从输入流中读入内容直到读入换行符,保留空白符,但换行符不存入string对象中
- 字面值与string:字符字面值和字符串字面值可以转化为string对象,相加时加号两侧的运算对象至少有一个是string型
- C++版的C头文件:C++版本的C标准库头文件中,定义的名字从属于命名空间std,对应的C语言的.h文件则不然
- size_type:string::size_type和vector::size_type可表示各自类型的长度或下标,无符号整数
- 实例化:根据模版创建类或函数的过程,必须指明实例化成何种类型
- 初始化特例:拷贝初始化时只能提供一个初始值;类内初始值只能使用拷贝初始化或花括号;初始元素值的列表只能放在花括号内
- 值初始化:通常使用圆括号;但如果使用花括号但提供的值又不能用来列表初始化时,编译器也会尝试用值初始化
- vector比较:当元素的值可比较时,按照字典顺序进行
- 下标运算符:vector或string的下标运算符可用于访问已存在的元素,而不能用于添加元素
- 迭代器成员:begin和end运算符的具体类型由对象是否是常量决定,cbegin和cend始终得到const_iterator
- 迭代器比较:两个迭代器,指向同一容器中的元素或尾元素的下一位置,比较的是位置的前后;相减得到different_type型的有符号整数,表示两个迭代器的距离
- 定义数组:必须指定数组类型,不能使用auto由初始值推断;类型不能是引用
- 字符数组:可以使用字符串字面型初始化,数组中需要空间放空字符
- 复杂数组声明:从数组名字开始按照由内向外的顺序阅读,如
int *(&a)[10]
表示a是对数组引用,该数组包含10个int* - 数组下标:通常定义为无符号类size_t类型,但与标准哭类型不同的是,数组允许负数作为下标
- 数组与指针:多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针
- 首指针和尾后指针:使用标准库函数begin()和end()可以得到数组的首指针和尾后指针;提供这两个指针可以将数组拷贝初始化vector对象;尾后指针不指向具体元素,不能解引用或递增
- 指针比较:只要两个指针指向同一个数组的元素或尾元素下一个位置,就能用关系运算符比较前后;两个指针相减的结果类型是有符号类型ptrdiff_t
- string转char*:
s.c_str()
函数将string对象s,转化为一个C风格的字符串,但无法避免s改变后之前返回的字符串失去效用 - 多维数组初始化:使用花括号的形式,未列出的元素执行默认值初始化
- 范围for处理多维数组:除最内层循环外,其他所有循环的控制变量都应该是引用类型,以避免这些数组形式的元素被自动转换成指向数组首元素的指针
- 多维数组指针:多维数组的指针是指向内层数组的指针;避免指针类型混淆可使用auto或decltype,begin()和end(),类型别名简化多维数组的指针
表达式
- 左值右值:左值用的是对象的身份,右值用的是对象的值
- 求值顺序:未指定执行顺序的表达式,如果指向并修改同一个对象,会引发错误产生未定义;只有4种运算符规定了从左到右的求值顺序(条件与
&&
、条件或||
、条件运算符?:
、逗号,
) - 整数相除:商向0取整;取余时m%(-n)=m%(n),-m%n=-(m%n)
- 真值测试:比较运算中除非比较对象是bool型,否则不使用布尔字面值
- 值初始化:无论左侧运算对象是什么类型,初始值列表都可以为空,编译器会创建值初始化的临时量赋给左侧运算对象
- 复合赋值运算符:只求值1次,普通运算符需要2次(右边表达式和赋值)
- 整型提升:小整型(如short、char)总会自动提升为较大的整型,一般为int;较大的char(如w_char)提升为大整型中可容纳原值的最小一类
- 移位运算符:右侧的移动位数必须非负且小于结果的位数;移出位被舍弃,符号位视机器而定
- sizeof运算符:可使用作用域来获取类成员大小,可使用无效的指针获取指针指向的对象所占空间,不会把数组当指针处理,对string或vector返回固定部分的大小;sizeof的返回值是常量表达式
- 逗号运算符:先对左侧表达式求值,然后丢弃求值结果,真正的结果是右侧表达式的值
- 数组指针转换:大多时候会自动隐式转换;当数组被当作decltype、&、sizeof、typeid的运算对象时,转换不发生
- 指针的转换:0或nullptr可转换成任意指针类型,非底层const的指针可以转为
void*
,底层const的指针可以转为const void*
- 命名的强制类型转换:static_cast(无底层const)、dynamic_cast(运行时类型识别)、const_cast(有底层const)、reinterpret_cast(低层的重新解释)
语句
- switch语句:对括号内表达式求值转换为整数类型,然后与case后的每个常量表达式比较
- default标签:当其他case都不满足时执行,位置自由,若为空也必须跟上一个空语句( ;)或空块({})
- 控制流跳转:在switch和goto中,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置
- 范围for语句:语法形式是
for (declaration: expression) statement
;expression表示一个可以返回迭代器的begin()和end()的序列,不允许增删容器元素改变范围for中预存的end()值 - 异常:throw引发异常,try中跑出的异常通过会被某个catch处理,在throw与catch间通过异常类传递信息
- 寻找异常处理代码:寻找异常处理代码的过程与函数调用相反,逐层回退直到找到适当类型的catch子句;若最终未找到则会转到terminate的标准库函数,全无try语句的程序会直接调用terminate
- 异常安全:确保对象有效,资源无泄漏,程序处于合理状态,等等
- 异常类:头文件exception中的exception类、stdexcept中定义了几种常见的异常类、new中的bad_alloc、type_info中的bad_cast
- 异常类初始化:exception、bad_alloc、bad_cast只允许默认初始化,what()返回编译器决定的异常信息;标准异常类必须使用string或C风格字符串初始化,what()返回该串
函数
- 函数参数:实参是函数中形参的初始值,存在对应关系,但并没有规定实参的求值顺序
- 局部静态对象:不同于只存在于块执行期间的自动对象,局部静态对象直到程序终止才被销毁;没有显式初始值时会执行值初始化
- 函数声明:函数可以只能声明一次,但可以定义多次,如果一个函数永远不会被用到,可以只声明不定义
- const参数:实参初始化形参时会忽略顶层const,所以有无顶层const版本等价,都定义则属于重复定义
- 数组实参:不允许拷贝数组,传递数组时实际上是传递指向首元素的指针;不同大小的数组是不同类型;若形参是引用,数组不会转换为指针
- 传多维数组:数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略
- initializer_list:实参数量未知但类型都相同,initializer_list可作形参;提供的操作类似容器,但对象中的元素永远是常量
- 省略符形参:出现在形参列表的最后一个位置,形式为
f(…)
或f(t,…)
;大多类类型对象传递给省略符形参时无法正常拷贝 - 返回引用:不要返回局部对象的引用或指针,调用一个返回引用的函数得到左值
- 列表初始化返回值:函数可以返回花括号包围的值的列表;若返回为内置类型,则花括号内至多一个值,而且所占空间不应大于目标类型空间
- main的返回值:控制到了main的结尾仍无return语句,编译器隐式插入
return 0;
;头文件cstdlib中定义了预处理变量EXIT_FAILURE和EXIT_SUCCESS表示成败 - 返回数组指针:形如
Type (*function(parameter_list))[dimension]
,函数返回类型是指向大小为dimension的数组的指针 - 尾置返回类型:形如
auto function(parameter_list) -> Type (*)[dimension];
- decltype确定返回类型:把左值转换为引用;由于decltype不负责把数组或函数类型转换为对应指针,所以函数声明时要加一个星号
- main函数:不能调用自己,不能重载
- const_cast和重载:常用const_cast修改const参数属性的性质,将const和非const版本的重载函数关联
- 重载与作用域:名字查找发生在类型检查前,所以在不同作用域无法重载函数名,且内层将隐藏外层声明的同名函数
- 形参默认值:被赋予了默认值的形参,后面所有形参都必须有默认值;在给定的作用域中一个形参只能被赋予一次默认值;可多次声明函数给不同形参赋予默认值
- 内联函数:一般只有当函数规模小、流程直接、无递归时,内联函数的请求才会被编译器接受
- constexpr函数:能用于常量表达式的函数,但返回值可以是非常量;返回和形参类型都是字面值类型,函数只执行一句return;被隐式指定为内联函数;
- 内联定义:和其他函数不同,内联和constexpr函数可以多次定义,但每次必须完全一致,通常定义在头文件中
- 预处理宏assert:根据提供的表达式判断是否要输出错误信息并终止程序,可定义预处理变量NDEBUG禁用assert的效果
- NDEBUG:可在
#ifndef NDEBUG
和#endif
之间编写自己的调试代码;有5个编译器预定义的名字变量__func__/__FILE__/__LINE__/__TIME__/__DATE__
,用于输出调试信息 - 函数匹配:先选择在调用点可用的同名候选函数,再找到参数数量相等且类型相同或可强制转换的可行函数,再寻找其中最佳匹配的函数
- 最佳匹配:每个实参的匹配都不劣于其他可行函数,且至少有一个优于其他;若找不到最佳匹配则报二义性错误
- 实参类型转化优先级:精确匹配>const转换>类型提升>算术类型或指针转换>类类型转换
- 函数指针:声明指向一个函数的指针,只需要用指针替换函数名;将函数名当作值使用时会自动转换成指针
- 重载函数的指针:指针类型必须与重载函数中的某一个精确匹配
- 函数指针形参:函数声明中,若形参是函数类型,则它会自动转换成指向函数的指针
类
- 数据抽象:定义数据成员和函数成员,抽象数据类型依赖封装
- 封装:分离接口(用户所能执行的操作)和实现(数据成员、实现接口的函数体、私有函数)
- 成员函数:声明必须在类内部,但定义可在类内或外,定义在类内部的函数是隐式的inline函数
- this:指向类类型非常量版本的常量指针,所以不能在常量类对象上调用普通成员函数
- 常量成员函数:参数列表后带const的函数叫常量成员函数,它可将this设置为指向常量的指针,使得常量对象可访问常量成员函数,但不可写入新值;若以引用形式返回*this,则返回类型是常量引用
- 合成的默认构造函数:会尽可能使用类内初始值初始化数据成员,当类不存在任何构造函数时编译器创建的默认构造函数;既需要其他形式又需要合成的默认构造函数,可在默认构造函数的参数列表后面加上
= default
- 类的动态内存:当类需要分配类对象之外的资源时,合成的版本常常会失效;需要动态内存的类应该使用vector或string对象管理存储空间,避免分配和释放内存带来的复杂性
- class和struct:唯一区别是默认的访问权限,class为private,struct为public
- 友元:允许其他类或函数访问非公有成员,则可在类的开头用friend关键字将其声明为友元;友元的声明仅仅限定了权限,可在真正的函数声明之后;友元函数可以定义在类的内部,但在类的外部仍然需要提供相应的声明使得函数可见
- 可变数据成员:添加mutable关键字的数据成员,即使在const成员函数中,依旧可以被改变
- 类内初始值:必须使用符号或花括号的直接初始化形式
- 重载const:根据是否是常量对象,决定调用是否是常量版本的成员函数
- 类的声明:仅声明不定义叫做前向声明:类在声明之后、定义完成之前是不完全类型,可以定义其指针或引用,作为函数的参数或返回值;只有当类全部完成后类才算被定义,所以类的成员中不能包含自己,但可以出现指针或引用
- 类的定义:编译所有成员的声明之后才会编译函数体
- 类型名的定义:内层作用域可以重新定义外层作用域的名字,但若外层作用域中的某个名字表示类型,则类中不能再重新定义该名字
- 访问类成员:使用this->或类名::可强制访问类成员,无视作用域的名字查找规则;无类名加::表示全局对象
- 成员初始化:若成员是const、引用、无默认构造函数的类,必须通过初始值列表将其初始化
- 初始化顺序:与类中定义的顺序一致,不受初始值列表中顺序影响
- 默认构造函数:当对象被默认初始化或值初始化时,自动执行默认构造函数;不那么明显的一种情况是类的某些数据成员缺少默认构造函数
- 默认初始化场景:无任何初始值定义非静态变量或数组,本身含有类成员且使用合成的默认构造函数,类类型成员没有在构造函数初始值列表中显式初始化
- 值初始化:初始值数量小于被初始化的数组大小,不使用初始值定义局部静态变量,通过T()表达式显式请求值初始化
- 默认构造对象:使用默认构造函数定义对象的格式为类名+对象名,无之后空的圆括号对,否则将定义为函数而非对象
- 类类型转换:构造函数只接受一个实参,则其定义了一种从构造函数的参数类型到类类型的隐式转换规则;编译器只会自动执行一步类型转换
- 显式构造函数:在类内添加explicit关键字的构造函数只能用于直接初始化,抑制隐式转换;编译器不会在自动转换过程中使用它,但可用于显式的强制转化
- 聚合类:所有成员public、未定义构造函数、无类内初始值、无基类或virtual函数;使用花括号初始化,花括号内的初始值顺序与声明顺序一致
- 字面值常量类:数据成员都是字面值类型的聚合类;或数据成员都是字面值类型、至少含有一个constexpr构造函数、类内初始值是常量表达式或拥有自己的constexpr构造函数、使用析构函数的默认定义
- constexpr构造函数:为保证构造函数的不包含返回语句,和constexpr函数唯一可执行语句即返回函数,其函数体一般为空
- 静态成员:独立于任何对象之外,不包含this指针;静态成员函数不能声明为const,也不能在内部使用this指针
- 静态数据成员:必须在类的外部定义和初始化,方式和类外部定义成员函数类似;若静态数据成员为constrexpr类型,且其只限于编译器替换值的使用场景,则可以在类内定义,否则必须在类外再重新定义
- 静态成员的优势:类内可包含自身类型的静态数据成员,但普通成员只能是指针或引用;静态成员可以作为默认实参,非静态数据成员的值属于对象的一部分,不能作为默认实参
IO库
- IO类继承机制:ifstream和istringstream继承自istream,ofstream和ostringstream都继承自ostream
- 宽字符IO类:在函数和类型前加前缀w,如wcin、wistream
- IO对象无拷贝赋值:IO操作的函数通常以引用方式传递和返回流;由于读写会改变状态,IO对象的引用不能是const
- 条件状态:iostate表示流状态的类型,其包含4种constexpr值,badbit(流崩溃)、failbit(可恢复错误)、goodbit、eofbit;对应4个函数bad()、fail()、good()、eof()
- 管理条件状态:rdstate()获取状态,clear()清除所有错误标志位,clear(flags)和setstate(flags)将状态置为flags
- 刷新输出缓冲区:可使用操纵符endl(换行)、flush、ends(空字符);开启unitbuf每次调用flush,nounitbuf解除
- 关联流:交互式系统通常关联输入和输出流,使所有输出在读操作前被打印;每个流同时最多关联到一个流,但多个流可以关联到同一个ostream;将其关联到空指针可彻底解开关联
- fstream特有:打开文件绑定流的open()、关闭绑定文件的close()、文件是否成功打开且尚未关闭的is_open()
- stringstream特有:将s拷贝到stringstream对象的str(s)、返回保存的string的拷贝的str()
顺序容器
- 选择容器:标准库容器性能通常优于同类数据结构;C++标准新增array数组和forward_list单向链表;很多小元素且额外空间开销大,不用list或forward_list
- 容器的额外操作:iterator表示迭代器类型,size_type无符号整型,value_type指元素类型,reference与value_type&等价
- size操作:返回容器内元素数量;forward_list没有size操作,其他容器的size操作是常量时间
- 反向容器:reverse_iterator按逆序寻址的迭代器,rbegin()是尾迭代器,crend()表示const首前迭代器,
- forward_list:不支持反向容器迭代器,且其迭代器不支持递减运算
- 容器初始化:必须是相同的容器类型,且保存相同类型的元素
- 用大小初始化:只有顺序容器的构造函数才接受大小参数,关联容器不支持
- array:具有固定大小,列表初始化的数目必须等于或小于array的大小;花括号列表只能初始化不能赋值
- assign:仅顺序容器支持,传递给assign的迭代器不能指向调用assign的容器;会导致指向容器的迭代器、引用、指针失效
- swap:除array外,swap不对任何元素进行拷贝、删除、插入,可保证常数时间完成;除string外,指向其他容器的迭代器、引用、指针在swap后都不会失效
- 关系运算符:两边的运算对象必须是相同的容器类型,且保存相同类型的元素
- 插入元素:返回新添加的第一个元素的迭代器;向vector、string、deque插入元素,会使指向的迭代器、引用、指针失效;插入迭代器表示的范围内的一段元素时,不能指向与目的位置相同的容器
- 元素是拷贝:用一个对象来初始化或插入容器时,实际上放入的是拷贝,容器中元素与提供值的对象无任何关联
- 访问元素:at和下标操作只支持string、vector、deque、array;访问成员函数返回的是引用
- 删除元素:clear和pop返回void,erase返回指向最后一个被删元素之后元素的迭代器
- 特殊的forward_list操作:添加或删除通过改变给定元素之后的元素来完成;定义了首前迭代器before_begin();insert_after()返回指向最后一个插入元素的迭代器;erase_after()返回指向被删元素之后的元素的迭代器
- 改变容器大小:resize()可用来增大或缩小容器,缩小容器则多余元素被删除,增大则填充指定值或默认初始值对象
- 不保存尾后迭代器:在循环中插入删除deque、string、vector中元素,应每次重新调用end()
- 管理容器大小:capacity()返回容器保留内存的大小,空容器返回0;reverse()只能增加容器保留内存大小;减少所占空间可用shrink_to_fit()申请将空间值缩小到元素数量,但不保证一定退回内存空间
- 重新分配内存:只有执行insert时size和capacity相等,或resize、reserve时给定大小超过capacity,vector才会分配新的内存空间,分配量取决于底层实现
- 修改string:replace和insert所允许的参数形式args依赖于范围range和位置pos是如何指定的
- string::npos:string搜索失败返回的static成员,类型是const string::size_type,被初始化为-1
- string搜索:find()第一次出现,rfind()最后一次出现,find_first_of()参数中任何一个字符第一次出现,find_last_not_of()非参数中的字符最后一次出现;找到返回下标,没找到返回npos
- string数值转换:
s=to_string(val); val = stoi(s, p, b);
val可以是任何算术类型,对应stoi函数替换为stol、stod等,p表示字符串开始转换的下标,b表示转换所用基数 - 容器适配器:容器适配器接受一个已有的容器类型,使其行为看起来像一种不同类型
- 底层容器:stack和queue默认基于deque,priority_queue默认基于vector;不能使用不能添加删除元素的array,或不能访问尾元素的forward_list作为底层容器;stack可用除array和forward_list外的任何容器构造,queue可使用list或deque,priority_queue可基于vector或deque
- 适配器操作:每个适配器都基于底层容器类型的操作定义了自己的特殊操作,只能使用适配器操作,不能使用底层容器操作
- 迭代器失效:添加或删除元素可能使指向容器元素的指针、引用、迭代器失效
添加元素 | 删除元素 | |
---|---|---|
vector、string | 存储空间未重新分配,则插入位置之后失效;否则全部失效 | 被删元素及之后失效 |
deque | 在首尾添加只有迭代器失效;否则全部失效 | 删除首元素只首元素失效;删除尾元素则尾后迭代器也失效;删除首尾之间元素则全部失效 |
list、forward_list | 不影响 | 只有被删元素受影响 |
范型算法
- 范型算法:实现了一些经典算法的公共接口,可用于不同类型的元素、多种类型的容器、其他类型序列
- 迭代器与算法:算法工作于迭代器之上,迭代器令算法不依赖于容器,但依赖于元素类型的操作;算法永远不会执行容器操作,所以不会改变底层容器的大小
- 输入范围:标准库算法通常对一个范围内的元素进行操作,用两个迭代器参数表示左闭右开区间
- 算法分类:是否读取元素、改变元素、重排元素顺序
- 只读算法:只读取而不改变元素,最好用常量迭代器;若计划用算法返回的迭代器改变元素值,就要传递非常量参数
- 写算法:算法不检查写操作,都假定目的位置空间足够大;常见错误有在刚定义的空容器上进行fill_n或其他写操作;保证算法有足够元素空间来容纳输出可用插入迭代器
- 迭代器参数:一些算法读两个序列,不要求容器或元素类型严格匹配,只要求两个序列中的元素能够比较
- 拷贝算法:copy返回目的位置迭代器(递增后)的值;多个算法提供“拷贝”版本,接受一个额外的目的迭代器参数,将新序列拷贝到里面,而不改变原输入范围的序列
- 重排算法:去除容器重复元素的方法是,先调用重排算法sort和unique,再用容器操作erase
- 定制操作:算法默认使用<或==运算符完成,但允许我们传递任何类型的可调用对象,用自定义操作代替默认运算符
- 可调用对象:函数、函数指针、重载了函数调用符()的类、lambda表达式
- 谓词:返回可转换为bool型的函数,通常用于判定元素关系;根据接受参数个数分为一元谓词和二元谓词
- lambda表达式:[捕获列表] (参数列表) -> return 返回类型 {函数体};某些时候参数列表和返回类型可省略;不能有默认参数
- 捕获列表:只需捕获局部非静态变量,分为值捕获和引用捕获;值捕获的变量的值是lambda创建时的拷贝,若要改变需要在参数列表后加mutable关键字;引用捕获要保证lambda执行时变量仍存在,是否可变由是否是常量引用而定
- 隐式捕获:让编译器根据lambda体中的代码推断需要使用哪些变量,使用&或=参数表示引用捕获或值捕获;混合使用隐式和显式捕获时,两者捕获方式必须不同
- lambda返回:lambda体只含有一个return语句可省略返回类型;否则lambda默认将返回void,必须指定返回类型
- bind函数:auto newCallable = bind(callable, arg_list);传给newCallable的参数会按照arg_list中占位符参数的顺序传给callable
- 占位符:形如_n,n为整数;定义在std::placeholders的命名空间
- 绑定引用参数:bind的那些不是占位符的参数默认被拷贝到可调用对象中;希望传递给bind一个对象而不拷贝它,必须使用ref或cref;bind、placeholders、ref都定义在头文件functianal中
- 插入迭代器:back_inserter、inserter、front_inserter调用容器操作push_back、insert、push_front;*it、++it、it++都返回it
- 流迭代器:将对应流当作特定类型的元素序列处理;istream_iterator输入,ostream_iterator输出
- istream_iterator:空的istream_iterator可作为尾后迭代器;比较两个istream_iterator是否相等必须读取相同类型,如果都是尾后迭代器或绑定到相同的输入则相等
- ostream_iterator:it、++it、it++都返回it;推荐使用it,可与其他迭代器的使用保持一致,将来修改更容易
- 反向迭代器:rbegin指向尾元素、rend指向首前元素;递增操作移动到前一个元素;反向迭代器的base操作可以得到普通迭代器,但正反向迭代器互相转换时指向的是不同元素
- 算法要求的迭代器:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器;C++标准指明了算法的每个迭代器参数的最小类别,但编译器通常不会报该类错
- 算法命名:接受谓词来替代默认运算符、或不接受额外参数的算法通常是重载函数;接受谓词来替代元素值的算法都有附加的_if,如find_if;拷贝到额外空间都有附加的_copy,如replace_copy;一些算法同时提供_copy和_if,如remove_copy_if
- 链表的算法:list和forward_list有自己的函数成员sort、merge、remove、reverse、unique,可改变底层容器,优先使用它们而不是通用算法
- splice成员:链表特有的算法,将一个链表的一截移动到另一个链表的指定位置,要保证移动的目的位置不在待移动范围内
关联容器
- 关联容器分类:set还是map、关键字是否重复、关键字是否有序
- 关联容器:可列表初始化;不支持位置相关操作,不支持参数为元素值和对应数量的构造或插入;迭代器是双向的
- 有序容器:关键字类型必须定义了比较方法,默认情况下使用<运算符,可自定义一个严格弱序
- 严格弱序:不能同时“小于等于”对方;“小于等于”具有传递性;双方都不“小于等于”对方称为“等价”,“等价”具有传递性
- pair类型:可用花括号包围的初始化器来返回pair类型的对象,老版本只允许显式构造
- 关联容器迭代器:map的value_type是pair<const key_type, mapped_type>,所以map迭代器只能改变关键字映射的值,不能修改关键字;set的value_type等于key_type,都是const关键字,不能修改
- 关联容器insert:只有当关键字不存在时才插入,函数返回一个pair,包含一个指向元素的迭代器,和一个指示是否插入成功的bool型
- 关联容器erase:若传入迭代器,必须保证指向真实元素;若传入关键字,则返回被删去的所有该关键字的元素的数量
- map的下标操作:map下标操作返回mapped_type,解引用返回value_type;若关键字还不存在,会创建一个对应关键字且值为0的新元素,已存在则返回最近一次赋的值;只能用于map和unorders_map
- 查找元素:find(第一个等于的迭代器)、count(数量)、lower_bound(第一个大于等于的迭代器)、upper_bound(第一个大于的迭代器)、equal_range(返回一个迭代器pair,表示所有相等的头及尾后位置)
- 无序关联容器:使用关键字的哈希函数和==运算符;不能直接定义关键字类型为自定义类类型的无序容器,必须提供自己的hash模板版本,或者用另一种方法,类似于有序容器重载关键字类型的默认比较操作
- 管理桶:无序容器在存储上组织为一组桶,每个桶保存哈希值相同的若干元素;无序容器的性能依赖于哈希函数的质量和桶的数量及大小;管理桶的函数包括桶接口、桶迭代、哈希策略
动态内存
- 动态内存:除了静态内存和栈内存,每个程序有自由空间(堆),用于存储动态分配的对象;自由空间中分配的对象直到显式释放或程序结束才被销毁
- 智能指针:自动释放指向的对象及对象所占内存;shared_ptr可多个指向一个对象,unique_ptr独占所指对象;智能指针不支持指针算术运算
- 引用计数:shared_ptr关联的计数器,拷贝shared_ptr时递增,赋值或销毁时递减;计数器为0则自动释放管理对象
- 使用动态内存的场景:不知道对象数量、不知道对象的准确类型、多个对象间共享数据
- 直接内存管理:new分配内存,delete释放;不能依赖类对象拷贝、赋值、销毁的默认定义;内置类型默认初始化为未定义,建议用圆括号直接初始化;只有当括号中仅有单一初始化器时才可以使用auto
- 动态分配的const:new分配const对象是合法的,它的值不能被改变,但本身可用delete销毁
- 空悬指针:指向一块曾经保存数据对象但现在无效的内存的指针;delete指针会变成空悬指针,此时建议用nullptr给它赋值
- 初始化智能指针:不能进行内置指针到智能指针的隐式转换,必须使用直接初始化形式;智能指针默认使用delete释放,所以用于初始化智能指针的普通指针必须指向动态内存,管理的资源不是new分配的内存,则必须传入一个删除器替代delete
- 混用智能指针和普通指针:shared_ptr绑定普通指针,不应再使用内置指针访问这块内存;只有确定代码不会delete指针时才能使用get将访问权限传递给代码,不要用get初始化或给另一个智能指针赋值
- 修改shared_ptr共享对象:当改变底层对象时调用unique判断自己是否是仅有用户,如果不是则用reset制作新拷贝
- unique_ptr:不支持普通的拷贝或赋值,但可拷贝或赋值一个即将销毁的unique_ptr,如函数返回值;调用release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr;调用reset()或用nullptr赋值可以释放对象,release只会切断管理关系
- unique_ptr的删除器:管理方式与share_ptr不同,删除器会影响到类型及构造过程;重载删除器必须在尖括号里指明删除器类型,并在参数列表里指定一个可调用对象(删除器)
- weak_ptr:是一种弱引用,指向shared_ptr的对象,但不改变引用计数;调用lock返回指向的shared_ptr对象,当对象计数器为0时返回空shared_ptr
- 分配动态数组:使用new T[]分配指定数量的内存,返回的是元素类型指针而不是数组;可用空括号或花括号对数组初始化,但不能在括号中出现初始化器,所以不能用auto;可分配0长度的数组,此时返回的指针类似尾后指针
- 释放动态数组:delete []释放,空的方括号是必须的,否则会没有警告但行为异常
- 动态数组的智能指针:unique_ptr管理new分配的数组,在尖括号里的对象类型后面跟一对空括号,不能使用点或箭头运算符,可使用下标运算符访问数组中的元素;shared_ptr不能直接管理动态数组,必须提供自己的删除器,并且只能通过get内置指针后进行指针算术运算来访问数组元素
- allocator类:allocate分配一定大小的原始未构造的内存;deallocate的参数必须是allocate返回的指针和allocate传入的大小;deallocate前调用destroy析构已创建的每个对象,allocate后调用cnostruct构造对象、或调用uninitialized_copy[_n]拷贝对象、或uninitialized_fill[_n]填充未初始化的内存
拷贝控制
- 拷贝控制成员函数:拷贝构造、拷贝赋值、移动构造、移动赋值、析构;不显式定义则编译器会生成合成版本
- 逐成员拷贝/移动:合成的拷贝和移动构造函数及拷贝与移动赋值运算符的工作方式,依次处理非stastic数据成员
- 拷贝构造函数:第一个参数必须是引用,常是const;不应设为explicit,因为某些情况下需隐式调用
- 拷贝初始化:直接初始化要求编译器使用普通的函数匹配,拷贝初始化则是将右侧运算对象拷贝到正在创建的对象中,并可能发生类型转换;拷贝初始化发生在用=定义变量、实参传递给非引用形参、返回类型为非引用、花括号列表初始化数组中的元素或聚合类中的元素、初始化标准库容器或调用其insert或push成员
- 绕过拷贝控制构造:编译器可以(非必需)跳过拷贝/移动构造函数,直接创建对象
- 重载运算符:若运算符是成员函数,其左侧运算对象绑定到隐式this参数;对二元运算符,右侧运算对象作为显式参数传递
- 赋值运算符:接受同类参数,返回指向左侧运算对象的引用
- 析构函数:释放对象使用资源,并销毁非stastic数据成员;不接受参数,不能重载;先执行函数体,再按初始化的逆序隐式销毁成员;内置指针类型不会delete对象,智能指针是类类型所有可以释放;引用和值指针离开作用域不析构绑定对象
- 三/五法则:5个拷贝控制成员应看作整体,定义了一个则应定义所有
- 使用=default:显式要求编译器生成合成版本的默认构造函数或拷贝控制成员;类内声明=default为内联
- 阻止拷贝:定义删除的拷贝控制函数;新标准发布前,通常将拷贝控制成员声明为private,并且不定义它们
- 删除的函数:使用=delete在函数第一次出现时声明,之后不能以任何方式调用这些函数;析构函数不能是删除,否则不能定义该类型对象,只能new动态分配并无法释放;一个类有数据成员不能默认构造、拷贝、复制、摧毁则会被定义为删除的
- 拷贝语义:类的行为像值,有自己的状态;类的行为像指针,共享状态;IO和unique_ptr不允许拷贝或赋值,值和指针都不像
- 类值拷贝赋值:自赋值也是安全的,发生异常后左侧对象状态仍有意义;先拷贝构造右侧对象到临时变量,再销毁左侧成员并赋予新值
- 引用计数:用于类指针拷贝控制成员中共享状态,构造、创建副本、赋值左侧递增,销毁、赋值右侧递减
- 拷贝并交换:在赋值运算符中,参数不是引用,此时结合拷贝和交换的赋值运算符是安全的
- 自定义拷贝控制成员:除了资源管理的原因,一些类还需要拷贝控制成员的帮助来进行簿记工作或其他操作,它们所具有的公共工作应放在private的工具函数中完成
- 动态内存管理类:alloc_n_copy (allocate, uninitialized_copy), free(destroy, deallocate), push_back (chk_n_alloc, construct), chk_n_alloc (capacity, reallocate), reallocate (construct(std::move), free)
- 右值引用:用&&获得,表示即将销毁的对象;返回非引用类型的函数、算数、关系、位以及后置递增递减运算符,用const左值引用或右值引用绑定;返回左值引用的函数、赋值、下标、解引用、和前置递增递减运算符,用非const左值绑定;绑定右值引用的变量仍是左值
- std::move:显式将左值转换为对应的右值类型,调用move意味着之后只能对其销毁或赋值
- 移动操作:第一个参数是非const右值引用,移动后源对象会被销毁,这意味着它必须可析构;定义了移动操作,则必须定义自己的拷贝操作,因为合成的拷贝操作会被认为是删除的
- noexcept:声明和定义不抛出异常的移动构造函数和移动赋值函数都要显式指定noexcept,否则系统会使用拷贝操作
- 合成的移动操作:没有任何自定义的拷贝控制成员,且每个非stastic数据成员都可以移动时,编译器才会为他们合成移动构造函数或移动赋值运算符
- 移动和拷贝:编译器使用普通的函数匹配规则确定使用哪个拷贝控制成员;通常拷贝左值,移动右值;如果没有移动构造函数,右值也被拷贝
- std::make_move_iterator:将普通迭代器转化为移动迭代器,解引用得到右值引用;与uninitialized_copy结合可调用移动构造替代拷贝构造
- 引用限定:用来指出一个非stastic成员函数可用于左值(&)或右值(&&)对象;放在参数列表或const之后可区分重载版本;同名同参数列表的函数必须都加引用限定符或都不加
重载运算与类型转换
- 重载运算符:至少含有一个类类型参数,除函数调用运算符外都不能含有默认实参;优先级、结合律、运算对象个数与内置版本一致;返回类型通常和内置版本兼容;可以像普通函数一样显式调用
- 不重载的运算符:多数可以重载,除了(:: .* . ?:);不应该重载逗号、取地址(具有特殊含义),不应重载逻辑与逻辑或(无法保留求值顺序)
- 是否重载为成员:改变成员状态或与给定类型密切相关应为成员函数,左侧运算对象必须是运算符所属类的一个对象;具有对称性的通常设计为普通非成员函数
- 重载IO:必须为非成员函数,输出应减少格式化操作,输入能够在出错时将对象恢复
- 算术运算符:若类含有算术或位运算符,最好也定义复合赋值运算符,前者用后者实现
- 关系运算符:相等和不等应将其一工作委托给另一个;当两个对象不等时,必须能够用关系运算符比出大小
- 赋值运算符:拷贝赋值、移动赋值、使用其它类型作为右侧运算对象(如vector使用花括号的元素列表赋值)
- 下标运算符:同时定义两个版本,返回普通引用的成员和返回常量引用的常量成员
- 递增/递减运算符:前置改变值后返回引用,后置版本接受一个额外不被用的值为0的int形参,对象存入临时变量,改变值后返回保存改变前的临时变量的值
- 成员访问运算符:包括解引用和箭头运算符,箭头运算符一般将工作委托给解引用,通常都定义为const成员
- 箭头运算符:重载的箭头运算符必须返回类的指针,此时直接执行
(*point).mem
;或返回自定义了箭头运算符的某个类的对象,递归执行point.operator()->mem
,直到返回指针 - 函数调用运算符:像函数一样使用类,类包含状态所以比普通函数更灵活,定义了函数调用运算符的类叫函数对象
- lambda:编译器将lambda表达式翻译成未命名类的未命名函数对象,该类中包含重载的函数调用运算符;lambda默认不改变捕获的变量,所以函数调用运算符是const成员,如果lambda被声明为可变的,则调用运算符不是const;产生的类不含默认构造函数、赋值运算符及默认析构函数
- 标准库函数对象:一组表示算术运算符、关系运算符、逻辑运算符的类,每个类分别定义了执行命名操作的调用运算符;都是模版类,并可适用于指针判断指向对象
- 可调用对象:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类
- 调用形式:指明返回类型和传递给调用的实参类型,如
int(int, int)
- 标准库function类:模版类型是调用形式,对象是可调用对象;可将运算符与对应的可调用function对象存入函数表;与旧版本的unary_function和binary_function无关,这两个已经被更通用的bind替代
- 重载与function:将重载存入function会产生二义性问题,解决方法有存储指针和使用lambda
- 类型转换运算符:没有显式返回类型、没有形参的const成员函数;可以面向除void外的任意可作为函数返回值的类型定义
- 显式类型转换运算符:开头添加explicit关键字,避免隐式转换导致的异常;但如果表达式被用作条件,仍然被隐式执行;想bool的转换一般被定义为explicit operator bool()
- 类型转换与二义性:两个类提供相同的类型转换,无法利用本身也面临二义性的强制类型转换解决;类中定义了多个转换源或目标是算数类型的转换;调用重载函数时,若多个定义的类型转换都提供了可行匹配,它们一样好;类可转化为算术类型,并重载了运算符;运算符调用不能通过调用形式区分成员和非成员
面向对象程序设计
- 面向对象程序设计(OOP):数据抽象(分离接口和实现)、继承(相似类型建模)、动态绑定(忽略相似类型区别)
- 虚函数:基类希望派生类定义自己版本的函数,声明的返回类型前加virtual;虚函数在派生类中隐式为虚函数,可不加virtual;每个派生类中用于覆盖的虚函数,在声明后必须定义;声明时可在形参列表后加override编译检测
- 类派生列表:冒号后紧跟以逗号隔开的直接基类列表,每个基类前有访问说明符以控制访问
- 动态绑定:使用基类的引用或指针调用虚函数时,函数的版本在运行时根据动态类型决定
- 派生类构造函数:每个类控制自己成员的初始化,派生类必须使用基类的构造函数初始化基类部分,没有显示初始化的部分都会执行默认初始化
- 静态成员:整个继承体系中只存在静态成员的唯一定义
- 派生类声明:声明语句的目的是令程序知晓某个名字的存在及它是怎样的实体,所以派生类声明不包含派生列表;一个类作为基类前,一定要定义而非仅声明
- 防止继承:在类名或函数形参列表后加final关键字,可防止其他类再继承它
- 派生类转基类:引用、内置指针和智能指针都支持派生类向基类的转换;编译时可知的为静态类型,运行时动态绑定变量在内存中的动态类型
- 基类转派生类:含有虚函数的基类可使用dynamic_cast在运行时请求转换为指定派生类,确定安全可转换的使用static_cast强制覆盖掉编译器的检查工作
- 转换规则:只对指针和引用有效,可能因访问受限无法转换;由于大多类定义了拷贝控制成员,将派生类对象也可转为基类对象,但派生部分会被切掉,只处理基类部分
- 覆盖:派生类中与基类中形参和返回类型相同的函数可覆盖基类函数,一个例外是返回类型是指针或引用且可发生类型转换
- 虚函数的默认实参:实参值由本次调用的静态类型决定
- 作用域运算符:回避虚函数的动态绑定机制,覆盖原有的查找规则,指定某个版本的虚函数
- 纯虚函数:类内函数声明语句后加=0,定义与否皆可
- 抽象基类:含有或未经覆盖直接继承纯虚函数的类,负责定义接口给后续类覆盖;不能直接创建抽象基类的对象
- 重构:重新设计类的体系以便移动操作或数据,必须重新编译
- 受保护成员:类使用protected声明那些希望与派生类共享但不想被其他公共访问使用的成员;派生类的成员或友元只能通过派生类对象来访问基类的受保护对象,不能通过基类
- 访问权限:受基类中成员和派生列表中访问说明符共同影响;struct默认公有继承,class默认私有继承;派生访问说明符对派生类成员及友元能够直接访问基类成员不影响,只会控制派生类用户(包括派生类的派生类)对基类成员的访问权限
- 向基类转换的可访问性:只有当可以访问基类的公有成员时,决定派生类向基类的转换才是可访问的
- 类的用户:边写代码使用类的普通用户只能访问公有成员,负责编写类成员和友元的实现者还能访问私有(实现)部分
- 友元关系:不能传递,不能继承;每个类负责控制各自成员的访问权限
- 改变可访问性:派生类可为那些它可以访问的名字提供using声明,置于期望的成员访问说明符后;构造函数的using声明不改变构造函数访问级别;using指定explicit或constexpr无效
- 隐藏:派生类作用域嵌套在基类作用域内,所以派生类中定义的成员隐藏同名的基类成员
- 名字查找与继承:确定对象的静态类型;再在本类和基类向上递归查找同名函数;找到后进行类型检查是否调用合法;合法则看调用的是不是虚函数,是虚函数则根据动态类型确定运行版本,不是虚函数或通过对象调用的则产生常规函数调用
- 覆盖重载函数:一条基类成员函数的using声明可以把该函数所有的重载实例添加到派生类作用域中,此时派生类只需定义其特有的函数
- 虚析构函数:基类的析构函数如果不是虚函数,delete指向派生类的基类指针将产生未定义
- 合成拷贝控制:对直接基类部分拷贝后,再拷贝类本身的成员;销毁本身后再销毁基类,基类部分会自动销毁;基类是否是合成版本无影响,只要对应成员可访问且不是删除的函数
- 移动操作:基类多有虚析构函数,所以不会合成移动操作,如果确实需要应首先在基类中显式定义,此时要同时显式定义拷贝操作
- 定义拷贝操作:基类默认构造函数初始化派生类对象的基类部分,如果想拷贝或移动基类部分,则需要在派生类的构造函数初始值列表中显式使用基类的拷贝或移动构造函数析构;自定义的析构函数只负责销毁派生类自己分配的资源;构造或析构函数调用了虚函数,则应该执行与其所属类型对应版本的虚函数
- 继承的构造函数:类不能继承默认、拷贝和移动构造函数,如果没有直接定义则编译器会合成它们,即使继承了基类的其他构造函数;派生类数据成员将被默认初始化
- 基类构造含默认实参:派生类将获得多个继承的构造函数,每个分别省略掉一个有默认实参的形参
- 容器与继承:容器中应放置(智能)指针而非对象
- 继承与组合:派生类应反映与基类的Is A关系,公有派生类的对象应该可以用在任何需要基类对象的地方;类型之间Has A则暗含成员的意思
- 接口类:在抽象基类中声明一个接口类的友元,接口类中包含该抽象基类指针,负责隐藏整个继承体系
模版与泛型编程
- 动态类型:面向对象编程在运行时确定类型,泛型编程在编译时获知类型
- 模版:泛型编程基础,一个创建类或函数的蓝图,适用于编译时才确定类和函数类型的情况
- 模板定义:以template开始,后跟尖括号包围的模板参数列表,内含一个或多个由逗号分隔的模板参数
- 实例化函数模板:调用模板时,隐式或显示的指明模板实参,实例化出一个特定版本的称为实例的函数
- 模板参数:类型参数可看作类型说明符,跟在class或typename关键字之后;非类型参数表是一个值,通过特定类型名指定,可以是常量表达式的整型,或具有静态生存期的指针或引用
- 泛型代码原则:尽量减少对实参类型的要求,函数参数是const引用,条件判断仅用<比较运算
- 模板编译:由于实例化时需要掌握函数模版和类模版成员函数的定义,其定义通常也放在头文件中,与普通函数和普通类成员函数不同
- 实例化类模板:必须提供显式模板实参,每种元素类型生成的各个实例类之间无关联,拥有各自的static成员
- 类模版成员函数:定义在类外需用template开始,后跟类模版参数列表;实例化了的类模板,其成员只有在用到时才实例化
- 类模板和友元:非模板友元被授权访问所有类模板实例,模板友元可根据声明访问特定实例或所有实例
- 模板类型别名:不能用typedef引用模板,新标准可使用using定义别名,如
template <typename T> using partNo = pair<T, unsigned>; parNode<string> books; partNo<Student> kids;
- 模板参数作用域:在模板内不能重用模板参数名,正常的名字隐藏规则依旧适用;用作用域运算符访问类的类型成员时,需在句首加typename关键字,否则会被默认为static成员
- 默认模板实参:就标准只允许类模板有默认实参,新标允许函数模板也有默认实参;调用类模板时必须加上尖括号,即使所有参数都有默认值
- 成员模板:无论普通类还模板类中,本身是模板的成员函数叫成员模板,不能是虚函数;在类模板外定义成员模板,必须同时为两者提供模板参数列表
- 显式实例化:避免相同模板参数产生多个实例导致的额外开销;可用extern多次声明实例,但必须有且仅有一次定义;编译器会实例化该类的所有成员,所以显式实例化提供的类型必须能用于所有成员
- 智能指针的删除器:shared_ptr保存删除器指针,方便运行时传递可调用对象重置;unique_ptr中包含显式实例化的删除器成员,避免间接调用删除器时的运行开销
- 类型参数转换:通常不转换而是生成一个新的模板实例;有限可用的转换规则包括const转换和数组或函数指针转换;非模板类型参数正常转换
- 显式模板实参:在函数之后,实参列表前用尖括号提供显式实参;只有尾部的显式模板实参,并且可从函数参数推断出来的时候,才可忽略;显式指定的实参可正常类型转换
- 尾置返回类型:需要根据传入的参数确定返回类型时,可通过decltype推断形参作为返回类型,使用尾置返回类型格式返回
- 标准库类型转换模板:定义在头文件type_traits中,每个模板有一个公有成员type用于转换,不能转换时则返回模板参数类型本身;如remove_reference可将T&、T&&、T转换为T
- 函数指针:用函数模板初始化函数指针时,根据指针的类型推断模板实参;使用显式模板实参消除二义性
- 引用折叠:间接创建引用的引用,除右值引用的右值引用折叠为右值引用,其他3种引用的引用都折叠为左值引用
- 右值引用形参:可传给它任意类型的实参,若传引用也不改变左/右值属性;通常用于模板转发实参或模板重载;使用右值引用的函数模板通常重载绑定非const右值引用或const左值引用
- 转发:函数参数指向模板类型参数的右值引用,可保持对应实参的const属性和左/右值属性;进一步为解决接受右值引用参数不被理解为左值的问题,使用utility头文件中定义的forward,返回显式实参的右值引用,可保持实参类型的所有细节
- 函数模版匹配:候选函数包括所有模板实参推断生成的实例,模板和非模板的可行函数统一按类型转换排序,多个函数同样好则顺次选择唯一的非模板函数、无非模板函数时最特例化的模板函数,否则调用有歧义
- 可变参数模板:接受可变数目参数的模板函数或模板类;
class...、typename...、type-name...
指定参数包,之后跟零个或多个参数,用sizeof...(args)
获取参数个数;如template <typename… Args> void foo(const Args& … args)
中,Args是模板参数包,args是函数参数包 - 可变参数函数模板:可变参数函数通常是递归的,非可变参数版本的声明必须在作用域内,否则无限递归
- 包扩展:将一个包分解为多个元素,并对每个元素应用模式,通过在模式后添加省略号触发
- 转发参数包:参数定义为右值引用
foo(Args&&… args)
,转发时应用forward模式bar(std::forward<Args>(args)…)
- 函数模板特例化:必须为每个模板函数提供实参,关键字template后跟一对空的尖括号;特例化的本质是实例化,而非重载,因此不影响函数匹配
- 特例化与作用域:所有同名模板的声明应放在同一头文件的前面,在该文件内再接着声明这些模板的特例化版本。
- 类模板特例化:若不指定全部参数部分特例化,则得到的是模板,所以要先定义模板参数;也可以事先只特例化特定成员函数,之后调用相同类型时则会调用该特例化函数而非重新实例化该成员函数
- 无序容器与特例化:无序容器会组合使用关键字类型对应的特例化hash版本,和关键字类型上的相等运算符
转载请注明出处