extern "C"

在.cpp中加 extern "C",即扩展为C的编译方式,可以让编译器不采用C++的编译方式,而是采用C语言的编译方式来编译。

..................................................................

引用

引用是通过别名(并没有开辟新的空间,与被引用的变量有相同的地址)直接访问某个变量。

    引用必须要初始化,并且在初始化后就不能再更改。引用的本质是指针常量,int &a = b会被编译器转换为int *const a = &b。常量引用主要用来修饰函数形参,即可以加const防止误操作(形参改变实参)。不允许建立void类型的引用,因为void在语法上相当于一个类型,本质上不是类型。void的
含义是无类型或者空类型,任何实际存在的变量都是非void类型的。引用返回可以提升空间效率,因为它不用临时创建第三方对象,但是什么时候可以使用引用返
回(加&):当返回的对象离开函数后还能继续存活(不被析构,比如*this离开函数后被析构,但
是它仍然存在,因为它永远代表当前对象)就可以使用。

..................................................................

const和constexpr和常量表达式

1)const

在用一个字面值常量初始化一个const对象时,在编译阶段编译器会把代码中用到这个对象的地方用它的值来替换,此时的对象可以理解为其值的别名。这里需要注意因为const对象一旦创建后其值就不能再改变,因此const对象必须初始化。

/*在编译时,编译器会将代码中的pi用3.14159来替换。若这里为const double pi就
是错误的,因为创建时没有初始化。注意这里使用的()是直接初始化,不同于使用{}的
列表初始化,使用列表进行初始化时默认缺省值使用0来初始化。*/
const double pi(3.14159);

引用一个const对象,需要显式指出其const属性:

const int ci = 0;  const int &r1 = ci;   //r1引用const int对象ci

利用const修饰符定义的对象并不意味着该对象与常量总是等价的,比如下面的利用对象i的值初始化ci。

/*利用一个非常量值来初始化const对象ci,在编译时,编译器无法计算ci的
值。因此ci只有在程序运行期间才能体现常量的特性。*/
int i = 100; const int ci = i;

2)constexpr和常量表达式

常量表达式是指值不会改变且在编译期间就能得到计算结果的表达式。例如字面值(比如上面的3.14159)是常量表达式,用字面值初始化的const对象(比如上面的pi)也是常量表达式;但是上面的对象ci是非常量表达式,因为尽管ci是一个const对象但它的值只有在程序运行期间才能获得。

为了让编译器更好地了解我们的意图,C++11提供了constexpt关键字,用来帮助编译器自动识别常量表达式。与const类似,constexpr修饰的对象是一个常量;与const不同的是constexpr修饰的对象必须用常量表达式来初始化(而如上所示const允许使用非常量表达式)。

unsigned cnt = 10;  //数组的[]里的值必须为大于0的整形常量表达式,而
int arr[cnt];        //错误,cnt不是常量表达式
constexpr int sz = 10;   //这里的constexpr也可以用const替代
int arri[sz];   //正确

..................................................................

左值 / 右值 / 右值引用 / 通用引用

对于任何一个表达式,那么是左值要么是右值。左值所在的内存空间的地址可以用&来获取,但右值的地址是无法获取的;因此左值对象既可以读又可以写,而右值对象只能读不能写。

int i = 0;  //用右值0初始化左值对象i
int j = i;  //左值对象j可以当成右值,只对其内容进行读操作
const int N = 4;  //N为右值对象
N = 40; //错误,不能改变右值对象N的值

由右值是不能被修改的,其不可写的特性和const对象是一致的,因此可以用右值来初始化一个const引用,比如字面值常量、右值表达式等。例如:

/*这里也可以使用double类型的数据来初始化,编译器将3.14转换成一个整
型数据并存放到一个int的临时对象里,r1则引用这个临时对象。*/
const int &r1 = 3.14; 

C++11引入了右值引用,有了右值引用,就可以操控右值对象,尤其是编译器产生的临时对象。右值引用也是引用,因此也只是已创建对象的别名,但它只能绑定到右值。这也意味着可以通过右值引用获取即将消亡的右值对象的资源。通过&&定义右值引用的语法格式为:

/*r1为右值引用,绑定到一个临时对象。这里算术表达式i+1为右值表达式,得到的
结果放到一个临时对象中,当表达式运算完之后,临时对象便消亡,但通过右值引
用r1引用之后,其生命得到了延续,这是因为r1是个左值(虽然r1是右值的引用,但
它本身是一个左值),等效于把一个短暂的无名右值命个名字使之变成一个持久的有
名左值。这里如果这里尝试:int &&r3= r1;  则编译器会错误,因为r1为左值
对象,r3为右值引用不能绑定左值对象。虽然右值引用不能绑定左值左值对象,但
可以利用标准库提供的move函数将一个左值转换为右值,例如将r1转换为右值:
int &&r3 = std::move(r1); */
int i = 0;  int &&r1 = i + 1;      

这里需要注意,当右值引用&&与类型推导结合在一起时,将变得非常灵活。它既可以与左值绑定也可以与右值绑定,此时它变成一种通用引用类型(universal reference)。 声明为atuo &&的对象都是通用引用。

int i = 0;
auto &&r1 = 10;  //auto &&根据字面值常量推导出r1为右值引用
auto &&r2 = i;  //r2被推导出为左值引用

类似地,如果一个模板函数形参为模板类型参数T的右值引用 T &&, 则形参也有相同的行为。

..................................................................

auto类型推导和decltype

为了让代码更简洁C++11引入了这二个关键字。

1)auto类型推导

使用atuo时,编译器利用它可以根据初始值的类型自动推导出需要的数据类型。

auto pi = 3.14159, rad = 1.0   //正确,pi和rad都为double类型
auto pi = 3.14159, rad = 1   //错误,pi和rad类型不一致

当初始值是一个非引用const对象时,atuo将忽略const属性(而对于引用则不会忽略);若希望编译器推断出其具有const属性,则需要显式指出const。

/*rad是一个double类型,而不是const double,因为pi的const属性被忽略。若
希望编译器推断出rad具有const属性,则需要显式指出。const auto rad = pi。*/
const double pi = 3.14159; auto rad = pi;/*rad是一个const double类型的引用,auto会被推导为const double(这里需要显
式指出rad的是引用类型);这里的const属性会保留下来,这是因为rad是pi的引用,
而pi具有const属性,因此rad也必须具有const属性,否则可以通过rad对pi进行写
操作,这也违背了const对象的定义。*/
const double pi = 3.14159; atuo &rad = pi;

再次强调,定义对象时符号&和*从属于对象名,并不是类型名的一部分,auto只是一个"占位符"。例如:

int i = 0;
auto &ref = i, *ptr = &i;  //auto被推导为int
auto &ref = i, ptr = &i;  //错误,auto的推导类型不一致

2)decltype (expr)

atuo能够利用表达式(表达式是指由运算符和操作对象组成的式子,字面值和对象是最简单的表达式)的值推导出用户想要定义的对象的数据类型,并用表达式的值初始化定义的对象,但是有时只想用表达式的类型而不想用表达式的值来定义对象。为此可以使用decltype关键字,它能够在不计算表达式的情况下获取表达式的数据类型,语法格式为decltype (expr)。

int i = 0;
decltype (i) j = 1;    //j为int类型
decltype (i + j)  k = 0; //k为int类型,decltype分析i + j的值的数据类型,但不会计算i + j的值。

注意,当decltype遇到const时,和auto的处理方式不同,它不会忽略const属性。

const double pi = 3.14159; decltype (pi) rad = 1.0 //rad为const double类型

..................................................................

指针使用的一些注意点

    指针就是地址,地址就是指针;指针变量对象就是存放内存单元地址第一个字节所在地
址(内存单元编号)的变量。指针的类型必须和所指向的对象的类型一致,void指针和基类指针除外。在C++11之前,使用NULL(在C++中NULL是0的宏定义)或0来代表空指针;在C++11中引入了
新的关键字nullptr来代表空指针。有些情况下可以避免一些使用上的不便,也不会引起理解是
的困难(常量0是int类型,不是int *类型)。void指针是一种特殊的指针,它能够指向任何类型的对象。对于这种类型的指针,它只是简
单地把对象的地址存储起来,对于对象的类型并不感兴趣(可以存放任意类型对象的地址);但是不
能把void指针随意赋给一个普通指针,比如确保它们指向相同类型的对象(若指向对象的数据类型
不一致会出现运行时错误),而且需要进行类型转换。

..................................................................

数组的使用

1)一些复杂数组的定义

int arr[5];
/*[]的优先级高于*。定义一个含有5个int *类型元素的数组,每个元素都是指针*/
int *arrp[5];
/*定义一个指向含有5个int类型元素的数组的指针。可以从内到外,从右到左的
顺序来阅读:首先圆括号里的parr前有个*,因此它是一个指针;然后从右到左的顺
序来阅读,parr指向一个含有5个int类型元素的数组。parr的初始值为arr的地址,
即parr指向数组arr。*/
int (*parr) [5] = &arr;
int (&rarr) [5] = arr;  //定义arr数组的一个引用。
int *(*parrp) [5] = &arrp;  //parrp为指向指针数组arrp的指针。
int *(&rarrp) [5] = arrp; //rarrp为指向指针数组arrp的引用。

2)使用range for语句来访问数组

除了传统的for语句外,C++11引入了range for语句。其语法格式为:

for (decl :expr) {statement;
}

其中expr必须是一个对象序列,比如数组,容器或字符串;decl是与序列中数据元素类型相同的对象,通常用auto来推导数据元素的类型。例如:

for (auto i : arr) {cout << i << endl;
}

上面i为数组arr中当前元素的副本,在执行过程中,用arr中的当前元素初始化对象i,注意这里对arr的处理只是简单读取其中的所有元素,并没有对数组中的数据进行写操作。如果想要对其进行写操作,则需要将i声明为引用。例如:

for (auto &i : arr) {  //i此时为arr中当前对象的引用i = 0;  //写操作:将每一个元素设置为0
}

当使用range for语句处理多维数组时,除最内层的循环外,其他各层循环中必须使用引用。例如:

int a2d[3][5];for (auto &row : a2d)      //row被推导为int (&) [5]类型,即一维数组的引用for (auto col : row) 

但是如果将上面row前的&去掉,则无法通过编译,因为此时row会别推导为int *类型(不是列表类型,内层无法使用range for语句)。

3)指针指向数组

一般情况下编译器对数组的操作都会转换成对指针的操作。例如数组名通常被转换成数组第一个元素的地址,而且是个右值。

int arr[] = {1, 2, 3, 4, 5};
auto pa = arr;  //pa为int *类型
cout << *pa;  //输出arr[0]的值1

可见指针pa和数组arr相关联,一般数组的操作都可以用指针操作替代。但是基于数组arr利用decltype定义一个新的数组时,数组名arr不会转换为指针。例如:

/*ar2为存放5个整型数的一维数组。因此arr与ar2没有任何关联,它们分别
存储在不同的内存空间内,只是类型一样而已。*/
decltype (arr) ar2;

一个数组名可以理解成一个指针常量,但二者并不完全等价,例如:

int *const p = &arr[0];  //arr可以理解指针常量p
cout << sizeof(arr) << " " << sizeof(p); //输出为20 4 

同样,可以利用指针指向一个多维数组。

int a2d[3][5];
int (*p2d) [5] = a2d;  //指针p2d指向a2d的第一个元素a2d[0]

4)利用指针访问二维数组

前面定义的指针p2d指向二维数组a2d的第一个元素,可以将下标运算符[]作用于指针来访问数组元素,例如:

p2d[1][1] = 1;

上面的代码与下面4句代码等价

/*按从里向外,从右向左的顺序来阅读。p2d指向a2d的第一个元素,p2d + 1指向
a2d的第二个元素,也就是说p2d + 1是数组a2d第二个元素的地址,即&a2d[1]。
因此*(p2d + 1)为数组的第二个元素,即p2d[1],代表a2d中第二个一维数组中第
一个元素的地址。*(p2d + 1) + 1是数组a2d中第二个一维数组中第二个元素的地
址,即&a2d[1][1],因此*(*(p2d + 1) + 1)为数组a2d中第二个一维数组中的第
二个元素,即a2d[1][1]。*/
*(*(p2d + 1) + 1) = 1;
*(p2d[1] + 1) = 1;
*(*(a2d + 1) + 1) = 1;
*(p2d[1] + 1) = 1; 

可以利用auto简化代码书写:

int a2d[3][5] = {{1}, {1}, {1}};
for (auto p = a2d; p < a2d + 3; ++p) {  //p的类型为int (*) [5]for (auto q = *p; q < *p + 5, ++q) {    //q的类型为int *cout << *q << " ";}cout << endl;
}

