《C++ Primer Plus》笔记整理
目前在我所接触到的几门语言里,C++ 应该算是相当难以理解与上手的一门。《C++ Primer Plus》这本书买来两年了,之前大二时读了一半,最近趁着暑假我才把这本书彻底地读完。这本书算是入门 C++ 的好书,和《C++ Primer》相比它有着大量的代码去帮助读者理解。但是中文版的质量实在太糟糕,不仅有大量的打印错误,而且部分存在机翻情况,语句明显不通顺,难以理解到底在说什么。所以要理解概念性的内容建议去阅读《C++ Primer》,我有机会也会把那本读完。
至于该笔记整理并非是详细记录每个知识点,只是摘录一些我个人觉得需要时刻回顾的内容,方便回顾,同时我更倾向于在写代码的过程中去学习理解。
开始学习 C++
- C++ 句法要求
main()函数的定义以函数头int main()开始,并应避免使用void main()格式。 - C++ 注释以双斜杠(
//)打头,到行尾结束。C - 风格注释包括在符号/*和*/之间。 #include <iostream>:#include编译指令导致iostream文件的内容随源代码文件的内容一起被发送给编译器。- 诸如
endl等对于cout来说有特殊含义的特殊符号被称为 控制符(manipulator) 。 endl确保程序继续运行前刷新输出(将其立即显示在屏幕上),而使用\n不能提供这样的保证,这意味着在有些系统中,有时可能在您输入信息后才会有提示。- 一行代码中不可分割的元素叫做 标记(token) ,空格、制表符和回车统称为 空白(white space) 。
main不是关键字。- 函数特性:有函数头和函数体;接受一个参数;返回一个值;需要一个原型。
- 函数原型:声明函数的返回类型、函数接受的参数数量和类型。
程序能够访问名称空间 std 的方法
- 将
using namespace std;放在函数定义之前,让文件中所有的函数都能够使用名称空间std中所有的元素 - 将
using namespace std;放在特定的函数定义中,让该函数能够使用名称空间std中所有元素。 - 在特定的函数中使用类似
using std:cout;这样的编译指令,而不是using namespace std;,让该函数能够使用指定的元素,如cout。 - 完全不使用编译指令
using,而在需要使用名称空间std中的元素时,使用前缀std::。
处理数据
- 面向对象编程(OOP) 的本质是设计并扩展自己的数据类型。
- 术语宽度(width) 用于描述存储整数时使用的内存量。
C++ 命名规则
- 在名称中只能使用 字母字符、数字 和 下划线(
_) - 名称的第一个字符不能是数字。
- 区分大写字符与小写字符。
- 不能将 C++ 关键字用作名称。
- 以两个下划线或下划线和大写字母打头的名称将被保留给实现(编译器及其使用的资源)使用。以一个下划线开头的名称被保留给实现,用作全局标识符。
- C++ 对于名称的长度没有限制,名称中所有的字符都有意义,但有些平台有长度限制。
整型
C++ 的基本整型分别是 char 、short 、int 、long 和 C++11 新增的 long long,其中每种类型都有符号版本和无符号版本。
标准
short至少 16 位int至少和short一样长long至少 32 位,且至少与int一样长long long至少 64 位,且至少与long一样长
初始化
int owls = 101;:C 初始化方式int wrens(432);:C++ 初始化方式int hamburgers = {24};或int emus{7};:C++11 初始化方式。大括号内可以不包含任何参数,这种情况下,变量将被初始化为零。
wchar_t
程序需要处理的字符集可能无法用一个 8 位的字节表示,如日文汉字系统。wchar_t (宽字符类型)可以表示扩展字符集。wchar_t 类型是一种整数类型,可以表示系统使用的最大扩展字符集。wcin 和 wcout 可以用于处理 wchar_t 流。可以通过加上 L 来指示宽字符常量和宽字符串。
1 | wchar_t bob = L'P'; |
char16_t 和 char32_t
C++11 新增了类型 char16_t 和 char32_t ,其中前者是无符号的,长 16 位,而后者也是无符号的,但长 32 位。C++11 使用前缀 u 表示 char16_t 字符常量和字符串常量;使用 U 表示 char32_t 常量。
其它
- 创建无符号版本的基本整型,只需要使用关键字
unsigned来修改声明即可。 sizeof运算符返回类型或变量的长度,单位为字节。对变量名使用该运算符时,括号是可选的。- 头文件
climits(在老式实现中为limits.h)中包含了关于整型限制的信息。INT_MAX为int的最大取值,CHAR_BIT为字节的位数。 - 如果节省内存很重要,则应使用
short而不是使用int,即使它们的长度是一样的。 - C++ 使用前一(两)位来标识数字常量的基数。如果第一位为 1
9,则基数为 10(十进制);如果第一位是 0,第二位为 17,则基数为 8(八进制);如果前两位为0x或0X,则基数为16(十六进制)。 - 默认情况下,
cout以 十进制格式 显示整数。头文件iostream提供了控制符dec、hex、oct,分别用于指示cout以十进制、十六进制和八进制格式显示整数。 - 后缀是放在数字常量后面的字母,用于表示类型。整数后面的
l或L后缀表示该整数为long常量,u或U后缀表示unsigned int常量,ul(可以采用任何一种顺序,大写小写均可)表示unsigned long常量(由于小写l看上去像1,因此应使用大写L作后缀)。C++11 提供了用于表示类型long long的后缀ll和LL,还提供了用于表示类型unsigned long long的后缀ull、Ull、uLL、ULL。 - C++ 对字符用单引号,对字符串使用双引号。
cout.put()函数显示一个字符。句点被称为 成员运算符。- 将转义序列作为字符常量时,应用单引号括起;将他们放在字符串中时,不要使用单引号。
- C++ 将非零值解释为
true, 将零解释为false。 - 如果使用关键字
auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同。自动类型推断只能用于单值初始化,而不能用于初始化列表。
浮点数
C++ 有两种书写浮点数的方式。第一种是使用常用的标准小数点表示法,第二种表示浮点的方法叫做 E 表示法 。可以使用 E 也可以使用 e,指数可以是整数也可以是负数,数字中不能有空格。
d.dddE+n 指的是将小数点向右移 n 位,而 d.dddE-n 指的是将小数点向左移 n 位。
C++ 有 3 种浮点类型:float、double 和 long double。float 至少 32 位,double 至少 48 位,且不少于 float,long double 至少和 double 一样多。
其他
- 调用
ostream中的setf()方法,迫使输出使用 定点表示法 ,以便更好地了解精度,它防止程序把较大的值切换成 E 表示法 ,并使程序显示到小数点后 6 位。 - 通常
cout会删除结尾的零。 - 参数
ios_base::fixed和ios_base::floatfield是通过包含iostream来提供的常量。 cout << setf(ios_base::fixed, ios_base::floatfield);- 如果希望常量为
float类型,请使用f或F后缀。对于long double类型,可使用l或L后缀。
算术运算符
% 运算符求模,它生成第一个数除以第二个数后的余数。两个操作数必须都是整型,将该运算符用于浮点数将导致编译错误。 如果其中一个是负数,则结果的符号满足如下规则:(a / b) * b + a % b = a 。
类型转换
- C++ 允许将一种类型的值赋给另一种类型的变量。
- C++11 将使用大括号的初始化称为 列表初始化(list - initialization) 。列表初始化不允许 缩窄(narrowing) ,即变量的类型可能无法表示赋给它的值。
- 在计算表达式时,C++ 将
bool、char、unsigned char、signed char和short值转换为int。具体地说,true被转换为1,false被转换为0。这些转换被称为 整型提升(integral promotion) 。 - 如果
short比int短,则unsigned short类型将被转换成int;如果两种类型的长度相同,则unsigned short类型将被转换成unsigned int。 wchar_t被提升称为下列类型中第一个宽度足够存储wchar_t取值范围的类型:int,unsigned int、long或unsigned long。
强制类型转换
通用格式:
1 | (typeName) value; |
static_cast<> 可用于将值从一种数值类型转换为另一种数值类型:static_cast<typeName> (value)。
复合类型
数组
- 数组(array) 声明应指出以下三点:
- 存储在每个元素中的值的类型
- 数组名
- 数组中的元素数
- 声明数组的通用格式如下:
typeName typeName[arraySize]; - 表达式
arraySize指定元素数目,它必须是整型常数或const值,也可以是常量表达式。 - 如果将
sizeof运算符用于数组名,得到的将是整个数组的 字节数 ;但如果将sizeof用于数组元素,则得到的将是 元素的长度(单位为字节)。
初始化规则
- 只有在定义数组时才能使用初始化,不能将一个数组赋给另一个数组。
- 初始化数组时,提供的值可以少于数组的元素数目。
- 如果只对数组的一部分进行初始化,则编译器将把其他元素设置为 0 。
- 如果初始化数组时方括号内(
[])为空,C++ 编译器将计算元素个数。
C++11 初始化
- 初始化数组时,可以省略等号(
=) - 可不在大括号内包含任何东西,这将把所有元素都设置为零
- 列表初始化禁止缩窄
字符串
- C - 风格字符串具有一种特殊的性质:以 空字符(null character) 结尾,空字符被写作
\0,其 ASCII 码为 0, 用来标记字符串的结尾。 - 字符串常量(使用双引号)不能与字符常量(使用单引号)互换。
char shirt_size = "S"; //illegal type mismatch注意,”S” 实际上表示的是字符串所在的内存地址。
读取
cin使用 空白(空格、制表符和换行符) 来确定字符串的结束位置,意味着cin在获取字符数组输入时只读取 一个单词 。istream中的类(如cin) 提供了一些面向行的类成员函数:getline()和get()。这两个函数都读取一行输入,直到到达换行符。然而,getline()将丢弃换行符,而get()将换行符保留在输入序列中。cin.getline()有两个参数,第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。get()接受解释参数的方式与getline()相同,但读取到行尾时不会丢弃换行符,而是将其保留在输入队列中。- 当
get()读取空行后将设置 失效位(failbit) 。这意味着接下来的输入将被阻断,但可以用cin.clear()或cin.get()来恢复输入。
String 类
- 要使用
string类,必须在程序中包含头文件string。string类位于名称空间std中。 strlen()函数接受一个 C - 风格字符串作为参数,返回的是字符串的包含的字符数,只计算可见的字符,而不把空字符计算在内,该函数包含在标准头文件cstring(老式实现为string.h) 中。- 函数
strcpy()将字符串复制到字符数组中。接受两个参数,第一个是目标地址,第二个是要复制的字符串的地址。函数strcat()将字符串添加到字符数组末尾。 getline(cin, str);:将一行输入读取到string对象中。
C++11 原始字符串
- 原始字符串将
"(和)"用作定界符,并使用前缀R来标识原始字符串。 - 原始字符串语法允许您在表示字符串开头的
"和(之间添加其他字符,这意味着表示字符串结尾的)和”之间也必须包含这些字符。 - 自定义定界符时,在默认定界符之间添加任意数量的基本字符,但空格、左括号、右括号、斜杠和控制字符(如制表符和换行符)除外。
结构
结构 是一种比数组更灵活的数据格式,因为同一个结构可以存储多种类型的数据。
创建结构包括两步。首先,定义 结构描述 —— 它描述并标记了能够存储在结构中的各种数据类型。然后,按描述创建结构变量(结构数据对象)。
1 | struct inflatable |
C++ 允许在声明结构变量时省略关键字 struct :
1 | struct inflatable goose; |
C++11 结构初始化
C++11 支持将列表初始化用于结构,且等号(=)是可选的。如果大括号内未包含任何东西,各个成员都将被设置为零。不允许缩窄转换。
结构中的位字段
C++ 允许指定占用特定位数的结构成员,这使得创建与某个硬件设备上的寄存器对应的数据结构非常方便。字段的类型应为整型或枚举,接下来是冒号,冒号后面是一个数字,它制定了使用的位数。可以使用没有名称的字段来提供间距。每个成员都被称为 位字段(bit field) 。
1 | struct torgle_register |
共用体
共用体(union) 是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。
1 | union one4all |
可以使用 one4all 变量来存储 int、long 或 double,条件是在不同的时间进行。
共用体的用途之一是,当数据使用两种或更多种格式(但不会同时使用)时,可节省空间。
匿名共用体(anonymous union) 没有名称,其成员将成为位于相同位置处的变量。
枚举
1 | enum spectrum {red, orange, yellow, green, blue, violet}; |
C++ 的 enum 工具提供了另一种创建符号常量的方式,这种方式可以代替 const。
在默认情况下,将整数值赋给枚举量,第一个枚举量的值为 0,第二个枚举量的值为 1,依次类推。可以通过显式地制定整数值来覆盖默认值。
对于枚举,只定义了赋值运算符。具体地说,没有为枚举定义算术运算。
指针和自由存储空间
指针 是一个变量,其存储的是 值的地址 ,而不是值本身。
只需对变量应用地址运算符(&),就可以获得它的位置。
指针用于存储值的地址。指针名表示的是地址。* 运算符被称为 间接值(indirect value) 或 解除引用(dereferencing)运算符 ,将其应用于指针,可以得到该地址处存储的值。
指针声明必须指定指针指向的数据的类型。
1 | int * p_updates; |
* p_updates 的类型为 int 。由于 * 运算符被用于指针,因此 p_updates 变量本身必须是指针。我们说 p_updates 指向 int 类型,我们还说 p_updates 的类型是指向 int 的指针,或 int* 。可以这样说,p_updates 是指针(地址),而 *p_updates 是 int ,而不是指针。
* 两边的空格是 可选 的。 对于每个指针变量名,都需要使用一个 * 。
可以在声明语句中初始化指针。在这种情况下,被初始化的是指针,而不是它指向的值。下面的语句将 pt (而不是 *pt) 的值设置为 &higgens 。
1 | int higgends = 5; |
C++ 中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
1 | typeName * pointer_name = new typeName; |
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。
new 分配的内存块通常与常规变量声明分配的内存块不同。常规变量的值都存储在被称为 栈(stack) 的内存区域中,而 new 从被称为 堆(heap) 或 自由存储区(free store) 的内存区域分配内存。
在 C++ 中,值为 0 的指针被称为 空指针(null pointer)。
使用 delete 时,后面要加上指向内存块的指针。这将释放指针指向的内存,但不会删除指针本身。一定要配对的使用 new 和 delete ,否则会发生 内存泄漏(memory leak) 。不能使用 delete 来释放声明变量所获得的内存。
使用 delete 的关键在于,将它用于 new 分配的内存。这并不意味着要使用用于 new 的指针,而是用于 new 的地址:
1 | int * ps = new int; //allocate memory |
在编译时给数组分配内存被称为 静态联编(static binding) ,意味着数组是在编译时加入到程序中的。但在使用 new 时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为 动态联编(dynamic binding) ,意味着数组是在程序运行时创建的。这种数组叫做 动态数组(dynamic array) 。
创建动态数组很容易,只要将数组的元素类型和元素数目告诉 new 即可。必须在类型名后加上方括号,其中包含元素数目。new 运算符返回第一个元素的地址,用另一种格式的 delete 来释放:delete [] xxx; 。
为数组分配内存的通用格式如下:
1 | type_name * pointer_name = new type_name [num_elements]; |
指针、数组和指针算术
指针和数组基本等价的原因在于 指针算术(pointer arithmetic) 和 C++ 内部处理数组的方式。将指针变量加 1 后,增加的量等于它指向的类型的字节数。C++ 将数组名解释为地址。
通常,使用数组表示法时,C++ 都执行下面的转换:
1 | arrayname[i] becomes *(arrayname + i) |
如果使用的是指针,而不是数组名,则 C++ 也将执行同样的转换:
1 | pointername[i] becomes *(pointername + i) |
对指针应用 sizeof 得到的是指针的长度。
数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。
1 | short tell[10]; //tell an array of 20 bytes |
1 | struct things |
创建动态结构时,不能将成员运算符 . 用于结构名,因为这种结构没有名称,只是知道它的地址。C++ 专门为这种情况提供了一个运算符:箭头成员运算符(->)。该运算符可用于指向结构的指针。grubnose.good 与 pt -> good 相同。
如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构的指针,则使用箭头运算符。
在函数内部定义的常规变量使用自动存储空间,被称为 自动变量(automatic variable) ,这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。自动变量通常存储在栈中。
静态存储 是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字 static。
new 和 delete 运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在 C++ 中被称为 自由存储空间(free store) 或 堆(heap)。
模板类 vector
模板类 vector 也是一种动态数组,可以在运行阶段设置 vector 对象的长度。它是使用 new 创建动态数组的替代品。
要使用 vector 对象,必须包含头文件 vector 。vector 包含在名称空间 std 中。
1 |
|
vector 对象在您插入或添加值时自动调整长度。
模板类 array (C++11)
array 也位于名称空间 std 中,array 对象的长度固定,使用栈(静态内存分配),而不是自由存储区。要创建 array 对象,需要包含头文件 array 。
1 |
|
vector 和 array 对象都有成员函数 at():
1 | a2.at(1) = 2.3; //assign 2.3 to a2[1] |
中括号表示法和成员函数 at() 的差别在于,使用 at() 时,将在运行期间捕获非法索引,而程序默认将中断。
循环和关系表达式
通常,cout 在显示 bool 值之前将它们转换为 int,但 cout.setf(ios:: boolalpha) 函数调用设置了一个标记,该标记命令 cout 显示 true 和 false ,而不是 1 和 0 。
for 循环
1 | for(initialization; test-expression; update-expression) |
1 | for(for-init-statement condition: expression) |
C - 风格字符串的比较
C - 风格字符串库中的 strcmp() 函数接受两个字符串地址作为参数。这意味着参数可以是指针、字符串常量或字符数组名。文件cstring 提供了 strcmp() 的函数原型。
如果两个字符串相同,该函数返回零;如果第一个字符串按字母顺序排在第二个字符串之前,则 strcmp() 将返回一个负数值;如果第一个字符串按字母顺序排在第二个字符串之后,则 strcmp() 将返回一个正数值。
while 循环
1 | while(test-condition) |
clock() 返回程序开始执行后的所用的系统时间。
头文件 ctime 定义了一个符号常量 —— CLOCKS_PER_SEC ,该常量等于每秒钟包含的系统时间单位数。将系统时间除以这个值,可以得到秒数。ctime 将 clock_t 作为 clock() 返回类型的别名。这意味着可以将变量声明为 clock_t 类型,编译器将把它转换为 long 、unsigned int 或适合系统的其他类型。
1 | //waiting.cpp -- using clock() in a time-delay loop |
类型别名
C++ 为类型建立别名的方式有两种。一种是使用预处理器:
1 | define BYTE char //preprocessor replaces BYTE with char |
预处理器将在编译程序时用 char 替换所有的 BYTE ,从而使 BYTE 成为 char 的别名。
第二种方法是使用 C++ 和 C 的关键字 typedef 来创建别名。
1 | typedef typeName aliasName; |
do while 循环
1 | do |
基于范围的 for 循环(C++)
1 | double prices[5] = {4.99, 10.99, 6.87, 7.99, 8.49}; |
要修改数组的元素,需要使用不同的循环变量语法:
1 | for(double & x : prices) |
这种声明让接下来的代码能够修改数组的内容。
循环和文本输入
读取 char 值时,与读取其他类型一样,cin 将忽略空格和换行符。
成员函数 cin.get(ch) 读取输入中的下一个字符(即使它是空格),并将其赋给变量 ch 。
文件尾条件
很多操作系统都允许通过键盘来模拟文件尾条件。在 Unix 中,可以在行首按下 Ctrl + D 来实现;在 Windows 命令提示符模式下,可以在任何位置按 Ctrl + Z 和 Enter 。
检测到 EOF 后,cin 将两位(eofbit 和 failbit)都设置为 1。可以通过成员函数 eof() 来查看是否被设置;如果检测到 EOF ,则 cin.eof() 将返回布尔值 true 。同样,如果 eofbit 或 failbit 被设置为 1,则 fail() 成员函数返回 true ,否则返回 false 。注意,eof() 和 fail() 方法报告最近读取的结果;也就是说,它们事后报告而不是预先报告。因此应将 cin.eof() 或 cin.fail() 测试放在读取后。
cin.clear() 方法清除 EOF 标记。
每次读取一个字符,直到遇到 EOF 的输入循环的基本设计如下:
1 | cin.get(ch); //attempt to read a char |
方法 cin.get(char) 的返回值是一个 cin 对象。然而,istream 类提供了一个可以将 istream 对象(如 cin)转换为 bool 值的函数;当 cin 出现在需要 bool 值的地方时,该转换函数将被调用。另外,如果最后一次读取成功了,则转换得到的布尔值为 true,否则为 false 。因此,由于 cin.get(char) 的返回值为 cin ,可以将 !cin.fail() 改成 cin.get(ch) 。
简单文件输入 / 输出
写入到文本文件中
- 必须包含头文件
fstream - 头文件
fstream定义了一个用于处理输出的ofstream类 - 需要声明一个或多个
ofstream变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则 - 必须指明名称空间
std - 需要将
ofstream对象与文件关联起来。为此,方法之一是使用open()方法 - 使用完文件后,应使用方法
close()将其关闭 - 可结合使用
ofstream对象和运算符<<来输出各种类型的数据
1 | ofstream outFile; //outFile an ofstream object |
程序使用完文件后,应该将其关闭。方法 close() 不需要使用文件名作为参数,这是因为 outFile 已经同特定的文件关联起来。如果你忘记关闭文件,程序正常终止时将自动关闭它。
当文件不存在的时候,open() 方法将新建一个文件。当文件已经存在的时候,open() 方法默认情况下将首先截断该文件,即将文件长度截断到零 —— 丢弃原有的内容,然后将新的输出加入到该文件中。
读取文本文件
- 必须包含头文件
fstream - 头文件
fstream定义了一个用于处理输入的ifstream类 - 需要声明一个或多个
ifstream变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则 - 必须指明名称空间
std - 需要将
ifstream对象与文件关联起来。为此,方法之一是使用open()方法 - 使用完文件后,应使用方法
close()将其关闭 - 可结合使用
ifstream对象和运算符>>来输出各种类型的数据 - 可以使用
ifstream对象和get()方法来读取一个字符,使用ifstream对象和getline()来读取一行字符 - 可以结合使用
ifstream和eof()、fail()等方法来判断输入是否成功 ifstream对象本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值true,否则被转换为false
1 | ifstream inFile; //inFile an ifsteam object |
如果试图打开一个不存在的文件用于输入,将导致后面使用 ifstream 对象进行输入时失败。检查文件是否被成功打开的首先方法是使用方法 is_open() 。
1 | inFile.open("bowling.txt"); |
函数 exit() 的原型是在头文件 cstdlib 中定义的,在该头文件中,还定义了一个用于同操作系统通信的参数值 EXIT_FAILURE 。函数 exit() 终止程序。
读者需要特别注意的是文件读取循环的正确设计。
首先,程序读取文件时不应超过 EOF 。如果最后一次读取数据时遇到 EOF ,方法 eof() 返回 true 。其次,如果最后一次读取操作中发生了类型不匹配的情况,方法 fail() 将返回 true (如果遇到了 EOF ,该方法也返回 true)。最后,如果最后一次读取文件时发生文件受损或硬件故障等情况,方法 bad() 将返回 true 。不要分别检查这些情况,一种简单的方法是使用 good() 方法,该方法在没有发生任何错误时返回 true 。
1 | while (inFile.good()) //while input good and not at EOF |
函数
1 | typeName functionName(parameterList) |
返回类值的类型不能是数组,但可以是其他任何类型。
函数 通过将返回值复制到指定的 CPU 寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。
函数原型
原型 描述了函数到编译器的接口,它将函数返回值的类型以及参数的类型和数量告诉编译器。
函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。函数原型不要求提供变量名,有类型列表就够了。原型中的变量名不必与定义中的变量名相同,而且可以省略。
1 | const double * f1(const double ar[], int n); |
在函数原型中,参数列表 const double ar[] 和 cosnt double *ar 的含义完全相同。其次,在函数原型中,可以省略标识符。因此 cosnt double ar[] 可简化为 const double [] ,而 const double *ar 可简化为 const double * 。
函数和数组
数组声明使用数组名来标记存储位置,对数组名使用 sizeof 将得到整个数组的长度(以字节为单位),将地址运算符 & 用于数组名时,将返回整个数组的地址。
1 | arr[i] == *(ar + i) //values in two notations |
将指针(包括数组名)加 1,实际上是加上了一个与指针指向的类型的长度。
为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递它们:
1 | void fillArray(int arr[], int size); //prototype |
为防止函数无意中修改数组的内容,可在声明形参时使用关键字 const 。
指针和 const
有两种不同的方式将 const 关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。
声明一个指向常量的指针:
1 | int age = 39; |
该声明指出,pt 指向一个 const int (这里为 39),因此不能使用 pt 来修改这个值。换句话来说,*pt 的值为 const ,不能被修改。
可以通过 age 变量来修改 age 的值,但不能使用 pt 指针来修改它:
1 | *pt = 20; //INVALID because pt points to a const int |
1 | const float g_earth = 9.80; |
1 | int age = 39; |
第二个声明中的 const 只能防止修改 pt 指向的值(这里为 39),而不能防止修改 pt 的值。也就是说,可以将一个新地址赋给 pt :
1 | int sage = 80; |
但仍然不能使用 pt 来修改它指向的值(现在为 80)。
第二种使用 const 的方式使得无法修改指针的值:
1 | int sloth = 3; |
这种声明格式使得 finger 只能指向 sloth ,但允许使用 finger 来修改 sloth 的值。
还可以声明指向 const 对象的 const 指针:
1 | double trouble = 2.0E3.0; |
函数指针
获取函数的地址很简单:只要使用 函数名(后面不跟参数) 即可。也就是说,如果 think() 是一个函数,则 think 是该函数的地址。
声明 指向函数的指针 时,必须指定指针指向的函数类型。意味着声明应指定 函数的返回类型 以及 函数的特征标(参数列表) 。
1 | double pam(int); //prototype |
*pf(int) 意味着 pf() 是一个返回指针的函数,而(*pf)(int) 意味着 pf 是一个指向函数的指针。
1 | ... |
函数探幽
内联函数
- 在函数声明前加上关键字
inline - 在函数定义前加上关键字
inline
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应能提供原型的地方。
内敛函数不能递归,按值传递参数。
引用变量
引用 是已定义的变量的别名,但引用变量的主要用途是用作函数的形参。
C++ 用符号 & 符与了另一个含义,将其用来声明引用。
1 | int rats; |
注意,& 不是地址运算符,而是类型标识符的一部分。
引用必须在创建时进行初始化。
1 | int & rodents = rats; //等同于 |
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为 按引用传递 。
1 | void swapr(int & a, int & b) //use references |
如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用 常量引用 。
临时变量、引用参数和 const
左值参数 是可被引用的数据对象。非左值包括 字面常量(用括号引起的字符串除外,它们由其地址表示) 和 包含多项的表达式 。常规变量 和 const 变量都可为左值,因为可以通过地址访问它们。但常规变量属于可修改的左值,而 const 变量属于不可修改的左值。
C++11 新增了另一种引用 —— 右值引用。这种引用可指向右值,是使用 && 声明的:
1 | double && rref = std::sqrt(36.00); //not allowed for double & |
新增右值引用的主要目的是,让库设计人员能够提供某些操作的更有效实现。
对于带参数列表的函数,必须 从右向左 添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。
函数重载
函数重载 的关键是函数的参数列表 —— 也称为 函数特征标 。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而 变量名是无关紧要的 。
编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。匹配函数时,并不区分 const 和非 const 变量。是特征标,而不是函数类型使得可以对函数进行重载。
函数模板
函数模板 使用 泛型 来定义函数,也被称为 通用编程 。
1 | template <typename AnyType> |
关键字 template 和 typename 是必须的,除非使用关键字 class 代替 typename 。必须使用尖括号。在标准 C++98 添加关键字 typename 之前,C++ 使用关键字 class 来创建模板。
显式具体化
- 对于给定的函数名,可以有非模板函数、模板函数和显示具体化以及它们的重载版本
- 显示具体化的原型和定义应以
template<>打头,并通过名称来指出类型 - 具体化优先于常规模板,而非模板函数优先于具体化和常规模板
1 | //non template function prototype |
显式实例化
C++ 允许 显式实例化(explicit instantiation) 。其语法是,声明所需的种类 —— 用 <> 符号指示类型,并在声明前加上关键字 template :
1 | template void Swap<int>(int, int); //explicit instantiation |
与显式实例化不同的是,显式具体化 使用下面两个等价的声明之一:
1 | template <> void Swap<int>(int &, int &); //explicit specialization |
显式具体化声明在关键字 template 后包含 <> ,而显式实例化没有。
隐式实例化 、显式实例化 和 显式具体化 统称为 具体化(specialization) 。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述 。
1 | ... |
关键字 decltype (C++11)
1 | int x; |
1 | decltype(expression) var; |
第一步:如果 expression 是一个没有用括号括起的标识符,则 var 的类型与该标识符的类型相同,包括 const 等限定符:
1 | double x = 5.5; |
第二步:如果 expression 是一个函数调用,则 var 的类型与函数的返回类型相同。
第三步:如果 expression 是一个左值,则 var 为指向其类型的引用。这好像意味着前面的 w 应为引用类型,因为 x 是一个左值。但别忘了,这种情况已经在第一步处理过了。要进入第三部,expression 不能是未用括号括起的标识符。那么,expression 是什么时将进入第三步呢?一种显而易见的情况时,expression 是用括号括起的标识符:
1 | double xx = 4.4; |
第四步:如果前面的条件都不满足,则 var 的类型与 expression 的类型相同。
如果需要多次声明,可结合使用 typedef 和 decltype :
1 | template<class T1, class T2> |
后置返回类型
1 | template<class T1, class T2> |
内存模型和名称空间
头文件中常包含的内容:
- 函数原型
- 使用
#define或const定义的符号常量 - 结构声明
- 类声明
- 模板声明
- 内联函数
如果文件名包含在尖括号中,则 C++ 编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录。如果没有在那里找到头文件,则将在标准位置查找。
在同一个文件中只能将同一个头文件包含一次。下面的代码片段意味着仅当以前没有使用预处理编译指令 #define 定义名称 COORDIN_H_ 时,才处理 #ifndef 和 #endif 之间的语句:
1 |
|
作用域和链接
作用域(scope) 描述了名称在文件(翻译单元)的多大范围内可见。链接性(linkage) 描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
寄存器变量:关键字 register 最初是由 C 语言引入的,它建议编译器使用 CPU 寄存器来存储自动变量。
1 | register int count_fast; //request for a register variable |
在 C++11 中,这种提示作用也失去了,关键字 register 只是显式地指出变量是自动的。
静态持续变量
C++ 为静态持续性变量提供了 3 种链接性:外部链接性(可在其他文件中访问) 、内部链接性(只能在当前文件中访问) 和 无链接性(只能在当前函数或代码块中访问) 。
要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用 static 限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用 static 限定符。
1 | ... |
未被初始化的静态变量的所有位都被设置为 0。这种变量为称为 零初始化(zero - initialized) 。
用于局部声明,以指出变量是无链接性的静态变量时,static 表示的是存储的持续性;而用于代码块外的声明时,static 表示内部链接性,而变量已经是静态持续性了。
静态持续性、外部链接性
链接性为外部性的变量通常简称为 外部变量 ,它们的存储储蓄性为静态,作用域为整个文件。
C++ 有 “单定义规则”(One Definition Rule, ODR) ,该规则指出,变量只能有一次定义。为满足这种要求,C++ 提供了两种变量声明。一种是 定义声明(defining declaration) 或简称为 定义(definition) ,它给变量分配存储空间;另一种是 引用声明(referencing declaration) 或简称为 声明(declaration) ,它不给变量分配存储空间,因为它引用已有的变量。
引用声明使用关键字 extern,且不进行初始化;否则,声明为定义,导致分配存储空间。
1 | double up; //definition, up is 0 |
如果要在多个文件中使用外部变量,只需要在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字 extern 声明它。
C++ 提供了作用域解析运算符(::)。放在变量名前面时,该运算符表示使用变量的全局版本。
静态持续性、内部链接性
将 static 限定符用于作用域内为整个文件的变量时,该变量的连接性将为内部的。链接性为内部的变量只能在其所属的文件中使用;但常规外部变量都具有外部链接性,即可以在其他文件中使用。
如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量。
在代码块中使用 static 时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数时,将不会像自动变量那样再次被初始化。
说明符和限定符
CV - 限定符 :const 和 volatile
关键字 volatile 表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。例如,假如编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为 volatile ,则编译器将进行这种优化;将变量声明为 volatile ,相当于告诉编译器,不要进行这种优化。
可以用 mutable 来指出,即使结构(或类)变量为 const,其某个成员也可以被修改。
1 | struct data |
const 全局变量的链接性为内部的,意味着每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的,这就是能够将常量定义放在头文件中的原因。
如果处于某种原因,程序员希望某个常量的链接性为外部的,则可以使用 extern 关键字来覆盖默认的内部链接性。
1 | extern const int states = 50; //definition with external linkage |
名称空间
使用关键字 namespace 创建 名称空间 。名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。名称空间是开放的,即可以把名称加入到已有的名称空间中。
需要有一种方法来访问给定名称空间中的名称。最简单的方法是,通过作用域解析运算符 :: ,使用名称空间来限定该名称。
C++ 提供了两种机制(using 声明和 using 编译指令)来简化对名称空间中名称的使用。*using 声明* 使特定的标识符可用,*using 编译指令* 使整个名称空间可用。
using 声明由被限定的名称和它前面的关键字 using 组成:
1 | using Jill::fetch; //a using declaration |
using 编译指令由名称空间名和它前面的关键字 using namespace 组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符。
1 | using namespace Jack; //make all the names in Jack available |
对象和类
结构的默认访问类型是 public ,而类为 private 。
类成员函数特征:
- 定义成员函数时,使用作用域解析运算符(
::)来标识函数所属的类 - 类方法可以访问类的
private组件
定义位于类声明中的函数都将自动称为 内联函数 。
构造函数
显式调用构造函数:
1 | Stock food = Stock("World Cabbage", 250, 1.25); |
隐式地调用构造函数:
1 | Stock garment("Furry Mason", 50, 2.5); |
析构函数
在类名前加上 ~ 。通常不应在代码中显式地调用析构函数。
const 成员函数
C++ 将 const 关键字放在函数的括号后面。
1 | void show() const; //promises not to change invoking object |
以这种方式声明和定义的类函数被称为 const 成员函数 。只要类方法不修改调用对象,就应将其声明为 const 。
当 const 在函数名前面的时候修饰的是 函数返回值 ,在函数名后面表示是 常成员函数 ,该函数不能修改对象内的任何成员,只能发生读操作,不能发生写操作。
this 指针
this 指针 指向用来调用成员函数的对象(this 被作为隐藏参数传递给方法)。所有的类方法都将 this 指针设置为调用它的对象的地址。
每个成员函数(包括构造函数和析构函数)都有一个 this 指针。this 指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式 *this 。在函数的括号后面使用 const 限定符将 this 限定为 const ,这样将不能使用 this 来修改对象的值。
使用类
运算符重载
要重载运算符,需要使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:
1 | operatorOP(argument-list); |
OP 必须是有效的运算符,不能虚构一个新的符号。
为区分 ++ 运算符的前缀版本和后缀版本,C++ 将 oparator++ 作为前缀版本,将 operator++(int) 作为后缀版本。
友元
友元 是一类特殊的非成员函数,可以访问类的私有成员。友元有 3 种:
- 友元函数
- 友元类
- 友元成员函数
创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend :
1 | friend Time operator*(double m, const Time & t); //goes in class declaration |
- 虽然
operator *()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用 - 虽然
operator *()函数不是成员函数,但它与成员函数的访问权限相同
第二步是编写函数定义。因为它不是成员函数,所以不要用 Time:: 限定符。另外,不要在定义中使用关键字 friend 。
1 | Time operator*(double m, const Time & t) //friend not used in definition |
类的自动转换和强制类型转换
C++ 新增了关键字 explicit ,用于关闭将构造函数用作自动类型转换函数的自动特性。
1 | explicit Stonewt(double lbs); //no implicit conversions allowed |
类和动态内存分配
动态内存和类
静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。
不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。初始化语句指出了类型,并使用了作用域运算符,但并没有使用关键字 static 。
如果静态数据成员为 const 整数类型或枚举型,则可以在类声明中初始化。
特殊成员函数
C++ 默认提供了下面这些成员函数:
- 默认构造函数,如果没有定义构造函数
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符,如果没有定义
复制构造函数
复制构造函数 用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。
1 | Class_name(cosnt class_name &); |
它接受一个指向类对象的常量引用作为参数。
默认的赋值构造函数逐个复制非静态成员(成员复制也称为 浅复制 ),复制的是成员的值。
深度复制(deep copy) :复制构造函数应当复制字符串并将副本的地址赋给成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串, 而不是引用另一个对象的字符串。调用析构函数时都将释放不同的字符串,而不会试图去释放已经被释放的字符串。
1 | StringBad::StringBad(const StringBad & st) |
赋值运算符
C++ 允许类对象赋值,通过自动为类重载赋值运算符实现的。
1 | Class_name & Class_name::operator=(const Class_name &); |
1 | StringBad & StringBad::operator=(const StringBad & st) |
默认构造函数
1 | String::String() |
为什么代码 str = new char[1]; 而不是 str = new char; ?上面两种方式分配的内存量相同,区别在于前者与类析构函数兼容,而后者不兼容。
C++11 引入关键字 nullptr 用于表示空指针。
静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字 static,但如果函数定义是独立的,则其中不能包含关键字 static)。
不能通过对象调用静态成员函数;静态成员函数甚至不能使用 this 指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
成员初始化列表
如果 Classy 是一个类,而 mem1、mem2 和 mem3 都是这个类的数据成员,则类构造函数可以使用如下语法来初始化数据成员:
1 | Classy::Classy(int n, int m) : mem1(n), mem2(0), mem3(n*m + 2) |
C++11 的类内初始化:
1 | class Classy |
类继承
基类、派生类
从一个类派生出另一类时,原始类称为 基类 ,继承类称为 派生类 。
1 | //RatedPlayer derives from the TableTennisPlayer base class |
冒号 指出 RatedPlayer 类的基类是 TableTennisPlayer 类。上述特殊的声明头表明 TableTennisPlayer 是一个公有基类,这被称为 公有派生 。派生类对象包含基类对象。使用公有派生,基类的公有成员将称为派生类的公有成员;基类的私有部分也将称为派生类的一部分,但只能通过基类的公有和保护方法访问。
派生类对象存储了基类的数据成员,派生类对象可以使用基类的方法。派生类需要自己的构造函数,派生类可以根据需要添加额外的数据成员和成员函数。
构造函数
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类构造函数必须使用基类构造函数。
基类对象应当在程序进入派生类构造函数之前被创建。C++ 使用成员初始化列表来完成这种工作。
1 | RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(f, ln, ht) |
释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
派生类和基类的特殊关系
派生类对象可以使用基类的方法,条件是方法不是私有的。
基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以在不进行显式类型转换的情况下引用派生类对象。
1 | RatedPlayer rplayer(1140, "Mallory", "Duck", true); |
基类指针或引用只能用于调用基类方法,不能用 rt 或 pt 来调用派生类的 ResetRanking 方法。 不可以将基类对象和地址赋给派生类引用和指针。
多态公有继承
方法的行为应取决于调用该方法的对象。这种较复杂的行为称为 多态 —— 具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可用于实现多态公有继承:
- 在派生类中重新定义基类方法
- 使用 虚方法(virtual method)
如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字 virtual ,程序将根据引用类型或指针类型选择方法;如果使用了 virtual ,程序将根据引用或指针指向的对象的类型来选择方法。
如果 ViewAcct() 不是虚的,则程序的行为如下:
1 | //behavior with non-virtual ViewAcct() |
引用变量的类型为 Brass,所以选择了 Brass::ViewAcct();使用 Brass 指针代替引用时,行为将与此类似。
如果 ViewAcct() 是虚的,则行为如下:
1 | //behavior with virtual ViewAcct() |
这里两个引用的类型都是 Brass ,但 b2_ref 引用的是一个 BrassPlus 对象,所以使用的是 BrassPlus::ViewAcct() 。使用 Brass 指针代替引用时,行为将类似。
经常在基类中将派生类会重新定义的方法声明为 虚方法 。方法在基类中被声明为虚的后,它在派生类中将自动称为虚方法。
基类声明了一个 虚析构函数 ,这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。
关键字 virtual 只用于类声明的方法原型中,而没有用于方法定义中。
静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为 函数名联编(binding)。在编译过程中进行联编被称为 静态联编(static binding) ,又称为 早期联编(early binding) 。编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为 动态联编(dynamic binding) ,又称为 晚期联编(late binding) 。
1 | BrassPlus dilly("Annie Dill", 493222, 2000); |
将派生类引用或指针转换为基类引用或指针被称为 向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。向上强制转换时可传递的。
将基类指针或引用转换为派生类指针或引用 —— 称为 向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。
隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++ 使用虚成员函数来满足这种需求。
虚成员函数和动态联编
编译器对非虚方法使用静态联编,对虚方法使用动态联编。
静态联编的效率更高,因此被设置为 C++ 的默认选择。
编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为 虚函数表 。虚函数表中存储了为类对象进行声明的虚函数的地址。
注意事项
构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数。
析构函数应当是虚函数,除非类不用做基类。即使基类不需要显式析构函数提供服务,也不应依赖于默认析构函数,而应提供虚析构函数,即使它不执行任何操作。
友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。
如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。
访问控制:protected
在类外只能用公有类成员来访问 protected 部分中的类成员。private 和 protected 之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员,对于外部世界来说,保护成员的行为与私有成员相似,对于派生类来说,保护成员的行为与公有成员相似。
抽象基类
C++ 通过使用 纯虚函数 提供未实现的函数。纯虚函数声明的结尾处为 =0 。
当声明中包含纯虚函数时,则不能创建该类的对象。包含纯虚函数的类只用作基类。但 C++ 允许纯虚函数有定义。
友元、异常和其他
友元
友元类的所有方法都可以访问原始类的私有成员和保护成员。
1 | friend class Remote; |
友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。
异常
调用 abort()
Abort() 函数的原型位于头文件 cstdlib (或 stdlib.h)中,其典型实现是向标准错误流(即 cerr 使用的错误流)发送消息 abnormal program termination (程序异常终止),然后终止程序。它返回一个随实现而异的值,告诉操作系统,处理失败。也可以使用 exit() ,该函数刷新文件缓冲区,但不显示消息。
异常规范
异常规范是 C++98 新增的功能,但是 C++11 却将其摒弃了。异常规范的作用之一是,告诉用户可能需要使用 try 块。您可以使用新增的关键字 noexcept 指出函数不会引发异常。
栈解退
假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于 try 块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为 栈解退 。
其他异常特性
引发异常时编译器总是创建一个临时拷贝,即使异常规范和 catch 块中指定的是引用。
如果有一个异常类继承层次结构,应该这样排列 catch 块:将捕获位于层次结构最下面的异常类的 catch 语句放在最前面,将捕获基类异常的 catch 语句放在最后面。
假设您编写了一个编写了一个调用另一个函数的函数,而您并不知道被调用的函数可能引发哪些异常。在这种情况下,仍能够捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常:
1 | catch(...){ |
exception 类
exception 头文件(以前为exception.h 或 except.h)定义了 exception 类,C++ 可以把它用作其他异常类的基类。
1 | try{ |
stdexcept 异常
头文件 stdexcept 定义了 logic_error 和 runtime_error 类。
异常类系列 logic_error 描述了典型的逻辑错误:
domain_errorinvalid_argument:给函数传递了一个意料之外的值length_error:指出没有足够的空间来执行所需的操作out_of_bounds:指示索引错误
runtime_error 异常系列描述了可能在运行期间发生但难以预计和防范的错误
range_erroroverflow_errorunderflow_error
bad_alloc 异常和 new
对于使用 new 导致的内存分配问题,C++ 的最新处理方式是让 new 引发 bad_alloc 异常。头文件 new 包含 bad_alloc 类的声明,它是从 exception 类公有派生而来。
C++ 标准提供了一种在失败时返回空指针的 new :
1 | int * pi = new (std::nothrow) int; |
异常何时会迷失方向
如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为 意外异常(unexpected exception) 。在默认情况下,这将导致程序异常终止(虽然 C++11 摒弃了异常规范,但仍支持它,且有些现有的代码使用了它)。如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有 try 块或没有匹配的 catch 块时,将出现这种情况),则异常被称为 未捕获异常(uncaught exception) 。
未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数 terminate() 。在默认情况下,terminate() 调用 abort() 函数。可以指定 terminate() 应调用的函数(而不是 abort)来修改 terminate() 的这种行为。为此,可调用 set_terminate() 函数。set_termiate() 和 terminate() 都是在头文件 exception 中声明的。
如果要捕获所有的异常(不管是预期的异常还是意外异常),可以这样做:
首先确保异常头文件的声明可用:
1 |
|
然后,设计一个代替函数,将意外异常转换为 bad_exception 异常,该函数原型如下:
1 | void myUnexpected() |
接下来在程序的开始位置,将意外异常操作指定为调用该函数:
set_unexpected(myUnexpected);
最后,将 bad_exception 类型包括在异常规范中,并添加如下 catch 块序列:
1 | double Argh(double, double) throw(out_of_bounds, bad_exception); |
RTTI
RTTI 是 运行阶段类型识别 的简称。
C++ 有 3 个支持 RTTI 的元素。
- 如果可能的话,
dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回 0 —— 空指针。 typeid运算符返回一个指出对象的类型的值type_info结构存储了有关特定类型的信息
只能将 RTTI 用于包含虚函数的类层次结构,原因在于只有对于这种类型结构,才应该将派生类对象的地址赋给基类指针。
dynamic_cast 运算符
dynamic_cast 用于回答 “是否可以安全地将对象的地址赋给特定类型的指针”。
1 | class Grand {...}; |
通常,如果指向的对象(*pt)的类型为 Type 或者是从 Type 直接或间接派生而来的类型,则下面的表达式将指针 pt 转换为 Type 类型的指针:
1 | dynamic_cast<Type *>(pt) |
typeid 运算符和 type_info 类
typeid 运算符使得能够确定两个对象是否为同种类型。可以接受两种参数:类名;结构为对象的表达式
typeid 运算符返回一个对 type_info 对象的引用,其中,type_info 是在头文件 typeinfo(以前为 typeinfo.h)中定义的一个类。type_info 类重载了 == 和 != 运算符,以便可以使用这些运算符来对类型进行比较。
1 | typeid(Magnificent) == typeid(*pg) |
如果 pg 是一个空指针,程序将引发 bad_typeid 异常。该异常类型是从 expection 类派生而来的,是在头文件 typeinfo 中声明的。
type_info 类包含一个 name() 成员,该函数返回一个随实现而异的字符串,通常是类的名称。
类型转换运算符
dynamic_castconst_caststatic_castreinterpret_cast
dynamic_cast 运算符的用途是,使得能够在类层次结构中进行向上转换(由于 is-a 关系,这样的类型转换是安全的),而不允许其他转换。
const_cast 运算符用于执行只有一种用途的类型转换,即改变值为 const 或 volatile ,其语法与 dynamic_cast 运算符相同:
1 | const_cast<type-name>(expression) |
除了 const 和 volatile 特征(有或无)可以不同外,type_name 和 expression 的类型必须相同。
1 | High bar; |
第一个类型转换使得 *pb 成为一个可用于修改 bar 对象值的指针,它删除 const 标签。第二个类型转换是非法的,以为它同时尝试将类型从 const High * 改为 const Low *。
const_cast 不是万能的,它可以修改指向一个值的指针,但修改 const 值的结果是不确定的。
static_cast 运算符的语法与其他类型转换运算符相同:
static_cast<type_name>(expression)
仅当 type-name 可被隐式转换为 expression 所属的类型或 expression 可被隐式转换为 type_name 所属的类型时,上述转换才是合法的,否则将出错。
由于无需进行类型转换,枚举值就可以被转换为整型,所以可以用 static_cast 将整型转换为枚举值。可以使用 static_cast 将 double 转换为 int 、将 float 转换为 long 以及其他各种数值转换。
reinterpret_cast 运算符用于天生危险的类型转换。它不允许删除 const。
reinterpret_cast<type_name>(expression)
不能将函数指针转换为数据指针。
String 类和标准模板库
stirng 类
string 实际上是模板具体化 basic_string<char> 的一个 typedef ,同时省略了与内存管理相关的参数。size_type 是一个依赖于实现的整型,实在头文件 string 中定义的。string 类将 string:npos 定义为字符串的最大长度,通常为 unsighed int 的最大值。
string 类输入
1 | string stuff; |
string 版本的 getline() 将自动调整目标 string 对象的大小。
使用字符串
string 库中,rfind() 方法查找子字符串或字符最后一次出现的位置。find_first_of() 方法在字符串中查找参数中任何一个字符首次出现的位置。find_last_of() 方法查找最后一次出现的位置。find_first_not_of() 方法在字符串中查找第一个不包含在参数中的字符。
open() 方法要求使用一个 C - 风格字符串作为参数,c_str() 方法返回一个指向 C - 风格字符串的指针,该 C - 风格字符串的内容与用于调用 c_str() 方法的 string 对象相同。
1 | fout.open(filename.c_str()); |
标准模板库
一般类的声明和实现放在两个文件中,然后在使用该类的主程序代码中,包含相应的头文件.h就可以了,但是,模板类必须包含该其实现的.cpp文件才行。
模板类 vector
要创建 vector 模板对象,可使用通常的 <type> 表示法来指出要使用的类型。另外,vector 模板使用动态内存分配,因此可以使用初始化参数来指出需要多少矢量。
1 | vector<int> ratings(5); //a vector of 5 ints |
STL 容器都提供了一些基本方法,size() 返回容器中元素数目,swap() 交换两个容器的内容,begin() 返回一个指向容器中第一个元素的迭代器,end() 返回一个表示超过容器尾的迭代器。
要为 vector 的 double 类型规范声明一个迭代器,可以这样做:
1 | vector<double>::iterator pd; //pd an iterator |
push_back() 将元素添加到矢量末尾。
erase() 方法删除矢量中给定区间的元素。它接受两个迭代器参数,这些参数定义了要删除的区间。第一个迭代器指向区间的起始处,第二个迭代器位于区间中指出的后一个位置。
insert() 方法的功能与 erase() 相反。它接受 3 个迭代器参数,第一个参数制定了新元素的插入位置,第二个和第三个迭代器参数定义了被插入区间,该区间通常是另一个容器对象的一部分。
1 | vector<int> old_v; |
for_each() 函数可用于很多容器类,它接受 3 个参数。前两个是定义容器中区间的迭代器,最后一个是指向函数的指针。被指向的函数不能修改容器元素的值。
1 | for_each(books.begin(), books.end(); ShowReview); |
Random_shuffle() 函数接受两个指向区间的迭代器参数,并随机排列该区间中的元素。该函数要求容器类允许随机访问。
1 | random_shuffle(books.begin(), books.end()); |
sort() 函数也要求容器支持随机访问。第一个版本接受两个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的 < 运算符,对区间中的元素进行操作。第二个版本接受 3 个参数,前两个参数指定区间的迭代器,最后一个参数是指向要使用的函数的指针,而不是用于比较的 operator<() 。
迭代器
对 reverse_iterator 执行递增操作将导致它被递减。
1 | ostream_iterator<int, char> out_iter(cout, " "); |
vector 类有一个名为 rbegin() 的成员函数和一个名为 rend() 的成员函数,前者返回一个指向超尾的反向迭代器,后者返回一个指向第一个元素的反向迭代器。
back_insert_iterator 将元素插入到容器尾部,而 front_insert_iterator 将元素插入到容器的前端。insert_iterator 将元素插入到 insert_iterator 构造函数的参数指定的位置前面。
这些迭代器将容器类型作为模板参数,将实际的容器标识符作为构造函数参数。要为名为 dice 的 vector<int> 容器创建一个 back_insert_iterator ,可以这样做:
1 | back_insert_iterator<vector<int> > back_iter(dice); |
关联容器
STL 提供了 4 中关联容器:set 、multiset 、map 和 multimap 。前两种是在头文件 set(以前分别为 set.h 和 multiset.h)中定义的。而后两种是在头文件 map (以前分别为 map.h 和 multimap.h )中定义的。
最简单的关联容器是 set ,其值类型与键相同,键是唯一的,这意味着集合中不会有多个相同的键。multiset 类似于 set ,只是可能有多个值的键相同。
在 map 中,值与键的类型不同,键是唯一的,每个键只对应一个值。multimap 与 map 相似,只是一个键可以与多个值相关联。
set
set 使用模板参数来指定要存储的值类型
1 | set<string> A; //a set of string objects |
第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模板 less<>。
1 | set<string, less<string> >A; //older implementation |
set_union() 函数接受 5 个迭代器参数。前两个迭代器定义了第一个集合的区间,接下来的两个定义了第二个集合区间,最后一个迭代器是输出迭代器,指出将结果集合复制到什么位置。
1 | set_union(A.begin(), A.end(), B.begin(), B.end(), ostream_iterator<string, char>out(cout, " ")); |
函数 set_intersection() 和 set_difference() 分别查找交集和获得两个集合的差。方法 lower_bound() 将键作为参数并返回一个迭代器,该迭代器指向集合中第一个不小于键参数的成员。方法 upper_bound() 将键作为参数,并返回一个迭代器,该迭代器指向集合中第一个大于键参数的成员。
multimap
基本的 multimap 声明使用模板参数指定键的类型和存储的值类型。
1 | multimap<int, string> codes; |
第 3 个模板参数是可选的,指出用于对键进行排序的比较函数和对象。在默认情况下,将使用模板 less<> ,该模板将键类型作为参数。
为将信息结合在一起,实际的值类型将键类型和数据类型结合为一对。STL 使用模板类 pair<class T, class U> 将这两种值存储到一个对象中。如果 keytype 是键类型,而 datatype 是存储的数据类型,则值的类型为 pair<const keytype, datatype> 。
1 | pair<const int, string> item(213, "Los Angles"); |
对于 pair 对象,可以使用 first 和 second 成员来访问其两个部分了:
1 | pair<const int, string> item(213, "Los Angles"); |
成员函数 count() 接受键作为参数,并返回具有该键的元素数目。成员函数 lower_bound() 和 upper_bound() 将键作为参数,且工作原理与处理 set 时相同。equal_range() 用键作为参数,且返回两个迭代器,它们表示的区间与该键匹配。
输入、输出和文件
C++ 程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。缓冲区时用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。输出时,程序首先填满缓冲区,然后把整块数据传输给硬盘,并清空缓冲区,以备下一批输出使用。这被称为刷新缓冲区 。
在默认情况下,cout 用空格填充字段中未被使用的部分,可以用 fill() 成员函数来改变填充字符。新的填充字符将一直有效,直到更改它为止。
浮点数精度的含义取决于输出模式。在默认模式下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。C++ 的默认精度为 6 位(但末尾的 0 将不显示)。precision() 成员函数使得能够选择其他值。精度设置将一直有效,直到被重新设置。
ios_base 类提供了一个 setf() 函数(用于 set 标记),能够控制多种格式化特性。下面的函数调用使 cout 显示末尾的小数点:
1 | cout.setf(ios_base::showpoint); |
要左对齐,可使用下面的调用:
1 | ios_base::fmtflags old = cout.setf(ios:left, ios::adjustfield); |
要恢复以前的设置,可以这样做:
1 | cout.setf(old, ios::adjustfield); |
1 | cout.setf(ios::base::showpoint); //show trailing decimal point |
C++ 有一种让在命令行环境中运行的程序能够访问命令行参数的机制,方法是使用下面的 main() 函数:
1 | int main(int argc, char *argv[]) |
argc 为命令行中的参数个数,其中包括命令名本身。argv 变量为一个指针,它指向一个指向 char 的指针。可以将 argv 看作一个指针数组,其中的指针指向命令行参数,argv[0] 是一个指针,指向存储第一个命令行参数的字符串的第一个字符。
假设由下面的命令行:
1 | wc report1 report2 report3 |
则 arc 为 4,argv[0] 为 wc ,argv[1] 为 report1 。
C++11 新标准
C++11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加。
初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。
C++11 提供了模板类 initializer_list ,可以将其用作构造函数的参数。如果类有接受 initializer_list 作为参数的构造函数,则初始化列表语法就只能用于该构造函数。
C++11 将 auto 用于实现自动类型推断,还可简化模板声明。
关键字 decltype 将变量的类型声明为表达式指定的类型。
在函数名和参数列表后面指定返回类型:
1 | double f1(double, int); //traditional syntax |
引入关键字 explicit 以禁止单参数构造函数导致的自动转换。
为避免与运算符 >> 混淆,C++ 要求在声明嵌套模板时使用空格将尖括号分开:
1 | std::vector<std::list<int> > vl; //>> not ok |
C++11 不再这样要求:
1 | std::vector<std::list<int>> vl; //ok in C++11 |
左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。
C++11 新增了右值引用,这是使用 && 表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C - 风格字符串除外,它表示地址)、诸如 x+y 等表达式以及返回值的函数(条件是该函数返回的不是引用)。
函数 generate() 接受一个区间(由前两个参数指定),并将每个元素设置为第三个参数返回的值,而第三个参数是一个不接受任何参数的函数对象。
count_if() 与 generate() 一样,前两个参数应指定区间,而第三个参时应是一个返回 true 或 false 的函数对象。
lambda 让您能够使用匿名函数 —— 即无需给函数命名。在 C++11 中,对于接受函数指针或函数符的函数,可使用匿名函数定义(lambda)作为其参数。
1 | [](int x){return x % 3 ==0;} |
差别有两个:使用 [] 代替了函数名;没有声明返回类型。
仅当 lambda 表达式完全由一条返回语句组成时,自动类型推断才有用;否则,需要使用新增的返回类型后置语法:
1 | [](double x) -> double{int y = x; return x - y;} //return type is double |