《C++ Primer》第五版中文版学习笔记
《C++ Primer》第五版中文版学习笔记
开始
编译程序
编译一个C++源文件在命令行上可使用如下命令:$ cc prog.cc
其中cc是编译器的名字,$是系统提示符。windows系统会将这个可执行文件命名为prog.exe。
访问main的返回值的方法依赖于系统。在UNIX好玩Windows系统中,执行完一个程序后,都可以通过echo命令获得其返回值,在windows系统中查看状态可键入:
$ echo %ERRORLEVEL%
编译器可以选择不同的编译选项,在g++编译器中,可能需要制定-std=c++0x参数来打开对C++11的支持。
运行微软VS2010编译器的命令为cl:
C:\Users\me\Programs> cl /EHsc prog.cpp
命令cl调用编译器,/EHsc是编译器选项,用来打开标准异常。另外,编译器还可以包含一些选项来打开对程序结构问题发出警告,在GNU编译器中使用-Wall选项,在VS编译器中使用/W4选项。
文件重定向
大多数操作系统支持文件重定向,这种机制允许我们将标准输入和标准输出与命名文件关联起来:
$ addItems <infile >outfile
假定$是操作系统提示符,addItems是已经编译成名为addItems.exe的可执行文件,则上述命令会从一个名为infile的文件读取数据,并将输出结果写入到一个名为outfile的文件中,两个文件都位于当前目录中。
变量和基本类型
指定字面值的类型
字符和字符串字面值
前缀 含义 类型
u(如u'a') Unicode16字符 char16_t
U(如U'a') Unicode32字符 char32_t
L(如L'a') 宽字符 wchar_t
u8(如u8"Hi") UTF-8(仅用于字符串字面常量) char
整型字面值 浮点型字面值
后缀 最小匹配类型 后缀 类型
u或U unsigned f或F float
l或L long l或L long double
ll或LL long long
nullptr为指针类型(值为0,在cstdlib中还定义了NULL的预处理变量,值也为0)。
区分初始化和赋值
用=号对变量初始化(如int v = 10)与赋值不同,前者系创建变量时即赋予一个初始值,而后者是把对象的当前值擦除,而以一个新值来代替。
列表初始化
诸如int v = {0}; int a{0};这种用花括号来初始化变量在C++11中得到全面应用。这种初始化的形式被称为列表初始化。当用于内置类型的变量时,使用这种初始化有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错,如:
double d = 3.14;
int a = {d}, b{d}; // 此时,编译器将报错,转换未执行
变量声明和定义的关系
如果想声明一个变量而不定义它,就在变量名前添加关键字extern,而且不要显式初始化。如:
extern int i; // 仅仅是声明 extern int i = 0; // 声明且定义 int v; // 声明且定义 int a = 0; // 声明且定义 |
变量能且只能被定义一次,但可以被多次声明。
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
引用与指针的区别
引用只是一个对象的别名,其本身不是对象,因此必须初始化,且后面不能变更绑定到别的对象。而指针本身也是一个对象,具有自身的内存空间,因此可以指向其他对象,可以变更。也正是由于引用本身不是对象,因此,也不能定义一个指向引用的指针。但是,指针本身也是对象,因此可以有对指针的引用。在数组引用中,引用保存了数组的纬度信息,因此若一个函数的形参是数组的引用,则必须传入指定纬度大小的数组,而用普通数组作为形参,则由于转换为了指针,因此无法单独保留纬度信息。
引用一般要非常匹配,除非是const引用非const对象,或者是在类的继承层次中,父类与子类之间的引用。引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
const对象
const指出了常量性质,也就是一个对象不能被改变值,因此对const变量必须进行初始化。初始值可以是任意复杂的表达式:
const int i = get_size(); // 正确:运行时初始化
const int j = 32; // 正确:编译时初始化
const int k; // 错误:k是一个未经初始化的常量
但可以用一种非const变量去初始化const变量,因为此时只是拷贝,如
int i = 42;
const int ci = i; // 正确,此时i的值被拷贝给了常量ci
默认状态下,const对象仅在文件内有效。因为编译器在编译过程中,把遇到的const变量都替换成了对应的值,因此编译器必须在每一个用到该const变量的文件中找到其定义。所以,同一名称的普通变量在多个文件中只能有一次定义,但const变量则需要在用到的每个文件中均定义。如果我们想定义一个const变量,只定义在一个文件中,同时能被其他文件的代码访问,则需要在在其定义前添加extern关键字(extern取消了其仅在文件内有效的限制),在声明的地方也添加该关键字:
// file_1.cpp定义并初始化一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h头文件,声明该常量
extern const int bufSize;// 与file_1.cpp中定义的bufSize是同一个
顶层const与底层const。顶层const指变量本身是个常量,而底层const指指针指向的对象是常量。普通变量只能定义顶层const,底层const则与指针和引用等复合类型的基本类型部分有关。指针类型既可以是顶层const,也可以是底层const。
constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式,如字面值、用常量表达式初始化的const对象都是常量表达式。
const由于既可以在编译时初始化,又可以在运行时初始化(如通过函数返回值初始化),所以const对象不一定是常量。然而,声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化,如:
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
//只有当size是一个constexpr函数时,才是正确的声明语句
constexpr int sz = size();
常量表达式的值需要在编译时就得到计算,因此对声明为constexpr的类型必须有所限制,类型需为字面值类型,即算术类型、引用和指针,而自定义的类、IO库、string之类均不属于字面值类型,因此这些类型不能定义成constexpr。
尽管指针和引用都能定义成constexpr,但初始值有严格限制。一个constexpr指针的初始值必须是nullptr或者0或者是存储于某个固定地址中的对象。存储于某个固定地址中的对象是指如定义于所有函数体外的对象、局部静态对象。constexpr声明中如果定义了指针,则限定符constexpr仅对指针有效,而与指针指向的对象无关,类似于顶层const,表明系常量指针。如:
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
此处,constexpr看似修饰int,实际上只能修饰指针变量本身。
类型别名
定义类型别名的2种方式:typedef和别名声明using。
typedef int INT, *PINT; 即把INT作为int类型别名,把PINT作为int *的别名。
using INT = int;
如果类型别名指代的是复合类型或常量,则用类型别名再复合声明的对象具有不同效果,如:
typedef char *pstring;
//此处const为顶层const,即cstr系常量指针,即char * const cstr = 0
const pstring cstr = 0;
const pstring *ps; // ps是个指针,其对象是指向char的常量指针
这是因为当声明别名后,char *整个成了pstring的类型构成,因此其本质是一种指针,故当const pstring时,const是修饰了指针,即为常量指针。如果仅仅简单错误地将上述类型扩展理解为const char *,则此时基本类型为char,*只是声明符的一部分而已,即const char成了基本数据类型。
auto类型说明符
auto说明符是在声明变量时,用auto去描述变量(或返回值),此时由编译器根据初始值来分析类型,因此用auto声明的变量必须有初始值,而且在用auto同时声明多个变量时,多个变量的初始值类型必须一致。auto的注意点如下:
当用一个引用来初始化一个auto说明符声明的变量时,引用的对象类型是该变量的真正类型,如int i = 0, &r = i; auto a = r;此时,r是i别名,a也就是一个整型变量。
auto一般会忽略顶层const,而保留底层const。如:
const int ci = i; &cr = ci;
auto b = ci; // b是一个整数,ci的const特性被忽略
// c是一个整数,cr是ci的别名,而本来ci是一个顶层const
auto c = cr;
auto d = &i; // d是一个整型指针,指向整数的指针
// e是一个指向整数常量的指针,对常量对象取地址是一种底层const
auto e = &ci;
如果需要指出推断出的auto是一个顶层const,则需要明确指出,如const auto f = ci。
设置一个类型为auto的引用时,初始值的顶层常量属性仍能保留。如果要同时声明多个变量,初始值的类型也必须一致。
auto &m = ci, *p = &ci;// m是对整型常量的引用,p是指向整型常量的指针
auto &n = i, *p2 = &ci;// 错误:i是int,而&ci是const int
decltype类型指示符
(1)decltype是用表达式的类型而不使用表达式的值,可以不必象auto那样必须指定初始值。decltype仍旧会保留顶层const特性,如:
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,y绑定到变量x
decltype(cj) z; // 错误:z是一个引用,必须初始化
(2)如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型,其结果可以是引用类型:
int i = 42, *p = &i, &r = i;
// 正确:加法的结果是int,因此b是一个未初始化的int
decltype(r + 0) b;
// 错误:c是int &(解引用得到引用类型),必须初始化
decltype(*p) c;
(3)如果在decltype使用的表达式中,是变量名加上一对括号,则得到的类型与不加括号不同,加上括号将永远得到引用类型,而不加括号则跟表达式中的变量的原始类型一致:
decltype((i)) d; // 错误:d是int &,必须初始化
decltype(i) e; // 正确:e是一个未初始化的int
一般情况下,出现数组名的表达式时会把数组名转换为指针,而用decltype一个数组名时,其返回类型是该数组的类型,如有int ia[10],则decltype(ia) da,此时da也为包含10个int元素的数组。用于函数时也一样,不会自动把函数名转换为指针,而是返回该函数类型。
如果作用于一个取地址运算符,则为指向指针的指针,如有int p,则decltype(&p)的结果是int **类型。
11.类内初始值
C++11新标准规定,可以为数据成员提供一个类内初始值。创建对象时,若构造函数没有初始化这些成员,则这些成员会用上述初始值初始化。类内初始值的提供方式是放在花括号里或者放在等号右边,但不能使用圆括号。如:
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0; // =号方式提供类内初始值
double revenue {0.0}; // {}形式提供类内初始值
};
12.预处理器变量
如#define DEBUG,此时DEBUG就是预处理器变量,该种变量无视C++的作用域规则。
字符串、向量和数组
string的初始化和提供的操作
初始化string的方式 |
|
string s1 |
默认初始化,s1是一个空串 |
string s2(s1) |
s2是s1的一个副本 |
string s3 = s1 |
等价于s3(s1),s3是s1的一个副本 |
string s4("value") |
s3是字面值"value"的副本,除了字面值最后的空字符 |
string s5 = "value" |
等价于s5("value"),s5是字面值"value"的副本 |
string s6(n, 'c') |
把s6初始化为由连续n个字符c组成的串 |
string的操作 |
|
os << s |
将s写到输出流os当中,返回os |
is >> s |
从is中读取字符串赋给s,字符串以空白字符分割,返回s |
getline(is, s) |
从is中读取一行赋给s,返回is |
s.empty() |
s为空返回true,否则返回false |
s.size() |
返回s中字符的个数 |
s.c_str() |
返回s的C风格字符串形式,类型为const char * |
s[n] |
返回s中第n个字符的引用,位置n从0开始计 |
s1 + s2 |
返回s1和s2连接后的结果 |
s1 = s2 |
用s2的副本替代s1中原来的字符 |
s1 == s2 |
如果s1和s2包含的字符完全一样返回true,否则返回false,对字母大小写敏感(s1 != s2则相反) |
s1 != s2 |
|
<,<=,>,>= |
利用字符在字典中的顺序进行比较,对字母大小写敏感 |
string::size_type类型
sting的size操作返回类型,系与机器无关的特性,是一个无符号类型。
cctype头文件中的函数
isalnum(c) |
当c是字母或数字时为真 |
isalpha(c) |
当c是字母时为真 |
iscntrl(c) |
当c是控制字符时为真 |
isdigit(c) |
当c是数字时为真 |
isgraph(c) |
当c不是空格但可打印时为真 |
islower(c) |
当c是小写字母时为真 |
isprint(c) |
当c是可打印字符时为真(即c是空格或具有可视形式) |
ispunct(c) |
当c是标点符号时为真 |
isspace(c) |
当c是空白时为真(含空格、制表符、回车符、进纸符、换行符) |
isupper(c) |
当c是大写字母时为真 |
isxdigit(c) |
当c是十六进制数字时为真 |
tolower(c) |
如果c是大写字母,输出对应的小写字母,否则原样输出c |
toupper(c) |
如果c是小写字母,输出对应的大写字母,否则原样输出c |
范围for语句
范围for语句遍历给定序列中的每个元素并对元素执行某种操作,其形式如下:
for (declaration : expression)
statement
其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。为了便于表示expression中元素的类型,一般用auto来定义变量。当expression中的元素可更改且需要更改,则可以用auto引用定义变量,如:
for (auto &e : vec)
范围for语句体内不应改变序列的大小。
当用于多维数组时,由于多维数组会自动把每个第一维上的元素也转换为指针,为遍历多维数组的每个元素,需要用引用类型,以引用子数组类型,如有int ia[3][4],则要遍历每个元素应如下使用:
for(auto &row : ia) // 此处row分别系ia[0]...ia[2]数组的引用
for(auto col : row)
……
vector的初始化和提供的操作
初始化vector对象的方式 |
|
vector<T> v1 |
v1是一个空vector,元素类型是T,执行默认初始化 |
vector<T> v2(v1) |
v2中包含有v1所有元素的副本 |
vector<T> v2 = v1 |
等价于v2(v1),v2中包含有v1所有元素的副本 |
vector<T> v3(n, val) |
v3包含了n个重复的元素,每个元素的值都是val |
vector<T> v4(n) |
v4包含了n个重复地执行了值初始化的对象 |
vector<T> v5{a, b, c...} |
*v5包含了初始值个数的元素,每个元素被赋予相应初始值 |
vector<T> v5 = {a, b, c...} |
*等价于v5{a, b, c...} |
vector支持的操作 |
|
v.empty() |
如果v中无元素,返回真,否则返回假 |
v.size() |
返回v中元素的个数 |
v.push_back(t) |
向v的尾端添加一个值为t的元素 |
v[n] |
返回v中第n个位置上元素的引用 |
v1 = v2 |
用v2中元素的拷贝替换v1中的元素 |
v1 = {a, b, c...} |
用列表中元素的拷贝替换v1中的元素 |
v1 == v2 |
v1和v2相等且当元素数量相同且对应位置的元素值相同返回真,否则返回假(v1 != v2则相反),需要元素支持这种比较 |
v1 != v2 |
|
<,<=,>,>= |
以字典顺序进行比较,需要元素支持这种比较 |
*在用列表初始化vector对象中,如果列表中的元素无法转换成vector中元素的类型,则要考虑含义转变,如vector<string> v{10},此时10不能转换成string,因此这里将把v初始化为含有10个元素,每个元素执行默认初始化——空串,再如vector<string> v1{5, "hi"},此处5无法转换成string,会把v1初始化为含有5个元素,每个元素均为"hi"的string。
迭代器
迭代器对所有标准库容器都适用,如string、vector、list等等。迭代器可以解引用,返回容器中对应位置元素的引用。有迭代器的类型都提供了返回迭代器的成员,如begin和end成员,其中begin负责返回指向第一个元素的迭代器,end成员负责返回指向容器(或string对象)尾元素下一位置的迭代器,称为尾后迭代器。迭代器类似于指针的使用。
标准容器迭代器的运算符 |
|
*iter |
返回迭代器iter所指元素的引用 |
iter->mem |
解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter |
令iter指示容器的下一个元素 |
--iter |
令iter指示容器的上一个元素 |
iter1 == iter2 |
判断两个迭代器是否相等(不相等),如果两个迭代器指示是同一个元素或者它们是同一个容器的尾后迭代器,则相等,反之,不相等 |
iter1 != iter2 |
当容器中的元素系常量时,begin和end成员返回的迭代器类型为该容器中定义的类型const_iterator,若元素非常量,则返回该容器定义的类型iterator。如果,我们只想对元素进行读操作,即只要使用const_iterator类型迭代器即可,此时可以用容器中的cbegin和cend成员,这两个成员无论元素是否系常量类型,均返回const_iterator类型。
迭代器运算
所有的标准库容器均支持迭代器的递增运算以及用==和!=对两个有效迭代器的比较。string和vector的迭代器提供更多额外的运算符。
vecotr和string迭代器支持的运算 |
|
iter + n |
迭代器加上一个整数,根据实际执行的加减决定迭代器向前或向后移动abs(n)个位置,仍旧得到一个迭代器 |
iter - n |
|
iter += n |
类似于iter = iter + n |
iter -= n |
类似于iter = iter - n |
iter1 - iter2 |
返回指向同一容器的两个迭代器的距离,类型difference_type,有正负 |
>,>=,<,<= |
根据迭代器指向的容器位置先后来判断关系式是否成立 |
数组
数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。
(1)标准库begin和end函数。如有int ia[10],我们用标准库函数begin(ia)可得到数组ia首元素的指针,而end(ia)则可以得到数组ia尾元素的指针。
(2)两个指针相减的结果类型为ptrdiff_t类型(在cstddef头文件中定义)。
(3)不存在元素类型为引用的数组,但存在数组的引用,但元素类型和数量必须相同,如:
int& ia1[10]; // 错误:不存在元素引用的数组
int ia[10];
int (&ia2)[10] = ia; // 正确:ia2是数组ia的引用
多维数组的元素遍历使用范围for语句时,注意事项见知识点范围for语句。
在使用数组下标的时候,通常将其定义为size_t类型,是一种机器相关的无符号类型,该类型定义在头文件cstddef中。
一般使用数组名时,仅仅是该数组的首元素的指针,但使用decltype则表示该数组名的数组类型,如:
int ia[] = { 1, 2, 3 };
auto ia2(ia); // ia2是一个整型指针(int *),指向ia的第一个元素
//正确:ia3与ia数组类型一样,包含3个元素
decltype(ia) ia3 = { 7, 8, 9 };
在C++11中,可以用begin和end函数分别得到一个数组的首元素地址和末元素下一个地址。如begin(ia)即为&ia[0],end(ia)即为&ia[4]。
两个指针如果指向同一个数组,则可以进行相减运算,其类型为定义在cstddef中的ptrdiff_t类型。
表达式
sizeof运算符
(1)解引用指针执行sizeof运算得到指针指向对象所占空间大小,指针无需有效。
(2)对数组执行sizeof运算得到整个数组所占空间大小,不会把数组名转换为指针。
(3)对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
运算符优先级表
运算符优先级 |
结合律和运算符 功能 用法 参考页码 |
左 :: 全局作用域 ::name 25 左 :: 类作用域 class::name 79 左 :: 命名空间作用域 namespace::name 74 |
左 . 成员选择 object.member 20 左 -> 成员选择 pointer->member 98 左 [] 下标 expr[expr] 104 左 () 函数调用 name(expr_list) 120 左 () 类型构造 type(expr_list) 145 |
右 ++ 后置递增运算 lvalue++ 131 右 -- 后置递减运算 lvalue-- 131 右 typeid 类型ID typeid(type) 731 右 typeid 运行时类型ID typeid(expr) 731 右 explicit cast 类型转换 cast_name<type>(expr) 144 |
右 ++ 前置递增运算 ++lvalue 131 右 -- 前置递减运算 --lvalue 131 右 ~ 位求反 ~expr 136 右 ! 逻辑非 !expr 126 右 - 一元负号 -expr 124 右 + 一元正号 +expr 124 右 * 解引用 *expr 48 右 & 取地址 &expr 47 右 () 类型转换 (type)expr 145 右 sizeof 对象的大小 sizeof expr 139 右 sizeof 类型的大小 sizeof(type) 139 右 sizeof... 参数包的大小 sizeof...(name) 619 右 new 创建对象 new type 407 右 new[] 创建数组 new type[size] 407 右 delete 释放对象 delete expr 409 右 delete[] 释放数组 delete [] expr 409 右 noexcept 能否抛出异常 noexcept(expr) 690 |
左 ->* 指向成员选择的指针 ptr->*ptr_to_member 740 左 .* 指向成员选择的指针 obj.*ptr_to_member 740 |
左 * 乘法 expr * expr 12 左 / 除法 expr / expr 124 左 % 取模(取余) expr % expr 124 |
左 + 加法 expr + expr 124 左 - 减法 expr - expr 124 |
左 << 向左移位 expr << expr 136 左 >> 向右移位 expr >> expr 136 |
左 < 小于 expr < expr 126 左 <= 小于等于 expr <= expr 126 左 > 大于 expr > expr 126 左 >= 大于等于 expr >= expr 126 |
左 == 相等 expr == expr 126 左 != 不相等 expr != expr 126 |
左 & 位与 expr & expr 136 |
左 ^ 位异或 expr ^ expr 136 |
左 | 位或 expr | expr 136 |
左 && 逻辑与 expr && expr 126 |
左 || 逻辑或 expr || expr 126 |
右 ? : 条件 expr ? expr : expr 134 |
右 = 赋值 lvalue = expr 129 |
右 *=, /=, %=, +=, -=, 复合赋值 lvalue += expr等 129 <<=, >>=, &=, |=, ^= |
右 throw 抛出异常 throw expr 173 |
左 , 逗号 expr, expr 140 |
显式强制转换
static_cast:任何具有明确定义的类型转换,只要不包含底层const(此时只能用const_cast转换),都可以使用static_cast进行转换。这种转换是一种标准转换,只能执行普通转换。
const_cast:只能改变运算对象的const,既可将const对象改变为非const,也可将非const对象改变为const。
reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释,如将int转换为指针类型等,或者将int指针转换为char指针等。
dynamic_cast:运行时类型识别,只用于具有类继承关系的引用和指针上,即父类指针(或引用)转换为子类指针(或引用)。
表达式
函数
含有可变形参的函数
(1)如果形参类型相同,我们可以使用initializer_list类型的形参,其提供的操作如下:
initializer_list提供的操作 |
|
initializer_list<T> lst |
默认初始化,T类型元素的空列表 |
initializer_list<T> lst{a, b, c...} |
lst的元素数量和初始值相同,lst的元素是对应初始值的副本,列表中的元素是const |
lst2(lst) |
拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素 |
lst2 = lst |
|
lst.size() |
列表中的元素数量 |
lst.begin() |
返回指向lst中首元素的指针 |
lst.end() |
返回指向lst中尾元素下一位置的指针 |
如有void fcn(initializer_list<string> ilstr),则调用时可如下形式调用:
fcn({"hello", "name", "aka"});
(2)形参类型不同,可以使用省略符形参,或者可变参数模板(P618)。
列表初始化返回值
C++11标准允许返回花括号包围的值的列表。如有
vector<int> func()
{ return {1, 2, 3}; }
对复杂返回类型的简化使用方式
使用尾置返回类型:即形如auto func() -> int (*) [10],此函数即表明返回类型是指向含有10个整数的数组的指针,其函数用auto作为前导。
使用decltype:如果我们知道返回类型与某个变量或者数组或者函数相同,则可如下使用:
int ia[10];
// 表示返回的是一种指针,指针指向含有5个整数的数组
decltype(ia)* fcn();
void p();
// 表示返回的是一种函数指针,函数类型与函数p相同
decltype(p)* fcn1();
默认实参
(1)默认实参只能是形参列表从最后开始定义或声明,也就是定义或声明了某个形参的默认实参,则该形参后面的形参(若有的话)都应该定义或声明默认实参。在调用时,不能仅仅省略前面的实参,而只提供后面的实参。
(2)在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
内联函数和constexpr函数
constexpr函数是指能用于常量表达式的函数。constexpr函数的形参必须都是字面值类型,而且函数体内必须有且只有一条return语句,其返回值必须是字面值类型。constexpr函数可以用于初始化constexpr类型的变量。我们允许constexpr函数的返回值并非一个常量:
// 若cnt是常量表达式,则scale(cnt)也是常量表达式
constexpr size_t scale(size_t cnt) { return 3 * cnt; }
因此,可以如此定义数组:int a[scale(2)],但却不能以下定义:int i = 2; int b[scale(i)]。
(2)内联函数和constexpr函数应该放在头文件内。因为内联函数和constexpr函数在单独编译单元内必须可见,而不仅仅是其声明,需要函数的完整定义。也正是因为如此,内联函数和constexpr函数可以在程序中多次定义。
调试帮助
(1)assert宏。定义在cassert头文件中,用来表明不可能发生的情况,其使用形式为assert(expr),若expr为假,则assert输出信息并终止程序的执行,若expr为真,则它什么也不做。
(2)NDEBUG预处理变量。assert宏依赖于该变量。如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG。C++编译器定义的部分预处理器变量:
__func__,编译器定义的一个局部静态变量,用于存放函数的名字。
__FILE__,存放文件名的字符串字面值。
__LINE__,存放当前行号的整型字面值。
__TIME__,存放文件编译时间的字符串字面值。
__DATE__,存放文件编译日期的字符串字面值。
7.函数重载
(1)定义重载函数。
重载函数必须是函数名字相同,但形参列表不同(包括类型和数量),返回值类型则无所谓。在形参列表中,需注意以下方面:
a.顶层const不影响形参列表,如int func(int)与int func(const int)无法重载,因为此时const是顶层的,即是值传递的,不影响实参。但如果形参是某种类型的指针或者引用,则通过区分是否是常量对象可以实现函数重载,此时的const是底层的。
b.类型别名不影响形参列表。
c.若两个函数中,其中一个函数的形参列表是另一个函数的形参列表的子集,且较大形参列表剩余形参具有默认实参,则也不影响形参类型的判断。如有int func(int)和void func(int, char ch = 'a'),此时虽然两个函数形参列表不同,但是无法重载。
d.类内成员函数,可以根据是否是const成员函数进行重载。
重载与作用域。
若在一个内层作用域存在某个名字的函数声明,则该声明将屏蔽外层作用域中的同名函数,因此这样的情况下,就无法形成真正的重载。因此,我们要能使用重载,就需要把重载函数在同一层作用域中声明(往往是全局作用域)。
const_cast和重载。
const_cast在重载函数的情景中最有用。例如:
// 比较两个string对象的长度,返回较短的那个引用
const string& shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
如果,我们需要当实参不是const引用时,返回得到不是const的引用,我们可以如下:
string& shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
函数匹配。
第一步:选定候选函数,即与被调用的函数同名,且在调用点可见。
第二步:从候选函数中选出可行函数,即形参数量与调用提供的实参数量相等(如果函数具有默认实参,则传入的实参数量可少于实际使用的实参数量),每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
第三步:从可行函数中寻找最佳匹配。最佳匹配包含以下几个等级,具体排序如下:
精确匹配。包含以下情况,其一,实参类型和形参类型相同;其二,实参从数组类型或函数类型转换成对应的指针类型,其三,向实参添加顶层const或者从实参中删除顶层const。
通过const转换实现的匹配(P143)。
通过类型提升实现的匹配。
通过算术类型转换或指针转换实现的匹配。
通过类类型转换实现的匹配。
类
成员函数
(1)定义在类内部的函数是隐式的inline函数。
(2)每个成员函数都有一个额外的隐式this形参,来表示当前调用它的那个对象,可以this->member这样的形式调用其他成员。
(3)const成员函数。默认情况下,this的类型是指向类类型的非常量版本的常量指针,因此默认情况下我们不能把this绑定到一个常量对象上,也就意味着我们不能在一个常量对象上调用普通的成员函数。因此,当我们需要指出this是指向常量的指针时,就需要在成员函数的形参列表后面添加const关键字。const成员函数指出当前的隐式this是指向常量对象的指针,不会修改对象。常量对象以及常量对象的引用或指针都只能调用常量成员函数。
当在类的外部定义常量成员函数时,类内的成员函数声明和外部定义都必须在形参列表后明确指定const属性。
如果一个const成员函数需要返回调用它的对象的引用,则也必须是const引用。
通过区分成员函数是否是const函数,也可以进行重载。const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
(4)与头文件中定义inline函数一样,对inline成员函数也应该与相应的类定义在同一个头文件中。
类内成员初始值
C++11标准规定,可以为类内数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。对类内初始值需要放在花括号内或者=号右边,不能使用圆括号。
struct D{
int i = 0;
int v{2};
double d (3.14); // 错误:不能使用圆括号
double e;
};
3.构造函数
(1)构造函数是一种特殊的成员函数,无返回类型,函数名与类名相同。
默认构造函数。只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内初始值时,这个类才适合于使用合成的默认构造函数。另外,如果类中包含一个其他类类型的成员,且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
default构造函数。当我们需要指出要求编译器生成构造函数时,可以在构造函数的形参列表后写上= default。default构造函数既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果= default在类的内部,则默认构造函数是内联的,否则系非内联的。
初始化方式,一般建议使用初始化列表形式,例如:
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0) {}
如果是在构造函数函数体内进行赋值,一则降低效率,二则对于某些数据成员类型来说会失败,例如类中有引用类型的数据成员,此时必须进行初始化列表形式进行初始化。因为,通过函数体内赋值方式实际上在函数体执行之前先是默认初始化,再进行赋值,但对于引用(或const或未提供默认构造函数的类类型)类型来说,必须有初始值,而不能默认,且一旦初始化就不能再赋值。
对成员的初始化顺序是以成员在类内的定义先后顺序进行初始化的,而跟初始化列表中的顺序无关。
委托构造函数。即一个类的某个构造函数的初始化列表中使用该类的其他构造函数帮助完成初始化,如:
struct Demo {
Demo(int i): v(i) {}
Demo(): Demo(1) {} // Demo()委托Demo(int)构造函数完成初始化
int v;
};
如果一个类定义了接受一个形参的构造函数,则也是定义了一条从该形参类型向类类型隐式转换的规则。但是,这种转换只能自动执行一步,除非是显式转换(P264)。如果我们要去除这种隐式的转换,可以在该构造函数前添加explicit关键字,这样就不能自动把该形参类型隐式转换为类类型,如要使用转换则必须用显式方式。也正是如此,我们只能把explicit构造函数用于直接初始化,而不能用于拷贝形式初始化(即使用=形式)。显式的方式有两种,一种是使用构造函数形式,另一种是使用static_cast形式。explicit只能在类内部使用,当在类外部定义单形参的构造函数时,不应再使用explicit关键字。
定义一个返回this对象的函数。当一个成员函数需要返回自身对象的引用时,在return语句中,使用return *this;这样的形式。
4.拷贝、赋值和析构
当我们使用了赋值运算符时会发生对象的赋值。如果我们不主动定义这些操作,则编译器将替我们合成它们。
访问控制与封装
定义在public说明符之后的成员在整个程序内可被访问,public成员定义了类的接口。
定义在protected说明符之后的成员,只能被类自身或子类的成员访问。
定义在private说明符之后的成员,只能被类对象自身访问。
(2)class与struct。两者只是默认的访问级别不同,前者默认为private,而后者默认为public,其他没有任何区别。
6.可变数据成员
当我们需要修改一个const对象的数据成员或者在const成员函数中能对一个数据成员进行修改,可以在定义类的时候,在相应的数据成员变量声明前加上mutable,如:
class Demo{
......
mutable int i;
}
此时,即使创建一个const Demo对象或者调用Demo对象中的const成员函数也能对i进行修改。
7.类的声明
当一个类只是被声明,而没有被定义时,这种类是一种不完全类型。由于编译器尚无法确定这个类的相关成员和空间大小,因此其使用非常有限,不能定义这种类的变量,只能定义指向这种类的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
只有当类被完全定义时,才能定义这种类的数据成员,因此一个类不能包含自身的类对象成员,例外情况是当这个成员是static静态成员时,可以定义。
class Demo{
......
static Demo d; // 正确
Demo e; // 错误
Demo &f; // 正确
Demo *g; // 正确
};
8.友元
(1)友元通过friend来声明,表示允许其他类或者函数访问它的非公有成员。友元声明仅仅指出了访问的权限,因此想让类的用户也能使用某个友元函数,则必须在友元声明之外再专门对函数进行一次声明。
(2)友元声明只能出现在类定义的内部,但是在类内的具体位置不限,同时也不受它所在区域访问控制级别的约束。
(3)友元关系不能传递,也不能被继承。
(4)由于类在被完全定义之前,只能定义类的指针或引用,因此,当一个类声明某个类的成员函数作为友元函数时需要注意以下顺序(以类A声明类B的成员函数func为例):
a.首先定义B类,其中声明func函数,但不能定义它。在func使用A的成员之前必须先声明A。
b.定义A,包括对func的友元声明。
c.最后定义func,此时它才可以使用A中的成员(必须包括相应头文件,或者在A的定义后面接着定义func)。
友元声明和作用域。尽管在一个类中可以直接声明一个函数为友元,但是该类的其他成员函数并不能因此直接可以调用该友元函数,而必须如同普通的作用域规则一样进行声明后方可使用。
函数重载和友元。尽管重载函数的名字相同,但它们任然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
9.名字查找与类的作用域
(1)对于定义在类内部的成员函数来说,解析其中名字的方式与普通的名字查找有所不同。类的定义分两步处理:其一,编译成员的声明;其二,直到类全部可见后才编译函数体。
(2)如果外层作用域已经定义了一个类型别名,且一个类中的成员也已经使用了外层作用域中的这个别名,则这个类不能在之后重新定义该别名。如:
typedef int INT;
class Demo {
public:
INT func() { return value; } // 使用外层作用域的INT
private:
typedef unsigned INT; // 错误:不能重新定义INT
INT value;
};
因此,一般情况下,我们在类中定义别名,应当把所有别名都定义在类的开始处,这样能确保所有使用该类型的成员都出现在类名的定义之后。
10.聚合类
即所有成员都是public的,没有定义任何构造函数,也没有类内初始值,也没有基类,也没有virtual函数,但是可以有普通的成员函数。对聚合类的对象进行初始化,可以通过初始值列表形式,即形如Demo d = {...},顺序应当与相关成员的声明顺序一致。
11.字面值常量类
(1)字面值常量类有两种情况:其一,所有数据成员都是字面值类型的聚合类;其二,如果一个类不是聚合类,但符合以下条件也是字面值常量类:
数据成员都是字面值类型。
类必须至少含有一个constexpr构造函数。
如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
类必须使用默认的析构函数,负责销毁类的对象。
(2)constexpr构造函数可以声明成=default的形式或是删除函数的形式(=delete),否则constexpr构造函数的函数体必须为空。constexpr构造函数是隐式const的(但普通类的构造函数不能定义为const)。
constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。
12.类的静态成员
(1)关键字static只在类内使用,在类外部定义静态成员时,不应再指明static。
(2)静态成员函数不能声明成const,也不能在静态成员函数体内使用this指针。
(3)访问静态成员既可以通过类作用域访问,也可以通过该类的某个对象及其引用或指针去调用。
(4)定义静态数据成员时,一般不应在类内指定初始值,而应在类外部定义,且从保证只定义一次的原则出发,不应该定义在头文件中,而应该与其他成员函数一样定义在普通的文件中。
如果需要在类内为一个静态数据成员提供初始值,则初始值必须是const整数类型,且该静态成员必须是字面值类型的constexpr,且在类的外部也定义一次(但不再提供初始值),但不应提供初始值(另外仍旧需添加constexpr关键字)。
静态数据成员可以是不完全类型(即可以声明为自身类的变量),且可以作为默认实参,而普通数据成员则不可以。
IO库
IO类
(1)IO库类型和头文件
IO库类型和头文件 |
头文件 类型 iostream istream,wistream从流读取数据 ostream,wostream向流写入数据 iostream,wiostream读写流 fstream ifstream,wifstream从文件读取数据 ofstream,wofstream向文件写入数据 fstream,wfstream读写文件 sstream istringstream,wistringstream从string读取数据 ostringstream,wostringstream向string写入数据 stringstream,wstringstream读写string |
(2)IO对象无拷贝或赋值。因此,有IO方面的函数形参需要定义为引用类型。
(3)IO库的条件状态和操作
IO库条件状态 |
|
strm::iostate |
strm是一种IO类型,iostate是一种机器相关的类型,提供了表达条件状态的完整功能 |
strm::badbit |
strm::badbit用来指出流已崩溃 |
strm::failbit |
strm::failbit用来指出一个IO操作失败了 |
strm::eofbit |
用来指出流到达了文件结束 |
strm::goodbig |
用来指出流未处于错误状态。此值保证为0 |
IO库条件状态操作 |
|
s.eof() |
若流s的eofbit置位,则返回true |
s.fail() |
若流s的failbit或badbit置位,则返回true |
s.bad() |
若流s的badbit置位,则返回true |
s.good() |
若流s处于有效状态,则返回true |
s.clear() |
将流s中所有条件状态位复位,将流的状态设置为有效。返回void |
s.clear(flags) |
根据给定的falgs标志位,将流s中对应条件状态位复位。flags的类型为strm::iostate。返回void |
s.setstate(flags) |
根据给定的falgs标志位,将流s中对应条件状态位置位。flags的类型为strm::iostate。返回void |
s.rdstate() |
返回流s的当前条件状态,返回值类型为strm::iostate |
(4)管理缓冲区。每个输出流都管理一个缓冲区,用来保存程序读写的数据,只有缓冲区刷新,才能将数据真正送出到设备上。要使缓冲区刷新,有以下情形:
a.程序正常结束,如作为main函数的return操作。
b.缓冲区满时,需要刷新缓冲,而后的数据才能写入缓冲区。
c.使用诸如endl等操纵符来显式刷新缓冲区。另外还有flush和ends也会刷新缓冲区。其中,endl为流完成换行,flush仅仅只是刷新,而ends则向缓冲区插入一个空字符。
d.用操纵符unitbuf设置流的内部状态,这样所有输出操作后都会立即刷新缓冲区,如:
cout << unitbuf; // 任何输出都立即刷新,无缓冲
cout << nounitbuf; // 回到正常的缓冲方式
e.关联输入和输出流。当一个输入流被关联到一个输出流时,任何从输入流读取数据的操作都会先刷新关联的输出流。标准库将cin和cout关联在一起。
关联的操作使用tie成员函数。tie有两个重载版本:一个无参数,调用会返回本对象当前关联的输出流指针,若对象未关联到流,则返回空指针;tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream,即形如is.tie(&os)。另外,也可以将一个os关联到另外一个os。每个流同时最多关联到一个流,但多个流可以同时关联到一个os。
文件输入输出
fstream特有的操作 |
|
fstream fstrm |
创建一个未绑定的文件流。fstream是头文件fstream中定义的一个类型 |
fstream fstrm(s) |
创建一个fstream,并打开名为s的文件。s可以是string或C风格字符串指针。这些构造函数都是explicit的。默认的文件模式mode依赖于fstream的具体类型 |
fstream fstrm(s, mode) |
与前一个构造函数类似,但按指定mode打开文件 |
fstrm.open(s) |
打开名为s的文件,并将文件与fstrm绑定。s可以是string或C风格字符串指针。默认的文件mode依赖于fstream类型,返回void |
fstrm.close() |
关闭与fstrm绑定的文件。返回void |
fstrm.is_open() |
返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭 |
当一个fstream对象被销毁时,close会自动调用。当一个fstream对象调用open失败时,会将该fstream对象的failbit置位。
文件模式
文件模式 |
|
in |
以读方式打开,只适用于ofstream或fstream对象 |
out |
以写方式打开,只适用于ifstream或fstream对象 |
app |
每次写操作前均定位到文件尾 |
ate |
打开文件后立即定位到文件尾 |
trunc |
截断文件 |
binary |
以二进制方式进行IO |
(1)只有当out也被设定时,才可设定trunc模式。
(2)只要trunc没被设定,就可以设定app模式。在app模式下,即使没有显式指定out模式,文件也总是以输出方式被打开。
(3)默认情况下,即使没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指针in模式,即打开文件同时进行读写操作。如:
ofstream file("file1", ofstream::out | ofstream::app);
(4)ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
(5)与ifstream关联的文件默认以in模式打开;与ofstream关联的文件默认以out模式打开;与fstream关联的文件默认以in和out模式打开。
string流
stringstream特有的操作 |
|
sstream strm |
strm是一个未绑定的stringstream对象。sstream是头文件sstream中定义的一个类型 |
sstream strm(s) |
strm是一个sstream对象,保存string s的一个拷贝。此构造函数是explicit的 |
strm.str() |
返回strm所保存的string的拷贝 |
strm.str(s) |
将string s拷贝到strm中。返回void |
顺序容器
C++11新标准中的新容器
forward_list:类似单向链表的性能,没有size操作。其iterator不支持--操作。
array:是一种更安全、更容易使用的数组类型,具有固定大小。
定义方式:array<int, 5> ai;表示ai容器具有5个元素。定义后不能增减容器大小。
一般来说,每种容器都定义在与容器名同名的头文件中。
2.容器赋值和swap
容器赋值运算 |
|
c1 = c2 |
将c1中的元素替换为c2中元素的拷贝。c1和c2必须有相同的类型。对于array还必须有相同的大小。 |
C c{a, b, c...} |
c初始化为初始化列表中元素的拷贝。列表中元素的类型必须与c的元素类型相容。对于array类型,列表中元素数目必须等于或者小于array的大小,任何遗漏的元素都进行值的初始化。 |
C C = {a, b, c...} |
|
c = {a, b, c...} |
将c中元素替换为初始化列表中元素的拷贝(array不适用,部分编译器可行,如g++) |
swap(c1, c2) |
交换c1和c2中的元素,两者需为相同类型。swap通常比从c2向c1拷贝元素快得多 |
c1.swap(c2) |
|
assign操作不适用于关联容器和array |
|
seq.assign(b, e) |
将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素 |
seq.assign(il) |
将seq中的元素替换为初始化列表il中的元素 |
seq.assign(n, t) |
将seq中的元素替换为n个值为t的元素 |
赋值运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)
3.容器大小操作
(1)除了forward_list容器不支持size操作外,顺序容器均支持size、empty、max_size操作。
(2)容器大小比较要求两个容器类型一致,包括元素类型也一致。两个容器大小比较依赖于元素类型是否定义了相关的关系运算符,若无定义,则不能进行比较。
4.关系运算符
容器的关系运算符使用元素的关系运算符完成比较。因此,只有当容器的元素也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器。
5.向顺序容器添加元素
除array外,其他顺序容器均支持以下操作。其中,forward_list有自己专有版本的insert和emplace,同时其不支持push_back和emplace_back。vector和string不支持push_front和emplace_front。
向顺序容器添加元素的操作 |
|
这些操作会改变容器的大小;array不支持这些操作。 forward_list有自己专有版本的insert和emplace; forward_list不支持push_back和emplace_back。 vector和string不支持push_front和emplace_front。 |
|
c.push_back(t) |
在c的尾部创建一个值为t或由args创建的元素,返回void。 |
emplace_back(args) |
|
c.push_front(t) |
在c的头部创建一个值为t或由args创建的元素,返回void。 |
emplace_front(args) |
|
c.insert(p, t) |
在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加的元素的迭代器。 |
c.emplace(p, args) |
|
c.insert(p, n, t) |
在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素的迭代器;若n为0,则返回p。 |
c.insert(p, b, e) |
将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素。返回指向新添加的第一个元素的迭代器;若范围为空,则返回p。 |
c.insert(p, il) |
il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前,返回指向新添加的第一个元素的迭代器;若列表为空,则返回p。 |
向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效。 |
6.访问元素
在顺序容器中访问元素的操作 |
|
at和下标操作只适用于string、vector、deque和array。 back不适用于forward_list。 |
|
c.back() |
返回c中尾元素的引用。若c为空,行为未定义。 |
c.front() |
返回c中首元素的引用。若c为空,行为未定义。 |
c[n] |
返回c中下标为n的元素的引用,n是一个无符号整数。若n>=c.size(),则行为未定义。 |
c.at(n) |
返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常。 |
7.删除元素
顺序容器的删除操作 |
|
这些操作会改变容器的大小,因此不适用于array。 forward_list有特殊版本的erase,且不支持pop_back;vector和string不支持pop_front。 |
|
c.pop_back() |
删除c中尾元素。若c为空,则函数行为未定义。返回void。 |
c.pop_front() |
删除c中首元素。若c为空,函数行为未定义。返回void。 |
c.erase(p) |
删除迭代器p所指定的元素,返回一个指向被删除元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数行为未定义。 |
c.clear() |
删除c中所有元素,返回void。 |
删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。 |
8.forward_list的插入、删除操作
在forward_list中插入或删除元素的操作 |
|
lst.before_begin() |
返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。cbefore_begin()返回一个const_iterator。 |
lst.cbefore_begin() |
|
lst.insert_after(p, t) |
在迭代器p之后的位置插入元素。t是一个对象,n是数量,b和e是表示范围的一对迭代器(b和e不能指向lst内),il是一个花括号列表。返回指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义 |
lst.insert_after(p, n, t) |
|
lst.insert_after(p, b, e) |
|
lst.insert_after(p, il) |
|
emplace_after(p, args) |
使用args在p指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义 |
lst.erase_after(p) |
删除p指向的位置之后的元素,或删除从b之后直到(但不包含)e之间的元素。返回一个指向被删除元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p指向lst的尾元素或者是一个尾后迭代器,则函数行为未定义 |
lst.erase_after(b, e) |
9.改变容器大小
顺序容器大小操作 |
|
resize不适用于array |
|
c.resize(n) |
调整c的大小为n个元素,若n < c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化 |
c.resize(n, t) |
调整c的大小为n个元素。任何新添加的元素都初始化为值t |
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效。 |
10.管理容量的成员函数
容器大小管理操作 |
|
shrink_to_fit只适用于vector、string和deque。 capacity和reserve只适用于vector和string。 |
|
c.shrink_to_fit() |
将capacity()减少为与size()相同大小 |
c.capacity() |
不重新分配内存空间的话,c可以保存多少元素 |
c.reserve(n) |
分配至少能容纳n个元素的空间 |
11.额外的string操作
构造string的其他方法 |
|||
n、len2和pos2都是无符号值 |
|||
string s(cp, n) |
s是cp指向的数组中前n个字符的拷贝。此数组至少应该包含n个字符 |
||
string s(s2, pos2) |
s是string s2从下标pos2开始的字符的拷贝。若pos2 > s2.size(),构造函数的行为未定义 |
||
string s(s2, pos2, len2) |
s是string s2从下标pos2开始len2个字符的拷贝。若pos2 > s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size() - pos2个字符 |
||
子字符串操作 |
|||
s.substr(pos, n) |
返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0。n的默认值为s.size() - pos,即拷贝从pos开始的所有字符 |
||
修改string的操作 |
|||
s.insert(pos, args) |
在pos之前插入args指定的字符。pos可以是一个下标或一个迭代器。接受下标的版本返回一个指向s的引用;接受迭代器的版本返回指向第一个插入字符的迭代器 |
||
s.erase(pos, len) |
删除从位置pos开始的len个字符。如果len被省略,则删除从pos开始直至s末尾的所有字符。返回一个指向s的引用 |
||
s.assign(args) |
将s中的字符替换为args指定的字符。返回一个指向s的引用 |
||
s.append(args) |
将args追加到s。返回一个指向s的引用 |
||
s.replace(range, args) |
删除s中范围range内的字符,替换为args指定的字符。range或者是一个下标和一个长度,或者是一对指向s的迭代器。返回一个指向s的引用 |
||
args可以是下列形式之一;append和assign可以使用所有形式。 str不能与s相同,迭代器b和e不能指向s。 |
|||
str |
字符串str |
||
str, pos, len |
str中从pos开始最多len个字符 |
||
cp, len |
从cp指向的字符数组的前(最多)len个字符 |
||
cp |
cp指向的以空字符结尾的字符数组 |
||
n, c |
n个字符c |
||
b, e |
迭代器b和e指定的范围内的字符 |
||
初始化列表 |
花括号包围的,以逗号分隔的字符列表 |
||
replace和insert所允许的args形式依赖于range和pos是如何指定的: replace replace insert insert args可以是 (pos, len, args) (b, e, args) (pos, args) (iter, args) 是 是 是 否 str 是 否 是 否 str, pos, len 是 是 是 否 cp, len 是 是 否 否 cp 是 是 是 是 n, c 否 是 否 是 b2, e2 否 是 否 是 初始化列表 |
|||
string的搜索操作 |
|||
搜索操作返回制定字符出现的下标,如果未找到则返回string::npos。 |
|||
s.find(args) |
查找s中args第一次出现的位置 |
||
s.rfind(args) |
查找s中args最后一次出现的位置 |
||
s.find_first_of(args) |
在s中查找args中任何一个字符第一次出现的位置 |
||
s.find_last_of(args) |
在s中查找args中任何一个字符最后一次出现的位置 |
||
s.find_first_not_of(args) |
在s中查找第一个不在args中的字符 |
||
s.find_last_not_of(args) |
在s中查找最后一个不在args中的字符 |
||
args必须是以下形式之一 |
|||
c, pos |
从s中位置pos开始查找字符c。pos默认为0 |
||
s2, pos |
从s中位置pos开始查找字符串s2。pos默认为0 |
||
cp, pos |
从s中位置pos开始查找指针cp指向的以空字符结尾的C风格字符串。pos默认为0 |
||
cp, pos, n |
从s中位置pos开始查找指针cp指向的数组的前n个字符。pos和n无默认值 |
||
string的比较操作s.compare(args)的几种参数形式 |
|||
比较操作根据s是等于、大于还是小于指定的字符串,返回0、正数或负数 |
|||
s2 |
比较s和s2 |
||
pos1, n1, s2 |
将s中从pos1开始的n1个字符与s2比较 |
||
pos1, n1, s2, pos2, n2 |
将s中从pos1开始的n1个字符与s2中从pos2开始的n2个字符进行比较 |
||
cp |
比较s与cp指向的以空字符结尾的字符数组 |
||
pos1, n1, cp |
将s中从pos1开始的n1个字符与cp指向的以空字符结尾的字符数组进行比较 |
||
pos1, n1, cp, n2 |
将s中从pos1开始的n1个字符与cp指向的地址开始的n2个字符进行比较 |
||
string和数值之间的转换 |
|||
如果string不能转换为一个数值,这些函数抛出一个invalid_argument异常。如果转换得到的数值无法用任何类型来表示,则抛出一个out_of_range异常。 |
|||
to_string(val) |
组重载函数,返回数值val的string表示。val可以是任何算术类型。对每个浮点类型和int或更大的整型,都有相应版本的to_string。与往常一样,小整型会被提升 |
||
stoi(s, p, b) |
返回s的起始子串(表示整数内容)的数值,返回值类型分别是int、long、unsigned long、long long、unsigned long long。b表示转换所用的基数,默认值为10。p是size_t指针,用来保存s中第一个非数值字符的下标,p默认为0,即函数不保存下标 |
||
stol(s, p, b) |
|||
stoul(s, p, b) |
|||
stoll(s, p, b) |
|||
stoull(s, p, b) |
|||
stof(s, p) |
返回s的起始子串(表示浮点数内容)的数值,返回值类型分别是float、double或long double。参数p的作用与整数转换函数中一样 |
||
stod(s, p) |
|||
stold(s, p) |
12.容器适配器
(1)所有容器适配器都支持的操作和类型
所有容器适配器都支持的操作和类型 |
|
size_type |
一种类型,足以保存当前类型的最大对象的大小 |
value_type |
元素类型 |
container_type |
实现适配器的底层容器类型 |
A a |
创建一个名为a的空适配器 |
A a(c) |
创建一个名为a的适配器,带有容器c的一个拷贝 |
关系运算符 |
==、!=、<、<=、>和>=,返回底层容器的比较结果 |
a.empty() |
若a包含任何元素,返回false,否则返回true |
a.size() |
返回a中元素的数目 |
swap(a, b) |
交换a和b的内容,a和b必须有相同的类型,包括底层容器类型也必须相同 |
a.swap(b) |
定义一个适配器,如假定deq是一个deque<int>,可以用deq来初始化一个新的stack:
stack<int> stk(deq); // 从deq拷贝元素到stk
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。
所有适配器都要求容器具有添加、删除元素及访问尾元素的能力,因此适配器不能构造在array和forward_list上。
可以改变适配器默认的实现基础,如
// 这样,statck就基于vector实现了
stack<string, vector<string>> str_std;
但是,由于stack只要求push_back、pop_back和back操作,因此可以使用除array和forward_list之外的任何容器类型来构造;queue适配器要求back、push_back、front和push_front,因此它可以构造于list或deque之上,但不能基于vector构造。priority_queue除了front、push_back和pop_back操作之外还要求随机访问的能力,因此它可以构造于vector或deque之上,但不能基于list构造。
(3)栈适配器(stack)
stack的其他操作 |
|
定义在头文件stack中,默认基于deque实现,也可以在list或vector之上实现 |
|
s.pop() |
删除栈顶元素,但不返回该元素值 |
s.push(item) |
创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造 |
s.emplace(args) |
|
s.top() |
返回栈顶元素,但不将元素弹出栈 |
队列适配器(queue和priority_queue),定义在queue头文件中。
priority_queue的优先级默认采用元素类型的<运算符来确定相对优先级。
queue和priority_queue的其他操作 |
|
queue默认基于deque实现,priority_queue默认基于vector实现。 queue也可以用list或vector实现,priority_queue也可以用deque实现。 |
|
q.pop() |
返回queue的首元素或priority_queue的最高优先级的元素,但不删除该元素 |
q.front() |
返回首元素或尾元素,但不删除该元素 只适用于queue |
q.back() |
|
q.top() |
返回最高优先级元素,但不删除该元素 只适用于priority_queue |
q.push(item) |
在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item或由args构造 |
q.emplace(args) |
算法
算法概述
大多数算法定义在头文件algorithm中,另外在numeric头文件中定义了一组数值泛型算法。算法不依赖于容器,但依赖于元素类型的操作。
只读算法
find算法,auto re = find(beg, end, value)。算法根据指定的beg和end迭代器范围寻找value,返回第一次找到的迭代器,若无法找到,则返回end。
find_if算法,auto re = find_if(beg, end, pFunc)。算法遍历beg至end迭代器,并对其指向的对象分别调用pFunc一元谓词,若返回真,则返回该迭代器,若均返回假,则返回end。
(3)count算法,auto re = count(beg, end, value)。算法根据指定的迭代器范围统计有多少个元素值等于value。
(4)accumulate算法,auto re = accumulate(beg, end, value)。算法以value为初始值,累加从beg到end迭代器指向的元素,返回计算结果。这要求beg指向的元素类型与value能执行相加操作。
(5)equal算法,auto re = equal(beg1, end1, beg2)。算法比较beg1至end1的范围与从beg2开始的范围内元素值是否都相等,这要求beg2的后续范围必须至少与beg1后续范围一样大,元素类型要支持==运算。
注:那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
写容器算法
fill算法,fill(beg, end, value)。算法将迭代器beg至end范围的元素均赋值为value。
fill_n算法,fill(beg, n, value)。算法将从beg开始的n个元素均赋值为value。
copy算法,auto re = copy(beg1, end1, beg2)。算法将迭代器beg1至end1指向的元素复制到从迭代器beg2开始的指向元素。re为迭代器beg2后第一个未被copy的迭代器。
replace算法,replace(beg, end, oldVal, newVal)。算法遍历迭代器beg至end,若元素值等于oldVal,则替换为newVal。
replace_copy算法,replace_copy(beg, end, beg2, oldVal, newVal)。算法遍历迭代器beg至end,若元素值等于oldVal,则将newVal添加到beg2的迭代器指向的元素中,beg2指向下一个元素。
sort算法,sort(beg, end)、sort(beg, end, pCmpFunc)。算法遍历迭代器beg至end,或用默认的<进行排序,或用pCmpFunc可调用对象进行排序。
unique算法,auto re = unique(beg, end)。算法将序列中相邻的重复元素放置于序列的最后,返回最后一个不重复元素之后的位置。
stable_sort算法,stable_sort(beg, end, pCmpFunc)。若pCmdFunc为根据字符串长度排序,则该算法最后对同等长度的则再根据字典序排序。
for_each算法,for_each(beg, end, func)。该算法对输入序列中每个元素都调用一元谓词func。
transform算法,transform(beg, end, beg2, func)。该算法对输入序列中每个元素都调用一元谓词func,并将结果写入目的位置。beg2可与beg相同。
谓词
即一种可调用的表达式,包括函数、函数指针、可调用的类、lambda表达式等。标准库算法中的谓词分为一元谓词和二元谓词,分别表示有1个形参和2个形参。
lambda表达式
一个lambda表达式有如下形式:
[capture list] (parameter list) -> return type { function body}
其中,capture list表示捕获列表,parameter list表示形参列表,参数不能有默认值,return type表示返回类型。lambda表达式会生成一种可调用的无名类,可以如下保存使用:
auto f = [] { reuturn 42; }
这样,f就表示了这个lambda表达式生成类的对象,因此f为可调用对象,它不接受参数,返回42。由上可见,形参列表和返回类型可以省略。调用时的形式为f(),表示无参。如果忽略了返回类型,lambda根据函数体中的代码推断返回类型,若函数体唯一的代码语句就是一个return语句,则lambda的返回类型与return表达式中的类型一致,否则返回类型被推断为void。
捕获列表。一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在其自身函数体中使用该变量,另外lambda可以直接使用局部static变量以及它所在函数之外声明的名字。
捕获列表的捕获方式有值捕获和引用捕获,后者可以改变外部变量。前者使用形式如[v1]{ return v1 * 2;},后者使用形式如[&v2]{ return ++v2; },此时外部变量v1值不变,而v2值被改变。我们可以从一个函数返回lambda,但此时与返回局部引用一样,lambda不能包含引用捕获。当采用值捕获时,被捕获的变量是在lambda创建时拷贝,而不是调用时拷贝。
void fcn1()
{
size_t v1 = 42; // 局部变量
auto f = [v1] { return v1; };
v1 = 0;
auto j = f();// 此时j为42,而不是0,因为f拷贝了创建时v1的值
}
隐式捕获和混合捕获。
隐式捕获:在捕获列表仅仅写一个&或者=,而忽略具体变量名,分别表示采用引用捕获和值捕获,在函数体内可以直接使用相关变量名,但捕获列表不能同时写上&和=,如:
[=] { return v1; } // 值方式捕获
混合捕获:即混合使用显式捕获和隐式捕获。捕获列表中的第一个元素必须是一个&或=,以表示隐式捕获采用的值方式或引用方式,对显式捕获的变量则必须使用与隐式捕获不同的方式,若隐式捕获为引用捕获,则显式捕获必须为值方式,若隐式捕获为值方式,则显式捕获必须为引用方式,如:
[=, &v1] () -> int { ++v1; return v1 + v; }
可变lambda。默认情况下,对值方式捕获的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,必须加上mutable,如:
void fcn()
{
size_t v1 = 42;
auto f = [v1] () mutable { return ++v1; };
v1 = 0;
auto j = f(); // 此时,j将是43,而不是1
}
bind函数
bind标准库函数定义在头文件functional中。它接受一个可调用对象,生成一个新的可调用对象,可以使用定义在std::placeholders命名空间中的_n来定制映射关系,从而改变原可调用对象的形参列表(通过映射)。如:
bool func(int v1, int v2, int v3, int v4);
auto newFunc = bind(func, 10, 20, _2, _1);
则实际产生bool newFunc(_1, _2)的可调用对象,当我们用实参3和5调用newFunc(3, 5)实际上将会被映射还原成func(10, 20, 5, 3)的样子。这样可以把一些不符合某些算法要求的可调用对象,通过bind的方式形成符合那些算法要求的可调用对象形式。
在bind中当需要使用引用方式指定一个实参时(如对于流无法拷贝,只能引用),则可以通过标准库ref函数来实现,如bind(print, ref(os), _1, ' ')。这样,对os输出流就是采用引用的方式绑定实参。如果无须写入引用实参,则可用cref函数,生成一个保存const引用的类。ref和cref均定义在头文件functional中。
插入迭代器
各种被插入迭代器绑定的容器必须支持对应插入迭代器所要求具有的操作。插入迭代器定义在头文件iterator中。
back_inserter,创建一个使用push_back方法的迭代器,如:
vector<int> vec;
auto it = back_inserter(vec);
*it = 32; // 则此时在vec尾部被插入一个值为32的元素
front_inserter,创建一个使用push_front的迭代器。
inserter,创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器,以表示后续元素将被插入到给定迭代器所表示的元素之前。
以上插入迭代器为某些要求容器具有一定大小的算法提供了实现方法。
插入迭代器的操作 |
|
it = t |
在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t)或c.insert(t, p),其中p为传递给inserter的迭代器位置 |
*it,++it,it++ |
这些操作虽然存在,但不会对it做任何事情。每个操作均返回it |
iostream迭代器
所有定义了输入运算符(>>)的类型都可以创建istream_iterator对象,所有定义了输出运算符(<<)的类型都可以创建ostream_iterator对象。创建一个流迭代器时,必须指定迭代器将要读写的对象类型,我们可以将它绑定到一个流,也可以默认初始化流迭代器,此时相当于创建了一个可以当作尾后迭代器使用的流迭代器。如:
istream_iterator<int> int_it(cin); // 从cin读取int
istream_iterator<int> eof; // 尾后迭代器
ifstream in("filename");
istream_iterator<string> str_it(in);// 从"filename"读取字符串
// 把从cin读取的值放入v1,然后cin继续读下一个值
int v1 = *int_it++;
注意,流迭代器没有递减(--)操作符,因为流不可逆。
其中,对istream_iterator要读取下一个值,必须用递增操作符,但对ostream_iterator则可以省略(也可以省略解引用),但建议也同样使用递增操作符和解引用,以方便阅读。
istream_iterator操作 |
||
istream_iterator<T> in(is); |
in从输入流is读取类型为T的值 |
|
istream_iterator<T> end; |
读取类型为T的值的istream_iterator迭代器,表示尾后位置 |
|
in1 == in2 |
in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等 |
|
in1 != in2 |
||
*in |
返回从流中读取的值 |
|
in->mem |
与(*in).mem的含义相同 |
|
++in,in++ |
使用元素类型所定义的>>运算符从输入流中读取下一个值。 |
|
ostream_iterator操作 |
||
ostream_iterator<T> out(os); |
out将类型为T的值写到输出流os中 |
|
ostream_iterator<T> out(os, d) |
out将类型为T的值写到输出流中,每个值后面都输出一个d。d指向一个空字符结尾的字符数组 |
|
out = val |
用<<运算符将val写到out所绑定的ostream中。val的类型必须与out可写的类型兼容 |
|
++in,in++ |
使用元素类型所定义的>>运算符从输入流中读取下一个值。 |
反向迭代器
除了forward_list外,其他容器都支持反向迭代器,可以通过调用rbegin、rend、crbegin、crend成员函数来获得反向迭代器。在反向迭代器中,相应的begin成员实际指向容器最后元素,而end成员实际指向容器首元素之前的位置。对rbegin取得的反向迭代器进行递增操作,则移向rend指向的元素一侧,反之亦然,即实际移动的效果与普通迭代器恰好相反。若要把反向迭代器转换为对应位置的普通迭代器,可以使用base成员函数,如riter是一个反向迭代器,则auto iter = riter.base(),iter则系普通迭代器,指向的元素与riter指向的一致。
特定容器算法
list和forward_list成员函数版本的算法 |
|
这些操作都返回void |
|
lst.merge(lst2) |
将来自lst2的元素合并入lst。lst和lst2都必须是有序的。元素将从lst2中删除。在合并之后,lst2变为空。第一个版本使用<运算符,第二个版本使用给定的比较操作 |
lst.merge(lst2, comp) |
|
lst.remove(val) |
调用erase删除掉与给定值相等(==)或令一元谓词为真的每个元素 |
lst.remove_if(pred) |
|
lst.reverse() |
反转lst中元素的顺序 |
lst.sort() |
使用<或给定比较操作排序元素 |
lst.sort(comp) |
|
lst.unique() |
使用erase删除同一个值的连续拷贝。第一个版本使用==,第二个版本使用给定的二元谓词 |
lst.unique(pred) |
list和forward_list的splice成员函数的参数 |
|
lst.splice(args)或flst.splice_after(args) |
|
(p, lst2) |
p是一个指向lst中元素的迭代器,或一个指向flst首前位置的迭代器。函数将lst2的所有元素移动到lst中p之前的位置或是flst中p之后的位置。将元素从lst2中删除。lst2的类型必须与lst或flst相同,且不能是同一个链表 |
(p, lst2, p2) |
p2是一个指向lst2中位置的有效的迭代器。将p2指向的元素移动到lst中,或将p2之后的元素移动到flst中。lst2可以是与lst或flst相同的链表 |
(p, lst2, b, e) |
b和e必须表示lst2中的合法范围。将给定范围中的元素从lst2移动到lst或flst。lst2与lst(或flst)可以是相同的链表,但p不能指向给定范围中的元素 |
关联容器
关联容器的类型
类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unodered_map和unodered_set中。
有序容器:
map:关联数组,保存关键字-值对。
set:关键字即值,即只保存关键字的容器。
multimap:关键字可重复出现的map。
multiset:关键字可重复出现的set。
对于有序容器,关键字类型必须定义元素的比较方法,默认为使用关键字类型<运算符进行比较操作,可以用我们自己的比较操作进行替换。因此,当对自定义类型使用有序容器时,得提供<运算符,或者提供其他比较函数,方可创建有序关联容器。
无序集合:
unodered_map:用哈希函数组织的map。
unodered_set:用哈希函数组织的set。
unodered_multimap:哈希函数组织的map,关键字可重复出现。
unodered_multiset:哈希函数组织的set,关键字可重复出现。
关联容器的初始化
默认初始化,为空容器,如map<string, int> word_count,此时word_count为空。
列表初始化,即用花括号的形式初始化容器,如set<string> strset{"wrod", "hello"},也可以用拷贝赋值右边用花括号的形式,如set<string> strset = {"wrod", "hello"}。当初始化一个map时,必须提供关键字类型和值类型,将每个关键字-值对包围在花括号中,如:
map<string, string> authors = {{"Jone", "James"}, {"Austen", "Jane"}};
提供自己的比较操作创建有序关联容器,如compareIsbn为自己定义的一个比较两个Sales_data的函数,则可以如下创建Sales_data的multiset:
multiset<Sale_data, decltype(compareIsbn)*> bookstore(compareIsbn);
这样就可以为没有重载<运算符的Sales_data类型支持关联容器操作。
pair类型
pair标准库类型定义在头文件utility中。pair系类模板,创建一个pair时需要提供两个类型名。因此,一个pair元素包含两个成员,分别命名为first和second,均为public访问属性。pair提供的操作有:
pair<T1, T2> p:p是一个pair,两个类型分别为T1和T2的成员都进行了值初始化。
pair<T1, T2> p(v1, v2):p是一个成员类型为T1和T2的pair,first和second成员分别用v1和v2进行初始化。
pair<T1, T2> p = {v1, v2}:等价于pair<T1, T2> p(v1, v2),此处也可省略=号。
make_pair(v1, v2):返回一个用v1和v2初始化的pair,类型从v1和v2的类型推断出来。可以用{v1, v2}的形式来替代。
p.first:返回p的名为first的公有数据成员。
p.second:返回p的名为second的公有数据成员。
p1 relop p2:关系运算符按字典序定义,如当p1.first < p2.first或!(p2.first < p1.first) && p1.second < p2.second成立时,p1 < p2为true。关系运算利用元素的<运算符来实现。
p1 == p2和p1 != p2:当first和second成员分别相等时,两个pair相等。相等性利用元素的==运算符实现。
关联容器的操作
这些操作对于有序关联容器和无序关联容器均适用。
key_type:此容器类型的关键字类型。
mapped_type:每个关键字关联的类型,只适用于map类关联容器。
value_type:对于set,与key_type相同,对于map,为pair<const key_type, mapped_type>。
迭代器相关操作,如begin()、end()分别返回容器的首迭代器和尾后迭代器,由于set只有关键字,而关键字不能被改变,所以set的迭代器无论是否是const_iterator,均只能用于读取。
c.insert(v)和c.emplace(args):v是value_type类型的对象;args用来构造一个元素。对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。对于multimap和multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器。
c.insert(b, e)和c.insert(il):b和e是迭代器,表示一个c::value_type类型值的范围;il是这种值的花括号列表。函数返回void。对于map和set,只插入关键字不在c中的元素。对于multimap和multiset,则会插入范围中的每个元素。
c.insert(p, v)和c.emplace(p, args):类似insert(v)(或emplace(args)),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。
c.erase(k):从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量。
c.erase(p):从c中删除迭代器p指定的元素。p必须指向c中一个真实的元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end()。
c.erase(b, e):删除迭代器b和e所表示的范围中的元素,返回e。
c.find(k):返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器。
c.count(k):返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1。
map和unodered_map的下标操作
c[k]:返回关键字为k的元素。如果k不在c中,添加一个关键字为k的元素,对其进行值初始化。
c.at(k):访问关键字为k的元素,带参数检查。若k不在c中,抛出一个out_of_range异常。该操作,只使用于非const的map和unodered_map。
只适用于有序关联容器的操作
c.lower_bound(k):返回一个迭代器,指向第一个关键字不小于k的元素。
c.upper_bound(k):返回一个迭代器,指向第一个关键字大于k的元素。
c.equal_range(k):返回一个迭代器pair,表示关键字等于k的元素的范围,其first成员即相当于c.lower_bound(k)的返回值,其second成员相当于c.upper_bound(k)的返回值。
以上操作,若容器中无关键字为k的元素,则相关返回的迭代器均为c.end()。
无序容器
无序容器使用一个哈希函数和关键字类型的==运算符来组织元素。除了哈希管理操作之外,无序容器提供了与有序容器相同的操作,如find、insert等。
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。
(1)无序容器的管理操作
桶接口
c.bucket_count():正在使用的桶的数目。
c.max_bucket_count():容器能容纳的最多的桶的数量。
c.bucket_size(n):第n个桶中有多少个元素。
c.bucket(k):关键字为k的元素在哪个桶中。
桶迭代
local_iterator:可以用来访问桶中元素的迭代器类型。
const_local_iterator:桶迭代器的const版本。
c.begin(n),c.end(n):桶n的首元素迭代器和尾后迭代器。
c.cbegin(n),c.cend(n):与前两个函数类似,但返回const_local_iterator类型。
哈希策略
c.load_factor():每个桶的平均元素数量,返回float值。
c.max_load_factor():c试图维护的平均桶大小,返回float值。c会在需要时添加新的桶,以使得load_factor <= max_load_factor。
c.rehash(n):重组存储,使得bucket_count >= n且bucket_count > size / max_load_factor。
c.reserve(n):重组存储,使得c可以保存n个元素且不必rehash。
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string和智能指针类型定义了hash。因此,我们可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针类型的无序容器。如
unodered_map<string, size_t> word_count;
但是,我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本。
size_t hasher(const Sales_data &sd)
{
return hash<string>()(sd.isbn());
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
有了上述函数,我们就可以为自定义类类型Sales_data创建无序容器,
using SD_multiset = unodered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
// 参数是桶大小、哈希函数指针和相等性判断运算符指针
SD_multiset bookstore(42, hasher, eqOp);
动态内存
动态内存与智能指针
为了更容易(同时也更安全)地使用动态内存,新的标准提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则"独占"所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。以上三种类型均定义在memory头文件中。
shared_ptr类
智能指针也是模板,因此使用必须给出需要管理的指针类型,如:
shared_ptr<string> p1; // shared_ptr,可以指向string
默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
shared_ptr与unique_ptr都支持的操作
shared_ptr<T> sp、unique_ptr<T> up:空智能指针,可以指向类型为T的对象。
p:将p用作一个条件判断,若p指向一个对象,则为true。
*p:解引用p,获得它指向的对象。
p->mem:等价于(*p).mem。
p.get():返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
swap(p, q)、p.swap(q):交换p和q中的指针。
shared_ptr独有的操作
make_shared<T>(args):返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。
shared_ptr<int> p2 = make_shared<int>(42);
shared_ptr<string> p3 = make_shared<string>(10, 'a');
shared_ptr<T> p(q):p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*,此构造函数是explicit的,q可以是new生成的普通指针。
p = q:p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。这里的q不能是普通指针。
p.unique():若p.use_count()为1,则为true,反之为false。
p.use_count():返回与p共享对象的智能指针数量;可能很慢,主要用于调试。
我们通常用auto定义一个对象来保存make_shared的结果,如:
// p6指向一个动态分配的vector<string>
auto p6 = make_shared<vector<string>>();
程序使用动态内存主要基于以下三种原因之一:
程序不知道自己需要使用多少对象。
程序不知道所需对象的准确类型。
程序需要在多个对象间共享数据。
3.直接管理内存
(1)初始化new对象。
string *ps = new string; // 默认初始化,初始化为空的string
// 初始化为"Hello world!"的string
string *ps2 = new string("Hello world!");
string *ps3 = new string{"Hello world!"}; // 同上
如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,因此只有当括号中仅有单一初始化器时才可以使用auto:
// p指向一个与obj类型相同的对象,该对象用obj初始化
auto p1 = new auto(obj);
auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
用new分配const对象是合法的:
const int *pci = new const int(1024);
const string *pcs = new const string;
由于分配的对象是const的,new返回的指针是一个指向const的指针。
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:
int *p2 = new (nothrow) int; // 如果分配失败,new返回一个空指针
这种形式称为定位new。定位new表达式允许我们向new传递额外的参数。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。bad_alloc和nothrow均定义在头文件new中。
shared_ptr和new结合使用
接受指针参数的智能指针构造函数是explicit的。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int> p1 = new int(1024);// 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
定义和改变shared_ptr的其他方法
shared_ptr<T> p(q):p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型。
shared_ptr<T> p(u):p从unique_ptr u那里接管了对象的所有权;将u置为空。
shared_ptr<T> p<q, d>:p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete。此时q不必是new生成的指针。
shared_ptr<T> p(p2, d):p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete。
p.reset()、p.reset(q)、p.reset(q, d):若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q。
为了正确使用智能指针,必须坚持的一些基本规范
不使用相同的内置指针初始化(或reset)多个智能指针。
不delete get()返回的指针。
不使用get()初始化或reset另一个智能指针。
如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,指针为无效。
如果使用智能指针管理的资源不是new分配的内存,记得传递给它一个删除器。
unique_ptr
一个unique_ptr"拥有"它所指向的对象。在某个时刻只能有一个unique_ptr指向一个给定的对象。当定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:
unique_ptr<double> p1; // 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); // 错误:unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误:unique_ptr不支持赋值
unique_ptr操作:
unique_ptr<T> u1、unique_ptr<T, D> u2:空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T, D> u(d):空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete。可以有u(pt, d),其中pt为T*类型指针。
u = nullptr:释放u指向的对象,将u置为空。
u.release():u放弃对指针的控制权,返回指针,并将u置为空,但不释放内存。
u.reset()、u.reset(q)、u.reset(nullptr):释放u指向的对象。如果提供了内置指针q,令u指向这个对象;否则将u置为空。
虽然不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。
不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
return ret;
}
weak_ptr
将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
weak_ptr的主要用途是在不影响shared_ptr的引用计数的前提下,提供相应的操作和数据。因此,一般用作伴随类。
weak_ptr操作
weak_ptr<T> w:空weak_ptr可以指向类型为T的对象。
weak_ptr<T> w(sp):与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型。
w = p:p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象。
w.reset():将w置为空。
w.use_count():与w共享对象的shared_ptr的数量。
w.expired():若w.use_count()为0,返回true,否则返回false。
w.lock():如果expired为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr。
动态数组
int *pia = new int[get_size()]; // pia指向第一个int
方括号中的大小必须是整型,但不必是常量。
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT; // 分配一个42个int的数组;p指向第一个int
不能对动态数组调用标准库函数begin和end,同样的,也不能用范围for语句来处理动态数组中的元素。
虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。
新标准的动态数组初始化方式(初始化列表)
int *pia = new int[10]; // 10个未初始化的int
int *pia2 = new int[10](); // 10个值初始化为0的int
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存,会抛出一个类型为bad_array_new_length(定义在头文件new中)的异常。
当用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就象尾后指针一样,可以象使用尾后迭代器一样使用这个指针。
为了释放动态数组,使用一种特殊的delete——在指针前加上一个空方括号对:
delete p; // p必须指向一个动态分配的对象或为空
delete [] pa; // pa必须指向一个动态分配的数组或为空
第二种delete语句中,按数组中的元素逆序销毁,即最后一个元素首先销毁,然后是倒数第二个,依此类推。
为了用一个unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号:
unique_ptr<int []> up(new int[10]);
up.release(); // 自动用delete []销毁此指针
当unique_ptr指向数组时,可用u[i]的形式返回u拥有的数组中位置i处的对象。
allocator类
标准库allocator类定义在头文件memory中,用于将内存分配和对象构造分离。它提供一种类型感知的内存分配方法,分配的内存是原始的、未构造的。这样可用于为无默认构造函数的类动态分配数组。
allocator<string> alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未初始化的string
标准库allocator类及其算法
allocator<T> a:定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存。
a.allocate(n):分配一段原始的、未构造的内存,保存n个类型为T的对象。
a.deallocate(p, n):释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy。
a.construct(p, args):p必须是一个类型为T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p):p为T*类型的指针,此算法对p指向的对象执行析构函数。
auto q = p; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi
还未构造对象的情况下就使用原始内存是错误的:
cout << *p << endl; // 正确:使用string的输出运算符
cout << *q << endl; // 错误:q指向未构造的内存
当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。我们只能对真正构造了的元素进行destroy操作。
allocator算法
这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。
uninitialized_copy(b, e, b2):从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝。返回下一个未构建对象的迭代器。
uninitialized_copy_n(b, n, b2):从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t):在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t):从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
拷贝控制
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo {
public:
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
// …
};
如果我们没有为一个类定义拷贝构造函数,编译器会帮我们合成一个,称合成拷贝构造函数。合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
每个成员的类型决定了它如何拷贝:对类类型的成员,会使用拷贝构造函数来拷贝;内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组,但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。
拷贝初始化不仅在使用=定义变量时会发生,在以下情况也会发生
将一个对象作为实参传递给一个非引用类型的形参。
从一个返回类型为非引用类型的函数返回一个对象。
用花括号列表初始化一个数组中的元素或一个聚合类的成员。
拷贝赋值运算符
重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。因此,赋值运算符就是一个名为operator=的函数。类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表。赋值运算符必须定义为成员函数,其左侧运算对象就绑定到隐式的this参数。
class Foo {
public:
Foo& operator=(const Foo&); // 赋值运算符
// …
};
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。
析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数:
class Foo {
public:
~Foo(); // 析构函数
// …
};
由于析构函数不接受参数,因此它不能被重载。对一个给定类,只有唯一一个析构函数。隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
析构函数的调用情形:
变量在离开其作用域时被销毁。
当一个对象被销毁时,其成员被销毁。
容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
对于临时对象,当创建它的完整表达式结束时被销毁。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来组织该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。
三/五法则
需要析构函数的类也需要拷贝和赋值操作。
需要拷贝操作的类也需要赋值操作,反之亦然。但不一定需要析构函数。
使用=default
我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本:
class Sales_data {
public:
Sales_data () = default;
Sales_data (const Sales_data&) = default;
~Sales_data () = default;
// …
};
当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联函数。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default。
阻止拷贝(=delete)
我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的:
struct NoCopy {
NoCopy() = default; // 使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; // 阻止拷贝
NoCopy& operator=(const NoCopy&) = delete; // 阻止赋值
~NoCopy() = default;
// 其他成员
};
=delete与=default的区别
=delete必须出现在函数第一次声明的时候。
可以对任何函数指定=delete(但对析构函数不要=delete,否则将无法销毁对象)。
如果一个类定义了删除的析构函数,编译器将不允许定义该类型的变量或创建该类的临时对象。而且,如果一个类有某个成员的类型删除了析构函数,也不能定义该类的变量或临时对象。
在合成的拷贝控制成员中,如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
在新标准之前,类通过将其拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝,但在新标准后,我们应该使用=delete的形式。
右值引用
所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此我们可以自由地将一个右值引用的资源"移动"到另一个对象中。
右值引用只能绑定到临时对象,因此所引用的对象将要被销毁,该对象没有其他用户。这意味着使用右值引用的代码可以自由地接管所引用的对象的资源。右值引用无需const限定。
变量是左值,因此不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。但是,我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式rr1是左值
int &&rr3 = std::move(rr1); // 正确
另外,对于即将销毁的左值,可以转换为右值引用类型,例如在函数的return中,可将return的临时对象返回为对应的右值引用类型,还包括临时构造的临时对象也可以转换为右值引用类型。
与大多数标准库名字的使用不同,对move我们不提供using声明,而是直接使用std::move的形式。
移动构造函数和移动赋值运算符
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。另外,移动构造函数不应抛出异常,通过在形参列表后声明noexcept实现。
StrVec::StrVec(StrVec &&s) noexcept // 移动操作不应抛出任何异常
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
// 令s进入这样的状态——对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
由于移动构造函数实际是把另外的对象(将销毁的对象)的资源窃取,可见移动构造实际是一种指针的赋值,也就是说支持移动构造函数(包括移动赋值运算符)的类,往往是存在申请内存等底层操作,通过这种移动构造和移动赋值避免再次申请内存和内存拷贝,而只要拷贝指针即可。
移动赋值运算符与移动构造函数类似,也是用右值引用类型的形参,且声明noexcept来保证不抛出异常。在移动赋值运算符中,必须正确处理自赋值的情况。
StrVec& StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs) // 检测自赋值
{
free(); // 释放已有元素
elements = rhs.elements; // 从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
// 将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
retrun *this;
}
内置类型都是可以移动的类型,如int等。
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
合成的移动操作定义为删除的情形:
有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
如果有类成员的移动构造函数或者移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
移动右值,拷贝左值:当一个类既有移动构造函数,又有拷贝构造函数时,当参数或者赋值运算符的右边是左值时,匹配拷贝运算,若为右值,则匹配移动运算:
StrVec v1, v2;
v1 = v2; // v2是左值;使用拷贝赋值
StrVec getVec(istream &); // getVec返回一个右值
v2 = getVec(cin); // getVec(cin)是一个右值;使用移动赋值
如果没有移动构造函数,则右值也将被拷贝。
移动迭代器
新标准库中定义了一种移动迭代器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。
我们通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。
引用限定符
当我们希望不能对一个类的右值进行赋值的时候,可以使用引用限定符,以指出this的左值/右值属性,从而使相关赋值运算符只能使用在特定的左值/右值场合。
指出this的左值/右值属性的方式与定义const成员函数相同,即在形参列表后放置一个引用限定符。引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。对于&限定的函数,我们只能将它用于左值,该引用限定符可跟const限定符同时使用,当有const限定符时,引用限定符必须在const之后;对于&&限定的函数,只能用于右值。
stuct Foo {
Foo& operator=(const Foo&) &; // 只能向可修改的左值赋值
Foo sorted() &&; // 可用于可改变的右值
Foo sorted() const &; // 可用于任何类型的Foo
vector<int> data;
// Foo的其他成员
};
Foo& Foo::operator=(const Foo &rhs) &
{
// 相关赋值操作的代码…
return *this;
}
Foo& retFoo(); // 返回一个引用;retFoo调用是一个左值
Foo retVal(); // 返回一个值;retVal调用是一个右值
Foo i, j; // i和j是左值
i = j; // 正确:i是左值
retFoo() = j; // 正确:retFoo()返回一个左值
retVal() = j; // 错误:retVal()返回一个右值
// 正确:我们可以将一个右值作为赋值操作符的右侧运算对象
i = retVal();
// 本对象为右值,可以原址排序(因为没有其他用户,改变源对象无所谓)
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
return *this;
}
// 本对象是const或是一个左值,哪种情况我们都不能对其进行原址排序
Foo Foo::sorted() const &
{
Foo ret(*this); // 拷贝一个副本
sort(ret.data.begin(), ret.data.end()); // 排序副本
return ret; // 返回副本
}
重载运算与类型转换
基本概念
除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象少一个。
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。
可以被重载的运算符:
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new[] delete delete[]
不能被重载的运算符:
:: .* . ?:
一个非成员运算符函数的等价调用:
data1 + data2; // 普通的表达式
operator+(data1, data2); // 等价的函数调用
成员运算符函数的等价调用:
data1 += data2; // 基于"调用"的表达式
data1.operator+=(data2); // 对成员运算符函数的等价调用
某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。特别是,逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来。除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。
将运算符定义为成员函数或非成员函数的一般规则:
赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员函数。
复合赋值运算符一般应为成员,但并非必须,此与赋值运算符略有不同。
改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象(即左侧运算对象的类型必须一致,而不能转换)。而非成员函数则可以根据可能的构造函数而转换类型再进行运算。
输入和输出运算符
通常情况下,输出运算符的第一个形参是一个非常量的ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。同样的,输入运算符的第一个形参也是非常量的istream对象的引用。但在第二个形参上,由于输出运算符不改变第二个实参的内容,因此对于输出运算符其第二个形参为const的引用,而对于输入运算符由于会改变第二个实参的内容,因此其第二个形参为普通的引用。输入输出运算符一般都返回流的引用。输入运算符一般还要处理输入失败的情况,最好还能指出流失败的状态,设置failbit。
ostream& operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue
<< " " << item.avg_price();
return os;
}
istream& operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNo >> item.units_sold >> price;
if(is) // 检查输入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 输入失败则对象被赋予默认的状态
return is;
}
由于左侧运算对象不是自定义类型的一个对象,而是流对象,因此输入输出运算符必须是非成员函数。
算术和关系运算符
如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符。一般的算术和关系运算符都定义成非成员函数,这样就可以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // 把lhs的数据成员拷贝给sum
sum += rhs; // 利用已经实现的+=运算符将rhs加到sum中
return sum;
}
定义相等运算符和不相等运算符往往实际只需要实现一个,因为实现另一个,另外一个只要对实现的那一个进行求非运算即可。
赋值运算符
要使用花括号内的元素作为赋值运算符的右侧对象,在赋值运算符的形参上应如下定义:
class StrVec {
public:
StrVec& operator=(std::initializer_list<std::string>);
// 其他成员
};
这样,我们就能使用诸如StrVec sv = {"hi", "hello"};这样的形式赋值。花括号中每个元素对应初始化StrVec容器中的一个底层元素。
赋值运算符和符合赋值运算符都应该返回左侧对象的引用。
下标运算符
下标运算符必须是成员函数。一般我们应定义下标运算符的两种版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。这样当对象是const时,对其取下标将返回元素的常量引用,不能赋值,而对象是非const时,则对其取下标将返回普通引用,此时可以对其赋值等。
class StrVec {
public:
std::string& operator[](std::size_t n)
{ return elements[n]; }
const std::string& operator[](std::size_t n) const
{ return elements[n]; }
// 其他成员
private:
std::string *elements; // 指向数组首元素的指针
};
递增和递减运算符
定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常应该被定义成类的成员。
定义前置递增/递减运算符:
class StrBlobPtr {
public:
StrBlobPtr& operator++(); // 前置递增运算符
StrBlobPtr& operator--();
// 其他成员与之前一样
};
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
由于前置版本和后置版本是同一个符号,因此普通重载无法区分,故为了解决该问题,后置版本接受一个额外的(不被使用)int类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。由于对该形参我们并不实际使用,因此也无需在后置版本的实现中为该形参命名。
定义后置递增/递减运算符:
StrBlobPtr StrBlobPtr::operator++(int)
{
StrBlobPtr ret = *this; // 记录当前值
++*this; // 利用已经实现的++前置版本,向前移动元素
return ret; // 返回之前记录的状态
}
为了与内置版本保持一致,后置运算符应该返回对象的原值(即递增或递减之前的值),返回的形式是一个值而非引用。
成员访问运算符
箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
struct ST
{
int Value;
string Name;
};
class demo
{
public:
demo(ST &st): pst(&st) {}
ST& operator*() const
{ return *pst; }
ST* operator->() const
{ return &this->operator*(); }
private:
ST *pst;
};
int main()
{
ST st = {18, "Hello world!"};
demo d(st);
cout << d->Name << "\t" << d->Value << endl;
ST st2 = *d;
cout << st2.Name << "\t" << st2.Value << endl;
return 0;
}
以上均会输出Hello world! 18。可见,对于->运算符实际上就是要返回一个某个类的指针或者自定义了箭头运算符的某个类的对象。上述*和->均定义成const成员,因为与递增、递减运算符不同,获取一个元素并不会改变对象的状态。
对于形如point->mem的表达式来说,point必须是指向类对象的指针或者是一个重载了operator->的类的对象。根据point类型的不同,point->mem分别等价于:
(*point).mem; // point是一个内置的指针类型
point.operator()->mem; // point是一个类的对象
point->mem的执行过程如下:
如果point是指针,则应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序出错。
如果point是定义了operator->的类的一个对象,则使用point.operator->()的结果来获取mem。其中,如果该结果是一个指针,则执行第1步;如果该结果本身含有重载的operator->(),则重复调用当前步骤。最终,当这一过程结束时,程序或者返回了所需的内容,或者返回一些表示程序错误的信息。
函数调用运算符
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
如果类定义了调用运算符,则该类的对象称作函数对象。
一个lambda实际是一个未命名的函数对象。当我们编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。例如:
stable_sort(words.begin(), words.end(), [](const string &a, const string &b)
{ return a.size() < b.size(); } );
其行为类似于下面这个类的一个未命名对象:
class ShorterString {
public:
bool operator()(const string &s1, const string &s2) const
{ return s1.size() < s2.size(); }
};
当一个lambda表达式调用引用捕获变量时,将由程序员确保lambda执行时引用所引的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。相反,通过值捕获的变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。如:
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
该lambda表达式产生的类将形如:
struct SizeComp {
SizeComp(size_t n): sz(n) {} // 该形参对应捕获的变量
// 该调用运算符的返回类型、形参和函数体都与lambda一致
bool operator()(const string &s) const
{ return s.size() >= sz; }
private:
size_t sz; // 该数据成员对应通过值捕获的变量
};
lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。
头文件functional中定义的标准库函数对象见P510。
专题:可调用对象与function
不同类型的可调用对象可能具有相同的调用形式,例如:
int add(int i, int j) { return i + j; }
auto mod = [](int i, int j){ return i % j; };
struct divide {
int operator()(int i, int j) { return i / j; }
};
对于上述三种调用对象,其调用形式都是int (int, int),但执行的功能不同。
如果我们希望能动态调用不同的调用对象,一种方式通过定义一个函数表,用于存储指向这些可调用对象的"指针",例如用map<string, int(*)(int, int)> binops;但是,此时我们不能将mod或者divide存入binops,因为mod是一个lambda表达式,每个lambda有它自己的类类型,并非binops中的值的类型。
利用定义在头文件functional中的标准库function类型可以解决上述类型不一致的问题。
function是个模板类,其提供的操作如下:
function<T> f:f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同(即T是retType(args))。
function<T> f(nullptr):显式地构造一个空的function。
function<T> f(obj):在f中存储可调用对象obj的副本。
f:将f作为条件,当f含有一个可调用对象时为真,否则为假。
f(args):调用f中的对象,参数是args。
定义为function<T>的成员的类型:
result_type:该function类型的可调用对象返回的类型。
argument_type:当T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type是该类型的同义词;如果T有两个实参,则first_argument_type和second_argument_type分别代表两个实参的类型。
学习心得:个人推断function模板类的实现原理是根据T的调用形式,在function的调用对象中,调用过程中返回保存的obj的调用结果。例如有
int add(int v1, int v2);
function<int(int, int)> f(add);
则f(v1, v2) { return add(v1, v2); },这样就保证了不同调用对象在封装之后的调用形式仍旧保持相同,另外,对象类型也一致了(即均为function<int(int, int)>)。
我们可以用map<string, function<int(int, int)>> binops;此时的binops也可以用来同时保存add、mod和divide对象了。
需要注意的是,我们不能(直接)将重载函数的名字存入function类型的对象中,我们可以用存储函数指针或者使用lambda的方式来解决这个问题(此问题比较简单,不再详细叙述,总体原理与function模板类的实现原理类似)。
重载、类型转换与运算符
转换构造函数(即单形参的构造函数)与类型转换运算符共同定义了类类型转换,或叫用户定义的类型转换。类型转换运算符如下:
// 若系explicti,则转换需要显式static_cast进行
[explicit] operator type() const;
不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。类型转换运算符没有显式的返回类型,也没有形参,而且必须是成员函数,同时通常应为const函数。
编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可置于一个标准(内置)类型转换之前或之后,并与其一起使用。
在excplicit类型转换运算符中,虽然一般应用static_cast的方式执行转换,但是如果表达式被用作条件,则编译器自动会将显式的类型转换予以应用,无需我们手工添加static_cast,如在if条件中以及其他条件表达式中,可象隐式转换那样使用。
专题:类型转换与重载确定
对于转换构造函数来说,如果调用某个重载函数,该重载函数的区别是各自有不同的自定义类型,而这些自定义类型都给标准类型到自定义类型提供了可行匹配,则标准类型之间的转换不会被当作精确匹配的依据,如
C::C(int);
D::D(double);
void func(const C&);
void func(const D&);
func(10); // 错误,func(C(10))还是func(D(double(10)))
如果定义了单形参B到A的构造函数,又定义了B到A的类型转换运算符,则在形参类型为A的函数中,会存在二义性,如
A::A(const B&);
B::operator A() const;
void func(const A&);
B b;
func(b); // 错误,系func(B::operator A())还是func(A::A(const B&))
重载运算符出现在表达式中时,由于使用a oper b的形式无法区分系成员函数(a.operator oper(b))还是非成员函数(operator oper(a, b)),因此会使重载函数的候选函数集扩大,导致二义性的可能性增加。
面向对象程序设计
虚函数
当基类希望它的派生类各自定义自身的(非静态)成员函数版本时,基类可以将这些函数通过添加关键字virtual声明为虚函数。
struct Quote {
virtual double net_price(std::size_t n) const;
};
任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前,而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。同样,派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。
派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,则新定义的这个函数与基类中原有的函数是相互独立的。此时,派生类的函数并没有覆盖掉基类中的版本。
在C++11新标准中可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B {
void f1(int) const override; // 正确:f1与基类中的f1匹配
void f2(int) override; // 错误:B没有形如f2(int)的函数
void f3() override; // 错误:f3不是虚函数
void f4() override; // 错误:B没有名为f4的函数
};
我们还能把某个函数指定为final,此时任何尝试覆盖该函数的操作都将引发错误。
sturct D2 : B {
void f1(int) const final; // 不允许后续的其他类覆盖f1(int)
};
struct D3 : D2 {
void f2(); // 正确:覆盖从间接基类B继承而来的f2
void f1(int) const; // 错误:D2已经将f1声明成final
};
和其他函数一样,虚函数也可以有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。
如果,我们希望对虚函数的调用不要进行动态绑定,而且强迫其执行虚函数的某个特定版本,则可使用作用域运算符实现。
动态绑定
动态绑定即用基类的引用或指针调用一个虚函数,这是多态性的一个基础条件。也即要实现动态绑定(多态性)有两个条件:其一,必须用基类的引用或指针进行调用相关函数;其二,所调用的函数必须是虚函数。如有
struct base {
virtual void print() { cout << "base" << endl; }
};
struct deni : public base {
void print() { cout << "deni" << endl; }
};
deni d; base b;
base &b1 = d, &b2 = b;
b1.print(); // 将打印deni
b2.print(); // 将打印base
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,则可以通过使用作用域运算符实现。
静态类型即表象类型,动态类型即实际类型,这发生在用基类的引用或指针定义变量时发生这种情况,不使用引用或指针时,静态类型与动态类型一致。如Base b;此时b的静态类型为Base,动态类型也为Base,而Base &b = d;此时,b的静态类型为Base,但动态类型则不一定,要看d的实际类型。
派生类
一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。对于静态成员,如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
对于静态成员,其遵循通用的访问控制,如果基类中的成员是public的,则派生类可以访问,如果基类中的成员是private的,则派生类无权访问。如果是可访问的,则既可以通过基类访问它,也可以通过派生类访问它。
class Base {
public:
static void statmem();
};
class Derived : public Base {
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmem(); // 正确:Base定义了statmem
Derived::statmem(); // 正确:Derived继承了statmem
derived_obj.statmem();// 正确:派生类的对象能访问基类的静态成员
statmem(); // 正确:通过this对象访问
}
因为在派生类对象中含有与基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。这种转换通常称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。
尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员,而必须使用基类的构造函数来初始化它的基类部分。每个类控制它自己的成员初始化过程。初始化顺序是首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final:
class NoDerived final { }; // NoDerived不能作为基类
类型转换与继承
不存在从基类向派生类的转换,同时在对象之间不存在类型转换。派生类向基类的自动转换只对指针或引用类型有效,在派生类类型对象和基类类型对象之间不存在这样的转换。另外也不存在从基类到派生类的隐式类型转换,即使一个基类指针或引用绑定在一个派生类对象上也不发生这样的转换:
Quote base;
Bulk_quote* bulkP = &base; // 错误:不能将基类转换成派生类
Bulk_quote& bulkRef = base; // 错误:不能将基类转换成派生类
Bulk_quote bulk;
Quote* itemP = &bulk; // 正确:动态类型是Bulk_quote
Bulk_quote *bulkP1 = itemP; // 错误:不能将基类转换成派生类
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或引用的静态类型来推断转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。同样,如果我们已知某个基类向派生类的转换是安全的,则我们可以用static_cast来强制覆盖掉编译器的检查工作。
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
抽象基类
纯虚函数是在一个虚函数的声明语句的分号前添加=0。其中,=0只能出现在类内部的虚函数声明语句处。纯虚函数无须定义,如果我们为纯虚函数提供定义,则函数体必须定义在类的外部。
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象。
6.访问控制与继承
protected除了可以被派生类访问而不能被普通用户直接访问外,还有一个重要性质,即派生类的成员或者友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。如果,一个派生类(及其友元)能访问基类对象的受保护成员,则我们只要定义一个派生类来简单规避掉protected提供的访问保护了。如下:
class Base {
protected:
int prot_mem; // protected成员
};
class Sneaky : public Base {
friend void clobber(Sneaky&); // 能访问Sneaky::prot_mem
friend void clobber(Base&); // 不能访问Base::prot_mem
int j; // j默认是private
};
// 正确:clobber能访问Sneaky对象的private和protected成员
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// 错误:clobber不能访问Base的protected成员
void clobber(Base &b) { b.prot_mem = 0; }
派生类向基类转换的可访问性
派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定D继承自B:
(1)只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或私有的,则用户代码不能使用该转换。
(2)不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
(3)如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用。
改变个别成员的可访问性
有时我们需要改变派生类继承的某个名字的访问级别,可以通过使用using声明来达到这一目的。
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base { // 系private继承
public:
using Base::size; // 访问符为public
protected:
using Base::n; // 访问符为protected
};
因为Derived使用了私有继承,因此继承而来的size和n默认情况下是Derived的私有成员。但通过使用using声明改变了这些成员的可访问性。改变之后,Derived的用户可以使用size成员,Derived的派生类将能使用n。需要注意的是,派生类只能为那些它可以访问的名字提供using声明。
继承中的类作用域
在编译时进行名字查找,因此名字是根据静态类型(即表象类型)决定的。如Base中只有成员size,而其派生类Derived中还有成员begin,则若Base &b = d;即使d为Derived对象,也不能通过b来调用size。
若派生类中定义了与基类中同名的成员,则该成员将隐藏基类中的同名成员,要访问基类中的同名成员需通过作用域访问符,如Base::size。
由于名字查找先于类型检查,因此,定义在派生类中的函数不会重载其基类中的成员,而是在派生类的作用域内隐藏了该基类成员。即使形参列表不同,也仍将隐藏。
struct Base {
int memfcn();
};
struct Derived : public Base {
int memfcn(int); // 隐藏基类的memfcn
};
Derived d; Base b;
b.memfcn(); // 调用Base::memfcn
d.memfcn(10); // 调用Derived::memfcn
d.memfcn(); // 错误:参数列表为空的memfcn被隐藏了
d.Base::memfcn(); // 正确:调用Base::memfcn
覆盖重载的函数
在不同类作用域的函数不能重载。如果派生类希望重载基类中的一组重载函数,则需要将这组函数全部重载,如果仅仅重新定义了其中一个,则就会隐藏基类中的同名其他函数。如果只需要重定义其中一个或几个,则可以先用using声明,using声明只声明函数名字,而无需将形参列表包含其中。通过这种方式将基类中的重载函数添加到派生类的作用域内,然后可以根据需要重载其中一个或几个。
7.派生类的拷贝控制成员
派生类构造函数在其初始化阶段不仅要初始化派生类自己的成员,还应该负责初始化派生类对象的基类部分成员。与此不同的是,派生类的析构函数只负责销毁自己部分的成员,无需显式调用基类的析构函数。初始化基类部分成员应当尊重基类的接口,也就是要使用对应的基类构造函数,这是通过在派生类构造函数的初始化列表中显式调用基类对应的构造函数完成的。同样的,在派生类的赋值运算符中,应当在函数体中显式调用基类的赋值运算符以完成对基类部分成员的赋值操作。
class D : public Base {
public:
D(const D &d): Base(d) // 拷贝基类成员
/*拷贝派生类部分成员*/ { ...... }
D(D &&d): Base(std::move(d)) // 移动基类成员
/*移动派生类部分成员*/ { ...... }
D& operator=(const D &d)
{
Base::operator=(d);
// 赋值派生类部分成员
return *this;
}
};
继承的构造函数
在C++11新标准中,派生类能够重用其直接基类定义的构造函数,这是通过将其直接基类的构造函数名进行using声明完成的。虽然,在普通情况下,using声明只是将某个名字在当前作用域可见,但是当using作用于构造函数时,using声明会令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。
class Bulk_quote : public Disc_quote {
public:
using Disc_quote::Disc_quote; // 继承Disc_quote的构造函数
};
上述using声明将使编译器为Bulk_quote类生成以下形式的构造函数:
derived(parms): base(args) {}
其中,derived是派生类的名字,base是基类的名字,parms是构造函数的形参列表,args将派生类构造函数的形参传递给基类的构造函数。就Bulk_quote类来说,继承的构造函数等价于:
Bulk_quote(const std::string &book, double price, std::size_t qty, double dic):
Disc_quote(book, price, qty, disc) {}
如果派生类还具有自己的成员,则这些成员将被默认初始化。
继承的构造函数的特点
和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。例如,基类对应的构造函数是私有的,则派生类继承了基类的构造函数的话,该构造函数在派生类中也是私有的。另外,一个using声明语句不能指定explicit或constexpr。如果基类的构造函数是explicit或constexpr,则继承的构造函数也拥有相同的属性。
当一个基类的构造函数具有默认实参时,这些实参并不会被继承,相反派生类将获得多个继承的构造函数。例如,如果基类有一个接受两个形参的构造函数,其中第二个形参具有默认值,则派生类将获得两个构造函数:一个构造函数接受两个形参(没有默认实参),另一个构造函数只接受一个形参,它对应于基类中最左侧的没有默认值的那个形参。
派生类并不能继承基类中的全部构造函数,不能继承的构造函数主要有以下两种情况:
派生类定义了自己的构造函数,而该构造函数与基类对应的构造函数具有相同的形参列表。此时,派生类自己定义的构造函数会替换继承而来的构造函数。
默认、拷贝和移动构造函数不会被继承。
容器和继承
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针)。这些指针的动态类型可能是基类类型,也可能是派生类类型
模板与泛型编程
函数模板
模板定义以关键字template开始,后跟一个模板参数列表,每一个类型参数前必须使用关键字class或typename,如:
template <typename T> int compare(const T &v1, const T &v2);
除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名(如int)而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所替代。这些值必须是常量表达式。
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{ return strcmp(p1, p2); }
当我们调用这个const引用版本的compare时:compare("hi", "mom"),不会自动将字符串转换为指针,编译器会使用字面常量大小来代替N和M,本例中即3替代N,4替代M(字符串末尾有空字符)。
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期,即普通局部变量或动态对象作为指针或引用非类型模板参数的实参。
inline和constexpr的函数模板
inline或constexpr说明符应放在模板参数列表之后,返回类型之前:
template<typename T> inline T min(const T&, const T);
模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
类模板
使用一个类模板时,必须提供额外信息,即显式模板实参列表,它们被绑定到模板参数,即如vector等需要用尖括号内提供类型。当要为类模板里声明别名时,需要用typename以指出这是一个类型名而非成员,如:
typedef typename std::vector<T>::size_type size_type;
类模板的成员函数
与其他任何类相同,我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) cosnt
{ …… }
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参,但在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:
template <typename T> class BlobPtr {
public:
BlobPtr(): curr(0) {} // 无需BlobPtr<T>(): curr(0) {}的形式
BlobPtr(Blob<T> &a, size_t sz = 0): wptr(a.data), curr(sz) {}
T& operator*() cosnt
{ auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p)为本对象指向的vector
}
BlobPtr& operator++();
BlobPtr& operator--();
private:
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string&) cosnt;
// 保存一个weak_ptr表示底层vector可能被销毁
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr; // 数组中的当前位置
};
在类模板外使用类模板名
template <typename T> BlobPtr<T> BlobPtr<t>::operator++(int)
{ …… }
在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。
类模板和友元
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
为了引用(类或函数)模板的一个特定实例,我们必须首先声明模板自身。一个模板声明包括模板参数列表:
// 前置声明,在Blob中声明友元所需要的
template <typename> class BlobPtr;
template <typename> class Blob; // 运算符==中的参数所需要的
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
// 其他成员定义与前述一致
};
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到
template <typename T> class Pal;
class C { // C是一个普通的非模板类
friend class Pal<C>; // 用类C实例化的Pal是C的一个友元
// Pal2的所有实例都是C的友元;这种情况无须前置声明
template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2本身是一个类模板
// C2每个实例将相同实例化的Pal声明为友元,Pal的模板声明须在作用域内
friend class Pal<T>;
// Pal2的所有实例都是C2的每个实例的友元,不需要前置声明
template <typename X> friend class Pal2;
// Pal3是一个非模板类,它是C2所有实例的友元,不需要Pal3的前置声明
friend class Pal3;
};
令模板自己的类型参数成为友元
在C++11中,可以将模板类型参数声明为友元:
template <typename Type> class Bar {
friend Type; // 将访问权限授予用来实例化Bar的类型
// ……
};
虽然,友元通常应该是一个类或是一个函数,但完全可以用一个内置类型来实例化Bar。这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化Bar这样的类。
模板类型别名
我们可以定义一个typedef来引用实例化的类:
typedef Blob<string> StrBlob;
新标准也允许我们为类模板定义一个类型别名:
template <typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一个pair<string, string>
当我们定义一个模板类型别名时,可以固定一个或多个模板参数:
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books是一个pair<string, unsigned>
类模板的static成员
类模板可以声明static成员:
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};
与任何其他static数据成员相同,模板类的每个static数据成员必须有且只有一个定义。但是,类模板的每个实例都有一个独有的static对象。因此,与定义模板的成员函数类似,我们将static数据成员也定义为模板(在模板外部定义):
template <typename T> size_t Foo<T>::ctr = 0; // 定义并初始化ctr
与非模板类的静态成员相同,我们可以通过类类型对象来访问一个类模板的static成员,也可以使用作用域运算符直接访问成员。当然,为了通过类来直接访问static成员,我们必须引用一个特定的实例:
Foo<int> fi; // 实例化Foo<int>类和static数据成员ctr
auto ct = Foo<int>::count();// 实例化Foo<int>::count
ct = fi.count(); // 使用Foo<int>::count
ct = Foo::count(); // 错误:使用哪个模板实例的count?
模板参数
模板参数与作用域
模板参数遵循普通的作用域规则。一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; // tmp的类型为模板参数A的类型,而非double
double B; // 错误:重声明模板参数B
}
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
template <typename V, typename V> ...// 错误:非法重用模板参数名V
模板声明
模板声明必须包含模板参数:
// 声明但不定义compare和Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
使用类的类型成员
默认情况下,C++语言假定通过作用域访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字typename来实现这一点:
template <typename T> typename T::value_type top(const T &c)
{
if(!c.empty())
return c.back();
return typename T::value_type();
}
当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。
默认模板实参
在新标准中,我们可以为函数和类模板提供默认实参(旧标准只能给类模板提供默认实参)。
// compare有一个默认实参less<T>和一个默认函数实参F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if(f(v1, v2)) return -1;
if(f(v2, v1)) return 1;
return 0;
}
上述代码中,名为F,表示可调用对象的类型,并定义了一个新的函数参数f,绑定到一个可调用对象上,是类型F的一个默认初始化对象(即less<T>对象)。
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
模板默认实参与类模板
无论何时使用一个类模板,都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化而来。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template <typename T = int> class Numbers { // T默认为int
public:
Numbers(T v = 0): val(v) {}
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // 空<>表示我们希望使用默认类型
成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
在实例化中,成员模板的实参类型由编译器推断得到,因此无须象类模板那样用一对尖括号指出。
在类模板中,与类模板的普通函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:
template <typename T> // 类的类型参数
template <typaname It> // 构造函数的类型参数,It的类型由实参推断
Blob<T>::Blob(It b, It e): ... {}
控制实例化
由于模板被使用时才会进行实例化,因此相同的实例可能存在于多个对象文件中,这容易造成额外开销。在新标准中,可以通过显式实例化来避免这种开销。一个显式实例化有如下形式:
extern template class Blob<string>; // 实例化声明
template int compare(const int&, const int&); // 实例化定义
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。
模板实参推断
对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推断。
类型转换与模板类型参数
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则,只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
与往常一样,顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:
const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
template <typename T> T fobj(T, T); // 实参被拷贝
template <typename T> T fref(const T&, const T&); // 引用
string s1("a value");
const string s2("another value");
fobj(s1, s2); // 调用fobj(string, string);const被忽略
// 调用fref(const string&, const string&),将s1转换为const是允许的
fref(s1, s2);
int a[10], b[20];
fobj(a, b); // 调用fobj(int *, int *)
fref(a, b); // 错误:数组类型不匹配
使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,则调用就是错误的。如针对前面的compare模板:
long lng;
compare(lng, 1024); // 错误:不能实例化compare(long, int)
如果希望允许对函数实参进行正常的类型转换,可以将函数模板定义为两个类型参数:
template <typename A, typename B>
int flexibleCompare(const A &v1, const B &v2)
{
if(v1 < v2) return -1;
if(v2 < v1) return 1;
return 0;
}
long lng;
flexibleCompare(lng, 1024);// 正确:调用flexibleCompare(long, int)
对于函数模板中普通类型(非模板类型参数)定义的参数,则可以应用正常的类型转换,如算术转换等。
指定显式模板实参
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
此处,T1没有任何函数实参的类型可用来推断T1的类型,每次调用sum必须为T1提供一个显式模板实参:
auto val1 = sum<int>(12, 10L); // int sum(int, long)
显式模板实参按由左至右的顺序与对应的模板参数匹配;第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
template <typename T1, typename T2, typename T3> T3 sum(T2, T1);
此时,我们必须为sum的三个形参指定所有实参:
auto val2 = sum<long long, int, long>(i, lng);
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换。
尾置返回类型与类型转换
当我们并不知道返回结果的准确类型,但知道所需类型是所处理的某种数据的元素类型时,使用尾置返回类型比较方便(decltype):
template <typename It> auto fcn(It beg, It end) -> decltype(*beg)
{
// 处理序列
return *beg; // 返回序列中一个元素的引用
}
上述返回引用类型,为了在上述情形中返回值类型,则可以使用标准库的类型转换模板,定义在头文件type_traits中。这个头文件中的类通常用于所谓的模板元程序设计。remove_reference(去掉引用特性)模板有一个模板类型参数和一个名为type的public成员。如果我们用一个引用类型实例化remove_reference,则type将表示被引用的类型。例如,如果我们实例化remove_reference<int&>,则type成员将是int。更一般地,给定一个迭代器beg:remove_reference<decltype(*beg)>::type将获得beg引用的元素类型:decltype(*beg)返回元素类型的引用类型。remove_reference::type脱去引用,剩下元素类型本身。
组合使用remove_reference、尾置返回及decltype,我们就可以在函数中返回元素值的拷贝:
// 为了使用模板参数的成员,必须用typename
template <typename It> auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
// 处理序列
return *beg; // 返回序列中的一个元素的拷贝
}
注意,type是一个类的成员,而该类依赖于一个模板参数。因此,我们必须在返回类型的声明中使用typename来告知编译器,type表示一个类型。
标准类型转换模板的操作 |
||
对Mod<T>,其中Mod为 |
若T为 |
则Mod<T>::type为 |
remove_reference |
X&或X&& 否则 |
X T |
add_const |
X&、const X或函数 否则 |
T const T |
add_lvalue_reference |
X& X&& 否则 |
T X& T& |
add_rvalue_reference |
X&或X&& 否则 |
T T&& |
remove_pointer |
X* 否则 |
X T |
make_signed |
unsigned X 否则 |
X T |
make_unsigned |
带符号类型 否则 |
unsigned X T |
remove_extent |
X[n] 否则 |
X T |
remove_all_extents |
X[n1][n2]... 否则 |
X T |
函数指针和实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。例如,假定我们有一个函数指针,它指向的函数返回int,接受两个参数,每个参数都是指向const int的引用。我们可以使用该指针指向compare的一个实例:
template <typename T> int compare(const T&, const T&);
// pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
pf1中参数的类型决定了T的模板实参的类型。在本例中,T的模板实参类型为int。指针pf1指向compare的int版本实例。如果不能从函数指针类型确定模板实参,则产生错误:
// func的重载版本;每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // 错误:使用compare的哪个实例?
我们可以通过使用显式模板实参来消除func调用的歧义:
func(compare<int>); // 传递compare(const int&, const int&)
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
模板实参推断和引用
template <typename T> void f(T &p);
其中函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:编译器会应用正常的引用绑定规则:const是底层的,不是顶层的。
当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&),绑定规则告诉我们,只能传递给它一个左值。实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型:
template <typename T> void f1(T&); // 实参必须是一个左值
f1(i); // i是一个int;模板参数类型T是int
f1(ci); // ci是一个const int;模板参数T是const int
f1(5); // 错误:传递给一个&参数的实参必须是一个左值
如果一个函数参数的类型是const T&,则我们可以传递给它任何类型的实参,包括字面值,即上述f1(5)的形式也正确,但是T的类型推断结果不会是一个const类型。
当一个函数参数是一个右值引用时,绑定规则告诉我们可以传递给它一个右值。此时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型:
template <typename T> void f3(T&&);
f3(42); // 实参是一个int类型的右值;模板参数T是int
注意:通常一般我们不能将右值引用绑定到一个左值上,但是对f3来说,可以用一个int对象i去调用,即f3(i)也是合法的。这是因为C++语言在正常的绑定规则之外定义了两个例外规则,允许这种绑定。这两种例外规则如下:
第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值(如i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int。也就是说,此时在函数模板中函数体可以对i的值进行改变。
通常,我们不能(直接)定义一个引用的引用。但是,通过类型别名或通过模板类型参数间接定义是可以的。
在上述定义引用的引用时,有第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了"折叠"。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用,且只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即一个给定类型X:只有类型X&& &&会折叠成类型X&&,其余如X& &、X& &&、X&& &都会折叠成类型X&。
上述两个规则导致了以下重要结果:如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值,且如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)。
在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。目前,使用右值引用的函数模板通常使用以前的方式来进行重载:
template <typename T> void f(T&&); // 绑定到非const右值
template <typename T> void f(const T&); // 左值和const右值
与非模板函数一样,第一个版本将绑定到可修改的右值,而第二个版本将绑定到左值或const右值。
理解std::move
std::move的定义
// 在返回类型和类型转换中也要用到typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
通常情况下,static_cast只能用于其他合法的类型转换,但是虽然不能将一个左值隐式地转换为右值引用,但从一个左值static_cast到一个右值引用是允许的。
转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
这个函数一般情况下工作良好,但当我们希望用它调用一个接受引用参数的函数时就会出现问题:
void f(int v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}
在这段代码中,f改变了绑定到v2的实参的值。但是,如果我们通过flip1调用f,f所做的改变就不会影响实参:
f(42, i); // f改变了实参i
flip1(f, j, 42); // 通过flip1调用f不会改变j
通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
与较早版本一样,如果我们调用flip2(f, j, 42),将传递给参数t1一个左值j。但是,在flip2中,推断出的T1的类型为int&,这意味着t1的类型会折叠为int&。由于是引用类型,t1被绑定到j上。当flip2调用f时,f中的引用参数v2被绑定到t1,也就是被绑定到j。当f递增v2时,它也同时改变了j的值。
如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。
这个版本的flip2解决了一半问题。它对于接受一个左值引用的函数工作良好,但不能用于接受右值引用参数的函数。例如:
void g(int &&i, int &j)
{
cout << i << " " << j << endl;
}
如果我们试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。即使我们传递一个右值给flip2:
flip2(g, i, 42); // 错误:不能从一个左值实例化int&&
传递给g的将是flip2中名为t2的参数。函数参数与其他任何变量一样,都是左值表达式。因此,flip2中对g的调用将传递给g的右值引用参数一个左值。
在调用中使用std::forward保持类型信息
我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。类似move,forward定义在头文件utility中。与move不同,forward必须通过显式模板实参来调用。forward返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&。
通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性:
template <typename Type> intermediary(Type &&arg)
{
// 注意forward的显式模板实参调用
finalFcn(std::forward<Type>(arg));
}
本例中我们使用Type作为forward的显式模板实参,它是从arg推断出来的。由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。如果实参是一个右值,则Type是一个普通(非引用)类型,forward<Type>将返回Type&&。如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型。在此情况下,返回类型是一个指向左值引用类型的右值引用。再次对forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节。
使用forward,我们可以再次重写翻转函数:
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
如果我们调用flip(g, i, 42),i将以int&类型传递给g,42将以int&&类型传递给g。
与std::move相同,对std::forward不使用using声明是一个好做法。
重载与模板
函数模板匹配规则的特殊点:
对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
-如果同样好的函数中只有一个是非模板函数,则选择此函数。
-如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
-否则,此调用有歧义。
template <typename T> string debug_rep(const T &t)
{
ostringstream ret; // 参见第8章
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str(); // 返回ret绑定的string的一个副本
}
template <typename T> string debug_rep(T *p)
{
ostringstream ret;
ret << "pointer: " << p; // 打印指针本身的值
if(p)
ret << " " << debug_rep(*p); // 打印p指向的值
else
ret << " null pointer"; // 或指出p为空
return ret.str(); // 返回ret绑定的string的一个副本
}
如果我们如下调用:
string s("hi");
cout << debug_rep(s) << endl;
此时,只有第一个版本的debug_rep是可行的,因为第二个debug_rep要求为指针参数。
如果我们用一个指针调用debug_rep:
cout << debug_rep(&s) << endl;
此时,两个函数都生成可行的实例:
debug_rep(cosnt string *&),T被绑定到string *。
debug_rep(string *),T被绑定到string。
但第二个版本的实例是此调用的精确匹配,第一个版本的实例需要进行普通指针到const指针的转换(且第二个版本更特例化——只适用于指针)。
可变参数模板
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示另个或多个模板参数;函数参数包:表示另个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:
// Args是一个模板参数包;rest是一个函数参数包
// Args表示零个或多个模板类型参数,rest表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
声明了foo是一个可变参数函数模板,它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。foo的函数参数列表包含一个const &类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。
与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。例如,给定下面的调用:
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // 包中有三个参数
foo(s, 42, "hi"); // 包中有两个参数
foo(d, s); // 包中有一个参数
foo("hi"); // 空包
编译器会为foo实例化出四个不同的版本:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
在每个实例中,T的类型都是从第一个实参的类型推断出来的。剩下的实参(如果有的话)提供函数额外实参的数目和类型。
sizeof...运算符
当我们需要知道包中有多少元素时,可以使用sizeof...运算符。sizeof...运算符返回一个常量表达式,而且不会对其实参求值:
template <typename ... Args> void g(Args ... args)
{
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(args) << endl; // 函数参数的数目
}
编写可变参数函数模板
可变参数函数模板通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
// 用来终止递归并打印最后一个元素的函数
template <typename T> ostream& print(ostream &os, const T &t)
{
return os << t; // 包中最后一个元素之后不打印分隔符
}
// 包中除了最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", "; // 打印第一个实参
return print(os, rest...); // 递归调用,打印其他实参
}
若给定:print(cout, i, s, 42); //包中有两个参数
递归执行如下:
调用 t rest...
print(cout, i, s, 42) i s, 42
print(cout, s, 42) s 42
print(cout, 42) 调用非可变参数版本的print,终止递归
包扩展
当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...)来触发扩展操作。
template <typename T, typename... Args>
// 扩展Args
ostream& print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", ";
return print(os, rest...); // 扩展rest
}
对Args的扩展中,编译器将模式const Args&应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&。例如:
print(cout, i, s, 42); // 包中有两个参数
最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:
ostream& print(ostream&, const int&, const string&, const int&);
第二个扩展发生在对print的递归调用中,此时模式是函数参数包的名字(rest),此模式扩展出一个由包中元素组成的、逗号分隔的列表,此调用等价于:
print(os, s, 42);
更复杂的包扩展
我们还可以对包进行如下扩展:
// 在print调用中对每个实参调用debug_rep
template <typename... Args>
ostream& errMsg(ostream &os, const Args&... rest)
{
// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an));
return print(os, debug_rep(rest)...);
}
这个print调用使用了模式debug_rep(rest)。此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep。扩展结果将是一个逗号分隔的debug_rep调用列表。即,如下调用:
errMsg(cerr, fcnName, code.num(), otherData, "other");
就好像我们如下编写代码一样:
print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData), debug_rep("other"));
与之相对,下面的模式会导致编译失败:
// 将包传递给debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 错误:此调用无匹配函数
转发参数包
我们可以组合使用可变参数模板与forward机制来编写函数,实现将其参数不变地传递给其他函数。
template <typename... Args>
inline void StrVec::emplace_back(Args&&... args)
{
chk_n_alloc(); // 如果需要的话重新分配StrVec内存空间
// 注意以下这种显式制定实参类型的方式为扩展模板参数包
alloc.construct(first_free++, std::forward<Args>(args)...);
}
由上可见,construct成员函数的参数是可变参数模板,而不是固定数量的参数数目。
注意,(1)要保持类型信息被正确传递,需要用右值引用类型;(2)这里对forward使用显式指定实参类型时,是直接使用模板参数包的名字Args,通过forward调用,既扩展了模板参数包Args,也扩展了函数参数包args。
模板特例化
定义函数模板特例化
当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(<>)。空尖括号对指出我们将为原模板的所有模板参数提供实参:
// compare的特殊版本,处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const &ps)
{
return strcmp(p1, p2);
}
当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模板的一个特殊实例提供了定义。重要的是要弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。
最佳实践:模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
类模板特例化
作为举例,对标准库hash模板定义一个特例化版本,可以用它来将Sales_data对象保存在无序容器中。默认情况下,无序容器使用hash<key_type>来组织其元素。为了让我们自己的数据类型也能使用这种默认组织方式,必须定义hash模板的一个特例化版本。一个特例化hash类必须定义:
一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个size_t。
两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型。
默认构造函数和拷贝赋值运算符(可以隐式定义)。
在定义此特例化版本的hash时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。我们可以向命名空间添加成员。为了达到这一目的,首先必须打开命名空间:
// 打开std命名空间,以便特例化std::hash
namespace std {
template <> // 我们正在定义一个特例化版本,模板参数为Sales_data
struct hash<Sales_data>
{
// 用来散列一个无序容器的类型必须要定义下列类型
typedef size_t result_type;
// 默认情况下,此类型需要==
typedef Sales_data argument_type;
size_t operator()(const Sales_data &s) const;
// 我们的类使用合成的拷贝控制成员和默认构造函数
};
size_t hash<Sales_data>::operator()(const Sales_data &s) cosnt
{
return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // 关闭std命名空间;注意:右花括号之后没有分号
我们的hash<Sales_data>定义以template<>开始,指出我们正在定义一个全特例化的模板。我们正在特例化的模板名为hash,而特例化版本为hash<Sales_data>。接下来的类成员是按照特例化hash的要求而定义的。
类似其他任何类,我们可以在类内或类外定义特例化版本的成员,本例中就是在类外定义的。
由于hash<Sales_data>使用Sales_data的私有成员,我们必须将它声明为Sales_data的友元:
template <class T> class std::hash; // 友元声明所需要的
class Sales_data {
friend class std::hash<Sales_data>;
// 其他成员定义,如前
};
这段代码指出特殊实例hash<Sales_data>是Sales_data的友元。由于此实例定义在std命名空间中,我们必须记得在friend声明中应使用std::hash。
为了让Sales_data的用户能使用hash的特例化版本,我们应该在Sales_data的头文件中定义该特例化版本。
类模板部分特例化
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
注意:我们只能部分特例化类模板,而不能部分特例化函数模板。
对于标准库remove_reference类型,该模板是通过一系列的特例化版本来完成其功能的:
// 原始的、最通用的版本
template <class T> struct remove_reference {
typedef T type;
};
// 部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> // 左值引用
{ typedef T type; };
template <class T> struct remove_reference<T&&> // 右值引用
{ typedef T type; };
第一个模板定义了最通用的模板。它可以用任意类型实例化;它将模板参数作为type成员的类型。接下来的两个类是原始模板的部分特例化版本。
由于一个部分特例化版本本质是一个模板,与往常一样,我们首先定义模板参数。类似任何其他特例化版本,部分特例化版本的名字与原模板的名字相同。对每个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。在类名之后,我们为要特例化的模板参数指定实参,这些实参列于模板名之后的尖括号中。这些实参与原始模板中的参数按位置对应。
部分特例化版本的模板参数列表是原始模板的参数列表的一个子集或者是一个特例化版本。在本例中,特例化版本的模板参数的数目与原始模板相同,但是类型不同。两个特例化版本分别用于左值引用和右值引用类型:
int i;
// decltype(42)为int,使用原始模板
remove_reference<decltype(42)>::type a;
// decltype(i)为int&,使用第一个(T&)部分特例化版本
remove_reference<decltype(i)>::type b;
// decltype(std::move(i))为int&&,使用第二个(T&&)部分特例化版本
remove_reference<decltype(std::move(i))>::type c;
三个变量a、b和c均为int类型。
特例化成员而不是类
我们可以只特例化特定成员函数而不是特例化整个模板。例如,如果Foo是一个模板类,包含一个成员Bar,我们可以只特例化该成员:
template <typename T> struct Foo {
Foo(const T &t = T()): mem(t) {}
void Bar() { /*...*/ }
T mem;
// 其他成员
};
template <> // 我们正在特例化一个模板
void Foo<int>::Bar() // 我们正在特例化Foo<int>的成员Bar
{
// 进行应用于int的特例化处理
}
标准库特殊设施
17.1 tuple类型
tuple类型类似于python中的元组类型。不同tuple类型的成员类型也不相同,但一个tuple可以有任意数量的成员。每个确定的tuple类型的成员数目是固定的,但一个tuple类型的成员数目可以与另一个tuple类型不同。
tuple类型及其伴随类型和函数都定义在头文件tuple中。
tuple支持的操作 |
|
tuple<T1, T2, ... Tn> t; |
t是一个tuple,成员数为n,第i个成员的类型为Ti。所有成员都进行值初始化 |
tuple<T1, T2, ...Tn> t(v1, v2, ..., vn); |
t是一个tuple,成员类型为T1...Tn,每个成员用对应的初始值vi进行初始化。此构造函数是explicit的 |
make_tuple<v1, v2, ..., vn) |
返回一个用给定初始值初始化的tuple。tuple的类型从初始值的类型推断 |
t1 == t2 |
当两个tuple具有相同数量的成员且成员对应相等时,两个tuple相等。这两个操作使用成员的==运算符来完成。一旦发现某对成员不等,接下来的成员就不用比较 |
t1 != t2 |
|
t1 relop t2 |
tuple的关系运算使用字典序。两个tuple必须具有相同数量的成员。使用<运算符比较t1的成员和t2中的对应成员 |
get<i>(t) |
返回t的第i个数据成员的引用:如果t是一个左值,结果是一个左值引用;否则,结果是一个右值引用。tuple的所有成员都是public的 |
tuple_size<tupleType>::value |
一个类模板,可以通过一个tuple类型来初始化。它有一个名为value的pulic constexpr static数据成员,类型为size_t,表示给定tuple类型中成员的数量 |
tuple_element<i, tupleType>::type |
一个类模板,可以通过一个整型常量和一个tuple类型来初始化。它有一个名为type的public成员,表示给定tuple类型中指定成员的类型 |
tuple的非默认构造函数为explicit的,因此必须使用直接初始化:
tuple<string, vector<double>, int, list<int>>
someVal("Hi", {3.14, 2.7}, 42, {0, 1, 2, 3})
tuple<size_t, size_t> TwoD = {1, 2}; // 错误
tuple<size_t, size_t> TwoD{1, 2}; // 正确
类似make_pair函数,标准库定义了make_tuple函数用来生成tuple对象:
auto item = make_tuple("Hi", 3.21, 42);
这里用初始值来推断tuple的类型,此处item是一个tuple,类型为:
tuple<const char *, double, int>。
要访问一个tuple的成员,就要使用一个名为get的标准库函数模板。为了使用get,必须指定一个显式模板实参,它指出我们要访问第几个成员:
auto str = get<0>(item); // 返回item的第1个成员
尖括号中的值必须是整型常量表达式。
如果不知道一个tuple准确的类型细节信息,可以用两个辅助类模板来查询tuple成员的数量和类型:
typedef decltype(item) trans; // trans是item的类型
// 返回trans类型对象中成员的数量
size_t sz = tuple_size<trans>::value; // 返回3
// cnt的类型与item中第2个成员相同
tuple_element<1, trans>::type cnt = get<1>(item);// cnt是一个int
tuple的一个常见用途是从一个函数返回多个值。
17.2 bitset类型
bitset类是一个类模板,具有固定的大小,定义在头文件bitset中。当定义一个bitset时,需要声明它包含多少个二进制位:
bitset<32> bitvec(1U); // 32位;低位为1,其他位为0
其位数大小必须是一个常量表达式。
初始化bitset的方法 |
|
bitset<n> b; |
b有n位;每一位均为0。此构造函数是一个constexpr |
bitset<n> b(u); |
b是unsigned long long值u的低n位的拷贝。如果n大于unsigned long long的大小,则b中超出unsigned long long的高位被置为0。此构造函数是一个constexpr |
bitset<n> b(s, pos, m, zero, one); |
b是string s从位置pos开始m个字符的拷贝。s只能包含字符zero或one;如果s包含任何其他字符,构造函数会抛出invalid_argument异常。字符在b中分别保存为zero和one。pos默认为0,m默认为string::npos,zero默认为'0',one默认为'1' |
bitset<n> b(cp, pos, m, zero, one |
与上一个构造函数相同,但从cp指向的字符数组中拷贝字符。如果未提供m,则cp必须指向一个C风格字符串。如果提供了m,则从cp开始必须至少有m个zero或one字符 |
接受一个string或一个字符指针的构造函数是explicit的。在新标准中增加了为0和1指定其他字符的功能。 |
17.2.2 bitset操作
bitset操作 |
|
b.any() |
b中是否存在置位的二进制位 |
b.all() |
b中所有位是否都置位 |
b.none() |
b中是否无置位 |
b.count() |
b中置位的位数 |
b.size() |
一个constexpr函数,返回b中的位数 |
b.test(pos) |
若pos位置的位是置位的,返回true,否则返回false |
b.set(pos, v) |
将位置pos处的位置设置为bool值v。v默认为true。若未提供实参,则将b中所有位置位 |
b.set() |
|
b.reset(pos) |
将位置pos处的位复位或将b中所有位复位 |
b.reset() |
|
b.flip(pos) |
改变pos处的位的状态或改变b中每一位的状态 |
b.flip() |
|
b[pos] |
访问b中位置pos处的位,置位返回true,否则返回false |
b.to_ulong() |
返回一个unsigned long或一个unsigned long long值,其位模式与b相同。如果b中位模式不能放入指定的结果类型,则抛出一个overflow_error异常 |
b.to_ullong() |
|
b.to_string(zero, one) |
返回一个string,表示b中的位模式。zero和one的默认值分别为0和1,用来表示b中的0和1 |
os << b |
将b中二进制流打印为字符1或0,打印到流os |
is >> b |
从is中读取字符存入b。当下一个字符不是1或0时,或是已经读入b.size()个位时,读取过程停止 |
17.3 正则表达式
正则表达式库定义在头文件regex中。
正则表达式库组件 |
|
regex |
表示有一个正则表达式类 |
regex_match |
将一个字符序列与一个正则表达式匹配 |
regex_search |
寻找第一个与正则表达式匹配的子序列 |
regex_replace |
使用给定格式替换一个正则表达式 |
sregex_iterator |
迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串 |
smatch |
容器类,保存在string中搜索的结果 |
ssub_match |
string中匹配的子表达式的结果 |
regex类表示一个正则表达式。除了初始化和赋值之外,regex还支持其他一些操作。
函数regex_match和regex_search确定一个给定字符序列与一个给定regex是否匹配。如果整个输入序列与表达式匹配,则regex_match函数返回true;如果输入序列中一个子串与表达式匹配,则regex_search函数返回true。
regex_match和regex_search的参数 |
|
注意:这些操作返回bool值,指出是否找到匹配 |
|
(seq, m, r, mft) |
在字符序列seq中查找regex对象r中的正则表达式。seq可以是一个string、表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针 m是一个match对象,用来保存匹配结果的相关细节。m和seq必须具有兼容的类型 mft是一个可选的regex_constants::match_flag_type值。它们会影响匹配过程 |
(seq, r, mft) |
17.3.1 使用正则表达式库
默认情况下,regex使用的正则表达式语言是ECMAScript。在ECMAScript中,模式[^c]表示匹配任意不是'c'的字符,模式[[::alpha:]]匹配任意字母,符号+和*分别表示希望"一个或多个"或"零个或多个"匹配。故:
string pattern("[^c]ei");
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern);
smatch results;
string test_str = "receipt freind theif receive";
if(regex_search(test_str, results, r))
cout << results.str() << endl;
上述代码表示我们希望匹配中间含除字符c开头以外的包含ei等3个字符的单词。上述将打印freind。
指定regex对象的选项
当我们定义一个regex或是对一个regex调用assign为其赋予新值时,可以指定一些标志来影响regex如何操作。这些标志控制regex对象的处理过程。默认情况下,ECMAScript标志被设置,从而regex会使用ECMA-262规范,这也是很多Web浏览器所使用的正则表达式语言。
regex(和wregex)选项 |
|
regex r(re) |
re表示一个正则表达式,它可以是一个string、一个表示字符范围的迭代器对、一个指向空字符结尾的字符数组的指针、一个字符指针和一个计数器或是一个花括号包围的字符列表。f是指出对象如何处理的标志。f通过下面列出的值来设置。如果未指定f,其默认值为ECMAScript |
regex r(re, f) |
|
r1 = re |
将r1中的正则表达式替换为re。re表示一个正则表达式,它可以是另一个regex对象、一个string、一个指向空字符结尾的字符数组的指针或是一个花括号包围的字符列表 |
r1.assign(re, f) |
与使用赋值运算符(=)效果相同;可选标志f也与regex构造函数中对应的参数含义相同 |
r.mark_count() |
r中子表达式的数目 |
r.flags() |
返回r的标志集 |
注:构造函数和赋值操作可能抛出类型为regex_error的异常 |
|
定义regex时指定的标志 |
|
定义在regex和regex_constants::syntax_option_type中 |
|
icase |
在匹配过程中忽略大小写 |
nosubs |
不保存匹配的子表达式 |
optimize |
执行速度优先于构造速度 |
ECMAScript |
使用ECMA-262指定的语法 |
basic |
使用POSIX基本的正则表达式语法 |
extended |
使用POSIX扩展的正则表达式语法 |
awk |
使用POSIX版本的awk语言的语法 |
grep |
使用POSIX版本的grep的语法 |
egrep |
使用POSIX版本的egrep的语法 |
regex r("[[:alpha:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results;
string filename;
while(cin >> filename)
if(regex_search(filename, results, r))
cout << results.str() << endl;
上述代码中,用于查找C++格式文件的文件名,\\.中,其中第一个\表示C++中对特殊字符\的处理,而.本来匹配任意字符,加上一个反斜杠后表示.本身,此处也即文件格式名前的.号。icase表示匹配过程忽略大小写。上述r对象即表示希望匹配一个或多个字母或数字后接一个句点再接三个文件扩展名之一。
一个正则表达式的语法是否正确是在运行时解析的。
如果我们编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常。其有一个what操作来描述发生了什么错误。regex_error还有一个名为code的成员,用来返回某个错误类型对应的数值编码,该值是由具体实现定义的。
try{
regex r("[[:alpha:]+\\.(cpp|cxx|cc)$", regex::icase);
} catch (regex_error e)
{
cout << e.what() << "\ncode : " << e.code() << endl;
}
该程序在gnu上程序生成:
regex_error
code : 4
正则表达式的错误类型 |
|
定义在regex和regex_constants::error_type中 |
|
error_collate |
无效的元素校对请求 |
error_ctype |
无效的字符类 |
error_escape |
无效的转义字符或无效的尾置转义 |
error_backref |
无效的向后引用 |
error_back |
不匹配的方括号([或]) |
error_paren |
不匹配的小括号((或)) |
error_brace |
不匹配的花括号({或}) |
error_badbrace |
{}中无效的范围 |
error_space |
内存不足,无法处理此正则表达式 |
error_badrepeat |
重复字符(*、?、+或{)之前没有有效的正则表达式 |
error_complexity |
要求的匹配过于复杂 |
error_stack |
栈空间不足,无法处理匹配 |
我们可以搜索多种类型的输入序列。输入可以是普通char数据或wchar_t数据,字符可以保存在标准库string中或是char数组中(或是宽字符版本,wstring或wchar_t数组中)。RE为这些不同的输入序列类型都定义了对应的类型。
正则表达式库类 |
|
如果输入序列类型 |
则使用正则表达式类 |
string |
regex、smatch、ssub_match和sregex_iterator |
const char * |
regex、cmatch、csub_match和cregex_iterator |
wstring |
wregex、wsmatch、wssub_match和wsregex_iterator |
const wchar_t * |
wregex、wcmatch、wcsub_match和wcregex_iterator |
17.3.2 匹配与Regex迭代器类型
我们可以使用sregex_iterator来获得所有匹配。regex迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex对象上。每种不同输入序列类型都有对应的特殊regex迭代器类型。
sregex_iterator操作 |
|
这些操作也适用于cregex_iterator、wsregex_iterator和wcregex_iterator |
|
sregex_iterator it(b, e, r); |
一个sregex_iterator,遍历迭代器b和e表示的string。它调用sregex_search(b, e, r)将it定位到输入中的第一个匹配的位置 |
sregex_iterator end; |
sregex_iterator的尾后迭代器 |
*it |
根据最后一个调用regex_search的结果,返回一个smatch对象的引用或一个指向smatch对象的指针 |
it-> |
|
++it |
从输入序列当前匹配位置开始调用regex_search。前置版本返回递增后迭代器;后置版本返回旧值 |
it++ |
|
it1 = it2 |
如果两个sregex_iterator都是尾后迭代器,则它们相等。两个非尾后迭代器是从相同的输入序列和regex对象构造,则相等 |
it1 != it2 |
// 查找前一个字符不是c的字符串ei
string pattern("[^c]ei");
// 我们想要包含pattern的单词的全部内容
pattern = "[[:alpha:]]*" + pattern + "[[:alpha]]*";
regex r(pattern, regex::icase); // 匹配时忽略大小写
// 它将反复调用regex_search来寻找文件中的所有匹配
for(sregex_iterator it(file.begin(), file.end(), r), end_it;
it != end_it; ++it)
cout << it->str() << endl; // 匹配的单词
smatch操作 |
|
这些操作也适用于cmatch、wsmatch、wcmatch和对应的csub_match、wssub_match和wcsub_match |
|
m.ready() |
如果已经通过调用regex_search或regex_match设置了m,则返回true;否则返回false。如果ready返回false,则对m进行操作是未定义的 |
m.size() |
如果匹配失败,则返回0;否则返回最近一次匹配的正则表达式中子表达式的数目 |
m.empty() |
若m.size()为0,则返回true |
m.prefix() |
一个ssub_match对象,表示当前匹配之前的序列 |
m.suffix() |
一个ssub_match对象,表示当前匹配之后的部分 |
m.format(...) |
替换格式,见表"正则表达式替换操作" |
在接受一个索引的操作中,n的默认值为0且必须小于m.size()。 第一个子匹配(索引为0)表示整个匹配 |
|
m.length(n) |
第n个匹配的子表达式的大小 |
m.position(n) |
第n个子表达式距序列开始的距离 |
m.str(n) |
第n个子表达式匹配的string |
m[n] |
对应第n个子表达式的ssub_match对象 |
m.begin(), m.end() |
表示m中sub_match元素范围的迭代器。与往常一样,cbegin和cend返回const_iterator |
m.cbegin(), m.cend() |
17.3.3 使用子表达式
正则表达式中的模式通常包含一个或多个子表达式。一个子表达式是模式的一部分,本身也具有意义。正则表达式语法通常用括号表示子表达式,如:
regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$", regex::icase);
上述模式包含2个括号括起来的子表达式:
([[:alnum:]]+),匹配一个或多个字符的序列
(cpp|cxx|cc),匹配文件扩展名
子表达式用于数据验证
子表达式的一个常见用途是验证必须匹配特定格式的数据。例如,美国的电话号码有十位数字,包含一个区号和一个七位的本地号码。区号通常放在括号里,但这并不是必需的。剩余七位数字可以用一个短横线、一个点或是一个空格分隔,但也可以完全不用分隔符。我们可能希望接受任何这种格式的数据而拒绝任何其他格式的数。因此,我们可分两步实现这一目标:首先,用一个正则表达式找到可能是电话号码的序列,然后再调用一个函数来完成数据验证。
ECMAScript正则表达式语言的一些特性:
\{d}表示单个数字而\{d}{n}则表示一个n个数字的序列。(如,\{d}{3}匹配三个数字的序列)
在方括号中的字符集合表示匹配这些字符中任意一个。(如,[-. ]匹配一个短横线或一个点或一个空格。注意,点在括号中没有特殊含义)
后接'?'的组件是可选的。(如,\{d}{3}[-. ]?\{d}{4}匹配这样的序列:开始是3个数字,后接一个可选的短横线或点或空格,然后是4个数字。此模式可以匹配555-0132或555.0132或555 0132或5550132)
类似C++,ECMAScript使用反斜线表示一个字符本身而不是特殊含义。由于我们的模式包含括号,而括号是ECMAScript中的特殊字符,因此我们必须用\(和\)来表示括号是我们的模式的一部分而不是特殊字符。
// 查找美国电话号码
string phone =
"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
regex r(phone); // regex对象,用于查找我们的模式
smatch m;
string s;
// 从输入文件中读取每条记录
while (getline(cin, s)) {
for(sregex_iterator it(s.begin(), s.end(), r), end_it;
it != end_it; ++it)
if(valid(*it))
cout << "valid: " << it->str() << endl;
else
cout << "not valid: " << it->str() << endl;
}
使用子匹配操作
每个smatch对象会包含8个ssub_match元素。位置[0]的元素表示整个匹配;元素[1]...[7]表示每个对应的子表达式。
子匹配操作 |
|
注意:这些操作适用于ssub_match、csub_match、wssub_match、wcsub_match |
|
matched |
一个public bool数据成员,指出此ssub_match是否匹配了 |
first |
public数据成员,指向匹配序列首元素和尾后位置的迭代器。如果未匹配,则first和second是相等的 |
second |
|
length() |
匹配的大小。如果matched为false,则返回0 |
str() |
返回一个包含输入中匹配部分的string。如果matched为false,则返回空string |
s = ssub |
将ssub_match对象ssub转化为string对象s。等价于s = ssub.str()。转换运算符不是explicit的 |
bool valid(const smatch &m)
{
// 如果区号前有一个左括号
if(m[1].matched)
// 则区号后必须有一个右括号,之后紧跟剩余号码或一个空格
return m[3].matched && (m[4].matched == 0 || m[4].str() == " ");
else
// 否则,区号后不能有右括号
// 另两个组成部分间的分隔符必须匹配
return !m[3].matched && m[4].str() == m[6].str();
}
17.3.4 使用regex_replace
当我们希望在输入序列中查找并替换一个正则表达式时,可以调用regex_replace。
正则表达式替换操作 |
|
m.format(dest, fmt, mft) m.format(fmt, mft) |
使用格式字符串fmt生成格式化输出,匹配在m中,可选的match_flag_type标志在mft中。第一个版本写入迭代器dest指向的目的位置并接受fmt参数,可以是一个string,也可以是表示字符数组中范围的一对指针。第二个版本返回一个string,也可以是一个指向空字符结尾的字符数组的指针。mft的默认值为format_default |
regex_replace(dest, seq, r, fmt, mft) regex_replace(seq, r, fmt, mft) |
遍历seq,用regex_search查找与regex对象r匹配的子串。使用格式字符串fmt和可选的match_flag_type标志来生成输出。第一个版本将输出写入到迭代器dest指定的位置,并接受一对迭代器seq表示范围。第二个版本返回一个string,保存输出,且seq既可以是一个string也可以是一个指向空字符结尾的字符数组的指针。在所有情况下,fmt既可以是一个string也可以是一个指向空字符结尾的字符数组的指针,且mft的默认值为match_default |
就像标准库定义标志来指导如何处理正则表达式一样,标准库还定义了用来在替换过程中控制匹配或格式的标志。这些标志可以传递给函数regex_search或regex_match或是类smatch的format成员。
匹配和格式化标志的类型为match_flag_type。这些值都定义在名为regex_constants的命名空间中。类似于用bind的placeholders,regex_constants也是定义在命名空间std中的命名空间。为了使用regex_constants中的名字,必须在名字前同时加上两个命名空间的限定符:
using std::regex_constants::format_no_copy;
匹配标志 |
|
定义在regex_constants::match_flag_type中 |
|
match_default |
等价于format_default |
match_not_bol |
不将首字符作为行首处理 |
match_not_eol |
不将尾字符作为行尾处理 |
match_not_bow |
不将首字符作为单词首处理 |
match_not_eow |
不将尾字符作为单词尾处理 |
match_any |
如果存在多于一个匹配,则可返回任意一个匹配 |
match_not_null |
不匹配任何空序列 |
match_prev_avail |
输入序列包含第一个匹配之前的内容 |
format_default |
用ECMAScript规则替换字符串 |
format_sed |
用POSIX sed规则替换字符串 |
format_no_copy |
不输出输入序列中未匹配的部分 |
format_first_only |
只替换子表达式的第一次出现 |
17.4 随机数
定义在头文件random中。包含随机数引擎类和随机数分布类。一个引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数。
17.4.1 随机数引擎和分布
随机数引擎操作 |
|
Engine e; |
默认构造函数;使用该引擎类型默认的种子 |
Engine e(s); |
使用整型值s作为种子 |
e.seed(s) |
使用种子s重置引擎的状态 |
e.min() |
此引擎可生成的最小值和最大值 |
e.max |
|
Engine::result_type |
此引擎生成的unsigned整型类型 |
e.discard(u) |
将引擎推进u步;u的类型为unsigned long long |
default_random_engine e; // 生成随机无符号数
for (size_t i = 0; i < 10; ++i)
// e()"调用"对象来生成下一个随机数
cout << e() << " ";
为了得到在一个指定范围内的数,我们使用一个分布类型的对象:
// 生成0到9之间(包含)均匀分布的随机数
分布类型和引擎
uniform_int_distribution<unsigned> u(0, 9);
default_random_engine e; //
for (size_t i = 0; i < 10; ++i)
// 将u作为随机数源
// 每个调用返回在指定范围内并服从均匀分布的值
cout << u(e) << " ";
当我们说随机数发生器时,是指分布对象和引擎对象的组合。
一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
设置随机数发生器种子
为引擎设置种子有两种方式:在创建引擎对象时提供种子,或者调用引擎的seed成员:
default_random_engine e1; // 使用默认种子
default_random_engine e2(2147483646); // 使用给定的种子值
// e3和e4将生成相同的序列,因为它们使用了相同的种子
default_random_engine e3; // 使用默认种子值
e3.seed(32767); // 调用seed设置一个新种子值
default_random_engine e4(32767); // 将种子值设置为32767
for(size_t i = 0; i != 100; ++i) {
if(e1() == e2())
cout << "unseeded match at iteration: " << i << endl;
if(e3() != e4())
cout << "seeded differs at iteration: " << i << endl;
}
选择种子的另一种好的方法是调用定义在头文件ctime中的time函数。time函数接受单个指针参数,指向用于写入时间的数据结构。如果此指针为空,则函数简单地返回从一个特定时刻到当前经过了多少秒的时间:
default_random_engine e1(time(0)); // 稍微随机些的种子
如果程序作为一个自动过程的一部分反复运行,将time的返回值作为种子的方式就无效了;它可能多次使用的都是相同的种子。
17.4.2 其他随机数分布
生成随机实数
default_random_engine e; // 生成无符号随机整数
// 0到1(包含)的均匀分布
uniform_real_distribution<double> u(0, 1);
for(size_t i = 0; i < 10; ++i)
cout << u(e) << " ";
分布类型的操作 |
|
Dist d; |
默认构造函数;使d准备好被使用。 其他构造函数依赖于Dist的类型;参见附录A.3节 分布类型的构造函数是explicit的 |
d(e) |
用相同的e连续调用d的话,会根据d 的分布式类型生成一个随机数序列;e是一个随机数引擎对象 |
d.min() |
返回d(e)能生成的最小值和最大值 |
d.max() |
|
d.reset() |
重建d的状态,使得随后对d的使用不依赖于d已经生成的值 |
分布类型都是模板,具有单一的模板类型参数,表示分布生成的随机数的类型,对此有一个例外。每个分布模板都有一个默认模板实参。生成浮点值的分布类型默认生成double值,而生成整型值的分布默认生成int值。
// 空<>表示我们希望使用默认结果类型
uniform_real_distribution<> u(0, 1); // 默认生成double值
生成非均匀分布的随机数
normal_distribution生成浮点值,使用头文件cmath中的lround函数将每个随机数舍入到最接近的整数。
default_random_engine e; // 生成随机整数
normal_distribution<> n(4, 1.5); // 均值4,标准差1.5
vector<unsigned> vals(9); // 9个元素均为0
for(size_t i = 0; i != 200; ++i) {
unsigned v = lround(n(e)); // 舍入到最接近的整数
if(v < vals.size()) // 如果结果在范围内
++vals[v]; // 统计每个数出现了多少次
}
for(size_t j = 0; j != vals.size(); ++j)
cout << j << ": " << string(vals[j], '*') << endl;
bernoulli_distribution类
这个类是一个普通类,而非模板。此分布总是返回一个bool值。它返回true的概率是个常数,此概率的默认值为0.5,但可以改变。
bernoulli_distribution b; // 默认是50/50的机会
bernoulli_distribution b1(0.55); // b1(e)返回true的概率为0.55
17.5 IO库再探
17.5.1 格式化输入与输出
当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。
定义在iostream中的操纵符 |
|
boolalpha |
将true和false输出为字符串 |
*noboolalpha |
将true和false输出为1,0 |
showbase |
对整型值输出表示进制的前缀 |
*noshowbase |
不生成表示进制的前缀 |
showpoint |
对浮点值总是显示小数点 |
*noshowpoint |
只有当浮点值包含小数部分时才显示小数点 |
showpos |
对非负数显示+ |
*noshowpos |
对非负数不显示+ |
uppercase |
在十六进制值中打印0X,在科学记数法中打印E |
*nouppercase |
在十六进制值中打印0x,在科学记数法中打印e |
*dec |
整型值显示为十进制 |
hex |
整型值显示为十六进制 |
oct |
整型值显示为八进制 |
left |
在值的右侧添加填充字符 |
right |
在值的左侧添加填充字符 |
internal |
在符号和值之间添加填充字符 |
fixed |
浮点值显示为定点十进制 |
scientific |
浮点值显示为科学记数法 |
hexfloat |
浮点值显示为十六进制 |
defaultfloat |
重置浮点数格式为十进制 |
unitbuf |
每次输出操作后都刷新缓冲区 |
*nounitbuf |
恢复正常的缓冲区刷新方式 |
*skipws |
输入运算符跳过空白符 |
noskipws |
输入运算符不跳过空白符 |
flush |
刷新ostream缓冲区 |
ends |
插入空字符,然后刷新ostream缓冲区 |
endl |
插入换行,然后刷新ostream缓冲区 |
*表示默认流状态 |
另外,对于指定打印精度,可以通过调用IO对象的precision成员或使用setprecision操纵符来改变精度。precision成员是重载的。一个版本接受一个int值,将精度设置为此值,并返回旧精度值。另一个版本不接受参数,返回当前精度值。setprecision操纵符接受一个参数,用来设置精度。操纵符setprecision和其他接受参数的操纵符都定义在头文件iomanip中。
cout << "Precision: " << cout .precision()
<< ", Value: " << sqrt(2.0) << endl;
cout.precision(12);
cout << "Value: " << sqrt(2.0) << endl;
cout << setprecision(3);
cout << "Value: " << sqrt(2.0) << endl;
输出补白
setw指定下一个数字或字符串的最小空间。
left表示左对齐输出。
right表示右对齐输出,右对齐是默认格式。
internal控制负数的符号的位置,它左对齐符号,右对齐值,用空格填满所有中间空间。
setfill允许指定一个字符代替默认的空格来补白输出。
定义在iomanip中的操纵符 |
|
setfill(ch) |
用ch填充空白 |
setprecision(n) |
将浮点值精度设置为n |
setw(w) |
读或写值的宽度为w个字符 |
setbase(b) |
将整数输出为b进制 |
17.5.2 未格式化的输入/输出操作
单字节操作
单字节底层IO操作 |
|
is.get(ch) |
从istream is读取下一个字节存入字符ch中。返回is |
os.put(ch) |
将字符ch输出到ostream os。返回os |
is.get() |
将is的下一个字节作为int返回 |
is.putback(ch) |
将字符ch放回is。返回is |
is.unget() |
将is向后移动一个字节。返回is |
is.peek() |
将下一个字节作为int返回,但不从流中删除它 |
多字节操作
多字节底层IO操作 |
is.get(sink, size, delim) 从is中读取最多size个字节,并保存在字符数组中,字符数组的起始地址由sink给出。读取过程直至遇到字符delim或读取了size个字节或遇到文件尾时停止。如果遇到了delim,则将其留在输入流中,不读取出来存入sink |
is.getline(sink, size, delim) 与接受三个参数的get版本类似,但会读取并丢弃delim |
is.read(sink, size) 读取最多size个字节,存入字符数组sink中。返回is |
is.gcount() 返回上一个未格式化读取操作从is读取的字节数 |
os.write(source, size) 将字符数组source中的size个字节写入os。返回os |
is.ignore(size, delim) 读取并忽略最多size个字符,包括delim。与其他未格式化函数不同,ignore有默认参数:size的默认值为1,delim的默认值为文件尾 |
17.5.3 流随机访问
虽然标准库为所有流类型都定义了seek和tell函数,但它们是否会做有意义的事情依赖于流绑定到哪个设备。在大多数系统中,绑定到cin、cout、cerr和clog的流不支持随机访问。因此,流随机访问一般适用于fstream和sstream类型。
seek和tell函数 |
|
tellg() tellp() |
返回一个输入流中(tellg)或输出流中(tellp)标记的当前位置 |
seekg(pos) seekp(pos) |
在一个输入流或输出流中将标记重定位到给定的绝对地址。pos通常是前一个tellg或tellp返回的值 |
seekp(off, from) seekg(off, from) |
在一个输入流或输出流中将标记定位到from之前或之后的off个字符,from可以是下列值之一: beg,偏移量相对于流开始位置 cur,偏移量相对于流当前位置 end,偏移量相对于流结尾位置 |
pos和offset的类型分别为pos_type和off_type,这两个类型都是机器相关的,定义在头文件istream和ostream中。 |
对于fstream和stringstream来说,p版本和g版本的位置标记是同一个。
用于大型程序的工具
18.1 异常处理
一个异常如果没有被捕获,则它将终止当前的程序。
当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。当throw出现在一个try语句块内时,检查与该try块关联的catch子句。如果找到了匹配的catch,就使用该catch处理异常。如果这一步没找到匹配的catch且该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句。如果还是找不到匹配的catch,则退出当前函数,在调用当前函数的外层函数中继续寻找。上述过程被称为栈展开。
在栈展开过程中,析构函数总是会被执行,但是函数中负责释放资源的代码却可能被跳过。因此,在析构函数中不应该抛出不能被它自身处理的异常。如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块当中,并且在析构函数内部得到处理。
当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。这意味着如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉派生类部分,而只有基类部分被抛出。
18.1.2 捕获异常
catch子句中的异常声明,如果无需访问抛出的表达式的话,则可以忽略捕获形参的名字。声明的类型决定了处理代码所能捕获的异常类型,它可以是左值引用,但不能是右值引用。如果catch的参数是基类类型,则可以使用其派生类类型的异常对象对其进行初始化。此时,如果catch的参数是非引用类型,则异常对象将被切掉一部分。如果,catch的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。通常情况下,如果catch接受的异常与某个继承体有关,则最好将该catch的参数定义成引用类型。
查找匹配的处理代码
在搜寻catch的过程中,如果已经找到一个匹配的catch子句,则不会继续查找其他匹配,因此匹配的catch不一定的最佳匹配。异常和catch异常声明的匹配规则受到更多限制,只有少数转换允许:
从非常量向常量的类型转换;
允许从派生类向基类的类型转换;
数组被转换成指向数组(元素)的指针,函数被转换成指向该函数类型的指针。
除此之外,包括标准算术类型转换和类类型转换在内,其他所有转换规则都不能在匹配catch的过程中使用。
因此,如果在多个catch子句的类型之间存在着继承关系,则应该把继承链最底端的类放在前面,而将继承链最顶端的类放在后面。
重新抛出
有时,一个单独的catch语句不能完整地处理某个异常,这时catch语句可以通过重新抛出的操作将异常传递给另一个catch语句。重新抛出是一条不包含任何表达式的throw语句:
throw;
捕获所有异常的处理代码
如果想捕获所有异常类型,则可以通过catch(...)实现,即在catch的参数表中用省略号。
18.1.3 函数try语句块与构造函数
要想处理构造函数初始值抛出的异常,我们必须将构造函数写成函数try语句块的形式。如:
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :
data(std::make_shared<std::vector<T>>(il)) {
/* 空函数体 */
} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }
18.1.4 noexcept异常说明
如果一个函数确定不会抛出异常,可以通过noexcept声明(早期C++版本中,也可以通过声明throw()的方式达到同样效果)该函数不会抛出异常,对确定不会抛出异常的函数,编译器能执行一些特殊的优化操作。如果一个函数声明了noexcept,但在函数体内throw了一个异常,则编译器也并不一定会报错或提出警告,但是如果在执行中抛出了异常,则程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。关键字noexcept紧跟在函数的参数列表后面:
void recoup(int) noexcept; // 不会抛出异常
noexcept说明符可以接受一个可选的能转换为bool型的实参,如果实参转换结果为true,则函数承诺不会抛出异常,如果为false,则可能抛出异常。
void alloc(int) noexcept(false); // 可能抛出异常
上述更普通的形式是:noexcept(e)。当e调用的所有函数都做了不抛出说明且e本身不含有throw语句时,上述表达式为true,否则为false。
void f() noexcept(noexcept(g())); // f和g的异常说明一致
noexcept有两层含义:当跟在函数参数列表后面时它是异常说明符;而当作为noexcept异常说明的bool实参出现时,它是一个运算符。
异常说明与指针、虚函数和拷贝控制
尽管noexcept说明符不属于函数类型的一部分,但是函数的异常说明仍然会影响函数的使用。函数指针及该指针所指的函数必须具有一致的异常说明。如果一个虚函数承诺了它不会抛出异常,则后续派生类的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的虚函数既可以允许抛出异常,也可以不允许抛出异常。
18.2 命名空间
命名空间为防止名字冲突提供了更加可控的机制。每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者(以及用户)可以避免全局名字固有的限制。
18.2.1 命名空间定义
一个命名空间的定义包含两部分:首先是关键字namespace,随后是命名空间的名字。在命名空间名字后面是一系列由花括号括起来的声明和定义。只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间:
namespace cplusplus_primer {
class Sales_data { /*...*/ };
Sales_data operator+(const Sales_data&, const Sales_data&);
class Query { /*...*/ };
class Query_base { /*...*/ };
} // 命名空间结束后无须分号
命名空间的特点:
每个命名空间都是一个作用域。因此,要使用命名空间中的类、变量、函数需要通过作用域操作符用该命名空间的名字进行限定:
cplusplus_primer::Query q = cplusplus_primer::Query("hello");
命名空间可以是不连续的,这一点与其他作用域不太一样。
namespace nsp {
// 相关声明
}
如果可能是定义了一个名为nsp的新命名空间,也可能是为已经存在的命名空间添加一些新成员。
命名空间的接口和实现可以分别组织。命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。命名空间成员的定义部分则置于另外的源文件中。
模板特例化必须定义在原始模板所属的命名空间中。当然,可以在命名空间中只声明该特例化,而在外部具体定义该特例化。
全局作用域中定义的名字也就是定义在全局命名空间中。全局命名空间以隐式的方式声明,并且在所有程序中都存在。作用域运算符也可以用于全局作用域的成员:
::member_name
表示全局命名空间中的一个成员。
内联命名空间
C++11新标准引入一种新的嵌套命名空间,称为内联命名空间。和普通的命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。
定义内联命名空间的方式是在关键字namespace前添加关键字inline,关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候,可以写inline,也可以不写:
inline namespace FifthEd {
// 该命名空间表示本书第5版的代码
}
namespace FifthEd { // 隐式内联
class Query_base { /*...*/ };
// 其他与Query有关的声明
}
未命名的命名空间
未命名的命名空间是指关键字namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。
一个未命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。
定义在未命名的命名空间中的名字可以直接使用,如果未命名的命名空间的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别。
18.2.2 使用命名空间成员
命名空间的别名
命名空间的别名可以使得我们可以为命名空间的名字设定一个短得多的同义词。命名空间的别名声明以关键字namespace开始,后面是别名所用的名字、=符号、命名空间原来的名字以及一个分号。
namespace primer = cplusplus_primer;
namespace Qlib = cplusplus_primer::QueryLib;
using声明
using声明语句一次只引入命名空间的一个成员。一条using声明语句可以出现在全局作用域、局部作用域、命名空间作用域以及类的作用域中。在类的作用域中,这样的声明语句只能指向基类成员。如:
using std::cout; // 只有std命名空间中的cout可直接使用
using指示
using指示和using声明类似的地方是,我们可以使用命名空间名字的简写形式;和using声明不同的是,我们无法控制哪些名字是可见的,因为所有名字都是可见的。如:
using namespace std; // 将std空间的所有名字都可见
using指示具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。
// 命名空间A和函数f定义在全局作用域中
namespace A {
int i, j;
}
void f()
{
using namespace A; // 把A中的名字注入到全局作用域中
cout << i * j << endl; // 使用命名空间A中的i和j
}
实参相关的查找与类类型形参
对于命名空间中名字的隐藏规则来说有一个重要例外,当我们给函数传递一个类类型对象时,除了在常规的作用域查找这个函数外,还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。
当类声明一个友元时,该友元声明并没有使得友元本身可见。然而,一个另外的未声明的类或函数如果第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员。这条规则与实参相关的查找规则结合在一起将产生意想不到的效果:
namespace A {
class C {
// 两个友元,在友元声明之外没有其他声明
// 这些函数隐式地成员命名空间A的成员
friend void f2(); // 除非另有声明,否则不会被找到
// 根据实参相关的查找规则可以被找到
friend void f(const C&);
};
}
int main()
{
A::C cobj;
f(cobj); // 正确:通过在A::C中的友元声明找到A::f
f2(); // 错误:A::f2没有被声明
}
18.3 多重继承与虚继承
多重继承是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。
18.3.1 多重继承
在派生类的派生列表中可以包含多个基类:
class Bear : public ZooAnimal { /*...*/ };
class Panda : public Bear, public Endangered { /*...*/ };
每个被继承的基类包含一个可选的访问说明符。如果访问说明符被忽略,则关键字class对应的默认访问说明符是private,关键字struct对应的是public。
和只有一个基类的继承一样,多重继承的派生列表也只能包含已经被定义过的类,而且这些类不能是final的。
派生类构造函数初始化所有基类
派生类构造函数初始化所有基类,其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。
继承的构造函数与多重继承
在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数。但是如果从多个基类中继承了相同的构造函数,则程序将产生错误:
struct Base1{
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};
struct Base2 {
Base2() = default;
Base2(const std::string&);
Base2(int);
};
// 错误:D1试图从两个基类中都继承D1::D1(const string&)
struct D1 : public Base1, public Base2 {
using Base1::Base1; // 从Base1继承构造函数
using Base2::Base2; // 从Base2继承构造函数
};
如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本:
struct D2 : public Base1, public Base2 {
using Base1::Base1; // 从Base1继承构造函数
using Base2::Base2; // 从Base2继承构造函数
// D2必须自定义一个接受string&的构造函数
D2(const string &s) : Base1(s), Base2(s) {}
D2() = default; // 一旦D2定义了它自己的构造函数,则必须出现
};
析构函数与多重继承
派生类的析构函数只负责清除派生类本身分配的资源。析构函数的调用顺序与构造函数正好相反,即根据派生列表中的基类顺序逆序析构基类部分,及间接基类部分,最后析构派生类部分。
多重继承的派生类的拷贝与移动操作
与只有一个基类的继承一样,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作。在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。
18.3.2 类型转换与多个基类
在只有一个基类的情况下,派生类的指针或引用能自动转换成一个可访问基类的指针或引用。多个基类的情况与之类似。编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好。
18.3.3 多重继承下的类作用域
在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到,则对该名字的使用将具有二义性。
18.3.4 虚继承
尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。
在默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现多次,则派生类中将包含该类的多个子对象。
在C++语言中,我们通过虚继承的机制解决上述问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
使用虚基类
我们指定虚基类的方式是在派生列表中添加关键字virtual:
// 关键字public和virtual的顺序随意
class Raccoon : public virtual ZooAnimal { /*...*/ };
class Bear : virtual public ZooAnimal { /*...*/ };
virtual说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例。至于什么样的类能够作为虚基类并没有特殊规定。
如果某个类指定了虚基类,则该类的派生仍按常规方式进行:
class Panda : public Bear, public Raccoon, public Endangered {};
由于Raccoon和Bear继承ZooAnimal的方式都是虚继承,所以在Panda中只有一个ZooAnimal基类部分。
虚基类成员的可见性
因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是,如果成员被多余一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新版本。
例如,假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性:
如果在D1和D2中都没有x的定义,则x被解析为B的成员,此时不存在二义性,一个D的对象只含有x的一个实例。
如果x是B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。
如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。
18.3.5 构造函数与虚继承
在虚派生中,虚基类是由最底层的派生类初始化的。如上述Panda中,当创建其对象时,由Panda的构造函数独自控制ZooAnimal的初始化过程。这是一位内,此时只有一个虚基类的子对象,如果可以由直接基类分别都对虚基类子对象进行初始化,则会造成重复初始化同一个子对象的问题。因此,在派生类的构造函数中,应当对虚基类进行初始化(而在非虚继承中,派生类只负责直接基类的初始化)。
含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序对其进行初始化。
对象销毁的顺序则与上述构造顺序正好相反。
特殊工具与技术
19.1 控制内存分配
19.1.1 重载new和delete
当我们使用一条new表达式时:
string *sp = new string("a value");
string *arr = new string[10];
实际执行了三步操作:
(1)new表达式调用一个名为operator new(或者operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间。
(2)编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
(3)对象被分配了空间并构造完成,返回一个指向该对象的指针。
当我们使用一条delete表达式删除一个动态分配的对象时:
delete sp; // 销毁*sp,然后释放sp指向的内存空间
delete [] arr; // 销毁数组中的元素,然后释放对应的内存空间
实际执行了两步操作:
(1)对指针所指的对象或者所指的数组中的元素执行相应的析构函数。
(2)编译器调用名为operator delete(或者operator delete[])的标准库函数释放内存空间。
如果应用程序希望控制内存分配的过程,则需要定义自己的operator new函数和operator delete函数。即使在标准库中已经存在这两个函数的定义,我们仍旧可以定义自己的版本。编译器不会对这种重复的定义提出异议,相反,编译器将使用我们自定义的版本替换标准库中的定义来执行上述new表达式中的第一步和delete表达式中的第二步。
需要注意的是,我们无法重载new表达式和delete表达式,只能重载它们的函数形式。同时,它们的函数形式能独立调用,并非需要用表达式的形式。
如果在一个类类型中定义了operator new成员函数和operator delete成员函数时,则当我们使用new表达式分配对象资源和用delete表达式释放对象资源时,将使用该类类型的成员函数版本的operator new函数和operator delete函数来分别分配和释放空间。
operator new接口和operator delete接口
标准库定义了operator new函数和operator delete函数的8个重载版本。其中前4个版本可能抛出bad_alloc异常,后4个版本则不会抛出异常:
// 这些版本可能抛出异常
void *operator new(size_t);
void *operator new[](size_t);
void operator delete(void*) noexcept;
void operator delete[](void*) noexcept;
// 这些版本承诺不会抛出异常
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void operator delete(void*, nothrow_t&) noexcept;
void operator delete[](void*, nothrow_t&) noexcept;
类型nothrow_t是定义在new头文件中的一个struct,在这个类型中不包含任何成员。new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new的非抛出版本。与析构函数类似,operator delete也不允许抛出异常。当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常。
当我们将上述运算符函数定义成类的成员时,它们是隐式静态的,无须显式声明static,当然也可以显式声明。
对于operator new函数或者operator new[]函数来说,它们的返回类型必须是void*,第一个形参的类型必须是size_t且该形参不能含有默认实参。当我们为一个对象分配空间时使用operator new;为一个数组分配空间时使用operator new[]。当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参;当调用operator new[]时,传入函数的则是存储数组中所有元素所需的空间。
如果我们想要自定义operator new函数,则可以为它提供额外的形参,但不允许以下形式:
void *operator new(size_t, void*);
上述形式只供标准库使用。
对于operator delete函数或者operator delete[]函数来说,它们的返回类型必须是void,第一个形参的类型必须是void*。执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参。
当我们将operator delete或operator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的operator delete函数版本也由对象的动态类型决定。
malloc函数与free函数
在自定义自己的全局operator new和operator delete函数中,分配空间可以用头文件cstdlib中的void* malloc(size_t)函数,释放空间可以用该头文件的void free(void*)函数。
19.1.2 定位new表达式
标准库中的operator new和operator delete函数与allocator的allocate成员和deallocate成员非常类似,分别负责分配或释放空间,但前后不同的是,前者分配的空间无法使用construct函数构造对象。相反,我们应该使用new的定位new形式构造对象。为构造对象的定位new形式如下:
new(place_address) type
new(place_address) type (initializers)
new(place_address) type [size]
new(place_address) type [size] { braced initializer list }
其中,place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象。
当仅通过一个地址值调用时,定位new使用operator new(size_t, void*)"分配"内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,它只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位new允许我们在一个特定的、预先分配的内存地址上构造对象。
传给construct成员函数的指针必须指向同一个allocator对象分配的空间,但是传给定位new的指针无相应限制,甚至可以不是一个动态内存的地址。
显式的析构函数调用
当通过对象或者其指针或引用显式调用析构函数时,仅仅是销毁了对象,但不会释放对象所需的空间。
19.2 运行时类型识别
运行时类型识别的功能由两个运算符实现:
typeid运算符:用于返回表达式的类型。
dynamic_cast运算符:用于将基类的指针或引用安全地转换成派生类的指针或引用。
这两个运算符特别适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数(因为虚函数会动态绑定,无需转换)。
19.2.1 dynamic_cast运算符
dynamic_cast运算符的使用形式如下:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,e必须是一个有效的指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。
在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:
e的类型是目标type的公有派生类。
e的类型是目标type的公有基类。
e的类型就是目标type的类型。
如果符合条件,则类型转换成功。否则转换失败。如果一条dynamic_cast语句的转换目标是指针类型且失败了,则结果为0。如果转换目标是引用类型且失败了,则抛出一个bad_cast异常。
19.2.2 typeid运算符
typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。type_info定义在头文件typeinfo中。
typeid运算符可以作用域任意类型的表达式,顶层const被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。不过当typeid作用域数组或函数时,并不会执行向指针的标准类型转换。也就是说,当我们对数组a执行typeid(a)时,所得的结果是数组类型而非指针类型。
当运算对象不属于类类型或是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会得到。
Derived *dp = new Derived;
Base *bp = dp; // 两个指针都指向Derived对象
// 运行时比较两个对象的类型
if(typeid(*bp) == typeid(*dp)){
// bp和dp指向同一类型的对象
}
// 检查运行时类型是否是某种指定的类型
if(typeid(*bp) == typeid(Derived)) {
// bp实际指向Derived对象
}
19.2.4 type_info类
type_info的操作 |
|
t1 == t2 |
如果t1和t2表示同一种类型,则两者相等,否则两者不等 |
t1 != t2 |
|
t.name() |
返回一个C风格字符串,表示类型名字的可打印形式 |
t1.before(t2) |
返回一个bool值,表示t1是否位于t2之前。before所采用的顺序关系是依赖于编译器的 |
type_info类没有默认构造函数,其拷贝和移动构造函数以及赋值运算符都被定义成删除的。因此创建type_info对象的唯一途径是使用typeid运算符。
19.3 枚举类型
枚举类型是将一组整型常量组织在一起,分为限定作用域的枚举类型和不限定作用域的枚举类型。
限定作用域的枚举类型的形式:
enum class open_modes { input, output, append };
不限定作用域的枚举类型的形式:
enum color { red, yellow, green };
// 未命名的、不限定作用域的枚举类型
enum { floatPrec = 6, doublePrec = 10, double_doublePrec = 10 };
如果enum是未命名的,则只能在定义该enum时定义它的对象,定义的方法即在右侧花括号和分好之间提供逗号分隔的声明列表。
在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。相反,在不限定作用域的枚举类型中,枚举成员与枚举类型本身的作用域相同。
// 错误:重复定义成员(在enum color中已定义red等)
enum otherColor {red, yellow, green};
// 正确:枚举成员被隐藏了
enum class peppers {red, yellow, green};
color eyes = green; // 正确:不限定作用域的枚举成员可直接使用
peppers p = green; // 错误:peppers的枚举成员不在有效作用域
color hair = color::red; // 正确:允许显式访问枚举成员
peppers p2 = peppers::red; // 正确
默认情况下,枚举值从0开始,依次加1,但也可以为成员指定专门的整型值。但未指定专门值的成员总是比前一位成员(无论是否指定值)增加1。
枚举成员是const整型常量,不限定作用域的枚举类型成员能自动隐式转换为整型或其他类型,但限定作用域的枚举类型成员不会进行隐式转换。
int i = color::red; // 正确
int j = peppers::red; // 错误
指定enum的大小
在C++11新标准中,我们可以在enum的名字后加冒号以及我们想在该enum中使用的类型:
enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535,
longTyp = 4294967295UL,
long_longTyp = 18446744073709551615ULL
};
如果没有给enum指定类型,则默认为int型。对于不限定作用域的枚举类型,不存在默认类型,只是成员的潜在类型足够大,肯定能够容纳枚举值。如果我们指定了枚举成员的潜在类型,则一旦某个成员的值超出了该类型所能容纳的范围,将引发错误。
枚举类型的前置声明
在C++11新标准中,可以提前声明enum,但对不限定作用域的枚举类型必须指定成员大小:
enum intValues : unsigned long long;
enum class open_modes; // 限定作用域的枚举类型默认为int
enum的声明和定义必须匹配,即在该enum的所有声明和定义中成员的大小必须一致。而且,我们不能在同一个上下文中先声明一个不限定作用域的enum名字,然后再声明一个同名的限定作用域的enum。
形参匹配与枚举类型
虽然枚举成员的值可能与某个整型值相等,但是两者类型不同。因此在函数参数中,若为枚举类型,不能用普通整型值去做实参,除非使用显式转换。在使用整型形参的地方,可以用不限定作用域的枚举类型成员去做实参,因为此时会执行隐式转换。但枚举类型成员不会隐式转换为比int更小的类型(如char、short)。
19.4 类成员指针
成员指针是可以指向类的非静态成员的指针。成员指针的类型囊括了类的类型及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。下面先提供一个类:
class Screen {
public:
typedef std::string::size_type pos;
char get_cursor() const { return contents[cursor]; }
char get() const;
char get(pos ht, pos wd) const;
private:
std::string contents;
pos cursor;
pos height, width;
};
19.4.1 数据成员指针
由于成员指针必须包含成员所属的类,因此在*之前要添加classname::以表示当前定义的指针可以指向classname的成员:
// pdata可以指向一个常量(非常量)Screen对象的string成员
const string Screen::*pdata;
当我们初始化一个成员指针(或者向它赋值)时,需要指定它所指的成员:
pdata = &Screen::contents;
其中,我们将取地址运算符作用域Screen类的成员而非内存中的一个该类的对象。在C++11新标准中,声明成员指针最简单的方法是使用auto或decltype:
auto pdata = &Screen::contents;
使用数据成员指针
当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。与成员访问运算符.和->类似,也有两种成员指针访问元算符:.*和->*,这两个运算符使得我们可以解引用指针并获得该对象的成员。
Screen myScreen, *pScreen = &myScreen;
// .*解引用pdata以获得myScreen对象的contents成员
auto s = myScreen.*pdata;
// ->*解引用pdata以获得pScreen所指对象的contents成员
s = pScreen->*pdata;
返回数据成员指针的函数
常规的访问控制对成员指针同样有效。因为数据成员一般情况下是私有的,所以我们通常不能直接获得数据成员的指针。如果一个像Screen这样的类希望我们可以访问它的contents成员,可以通过定义一个函数,令其返回值是指向该成员的指针:
class Screen {
public:
// data是一个静态成员,返回一个成员指针
static const std::string Screen::* data()
{ return &Screen::contents; }
// 其他成员与之前一致
};
19.4.2 成员函数指针
创建一个指向成员函数的指针与数据成员的指针类似,最简单的方法是通过auto来推断类型:
auto pmf = &Screen::get_cursor;
如果成员函数有重载的情况,则必须显式声明函数类型以明确指出我们想要使用的是哪个函数,如果成员函数是const成员或引用成员,则我们必须将const限定符或引用限定符包含进来:
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;
和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则,即在上述赋值中必须显式地使用取地址操作符,而不能使用函数名自动转换为函数指针的规则。
使用成员函数指针
与使用数据成员指针方式类似,也通过.*和->*运算符。
Screen myScreen, *pScreen = &myScreen;
char c1 = (pScreen->*pmf)();
char c2 = (myScreen.*pmf2)(0, 0);
使用成员指针的类型别名
使用类型别名或typedef可以让成员指针更容易理解。下面的类型别名将Action定义为两参数get函数的同义词:
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get;
我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型。其中指向成员的指针形参也可以有默认实参:
Screen& action(Screen&, Action = &Screen::get);
其调用方式:
Screen myScreen;
action(myScreen); // 使用默认实参
action(myScreen, get); // 使用之前定义的变量get
action(myScreen,&Screen::get); // 显式地传入地址
成员指针函数表
对普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中,以方便选择具有相同类型的成员函数。如:
class Screen{
public:
// 其他接口和实现成员与之前一致
using Action = Screen& (Screen::*)();
Screen& home();
Screen& forward();
Screen& back();
Screen& up();
Screen& down();
enum Directions {HOME, FORWARD, BACK, UP, DOWN};
Screen& move(Directions);
private:
static Action Menu[]; // 函数表
};
Screen& Screen::move(Direction cm)
{
return (this->*Menu[cm](); // Menu[cm]指向一个成员函数
}
Screen::Action Screen::Menu[] = { &Screen::home, &Screen::forward, &Screen::back, &Screen::up, &Screen::down};
这里的函数表需注意数组成员的顺序要与Directions中的成员位置对应。
Screen myScreen;
myScreen.move(Screen::HOME); // 调用myScreen.home
19.4.3 将成员函数用作可调用对象
由于指向成员函数的指针不能诸如pf(...)的形式使用,因此其不能直接作为可调用对象。但是,我们可以通过function模板来产生一个调用对象。
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
上述vector中保存的是string对象。如果vector中保存的是指向string的指针,则fcn的定义如下:
function<bool (string *)> fcn = &string::empty;
find_if内部会正确地调用函数对象,对于前者,find_if类似以下调用:
if(fcn(*it));
对于后者,find_if类似以下调用:
if(((*it).*p)()) // 假设p是fcn内部一个指向成员函数的指针
之所以function中的函数形式与empty有所不同是因为empty作为成员函数有个隐式的this形参。执行成员函数的对象将被传给隐式的this形参。在function中,就需要显示地写明该形参(下面的bind同理)。
将成员函数用作可调用对象的另外一种简单的方法是使用同样定义在头文件functional中的mem_fn:
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
mem_fn会自动推断成员函数的类型产生一个可调用对象。mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用:
auto f = mem_fn(&string::empty);
f(*svec.begin()); // 传入一个string对象,f使用.*调用empty
f(&svec[0]); // 传入一个string的指针,f使用->调用empty
使用bind生成一个可调用对象
我们还可以使用bind从成员函数生成一个可调用对象:
auto f = bing(&string::empty, _1);
f(*svec.begin());
f(&svec[0]);
19.5 嵌套类
一个类可以定义在另一个类的内部,前者称为嵌套类或嵌套类型。嵌套类常用于定义作为实现部分的类。嵌套类是一个独立的类,与外层类基本没什么关系。特别是,外层类的对象和嵌套类的对象是相互独立的。在嵌套类的对象中不包含任何外层类定义的成员;类似的,在外层类的对象中也不包含嵌套类定义的成员。
嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。对嵌套类的访问,要通过外层类用访问限定符进行访问。
声明一个嵌套类
class TextQuery {
public:
class QueryResult;
// 其他成员与之前同
};
上述只是声明TextQuery中有个嵌套类QueryResult,但还没有定义。
在外层类之外定义一个嵌套类
class TextQuery::QueryResult {
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
// 无需定义QueryResult::line_no,可对外层类的名字直接使用
QueryResult(std::string, std::shared_ptr<std::set<line_no>>,
std::shared_ptr<std::vector<std::string>>);
// 其他成员与之前一致
};
19.6 union:一种节省空间的类
联合是一种特殊的类。一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给union的某个成员赋值后,该union的其他成员就变成未定义的状态了。union不能含有引用类型的成员。在C++11新标准中,含有构造函数或析构函数的类类型也可以成为union的成员类型。默认情况下,union的成员都是public的。
union可以定义包括构造函数和析构函数在内的成员函数,但是由于union不能继承其他类,也不能作为基类,因此不能有虚函数。
定义union
union Token {
char cval;
int ival;
double dval;
};
使用union类型
Token first_token = {'a'};// 初始化cval成员
Token last_token; // 未初始化的Token对象
Token *pt = new Token; // 指向一个未初始化的Token对象的指针
last_token.cval = 'z';
pt->ival = 42;
匿名union
匿名union是一个未命名的union,并且在右花括号和分号之间没有任何声明。一旦我们定义了一个匿名union,编译器就自动地为该union创建一个未命名的对象:
union {
char cval;
int ival;
double dval;
};
cval = 'c';
ival = 42;
定义了一个匿名union时,可以直接访问其成员。匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。
含有类类型的union
C++11新标准允许在union中含有定义了构造函数或拷贝控制成员的类类型成员。对union的值改为其中某个类类型成员对应的值,则必须构造该成员,如果从一个类类型成员的值改为另一个不同类类型成员的值,则需分别运行前者的析构函数和后者的构造函数。另外,如果union含有类类型的成员,且该类型定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并声明为删除的
对于union来说,要想构造或销毁类类型的成员必须执行非常复杂的操作,因此通常把含有类类型成员的union内嵌在另一个类当中。这个类可以管理并控制union的类类型成员有关的状态转换。一般方案是:在外层类中定义这个内嵌的匿名union,同时定义一个enum类型的判别式对象,以保存内嵌匿名union中存储的值的类型。
class Token {
public:
Token(): tok(INT), ival{0} {}
Token(const Token &t): tok(t.tok) { copyUnion(t); }
Token &operator=(const Token&);
~Token() { if(tok == STR) sval.~string(); }
Token &operator = (const std::string&);
Token &operator = (char);
Token &operator = (int);
Token &operator = (double);
private:
enum {INT, CHAR, DBL, STR} tok; // 判别式
union { // 匿名union
char cval;
int ival;
double dval;
std::string sval;
};
void copyUnion(const Token&);
};
Token& Token::operator=(int i)
{
if(tok == STR) sval.~string();
ival = i;
tok = INT;
return *this;
}
Token& Token::operator=(const std::string &s)
{
if(tok == STR)
sval = s;
else
new(&sval) string(s); // 使用定位new构造sting
tok = STR;
return *this;
}
void Token::copyUnion(const Token &t)
{
switch(t.tok) {
case Token::INT: ival = t.ival; break;
case Token::CHAR: cval = t.cval; break;
case Token::DBL: dval = t.dval; break;
case Token::STR: new(&sval) string(t.sval); break;
}
}
Token& Token::operator=(const Token &t)
{
if(tok == STR && t.tok != STR) sval.~string();
if(tok == STR && t.tok == STR) sval = t.sval;
else copyUnion(t);
tok = t.tok;
return *this;
}
19.7 局部类
类可以定义在某个函数的内部,称为局部类。局部类的成员受到严格限制。局部类不能定义static成员,也不能使用所在函数的普通变量和形参,只能访问外层作用域定义的类型名、static变量以及枚举成员。
19.8 固有的不可移植的特性
19.8.1 位域
类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。位域在内存中的布局是与机器相关的。
位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,所以在通常情况下使用无符号类型保存一个位域。位域的声明形式是在成员名字之后紧跟一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数:
typedef unsigned int Bit;
class File {
Bit mode: 2;
Bit modified: 1;
Bit prot_owner: 3;
Bit prot_group: 3;
Bit prot_world: 3;
// File的操作和数据成员
public:
// 文件类型以八进制的形式表示
enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };
File &open(modes);
void close();
void write();
bool isRead() const;
void setWrite();
};
取地址操作符不能作用于位域,因此任何指针都无法指向类的位域。
使用位域
访问位域的方式与访问类的其他数据成员的方式非常相似:
void File::write()
{
modified = 1;
// ……
}
通常使用内置的位运算符操作超过1位的位域:
File& File::open(File::modes m)
{
mode |= READ; // 按默认方式设置READ
if (m & WRITE)
// 按照读/写方式打开文件
return *this;
}
19.8.2 volatile限定符
当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile。关键字volatile告诉编译器不应对这样的对象进行优化。
volatile的用法与const很相似,起到对类型额外修饰的作用:
volatile int display_register;
volatile int iax[max_size]; // 每个元素都是volatile
volatile与const可以同时存在,表明兼具两者特性。一个类可以定义volatile成员函数,只有volatile成员函数才能被volatile对象调用。
volatile指针与const用法类似,一个volatile对象只能被volatile指针指向。
const和volatile的一个重要区别是不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。合成的成员接受的形参类型是(非volatile)常量引用,显然不能把一个非volatile引用绑定到一个volatile对象上。
如果一个类希望拷贝、移动或赋值它的volatile对象,则该类必须自定义拷贝或移动操作。例如,我们可以将形参类型指定为const volatile引用,这样我们就能利用任意类型的Foo进行拷贝或赋值操作了:
class Foo {
public:
Foo(const volatile Foo&);
// 将一个volatile对象赋值给一个非volatile对象
Foo& operator=(volatile const Foo&);
Foo& operator=(volatile const Foo&) volatile;
// Foo类的剩余部分
};
19.8.3 链接指示:extern "C"
C++程序要调用C语言编写的函数,由于编译器检查其调用的方式与处理普通C++函数相同,但生成的代码有所区别,因此需要使用链接指示指出任意非C++函数所用的语言。
声明一个非C++的函数
声明一个C函数:
extern "C" size_t strlen(const char*);
如果是其他语言写的函数,则可能需要用其他链接指示,如extern "ADA"、extern "FORTRAN"等,具体看编译器的支持与否。
链接指示与头文件
多重声明的形式可以应用于整个头文件:
extern "C" {
#include <string.h> // 操作C风格字符串的C函数
}
指向extern "C"函数的指针
// pf指向一个C函数,该函数接受一个int返回void
extern "C" void (*pf) (int);
当我们使用pf调用函数时,编译器认定当前调用的是一个C函数。指向C函数的指针与指向C++函数的指针是不一样的类型:
void (*pf1) (int); // 指向一个C++函数
extern "C" void (*pf2)(int); // 指向一个C函数
pf1 = pf2; // 错误:pf1和pf2的类型不同
链接指示对整个声明都有效,即对作为返回类型或形参类型的函数指针也同时有效:
// f1是一个C函数,它的形参是一个指向C函数的指针
extern "C" void f1(void (*) (int));
有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理器定义__cplusplus。利用这个变量,我们可以在编译C++程序的时候,有条件地包含进来一些代码:
#ifdef __cplusplus
// 正确:我们正在编译C++程序
extern "C"
#endif
int strcmp(const char*, const char*);
重载函数与链接指示
C语言中不支持函数重载,因此一个C链接指示只能用于说明一组重载函数中的某一个:
// 错误:两个extern "C"函数的名字相同
extern "C" void print(const char*);
extern "C" void print(int);
如果在一组重载函数中有一个是C函数,其余的必须都是C++函数。
附录 A
A.1 标准库名字和头文件
名字 |
头文件 |
abort |
<cstdlib> |
accumulate |
<numeric> |
allocator |
<memory> |
array |
<array> |
auto_ptr |
<memory> |
back_inserter |
<iterator> |
bad_alloc |
<new> |
bad_array_new_length |
<new> |
bad_cast |
<typeinfo> |
begin |
<iterator> |
bernoulli_distribution |
<random> |
bind |
<functional> |
bitset |
<bitset> |
boolalpha |
<iostream> |
cerr |
<iostream> |
cin |
<iostream> |
cmatch |
<regex> |
copy |
<algorithm> |
count |
<algorithm> |
count_if |
<algorithm> |
cout |
<iostream> |
cref |
<funcional> |
csub_match |
<regex> |
dec |
<iostream> |
default_float_engine |
<iostream> |
default_random_engine |
<random> |
deque |
<deque> |
domain_error |
<stdexcept> |
end |
<iterator> |
endl |
<iostream> |
ends |
<iostream> |
equal_range |
<algorithm> |
exception |
<exception> |
fill |
<algorithm> |
fill_n |
<algorithm> |
find |
<algorithm> |
find_end |
<algorithm> |
find_first_of |
<algorithm> |
find_if |
<algorithm> |
fixed |
<iostream> |
flush |
<iostream> |
for_each |
<algorithm> |
forward |
<utility> |
forward_list |
<forward_list> |
free |
<cstdlib> |
front_inserter |
<iterator> |
fstream |
<fstream> |
function |
<functional> |
get |
<tuple> |
getline |
<string> |
greater |
<functional> |
hash |
<functional> |
hex |
<iostream> |
hexfloat |
<iostream> |
ifstream |
<fstream> |
initializer_list |
<initializer_list> |
inserter |
<iterator> |
internal |
<iostream> |
ios_base |
<ios_base> |
isalpha |
<cctype> |
islower |
<cctype> |
isprint |
<cctype> |
ispunct |
<cctype> |
isspace |
<cctype> |
istream |
<iostream> |
istream_iterator |
<iterator> |
istringstream |
<sstream> |
isupper |
<cctype> |
left |
<iostream> |
less |
<functional> |
less_equal |
<functional> |
list |
<list> |
logic_error |
<stdexcept> |
lower_bound |
<algorithm> |
lround |
<cmath> |
make_move_iterator |
<iterator> |
make_pair |
<utility> |
make_shared |
<memory> |
make_tuple |
<tuple> |
malloc |
<cstdlib> |
map |
<map> |
max |
<algorithm> |
max_element |
<algorithm> |
mem_fn |
<functional> |
min |
<algorithm> |
move |
<utility> |
multimap |
<map> |
multiset |
<set> |
negate |
<functional> |
noboolalpha |
<iostream> |
normal_distribution |
<random> |
noshowbase |
<iostream> |
noshowpoint |
<iostream> |
noskipws |
<iostream> |
not1 |
<functional> |
nothrow |
<new> |
nothrow_t |
<new> |
nounitbuf |
<iostream> |
nouppercase |
<iostream> |
nth_element |
<algorithm> |
oct |
<iostream> |
ofstream |
<fstream> |
ostream |
<iostream> |
ostream_iterator |
<iterator> |
ostringstream |
<sstream> |
out_of_range |
<stdexcept> |
pair |
<utility> |
partial_sort |
<algorithm> |
placeholders |
<functional> |
placeholders::_1 |
<functional> |
plus |
<functional> |
priority_queque |
<queue> |
ptrdiff_t |
<cstddef> |
queue |
<queue> |
rand |
<random> |
random_device |
<random> |
range_error |
<stdexcept> |
ref |
<functional> |
regex |
<regex> |
regex_constants |
<regex> |
regex_error |
<regex> |
regex_match |
<regex> |
regex_replace |
<regex> |
regex_search |
<regex> |
remove_pointer |
<type_traits> |
remove_reference |
<type_traits> |
replace |
<algorithm> |
replace_copy |
<algorithm> |
reverse_iterator |
<iterator> |
right |
<iostream> |
runtime_error |
<stdexcept> |
scientific |
<iostream> |
set |
<set> |
set_difference |
<algorithm> |
set_intersection |
<algorithm> |
set_union |
<algorithm> |
setfill |
<iomanip> |
setprecision |
<iomanip> |
setw |
<iomanip> |
shared_ptr |
<memory> |
showbase |
<iostream> |
showpoint |
<iostream> |
size_t |
<cstddef> |
skipws |
<iostream> |
smatch |
<regex> |
sort |
<algorithm> |
sqrt |
<cmath> |
sregex_iterator |
<regex> |
ssub_match |
<regex> |
stable_sort |
<algorithm> |
statck |
<stack> |
stoi |
<string> |
strcmp |
<cstring> |
strcpy |
<cstring> |
string |
<string> |
stringstream |
<sstream> |
strlen |
<cstring> |
strncpy |
<cstring> |
strtod |
<string> |
swap |
<utility> |
terminate |
<exception> |
time |
<ctime> |
tolower |
<cctype> |
toupper |
<cctype> |
transform |
<algorithm> |
tuple |
<tuple> |
tuple_element |
<tuple> |
tuple_size |
<tuple> |
type_info |
<typeinfo> |
unexpected |
<exception> |
uniform_int_distribution |
<random> |
uniform_real_distribution |
<random> |
uninitialized_copy |
<memory> |
uninitialized_fill |
<memory> |
unique |
<algorithm> |
unique_copy |
<algorithm> |
unique_ptr |
<memory> |
unitbuf |
<iostream> |
unordered_map |
<unordered_map> |
unordered_multimap |
<unordered_map> |
unordered_multiset |
<unordered_set> |
unordered_set |
<unordered_set> |
upper_bound |
<algorithm> |
uppercase |
<iostream> |
vector |
<vector> |
weak_ptr |
<memory> |