从上面可以看出,数组名在一般情况下会被编译器转换为指针。尤其是在做下标运算时,不管下标运算符前面是指针还是数组名,都会转换为指针,然后进行相关操作。还可以利用数组元素连续性的特性,通过如下代码遍历数组a2d:

for (atuo p = &a2d[0][0]; p < a2d[0] + 15; ++p) {if ((p - a2d[0]) % 5 == 0)       //a2d[0] 等价于&a2d[0][0]cout << endl;    //每打印5个换行cout << *p << " ";
}

5)可以使用C++11引入的新的库函数begin()和end()来获取首元素和尾元素的地址

和容器类的二个同名成员函数类似,begin和end库函数分别可以获取数组的首元素和尾元素的下一个元素的地址。

..................................................................

C++定义枚举类型的2种方式

C++提供2种方式来定义枚举类型:不限定作用域方式和限定作用域方式。其中限定作用域的枚举类型是C++11新引入的类型,在定义时需要在enum和所定义的枚举类型名之间加上关键字class或者struct即可,在访问其枚举成员时必须标明其枚举成员的作用域。

enum class zsf {red green, yellow};
zsf a = zsf::red

..................................................................

自增/自减/逗号/sizeof/位运算符

1)++和--运算符

它们分别有前置和后置二个版本。例如对于++:

int i = 0, j;
j = i++;  //后置,i的值自增变为1,表达式i++的值为i自增之前的值,即j的值为0
j = ++i;//前置,i的值自增变为2,表达式++i的值为i自增之后的值,即j的值为2

由此可见,上面的前置版本避免了额外的运输代价,直接返回操作对象本身,而后置版本编译器需要产生一个临时对象来保存变化之前的值。因此开发中尽量使用前置版本。

2)逗号运算符

在所有的运算符中逗号运算符的优先级最低。其计算方法为依次从左到右计算每个运算对象,表达式的结果为最右边的运算对象。

int i, j; i = (j = 3, j+=6, 5+6);  //i的值为11, j的值为9

3)sizeof运算符

返回一个表达式或一个类型所占内存的字节数,其格式为sizeof(type)或者sizeof(expr)。

/*输出4,i的值为0。这是因为sizeof(expr)形式只是返回表达式结果的数据类型
的字节数,并不会实际运算表达式。*/
int i = 0;  cout << sizeof(++i) <<endl;

4)位运算符

包括~(按位取反), <<, >>, &, | , ^共6种。

<<运算符在右侧插入0;>>运算符在左侧插入的值取决于操作对象的数据类型,如果是无符号数则左侧插入0,如果是有符号数左侧插入符号位的副本。

这里需要注意位运算符虽然可以处理有符号数,但是并没有规定如何处理符号位,所以位运算符一般不宜处理有符号数。

..................................................................

string类型

1)string类型对象的定义方法

string类型是类类型,可以采用如下方法定义一个string类型对象。

string str1;        //默认初始化,定义一个空字符串
string str2(str1);        //等价于string str2 = str1; str2是str1的一个副本
string str3 = "Rosita";        //复制初始化
string str4("Rosita");        //直接初始化
string str5(5, 'R');        //直接初始化,str5的内容为RRRRR

一般情况下,如果只有一个初始值,则可以采用直接或复制初始化的方式,如对象str2/str3/str4的初始化。如果使用=运算符,则执行复制初始化,编译器将=右侧的值复制到定义的对象里。如果初始值有多个,则一般采用直接初始化,如对象str5的初始化。

2)string对象的加法操作

把二个string对象的内容连接起来,形成一个新的string对象,例如:

string s1 = "hello ", s2 = "C++";
string s3 = s1 + s2;        //s3此时内容为“Hello C++”
s1 += s2;        //s1此时内容为“Hello C++”

string对象还可以和字面值常量相加,例如:

string s4 = "Hello " + s2;

3)访问单个字符

string s = "hello";
s[1] = 'H';        //对第二个元素进行写操作
/*使用at成员函数是比较安全的,它会自动检查位置的合法性;如果位置越界则
会抛出一个异常。但是对于下标运算符[]来说,越界行为是未定义的。*/
cout << s.at(1) << endl;         

C++11还支持front和back操作访问第一个和最后一个字符,例如:

cout << s.front() << " " << s.back() << endl;

 4)string类对象使用C风格字符串处理函数

C风格的字符串不是一种类型,而是以空字符结尾('\0')的字符数组,在使用C风格的字符串函数时,需要确保每个操作对象必须以空字符'\0'结尾。

如果想使用C风格字符串处理函数处理string类对象或其他需要字符指针的操作,需要通过string类成员函数c_str来获取string对象存储的字符串的首地址,例如:

sting str = "hello"
char carr[10];
/*c_str成员函数会返回const char*类型的指针,确保其指向的对象不被修改*/
strcpy(carr, str.c_str());

..................................................................

局部对象和全局对象的生命期、作用域和链接性

需要注意局部自动对象具有自动存储周期,它们在栈区被分配存储空间;静态和全局对象具有静态存储周期,它们在全局数据区分配存储空间;这些对象由编译器负责内存的分配和回收,程序员无法控制内存分配和回收的时刻及内存的大小(存储在堆区(也叫动态内存区或自由存储区)的对象可以由程序员控制)。

1)对象的4类生命期

存储周期表明了对象可以在内存里存储的时间,C++支持以下4种类型的存储周期:

a.自动存储周期:定义在函数体或者语句块内部的对象(包括函数的形参)具有自动存储周期,即在程序执行
到其定义的位置时在内存中创建,离开其作用域时被释放。
b.静态存储周期:定义在函数外面或者使用staic关键字声明的对象具有静态存储周期,即在程序运行期间,
它们始终存在,直到程序结束。
c.动态存储周期:利用运算符new生产的对象具有动态存储周期,可以利用运算符delete释放其内存空间,
也就是说,它们的存储周期具有从new操作开始,到delete操作结束。
d.线程存储周期:为了支持并行程序设计,C++11引入了thread_local关键字,利用thread_local创建的对
象的存储周期在其所在的线程创建时开始,在线程结束时结束。

在程序运行期间,存储周期类型不同的对象在不同的内存区域被分配存储空间。具有自动存储周期的对象在栈区被分配存储空间,具有静态存储周期的对象在全局数据区被分配存储空间,而具有动态存储周期的对象则在堆区被分配存储空间。

一个对象的生命期是指在其存储周期内可以访问该对象的时间(该时间通常为一个对象从产生到消亡的时间)。需要注意的是存储周期类型相同的对象不一定有相同的生命期,如在一个块内先定义的对象的生命期要早于后面定义的对象。

2)对象的2类作用域

a.局部作用域:只能在函数/语句块内部可见/访问。
b.全局作用域:从定义处到其所在的文件末尾都是可见/可访问的,因此也称为文件域。具有外链接性的对
象,当在其他文件中比如main.cpp使用extern声明后,其作用域可以被扩展到main.cpp,即
在main.cpp也可以访问它。

3)对象的3类链接性

a.无链接性:只能够在其定义域内部访问,不可以在其他的作用域访问。
b.内部链接性:只能在其定义的文件中被访问,不可以在其他文件中被访问。
c.外部链接性:不仅可以在其定义的文件中被访问,还可以在其他的文件中被访问。

4)局部对象(在语句块内部定义的对象,包括函数形参)和全局对象(在函数外面定义的对象) 

局部对象都具有局部作用域,比如在函数func内定义的局部自动变量局部静态变量都只能在函数func内部访问/可见。但是它们有不同类型的存储周期,其中的局部自动对象具有自动存储周期,局部静态对象(用static定义的对象,如果没有显式初始化则被默认初始化为0)具有静态存储周期。另外局部对象无链接性。

全局对象具有全局作用域,和具有静态存储周期。对于内置类型的全局对象,如果没有显式初始化则被默认初始化为0

int a = 10;        //定义全局对象
int main () {int a = 1;        //定义局部自动变量cout << a << endl;        //访问局部自动变量a,打印1cout << ::a << endl;        //访问全局对象a,打印10。::为全局作用域符
}

另外全局对象具有外部链接性,可以利用关键字static来限制一个全局对象只能在其定义的文件中被访问,使其具有内部链接性。例如在源文件fun.cpp中定义一个全局对象:

int b = 10;        //b具有外部链接性

对象b具有外部链接性,可以在其他文件中被访问。在main.cpp中,可以通过extern声明来访问b,例如:

/*通过extern声明后,b的作用域从fun.cpp扩展到了main.cpp文件中,因此
在main.cpp中也可以访问它。*/
extern int b;
/*c具有内部链接性,其作用域被限制在main.cpp文件,不能在其他文件中访问它。*/
static int c = 20;

..................................................................

函数使用注意点

1)函数声明

        由于函数声明没有函数体,因此形参的名字也可以省略,只给出类型即可,比如

int max(int, int); 

2)const形参

        形参的初始化和对象的初始化过程是一样的,所以const修饰的形参名字和const修饰的对象名字的含义是一样的。例如下面其中第一行声明标明函数f_cval只能读取i的值,不能对i进行写操作;第二行声明标明函数f_cptr可以更改i的指向,但不能更改i所指向的实参的值;第三行声明表明函数f_cref不能更改i的值,也就是说不能更改与引用i绑定的实参的值。

void f_cval(const int i);        //i为const对象
void f_cptr(const int *i);        //i为指向const类型的实参
void f_cref(const int &i);        //i为const类型实参的引用。

在C++中应尽量使用const引用形参,因为此种方式形参就是实参的引用,作用域形参上的操作等价于作用实参,而加上const又可以保证其安全性;另外也可以避免像值传递那样的复制操作。

const引用形参可以接受字面值常量、表达式的求值结果、需要转换的对象或者const对象,但非const引用形参是不可以的,例如:

void f_ref(int &i);        //引用形参
const int cx = 1;
int x = 1;
f_ref(41);        //错误,左值引用不能绑定字面值常量
f_ref(cx);        //错误,左值引用不能绑定常量
f_ref(x + 1);        //错误,左值引用不能绑定右值表达式

上述函数调用如果改用const引用参数版本f_cref,则能够得到正确的调用

3)数组形参

         通常使用指针的方式来传递首元素的地址。但可以把形参以数组的形式来书写。以下二种写法是等价的。

void fun(int *p);
void fun(int p[ ]);

下面的函数调用都是合法的:

int arr[5] = {1, 2};
fun(arr);        //数组名转化为首元素的地址
fun(&arr[0]);        //显式传递首元素的地址

当多维数组作为实参时,传递的仍然是数组的首元素地址。例如,下面函数声明的形参可以接受一个二维数组,其中数组的第二维长度为5:

void fun(int (*a2d) [5]);         //a2d指向一个含有5个元素的的一维实参数组。
void fun(int a2d[] [5]);        //用数组的形式来书写

上面的函数可以接受如下二维数组:

int matrix[4][5] = {};
fun(matrix);        //传递matrix首元素地址,即一个具有5个元素的一维数组

4)constexpr函数

通常情况下,const关键字修饰的对象为编译时常量,例如

const int num = 30;

可是,如果试图利用一个返回const类型的函数来获取一个常量,例如

const int getNumber() {return 10;}
const int numStudent = getNumber();

并利用这个常量来定义一个数组,例如

int arr[numStudent];

则编译器件会报错,提示无法在编译时计算numStudent的值。这是因为只能在运行期间调用函数getNumber后才能计算numStudent的值。此时numStudent是一个运行时常量,而不是编译时常量。

为了解决这个问题C++11引入了constexpr函数。constexpr函数指的是能用于常量表达式的函数。另外需要注意的是constexpr函数会隐式转换为内联函数,因此constexpr函数的定义也应该放在头文件中。

使用constexpr函数需要严格遵守以下几点(之后的C++14和C++17有所改进):

.函数中有且仅有一条reutrn语句,并且return语句中的表达式必须是编译时的常量表达式;

.函数返回值类型不能为void;

        .函数体可以有类型别名和using声明等运行时不执行任何操作的语句。

constexpr int getNumber(int i){return i;}
int stu1[getNumber(10)];        //正确,因为getNumber(10)是常量表达式
int num = 10
int stu2[getNumber(num)];        //错误,因为运算时才能确定num的值。

5)函数指针

指针可以指向一个对象,也可以指向一个函数。函数的类型由返回值和形参列表决定,例如,声明一个compareInt函数:

bool compareInt(int, int);        //比较二个整数大小,

该函数的类型为bool (int, int),则定义一个指向该类型的函数的指针为:

