C++ Primer Plus(第6版)笔记
C++ Primer Plus Notes
知识点来自《C++ Primer Plus(第6版)中文版》。
并非所有条目都有用,仅供查漏补缺。条目后小号数字为原文在原书中的页码。
第1章 预备知识
1.1.C++简介
- OOP 强调的是编程的数据方面,而泛型编程强调的是独立于特定数据类型。3
第2章 开始学习 C++
2.1.进入 C++
- C++对大小写敏感。11
- 类、函数和变量是 C++ 编译器的标准组件,被放置在命名空间 std 中。15
- endl 确保程序继续运行前刷新输出,而“\n”不能。17
2.2.C++语句
- C/C++可以连续使用赋值运算符(=)。20
- 在打印之前,cout 必须将整数形式的数字转换为字符串形式。20
2.3.其他 C++语句
- 要对特定对象执行允许的操作,需要给对象发送一条消息。C++提供了两种发送消息的方式:类方法(本质上是函数调用)、重新定义运算符。22
2.4.函数
- 在使用函数前,C++编译器必须知道函数的参数类型和放回值类型。C++提供这种信息的方式是使用函数原型语句。24
- 库文件中包含了函数的编译代码,而头文件中则包含了原型。24
- main 不是关键字。27
第3章 处理数据
- 内置的 C++类型分两组:基本类型和复合类型。32
3.1.简单变量
-
对类型名使用 sizeof 运算符时,应将名称放在括号中,但对变量名使用该运算符,括号是可选的。36
-
C++另一种 C 语言没有的初始化语法:
int wrens(432); 还有另一种初始化方式,这种方式用于数组和结构,但在 C++98 中,也可用于单值变量:
int hamburgers = {24}; 将大括号初始化器用于单值变量的情形还不多,但 C++11 标准使得这种情形更多了。首先,采用这种方式时,可以使用等号,也可以不使用。其次,大括号内可以不包含任何东西,在这种情况下,变量将被初始化为零。最后,这有助于更好地防范类型转换错误:
int emus{7}; int rheas = {12}; int rocs = {}; int psychics{}; 37
-
C++确保了无符号整型在上溢和下溢时会循环取值,但并不保证符号整型发生上溢和下溢时不出错。38
-
自然长度指的是计算机处理起来效率最高的长度。如果没有非常说服力的理由来选择其他类型,则应使用 int。38
-
通常,仅当有大型整数数组时,才有必要使用 short。38
-
C++使用前一(两)位来标识数字常量基数。如果第一位为 1~9,则基数为 10;如果第一位是 0,第二位为 1~7,则基数为 8;如果前两位为 0x 或 0X,则基数为 16。39
-
iostream 提供控制符 dec、hex 和 oct 分别用于指示 cout 以十进制、十六进制和八进制格式显示整数。
#include <iostream> using namespace std; int main() { int chest = 42; int waist = 42; int inseam = 42; cout << "Monsieur cuts a striking figure!" << endl; cout << "chest = " << chest << " (decimal for 42)" << endl; cout << hex; cout << "waist = " << waist << " (hexadecimal for 42)" << endl; cout << oct; cout << "inseam = " << inseam << " (octal for 42)" << endl; return 0; } Monsieur cuts a striking figure! chest = 42 (decimal for 42) waist = 2a (hexadecimal for 42) inseam = 52 (octal for 42) 40
-
程序的声明将特定的整型变量的类型告诉了 C++编译器,除非有理由存储为其他类型(如使用了特殊的后缀来表示特定的类型,或者值太大,不能存储为 int),否则 C++将整型常量存储为 int 类型。
对于后缀:添加特定后缀会以相应的类型来存储。
对于长度:对于十进制整数,使用下面几种类型中能存储该数的最小类型来表示:int、long 或 long long;对于不带后缀的十六进制或八进制整数使用下面几种类型中能存储该数的最小类型来表示:int、unsigned int、long、unsigned long、long long 或 unsigned long long。
40
-
C++实现使用的是主机编码。41
-
与 int 不同的是,char 在默认情况下既不是没有符号,也不是有符号。是否有符号由 C++实现决定。如果 char 有某种特定的行为对您来说特别重要,则可以显式地将类型设置为 signed char 或 unsigned char。45
-
wchar_t 类型是一种整数类型,它有足够的空间,可以表示系统使用的最大扩展字符集。这种类型与另一种整型(底层[underlying]类型)的长度和符号属性相同。对底层类型的选择取决于实现,在不同系统可能是不同类型(如可能是 unsigned short,也有可能是 int)。45
-
cin 和 cout 使用 char 流,处理 wchar_t 流使用 wcin 和 wcout。另外,加上前缀 L 来指示宽字符常量和宽字符串:
wchat_t bob = L'P'; wcout << L"tall" << endl; 45
-
wchar_t 类型的长度和符号特征随实现而异,而 char16_t 和 char32_t 指定了特定长度(16位和32位)和类型(无符号)。使用前缀 u/U 表示 char16_t/chat32_t 类型字符常量和字符串常量。
char16_t ch1 = u'q'; // basic character in 16-bit form char32_t ch2 = U'/U0000222B'; // universal character name in 32-bit form 45-46
3.2.const 限定符
-
const 与 #define 相比,有以下优点:
- 能够明确指定类型。
- 可以使用 C++的作用域规则将定义限制在特定的函数或文件中。
- 可以将 const 用于更复杂的类型。
46
-
C 与 C++中 const 的主要区别:
- 两者初始化后不可修改,但 C 中 const 可以不初始化,但这样后后面也没法赋值了。
- C 中 const 是常变量,拥有自己的内存空间。C++ 中 const 是变量,编译时出现的地方都会被替代为初始化的值。因此在 C++(而不是 C)中可以使用 const 值来声明数组长度。需要注意的是,编译过程中若发现对 const 使用了 extern 或者&操作符,则给对应的常量和分配存储空间(兼容C)。
47
3.3.浮点数
- C 和 C++对于有效位数的要求是,float 至少 32 位,double 至少 48 位,且不少于 float,long double 至少和 double 一样多。通常,float 为 32 位,double 为 64 位,long double 为 80、96 或128 位。48
- 在默认情况下,像 8.24 和 2.4E8 这样的浮点常量都属于 double 类型。如果希望常量为 float 类型,请使用 f 或 F 后缀。对于 long double 类型,可使用 l 或 L 后缀。49
3.4.C++运算符
-
对于运算的符号问题,满足如下规则:
(a/b)*b + a%b = a
。50 -
将浮点数转化为整型时,C++采取截取而不是四舍五入。54
-
C++11 使用的列表初始化不允许缩窄。54
-
对于表达式中的类型转换:
自动转换:在计算表达式时,可能会有整型提升,以提升运算速度。
运算时出现浮点数,转换为有效位更大的类型,否则都为整数做整型提升。
整型级别:对于有符号整数从高到低为 long long、long、int、short、signed char。无符号整型与其相同。char、unsigned char 和 signed char 级别相同。bool 级别最低。wchat_t、char16_t、char32_t 的级别与其底层类型相同。
54-55
-
强制类型转换不会改变变量本身,而是创建一个新的、指定类型的值。55
-
C++强制类型转换推荐写法:
typeName (value) 目的要让强制类型转换就像是函数调用。
55
第4章 复合类型
4.1.数组
-
数组的声明指定元素数目时,必须是整型常数或 const 值,也可以是常量表达式,即其中所有的值在编译时都是已知的。60
-
编译器不会检查数组下标使用是否有效。60
-
只有在数组定义时初始化,此后就不能使用了。
int cards[4] = {3, 6, 8, 10}; int hand[4]; hand[4] = {5, 6, 7, 8}; // not allowed hand = cards; // not allowed 61
-
数组只初始化一部分元素,编译器会把其他元素设置为 0。61
-
如果初始化时数组方括号内为空,编译器将计算元素个数。这种方法初始化字符串比较安全。61-62
4.2.字符串
- 拼接字符串常量时,第一个字符串末尾的‘\0’会被第二个字符串的第一个字符取代。63
- cin 使用空白(空格、制表符和换行符)来确定字符串的结束位置。65
- 面向行的输入 getline:第一个参数表示存储用的数组名称,第二个参数表示要读取的字符数(最多读取该值-1,因为要保留一个空字符位置)。getline 函数在读取到指定数目的字符或遇到换行符后停止读取(读取后会抛弃换行符)。65
- 面向行的输入 get:两种变体,一种参数和 getline 一样,但不会读取换行符。另一种为不带参数,表示只读取一个字符。66
- 当 get 读取空行会设置失效位,接下来的输入将会被阻断,可使用
cin.clear();
来恢复。当 getline 输入时,如果输入行包含的字符数比指定的多,则会把剩下的字符留在输入队列中,并设置失效位,关闭后面的输入。67
4.3.string 类简介
- string 类位于名称空间 std 中。68
- C++11 新增原始字符串,使用 R 做前缀并使用
"(
和)"
来做界定符。两者中冒号与括号之间可以添加相同其它内容。72-73
4.4.结构简介
- C++不提倡使用外部变量,但提倡使用外部结构声明。75
4.5.共用体
-
共用体的长度为其最大成员的长度。78
-
匿名共用体没有名称,其成员将成为位于相同地址处的变量。
struct widget { char brand[20]; int type; union { long id_num; char id_char[20]; }; }; ... widget prize; ... if (prize.type == 1) cin >> prize.id_num; else cin >> prize.id_char; 78-79
4.6.枚举
-
对于枚举,只定义了赋值运算符,没有定义算术运算符。79
-
枚举量是整型,可被提升为 int 类型,但 int 类型不能自动转换为枚举类型:
int color = blue; // vaild band = 3; // invalid color = 3 + red; // valid 79
-
如果试图对一个不恰当的值进行强制类型转换为枚举,结果是不确定的。80
-
枚举更常被用来定义相关的符号常量,而不是新类型。80
-
如果打算只使用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称。
enum {red, orange, yellow} 80
-
可以创建多个值相同的枚举量。80
-
枚举的取值范围:上限为大于最大枚举量的2的幂减一,对于下限,若最小枚举量非负,则为0,否则寻找方式与上限相同,添加负号。80
4.7.指针和自由存储空间
- 面向对象编程与传统的过程性编程的区别在于,OOP 强调的是在运行阶段而不是编译阶段进行抉择。81
- new 从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。86
- 值为 0 的指针被称为空指针。C++确保空指针不会只想有效的数据,因此他被用来表示运算符或函数失败。86
- 不要尝试释放已经释放的内存块,C++标准指出,这样做的结果是不确定的。86
- 对空指针使用 delete 是安全的。86
- 实际上,程序确实跟踪了分配的内存量,以便以后使用 delete []运算符时可以正确地释放这些内存。但这种信息不是公用的。例如,不能使用 sizeof 运算符来确定动态分配的数组包含的字节数。87
4.8.指针、数组和指针算术
-
给 cout 提供一个指针,它将打印地址。但如果指针类型为 char *,则 cout 将显示指向的字符串。如果要显示的是字符串的地址,则必须进行强制类型转换。93
-
为避免 strcpy 使用时被复制的字符串过长而原数组后面的内存被覆盖,可使用 strncpy,如:
char food[20]; stncpy(food, "a picnic basket filled with many goodies", 19); food[19] = '\0'; 94
-
根据用于分配内存的方法,C++有 3 种管理内存的方式:自动存储、静态存储和动态存储。(C++11 新增了第四种类型——线程存储)
- 自动存储:在所属的函数被调用时自动产生,在该函数结束时消亡。自动变量通常存储在栈中。
- 静态存储:整个程序执行期间都存在。使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字 static。
- 动态存储:new 和 delete 运算符管理了一个内存池,在 C++中被称为自由存储空间(free store)或堆(heap)。数据的生命周期不完全受程序或函数的生存时间控制。
96-97
4.9.类型组合
4.10.数组的替代品
-
vector 是一种动态数组。以下声明创建一个名为 vt 的 vector 对象,它可存储 n_elem 个类型为 typeName 的元素:
vector<typeName> vt(n_elem); 99
-
array 对象长度固定,使用栈(静态内存分配)。以下声明创建一个名为 arr 的 array 对象,它包含 n_elem 个类型为 typeName 的元素:
array<typeName, n_elem> arr; 与创建 vector 对象不同的是,n_elem 不能是变量。
99
第5章 循环和关系表达式
5.1.for 循环
- for 循环中的测试表达式(test-expression)结果被强制转换为 bool 类型。105
- C++针对于类定义的自增运算符,前缀版本是将值加 1,然后返回结果,后缀版本首先复制一个版本,将其加 1,然后将复制的副本返回。因此,对于类而言,前缀版本的效率比后缀版本高。112
- 逗号运算符是一个顺序点。116
- 逗号表达式的值是第二部分的值。116
5.2.while 循环
-
string 对象不使用空字符来标记字符串末尾。121
-
函数 clock 返回程序开始执行后所用的系统时间。CLOCKS_PER_SEC 常量等于每秒钟包含的系统时间单位数。ctime 将 clock_t 作为 clock 返回类型的别名。
#include <iostream> #include <ctime> int main() { using namespace std; cout << "Enter the delay time, in seconds: "; float secs; cin >> secs; clock_t delay = secs * CLOCKS_PER_SEC; cout << "starting\a\n"; clock_t start = clock(); while (clock() - start < delay) ; cout << "done \a\n"; return 0; } 123
-
C++为类型建立别名的方式有两种:使用预处理器、使用 typedef。123
5.3.do while 循环
5.4.基于范围的 for 循环(C++11)
5.5.循环和文本输入
-
检测到 EOF 后,cin 将 eofbit 和 failbit 都设置为 1。检测到 EOF,cin 的成员函数 eof 返回 true,eofbit 或 failbit 被设置为 1,cin 的成员函数 fail 返回 true。这两个函数都在事后报告,而不是预先报告。128
-
cin 的 clear 方法可清除 EOF 标记,使输入继续运行。129
-
istream 类提供了一个可以将 istream 对象转换为 bool 值的函数,读取成功时转换为 true。因此可以这样写:
while (cin.get(ch)) { ... } 129
-
对于
cin.get()
,到达 EOF 后,返回一个用符号常量 EOF 表示的特殊值,通常被定义为 -1,因为 ASCII 码中没有 -1 的字符。130 -
由于 EOF 表示的不是有效字符编码(-1),因此可能不与 char 类型兼容(因为某些系统中 char 类型是无符号的),所以
cin.get()
返回值需要赋给 int 变量而不是 int 变量(显示时需要强制转换为 char)。130
5.6.循环嵌套和二维数组
第6章 分支语句和逻辑运算符
6.1.if 语句
6.2.逻辑表达式
||
、&&
运算符是一个顺序点。141- 对于
||
、&&
,如果判定左侧表达式为 true、false,则右侧不会去判定(执行)。141、142
6.3.字符函数库 cctype
6.4.?:
运算符
6.5.switch 语句
- switch 中每个标签必须是整数常量表达式。149
6.6.break 和 continue 语句
6.7.读取数字的循环
6.8.简单文件输入/输出
-
检查文件是否被成功打开的首先方法是
is_open()
:inFile.open("bowling.txt"); if (!inFile.is_open()) exit(EXIT_FAILURE); 161
-
Windows 文本文件的每行都以回车字符和换行符结尾。通常情况下,C++在读取文件时将这两个字符转换为换行符,并在写入文件时执行相反的转换。162
第7章 函数——C++的编程模块
7.1.复习函数的基本知识
-
C++函数返回值类型不能是数组。168
-
通常,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中将其返回。随后,调用程序将查看该内存单元。168-169
-
在 C++中,函数原型不指定参数列表时应使用省略号:
void say_bye(...); 171
7.2.函数参数和按值传递
7.3.函数和数组
7.4.函数和二维数组
7.5.函数和 C-风格字符串
7.6.函数和结构
7.7.函数和 string 对象
7.8.函数与 array 对象
- 在 C++中,类对象是基于结构的。195
7.9.递归
- 与 C 语言不同的是,C++不允许 main()调用自己。196
7.10.函数指针
-
函数的地址是存储其机器语言代码的内存的开始地址。199
-
auto 自动类型推断只能用于单值初始化,而不能用于初始化列表。201-202
const double * (*pa[3])(const double *, int) = {f1, f2, f3}; // f1、f2、f3 是三个函数指针
第8章 函数探幽
8.1.内联函数
- 内联函数的实现是编译器使用相应的代码替换函数调用。208
- 内联函数不能递归。209
- 内联代码的原始实现——宏。210
8.2.引用变量
-
必须在声明引用时将其初始化。211
-
如果引用参数是 const,则编译器将在下面两种情况下生成临时变量:
- 实参的类型正确,但不是左值。
- 实参的类型不正确,但可以转换为正确的类型。
这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。
215-216
-
右值引用的目的:让库设计人员能够提供有些操作的更有效实现。216
-
如果返回对象是对应类型而不是对应类型的引用,那么返回引用时返回的是目标的拷贝。219
-
string 类定义了一种 char *到 string 的转换功能,这使得可以使用 C-风格字符串来初始化 string 对象。222
-
继承的一个特征:基类引用可以指向派生类对象,而无需进行强制类型转换。223
8.3.默认参数
- 对于带参数列表的函数,必须从右向左添加默认值。225
- 实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。225
8.4.函数重载
8.5.函数模板
- 模板并不创建任何函数,而只是告诉编译器如何定义函数。但使用对应的函数时,编译器将按模板模式创建这样的函数。231
- 试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显示具体化将出错。236
decltype(expression)
中 express 如果是个函数调用,实际并不会调用函数,而是直接查看函数原型来获悉返回类型。242
第9章 内存模型和名称空间
9.1.单独编译
-
通常,C++编译器既编译程序,也管理链接器。246
-
不要将函数定义或变量声明放到头文件中。247
-
头文件中常包含的内容:
- 函数原型。
- 使用#define 或 const 定义的符号常量。
- 结构声明。
- 类声明。
- 模板声明。
- 内联函数。
247
9.2.存储连续性、作用域和链接性
- C++11 之前,关键字 register 建议编译器使用 CPU 寄存器来存储自动变量,旨在提高访问变量速度。但 C++11 之后,只是显示地指出变量是自动的了。252-253
- 所有的静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置为 0,这种变量被称为零初始化。254
- 零初始化和常量初始化被统称为静态初始化,即编译器处理文件(翻译单元)时初始化变量。254
- 两次调用之间,静态局部变量的值将保持不变。且只在启动时进行一次初始化。258
- 在默认情况下全局变量的链接性为外部的,但 const 全局变量的链接性为内部的。260
- 可以使用 extern 关键字来覆盖默认的内部链接性。在这种情况下,所有使用该常量的文件中使用 extern 关键字来声明它,且只有一个对其初始化。261
- 所有函数的存储持续性都自动为静态的。261
- 在默认情况下,函数的链接性为外部的。可以使用 static 关键字将函数的链接性设置为内部,此时必须在原型和函数定义中同时使用 static。261
- C++要求同一个函数的所有内联定义都必须相同。261
9.3.名称空间
第10章 对象与类
10.1.过程性编程和面向对象编程
10.2.抽象和类
- 不必在类声明中使用关键字 private,因为这是类对象的默认访问控制。282
- 定义位于类声明中的函数都将自动成为内联函数。284
- 创建的每个对象都有自己的存储空间,用于存储其内部变量和类成员。但同一个类的所有对象共享同一组类方法。285
10.3.类的构造函数和析构函数
- 实际上,构造函数没有声明类型。288
- 构造函数被用来构建对象,而不能通过对象调用。290
- 当仅当没有定义任何构造函数时,编译器才会提供默认构造函数。290
- 定义默认构造函数有两种方式(不可同时使用):没有参数、提供参数并给所有参数设置默认值。290
- 析构函数调用时机:静态存储类,程序结束时;自动存储类,执行完代码块;new,使用 delete 时。291
- 在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中的相应的数据成员中。294-295
- 只要类方法不修改调用对象,就应将其声明为 const。295
10.4.this 指针
10.5.对象数组
10.6.类作用域
- 使用枚举或关键字 static 来解决类中需要特定的值。因为在类中使用
const int Months = 12
这类语句是行不通的。声明类只是描述了对象的形式,并没有创建对象,在创建类之前没有用于存储的空间。303 - 默认情况下,C++11 作用域内枚举的底层类型为 int。可以通过如以下语法修改底层类型(底层类型必须为整型):
enum class : short pizza {Small, Medium, Large, XLarge}
。304
10.7.抽象数据类型
第11章 使用类
11.1.运算符重载
11.2.计算时间:一个运算符重载示例
- 重载后的运算符必须至少有一个操作数是用户定义的类型,防止用户为标准类型重载运算符。315
- 使用运算符时不能违反运算符原来的句法规则。不能修改运算符的优先级。315
11.3.友元
- 不要在定义中使用关键字 friend。319
11.4.重载运算符:作为成员函数还是非成员函数
- 重载运算符时,不能同时使用重载的两种格式。324
11.5.再谈重载:一个矢量类
11.6.类的自动转换和强制类型转换
- 只有接受一个参数的构造函数才能作为转换函数,除非第二个参数提供默认值。336
- 关键字 explicit 用于关闭自动转换(C++11)时,传参时的隐式转换,但仍然运行显示转换。336
- 转换函数必须是类方法,不能指定返回类型,不能有参数。338
第12章 类和动态内存分配
12.1.动态内存和类
- 自动存储对象被删除的顺序与创建顺序相反。351
- 默认的复制构造函数是浅复制。353
12.2.改进后的新 String 类
- C++11 引入新关键字 nullptr 表示空指针。358
12.3.在构造函数中使用 new 时应注意的事项
12.4.有关返回对象的说明
12.5.使用指向对象的指针
12.6.复习各种技术
12.7.队列模拟
- 调用构造函数时,对象将在代码块执行前被创建。对于 const 变量,可以使用成员初始化列表来进行初始化。378
- 只有构造函数可以使用初始化列表。379
- 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的顺序无关。379
第13章 类继承
13.1.一个简单的基类
- 基类对象在程序进入派生类构造函数之前被创建。395
- 如果不调用基类构造函数,程序使用默认的基类构造函数。396
- 基类指针/引用在指针/引用派生类的情况下,不能使用派生类独有的方法。
13.2.继承:is-a 关系
13.3.多态公有继承
- 如果没有使用关键字 virtual,程序将根据应用类型或指针类型选择方法,否则使用指向的对象来选择方法。403
- 方法在基类中被声明为虚方法后,在派生类自动成为虚方法。403
- 基类声明虚析构函数,是确保释放派生类时按照正确的顺序调用析构函数。403
13.4.静态联编和动态联编
- 虚方法是动态联编。409
13.5.控制访问:protected
13.6.抽象基类
- 当类声明中包含纯虚函数时,不能创建该类对象。,纯虚函数只用作基类。416
- C++ 允许纯虚函数有定义。416
13.7.继承和动态内存分配
- 对于析构函数,派生类会执行自身代码后调用基类析构函数;对于构造函数,通过在初始化成员列表调用基类复制构造函数/默认构造函数;对于赋值运算符,通过使用作用域解析运算符显式调用基类的赋值运算符。423
13.8.类设计回顾
第14章 C++中的代码重用
14.1.包含对象成员的类
14.2.私有继承
- 使用私有继承时,基类的公有成员和保护成员都将成为派生类的私有成员。443
- 私有继承提供的特性与包含相同:获得实现,但不获得接口。443
- 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。448
14.3.多重继承
- C++ 在基类是虚的时,禁止信息通过中间类自动传递给基类。454
14.4.类模板
- 不能将模板成员函数放在独立的实现文件中。463
- 可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。可以为非类型模板参数提供默认值,这对于类模板和函数模板都是适用的。473
第15章 友元、异常和其他
15.1.友元
- 友元类的声明可以位于公有、私有或保护部分,其所在位置无关紧要。489
15.2.嵌套类
- 结构是一种其成员在默认情况下为公有的类。495
15.3.异常
- 如果函数引发了异常,而没有 try 块或没有匹配的处理程序时,在默认情况下,程序最终调用 abort() 函数。504
- 引发异常时,thorw 的内容总是创建一个临时拷贝。510
- 异常规范不适用于模板。519
15.4.RTTI
- 只能将 RTTI 用于包含虚函数的类层级结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。521
15.5.类型转换运算符
第16章 string 类和标准模板库
16.1.string 类
16.2.智能指针模板类
- new double 是 new 返回的指针,指向新分配的内存块。541
- auto_ptr 和 unique_ptr 用于特定对象,只能有一个智能指针可拥有它。542
- 使用 new 分配内存时,才能使用 auto_ptr 和 shared_ptr,使用 new [] 分配内存时,不能使用它们。不使用 new 分配内存时,不能使用 auto_ptr 或 shared_ptr;不使用 new 或 new [] 分配内存时,不能使用 unique_ptr。
16.3.标准模板库
- STL 提供了一组表示容器、迭代器、函数对象和算法的模板。545
- 迭代器是一个广义指针。它可以是指针,也可以是一个可对其执行类似指针的操作(如解除引用和递增)的对象。547
- for_each 第三个参数是指向需要执行的函数的指针,该函数不能修改容器元素的值。550
- 如果 vector 容器元素是用户定义的对象,要使用 sort(),则必须定义能够处理该类型对象的 operator<() 函数。551
- 不同于 for_each(),基于范围的 for 循环可修改容器内容,方法是指定一个引用参数。553
16.4.泛型编程
- OOP 关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码。553
- 模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。553
- 为区分++运算符的前缀版本和后缀版本,C++使用 operator++作为前缀版本,operator++(int) 作为后缀版本。555
- 作为一种编程风格,最好避免直接使用迭代器,而应尽可能使用 STL 函数。556
- 输入/输出迭代器只可读取/写入容器中的值,正向迭代器可读取、写入容器中的值,这三者都是单向迭代器,通过++运算符实现(前缀和后缀形式),可以递增,不可倒退。输入/输出迭代器被递增后不能保证之前的值仍然可被解除引用,但正向迭代器可以。双向迭代器具有正向迭代器所有特性,同时支持递减运算符(前缀和后缀形式)。随机访问迭代器具有双向迭代器所有特性,同时支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符。556-557
- 不能将继承机制用于迭代器(因为实现方式可能不同,如一个是指针一个是类)。558
- 关联容器通常是使用某种树实现的。568
16.5.函数对象
- 预定义函数符都是自适应的,因为它携带了标识参数类型和返回类型的 typedef 成员。576
- 函数符自适应的意义在于:函数适配器对象可以使用函数对象,并认为存在这些 typedef 成员。例如,接受一个自适应函数符参数的函数可以使用 result_type 成员来声明一个与函数的返回类型匹配的变量。576
16.6.算法
- STL 中,就地算法:结果被放在原始数据的位置。复制算法:结果被放在另一个位置。578
- STL 中,对于复制算法,统一的一个规定是:返回一个迭代器,该迭代器指向复制的最后一个值后面的一个位置。578
16.7.其他库
-
模板 initializer_list 是 C++11新增的。使用初始化列表语法将 STL 容器初始化为一系列值:
std::vector<double> payments {45.99, 39.23, 19.95, 89.01}; 其实是使用容器类以
initializer_list<T>
作为参数的构造函数:vector::vector<double>(initializer_list<T>) 通常考虑到 C++11 新增的通用初始化语法,可使用
{}
而不是()
来调用类构造函数。如果同时存在以对应 T 类型为参数的构造函数和以initializer_list<T>
为参数的构造函数,则会调用以initializer_list<T>
为参数的构造函数。如std::vector<int> vi {10}
调用的是std::vector<int> vi ({10})
而不是std::vector<int> vi (10)
。586-587 -
初始化列表不能做隐式的窄化转换。如以下代码就不行:
std::vector<int> values = {10, 8, 5.5}; // narrowing, compile-time error 587
第17章 输入、输出和文件
17.1.C++输入和输出概述
-
对输入进行缓冲可以让用户在将输入传输到程序之前返回并更正。C++程序通常在用户按下回车键时刷新输入缓冲区。594
-
实现管理流和缓存区相关的类关系:
streambuf 类为缓冲区提供了内存和相关处理缓冲区的方法;ios_base 类表示流的一般特征,如是否可读取、是二进制流还是文本流等;iso 类基于 iso_base,还包括一个指向 streambuf 对象的指针成员;ostream/istream 类从ios 类派生而来,提供输出/输入方法;isotream 类基于 ostream、istream 类。
595
-
程序包含 iostream 文件将自动创建8个流对象:
- cin、wcin:用于标准输入流,默认关联到标准输入设备(通常为键盘)。
- cout、wcout:用于标准输出流,默认关联到标准输出设备(通常为显示器)。
- cerr、wcerr:用于标准错误流,默认关联到标准输出设备(通常为显示器)。这个流没有缓冲区。
- clog、wclog:用于标准错误流,默认关联到标准输出设备(通常为显示器)。这个流有缓冲区。
595-596
17.2.使用 cout 进行输出
-
刷新缓冲区可通过 flush。实际上
cout << flush
,是 ostream 类对<<
进行了重载,使得该表达式调用了flush(cout)
。601 -
使用 cout 格式化:
-
修改进制:以十进制、十六进制或八进制显示,可用 dec、hex 或 oct 控制符。如:
hex(cout); cout << hex; 控制符不是成员函数,因此不必通过对象调用。
-
调整字段宽度:width 方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。width 是成员函数,因此必须使用对象来调用它。
cout << '#'; cout.width(12); cout << 12 << "#" << 24 << "#\n"; # 12#24# -
填充字符:默认情况下,cout 用空格填充字段中未被使用的部分,可使用 fill 成员函数来改变,与 width 不同的是一直有效。
cout.fill('*'); -
设置浮点数显示精度:默认情况下是6,使用 precision 成员函数来改变,与 width 不同的是一直有效。
cout.precision(2); -
打印末尾的 0 和小数点:ios_base 类提供了 setf 函数控制多种格式化特征,还定义了多个常量,用作该函数的参数。
cout::setf(ios_base::showpoint); showpoint 是 ios_base 类声明中定义的类级静态常量。类级静态常量意味着如果在成员函数定义的外面使用它,则必须在常量名前加上作用域运算符(::)。
-
再谈 setf():ios_base 类有一个受保护的数据成员,其中的各位分别控制格式化的各个方面。setf() 函数有两个原型:
fmtflags setf(fmtflags); fmtflags setf(fmtflags, fmtflags); fmtflags 是 bitmask 类型的 typedef 名,用于存储格式标记。bitmask 类型是一种用来存储各个位值的类型,它可以是整型、枚举,也可以是 STL bitset 容器。这里的主要思想是,每一位都是可以单独访问的,都有自己的含义。
ios_base 类定义了代表位置的常量:
常量 含义 ios_base::boolalpha 输入和输出 bool 值,可以为 true 或 false ios_base::showbase 对于输出,使用 C++ 基数前缀(0,0x) ios_base::showpoint 显示末尾的小数点 ios_base::uppercase 对于16进制输出,使用大写字母,E 表示法 ios_base::showpos 在正数前面加上+ 仅当基数为10时才使用加号。
对于带两个参数的,第二个参数指出要清除第一个参数中的哪些位。
第二个参数 第一个参数 含义 ios_base::basefield ios_base::dec 使用基数10 ios_base::oct 使用基数8 ios_base::hex 使用基数16 ios_base::floatfield ios_base::fixed 使用定点计数法 ios_base::scientific 使用科学计数法 ios_base::adjustfield ios_base::left 使用左对齐 ios_base::right 使用右对齐 ios_base::internal 符号或基数前缀左对齐,值右对齐 调用 setf 的效果可以用 unsetf 清除,原型如下:
void unsetf(fmtflags mask); 没有专门指示浮点数默认显示模式的标记,系统的工作原理如下:仅当只有定点位被设置时使用定点表示法;仅当只有科学位被设置时使用科学表示法;对于其他组合,使用默认模式。因此,启用默认模式可采取以下之一:
cout.setf(0, ios_base::floatfield); cout.unsetf(ios_base::floatfield); -
标准控制符:C++提供了多个控制符,能调用 setf,并自动提供正确的参数。如前面提到的 dec、hex 和 oct。如下面语句打开左对齐和定点选项:
cout << left << fixed; -
头文件 iomanip:iomanip 提供了一些其他控制符用来提供前面讨论过的服务,但表示起来更方便,如
setprecision()
、setfill()
、setw()
。
602-610
-
17.3.使用 cin 进行输入
-
cin 解释输入的方式取决于存储输入内存单元的数据类型。661
-
cin/cout 对象包含一个描述流状态的数据成员(从 ios_base 类那里继承而来),由三个 ios_base 元素组成:
- eofbit:cin 操作到达文件末尾时被设置。
- failbit:cin 操作未能读取到预期字符时被设置。I/O 失败时也有可能被设置。
- badbit:在一些无法诊断的失败破坏流时被设置。
三个状态位都被设置位0时一切顺利,0的另一种表示方法为 goodbit。
-
设置状态:clear 和 setstate 都可设置状态,使用 clear 时其他状态位会被清除,但 setstate 不会。setstate 也调用了 clear 方法,为
clear(rdstate() | s)
(rdstate 返回当前状态)。 -
I/O 和异常:使用 exceptions 方法可控制异常如何被处理,可以设置出现那些状态时引发异常。修改状态流都使用了 clear 方法,clear 方法会比较修改后的状态与 exceptions 方法设置的状态,存在位都被设为 1 时引发 ios_base::failure 异常。如以下代码:
#include <iostream> #include <exception> int main() { using namespace std; cin.exceptions(ios_base::failbit); cout << "Enter numbers: "; int sum = 0; int input; try { while (cin >> input) { sum += input; } } catch(ios_base::failure & bf) { cout << bf.what() << endl; cout << "O! the horror!\n"; } cout << "Last value entered = " << input << endl; cout << "Sum = " << sum << endl; return 0; } -
流状态的影响:设置流状态后,后面输入或输出将关闭,直到位被清除。注意,不匹配输入仍留在输入队列中。
613-615
17.4.文件输入和输出
-
重定向来自操作系统。623
-
对于大多数实现来说,包含 fstream 文件便自动包含 iostream 文件。623
-
创建多个 ofstream/ifstream 对象也会创建多个对应的缓冲区。623
-
以默认模式打开文件进行输出将自动把文件长度截为0,这相当于删除已有内容。623
-
当输入/输出流对象过期时,到文件的连接将自动关闭。也可使用 close 方法显式关闭,这种方法并未删除流,可以重新连接到同一个或另一个文件。624
-
关闭文件会刷新缓冲区。624
-
文件有文本模式和二进制模式。对于字符来说,表示都是一样的,但对于数字,文本模式会将其转换为对应字符保存,而二进制模式保存的是对应的值。630
-
使用 write/read 方法保存/读取二进制文件,这也适用于不使用虚函数的类。这种情况下只有数据数据成员被保存,而方法不会。如果类有虚方法,会复制隐藏指针(该指针指向虚函数的指针表),但下次运行程序时,虚函数表可能在不同位置,因此将该指针复制到对象中,可能造成混乱。631-632
-
string 对象本身没有包含字符串,而是指向字符串内存单元的指针,将其保存在文件时需要注意。633
-
seekg/seekp 函数将输入/输出指针移到指定的文件位置上,由于 fstream 类使用缓冲区来存储中间数据,因此指针指向的其实是缓冲区中的位置,而不是实际的文件。634
17.5.内核格式化
-
ostringstream 的 str 方法返回一个被初始化为缓冲区内容的字符串对象。使用 str 方法可以“冻结”该对象,这样便不能将信息写入该对象。639
// TODO meyok: 如何冻结?
第18章 探讨 C++新标准
18.1.复习前面介绍过的 C++11功能
18.2.移动语句和右值引用
- 移动构造函数可能修改实参,这意味着右值引用参数不应是 const。650-651
18.3.新的类功能
- 如果提供了移动构造函数或移动赋值运算符,编译器不会自动提供复制构造函数和复制赋值运算符。658
- 禁用函数只用于查找匹配函数,使用它们将导致编译错误。659-660
- C++11允许在一个构造函数的定义中使用另一个构造函数,这杯成为委托。660
- 继承构造函数,继承的基类构造函数只初始化基类成员,如果还要初始化派生类成员,应使用成员列表初始化语法。661
- override 和 final 并非关键字,而是具有特殊含义的标识符。661
18.4.Lambda 函数
-
lambda 返回类型相当于使用 decltyp 根据返回值推断得到的。仅当 lambda 表达式完全由一条返回语句组成时,自动类型推断才管用;否则需要使用返回类型后置语法:
[](int x) {return x % 3 == 0;} [](double x) -> double {int y = x; return x - y;} 663
-
lambda 可以访问作用域内的任何动态变量。只指定了变量名按值访问,如
[z]
;名称前加&
按引用访问,如[&z]
;[=]
按值访问所有;[&]
按引用访问所有;还可混合使用,如:[ted, &ed]
。664
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具