bool (*pf) (int, int);        //指针名为pf

在实际使用中可以利用typedef或者using来简化函数指针的定义:

typedef bool (*pFun) (int, int); pFun pf; //和定义普通指针一样的方式来定义一个函数指针
using pFun = bool (*)(int, int); pFun pf; //一般使用这种定义方式

和普通指针类型一样,应该在定义函数指针时初始化(当定义的函数对象,不指向任何函数时初始化为nullptr),例如:

pFun pf1 = compareInt;        //隐函初始化
bool b1 = pf1(1, 2);
pFun pf2 = &compareInt;        //显式初始化
bool b2 = (*pf2)(1, 2);

上面函数指针是比较简单的,对于复杂函数指针,比如:

/*可以按照这样的原则来理解复杂指针声明:先取出标识符,然后按照由
里向外,由右向左的顺序来阅读。标识符a是一个具有5个指针类型元素的
数组,每个指针指向具有一个int类型形参、返回类型为void类型的函数。*/
void (*a[5]) (int);
/*b是一个指针,指向具有5个元素的数组,每个元素为指向函数的指针类型,
指向的函数具有一个int类型的形参,返回值类型为void类型。*/
void(*(*b)[5])(int);
/*从最里层的fp开始,fp是一个指向具有int类型形参、返回值为void类型的
函数的指针。然后阅读c:c是一个函数,该函数有二个形参(类型分别为int和fp类
型),返回值类型是指向一个具有int类型形参、返回值类型void的函数指针。*/
void(*c(int, void(*)(int)))(int);        

可以使用using声明将上面三条语句简化:

using PF = void(*)(int);
PF a[5];
PF (*b) [5];
PF c(int, PF);

6)lambda表达式

有时候设计的函数比较短小简单,和内联函数比较相似,但和内联函数不同的是这些函数只使用一次,用完即可“扔掉”。如果程序中充满了这样的函数,则会影响程序的结构性和可读性,这也有悖于设计函数的初衷。为此C++11引入了lambda表达式。lambda函数可以理解为一个临时的匿名函数,表示一个可以调用的代码单元。与函数类似,lambda表达式具有返回值、形参列表和函数体。但与函数不同的是,一个lambda表达式可以定义在一个函数内部。定义一个lambda表达式的语法格式为:

/*其中方括号[]代表lambda引导,里面的captures子句指定在同一作用域下lambda主
体捕获(访问)哪些对象以及如何捕获这些对象,captures子句可以为空,表示不会访问
外围对象;parameters、returen type和statements分别表示形参列表、返回类型和
函数体。*/
[captures] (parameters) -> return type {statements}

有了auto关键字,可以很容易把lambda表达式放到一个对象里,例如:

atuo fun = [ ] (int i) {cout << i << endl;};
/*输出17。上面定义了一个lambda表达式,捕获列表为空,有一个int类型形参,函数体直接
打印形参值;然后fun(17)会执行该表达式,和函数调用方式一样,使用调用运算符()来执行
一个lambda表达式。*/
fun(17);        

lambda表达式可以根据函数主体来推断返回类型,此时可以省略return type说明。例如,若lambda函数主体无return语句,则返回类型为void;如果只有一条return语句,则通常编译器可以根据return语句的表达式类型来推断返回值类型。当然也可以显式指明返回值类型,例如:

[int] (int i) -> int {return i * i;}   //该lambda表达式返回值类型指定为int类型

lambda表达式可以捕获外围作用域内的局部对象,而且还可以指定捕获方式,下面通过标准库中的for_each算法来介绍lambda表达式的常用捕获方式。for_each算法的第三个参数接受一个调用函数,逐个处理第一个和第二个参数所指定范围内的列表元素。

*按值捕获特定的外围对象,作为副本在函数体内部使用;

int divisor = 5;
vector <int> numbers {1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50};
for_each(numbers.begin(), numbers.end(),  [divisor](int y) {if (y % divisor ==0)        //divisor为外围divisor的副本cout << y <endl;        //输出被divisor整除的元素
}); 

*按引用捕获特定的外围对象,作为引用在函数体内部使用;

int sum = 0;
for_each (numbers.begin(), numbers.end(), [divisor, &sum] (int y) {if (y % divisor == 0)        //sum为外围sum的引用sum +=y;        //累加被divisor元素整除的元素,结果存放在外围对象sum中。
});

* [ = ]和[ & ]分别以值捕获和引用捕获方式捕获外围所有对象。

..................................................................

宏定义

1)宏定义展开的机制与const对象和函数调用机制完全不同。宏展开只是简单的字符串替换,不会像定义对象或函数调用那样进行语法检查。因此建议使用const对象和内联函数来代替宏定义。

2)在宏定义中,符号#可以把语言符号转换为字符串,符号##用来将二个语言符号连接为一个语言符号。

3)#pragma once是一种头文件保护技术,尽管大多数编译器都支持它,但它不是C++官方语言,因此谨慎使用。

..................................................................

1)this指针

实际上当通过对象调用成员函数时,有一个隐式的指针类型形参this接受了调用对象的地址。this指针是一个指针常量,指向所调用成员函数的的对象。

假设Fraction是自定义的一个类,double value()是其成员函数。则:

Fraction a;
/*通过对象a调用成员函数value(),这个成员函数调用相当于:a.value(&a),编译器会将成
员函数value()的声明转换为double value(Fraction *const this)。即编译器负责把对象
a的地址传递给this指针。*/
cout << a.value() << endl;        

this指针默认指向非const对象,C++采用在函数参数列表后面(圆括号后面)引用const关键字的形式来使this指针指向const对象。假设double value() const是其成员函数,这样的成员函数称为常量成员函数/const成员函数(在函数体内部,任何通过this指针对其指向的对象的数据成员进行操作都是非法的)。这样可以把成员函数看成如下形式:

/*此时this指针是一个指向const对象的const指针。*/
double value(const Fraction * const this);  

2)友元

除了将一个普通的函数声明为一个类的友元外,还可以将其他类或者其他类的成员函数声明为该类的友元。友元的声明必须放在类的内部。需要注意的是,友元不是类的成员,仅显式授予其访问权限。

友元函数:

在类中声明友元函数时,需在其函数名前加上关键字friend。此声明可以放在公有部分,也可放在私有部分和保护部分。友元函数可以定义在类的内部也可定义在类的外部。如果没有友元机制,外部函数访问类的私有/保护机制必须通过调用公有的成员函数。

当将某类的成员函数声明为友元函数时,要加上成员函数所在类的类名。

友元类:

当类Y被声明为类X的友元时,类Y的所有成员函数都称为类X的友元函数。这就意味着友元Y中的所有成员函数都可以访问类X中的所有成员(包括私有成员和保护成员)。

派生类的友元:

由于友元函数并非类成员,因此不能被继承,在某种需求下,可能希望派生类的友元函数能够使用基类中的友元函数。为此可以通过强制类型转换,将派生类的指针或引用强转为其类的引用或指针,然后使用转换后的引用或是指针来调用基类中的友元函数。

3)类的访问权限(class定义的类的默认访问权限为private)

private:只有同一个类的其他成员,或该类的friend类可以访问这些成员。

protected:只有同一个类的其他成员,或该类的friend类可以访问这些成员

public:任何可以看到这个类的地方都可以访问这些成员。

4)类的static成员、const成员和static const成员

类的static成员:

静态数据成员不属于类对象而是属于整个类,因此它们并非由构造函数类初始化,通常它们放在类的外部定义和初始化(一个例外是当静态成员为常量整型类型时,可以在类内部为其提供初始值)。
        静态成员函数没有this指针(因为静态成员函数不与任何对象绑定),也不能声明为常成员函数。

静态成员函数可以直接访问本类中的静态数据成员,因为静态数据成员同样属于类的,可以直接访问。一般而言静态成员函数不访问类中的非静态成员的,但是若确实要访问非静态成员,静态成员函数只能通过对象名或对象指针或对象引用来访问该对象的非静态成员。

/* PartTimeWorker定义的对象只包含m_name和m_hours二个数据成员,ms_payrate属于整个类,
为所有PartTimeWorker对象所共享。*/
class PartTimeWorker {string m_name;double m_hours;static double ms_payrate;static const int ms_maxHourWeek = 20;
public:double salary();/*在类的内部定义静态成员函数*/static double rate() {return ms_payrate;}static void initrate(double rate);
}
/*私有的静态数据成员不能在类外直接访问,必须通过公有的成员函数访问。*/
double PartTimeWorker::salary() {return ms_payrate * hours;    //成员函数内部可以直接访问静态成员
}
/*在类的外部定义静态成员函数*/
void PartTimerWorker::initrate(double rate) {ms_payrate = rate;
}
/*定义类的静态数据成员ms_payrete并完成初始化。这里需要注意在类外定义的时候必须指明所属
的类,而且不能重复static关键字,而且应在定义对象之前进行初始化。*/
double PartTimeWorker::ms_payrete = 7.53;

静态数据成员属于类,而不像普通数据成员那样属于某一对象,因此可以用“类名 ::”访问
静态的数据成员。另外虽然静态成员不属于具体的类对象,但可以通过类对象来访问静态成员。

cout << PartTimeWorker::rate() << endl;    //类内使用静态数据成员:通过类名访问静态成员PartTimeWorker worker;
cout << worker.rate() << endl;    //通过类对象访问静态成员函数

类的const成员:

    类的引用类型成员或者具有const修饰符的成员必须利用构造函数的初始值列表(在构造函
数参数列表后面和左花括号之间以冒号开始,利用形参值直接初始化类的数据成员,数据成员之
间用逗号隔开)进行初始化,否则会错过初始化时机,无法完成初始化。常成员函数的格式为:类型说明符 函数名(参数表)const; 这里需要注意const是函数类
型的一个组成部分,因此在声明函数和定义函数时都要有关键字const。在调用时不必加const。一般情况下常成员函数/常方法不能改变该方法里定义的所有数据成员(但是数据成员声明时
加关键字mutable后在常成员函数中依然可以修改),这是因为this指针的本质是指针常量,指针
的指向某个对象后是不可以修改的,但是this指针指向的值是可以修改的,若想让这个指向的值
也不能该就要要再加一个const;在成员函数后面加const本质修饰的还是this指针。常方法是不能调动普通方法的。常对象只能调用常方法。

类的static const成员:

/*如果一个类的成员,既要实现共享,又要实现不可改变,那就用static const修饰。修饰
成员函数,格式并无二异,修饰数据成员,必须要类内部初始化。*/
class A {
public:static const void dis() {cout << i << endl;}
private:const static int i = 100;
}int main()
{A::dis();return 0;
}

5)类成员指针(指向类的非静态成员的指针)

数据成员指针

除需要指明类成员外,还需要显式指明成员所属的类。例如:

/*定义一个指向PartTimeWorker类数据成员m_name的指针p1,可以使用C++11中的auto来简
化类型声明auto p2 = &PartTimeWorker::name;*/
string PartTimeWorker::*p1 = &PartTimeWorker::name;/*类的访问控制规则同样适用于成员指针。m_name为PartTimeWorker的类的私有成员,因此
下列代码必须位于成员函数或类的友元中。否则会编译错误*/
PartTimeWorker w1, *w2 = &w1;
cout << w1.n_name << endl;   //普通访问方式
cout << w1.*p1 << endl;    //数据成员指针访问方式,等价于w1.name
cout << w2->*p1 << endl;    //数据成员指针访问方式,等价于w2->m_name

成员函数指针

和数据成员指针类似,定义函数指针时也需要指明成员函数所属的类,可以使用auto来简化成员函数指针的定义。当然也可以使用函数指针的别名来简化成员函数指针的定义:

double (PartTimeWorker::*pf)();
pf = &PartTimeWorker::salary;
/*使用auto*/
auto pf2 = &PartTimeWorker::salary;
/*使用using声明这种方式可以让成员函数指针的定义更容易理解*/
using PTWS = double (PartTimeWorker::*)();
ptws pf3 = &PartTimeWorker::salary;/*和数据成员指针一样,需要使用.*和->*运算符作用于指向成员函数的指针,来调用类成员函数*/
PartTimeWorker w;
cout << w.salary() << endl;
cout << (w.*pf)() <<endl; 

6)运算符重载

    C++语言中大部分运算符都可以重载,但是有5个运算符不可重载(::和.和.*和sizeof和?:)如果一个类没有显式重载赋值运算符=,那么编译器将会合成一个默认的赋值运算符。在重载运算符时要考虑将其声明为类成员函数还是类的辅助函数,一般来说可以按照这样的规则
来做出选择:对于赋值= 、下标[] 、函数调用() 、和成员访问箭头->运算符必须是类成员。对于改
变运算对象自身状态的运算符应该是类成员,比如自增、自减运算符等。具有对称性的运算符一般应
作为类的辅助函数,比如算术、关系、逻辑运算符等。如果提供含有类类型的混合类型表达式(比如5/a,
其中a为类对象),则运算符应该定义成辅助函数,而且在通常情况下,将它们声明为类的友元。

           一般来说,成员运算符函数的显式参数比运算符的数目少一个,这是因为运算符函数的第一个运算对象与隐含的this指针绑定(如果运算符作为类的成员函数,则左侧对象一定是该类类型对象,否则将无法通过该类对象调用此运算符)。例如:

class Fraction {
public:Fraction& opeartor*= (const Fraction &rhs) {if(&rhs == this)    return *this;    //不能给自己赋值,比如a = a;m_numerator *= rhs.numerator;m_denominator *= rhs.m_denominator;return *this;}
};/*对于Fraction类对象a和对象b,在执行下面表达式时,第一个运算对象a绑定到成员函
数operator *= 的隐式this指针参数,形参rhs为对象b的引用*/
a *= b;//调用重载的*=/*类似地,可以像使用成员函数一样,通过点运算符或者箭头运算符来调用重载的成员运算符函数*/
a.operator *= (b);    //与a *= b等价

<<和>>运算符的重载

这二个是IO标准库的运算符。通常情况下,为了与IO标准库兼容,被声明为友元。

class Fraction {/*通常情况下输出运算符的第一个形参是一个非常量ostream类对象,这是因为需要向其写入数据,第二个形参是Fraction类常量引用,避免对实参进程写操作*/friend ostream& operator<< (ostream &os, const Fraction &a);/*通常情况下,输入运算符的第一个形参为istream类对象的引用,第二个形参为非常量对象引用来对与a进程绑定的实参进行写操作,并返回istream类对象的引用。*/friend istream& operator>> (istream &is, Fraction &a);
};/*Fraction类输出运算符定义如下,之所以要返回ostream类对象的引用,是因为:IO类对象不允许
复制;可以实现连续输出。例如 Fraction a, b; cout << a << " " << b << endl; */
ostream& operator<< (ostream &os, const Fraction &a) {os << a.m_numerator << "/" << a.denominator;return os;
}
/*Fraction类重载运算符>>如下*/
istream& operator>> (istream &is, Fraction &a) {is >> a.m_unmerator >> a.m_denominator;return is;
}

I/O运算符必须为非成员函数,否则使用上无法与IO标准库兼容,如果作为Fraction类的成员函数,只能这样使用:

Fraction a;
a >> cin;
a << cout << endl;

++和--运算符的重载

这些运算符会改变操作对象的内容,因此通常作为类的成员函数。前置和后置版本的递增运算符均为单目运算符,为了区分这二种形式,后置版本采用一个额外的int形参。这个形参值不会被使用,只是为了与前置版本相区分。例如,在Fraction类中,递增运算符将m_numerator加1

class Fraction {
public:Fraction& operator ++ ();    //前置版本Fraction operator ++ (int);    //后置版本
};/*为了与内置版本的行为一致,前置版本递增运算符返回对象的引用*/
Fraction& Fraction::operator ++ {++m_numerator;return *this;
}/*为了与内置版本的行为一致,在后置版本中,递增之前需要创建一个局部对象保存运算对象的值,
在递增之后,将局部对象返回,返回的方式为值返回而非引用返回*/
Fraction Fraction::operator ++ (int) {Fraction a(*this);    //保存当前值m_numerator++;return a;
}

函数调用运算符的重载

如果一个类重载了函数调用运算符,则可以通过函数的方式来使用该类的对象,因此该类的对象也称为函数对象。例如在Fraction类里面重载函数调用运算符,该运算符函数有二个形参,分别用来设置m_numerator和m_denominator,返回调用对象的const引用:

class Fraction {
public:const Fraction& operator() (int a, int b) {m_numerator = a;m_denominator = b;return *this;}
};/*通过一个Fraction类对象作用于一个实参列表来调用运算符(),这种方式和函数调用的方式
非常相似*/
Fraction f;
f(3, 5);    //调用运算符()

这里需要注意,一个类可以重载多个版本的函数调用运算符,但它们必须为类的非静态成员函数。

类型转换运算符的重载

类型转化运算符是一种特殊的类成员函数,用来将类类型数据转换为其他类型数据,其语法格式为:

/*type为某种类型,除void类型外,一个类的类型转换运算符可以将类类型转换成该类支持的任意
其他类型*/
operator type () const;

这里需要注意:类型转换运算符都是以隐式的方式调用的,因此类型转换运算符既没有显式的返回值类型,也没有形参,而且必须为类的成员函数。一般情况下将类型转换运算符声明为类的const成员。 例如为Fraction类添加一个转换为double类型的类型转换运算符,代码如下:

class Fraction {
public:Fraction(int above=0, int below=1):m_numerator(above), m_denominator(below) { }operator double () const {return 1.*m_numerator / m_denominator;}
};/*此时,Fraction类既支持向类类型的转换,也支持类类型向其他类型的转换*/
Fraction f = 2;    //调用默认构造函数,这里会将int类型隐式转换成Fraction类型
double x = 1.5 + f;    //调用类型转换运算符,将f隐式转换为double类型
int i = f;    //首先将f隐式转换为double类型,然后再转换为int类型。

下标运算符[]的重载

在C++中,重载下标运算符[]时,认为它是一个双目运算符。设X为某一个类的对象,类中定义了重载的operator[]函数,则表达式X[Y]可被解释为:X.operator[] (Y);

下标运算符重载函数只能定义成员函数,其形式如下;

返回类型 类名::operator[] (形参)    //注意形参在此表示下标,C++规定只能有一个参数。
{//函数体
}

赋值运算符的重载

在某些特殊情况下,如类中有指针类型时,使用默认的赋值运算符重载函数会产生错误。所以有的时候还需要用户根据实际需要自己对赋值运算符进行重载,以解决遇到的问题。指针悬挂就是这方面的一个典型问题。

1)如果类中有指针类型(比如类中含有指向new动态空间的指针指针ptr )并且没有自己定义赋值重载函数时,当执行对象间的赋值(比如p2 = p1),调用的是默认的运算符函数即采用的是浅层复制的方法,使二个对象p2和p1的指针ptr都指向new开辟的同一个空间。于是就出现了所谓的指针悬挂现象。当对象被析构的时候就会导致统一空间被释放二次,这当然是不允许的。

2)可以用深层复制来解决指针悬挂问题:必须显式地定义一个自己的赋值运算符重载函数,使之不但复制数据成员,而且为对象p1和p2分配了各自的内存空间,这就是所谓的深层复制。

7)构造函数

注意点:

构造函数用来创建对象执行对象的初始化操作,在创建类类型对象的时候执行,它没有返
回类型也不能声明为const成员函数。若不显式定义构造函数,则使用编译器合成的默认构造函数;
    另外C++11允许在显式定义构造函数的情况下使用默认的构造函数,语法格式为:类名() =default

委托构造函数:

        使用其他的构造函数来完成数据成员的初始化。下面的employee类的第二个构造函数是一个
委托构造函数。

class employee {int m_id;string m_name;
publicemployee(int id = 0, const string &name = " ") : m_id(id), m_name(name) { }employee(const string &name) : employee(0, name) { }
};

默认的复制(拷贝)构造函数和普通构造函数的区别:

1) 在类内声明时:
/*普通构造函数的声明,比如Box(int h,intb);*/
类名(形参列表);
/*拷贝构造函数的声明,一般情况下加const, 比如Box(const Box &b)。该构造函数的形
参为该类的引用类型,功能是将给定对象的数据成员依次复制给给正在创建的对象。需要强调
的是复制构造函数的形参必须是引用类型,即给定实参的引用。如果是非引用类型,则形参需
要通过复制实参的方式来构造,因此又需要调用复制构造函数,这样会引起一个无穷递归。*/
类名(类名& 对象名)
.............................................................................
2) 在类外建立对象时,实参类型不同,系统会根据实参的类型决定调用普通构造函数或拷贝构造
函数。比如:
/*实参为整数,调用普通构造函数*/
Box box1(1,2)
/*实参为对象名,调用拷贝构造函数,这种调用拷贝构造函数的方法 称为代入法。除了该方
法外还可以采用赋值法调用拷贝构造函数:Box box2=box1(不能分开写,否则调用的是
赋值语句;赋值语句针对的是对象的非静态成员)。*/
Box box2(box1)

阻止隐式类型转换:explicit关键字

上面提到了也可以使用赋值法来调用拷贝构造函数,需要注意的是当类型不同的时候会发生隐式类型转换。

/*通过一个整型来复制构造一个Box类对象,其实构造box3对象的过程中发生了隐式类型转换
。会先将一个整型转换为一个Box类临时对象,然后通过这个临时对象拷贝构造对象box3*/
Box box3 = 7;

但是有的时候并不希望隐式类型转换发生,因为隐式类型转换可能产生临时对象带来程序性能的下降,为了解决这个问题,C++11提供了explicit关键字来阻止隐式类型转换。比如在类中将构造函数声明为explicit后上述“Box box3 = 7” 就编译不过了(除非Box box3 = (Box)7或者使用static_cast来进行强制类型转换)。

阻止复制(拷贝)和阻止赋值:delete关键字

通常情况下,一个对象是允许拷贝或者赋值的,但有的时候不希望一个对象被拷贝或赋值给其他对象,为了解决这个问题,C++11引入了delete关键字。使用方法如下:

class Employee {public:Employee(const Enployee &) =delete;    //阻止复制Employee& operater = (const employee &) =delete;    //阻止赋值
}/*通过=delete来通知编译器哪些函数是删除的,删除的函数不能以任何形式被使用,因此,如果
有如下代码,则编译器会提示错误*/
Employee e1;    //调用默认构造函数
Employee e2(e1);    //错误:赋值构造函数是删除的,不能调用

9)析构函数

当一个对象生命期结束时,编译器会自动调用析构函数来销毁对象的成员函数。析构函数的名字由波浪号紧接类名构成,不能有返回值,也不能包含形参。

10)深复制和深赋值 

当定义一个类时,编译器将自动合成默认的复制构造函数、赋值运算符和析构函数,这些成员称为拷贝控制成员。如果类的数据成员为堆区的动态对象则需要重新定义这些成员函数,否则使用这些默认成员函数将会带来灾难。当决定一个类是否需要显式定义拷贝控制成员时,可以采用以下基本原则:首先确定是否需要显式定义析构函数,如果需要定义析构函数,则也需要显式定义复制构造函数(深复制)和重载赋值运算符(深赋值)。

11)移动对象

C++11的最主要的一个特性就是引入了右值引用和移动语义(move semantic),右值要么是字面值常量,要么是临时对象,右值引用所引用的对象具有如下特性:一是所引用的对象将要消亡;二是该对象没有其他用户。因此可以自由地接管所引用对象的资源。这样可以避免无谓的复制,大幅提升程序的性能。例如:

Mystr s1("move"), s2("constructor");
/*创建s3时,需要分配新的内存空间并复制s1+s2返回的临时对象的内容。其实,这个临时对象是一个
将亡值,程序中不会有其他对象使用它。如果能“窃取”该临时对象的资源,在构造s3的过程中不再执行
内存分配和数据的复制操作,那么会大大提升程序的性能*/
Mystr s3(s1 + s2);

如果让一个对象支持移动操作,则需要显式定义移动构造函数和移动赋值运算符,这是C++11引入的二个新的拷贝控制成员。

移动构造函数

        和复制构造函数类似,一个类的移动构造函数的参数也是该类的引用。不同的是,这个引用是个右值引用,下面为Mystr类显式定义一个移动构造函数:

/*形参rhs是实参(临时对象)的引用,在移动构造函数里面,并没有分配新的内存,也没有数据的复制,
而是将临时对象的内存“窃取”出来,用来构造正在创建的对象,并将临时对象的指针成员置为空指针,保
证它能被析构*/
Mystr::Mystr(Mystr &&rhs):m_length(rhs.m_length), m_buffer(rhs.m_buffer) {rhs.m_buffer = nullptr;    //将临时对象指针成员置为空指针rhs.m_length = 0
}
/*在Mystr类添加了移动构造函数之后,再一次分析对象s3的构造过程:Mystr类中重载的运算符函数将
返回一个局部对象。在返回时,编译器以复制该局部对象的方式构造一个临时对象,并用该临时对象构造
s3。在构造s3时,由于其形参是一个右值临时对象,因此会触发上面移动构造函数的调用。在构造完成s3
之后,这个临时对象便消亡了,但在它消亡之前,成功将资源“窃取”出来,构造了s3。换句话说,可以认
为s3是该临时对象的延续。*/
Mystr s3(s1 + s2);

这里需要注意,虽然复制构造函数的形参(const左值引用)也能匹配右值临时对象,但需要进行一次到const的转换。根据重载函数的匹配原则,构造s3时会调用移动构造函数,因为右值引用是精确匹配。

移动赋值运算符

和移动构造函数的思想类似,移动赋值运算符也可以避免数据的复制,提高程序的性能。下面为Mystr类定义一个移动赋值运算符:

Mystr& Mystr::operator = (Mystr &&rhs) {if(this != &rhs) {    //这行代码不能少,避免自身赋值引发内容错误delete [] m_buffer;m_length = rhs.m_length;m_buffer = rhs.m_buffer;rhs.m_buffer = nullptr;    //将临时对象指针成员置为空指针    rhs.m_length = 0;}return *this;
}Mystr s1("move"), s2("assignment"), s3;
/*当执行如下代码时,会触发移动赋值运算符的调用,从而将临时对象的资源“窃取”给s3*/
s3 = s1 + s2;

12)嵌套类(在一个类的内部定义的类)

嵌套类经常用于实现外层类的某些特殊功能,这些功能的底层实现对于外层类的用户来说是隐藏的。嵌套类的对象不包含外层类的成员,也不能直接访问外层类的成员;同样,外层类的对象也不包含任何嵌套类的成员,对嵌套类成员也没有任何访问特权。

/*实现一个二维数组类,该类可以像二维数组那样支持两个下标运算符(C++仅支持一位下标运算符的
重载,因此不能直接重载两个下标运算符)。实现思路为:Array2D类重载[]函数返回另一个类Array1D类的对象,然后让Array1D类的对象进一步
调用其重载的[]函数*/
template<typename T>
class Array2D {
private:/*Array1D用于嵌套类,来实现Array2D的第二个下标运算符的功能。定义在Array2D的private访问限定区域内的好处是可以将Array2D的第二个下标运算符的底层实现隐藏起来,用户不需要知道Array1D的存在,在语法上也无法访问Array1D。*/class Array1D {friend class Array2D;public:~Array1D() {delete[] m_arr;}T& operator[] (int idx) (idx) {return m_arr[idx];}private:size_t m_size = 0;    //第二维长度T* m_arr = nullptr;};size_t m_size;    //第一维长度Array1D *m_arr;    //元素类型为Array1D
public:Array2D(size_t s1, size_t s2):m_size(s1), m_arr(new Array1D[s1]) {for(int i = 0; i < m_size; i++) {m_arr[i].m_size = s2;m_arr[i].m_arr = new T[s2];}}~Array2D() {delete[] m_arr;}Array1D& operator[](int idx) {return m_arr[idx];}size_t size() {return m_size;}
}
/*有了上面的实现,就可以像普通二维数组那样使用Array2D了*/
Array2D<int> arr(2, 2);
/*对于arr[0][0],编译器首先调用Array2D的成员函数[],该成员函数返回arr[0]的引用,即一个
类型为Array1D的引用。然后对于第二个下标运算符,编译器调用Array1D的成员函数[],返回arr[0][0]
的引用*/
arr[0][0] = 1;

..................................................................

 union类型

一个union类型由关键字union、类型名和一系列的成员声明组成

union ID {//名为ID的union类型,使用时只会有一个数据成员处于激活状态,其他数据成员处于未定义态char char_type;int int_type;long long llong_type; //一个ID类对象的内存长度为long long类型的内存长度
}
/*对于union类型对象,可以使用花括号对其第一个数据成员赋值,其他成员将处于未定义状态。通过
普通的成员使用操作就可以激活指定成员为定义后的状态,并使其余成员变成未定义状态*/
ID wang = {'a'};    //构造了一个ID类对象,为其第一个成员赋值
cout << wang.char_type << endl;    //输出a
wang.int_type = 1001;    //激活使用第二个成员
cout << wang.int_type << endl;

除不能包含引用类型的数据成员外,class的大多数特性适用于union,其数据成员也可以用private、public和protected修饰符。默认为public。union中也可以定义成员函数,包括构造函数和析构函数。但union类不能继承其他类,也不能被继承,因此,也不能有虚函数。如果成员为类类型,则当成员的状态发生变化时,将会调用相应成员的析构和构造函数。

..................................................................

继承和多态

继承是代码重用的重要手段之一,多态能够使不同的对象在接受相同的信息时,引发不同的行为。

1)继承

        如果一个派生类只有一个基类,则这种情况称为单继承,否则称为多继承(多继承的派生类继承所有的基类成员)。使用class定义类的默认继承方式为privare,使用struct定义类的默认继承方式是public。

需要注意的是基类的构造函数和析构函数不能被继承, 即除了父类的生死过程外,子类都可以继承。

/*
1) public继承方式基类中所有public成员在派生类中为public属性;基类中所有protected成员在派生类中为protected属性;基类中所有private成员在派生类中不可直接访问。(但可以调用基类的方法进行访问,这时需
要加上作用域限定符:: 。 需要注意的是私有成员只是被编译器隐藏了,但是还可以继承下去。)
2) protected继承方式基类中的所有public成员在派生类中为protected属性;基类中的所有protected成员在派生类中为protected属性;基类中的所有private成员在派生类中仍然不可直接访问。
3) private继承方式基类中的所有public成员在派生类中均为private属性;基类中的所有protected成员在派生类中均为private属性;基类中的所有private成员在派生类中均不可直接访问。
*/
class Base {
private:int m_pri;
protected:int m_pro;
public:int m_pub;
};
/*pubDerv公有继承Base,因此在派生类pubDerv的成员函数foo里,可以访问基类Base的受保护的成
员m_pro,但不能访问基类私有成员m_priv。*/
class PubDerv :public Base {void foo() {m_pri = 10;    //错误:不能访问Base类私有成员m_pro = 1;    //正确:可以访问Base类受保护成员}
};
/*priDerv私有继承Base,这不会影响priDerv成员对Base类成员的访问权限,但它会影响privDerv类
对象对Base类的访问权限*/
class priDerv :private Base {void foo () {m_pro = 1;    //正确:可以访问Base类受保护成员m_pub = 1;    //正确:可以访问Base类公有成员}
};/*在普通函数test的函数体里不能访问Base类中受保护的成员m_pro*/
void test() {Base b;b.m_pro = 10;    //错误:不能访问Base类受保护成员
}pubDerv d1;
priDerv d2;
d1.m_pub = 10;    //正确:m_pub在pubDerv中是公有的
/*Base类中受保护的和公有的成员在派生类priDerv中都变成了私有的,因此priDerv类用户不可以
访问其中Base类中继承的公有成员。另外需要注意如果派生类以受保护的方式继承基类,那么基类的
受保护成员和公有成员在派生类里都是受保护的;和私有继承类似,派生类对象不可以访问基类的公
有成员*/
d2.m_pub = 1;    //错误:m_pub在priDerv中是私有的

final关键字

C++11提供的关键字final可以用来阻止继承的发生。

class NoDerived final { };    //NoDerived不能作为基类

using声明

using声明改变派生类中基类成员的访问权限

//使用using声明需要注意派生类只能为它可以访问的名字提供using声明
class pubDerv :public Base {
public:using Base::m_pro    //声明为公有的
}
/*基类成员m_pro的访问权限本来应该是受保护的,在pubDerv类外面是不能访问的,但在派生类
里面把m_pro的using声明放到public访问限定符的作用域中,m_pro的访问权限变为公有的,也就
意味着pubDerv的用户是可以直接访问的*/
pubDerv d;
d.m_pro;    //正确

        使用using声明来解决派生类和基类中的命名冲突:和其他作用域命名冲突一样,如果派生类成员和基类的成员名字相同(如果成员函数,只要名字相同就会隐藏不用看返回值和形参),那么定义在派生类的名字将会屏蔽掉基类的名字(基类的成员被隐藏),如果在派生类里面需要访问基类的同名成员有二种办法,一种是使用基类的作用域运算符,另一种是在派生类中使用using声明。

class Base {
protected:int m_data;
public:void foo(int) {/*...*/}
};class Derived :public Base {
protected:int m_data;
public:int foo() {return m_data;}
}Derived d;d.Base::foo(10);    //调用Base::foo/*using声明语句只需要提供一个名字,不需要形参列表。*/
class Derived :public Base {
public:using Base::foo/*..*/
};
d.foo(10);    //调用Base::foo

类型转换

一个派生类不仅包含自己定义的(非静态)成员,而且还包含其从基类继承的成员。因此可以将派生类对象当成基类对象使用,也就是说可以将基类的指针或引用与派生类对象绑定,例如:

/*这种转换称为派生类到基类的转换,实际上可以这样理解:基类指针或引用指向的是派生类对象中
从基类继承的部分。*/
PartTimeWorker w("Kevin", 21);
Persion p, *ptr;
ptr = &w;    //基类指针ptr指向派生类对象w
Persion &p2 = w;    //基类引用绑定到派生类对象
/*下面一行代码将调用基类的赋值运算符,其形参类型为基类的const引用,实参类型为派生类。实际
上赋值运算符处理的只是派生类中从基类继承的部分。与其他隐式类型转换一样,这种转换是编译器自
动完成的。*/
p = w;    //派生类对象赋值给基类对象

虽然派生类可以自动转换为基类的引用或指针,但没有从基类到派生类的自动转换。这是显而易见的,因为基类对象不能提供派生类对象新定义的部分,例如:

PartTimerWorker *w2 = &p;    //错误,不能将基类转换为派生类
/*下面代码将调用派生类的赋值运算符,该运算符的形参为派生类的const引用,但实参p为基类对象,
不可以进行类型转换*/
w = p;    //错误;不能将基类转换为派生类

派生类到基类的自动转换的前提是公有继承(如果派生类以私有或者受保护的方式继承基类,那么派生类不能自动转换为基类类型)。公有继承意味着派生类对象也是基类类型,和基类的关系是IS-A关系,即派生类对象也是基类对象。因此可以用派生类对象来创建一个基类对象,例如:

PartTimerWorker w("Kevin", 21);    //派生类对象
Persion(w);    //利用派生类对象构造基类对象

派生类对象的构造和析构

尽管派生类对象含有从基类继承的数据成员,但派生类的构造函数不能直接初始化这些成员。派生类必须使用基类的构造函数来初始化从基类继承的成员,如果派生类构造函数的初始化列表没有显式调用其直接基类的构造函数,那么编译器将调用其直接基类的默认构造函数(直接基类成员指向默认初始化,而非使用提供的初始值。当然不希望发生这种情况)。如果派生类有多个直接基类,按照在派生类构造函数后面紧跟的列表的顺序依次初始化。也就是说对于存在继承关系的类的成员初始化:在派生类对象构造过程中,每个类仅负责自己的成员初始化。

class Mammal {
public:virtual void feedMilk() {}    //母乳喂养
};
class WingedAnimal {
publicvirtual void flap() {}    //振翅飞翔
};
class Bat:public Mammal, public WingedAnimal {};
/*调用基类的构造函数可以采用隐式的方式也可以采用显式的方式*/
Bat::Bat() {}    //隐式调用Mammal和WingedAnimal的默认构造函数
Bat::Bat():Mammal, WingedAnimal() {}    //显式调用基类的默认构造函数

析构函数负责释放成员空间,每个类也是仅负责自己的成员。当一个派生类对象消亡时,成员析构的过程与构造的过程相反。

派生类的复制构造函数和移动构造函数和赋值运算符
       类似于派生类对象的构造,一个派生类对象在复制或移动或赋值的时候,除复制或移动或赋值自有成员外,还要复制或移动或赋值基类部分成员。

class A {/*....*/}
class B: public A {string m_d;
public:/*调用派生类B的复制构造函数时,在类B的复制构造函数的初始化列表中显式调用基类A的复制构造函数A(d),基类A的复制构造函数的引用形参将与B类对象d绑定,完成基类成员的复制。如果没有显式调用基类A的复制构造函数,那么编译器将调用基类A的默认构造函数,这很可能导致不正确的复制,因为基类成员指向默认初始化,而非复制d的基类成员值*/B(const B &d) :A(d)/*复制A的成员*/,m_d(d.m_d)/*复制B的成员*/{/*......*/}/*与复制构造函数一样,如果想要移动基类部分成员,必须在派生类移动构造函数的初始化列表中显式调用基类的移动构造函数。通过move函数可以将左值转换为右值,进而触发移动语义*/B(B &&d) :A(std::move(d))/*移动A的成员*/, m_d(std::move(d.m_d))/*移动B的成员*/{/*......*/}/*与复制和移动构造函数类似,必须在派生类的赋值运算符中显式调用调用基类的赋值运算符,才能完成基类成员的赋值*/B& operator = (const B& d) {if(this == &d)    return *this;A::operator = (d);    //赋值A的成员m_d = d.m_d;    //赋值自身成员return *this;}
};

虚继承(虚拟继承基类)

/*考虑到Mammal和WingedAnimal都属于动物类,有着共同的属性和行为,将这两个类进一步抽象,
设计一个公共基类Animal*/
class Animal {
protected:int m_age;
public:Animal(int n = 0):m_age(n) {}virtual void eat() {}
};class Mammal :public Animal {
public:virtual void feedMilk() {}
};
class WingedAnimal :public Animal {
publicvirtual void flap() {}
};
class Bat:public Mammal, public WingedAnimal {};/*若按照上面这种方式设计,Mammal和WingedAnimal都会分别继承公共基类Animal的成员,Bat类继承
Mammal和WingedAnimal类的所有成员,即Bat类拥有两份Animal类成员的副本,分别来自它继承的两个
直接基类Mammal和WingedAnimal,因此会造成二义性问题。换句话说只要继承层次中出现了菱形关系,就
会出现二义性问题。这种问题也被称为死亡钻石(diamond of death)。例如,当如下Bat类对象b访问公
共基类Animal的成员时,会产生二义性问题:*/
b.eat();    //错误,二义性访问
Animal a = b;    //错误,类型无法转换

C++通过虚继承的机制来解决上面的问题。在某个类的派生列表中将其基类声明为虚基类(virtual base class),不论该虚基类在继承体系中出现多少次,在派生类中只包含唯一一份共享的虚基类成员。通过在派生类列表中添加关键字virtual来指定虚基类,例如:

/*关键字virtual的位置可以放到继承方式说明符的前面或者后面*/
calss Mammal :virtual public Animal {/*...*/};
class WingedAnimal :virtual public Animal {/*...*/};

通常情况下,类的构造函数只初始化自己的成员及其基类成员,但虚继承的基类例外,它们由底层的派生类进行初始化。因此,Animal不是由Mammal和WingedAnimal初始化,而是由Bat初始化。

class Bat:public Mammal, public WingedAnimal {
public:/*如果这里Bat构造函数没有显式调用Animal的构造函数,则调用Animal的默认构造函数。如果Animal没有默认的构造函数,则产生编译错误*/Bat():Animal(1), Mammal(), WingedAnimal() {}
};

2)虚函数与多态性

C++语言支持二种多态,其中编译时的多态性指的是程序在编译时调用哪一个版本的函数,它通过函数重载(包括运算符重载)和模板实例化来实现的。运行时的多态可以简单理解为“一个接口,多种实现”,即在程序运行时才能决定调用的函数,这是面向对象程序设计的核心思想;它是通过 继承关系和虚函数来实现的。

虚函数

有的时候基类的成员函数在不同的派生类中需要不同的实现/重写,因此可以把此函数声明为虚函数。将类的某成员设为虚函数后,该类的对象中就会多出了一个成员__vfptr,指向虚表vftable (表内记录虚函数地址,需要注意在类里面无论有多少个虚函数,虚表只有一个,实现多态就是要靠这个虚表来完成的)。一旦派生类重写了基类的方法,会用该派生类方法的地址去覆基类中该方法在虚表中的地址(在虚表中每个地址都对应一个方法)。若不重写,虚表里装的是基类的方法和地址,所以当用基类指针去访问派生类方法的时候就不行。

    构造函数不能声明为虚函数,多态是指不同对象对同一消息有不同的行为特性。虚函数作为运行
过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此构造函数的多态是
没有意义的。派生类版本的声明必须与基类版本的声明完全一致,包括函数名、形参列表和返回值类型。如果参数
具有默认值,则各个版本中对应形参的默认值也必须相同。但有一个例外,基类版本返回基类指针或引用,
派生类版本可以返回派生类指针或引用。内联函数、静态成员和模板成员均不能声明为虚函数,因为这些成员的行为必须在编译时确定,不能
实现动态绑定。若基类的某个函数声明为虚函数,则派生类对应的重写版本自动为虚函数,可不必进行virtual声明。

动态绑定

对象的静态类型指对象声明时的类型或表达式生成时的类型,在编译时就已经确定;动态类型指的是指针或引用所绑定的对象的类型,尽在运行时可知。如果一个对象既不是指针也不是引用,那么它的静态类型和动态类型一致。

class Base { };
class Derived :public Base { };
Derived d;    //d的静态类型和动态类型都是Derived
Base *p = &d;    //指针p的静态类型的Base, 动态类型为Derived

除了需要重写基类的虚函数外,动态绑定必须通过基类指针或引用绑定到派生类对象才能触发。但是需要注意动态绑定的实现是有代价的,每个派生类需要额外的空间保存虚函数的入口地址,函数的调用机制也是间接实现的,动态绑定的实现是以时间和空间为代价的,因此大量的虚函数会导致程序性能的下降。

虚析构函数

通常情况下,基类的析构函数应该是虚函数,保证正确析构一个动态派生类对象。

/*由于Shape类的析构函数为虚函数,因此在执行delete操作时,将会执行p的动态类型的析构,即
派生类Circle的析构函数,然后再执行基类Shape的析构函数,从而保证p指向的动态Circle类对象
能够正确释放内存。
如果基类析构函数为非虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
*/
Shape *p = new Circle();    //这里Shape为基类,Circle为派生类。
delete p;

final和override说明符

动态绑定是通过同名虚函数的覆盖来实现的,对该实现有严格的语法要求,某些忽略可能会导致错误的行为。例如,派生类版本和基类版本的虚函数形参不一致,编译器会认为这二个版本的函数是相互独立的,这会导致派生类版本的函数没有覆盖掉基类的版本,也不会产生多态的行为。为了避免这类错误,C++11引入了override关键字以显式说明派生类的函数要覆盖基类的虚函数。类似地,可以使用关键字final阻止派生类覆盖基类版本的虚函数。

struct B {virtual void f1(int) { }virtual void f2() { }void f3() { }
};
struct D1 : public B {void f1() override { }    //错误,基类没有不带参数的f1函数void f2() final { }    //D1::f2为最终版本void f3() override { }    //错误,基类没有可覆盖的函数
}
struct D2 :public D1 {void f2() { }    //错误,不允许覆盖基类D1中的f2函数
};

抽象类

在多态中,通常基类中虚函数的实现是毫无意义的,主要都是调用派生类重写的内容,也不希望用户创建一个这样的基类对象。C++允许将这样的虚函数声明为纯虚函数(在虚函数的声明结束处加上 =0)。

含有纯虚函数的类(包含派生类中有未被覆盖的纯虚函数的类)为抽象类。抽象类只负责接口的声明,而接口的定义由派生类来负责(派生类必须重写抽象类的中的纯虚函数,否则也属于抽象类)。另外抽象类不能创建类的实例。

公有继承方式下的基类成员函数的继承与覆盖 总结:

    不要重新定义基类非虚函数,所有作用于基类的非虚操作都适用于它的派生类。如果需要重新定义基类函数,则该函数应该声明为虚函数。派生类继承基类非虚函数的接口和实现、虚函数的接口和默认实现,以及纯虚函数的接口。一般情况下,纯虚函数不需要定义,但可以为纯虚函数提供定义,而且必须放在类外。

3)重载和隐藏和覆盖的区别

重载:在同一类中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载。函数重载发生在相同作用域

隐藏:派生类的方法隐藏了基类的所有同名方法,要求是函数名相同并且基类里不能是虚方法,参数列表和返回值不做要求;根据这种同名隐藏的原则,我们仍可以通过::来访问基类被隐藏的基类函数

覆盖:强调的是虚表里的覆盖,针对的是多态,所以基类方法中要有virtual关键字。函数覆盖就是函数重写。准确地叫做虚函数覆盖和虚函数重写。

..................................................................

异常处理

异常处理(exception handling)允许将异常检测和解决的过程分离开来,程序中某一个模块出现了异常不会导致整个程序无法正确运行。C++语言提供了异常内部处理机制,该处理机制涉及三个关键字:try、catch和throw。它们的功能分别是:检测可能产生异常的语句块、捕获异常和抛出异常。

1)抛出异常

当程序在运行期间出现异常时,可以通过throw来抛出一个异常。例如以下函数返回a除以b的结果,如果出现除数为0的情况,则抛出一个异常:

double divide(int a, int b) {if(b == 0)/*当执行throw语句时,其后面的语句不会被执行。程序的控制权将转移到与之匹配的catch模块。*/throw "Error, division by zero!";return a / b;
}/*throw可以抛出任意类型对象。通常情况下,抛出的异常为错误的编号、错误描述或用户自定义的
异常类对象。例如上面的代码抛出的异常为一个C风格字符串常量。还可以抛出:*/
throw -1;    //抛出一个整型数
throw x;    //x为double类型对象
throw MyException("Fatal Error");    //MyException为一个类类型

2)检测异常

通过关键字try来检测可能发生异常的代码。通常情况下,将可能发生异常的代码放到try语句块中,该语句块中的任何异常都可以被检测到。例如:

/*一旦在try语句块内部有异常抛出,系统则检查与该try块关联的catch子句,并寻找与异常相匹配
的catch字句*/
try {divide(a, b);    //函数调用语句
}

3)捕获异常

最终通过catch子句捕获异常,并处理它:

/*catch语句中的异常声明与只包含一个形参的函数形参列表类似。异常声明包括类型和名字,其中类型
决定了该catch子句能够捕获的异常的类型,它可以为左值引用,但不能为右值引用*/
catch(const char*str) {    //捕获一个C风格字符串常量对象/*任何能够被char*接受的异常都将被捕获。其中cerr为标准错误ostream对象,常用于输出程序错误信息。*/cerr << "捕获异常" << str << endl;}

当异常被抛出后,其后面的catch子句依次被检查是否与抛出的异常匹配,如果找到,则执行该catch子句中的异常处理代码。如果没有找到,则沿着调用链向外逐层检查与try块匹配的catch子句。如果在整个调用链中都没有找到匹配的catch子句,则调用标准库函数terminate()来终止当前程序的执行。

通常情况下,异常的类型与catch声明的类型要求严格匹配,但不包括以下情况:a. 允许从非常量到常量的转换,即抛出的非常量异常可以被常量catch声明捕获;b. 允许从派生类到基类的转换;c. 数组或函数被转换成指向数组元素或函数的指针。

//下面将try、catch和throw放在一起
int a = 1, b = 0;
try {int c = divide(a, b);    //函数调用语句
}
catch(const string &str) {cerr << "捕获异常" << str << endl;
}
catch(const char* str) {    //在divide函数里抛出的异常类型为const char *,所以在这里捕获cerr << "捕获异常" << str << endl;}

4)使用标准库异常类

C++标准库提供了标准异常类,使用时需要包含头文件exception。标准异常类的继承关系中,基类exception只定义了默认的构造函数、复制构造函数、复制运算符、虚析构函数和一个名为what的虚成员。what函数返回一个const char*,该指针指向一个以null为结尾的字符数组,它可以获取发生异常的类型。该函数不会抛出任何异常。可以继承exception类,并使用自定义版本的what成员。

/*noexcept是C++11标准引入的新关键字,用来指明某个函数不会抛出异常。noexception说明必须放到
形参列表后面,且函数的声明和定义处必须出现。如果是成员函数,则放置在const限定符之后,在final、
override或纯函数 =0之前*/
struct myExcetpion :public exception {const char* what()const noexcept {return "Ooops!";}
};/*下面代码将抛出一个myException异常对象,该对象可被异常声明为基类excetpion类型的catch子句捕获*/
try {throw myException();
}
catch(const exception &ex) {cerr << ex.what() << endl;
}

..................................................................

运行时类型识别(Run-Time Type Identification, RTTI)

RTTI指的是通过基类的指针或引用来检查其指向的派生类型。RTTI提供了两个非常有用的运算符:typeid和dynamic_cast。这二个运算符作用于基类的指针或引用,如果该类型含有虚函数,则返回基类指针或引用的动态类型;否则返回该类型的静态类型。

dynamic_cast一般用于基类指针或引用到派生类的转换,因为C++支持派生类指针或引用到基类的自动转换。成功转换的前提是,expr的类型必须是type的基类、派生类或type本身;否则转换失败。在转换失败的情况下,如果是指针类型转换,则返回空指针;如果是引用转换,则抛出一个std::bad_cast异常(bad_cast的基类为excetion)。

/*dynamic_cast的使用方式有如下3种。其中type必须是一个类类型,通常情况下,该类型应含有
虚函数成员。在第一中方式中,expr必须为有效的指针;在第二种方式中,expr必须为左值;在第
三种方式中,expr必须为右值。*/
dynamic_cast<type*>(expr)
dynamic_cast<type&>(expr)
dynamic_cast<type&&>(expr)//1)使用指针类型的dynamic_cast:
struct Base {virtual ~Base() {}
};
struct Derived:Base {void name() {}
};
/*在下述两处转换调用均将Base类型指针转换为Derived类型指针。对于第一个转换,由于b1与基类
对象绑定,不能将其转换为派生类型,因此转换失败,d的结果为空指针,不会执行name调用;对于第
二个转换,虽然b2也是基类指针,但由于b2与派生类对象绑定,因此可以将其转换为派生类型,转换的
结果为将b2的值赋给d,并调用name函数。*/
Base *b1 = new Base, *b2 = new Derived;
if(Derived *d = dynamic_cast<Derived*>(b1))d->name();    //转换失败,d为nullptr,不会执行此调用
if(Derived *d = dynamic_cast<Derived*>(b2))d->name();    //转换成功,执行此调用//2)使用引用类型的dynamic_cast:
/*引用类型和指针类型的dynamic_cast的区别在于错误的报警方式不同。由于没有空引用,因此当
引用类型转换失败时,程序将抛出一个std::bad_cast异常,该异常定义在typeinfo标准库文件中。
例如上面的Base类指针所指向的对象转换为Derived类的引用时,会抛出此异常。*/
try {    //转换失败,抛出std::bad_cast异常Derived &d = dynamic_cast<Derived&>(*b1);
} catch(std::bad_cast) {cout << "downcast failed" << endl;
}

关键字typeid用来查询一个类型的信息。

/*typeid有如下两种格式。其中,type为任意类型,expr为任意表达式。如果表达式类型是类类型且包含
虚成员函数,那么需要在运行时计算并返回表达式的动态类型;否则,typeid运算符将返回表达式的静态
类型,在编译时就可以获得*/
typeid(type)
typeid(expr)
/*typeid操作符的返回结果是名为type_info的标准库类型对象的引用。该类型定义在标准库头文件
typeinfo中,支持如下四种操作。其中type_info的name成员函数返回一个C风格字符串,用来表示相应
的类型名,但这个返回的类型名与程序中使用的类型名并不一定一致,具体由编译器的实现决定。*/
t1 == t2    //如果两个type_info对象t1和t2类型相同,则返回真,否则返回假
t1 != t2    //如果两个type_info对象t1和t2类型不同,则返回真,否则返回假
t1.name()    //返回类型的C风格字符串,类型名字用系统相关的方法产生
t1.before(t2)    //返回一个bool值,表示t1类型是否出现在t2类型之前/*通常情况下,使用typeid比较两个表达式的类型是否相同,或者比较一个表达式的类型是否与指的的类型
一致,例如:*/
Derived *d = new Derived;
Base *b = d;
if(typeid(*d) == typeid(*b)) {/*检查d和b指向的对象的动态类型是否相同,如果相同则检测成功*/}
if(typeid(*b) == typeid(Derived)) {/*检查b是否指向Derived类型对象*/}

..................................................................

显式类型转换(强制类型转换)

除了由编译器根据需要自动进行的隐式类型转换外,也可以手动强制将一个对象显式转换为另一种相关的类型,C++提供了4种强制类型转换的方法。

1)static_cast <type> (expr)

在算术表达式中,常用static_cast来执行以下二种操作。

执行浮点数操作,例如:

int i = 5, j = 3;
double k = i /static_cast <double> (j)   //强制将j的值转化为double类型

告诉编译器用户有意将宽类型转换为窄类型,请关闭警告信息,例如:

double i = 5., j = 3.;
int k = i /static_cast <int> (i / j)   //强制将i / j的结果转化为int类型

2)dynamic_cast <type> (expr)

3)reinterpret_cast <type> (expr) 和 const_cast <type> (expr)

const_cast用来去掉对象的const属性,即把const对象转换成非const对象。

在程序中一般不不使用这二种。

..................................................................

函数模板/类模板/成员模板

1)模板的特例化

特例化一个模板时,模板参数列表为空,标明将显式提供所有模板实参。需要注意的是一个特例化的函数模板并不是模板重载,本质上是一个实例。

templete<>
/*形参a,b分别为一个指向const char对象的const指针的引用*/
const char* const& getMax(const char* const &a, const char* const &a) {return strcmp(a, b) > 0 ? a:b;const char *a = "hi", *b = "hello";
cout << getMax(a, b) << endl;    //输出hi

2)可变参函数模板

C++11允许使用数目可变的模板参数,可变数目的参数称为参数包,用省略号表示,可包含从0到任意个模板参数。例如:

template <typename...Args>
void foo(Args...args) {    //形参args为模板参数包类型,接受可变数目的实参cout << sizeof...(args) << endl;    //使用sizeof...运算符打印参数包args中参数的个数
}foo();    //输出0
foo(1, 1.5);    //输出2
foo(1, 1,5, "C++");    //输出3

包展开

/*可以通过递归的方式展开函数模板参数包:第一次处理参数包中的第一个参数,然后用剩余
参数调用自身*/
template <typename T, Typename...Args>
ostream& print(ostream &os, const T &t, const Args&...rest) {os << t << " ";    //打印第一个参数return print(os ,rest...);    //递归调用
}
/*此外,还需要定义一个带有非可变参的终止函数*/
template <typename T>
ostream& print(ostream &os, const T &t) {return os << t;    //打印最后一个参数
}

上面print模板函数带有3个参数,但是函数体中递归调用语句只有二个参数,执行过程为:参数包rest中的第一个参数与形参t绑定,剩余的参数组成一个新的参数包,当参数包里面只生剩下一个参数时,非可变参模板与可变参模板都匹配,但非可变参模板更特例化,编译器将首选非可变参数模板,例如:

print(cout, 1, 2,5, "C++") << endl;    //输出1 2.5 C++

转发参数包

当把一个右值引用的参数转发给另一个函数时,这个参数就会变成左值。例如:

void rvalue(int &&val) { }templete<typename T>
void forwardValue(T &&val) {/*以下会发生错误:forwardValue的形参val为右值引用,但是val在rvalue调用中变成了一个左值, 而rvalue接受右值形参*/rvalue(val);
}/*以下调用会产生一个无法转换为右值引用错误*/
forwardValue(42);    //错误,不能将int转换为int &&

有时候,需要一个函数能够将函数的参数连同参数的类型不变地转发给其他函数。在转发的过程中,需要保持实参的所有性质,包括const属性和左值或右值属性。在C++11新标准下,可以使用std::forward函数实现完美转发。它可以实现完全依照参数类型将它们转发给其他函数:

/*函数模板的参数类型T是通过函数形参val推断出来的,val类型为右值引用。如果实参为一个右值,则T
为一个非引用的普通类型,forward<T>将返回T&&。如果实参是个左值,则右值引用val将与左值绑定(指
向左值的右值引用),这是不允许的,但在C++11中,这种情况是个例外。这时,T将会实例化为左值引用T&,
那么forword<T>将返回一个左值引用,从而保证实参的所有细节。*/
template <typename T>
void forwardValue(T &&val) {rvalue(std::forward<T>(val));
}提示:在模板函数形参类型推断中,如果模板函数形参是一个指向模板类型参数的右值引用T&&,则它可以与
一个左值绑定。当模板实参类型为一个左值引用时,模板函数的形参类型被实例化为一个左值引用T&。

可以组合使用可变参模板和forward函数,实现参数包完美转发:

template <typename...Args>
void fun(Args&&...args) {    //foo函数负责将所有的实参不变地传给foo函数foo(std::forward<Args>(args)...)
}/*上面的参数扩展模式std::forward<Args>(args)... 相当于std::forward<Ti>(ti)。
其中Ti为参数包中第i个参数ti的类型。例如有如下foo函数和函数调用*/
void foo(const string &s, int &&i) {cout << s << i << endl;
}
/*该调用将扩展为std::forward<const char*>("abc"), std::forward<int>(42)*/
foo("abc", 22); 

3)类模板成员函数定义

类模板成员函数具有与类模板相同的模板参数,因此如果在类模板之外定义成员函数,则它们必须以关键字template开始,后接与类模板相同的模板参数列表。注意,紧随类名后面的参数列表代表一个实例化的实参列表,每个参数不需要typename/class说明符,例如:

template <typename T, size_t N>
class Array {T m_ele[N];
public:Array() { }/*initializer_list是C++11标准库提供的新类型,支持具有相同类型但数量未知的列表类型。因此,利用initializer_list类型形参可以实现以列表初始化的方式来构造对象。*/Array(const std::initializer_list<T>&);T& operator[] (size_t i);constexpr size_t size() {return N;}
}
/*下面的构造函数初始化列表中,利用T类型的默认初始化方式(T())初始化m_ele中的每一个元素。*/
template <typename T, size_t N>
Array <T, N>::Array(const std::initializer_list<T> &1):m_ele{T()} {size_t m = 1.size() < N ? 1.size() : N;for(size_t i = 0; i < m; ++i) {m_ele[i] = *(1.begin() + i);}
}
/*下标运算符函数返回数组m_ele中第i个元素的引用*/
template <typename T, size_t N>
T& Array<T, N>::operator[](size_t i) {return m_ele[i];
}

4)类模板的实例化

/*下面代码在编译时创建另一个实例类Array<int, 5>, 创建对象b时,将指向上面的带形参的
构造函数,其中形参1接受初始化列表{1, 2, 3},其余元素具有默认值0*/
Array<int, 5>b = {1, 2, 3};    //创建一个Array<int, 5>类型对象bfor(int i = 0; i < b.size(); ++i)cout << b[i] << " ";    //输出结构为1 2 3 0 0

另外需要注意:类模板实例化时类模板中的成员函数并没有实例化,成员函数只有在被调用时才会被实例化(即产生真正的成员函数) ,成员虚函数除外。

5)类模板的分文件编写的2种解决办法

类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到,因此类模板的分文件编写有二种解决办法。第一种解决方式是直接包含源文件(比如:#include "person.cpp");第二种解决方式是将.h和.cpp中的内容写到一起并将后缀改写为.hpp,这种方法比较常用,但是.hpp后缀只是约定俗称,看到.hpp就知道是类模板,并不是必须的。

..................................................................

堆区(也叫动态内存区或自由存储区)内存的管理

1)new和delete

set_new_handler机制

C++的set_new_handler机制:当开辟不成功时会调用set_new_handler设置的函数

#include <iostream>
using namespace std;void out_of_memory()
{cout << "out of memory!" << endl;
}void main()
{set_new_handler(out_of_memory);int *p = new int[222222222];cout << "OK" << endl;
}

动态数组

可以使用new来创建动态数组,使用delete []来释放。

int n = 5;
/*方括号中必须是整型,但不必是常量,值可以为0(为0时不能解引用)。数组中的每个元素都执行默认
的初始化。返回第一个元素的地址*/
int *pa1 = new int[n];    //5个未定义的int
/*可以为数组中的元素执行值初始化,方法是在括号后面跟一对空括号*/
int *pa2 = new int[n]();    //5个值为0的int
/*C++11中,可以使用花括号来实现数组元素的初始化*/
int *pa3 = new int[n]{1, 2, 3, 4, 5};    //5个值为0的int/*释放,pa1必须为指向动态分配的数组或为空*/
delete [] pa1;

2)智能指针(都是类模板)

编译器不能分辨指针所指向的对象是否为堆区的动态对象,也不能判断指针所指向的动态内存是否已被释放。对于这样的bug,大多数编译器不会报错,需要程序员仔细查找。对于一个指向堆区动态内存的指针,当执行delete后,它便无效了,但是该指针依然存有内存地址,此时的指针也称为空悬指针(dangling pointer),该指针的危害了类似于未初始化的野指针;为了避免出现空悬指针,通常在delete之后重置该指针为nulllptr。另外对于堆区内存,new后若不及时delete还会导致内存泄漏。

可见通过new和delete直接分配和释放动态内存,很容易引起空悬指针或内存泄漏等问题,为此引入了智能指针。用于控制动态对象的生命期,能够保证自动释放动态内存,从而防止内存泄漏。在C++11中支持以下三个智能指针(其中c++98的atuo_ptr已经被c++11弃用)。

unique_ptr (不允许多个指针共享资源)

unique_ptr独自拥有所指向的动态对象,也就是说只能有一个unique_ptr指向给定的对象。接受指针参数的智能指针的构造函数为explicit来阻止隐式类型转换,因此初始化一个unique_ptr必须采用直接初始化方式(直接初始化使用普通函数匹配方式来选择构造函数,而复制初始化将=右侧的对象复制给待创建的对象,如果类型不一致,编译器尝试通过构造函数进行隐式类型转换,如果没有可用构造函数,将产生类型无法转换的错误),例如:

{/*p1为nullptr*/unique_ptr <string> p1;/*直接初始化方式创建p2, p2消亡时,p2所指向的对象也会消亡,完成动态内存的自动释放,在C++14中可以使用make_unique标准库函数模板来初始化unique_ptr,使用时需要提供类型和初始化列表,例如unique_ptr <int> p3 = make_unique <int> (207)*/unique_ptr <int> p2(new int(207));
} //p1和p2离开作用域,被销毁,同时释放其指向的动态内存

智能指针的使用和普通指针类似,解引用时返回其指向的对象,例如:

unique_ptr <string> p1(new string("Mandy")); //p1指向string类型对象
if(p1 && p1->empty())    //指针p1非空且其指向的对象为空string*p1 = "Lisha";

一个unique_ptr不能被复制,也不支持赋值(可以复制或赋值一个将要被销毁的unique_ptr,如从函数返回的unique_ptr);但可以通过relese或者reset将一个动态内存的所有权从一个unique_ptr转移给另外一个unique_ptr。例如:

unique_ptr<int> p1(new int(207));
unique_ptr<int> p2(p1);    //错误,unique_ptr不允许复制
unique_ptr<int> p3;
p3 = p2;    //错误,unique_ptr不支持赋值/*release放弃动态内存的拥有权并返回拥有的指针。在下面的语句中,release将p4置为nullptr
并返回p4原来的指针,同时p5接受p4释放的指针,获得动态内存的拥有权。*/
unique_ptr<int> p4(new int(207));
unique_ptr<int> p5(p4.release());
/*reset销毁原来的内存,并指向新的内存(如果有的话),下面reset函数释放p6原来的动态内存,
并指向p5释放出来的内存,p5指向的动态内存转移给了p6*/
unique_ptr<int> p6(new int(105));
p6.reset(p5.release());

shared_ptr (多个指针共享资源)

与unique_ptr类似,可以利用new运算符来定义一个shared_ptr,但是必须使用直接初始化的形式来初始化。例如:

shared_ptr<int> p1 = new int(105);    //错误,必须使用直接初始化的形式
shared_ptr<int> p2(new int(614));    //正确/*最安全的分配和使用动态内存的方法是调用make_shared标准库函数。make_shared是函数模板,
在使用它的时候,必须要在尖括号中指定要创建的对象类型。
例如下面定义一个shared_ptr,使其指向一个值为10的int类型对象:*/
shared_ptr<int> pi = make_shared<int>(10); //等效于auto pi = make_shared<int>(10);

与unique_ptr不同的是,shared_ptr允许复制或赋值。shared_ptr实现了引用计数型的智能指针,当进行复制的时候,计数器会递增。可以使用use_count来测试。use_count返回与当前shared_ptr共享内存的智能指针的数量,例如:

auto p1 = make_shared<int>(10);    //p1指向的对象只有p1一个引用者
cout << p1.use_count() << endl;    //输出1
auto p2(p1);
cout << p1.use_cout() << endl;    //输出2/*对一个shared_ptr类型对象进行赋值时,赋值操作符将左操作数所指对象的引用计数减1(如果引用计
数减至为0,则消亡其所指对象),并将右操作数所指对象的引用计数加1*/
auto p3 = make_shared<int>(11), p4(p3)
cout << p4.use_count() << endl;    //输出2,因为p3和p4指向同一个动态对象
p3 = p1;    //当把p1赋值给p3时,将p3指向的对象的引用计数减1,同时将p1指向的对象的引用计数加1
cout << p4.use_count() << " " << p1.use_count() << endl;    //输出1 3

weak_ptr (可复制shared_ptr,但其构造或者释放对资源不产生影响)

weak_ptr是一种不控制所指向对象生命期的智能指针,它指向由shared_ptr管理的对象。weak_ptr的使用和析构都不会改变shared_ptr的引用计数。

由于weak_ptr无法管理所指对象的生命期,它所指向的对象可能是不存在的,因此不能直接使用weak_ptr访问对象,而必须使用lock函数。

auto ps = make_shared<int>(10);
weak_ptr<int>pw(ps);    //ps的引用计数不会改变/*lock函数检查指向的对象是否存在,如果对象存在则返回一个可用的shard_ptr,否则返回一个存储
nullptr的shared_ptr。下面的语句中只有当p为非nullptr时,才会进入if语句体,因此使用p访问共
享对象是安全的*/
if(auto p = pw.lock())cout << *p;

..................................................................

tuple类型

标准库中的元组是一种固定大小的不同类型值的集合,是pair模板的泛化。pair只能有两个成员,而tuple可以包含任意数目的成员。当需要结构体的特性但又不想创建一个新的结构体时,tuple是一个很好的选择。可以把它作为一个通用的结构体来用,但在某些情况下可以取代结构体使程序更简洁、直观。

/*定义一个tuple对象时,需要显式提供每个成员类型。例如构造一个tuple对象,这里必须直接初始化
的方式初始化每个成员,不能采用复制初始化,这是因为tuple的构造构造函数是explicit的。*/
tuple<string, double, int, list<string>> book("titlel", 58.99, 2017, {"Mandy", "Lisha", "Rosieta"});
/*还可以使用make_tuple函数来生成一个tuple对象,使用make_tuple的好处是可以利用auto自动推导
每个成员的类型,不必显式指定*/
auto book2 = make_tuple(string("title2"), 68.99, 2017, list<string>{"Mandy", "Lisha", "Rosieta"});/*标准库函数模板get可以用来获取tuple的成员。使用get函数时,必须提供一个整型常量表达式的实
参,它返回相应成员的引用。例如,get<0>表示第一个成员。*/
auto item1 = get<0>(book);    //访问第一个元素
get<1>(book) = 48.99;    //对第二个元素赋值
for(auto &i :get<3>(book))    //使用范围for访问第四个成员的所有元素cout << i << " ";/*只要两个tuple对象成员的数量相等且对应的成员可以比较,则这两个tuple对象是可以比较的。比较的
规则为逐个比较每个成员的关系,例如:*/
if(book < book2) {/*...*/}
if(book == book2) {/*...*/}

可以使用标准库tie函数将多个对象连接到一个给定的tuple上,生成一个成员全是左值引用类型的tuple对象,例如:

string title;
doubel price:
int year;
list<string> author;
/*book3中的每个成员类型分别为上面定义的4个对象的左值引用,基于这个特性,可以很
方便地对tuple对象进行“解包”操作*/
auto book3 = tie(title, price, year, author);    //创建一个tuple对象

..................................................................

bitset类型

标准库提供了bitset类模板,可以构造任意长度地的位集合,并且使位操作更加方便。

/*bitset类模板的模板参数是一个常量表达式,用来表示位集合的长度,类似于数组的定义,定义一个
bitset时,必须显式指明次参数的值。bitset类构造函数的参数可以是一个整数,在将其转化为unsigned
long long后再转化成位模式存储,如果bitset的长度大于unsigned long long的位数,则多余的高位设
为0;若小于unsigned long long的位数,则不足的高位被舍弃*/
bitset<8>b1(1002);    //舍弃高位,b1的二进制序列为[1110 1010]
/*bitset类构造函数的参数也可以是一个由0和1组成的字符串常量,通过这种方式构造的位集合的二进制
序列就是字符串的内容。位的删补规则和整型参数构造函数相同*/
bitset<12>b2("110010");    //二进制序列为[0000 0011 0010]
/*通过bitset类的成员函数to_ulong和to_ullong将位集合的二进制序列转化成整型值,需要注意的是待
转换的位集合的长度不能大于long或long long的位数*/
bitset<12> b(1002);
cout << b.to_ullong << endl;    //输出1002

C++11使用的一些细节点相关推荐

  1. 从数据类型 nvarchar 转换为 bigint 时出错_JavaScript数据类型的一些细节点

    ▲ 点击上方蓝字关注我 ▲文 / 景朝霞来源公号 / 朝霞的光影笔记ID / zhaoxiajingjing图 / 自己画 目录JS数据类型的一些细节点0 / JS 中的数据类型的一些细节点(1)JS ...

  2. 微软算法100题11 求二叉树中两节点之间的最大距离

    第11 题 求二叉树中节点的最大距离... 如果我们把二叉树看成一个图,父子节点之间的连线看成是双向的, 我们姑且定义"距离"为两节点之间边的个数. 写一个程序, 求一棵二叉树中相 ...

  3. 六十、深入理解Vue组件,使用组件的三个细节点

    @Author:Runsen @Data:2020/10/16 文章目录 is的使用 组件中的data必须是方法 ref 引用 Vue中如何操作dom 实现计算器中的功能 后言 备战前端.大四加油.下 ...

  4. vue.js踩坑之ref引用细节点

    vue.js组件之H5页面,DOM标签或者组件中,通过ref="自定义name名称"引用的细节点 要点简介:[ 见下文案例 ] 使用is=" "解决H5出现的标 ...

  5. 【双11】阿里云边缘节点ENS助力淘宝构建音视频通信网络

    前言 淘宝在2016年推出直播平台,和娱乐直播性质不同,电商直播的主角多为网红店铺及网红达人,以直播带动产品售卖.在淘宝的双11流量加持之下,淘宝直播平台关注度持续攀升,通常的网红店主一场直播带来的收 ...

  6. 组件使用中的细节点02

    使用is属性解决bug 页面 Vue根实例中除外 在子组件定义data时,data必须是个函数, <!DOCTYPE html> <html lang="en"& ...

  7. 【项目管理一点通】(11) 找到项目的节点和里程碑

    很多项目把控不住,其根本原因在于节点和里程碑不清晰,或者节点和里程碑没有控制住. 那么,什么是节点和里程碑?关于概念,道听途说.人云亦云的比较多,而且很多人对里程碑是啥还不太清楚.里程碑实际上就是路碑 ...

  8. Dubbo基础专题——第四章(Dubbo整合Nacos分析细节点)

    应广大的读者要求,也是公司目前需要一些支持,我就自己亲身搭建一个Springboot+nacos+dubbo的框架和项目,并演示dubbo面对一些系统的 业务场合,应该怎么去做支持,文章中我会先贴出代 ...

  9. response细节点

    一. 1).response获得的流不需要手动关闭,Tomcat容器会帮你自动关闭 2).getWriter和getOutputStream不能同时调用 //error package com.ith ...

最新文章

  1. 2021全国高校计算机能力挑战赛(初赛)Java试题四
  2. linux常用基础命令操作收集
  3. Python 第三方模块之 numpy.linalg - 线性代数
  4. Hdu-6243 2017CCPC-Final A.Dogs and Cages 数学
  5. Python 迁移学习实用指南 | iBooker·ApacheCN
  6. Leetcde每日一题:160.intersection-of-two-linked-lists(相交链表)
  7. 华为自动驾驶域控制器:现货PK期货,工程能力PK只有算力
  8. MongoDB集群配置
  9. icem合并面网格_ICEM CFD混合网格
  10. 宝塔面板网站解决跨域问题
  11. PyTorch: TORCHVISION.TRANSFORMS
  12. openwrt屏蔽广告不生效
  13. MySQL系统变量auto_increment_increment与auto_increment_offset学习总结
  14. preg_match_all 和 preg_replace 区别
  15. PMAC应用六-前瞻
  16. 账号和权限管理——管理用户账号和组账号(一)
  17. WIN10系统如何完全获取用户管理员权限
  18. 【2020.11.4 洛谷团队赛 普及组】T1 U138644 小Biu的礼物
  19. 每日一题 LeetCode909. 蛇梯棋 java题解
  20. Spring7种事务的传播行为

热门文章

  1. 远程管理软件(xshell)介绍和系统连接
  2. Go日志-Uber开源库zap使用
  3. 装双系统需要给linux系统单独分一个区,PC技巧分享 篇一:教你如何在单硬盘单分区中安装双系统...
  4. 试验箱的基本参数与循环系统无效缘故
  5. 巨杉2017行业认可盘点
  6. DPDK in KVM
  7. 2018科大讯飞的Java笔试题附带参考答案
  8. Android-USB-OTG-读写U盘文件
  9. 先来先服务(FCFS)调度算法(Java实现)
  10. 要么承受自律的苦,要么承担自责的悔。