文章目录
- 前言
- 第 1 章 绪论
- 第 2 章 C++程序的组成部分
- 第 3 章 使用变量和常量
- 第 4 章 管理数组和字符串
- 第 5 章 表达式、语句和运算符(略)
- 第 6 章 控制程序流程(略)
- 第 7 章 使用函数组织代码
- 第 8 章 阐述指针和引用
- 第 9 章 类和对象
- 第 10 章 实现继承
- 第 11 章 多态
- 第 12 章 运算符类型与运算符重载
- 第 13 章 类型转换运算符
- 第 14 章 宏和模板简介
- 第 15 章 标准模板库简介
- 第 16 章 STL string 类
- 第 17 章 STL 动态数组类
- 第 18 章 STL list 和 forward_list
- 第 19 章 STL 集合类
- 第 20 章 STL 映射类
- 第 21 章 函数对象
- 第 22 章 lambda 表达式
- 第 23 章 STL 算法
- 23.1 什么是 STL 算法
- 23.2 STL 算法的分类
- 23.3 使用 STL 算法
- 23.3.1 find( )和 find_if( )
- 23.3.2 count( )和 count_if( )
- 23.3.3 search( )和 search_n( )
- 23.3.4 fill( )和 fill_n( )
- 23.3.5 std::generate( )将元素设置为运行阶段生成的值
- 23.3.6 for_each( )
- 23.3.7 std::transform( )对范围进行变换
- 23.3.8 复制和删除操作
- 23.3.9 替换值以及替换满足给定条件的元素
- 23.3.10 排序、在有序集合中搜索以及删除重复元素
- 23.3.11 将范围分区 partition( )
- 23.3.12 在有序集合中插入元素 lower_bound( )和 upper_bound( )
- 第 24 章 栈和队列
- 24.3 使用 STL queue 类
- 第 25 章 使用 STL 位标志
- 第 26 章 理解智能指针
前言
提示:本文章是个人学习c++时写下的笔记,内容不全面,无任何参考价值。
参考书推荐:
- 《21天学通C++》第8版
- 《C++ Primer》
第 1 章 绪论
1.2 生成可执行程序步骤
要创建可在操作系统中运行的可执行文件,第一步是编写一个 C++程序。创建 C++应用程序的基
本步骤如下。
- 使用文本编辑器编写 C++代码。
- 使用 C++编译器对代码进行编译,将代码转换为包含在目标文件中的机器语言版本。
- 使用链接器链接编译器的输出,生成一个可执行文件(如 Windows 中的.exe 文件)。
在编译过程中,C++代码(通常包含在.cpp 文本文件中)被转换为处理器能够执行的字节码。编译器每次转换一个代码文件,生成一个扩展名为.o 或.obj 的目标文件,并忽略这个 cpp 文件可能对其他文件中代码的依赖。解析这些依存关系的工作由链接程序负责,如果链接成功,则创建一个可执行文件,供程序员执行和分发。整个过程也被称为构建可执行文件。
1.3 第一个 C++应用程序
Hello Wrold 程序(Hello.cpp)
#include <iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}
【注意】
默读程序时,知道特殊字符和关键字的发音可能会有所帮助。
例如,对于#include,可读作 hash-include、sharp-include 或 pound-include,这取决于您以前的背景。
同样,对于 std::cout,可读作 standard-c-out,而 endl 可读作 end-line。
1.4 在Linux上编写C++程序
- 编写上述Hello.cpp文件
- 请打开终端,切换到文件 Hello.cpp 所在的目录,再使用如下命令行
调用 g++编译器和链接程序:g++ -o hello Hello.cpp
该命令行让 g++编译 C++文件 Hello.cpp,并创建一个名为 hello 的可执行文件
第 2 章 C++程序的组成部分
2.1 Hello World 程序的组成部分
程序清单 2.1 HelloWorldAnalysis.cpp:分析一个简单的 C++程序
1: // Preprocessor directive that includes header iostream
2: #include <iostream>
3:
4: // Start of your program: function block main()
5: int main()
6: {
7: /* Write to the screen */
8: std::cout << "Hello World" << std::endl;
9:
10: // Return a value to the OS
11: return 0;
12: }
可将程序划分为两个部分:
- 以#打头的预处理器编译指令
- 以 int main( )打头的程序主体
2.1.1 预处理器编译指令 #include
预处理器是一个在编译前运行的工具。预处理器编译指令是向预处理器发出的命令,总是以磅字符#打头。在程序清单 2.1 中,第 2 行的#include 让预处理器获取指定文件(这里是 iostream)的内容,并将它们放在在编译指令所处的位置。iostream 是一个标准头文件,让您能够在第 8 行使用 std::cout 将 Hello World 显示到屏幕上。换句话说,编译器之所以能够编译包含 std::cout 的第 8 行,是因为第 2 行指示预处理器包含了 std::cout 的定义。
问:#include 的作用是什么?
答:这是一个预处理器编译指令。
预处理器在您调用编译器时运行。该指令使得预处理器将 include后面的<>中的文件读入程序,其效果如同将这个文件输入到源代码中的这个位置。
2.1.2 程序的主体 main( )
预处理器编译指令的后面是程序的主体—main( )函数,执行 C++程序时总是从这里开始。声明main( )时,总是在它前面加上 int,这是一种标准化约定,表示 main( )函数的返回类型为整数。
【注意】
在很多 C++应用程序中,都使用了类似于下面的 main( )函数变种:
int main (int argc, char* argv[])
这符合标准且可以接受,因为其返回类型为 int。括号内的内容是提供给程序的参数。该程序可能允许用户执行时提供命令行参数,如:
program.exe /DoSomethingSpecific
其中/DoSomethingSpecific 是操作系统传递给程序的参数,以便在 main (int argc, char*argv[])中进行处理。
下面来看第 8 行,它实际执行该程序的功能:
std::cout << "Hello World" << std::endl;
cout 是在名称空间 std 中定义的一个流(因此这里使用了 std::cout),这里使用流插入运算符<<将文本 Hello World放到这个流中。
std::endl 用于换行,将其插入流中相当于插入回车。每次需要将新实体插入流中时,都使用了流插入运算符<<。
2.2 名称空间概念
在这个程序中,使用的是 std::cout 而不是 cout,原因在于 cout 位于标准(std)名称空间中。
那么什么是名称空间呢?
假设调用 cout 时没有使用名称空间限定符,且编译器知道 cout 存在于两个地方,编译器应调用哪个呢?当然,这会导致冲突,进而无法通过编译。这就是名称空间的用武之地。
名称空间是给代码指定的名称,有助于降低命名冲突的风险。通过使用 std::cout,您命令编译器调用名称空间 std 中独一无二的 cout。
很多程序员发现,使用 cout 和 std 名称空间中的其他功能时,在代码中添加 std 限定符很繁琐。为避免添加该限定符,可使用声明 using namespace
,如程序清单 2.2 所示。
程序清单 2.2 using namespace 声明
1: // Preprocessor directive
2: #include <iostream>
3:
4: // Start of your program
5: int main()
6: {
7: // Tell the compiler what namespace to search in
8: using namespace std;
9:
10: /* Write to the screen using std::cout */
11: cout << "Hello World" << endl;
12:
13: // Return a value to the OS
14: return 0;
15: }
请注意第 8 行。通过告诉编译器您要使用名称空间 std,在第 11 行使用 cout 和 endl 时,就无需显
式地指定名称空间了。
程序清单 2.3 是程序清单 2.2 的更严谨版本,它没有包含整个名称空间,而只包含要使用的元素。
1: // Preprocessor directive
2: #include <iostream>
3:
4: // Start of your program
5: int main()
6: {
7: using std::cout;
8: using std::endl;
9:
10: /* Write to the screen using std::cout */
11: cout << "Hello World" << endl;
12:
13: // Return a value to the OS
14: return 0;
15: }
在程序清单 2.3 中,使用第 7~8 行替换了程序清单 2.2 的第 8 行。
两者的差别在于,前者让您能够在不显式指定名称空间限定符 std::的情况下使用名称空间 std 中的所有元素(cout、cin 等),而后者让您能够在不显式指定名称空间限定符 std::的情况下使用 std::cout 和 std::endl。
第 3 章 使用变量和常量
3.1 变量
3.1.1 声明并初始化多个类型相同的变量
int firstNumber = 0, secondNumber = 0, multiplicationResult = 0;
3.1.2 全局变量
变量 firstNumber、secondNumber 和multiplicationResult 都不是在函数内部声明的,这些变量为全局变量。
1: #include <iostream>
2: using namespace std;
3:
4: // three global integers
5: int firstNumber = 0;
6: int secondNumber = 0;
7: int multiplicationResult = 0;
8:
9: void MultiplyNumbers ()
10: {
11: cout << "Enter the first number: ";
12: cin >> firstNumber;
13:
14: cout << "Enter the second number: ";
15: cin >> secondNumber;
16:
17: // Multiply two numbers, store result in a variable
18: multiplicationResult = firstNumber * secondNumber;
19:
20: // Display result
21: cout << "Displaying from MultiplyNumbers(): ";
22: cout << firstNumber << " x " << secondNumber;
23: cout << " = " << multiplicationResult << endl;
24: }
25: int main ()
26: {
27: cout << "This program will help you multiply two numbers" << endl;
28:
29: // Call the function that does all the work
30: MultiplyNumbers();
31:
32: cout << "Displaying from main(): ";
33:
34: // This line will now compile and work!
35: cout << firstNumber << " x " << secondNumber;
36: cout << " = " << multiplicationResult << endl;
37:
38: return 0;
39: }
3.1.3 命名约定
在函数名 MultiplyNumbers()中,每个单词的首字母都大写,这被称为 Pascal 拼写法,而在变量名 firstNumber、secondNumber 和 multiplicationResult 中,第一个单词的首字母采用小写,这被称为骆驼拼写法。
可能遇到这样的 C++代码,即在变量名开头包含指出变量类型的字符。这种约定被称为匈牙利表示法,在 Windows 应用程序编程中很常见。对于变量名 firstNumber,如果使用匈牙利表示法,将为iFirstNumber,其中前缀 i 表示整型。如果这个变量为全局整型变量,其名称将g_iFirstNumber。几年来,匈牙利表示法不那么流行了,其中的原因之一是集成开发环境(IDE)得到了改进,能够在需要时(如被鼠标指向时)显示变量的类型。
3.2 变量类型
类型 | 值 |
---|---|
bool | true/false |
char | 256 个字符值 |
unsigned short in | t 0~65535 |
short int | –32768~32767 |
unsigned long int | 0~4294967295 |
long int | –2147483648~2147483647 |
int (16 位) | –32768~32767 |
int (32 位) | –2147483648~2147483647 |
unsigned int(16 位) | 0~65535 |
unsigned int(32 位) | 0~4294967295 |
float | 1.2e–38~3.4e38 |
double | 2.2e–308~1.8e308 |
3.2.1 选择正确的数据类型以免发生溢出错误
诸如 short、int、long、unsigned short、unsigned int、unsigned long 等数据类型的容量有限,如果算术运算的结果超出了选定数据类型的上限,将导致溢出。
就拿 unsigned short 来说吧,它占用 16 位内存,因此取值范围为 0~65535。usigned short 变量的值
为 65535 后,如果再加 1,将导致溢出,结果为 0。
3.2.2 浮点类型 float 和 double
要声明一个可存储小数值的float 变量,可像下面这样做:
float pi = 3.14;
要声明双精度浮点数(double)变量,可像下面这样做:
double morePrecisePi = 22.0 / 7;
【提示】
C++14 新增了用单引号表示的组块分隔符(chunking separator)。使用这种分隔符可提高代码的可读性,如下面的初始化语句所示:
int moneyInBank = -70’000; // -70000
long populationChange = -85’000; // -85000
long long countryGDPChange = -70’000’000’000; //-70 billion
double pi = 3.141’592’653’59; // 3.14159265359
3.3 使用 sizeof 确定变量的长度
C++提供了一个方便的运算符—sizeof,可用于确定变量的长度(单位为字节)或类型。
程序清单 3.5 获悉标准 C++变量类型的长度
1: #include <iostream>
2:
3: int main()
4: {
5: using namespace std;
6: cout << "Computing the size of some C++ inbuilt variable types" << endl;
7:
8: cout << "Size of bool: " << sizeof(bool) << endl;
9: cout << "Size of char: " << sizeof(char) << endl;
10: cout << "Size of unsigned short int: " << sizeof(unsigned short) << endl;
11: cout << "Size of short int: " << sizeof(short) << endl;
12: cout << "Size of unsigned long int: " << sizeof(unsigned long) << endl;
13: cout << "Size of long: " << sizeof(long) << endl;
14: cout << "Size of int: " << sizeof(int) << endl;
15: cout << "Size of unsigned long long: "<< sizeof(unsigned long long)<<endl;
16: cout << "Size of long long: " << sizeof(long long) << endl;
17: cout << "Size of unsigned int: " << sizeof(unsigned int) << endl;
18: cout << "Size of float: " << sizeof(float) << endl;
19: cout << "Size of double: " << sizeof(double) << endl;
20:
21: cout << "The output changes with compiler, hardware and OS" << endl;
22:
23: return 0;
24: }
输出:
Computing the size of some C++ inbuilt variable types
Size of bool: 1
Size of char: 1
Size of unsigned short int: 2
Size of short int: 2
Size of unsigned long int: 4
Size of long: 4
Size of int: 4
Size of unsigned long long: 8
Size of long long: 8
Size of unsigned int: 4
Size of float: 4
Size of double: 8
The output changes with compiler, hardware and OS
程序清单 3.5 的输出指出了各种类型的长度(单位为字节),这是针对我使用的平台(编译器、操作系统和硬件)而言的。具体地说,这是在 64 位系统中以 32 位模式(使用 32 位编译器进行编译)运行该程序得到的结果。如果使用 64 位编译器进行编译,结果可能不同。
之所以使用 32 位编译器,是因为这样该应用程序在 32 位和 64 位系统上都能运行。
输出表明,无符号类型和相应的有符号类型的长度相同,唯一的差别在于,后者的 MSB 包含符号信息。
【注意】
C++11 引入了固定宽度的整型,让您能够以位为单位指定整数的宽度。这些类型为 int8_t和unit8_t,分别用于存储 8 位的有符号和无符号整数。您还可能使用 16 位、32 位和 64位的整型,它们为 int16_t、uint16_t、int32_t、uint32_t、int64_t 和 uint64_t。要使用这些类型,必须包含头文件<cstdint>。
3.3.1 使用列表初始化避免缩窄转换错误
使用取值范围较大的变量来初始化取值范围较小的变量时,将面临出现缩窄转换错误的风险。例如下例:
int largeNum = 5000000;
short smallNum = largeNum; // compiles OK, yet narrowing error
缩窄转换并非只能在整型之间进行,但如果使用 double 值来初始化 float 变量、使用 int 值来初始化 float 或 double 变量,或者使用 float 值来初始化 int 变量,可能导致缩窄转换错误。
有些编译器可能发出警告,但这种警告并不会导致程序无法通过编译。在这种情况下,程序可能在运行阶段出现 bug,但这种 bug 并非每次运行时都会出现。
为避免这种问题,C++11 引入了列表初始化来禁止缩窄。要使用这种功能,可将用于初始化的变量或值放在大括号({})内。列表初始化的语法如下:
int largeNum = 5000000;
short anotherNum{ largeNum }; // error! Amend types
int anotherNum{ largeNum }; // OK!
float someFloat{ largeNum }; // error! An int may be narrowed
float someFloat{ 5000000 }; // OK! 5000000 can be accomodated
这种功能的作用虽然不明显,但可避免在执行阶段对数据进行缩窄转换导致的 bug:这种 bug 是不合理的初始化导致的,难以发现。
3.4 使用 auto 自动推断类型
在有些情况下,根据赋给变量的初值,很容易知道其类型。例如,如果将变量的初值设置成了 true,就可推断其类型为 bool。如果您使用的编译器支持 C++11 和更高版本,可不显式地指定变量的类型,而使用关键字 auto:auto coinFlippedHeads = true;
这将指定变量 coinFlippedHeads 的类型的任务留给了编译器。编译器检查赋给变量的初值的性质,再确定将变量声明为什么类型最合适。
程序清单 3.6 使用关键字 auto 依靠编译器的类型推断功能
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: auto coinFlippedHeads = true;
7: auto largeNumber = 2500000000000;
8:
9: cout << "coinFlippedHeads = " << coinFlippedHeads;
10: cout << " , sizeof(coinFlippedHeads) = " << sizeof(coinFlippedHeads) << endl;
11: cout << "largeNumber = " << largeNumber;
12: cout << " , sizeof(largeNumber) = " << sizeof(largeNumber) << endl;
13:
14: return 0;
15: }
输出:
coinFlippedHeads = 1 , sizeof(coinFlippedHeads) = 1
largeNumber = 2500000000000 , sizeof(largeNumber) = 8
在第 6 和 7 行声明变量 coinFlippedHeads 和 largeNumber 时,没有将其类型分别指定为 bool 和 long long,而使用了关键字 auto。这让编译器去决定变量的类型,而编译器将根据初始值来确定合适的类型。
【注意】
使用 auto 时必须对变量进行初始化,因为编译器需要根据初始值来确定变量的类型。如果将变量的类型声明为 auto,却不对其进行初始化,将出现编译错误。
3.5 使用 typedef 替换变量类型
C++允许您将变量类型替换为您认为方便的名称,为此可使用关键字 typedef。在下面的示例中,程序员想给 unsigned int 指定一个更具描述性的名称—STRICTLY_POSITIVE_INTEGER:
typedef unsigned int STRICTLY_POSITIVE_INTEGER;
STRICTLY_POSITIVE_INTEGER numEggsInBasket = 4532;
编译时,第 1 行告诉编译器,STRICTLY_POSITIVE_INTEGER 就是 unsigned int。以后编译器再遇到已定义的类型 STRICTLY_POSITIVE_INTEGER 时,就会将它替换为 unsigned int 并继续编译。
3.6 是常量
在 C++中,常量可以是:
- 字面常量;
- 使用关键字 const 声明的常量;
- 使用关键字 constexpr 声明的常量表达式(C++11 新增的);
- 使用关键字 enum 声明的枚举常量;
- 使用#define 定义的常量(已摒弃,不推荐)。
3.6.1 字面常量
字面常量可以是任何类型:布尔型、整型、字符串等。在您编写的第一个 C++程序(程序清单 1.1)中,您使用了如下语句来显示 Hello World:
std::cout << "Hello World" << std::endl;
其中的 Hello World 就是一个字符串字面常量。您几乎一直在使用字面常量!当您像下面这样声明整型变量 someNumber 时:
int someNumber = 10;
将这个整型变量的初始值设置成了 10。这个 10 是代码的一部分,被编译到应用程序中,是不可修改的,因此也是字面常量。您可能使用八进制字面值来初始化整型变量,如下所示:
int someNumber = 012 // octal 12 evaluates to decimal 10
从 C++14 起,您还可使用二进制字面量,如下所示:
int someNumber = 0b1010; // binary 1010 evaluates to decimal 10
在 C++中,您还可定义自己的字面量,如温度 32.0_F(华氏)或 0.0_C(摄氏)、距离16_m(英里)或 10_km(公里)等。
这些后缀(_F、_C、_m 和_km)被成为用户定义的字面量.
3.6.2 使用 constexpr 定义常量表达式
通过关键字 constexpr,可让常量声明像函数:
constexpr double GetPi() {return 22.0 / 7;}
在一个常量表达式中,可使用另一个常量表达式:
constexpr double TwicePi() {return 2 * GetPi();}
常量表达式看起来像函数,但在编译器和应用程序看来,它们提供了优化可能性。只要编译器能够从常量表达式计算出常量,就可在语句和表达式中可使用常量的地方使用它。
程序清单 3.8 使用常量表达式来计算 pi 的值
1: #include <iostream>
2: constexpr double GetPi() { return 22.0 / 7; }
3: constexpr double TwicePi() { return 2 * GetPi(); }
4:
5: int main()
6: {
7: using namespace std;
8: const double pi = 22.0 / 7;
9:
10: cout << "constant pi contains value " << pi << endl;
11: cout << "constexpr GetPi() returns value " << GetPi() << endl;
12: cout << "constexpr TwicePi() returns value " << TwicePi() << endl;
13: return 0;
14: }
//输出警告
constant pi contains value 3.14286
constexpr GetPi() returns value 3.14286
constexpr TwicePi() returns value 6.28571
这个程序演示了两种计算 pi 值的方法:一是在第 8 行声明常量 pi;二是在第 2 行声明常量表达式 GetPi()。GetPi()和 TwicePi()看起来像函数,但其实不是函数。函数在程序执行期间被调用,但GetPi()和 TwicePi()是函数表达式,编译器将每个 GetPi()都替换成了 3.14286,并将每个 TwicePi()都替换成了 6.28571。通过在编译阶段对 TwicePi()进行解析,程序的执行速度比将这些计算放在函数中
时更快。
【注意】
常量表达式必须包含简单的实现,并返回简单类型,如整数、双精度浮点数等。在C++14 中,常量表达式可包含决策结构,如 if 和 switch 语句。
使用 constexpr 并不能保证一定会进行编译阶段优化。例如,如果您使用常量表达式来计算用户输入的数字的两倍,由于编译器无法计算这种表达式的结果,因此它们可能忽略关键字 constexpr,进而将常量表达式视为常规函数进行编译。
【提示】
在前面的示例中,为学习常量和常量表达式的声明语法,我们定义了常量 pi。在大多数流行的 C++编译器中,都通过常量 M_PI 提供了精度相当高的 pi 值,您可在程序中使用这个常量,但必须包含头文件<cmath>。
3.6.3 枚举
在有些情况下,变量只能有一组特定的取值。例如,彩虹不能包含青绿色,指南针的方位不能为“左”。在这些情况下,需要定义这样一种变量,即其可能取值由您指定。为此,可使用关键字 enum 来声明枚举。枚举由一组称为枚举量(emumerator)的常量组成。
程序清单 3.9 使用枚举量指示基本方位
1: #include <iostream>
2: using namespace std;
3:
4: enum CardinalDirections
5: {
6: North = 25,
7: South,
8: East,
9: West
10: };
11:
12: int main()
13: {
14: cout << "Displaying directions and their symbolic values" << endl;
15: cout << "North: " << North << endl;
16: cout << "South: " << South << endl;
17: cout << "East: " << East << endl;
18: cout << "West: " << West << endl;
19:
20: CardinalDirections windDirection = South;
21: cout << "Variable windDirection = " << windDirection << endl;
22:
23: return 0;
24: }
//输出结果
Displaying directions and their symbolic values
North: 25
South: 26
East: 27
West: 28
Variable windDirection = 26
编译器将枚举量转换为整数,每个枚举量都比前一个大 1。您可以指定起始值,如果没有指定,编译器认为起始值为 0,因此 North 的值为 0。如果愿意,还可通过初始化显式地给每个枚举量指定值。
上述代码将 4 个基本方位定义为枚举常量,并将第一个常量(North的值设置为 25(第 6 行),这自动将随后的常量分别设置为 26、27 和 28,如输出所示。第 20 行创建了一个类型为CardinalDirections的变量,并将其初始值设置为 South。第 21 行显示该变量时,编译器显示的是 South 对应的整数值 26。
3.6.4 使用#define 定义常量
首先,也是最重要的是,编写新程序时,不要使用这种常量。
这里介绍使用#define 定义常量,只是为了帮助您理解一些旧程序,它们使用下面的语法定义常量:#define pi 3.14286
#define 是一个预处理器宏,让预处理器将随后出现的所有 pi 都替换为 3.14286。预处理器将进行文本替换,而不是智能替换。编译器既不知道也不关心常量的类型。
【警告】
使用#define 定义常量的做法已被摒弃,因此不应采用这种做法。
3.7 不能用作常量或变量名的关键字
asm | else | new | this |
auto | enum | operator | throw |
bool | explicit | private | true |
break | export | protected | try |
case | extern | public | typedef |
catch | false | register | typeid |
char | float | reinterpret_cast | typename |
class | for | return | union |
const | friend | short | unsigned |
constexpr | goto | signed | using |
continue | i f | sizeof | virtual |
default | inline | static | void |
delete | int | static_cast | volatile |
do | long | struct | wchar_t |
double | mutable | switch | while |
dynamic_cast | namespace | template | |
另外,下列单词被保留 | |||
and | bitor | not_eq | xor |
and_eq | compl | or | xor_eq |
bitand | not | or_eq |
第 4 章 管理数组和字符串
4.1 一维数组
4.1.1 声明与初始化
声明一个一维数组,并将每个元素都初始化为零:
int myNumbers1 [5] = {0}; // initializes all integers to 0
int myNumbers2 [5] = {}; // initializes all integers to 0
int myNumbers3 [5] = {34, 56}; // initialize first two elements to 34 and 56 and the rest to 0
int myNumbers4 [5] = {34, 56, -21, 5002, 365};
int myNumbers5 [] = {2016, 2052, -525}; // array of 3 elements
这样的数组被称为静态数组,因为在编译阶段,它们包含的元素数以及占用的内存量都是固定的。
4.2 多维数组
4.2.1 声明和初始化多维数组
int solarPanels [2][3];
int solarPanels [2][3] = {{0, 1, 2}, {3, 4, 5}};
int threeRowsThreeColumns [3][3] = {{-501, 206, 2016}, {989, 101, 206}, {303,
456, 596}};
int solarPanels [2][3] = {0, 1, 2, 3, 4, 5};
虽然 C++让您能够模拟多维数组,但存储数组的内存是一维的。
4.3 动态数组
为减少占用的内存,可不使用前面介绍的静态数组,而使用动态数组,并在运行阶段根据需要增大动态数组。C++提供了 std::vector,这是一种方便而易于使用的动态数组。
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> myVector(5);
myVector[0] = 0;
myVector[1] = 1;
myVector[2] = 2;
myVector[3] = 3;
myVector[4] = 4;
for (auto i : myVector) {
cout << "myVector[" << i << "] = " << i << endl;
}
cout << "--------------------------------------------" << endl;
myVector.push_back(5); //用 push_back( )将这个数字插入到数组末尾。
for (auto i : myVector) {
cout << "myVector[" << i << "] = " << i << endl;
}
return 1;
}
4.4 C 风格字符串
char sayHello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd','\0'};
std::cout << sayHello << std::endl;
该数组的最后一个字符为空字符‘\0’,也被称为字符串结束字符,因为它告诉编译器,
字符串到此结束。这种 C 风格字符串是特殊的字符数组,因为总是在最后一个字符后加上空字符‘\0’。在代码中使用字符串字面量时,编译器将负责在它后面添加‘\0’。
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: char sayHello[] = {'H','e','l','l','o',' ','W','o','r','l','d','\0'};
6: cout << sayHello << endl;
7: cout << "Size of array: " << sizeof(sayHello) << endl; //包含'/0'
8:
9: cout << "Replacing space with null" << endl;
10: sayHello[5] = '\0';
11: cout << sayHello << endl;
12: cout << "Size of array: " << sizeof(sayHello) << endl;
13:
14: return 0;
15: }
输出结果:
Hello World
Size of array: 12
Replacing space with null
Hello
Size of array: 12
在数组中间插入‘\0’并不会改变数组的长度,而只会导致将该数组作为输入的字符串处理将到这个位置结束。
C 风格字符串充斥着危险,案例如下:
0: #include<iostream>
1: #include<string.h>
2: using namespace std;
3: int main()
4: {
5: cout << "Enter a word NOT longer than 20 characters:" << endl;
6:
7: char userInput [21] = {'\0'}; //初始化一个长度为21,且每个元素值为0的字符数组
8: cin >> userInput;
9:
10: cout << "Length of your input was: " << strlen (userInput) << endl;
11:
12: return 0;
13: }
该程序请求用户输入数据时不要超过 20 个字符,因为第 7 行声明了一个字符数组,用于存储用户输入,其长度是固定的(静态的),为 21 个字符。由于最后一个字符必须是终止空字符‘\0’,因此该数组最多可存储 20 个字符。
第 10 行使用了 strlen 来计算该字符串的长度。strlen遍历该字符数组,直到遇到表示字符串末尾的终止空字符,并计算遍历的字符数。cin 在用户输入的末尾插入终止空字符。strlen 的这种行为非常危险,因为如果用户输入的文本长度超过了指定的上限,strlen将跨越字符数组的边界。
4.5 C++字符串:使用 std::string
无论是处理文本输入,还是执行拼接等字符串操作,使用 C++标准字符串都是更高效、更安全的方式。不同于字符数组(C 风格字符串实现),std::string 是动态的,在需要存储更多数据时其容量将增大。
#include <iostream>
using namespace std;
int main(){
string str1 = "my name is ";
string str2;
cout << "input you name:" << endl;
getline(cin,str2);
string str = str1 + str2;
cout << str << endl;
return 0;
}
输出结果:
input you name:
Lucas
my name is Lucas
第 5 章 表达式、语句和运算符(略)
5.1 一条语句放到两行
cout << "Hello
World" << endl; // new line in string literal not allowed
这样的代码通常会导致错误—编译器指出第一行缺少引号(”)和结束语句的分号(;)。如果出于某种原因,要将一条语句放到两行中,可在第一行末尾添加反斜杆(\):
cout << "Hello \
World" << endl; // split to two lines is OK
对于前面的语句,另一种书写方式是将字符串字面量分成两个:
cout << "Hello "
"World" << endl; // two string literals is also OK
编译器注意到两个相邻的字符串字面量后,将把它们拼接成一个。
5.2 复合语句(语句块)
可使用花括号({})将多条语句组合在一起,以创建复合语句(语句块):
{
int daysInYear = 365;
cout << "Block contains an int and a cout statement" << endl;
}
语句块通常将众多语句组合在一起,指出它们属于同一条语句。编写 if 语句或循环时,语句块特别有用。
5.3 使用运算符(略)
您经常会听到前缀运算符的性能更高还是后缀运算符性能更高的争论。
++startValue 优于 startValue++。
至少从理论上说确实如此,因为使用后缀运算符时,编译器需要临时存储初始值,以防需要将其赋给其他变量。就整型变量而言,这对性能的影响几乎可以忽略不计,但对某些类来说,这种争论也许有意义。聪明的编译器可能通过优化消除这种差异。
C++提供了按位 XOR 运算,用运算符^表示。这个运算符对操作数相应的各位执行 XOR 运算。
5.3.10 按位运算符 NOT(~)、AND(&)、OR(|)和 XOR(^)
逻辑运算符和按位运算符之前的差别在于,按位运算符返回的并非布尔值,而是对操作数对应位执行指定运算的结果。
0: #include <iostream>
1: #include <bitset>
2: using namespace std;
3:
4: int main()
5: {
6: cout << "Enter a number (0 - 255): ";
7: unsigned short inputNum = 0;
8: cin >> inputNum;
9:
10: bitset<8> inputBits (inputNum);
11: cout << inputNum << " in binary is " << inputBits << endl;
12:
13: bitset<8> bitwiseNOT = (~inputNum);
14: cout << "Logical NOT ~" << endl;
15: cout << "~" << inputBits << " = " << bitwiseNOT << endl;
16:
17: cout << "Logical AND, & with 00001111" << endl;
18: bitset<8> bitwiseAND = (0x0F & inputNum);// 0x0F is hex for 00001111
19: cout << "0001111 & " << inputBits << " = " << bitwiseAND << endl;
20:
21: cout << "Logical OR, | with 00001111" << endl;
22: bitset<8> bitwiseOR = (0x0F | inputNum);
23: cout << "00001111 | " << inputBits << " = " << bitwiseOR << endl;
24:
25: cout << "Logical XOR, ^ with 00001111" << endl;
26: bitset<8> bitwiseXOR = (0x0F ^ inputNum);
27: cout << "00001111 ^ " << inputBits << " = " << bitwiseXOR << endl;
28:
29: return 0;
30: }
输出结果
Enter a number (0 - 255): 181
181 in binary is 10110101
Logical NOT ~
~10110101 = 01001010
Logical AND, & with 00001111
0001111 & 10110101 = 00000101
Logical OR, | with 00001111
00001111 | 10110101 = 10111111
Logical XOR, ^ with 00001111
00001111 ^ 10110101 = 10111010
5.3.11 按位右移运算符(>>)和左移运算符(<<)
移位运算符将整个位序列向左或向右移动,其用途之一是将数据乘以或除以 2n。
下面的移位运算符使用示例将变量乘以 2:
int doubledValue = num <<1;
下面的的移位运算符使用示例将变量除以 4:
int halvedValue = num >> 2;
5.3.14 运算符优先级
等级 | 名称 | 运算符 |
---|---|---|
1 | 作用域解析运算符 | :: |
2 | 成员选择、下标、后缀递增和后缀递减 | .、->、( )、++、– |
3 | sizeof、前缀递增和递减、求补、逻辑 NOT、单目加和减、取址和解除引用、new、new[]、delete、delete[]、类型转换、sizeof( ) | ++、–、^、!、+、-、&、*、( ) |
4 | 用于指针的成员选择 | . x 、-> x |
5 | 乘、除、求模 | *、/、% |
6 | 加、减 | +、- |
7 | 移位(左移和右移) | <<、>> |
8 | 不等关系 | <、<=、>、>= |
9 | 相等关系 | ==、!= |
10 | 按位 AND | & |
11 | 按位 XOR | ^ |
12 | 按位 OR | | |
13 | 逻辑 AND | && |
14 | 逻辑 OR | || |
15 | 条件运算符 | ?: |
16 | 赋值运算符 | =、*=、/=、%=、+=、-=、<<=、>>=、&=、 |
17 | 逗号运算符 | , |
第 6 章 控制程序流程(略)
6.1 使用 switch-case 进行条件处理
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: enum DaysOfWeek
6: {
7: Sunday = 0,
8: Monday,
9: Tuesday,
10: Wednesday,
11: Thursday,
12: Friday,
13: Saturday
14: };
15:
16: cout << "Find what days of the week are named after!" << endl;
17: cout << "Enter a number for a day (Sunday = 0): ";
18:
19: int dayInput = Sunday; // Initialize to Sunday
20: cin >> dayInput;
21:
22: switch(dayInput)
23: {
24: case Sunday:
25: cout << "Sunday was named after the Sun" << endl;
26: break;
27:
28: case Monday:
29: cout << "Monday was named after the Moon" << endl;
30: break;
31:
32: case Tuesday:
33: cout << "Tuesday was named after Mars" << endl;
34: break;
35:
36: case Wednesday:
37: cout << "Wednesday was named after Mercury" << endl;
38: break;
39:
40: case Thursday:
41: cout << "Thursday was named after Jupiter" << endl;
42: break;
43:
44: case Friday:
45: cout << "Friday was named after Venus" << endl;
46: break;
47:
48: case Saturday:
49: cout << "Saturday was named after Saturn" << endl;
50: break;
51:
52: default:
53: cout << "Wrong input, execute again" << endl;
54: break;
55: }
56:
57: return 0;
58: }
6.2 基于范围的 for 循环
C++11 引入了一种新的 for 循环,让对一系列值(如数组包含的值)进行操作的代码更容易编写和理解。基于范围的 for 循环也使用关键字 for。
语法:
for (VarType varName : sequence)
{
// Use varName that contains an element from sequence
}
通过使用关键字 auto 来自动推断变量的类型,可编写一个通用的 for 循环,对任何类型的数组 elements 进行处理,从而进一步简化 for 语句。
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> myVector(5);
myVector[0] = 0;
myVector[1] = 1;
myVector[2] = 2;
myVector[3] = 3;
myVector[4] = 4;
for (auto i : myVector) {
cout << "myVector[" << i << "] = " << i << endl;
}
return 1;
}
6.3 使用循环计算斐波纳契数列
#include <iostream>
using namespace std;
int main(){
int num1 = 0;
int num2 = 1;
cout << num1 << " " << num2;
int count = 2; //计算数列的个数
while(true) {
cout << " " << num1 + num2;
count ++;
int temp = num1;
num1 = num2;
num2 = temp + num2;
//只输出10个
if (count >= 10){
break;
}
}
}
6.4 不使用goto语句
不推荐使用 goto 语句来编写循环,因为大量使用 goto 语句将导致代码的执行流程无法预测,即不按特定的顺序从一行跳转到另一行;在有些情况下,也可能导致变量的状态无法预测。
第 7 章 使用函数组织代码
7.1 带默认值的函数参数
#include <iostream>
using namespace std;
double getAera(double radius, double PI = 3.14){
return PI * radius * radius;
}
int main(){
double radius = 0;
cout << "input the radius:" << endl;
cin >> radius;
cout << "the PI has a default value,do you want to change(y/n)" << endl;
char myChar = 'n';
cin >> myChar;
double PI = 3.14;
if (myChar == 'y'){
cout << "please input your PI:" << endl;
cin >> PI;
}
double aera = getAera(radius,PI);
cout << "radius = " << radius << " PI = " << PI << " area = " << aera << endl;
return 0;
}
如果PI=3.14的精度足够,则可以直接调用函数:getArea(radius)
7.2 函数重载
名称和返回类型相同,但参数不同的函数被称为重载函数。
7.2.1 将数组传递给函数
#include <iostream>
using namespace std;
void display(int arr[], int length){
for (int i = 0; i < length; ++i) {
cout << arr[i] << "\t";
}
cout << endl;
}
void display(char arr[],int length){
for (int i = 0; i < length; ++i) {
cout << arr[i] << "\t";
}
cout << endl;
}
int main(){
int arr1[] = {1,2,3,4,5,6,7,8,9,10};
int len1 = 10;
display(arr1,len1);
char arr2[] = {'h','e','l','l','o'};
int len2 = 5;
display(arr2,len2);
}
//输出结果
1 2 3 4 5 6 7 8 9 10
h e l l o
7.2.2 按引用传递参数
程序清单 7.9 以引用参数(而不是返回值)的方式提供圆的面积
0: #include <iostream>
1: using namespace std;
2:
3: const double Pi = 3.1416;
4:
5: // output parameter result by reference
6: void Area(double radius, double& result)
7: {
8: result = Pi * radius * radius;
9: }
10:
11: int main()
12: {
13: cout << "Enter radius: ";
14: double radius = 0;
15: cin >> radius;
16:
17: double areaFetched = 0;
18: Area(radius, areaFetched);
19:
20: cout << "The area is: " << areaFetched << endl;
21: return 0;
22: }
输出结果:
Enter radius: 2
The area is: 12.5664
第二个形参 result 旁边的&,它告诉编译器,不要将第二个实参复制给函数,而将指向该实参的引用传递给函数。相当于起匿名。
由于 Area()的第二个参数是按引用传递的,因此 Area( )中第 8 行使用的变量 result,与 main( )中第 17 行声明的 double
areaFetched 指向同一个内存单元。
7.3 微处理器如何处理函数调用
函数调用意味着微处理器跳转到属于被调用函数的下一条指令处执行。执行完函数的指令后,将返回到最初离开的地方。
为了实现这种逻辑,编译器将函数调用转换为一条供微处理器执行的 CALL指令,该指令指出了接下来要获取的指令所在的地址,该地址归函数所有。遇到 CALL 指令时,微处理器将调用函数后将要执行的指令的位置保存到栈中,再跳转到 CALL 指令包含的内存单元处。
该内存单元包含属于函数的指令。微处理器执行它们,直到到达 RET 语句(与编写的 return 语句对应的微处理器代码)。RET 语句导致微处理器从栈中弹出执行 CALL 指令时存储的地址。该地址包含调用函数中接下来要执行的语句的位置。这样,微处理器将返回到调用函数,从离开的地方继续执行。
7.3.1 内联函数
常规函数调用被转换为 CALL 指令,这会导致栈操作、微处理器跳转到函数处执行等。听起来在幕后发生了很多事情,但在大多数情况下速度都很快。然而,如果函数非常简单,可以使用内联函数。
程序员使用关键字 inline 发出请求,要求在函数被调用时就地
展开它们。
inline double GetPi()
{
return 3.14159;
}
只执行将数字翻倍等简单操作的函数非常适合声明为内联的。
程序清单 7.10 将把整数翻倍的函数声明为内联的
0: #include <iostream>
1: using namespace std;
2:
3: // define an inline function that doubles
4: inline long DoubleNum (int inputNum)
5: {
6: return inputNum * 2;
7: }
8:
9: int main()
10: {
11: cout << "Enter an integer: ";
12: int inputNum = 0;
13: cin >> inputNum;
14:
15: // Call inline function
16: cout << "Double is: " << DoubleNum(inputNum) << endl;
17:
18: return 0;
19: }
输出结果:
Enter an integer: 35
Double is: 70
第 4 行使用了关键字 inline。编译器通常将该关键字视为请求,请求将函数 DoubleNum()的内容直接放到调用它的地方(第 16 行),以提高代码的执行速度。
将函数声明为内联的会导致代码急剧膨胀,在声明为内联的函数做了大量复杂处理时尤其如此。应尽可能少用关键字 inline,仅当函数非常简单,需要降低其开销时(如前面所示),才应使用该关键字。
7.3.2 自动推断返回类型
从 C++14起,可以使用 auto 让编译器根据您返回的值来推断函数的返回类型,而不直接指定返回类型。
程序清单 7.11 将函数 Area()的返回类型指定为 auto
0: #include <iostream>
1: using namespace std;
2:
3: const double Pi = 3.14159265;
4:
5: auto Area(double radius)
6: {
7: return Pi * radius * radius;
8: }
9:
10: int main()
11: {
12: cout << "Enter radius: ";
13: double radius = 0;
14: cin >> radius;
15:
16: // Call function "Area"
17: cout << "Area is: " << Area(radius) << endl;
18:
19: return 0;
20: }
输出结果:
Enter radius: 2
Area is: 12.5664
意第 5 行,它将函数 Area()的返回类型指定为 auto。编译器将根据 return 语句中使用 double变量的表达式来推断返回类型。
【注意】
对于依赖于返回类型自动推断的函数,必须先定义(即实现)再调用。这是因为调用函数时,编译器必须知道其返回类型。
如果这种函数包含多条 return 语句,必须确保根据它们推断出的返回类型都相同。
另外,在递归调用后面,至少得有一条 return 语句。
7.3.3 lambda 函数
lambda 函数是 C++11 引入的,有助于使用 STL 算法对数据进行排序或处理。
排序函数要求您提供一个二元谓词,以帮助确定元素的顺序。二元谓词是一个这样的函数,即对两个参数进行比较,并在一个小于另一个时返回 true,否则返回 false。这种谓词通常被实现为类中的运算符,这导致编码工作非常烦琐。使用 lambda 函数可简化谓词的定义,如程序清单 7.12 所示。
程序清单 7.12 使用 lambda 函数对数组中的元素进行排序并显示它们
0: #include <iostream>
1: #include <algorithm>
2: #include <vector>
3: using namespace std;
4:
5: void DisplayNums(vector<int>& dynArray)
6: {
7: for_each (dynArray.begin(), dynArray.end(), \
8: [](int Element) {cout << Element << " ";} );
9:
10: cout << endl;
11: }
12:
13: int main()
14: {
15: vector<int> myNums;
16: myNums.push_back(501);
17: myNums.push_back(-1);
18: myNums.push_back(25);
19: myNums.push_back(-35);
20:
21: DisplayNums(myNums);
22:
23: cout << "Sorting them in descending order" << endl;
24:
25: sort (myNums.begin(), myNums.end(), \
26: [](int Num1, int Num2) {return (Num2 < Num1); } );
27:
28: DisplayNums(myNums);
29:
30: return 0;
31: }
输出:
501 -1 25 -35
Sorting them in descending order
501 25 -1 -35
函数 DisplayNums()使用 STL 算法 for_each 遍历数组的每个元素,并显示其值。为此,它在第 8 行使用了一个 lambda 函数。
第 25 行使用 std::sort 时,也以 lambda 函数的方式提供了一个二元谓词(第 26 行),这个函数在第二个数比第一个数小时返回 true,这相当于将集合按升序排列。
第 8 章 阐述指针和引用
8.1将 sizeof( )用于指针的结果
如果使用 32 位编译器编译代码,将 sizeof 用于指针的结果为 4 字节;如果使用的是 64 位编译器,并在 64 位系统上运行该程序,可能发现将 sizeof 用于指针的结果为 64 位,即 8字节。
8.2 动态内存分配
8.2.1 new 和 delete 动态地分配和释放内存
运算符 new 和 delete 分配和释放自由存储区中的内存。自由存储区是一种内存抽象,表现为一个内存池,应用程序可分配(预留)和释放其中的内存。
使用 new 来分配新的内存块。通常情况下,如果成功,new 将返回指向一个指针,指向分配的内存,否则将引发异常。Type* Pointer = new Type;
需要为多个元素分配内存时,还可指定要为多少个元素分配内存:Type* Pointer = new Type[numElements];
【注意】
new 表示请求分配内存,并不能保证分配请求总能得到满足,因为这取决于系统的状态以及内存资源的可用性。
使用 new 分配的内存最终都需使用对应的 delete 进行释放:
Type* Pointer = new Type; // allocate memory
delete Pointer; // release memory allocated above
这种规则也适用于为多个元素分配的内存:
Type* Pointer = new Type[numElements]; // allocate a block
delete[] Pointer; // release block allocated above
【注意】
对于使用 new[…]分配的内存块,需要使用 delete[]来释放;对于使用 new 为单个元素分配的内存,需要使用 delete 来释放。
不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给您的应用程序。这将减少可供其他应用程序使用的系统内存量,甚至降低您的应用程序的执行速度。这被称为内存泄露,应不惜一切代价避免这种情况发生。
【警告】
不能将运算符 delete 用于任何包含地址的指针,而只能用于 new 返回的且未使用 delete释放的指针。
8.2.2 将递增和递减运算符用于指针
int 指针值位0x002EFB34,int本身长 4 字节,因此该整型数据占用 0x002EFB34~0x002EFB37 的内存。
将递增运算符用于该指针后,它指向的并不是 0x002EFB35,因为指向 int 中间毫无意义。如果对指针执行递增或递减运算,编译器将认为您要指向内存块中相邻的值(并假定这个值的类型与前一个值相同),而不是相邻的字节(除非值的长度刚好是 1 字节,如 char)。
因此,对整型指针执行递增运算将导致它增加4字节,即sizeof(int)。将++用于该指针相当于告诉编译器,您希望它指向下一个 int,因此递增后该指针将指向 0x002EFB38。同样,将该指针加 2 将导致它向前移动两个 int,即 8 字节。
使用运算符–将指针递减的效果类似:将指针包含的地址值减去它指向的数据类型的 sizeof。
#include <iostream>
using namespace std;
int main(){
const int len = 3;
int * p_arr = new int[len];
p_arr[0] = 0;
p_arr[1] = 1;
p_arr[2] = 2;
for (int i = 0; i < len; ++i) {
cout << *(p_arr + i) << "\t";
}
delete[] p_arr;
p_arr = NULL;
}
8.2.3 将关键字 const 用于指针
const 指针有如下三种:
- 指针常量:指针包含的地址是常量,不能修改,但可修改指针指向的数据
int daysInMonth = 30;
int* const pDaysInMonth = &daysInMonth;
*pDaysInMonth = 31; // OK! Data pointed to can be changed
int daysInLunarMonth = 28;
pDaysInMonth = &daysInLunarMonth; // Not OK! Cannot change address!
- 常量指针:指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方
int hoursInDay = 24;
const int* pointsToInt = &hoursInDay;
int monthsInYear = 12;
pointsToInt = &monthsInYear; // OK!
*pointsToInt = 13; // Not OK! Cannot change data being pointed to
int* newPointer = pointsToInt; // Not OK! Cannot assign const to non-const
- 指针包含的地址以及它指向的值都是常量,不能修改(这种组合最严格):
int hoursInDay = 24;
const int* const pHoursInDay = &hoursInDay;
*pHoursInDay = 25; // Not OK! Cannot change data being pointed to
int daysInMonth = 30;
pHoursInDay = &daysInMonth; // Not OK! Cannot change address
8.2.4 指针作为函数参数
指针是一种将内存空间传递给函数的有效方式,其中可包含函数完成其工作所需的数据,也可包含操作结果。将指针作为函数参数时,确保函数只能修改您希望它修改的参数很重要。
例如,如果函数根据以指针方式传入的半径计算圆的面积,就不应允许它修改半径。为控制函数可修改哪些参数以及不能修改哪些参数,可使用关键字 const。
程序清单 8.10 在计算圆面积的函数中使用关键字 const
0: #include <iostream>
1: using namespace std;
2:
3: void CalcArea(const double* const ptrPi, // const pointer to const data
4: const double* const ptrRadius, // i.e. no changes allowed
5: double* const ptrArea) // can change data pointed to
6: {
7: // check pointers for validity before using!
8: if (ptrPi && ptrRadius && ptrArea)
9: *ptrArea = (*ptrPi) * (*ptrRadius) * (*ptrRadius);
10: }
11:
12: int main()
13: {
14: const double Pi = 3.1416;
15:
16: cout << "Enter radius of circle: ";
17: double radius = 0;
18: cin >> radius;
19:
20: double area = 0;
21: CalcArea (&Pi, &radius, &area);
22:
23: cout << "Area is = " << area << endl;
24:
25: return 0;
26: }
输出:
Enter radius of circle: 10.5
Area is = 346.361
8.2.5 数组和指针的类似之处
程序清单 8.11 数组变量是指向第一个元素的指针
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: // Static array of 5 integers
6: int myNumbers[5];
7:
8: // array assigned to pointer to int
9: int* pointToNums = myNumbers;
10:
11: // Display address contained in pointer
12: cout << "pointToNums = 0x" << hex << pointToNums << endl;
13:
14: // Address of first element of array
15: cout << "&myNumbers[0] = 0x" << hex << &myNumbers[0] << endl;
16:
17: return 0;
18: }
输出:
pointToNums = 0x004BFE8C
&myNumbers[0] = 0x004BFE8C
可将数组变量赋给类型与之相同的指针,,存储在指针中的地址与数组第一个元素在内存中的地址相同。
要访问第二个元素,可使用 myNumbers[1],也可通过指针 pointToNums 来访问,其语法为 *(pointToNums + 1)。要访问静态数组的第三个元素,可使用 myNumbers[2],而要访问动态数组的第三个元素,可使用语法*(pointToNums + 2)。
由于数组变量就是指针,因此也可将用于指针的解除引用运算符(*)用于数组。同样,可将数组运算符([])用于指针,如程序清单 8.12 所示。
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: const int ARRAY_LEN = 5;
6:
7: // Static array of 5 integers, initialized
8: int myNumbers[ARRAY_LEN] = {24, -1, 365, -999, 2011};
9:
10: // Pointer initialized to first element in array
11: int* pointToNums = myNumbers;
12:
13: cout << "Display array using pointer syntax, operator*" << endl;
14: for (int index = 0; index < ARRAY_LEN; ++index)
15: cout << "Element " << index << " = " << *(myNumbers + index) << endl;
16:
17: cout << "Display array using ptr with array syntax, operator[]" << endl;
18: for (int index = 0; index < ARRAY_LEN; ++index)
19: cout << "Element " << index << " = " << pointToNums[index] << endl;
20:
21: return 0;
22: }
输出:
Display array using pointer syntax, operator*
Element 0 = 24
Element 1 = -1
Element 2 = 365
Element 3 = -999
Element 4 = 2011
Display array using ptr with array syntax, operator[]
Element 0 = 24
Element 1 = -1
Element 2 = 365
Element 3 = -999
Element 4 = 2011
数组 myNumbers 和指针 pointToNums 都具有指针的特点。但不能将指针赋给数组,因为数组是静态的,不能用作左值。myNumbers 是不能修改的。
8.3 使用指针时常犯的编程错误
C++让您能够动态地分配内存,以优化应用程序对内存的使用。不同于 C#和 Java 等基于运行时环境的新语言,C++没有自动垃圾收集器对程序已分配但不能使用的内存进行清理。使用指针来管理内存资源时,程序员很容易犯错。
8.3.1 内存泄露
如果在使用 new 动态分配的内存不再需要后,程序员没有使用配套的 delete 释放,就造成了内存泄漏。
int* pointToNums = new int[5]; // initial allocation
// use pointToNums
...
// forget to release using delete[] pointToNums;
...
// make another allocation and overwrite
pointToNums = new int[10]; // leaks the previously allocated memory
8.3.2 指针指向无效的内存单元
如果指针未进行初始化,就对该指针进行取值操作或delete操作,就会爆发该错误。
程序清单 8.13 在存储布尔值的程序中错误地使用指针
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: // uninitialized pointer (bad)
6: bool* isSunny;
7:
8: cout << "Is it sunny (y/n)? ";
9: char userInput = 'y';
10: cin >> userInput;
11:
12: if (userInput == 'y')
13: {
14: isSunny = new bool;
15: *isSunny = true;
16: }
17:
18: // isSunny contains invalid value if user entered 'n'
19: cout << "Boolean flag sunny says: " << *isSunny << endl;
20:
21: // delete being invoked also when new wasn't
22: delete isSunny;
23:
24: return 0;
25: }
输出:
Is it sunny (y/n)? y
Boolean flag sunny says: 1
再次运行的输出:
Is it sunny (y/n)? n
<CRASH!>
这个程序的问题很多,有些已通过注释指出了。第 14 行分配内存并将其赋给指针,但这行代码仅在用户按 y(表示 yes)时才会执行。用户提供其他输入时,该 if 块都不会执行,因此指针 isSunny 无效。
第二次运行时,用户按 n,导致应用程序崩溃。因为 isSunny 包含无效的内存地址,而第 19 行对这个无效的指针解除引用,导致应用程序崩溃。同样,第 22 行对这个指针调用 delete,但并未使用 new 分配这个指针,这也是大错特错。
如果有指针的多个拷贝,只需对其中一个调用 delete(应避免指针拷贝满天飞)。要让这个程序更好,更安全,更稳定,应对指针进行初始化,确定指针有效后再使用并只释放指针一次(且仅当指针有效时才释放)。
8.3.3 悬浮指针(也叫迷途或失控指针、野指针)
使用 delete 释放后,任何有效指针都将无效。
我们应该在初始化指针或释放指针后将其设置为 NULL,并在使用运算符*对指针解除引用前检查它是否有效(将其与 NULL 比较)。
8.3.4 new 发出的分配请求得不到满足的两种情况
除非请求分配的内存量特大,或系统处于临界状态,可供使用的内存很少,否则 new 一般都能成功。
有些应用程序需要请求分配大块的内存(如数据库应用程序),因此最好不要假定内存分配能够成功。
C++提供了两种确认指针有效的方法:
- 默认方法是使用异常(这也是前面一直使用的方法),即如果内存分配失败,将引发 std::bad_alloc 异常。这导致应用程序中断执行,除非您提供了异常处理程序,否则应用程序将崩溃,并显示一条类似于“异常未处理”的消息。
- 不想依赖于异常的程序员可使用 new 变种 new(nothrow),这个变种在内存分配失败时不引发异常,而返回 NULL,让您能够在使用指针前检查其有效性,
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: // Request LOTS of memory space, use nothrow
6: int* pointsToManyNums = new(nothrow) int [0x1fffffff];
7:
8: if (pointsToManyNums) // check pointsToManyNums != NULL
9: {
10: // Use the allocated memory
11: delete[] pointsToManyNums;
12: }
13: else
14: cout << "Memory allocation failed. Ending program" << endl;
15:
16: return 0;
17: }
输出:
Memory allocation failed. Ending program
是new(nothrow)这个变种在分配内存失败时返回NULL,让我们能够在使用指针前检查其有效性,
8.4 引用
引用是变量的别名。
声明引用时,需要将其初始化为一个变量,因此引用只是另一种访问相应变量存储的数据的方式。
VarType original = Value;
VarType& ReferenceVariable = original;
8.4.1 使用引用的好处
典型的函数声明类似于下面这样:
ReturnType DoSomething(Type parameter);
调用函数 DoSomething( )的代码类似于下面这样:
ReturnType Result = DoSomething(argument); // function call
上述代码导致将 argument 的值复制给 Parameter,再被函数 DoSomething( )使用。如果 argument占用了大量内存,这个复制步骤的开销将很大。同样,当 DoSomething( )返回值时,这个值被复制给Result。如果能避免这些复制步骤,让函数直接使用调用者栈中的数据就太好了。
可避免复制步骤的函数版本类似于下面这样:
ReturnType DoSomething(Type& parameter); // note the reference&
调用该函数的代码类似于下面这样:
ReturnType Result = DoSomething(argument);
由于 argument 是按引用传递的,Parameter 不再是 argument 的拷贝,而是它的别名,
8.4.2 将关键字 const 用于引用
可能需要禁止通过引用修改它指向的变量的值,为此可在声明引用时使用关键字 const:
int original = 30;
const int& constRef = original;
constRef = 40; // Not allowed: constRef can’t change value in original
int& ref2 = constRef; // Not allowed: ref2 is not const
const int& constRef2 = constRef; // OK
8.4.3 按引用向函数传递参数
引用的优点之一是,可避免将形参复制给形参,从而极大地提高性能。
然而,让被调用的函数直接使用调用函数栈时,确保被调用函数不能修改调用函数中的变量很重要。为此,可将引用声明为 const的,const 引用参数不能用作左值,因此试图给它们赋值将无法通过编译。
程序清单 8.19 使用 const 引用确保被调用的函数不能修改按引用传入的值
0: #include <iostream>
1: using namespace std;
2:
3: void GetSquare(const int& number, int& result)
4: {
5: result = number*number;
6: }
7:
8: int main()
9: {
10: cout << "Enter a number you wish to square: ";
11: int number = 0;
12: cin >> number;
13:
14: int square = 0;
15: GetSquare(number, square);
16: cout << number << "^2 = " << square << endl;
17:
18: return 0;
19: }
在前一个程序中,使用同一个参数来接受输入和存储结果,但这里使用了两个参数,一个用于接受输入,另一个用于存储运算结果。
为禁止函数 GetSquare 修改传入的值,必须使用关键字 const 将其声明为 const 引用,如第 3 行所示。这让 number 自动变为其值不能修改的参数。
第 9 章 类和对象
9.1 类和对象
9.1.1 使用指针运算符(->)访问成员
如果对象是使用 new 在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法:
Human* firstWoman = new Human();
firstWoman->dateOfBirth = "1970";
firstWoman->IntroduceSelf();
delete firstWoman;
9.2 关键字 public 和 private
- C++让您能够将类属性和方法声明为公有的,这意味着有了对象后就可获取它们;
- 也可将其声明为私有的,这意味着只能在类的内部或其友元中访问。
9.2.1 使用关键字 private 实现数据抽象
够使用关键字 private指定哪些信息不能从外部访问(即在类外不可用)。另外,可将方法声明为公有的(public),以便从外部通过这些方法访问私有信息。因此,类的实现可对(其他类和函数)隐藏它们无需知道的成员信息。
回到 Human 类,其中的 age 是一个私有成员。在现实世界中,很多人不想公开自己的真实年龄。要 Human 类向外指出的年龄比实际年龄小两岁很容易,只需在公有方法 GetAge( )中将 age减 2,再返回结果,如程序清单 9.2 所示。
程序清单 9.2 一个对外隐藏真实年龄并将自己说得更年轻的 Human 类
0: #include <iostream>
1: using namespace std;
2:
3: class Human
4: {
5: private:
6: // Private member data:
7: int age;
8:
9: public:
10: void SetAge(int inputAge)
11: {
12: age = inputAge;
13: }
14:
15: // Human lies about his / her age (if over 30)
16: int GetAge()
17: {
18: if (age > 30)
19: return (age - 2);
20: else
21: return age;
22: }
23: };
在面向对象编程语言中,抽象是一个非常重要的概念,让程序员能够决定哪些属性只能让类及其成员知道,类外的任何人都不能访问(友元除外)。
9.3 构造函数
- 构造函数是一种特殊的函数,它与类同名且不返回任何值。
- 构造函数可在类声明中实现,也可在类声明外实现。
- 构造函数总是在创建对象时被调用,这让构造函数成为将类成员变量(int、指针等)初始化为选定值的理想场所。
- 默认构造函数是可选的。如果没有提供默认构造函数且无其它有参构造函数,编译器将会自动创建一个默认构造函数。这种构造函数会创建成员属性,但不会将 POD 类型(如 int)的属性初始化为非零值。
- 没有默认构造函数,而在您提供了重载的构造函数时,C++编译器不会为您生成默认构造函数
【注意】
默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造
函数。因此,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数:
class Human
{
private:
string name;
int age;
public:
// default values for both parameters
Human(string humansName = "Adam", int humansAge= 25){
name = humansName;
age = humansAge;
cout << "Overloaded constructor creates ";
cout << name << " of age " << age;
}
};
9.3.1 包含初始化列表的构造函数
冒号后面列出了各个成员变量及其初始值。初始值可以是参数,也可以是固定的值。
#include <iostream>
using namespace std;
class Person{
private:
int age;
string name;
public:
Person(int personAge = 23, string personName = "hao")
:name(personName),age(personAge)
{
cout << "带参数的默认构造函数被调用" << endl;
}
void showInfo(){
cout << "age = " << age << "\t";
cout << "name = " << name << endl;
}
};
int main(){
cout << "========= person1 =========" << endl;
Person person1;
person1.showInfo();
cout << "========= person2 =========" << endl;
Person person2(24,"shahao");
person2.showInfo();
return 0;
}
输出结果:
========= person1 =========
带参数的默认构造函数被调用
age = 23 name = hao
========= person2 =========
带参数的默认构造函数被调用
age = 24 name = shahao
【注意】
也可使用关键字 constexpr 将构造函数定义为常量表达式。在有助于提高性能的情况下,可在构造函数的声明中使用这个关键字,如下所示:
class Sample
{
const char* someString;
public:
constexpr Sample(const char* input):someString(input)
{ // constructor code }
};
9.4 析构函数
- 与构造函数一样,析构函数也是一种特殊的函数。构造函数在实化对象时被调用,而析构函数在对象销毁时自动被调用。
- 析构函数可在类声明中实现,也可在类声明外实现。
9.4.1 何时及如何使用析构函数
每当对象不再在作用域内或通过 delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。
【注意】
析构函数不能重载,每个类都只能有一个析构函数。如果您忘记了实现析构函数,编译器将创建一个伪(dummy)析构函数并调用它。伪析构函数为空,即不释放动态分配的内存。
9.5 复制构造函数
9.5.1 浅复制及其存在的问题
0: #include <iostream>
1: #include <string.h>
2: using namespace std;
3: class MyString
4: {
5: private:
6: char* buffer;
7:
8: public:
9: MyString(const char* initString) // Constructor
10: {
11: buffer = NULL;
12: if(initString != NULL)
13: {
14: buffer = new char [strlen(initString) + 1];
15: strcpy(buffer, initString);
16: }
17: }
18:
19: ~MyString() // Destructor
20: {
21: cout << "Invoking destructor, clearing up" << endl;
22: delete [] buffer;
23: }
24:
25: int GetLength()
26: { return strlen(buffer); }
27:
28: const char* GetString()
29: { return buffer; }
30: };
31:
32: void UseMyString(MyString str)
33: {
34: cout << "String buffer in MyString is " << str.GetLength();
35: cout << " characters long" << endl;
36:
37: cout << "buffer contains: " << str.GetString() << endl;
38: return;
39: }
40:
41: int main()
42: {
43: MyString sayHello("Hello from String Class");
44: UseMyString(sayHello);
45:
46: return 0;
47: }
上述程序会导致崩溃呢,原因如下:
在 main( )中,将 MyString 对象 sayHello 的交给了函数UseMyString(),如第 44 行所示。在 main( )中将工作交给这个函数的结果是,对象 sayHello 被复制到形参 str,并在UseMyString( )中使用它。编译器之所以进行复制,是因为函数 UseMyString( )的参数 str被声明为按值(而不是按引用)传递。对于整型、字符和原始指针等 POD 数据,编译器执行二进制复制,因此 sayHello.buffer 包含的指针值被复制到 str 中,即 sayHello.buffer 和 str.buffer 指向同一个内存单元。这导致两个 MyString 对象指向同一个内存单元。
函数UseMyString( )返回时,变量 str 不再在作用域内,因此被销毁。为此,将调用 MyString 类的析构函数,而该析构函数使用 delete[]释放分配给 buffer 的内存。这将导致 main( )中的对象 sayHello 指向的内存无效,而等 main( )执行完毕时,sayHello 将不再在作用域内,进而被销毁。但这次销毁操作,在不再有效的内存地址调用 delete(销毁 str 时释放了该内存,导致它无效)。正是这种重复调用 delete 导致了程序崩溃。
9.5.2 使用复制构造函数确保深复制
复制构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被复制时,编译器都将调用复制构造函数。
复制构造函数接受一个以引用方式传入的当前类的对象作为参数。这个参数是源对象的别名,使用它来编写自定义的复制代码,确保对所有缓冲区进行深复制。
#include <iostream>
using namespace std;
class Person{
public:
int * p_age;
Person(){}
Person(int personAge){
p_age = new int(personAge);
}
Person(const Person& person){
p_age = NULL;
if (person.p_age != NULL)
p_age = new int(*(person.p_age));
}
~Person(){
//cout << this->p_age <<" ~Person()" << endl;
if (p_age != NULL){
delete p_age;
}
p_age = NULL;
}
};
void usePerson(Person p){
cout << p.p_age << endl;
}
Person getPerson(Person person){
return person;
}
int main(){
Person person(23);
cout << person.p_age << endl;
//对象作为函数的返回值,以值的方式从函数返回
Person aPerson = getPerson(person);
cout << aPerson.p_age << endl;
//使用一个对象给另一个对象初始化
Person newPerson1 (person);
cout << newPerson1.p_age << endl;
Person newPerson2 = person;
cout << newPerson2.p_age << endl;
//对象作为函数的参数,以值传递的方式传给函数,会调用拷贝构造函数
usePerson(person);
return 0;
}
(重点理解上述代码)
调用拷贝构造函数主要有以下场景:
- 对象作为函数的参数,以值传递的方式传给函数
- 对象作为函数的返回值,以值的方式从函数返回
- 使用一个对象给另一个对象初始化
【注意】
通过在复制构造函数声明中使用 const,可确保复制构造函数不会修改指向的源对象。
另外,复制构造函数的参数必须按引用传递,否则复制构造函数将不断调用自己,直到耗尽系统的内存为止。
9.5.2(补) 拷贝和赋值构造的区别
#include <iostream>
using namespace std;
class Student{
public:
int age;
Student(){
this->age = 23;
}
Student(const Student& student){
age = student.age;
cout << "Student(const Student& student)" << endl;
}
Student& operator=(const Student student){
age = student.age;
cout << "Student& operator=(const Student& student)" << endl;
return *this;
}
};
int main(){
Student student;
Student student1(student); //调用拷贝构造
student1 = student; //调用赋值构造
Student student2 = student; //调用拷贝构造
Student student3;
student3 = student; //调用赋值构造
return 0;
}
输出结果
Student(const Student& student)
Student(const Student& student)
Student& operator=(const Student& student)
Student(const Student& student)
Student(const Student& student)
Student& operator=(const Student& student)
9.5.3 C++11移动构造函数详解
在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。通过上一节的学习我们知道,拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。
需要注意的是,当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员。
举个例子:
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
//拷贝构造函数
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
如上所示,我们为 demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。
可以看到,程序中定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段:
- 执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象;
- 执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);
- 执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);
- 程序执行结束前,会自行调用 demo 类的析构函数销毁 a。
注意,目前多数编译器都会对程序中发生的拷贝操作进行优化,因此如果我们使用 VS 2017、codeblocks 等这些编译器运行此程序时,看到的往往是优化后的输出结果:
construct!
class destruct!
而同样的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors命令运行(其中 demo.cpp 是程序文件的名称),就可以看到完整的输出结果:
construct! <-- 执行 demo()
copy construct! <-- 执行 return demo(),第一次拷贝
class destruct! <-- 销毁 demo() 产生的匿名对象
copy construct! <-- 执行 a = get_demo()
class destruct! <-- 销毁 get_demo() 返回的临时对象(第一次拷贝的对象)
class destruct! <-- 销毁 a
如上所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果==临时对象(第一次拷贝的对象)==中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。
为了避免深拷贝导致的效率问题。C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
上面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的整型指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。
事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
例如,下面程序对 demo 类进行了修改:
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
//添加移动构造函数
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
(&:左值引用;&&:右值引用)
可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。
在 Linux 系统中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors命令执行此程序,输出结果为:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
读者可能会问,如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。
有关 std::move() 函数的用法,感兴趣的读者可以继续阅读《C++11 move()函数:将左值强制转换为右值》一节。
9.6 构造函数和析构函数的其他用途
9.6.1 不允许复制的类
假设您需要模拟国家的政体。一个国家只能有一位总统,而 President 类面临如下风险:
President ourPresident;
DoSomething(ourPresident); // duplicate created in passing by value
President clone;
clone = ourPresident; // duplicate via assignment
显然,需要避免这样的情况发生。如果您不声明复制构造函数,C++将为您添加一个公有的默认拷贝构造函数,这破坏了您的设计,威胁着您的实现。然而,C++提供了实现这种设计范式的解决方案。要禁止类对象被复制,可声明一个私有的拷贝构造函数。这确保函数调用DoSomething(OurPresident)无法通过编译。为禁止赋值,可声明一个私有的赋值运算符。因此,解决方案如下:
class President
{
private:
President(const President&); // private copy constructor
President& operator= (const President&); // private copy assignment operator
// … other attributes
};
无需给私有复制构造函数和私有赋值运算符提供实现,只需将它们声明为私有的就足以实现目标:确保 President 的对象是不可复制的。
9.6.2 只能有一个实例的单例类
前面讨论的 President 类很不错,但存在一个缺陷:无法禁止通过实例化多个对象来创建多名总统:
President One, Two, Three;
由于复制构造函数是私有的,其中每个对象都是不可复制的,但您的目标是确保 President 类有且只有一个化身,即有了一个 President 对象后,就禁止创建其他的 President 对象。要实现这种功能强大的模式,可使用单例的概念,它使用私有构造函数、私有赋值运算符和静态实例成员。
1: #include <string>
2: using namespace std;
3:
4: class President
5: {
6: private:
7: President() {}; // private default constructor
8: President(const President&); // private copy constructor
9: const President& operator=(const President&); // assignment operator
10:
11: string name;
12:
13: public:
14: static President& GetInstance()
15: {
16: // static objects are constructed only once
17: static President onlyInstance;
18: return onlyInstance;
19: }
20:
21: string GetName()
22: { return name; }
23:
24: void SetName(string InputName)
25: { name = InputName; }
26: };
27:
28: int main()
29: {
30: President& onlyPresident = President::GetInstance();
31: onlyPresident.SetName("Abraham Lincoln");
32:
33: // uncomment lines to see how compile failures prohibit duplicates
34: // President second; // cannot access constructor
35: // President* third= new President(); // cannot access constructor
36: // President fourth = onlyPresident; // cannot access copy constructor
37: // onlyPresident = President::GetInstance(); // cannot access operator=
38:
39: cout << "The name of the President is: ";
40: cout << President::GetInstance().GetName() << endl;
41:
42: return 0;
43: }
第 28~43 行的 main( )包含大量注释,演示了各种创建 President 实例和拷贝的方式,它们都无法通过编译。下面逐一进行分析。
34: // President second; // cannot access constructor
35: // President* third= new President(); // cannot access constructor
第 34 和 35 行分别试图使用默认构造函数在堆和自由存储区中创建对象,但默认构造函数不可用,因为它是私有的,如第 7 行所示。
36: // President fourth = onlyPresident; // cannot access copy constructor
第 36 行试图使用复制构造函数创建现有对象的拷贝(在创建对象的同时赋值将调用复制构造函数),但在 main( )中不能使用复制构造函数,因为第 8 行将其声明成了私有的。
37: // OnlyPresident = President::GetInstance(); // cannot access operator=
第 37 行试图通过赋值创建对象的拷贝,但行不通,因为第 9 行将赋值运算符声明成了私有的。
9.6.3 禁止在栈中实例化的类
栈空间通常有限。如果您要编写一个数据库类,其内部结构包含数 TB 数据,可能应该禁止在栈上实例化它,而只允许在自由存储区中创建其实例。为此,关键在于将析构函数声明为私有的:
通过声明私有的析构函数,可禁止像下面这样创建实例:
class MonsterDB
{
private:
~MonsterDB(); // private destructor
//... members that consume a huge amount of data
};
int main()
{
MonsterDB myDatabase; // compile error
// … more code
return 0;
}
上述代码试图在栈上创建实例。退栈时,将弹出栈中的所有对象,因此编译器需要在 main( )末尾调用析构函数~MonsterDB(),但这个析构函数是私有的,即不可用,因此上述语句将导致编译错误。
将析构函数声明为私有的并不能禁止在堆中实例化:
int main()
{
MonsterDB* myDatabase = new MonsterDB(); // no error
// … more code
return 0;
}
上述代码将导致内存泄露。由于在 main 中不能调用析构函数,因此也不能调用 delete。(delete关键字原理是调用析构函数)
为了解决这种问题,需要在 MonsterDB 类中提供一个销毁实例的静态公有函数
0: #include <iostream>
1: using namespace std;
2:
3: class MonsterDB
4: {
5: private:
6: ~MonsterDB() {}; // private destructor prevents instances on stack
7:
8: public:
9: static void DestroyInstance(MonsterDB* pInstance)
10: {
11: delete pInstance; // member can invoke private destructor
12: }
13:
14: void DoSomething() {} // sample empty member method
15: };
16:
17: int main()
18: {
19: MonsterDB* myDB = new MonsterDB(); // on heap
20: myDB->DoSomething();
21:
22: // uncomment next line to see compile failure
23: // delete myDB; // private destructor cannot be invoked
24:
25: // use static member to release memory
26: MonsterDB::DestroyInstance(myDB);
27:
28: return 0;
29: }
【总结】如何创建禁止在栈中实例化的类。
关键是将构造函数声明成私有的,为分配内存,还要有像 DestroyInstance( )的静态函数,因为在 main( )中不能 调用 delete。
9.6.4 使用构造函数进行类型转换
#include <iostream>
using namespace std;
class Student{
private:
int age;
public:
//这样的转换构造函数让您能够执行隐式转换:
Student(int studentAge):age(studentAge){
cout << "Student(int studentAge)" << endl;
}
};
void study(Student student){
cout << "study(Student student)" << endl;
}
int main(){
Student student = 10; //执行隐式转换 Student student(10)
study(11); //执行隐式转换 study(Student(11))
}
函数study(Student student)被声明为接受一个 Student (而不是 int)参数!前面的代码为何可行呢?这是因为编译器知道 Student 类包含一个将整数作为参数的构造函数,进而替您执行了隐式转换:将您提供的整数作为参数发送给这个构造函数,从而创建一个Human 对象。
为避免隐式转换,可在声明构造函数前使用关键字 explicit。并非必须使用关键字 explicit,但在很多情况下,这都是一种良好的编程实践。
explicit Student(int studentAge):age(studentAge){
cout << "Student(int studentAge)" << endl;
}
9.7 this指针
在 C++中,一个重要的概念是保留的关键字 this。在类中,关键字 this 包含当前对象的地址,换句话说,其值为&object。当您在类成员方法中调用其他成员方法时,编译器将隐式地传递 this 指针。
9.8 将 sizeof( )用于类
#include<iostream>
using namespace std;
class Student{
private:
int age;
char sex;
public:
explicit Student(int studentAge,char studentSex)
:age(studentAge),sex(studentSex)
{
cout << "Student(int studentAge)" << endl;
}
};
int main(){
cout << sizeof (Student) << endl;
return 0;
}
输出结果:
8
sizeof( )对某些属性进行填充,使其与字边界对齐,所以上述代码中,类的大小为8字节。
9.9 结构体不同于类的地方
关键字 struct 来自 C 语言,在 C++编译器看来,它与类及其相似,差别在于程序员未指定时,默认的访问限定符(public 和 private)不同。
因此,除非指定了,否则结构中的成员默认为公有的(而类成员默认为私有的);
另外,除非指定了,否则结构以公有方式继承基结构(而类为私有继承)。
struct Human
{
// constructor, public by default (as no access specified is mentioned)
Human(const MyString& humansName, int humansAge, bool humansGender)
: name(humansName), age (humansAge), Gender(humansGender) {}
int GetAge ()
{
return age;
}
private:
int age;
bool gender;
MyString name;
};
//正如您看到的,结构 Human 与类 Human 很像;结构的实例化与类的实例化也很像:
Human firstMan("Adam", 25, true);
9.10 友元
不能从外部访问类的私有数据成员和方法,但这条规则不适用于友元类和友元函数。要声明友元类或友元函数,可使用关键字 friend。
#include <iostream>
using namespace std;
class Person{
private:
friend class God; //友元类
friend void showInfo(Person person); //友元全局方法
int age;
string name;
public:
Person(int age,string name)
:age(age),name(name){}
};
void showInfo(Person person){
cout << "age = " << person.age << "\t name= " << person.name << endl;
}
class God{
public:
void showInfo(Person person){
cout << "age = " << person.age << "\t name= " << person.name << endl;
}
};
int main(){
God().showInfo(Person(23,"hao"));
showInfo(Person(24,"kai"));
return 0;
}
9.11 共用体:一种特殊的数据存储机制
共用体是一种特殊的类,每次只有一个非静态数据成员处于活动状态。因此,共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。
9.11.1 声明共用体
要声明共用体,可使用关键字 union,再在这个关键字后面指定共用体名称,然后在大括号内指定
其数据成员:
union UnionName
{
Type1 member1;
Type2 member2;
…
TypeN memberN;
};
要实例化并使用共用体,可像下面这样做:
UnionName unionObject;
unionObject.member2 = value; // choose member2 as the active member
【注意】
与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。另外,将 sizeof()用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活动状态。
9.11.2 共用体的用法
在结构中,常使用共用体来模拟复杂的数据类型。共用体可将固定的内存空间解释为另一种类型,有些实现利用这一点进行类型转换或重新解释内存,但这种做法存在争议,而且可采用其他替代方式。
0: #include <iostream>
1: using namespace std;
2:
3: union SimpleUnion
4: {
5: int num;
6: char alphabet;
7: };
8:
9: struct ComplexType
10: {
11: enum DataType
12: {
13: Int,
14: Char
15: } Type;
16:
17: union Value
18: {
19: int num;
20: char alphabet;
21:
22: Value() {}
23: ~Value() {}
24: }value;
25: };
26:
27: void DisplayComplexType(const ComplexType& obj)
28: {
29: switch (obj.Type)
30: {
31: case ComplexType::Int:
32: cout << "Union contains number: " << obj.value.num << endl;
33: break;
34:
35: case ComplexType::Char:
36: cout << "Union contains character: " << obj.value.alphabet << endl;
37: break;
38: }
39: }
40:
41: int main()
42: {
43: SimpleUnion u1, u2;
44: u1.num = 2100;
45: u2.alphabet = 'C';
46: cout << "sizeof(u1) containing integer: " << sizeof(u1) << endl;
47: cout << "sizeof(u2) containing character: " << sizeof(u2) << endl;
48:
49: ComplexType myData1, myData2;
50: myData1.Type = ComplexType::Int;
51: myData1.value.num = 2017;
52:
53: myData2.Type = ComplexType::Char;
54: myData2.value.alphabet = 'X';
55:
56: DisplayComplexType(myData1);
57: DisplayComplexType(myData2);
58:
59: return 0;
60: }
输出结果:
sizeof(u1) containing integer: 4
sizeof(u2) containing character: 4
Union contains number: 2017
Union contains character: X
这个示例表明,对共用体 u1 和 u2 使用 sizeof()时,返回的值相同,虽然 u1 被用来存储一个 int,而 u2 被用来存储一个比 int 短的 char。这是因为对于共用体,编译器为其预留最大成员占用的内存量。
第 9~25 行定义了结构 ComplexType,它包含两个数据成员:类型为枚举 DataType 的 Type 和共用体 Value,其中前者用于指出后者存储的是哪种对象。换句话说,这个结
构使用枚举来存储信息的类型,并使用共用体来存储实际值。这是共用体的一种常见用法。例如,在Windows 应用程序编程中常用的结构 VARIANT 就以这样的方式使用了共用体。
第 27~39 行定义了函数 DisplayComplexType(),它根据枚举的值执行 switch-case 结构中相应的 case 部分。出于演示考虑,这里的共用体包含构造函数和析构函数;
就程序清单 9.16 而言,由于共用体包含的是普通数据类型,因此这些构造函数和析构函数是可选的,但如果共用体包含用户定义的数据类型(如类或结构),构造函数和析构函数就可能是必不可少的。
9.12 对类和结构使用聚合初始化
下面的初始化语法被称为聚合初始化(aggregate initialization)语法:
Type objectName = {argument1, …, argumentN};
例如:
int myNums[] = { 9, 5, -1 }; // myNums is int[3]
char hello[6] = { 'h', 'e', 'l', 'l', 'o', ' \0' };
然而,并非只有由整数或字符等简单类型组成的数组属于聚合类型,类(以及结构和共用体)也可能属于聚合类型。
满足如下条件的类或结构为聚合类型:
- 可作为一个整体进行初始化:
- 只包含公有和非静态数据成员,而不包含私有或受保护的数据成员;
- 不包含任何虚成员函数;只涉及公有继承(不涉及私有、受保护和虚拟继承);
- 不包含用户定义的构造函数。
0: #include <iostream>
1: #include<string>
2: using namespace std;
3:
4: class Aggregate1
5: {
6: public:
7: int num;
8: double pi;
9: };
10:
11: struct Aggregate2
12: {
13: char hello[6];
14: int impYears[3];
15: string world;
16: };
17:
18: int main()
19: {
20: int myNums[] = { 9, 5, -1 }; // myNums is int[3]
21: Aggregate1 a1{ 2017, 3.14 };
22: cout << "Pi is approximately: " << a1.pi << endl;
23:
24: Aggregate2 a2{ {'h', 'e', 'l', 'l', 'o'}, {2011, 2014, 2017}, "world"};
25:
26: // Alternatively
27: Aggregate2 a2_2{'h', 'e', 'l', 'l', 'o', '\0', 2011, 2014, 2017, "world"};
28:
29: cout << a2.hello << ' ' << a2.world << endl;
30: cout << "C++ standard update scheduled in: " << a2.impYears[2] << endl;
31:
32: return 0;
33: }
输出:
Pi is approximately: 3.14
hello world
C++ standard update scheduled in: 2017
【警告】
聚合初始化只初始化共用体的第一个非静态成员。对于程序清单 9.16 声明的共用体,聚合初始化代码如下:
SimpleUnion u1{ 2100 }, u2{ ‘C’ };
// In u2, member num (int) is initialized to ‘C’(ASCII 67)
// Although, you wished to initialize member alphabet(char)
因此,出于清晰考虑,最好不要对共用体使用聚合初始化。
9.12.1 将 constexpr 用于类和对象
第 3 章介绍了 constexpr,这为改善 C++应用程序的性能提供了一种强有力的方式。通过使用constexpr 来声明操作常量或常量表达式的函数,可让编译器计算并插入函数的结果,而不是插入计算结果的指令。
这个关键字也可用于类和结果为常量的对象,如程序清单 9.18 所示。请注意,将这样的函数或类用于非常量实体时,编译器将忽略关键字 constexpr。
程序清单 9.18 将关键字 constexpr 用于 Human 类
0: #include <iostream>
1: using namespace std;
2:
3: class Human
4: {
5: int age;
6: public:
7: constexpr Human(int humansAge) :age(humansAge) {}
8: constexpr int GetAge() const { return age; }
9: };
10:
11: int main()
12: {
13: constexpr Human somePerson(15);
14: const int hisAge = somePerson.GetAge();
15:
16: Human anotherPerson(45); // 报错 not constant expression
17:
18: return 0;
19: }
注意到第 3~9 行定义的 Human 类与以前稍有不同:其构造函数和成员函数 GetAge()的声明中包含关键字 constexpr。这个关键字让编译器尽可能将创建和使用 Human 实例的代码视为常量表达式。第 13 行声明了一个 Human 常量实例,而第 14 行使用了该常量,因此编译器将计算结果,从而改善代码的执行性能。在第 16 行,没有将实例 anotherPerson 声明为常量,因此编译器不会将实例化它的代码视为常量表达式。
第 10 章 实现继承
10.1 继承继承
10.1.1 访问限定符 protected
与 public 和 private 一样,protected 也是一个访问限定符。将属性声明为 protected 的时,相当于允许派生类和友元类访问它,但禁止在继承层次结构外部(包括 main( ))访问它。
10.1.2 基类初始化—向基类传递参数
enum PersonType{
StudentType = 0,
TeacherType
};
class Person{
protected:
PersonType personType;
public:
Person(PersonType personType):personType(personType){}
void introduceSelf(){
switch (personType) {
case PersonType::StudentType:
cout << "I am a student" << endl;
break;
case PersonType::TeacherType:
cout << "I am a teacher" << endl;
break;
}
}
};
class Student : public Person{
public:
Student():Person(PersonType::StudentType){}
};
class Teacher : public Person{
public:
Teacher():Person(PersonType::TeacherType){}
};
int main(){
Student student;
student.introduceSelf();
Teacher teacher;
teacher.introduceSelf();
return 0;
}
Person 有一个构造函数,它接受一个参数,用于初始化 Person::personType。因此,要创建 Person对象,必须提供一个用于初始化该保护成员的参数。这样,Person类便避免了保护成员包含随机值,尤其是派生类忘记设置它时。派生类 Student和 Teacher被迫定义一个这样的构造函数,即使用合适的参数来实例化基类 Person 。
10.1.3 在派生类中覆盖基类的方法
如果派生类实现了从基类继承的函数,且返回值和特征标相同,就相当于覆盖了基类的这个方法,如下面的代码所示:
0: #include <iostream>
1: using namespace std;
2:
3: class Fish
4: {
5: private:
6: bool isFreshWaterFish;
7:
8: public:
9: // Fish constructor
10: Fish(bool isFreshWater) : isFreshWaterFish(isFreshWater){}
11:
12: void Swim()
13: {
14: if (isFreshWaterFish)
15: cout << "Swims in lake" << endl;
16: else
17: cout << "Swims in sea" << endl;
18: }
19: };
20:
21: class Tuna: public Fish
22: {
23: public:
24: Tuna(): Fish(false) {}
25:
26: void Swim()
27: {
28: cout << "Tuna swims real fast" << endl;
29: }
30: };
31:
32: class Carp: public Fish
33: {
34: public:
35: Carp(): Fish(true) {}
36:
37: void Swim()
38: {
39: cout << "Carp swims real slow" << endl;
40: }
41: };
42:
43: int main()
44: {
45: Carp myLunch;
46: Tuna myDinner;
47:
48: cout << "About my food" << endl;
49:
50: cout << "Lunch: ";
51: myLunch.Swim();
52:
53: cout << "Dinner: ";
54: myDinner.Swim();
55:
56: return 0;
57: }
输出:
About my food
Lunch: Carp swims real slow
Dinner: Tuna swims real fast
10.1.4 调用基类中被覆盖的方法
如果要在 main( )中调用 Fish::Swim( ),需要使用作用域解析运算符(::),如下所示:
myDinner.Fish::Swim(); // invokes Fish::Swim() using instance of Tuna
10.1.5 在派生类中调用基类的方法
通常,Fish::Swim( )包含适用于所有鱼类(包括金枪鱼和鲤鱼)的通用实现。如果要在 Tuna::Swim( )和 Carp::Swim( )的实现中重用 Fish::Swim( )的通用实现,可使用作用域解析运算符(::),如下面的代码所示:
class Carp: public Fish
{
public:
Carp(): Fish(true) {}
void Swim()
{
cout << "Carp swims real slow" << endl;
Fish::Swim(); // invoke base class function using operator::
}
};
10.1.6 在派生类中隐藏基类的方法
覆盖的一种极端情形是,Tuna::Swim( )可能隐藏 Fish::Swim( )的所有重载版本,使得调用这些重载版本会导致编译错误(因此称为被隐藏),程序清单 10.6 演示了这一点。
程序清单 10.6 Tuna::Swim( )隐藏了重载方法 Fish::Swim(bool)
0: #include <iostream>
1: using namespace std;
2:
3: class Fish
4: {
5: public:
6: void Swim()
7: {
8: cout << "Fish swims... !" << endl;
9: }
10:
11: void Swim(bool isFreshWaterFish) // overloaded
12: {
13: if (isFreshWaterFish)
14: cout << "Swims in lake" << endl;
15: else
16: cout << "Swims in sea" << endl;
17: }
18: };
19:
20: class Tuna: public Fish
21: {
22: public:
23: void Swim()
24: {
25: cout << "Tuna swims real fast" << endl;
26: }
27: };
28:
29: int main()
30: {
31: Tuna myDinner;
32:
33: cout << "About my food" << endl;
34:
35: // myDinner.Swim(false);//failure: Tuna::Swim() hides Fish::Swim(bool)
36: myDinner.Swim();
37:
38: return 0;
39: }
要通过 Tuna 实例调用 Fish::Swim(bool),可采用如下解决方案。
- 解决方案 1:在 main( )中使用作用域解析运算符(::):
myDinner.Fish::Swim();
- 解决方案 2:在 Tuna 类中,使用关键字 using 解除对Fish::Swim( )的隐藏:
class Tuna: public Fish
{
public:
using Fish::Swim; // unhide all Swim() methods in class Fish
void Swim()
{
cout << "Tuna swims real fast" << endl;
}
};
- • 解决方案 3:在 Tuna 类中,覆盖 Fish::Swim( )的所有重载版本(如果需要,可通过 Tuna::Swim( )调用方法Fish::Swim( )):
class Tuna: public Fish
{
public:
void Swim(bool isFreshWaterFish)
{
Fish::Swim(isFreshWaterFish);
}
void Swim()
{
cout << "Tuna swims real fast" << endl;
}
};
10.1.7 继承中构造和析构顺序
class Base
{
public:
Base()
{
cout << "Base构造函数!" << endl;
}
~Base()
{
cout << "Base析构函数!" << endl;
}
};
class Son : public Base
{
public:
Son()
{
cout << "Son构造函数!" << endl;
}
~Son()
{
cout << "Son析构函数!" << endl;
}
};
void test01()
{
//继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
Son s;
}
int main() {
test01();
system("pause");
return 0;
}
输出:
Base构造函数!
Son构造函数!
Son析构函数!
Base析构函数!
请按任意键继续. . .
【总结】
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
10.2 继承方式
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
10.3 切除问题
如果程序员像下面这样做,结果将如何呢?
Son son;
Base objectBase = son;
如果程序员像下面这样做,结果又将如何呢?
void UseBase(Base input);
...
Son son;
UseBase(son);
它们都将 son对象复制给 Base 对象,一个是通过显式地复制,另一个是通过传递参数。在这些情形下,编译器将只复制 son 的 Base 部分,即不是整个对象。换句话说,son 的数据成员包含的信息将丢失。这种无意间裁减数据,导致 son 变成 Base 的行为称为切除(slicing)。
10.5 多继承
C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
class Base1 {
public:
Base1()
{
m_A = 100;
}
public:
int m_A;
};
class Base2 {
public:
Base2()
{
m_A = 200; //开始是m_B 不会出问题,但是改为mA就会出现不明确
}
public:
int m_A;
};
//语法:class 子类:继承方式 父类1 ,继承方式 父类2
class Son : public Base2, public Base1
{
public:
Son()
{
m_C = 300;
m_D = 400;
}
public:
int m_C;
int m_D;
};
//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
}
int main() {
test01();
system("pause");
return 0;
}
【总结】 多继承中如果父类中出现了同名情况,子类使用时候要加作用域
10.6 菱形继承
菱形继承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承者两个派生类
- 这种继承被称为菱形继承,或者钻石继承
菱形继承问题:
-
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用动物的数据时,就会产生二义性。
-
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
解决方法:
使用关键字virtual,实现虚继承。底层原理不是继承父类的数据,而且拥有一个指针指向该数据。数据只有一份。
class Animal
{
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
10.6 使用 final 禁止继承
从 C++11 起,编译器支持限定符 final。被声明为 final 的类不能用作基类。例如,Platypus 类表示一种进化得很好的物种,因此您可能想将其声明为 final 的,从而禁止继承它。
可像下面这样做:类Platypus 继承类Mammal、类Bird和类Reptile,且不允许由类继承Platypus类。
class Platypus final: public Mammal, public Bird, public Reptile{
public:
void Swim() {
cout << "Platypus: Voila, I can swim!" << endl;
}
};
第 11 章 多态
11.1 多态基础
11.1.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
下面通过案例进行讲解多态
程序清单 11.1 将 Tuna 实例传递给 Fish 参数,并通过该参数调用方法
0: #include <iostream>
1: using namespace std;
2:
3: class Fish
4: {
5: public:
6: void Swim()
7: {
8: cout << "Fish swims! " << endl;
9: }
10: };
11:
12: class Tuna:public Fish
13: {
14: public:
15: // override Fish::Swim
16: void Swim()
17: {
18: cout << "Tuna swims!" << endl;
19: }
20: };
21:
22: void MakeFishSwim(Fish& inputFish)
23: {
24: // calling Fish::Swim
25: inputFish.Swim();
26: }
27:
28: int main()
29: {
30: Tuna myDinner;
31:
32: // calling Tuna::Swim
33: myDinner.Swim();
34:
35: // sending Tuna as Fish
36: MakeFishSwim(myDinner);
37:
38: return 0;
39: }
输出:
Tuna swims!
Fish swims!
第36行代码,虽然传入的是 Tuna 对象,但是MakeFishSwim(Fish&)也将其视为 Fish,进而调用 Fish::Swim。第 2 行输出表明,虽然传入的是 Tuna对象,但得到的却是 Fish 的输出(这也适用于 Carp 对象)。
理想情况下,我们希望 Tuna 对象表现出金枪鱼的行为,即便通过 Fish 参数调用 Swim( )时亦如此。换句话说,第 25 行调用 inputFish.Swim( )时,我们希望执行的是 Tuna::Swim( )。要实现这种多态行为—让 Fish 参数表现出其实际类型(派生类 Tuna)的行为,可将 Fish::Swim( )声明为虚函数。
11.1.2 虚函数实现多态
通过使用关键字 virtual,可确保编译器调用覆盖版本。也就是说,如果 Swim( )被声明为虚函数,则将参数 myFish(其类型为 Fish&)设置为一个 Tuna 对象时,myFish.Swim( )将执行 Tuna::Swim( )。
程序清单 11.2 将 Fish::Swim( )声明为虚函数带来的影响
0: #include <iostream>
1: using namespace std;
2:
3: class Fish
4: {
5: public:
6: virtual void Swim()
7: {
8: cout << "Fish swims!" << endl;
9: }
10: };
11:
12: class Tuna:public Fish
13: {
14: public:
15: // override Fish::Swim
16: void Swim()
17: {
18: cout << "Tuna swims!" << endl;
19: }
20: };
21:
22: class Carp:public Fish
23: {
24: public:
25: // override Fish::Swim
26: void Swim()
27: {
28: cout << "Carp swims!" << endl;
29: }
30: };
31:
32: void MakeFishSwim(Fish& inputFish)
33: {
34: // calling virtual method Swim()
35: inputFish.Swim();
36: }
37:
38: int main()
39: {
40: Tuna myDinner;
41: Carp myLunch;
42:
43: // sending Tuna as Fish
44: MakeFishSwim(myDinner);
45:
46: // sending Carp as Fish
47: MakeFishSwim(myLunch);
48:
49: return 0;
50: }
输出:
Tuna swims!
Carp swims!
函数 MakeFishSwim(Fish&)与程序清单 11.1 中完全相同,但输出截然不同。首先,根本没有调用Fish::Swim( ),因为存在覆盖版本 Tuna::Swim( )和 Carp::Swim( ),它们优先于被声明为虚函数的Fish::Swim( )。这很重要,它意味着在 MakeFishSwim( )中,可通过 Fish&参数调用派生类定义的 Swim( ),而无需知道该参数指向的是哪种类型的对象。
void MakeFishSwim(Fish& inputFish),参数这里必须使用引用&
这就是多态:将派生类对象视为基类对象,并执行派生类的 Swim( )实现
11.1.3 虚析构函数
class Person{
public:
Person(){
cout << "Person()" << endl;
}
~Person(){
cout << "~Person()" << endl;
}
};
class Student : public Person{
public:
Student(){
cout << "Student()" << endl;
}
~Student(){
cout << "~Student()" << endl;
}
};
void destroyPerson(Person* person){
delete person;
}
int main(){
Student * student = new Student;
destroyPerson(student);
cout << "=============================" << endl;
Student student1;
return 0;
}
输出结果:
Person()
Student()
~Person()
=============================
Person()
Student()
~Student()
~Person()
main( )中,第 37 行使用 new 在自由存储区中创建了一个 Student实例;然后马上使用辅助函数destroyPerson( )释放分配的内存。出于比较的目的,同时也创建了Student实例—局部变量 student1,main( )结束时,它将不再在作用域内。输出是由 Person和 Student类的构造函数和析构函数中的 cout 语句生成的。
注意到由于使用了关键字 new,在自由存储区中构造了 Person和 Student,但 delete 没有调用 Student的析构函数,而只调用了 Person的析构函数;而构造和析构局部变量student1时,调用了基类和派生类的构造函数和析构函数,这形成了鲜明的对比。
但是在析构过程中,需要调用所有相关的析构函数,包括~Student( );显然是什么地方出了问题。这个程序清单表明,对于使用 new 在自由存储区中实例化的派生类对象,如果将其赋给基类指针,并通过该指针调用 delete,将不会调用派生类的析构函数。这可能导致资源未释放、内存泄露等问题,必须引起重视。
要避免这种问题,可将基类析构函数声明为虚函数,修改如下。
class Person{
public:
Person(){
cout << "Person()" << endl;
}
virtual ~Person(){ //虚析构函数
cout << "~Person()" << endl;
}
};
【注意】
务必像下面这样将基类的析构函数声明为虚函数:
class Base
{
public:
virtual ~Base() {}; // virtual destructor
};
这可避免将 delete 用于 Base 指针时,派生类实例未被妥善销毁的情况发生。
11.1.4 虚函数的工作原理
请看下面的 Base 类,它声明了 N 个虚函数:
class Base
{
public:
virtual void Func1()
{
// Func1 implementation
}
virtual void Func2()
{
// Func2 implementation
}
// .. so on and so forth
virtual void FuncN()
{
// FuncN implementation
}
};
下面的 Derived 类继承了 Base 类,并覆盖了除 Base::Func2( )外的其他所有虚函数:
class Derived: public Base
{
public:
virtual void Func1()
{
// Func2 overrides Base::Func2()
}
// no implementation for Func2()
virtual void FuncN()
{
// FuncN implementation
}
};
编译器见到这种继承层次结构后,知道 Base 定义了一些虚函数,并在 Derived 中覆盖了它们。在这种情况下,编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(VirtualFunction Table,VFT)。
换句话说,Base 和 Derived 类都将有自己的虚函数表。实例化这些类的对象时,将创建一个隐藏的指针(我们称之为 VFT*),它指向相应的 VFT。可将 VFT 视为一个包含函数指针的静态数组,其中每个指针都指向相应的虚函数,如下图 所示。
每个虚函数表都由函数指针组成,其中每个指针都指向相应虚函数的实现。在类 Derived 的虚函数表中,除一个函数指针外,其他所有函数指针都指向 Derived 本地的虚函数实现。Derived 没有覆盖Base::Func2( ),因此相应的函数指针指向 Base 类的 Func2( )实现。
这意味着遇到下述代码时,编译器将查找 Derived 类的 VFT,确保调用 Base::Func2( )的实现:
CDerived objDerived;
objDerived.Func2();
调用被覆盖的方法时,也将如此:
void DoSomething(Base& objBase)
{
objBase.Func1(); // invoke Derived::Func1
}
int main()
{
Derived objDerived;
DoSomething(objDerived);
};
在这种情况下,虽然将 objDerived 传递给了 objBase,进而被解读为一个 Base 实例,但该实例的VFT 指针仍指向 Derived 类的虚函数表,因此通过该 VTF 执行的是 Derived::Func1( )。
虚函数表就是这样帮助实现 C++多态的。
11.1.5 抽象基类和纯虚函数
不能实例化的基类被称为抽象基类,这样的基类只有一个用途,那就是从它派生出其他类。在 C++中,要创建抽象基类,可声明纯虚函数。
以下述方式声明的虚函数被称为纯虚函数:
class AbstractBase
{
public:
virtual void DoSomething() = 0; // pure virtual method
};
该声明告诉编译器,AbstractBase 的派生类必须实现方法 DoSomething( ):
11.2 表明覆盖意图的限定符 override
class Fish
{
public:
virtual void Swim()
{
cout << "Fish swims!" << endl;
}
};
假设派生类 Tuna 要定义函数 Swim(),但特征标稍微不同—程序员原本想覆盖 Finish::Swim(),但不小心插入了关键字 const,如下所示:
class Tuna:public Fish
{
public:
void Swim() const
{
cout << "Tuna swims!" << endl;
}
};
在这种情况下,函数 Tuna::Swim()实际上并不会覆盖 Finish::Swim(),这是因为 Tuna::Swim()包含const,导致它们的特征标不同。然而,这些代码能够通过编译,导致程序员误以为他在 Tuna 类中成功地覆盖了函数 Swim()。
从 C++11 起,程序员可使用限定符 override 来核实被覆盖的函数在基类中是否被声明为虚的:
class Tuna:public Fish
{
public:
void Swim() const override // Error: no virtual fn with this sig in Fish
{
cout << "Tuna swims!" << endl;
}
};
换而言之,override 提供了一种强大的途径,让程序员能够明确地表达对基类的虚函数进行覆盖的意图,进而让编译器做如下检查:
-
基类函数是否是虚函数?
-
基类中相应虚函数的特征标是否与派生类中被声明为 override 的函数完全相同?
11.3 使用 final 来禁止覆盖函数
C++11 引入了限定符 final,被声明为 final 的类不能用作基类。同样,对于被声明为 final 的虚函数,不能在派生类中进行覆盖。
因此,要在 Tuna 类中禁止进一步定制虚函数 Swim(),可像下面这样做:
class Tuna:public Fish
{
public:
// override Fish::Swim and make this final
void Swim() override final
{
cout << "Tuna swims!" << endl;
}
};
可继承这个版本的 Tuna 类,但不能进一步覆盖函数 Swim():
{
public:
void Swim() //报错 因为Tuna::Swim()不允许北覆盖
{
}
};
【注意】
我们声明 BluefinTuna 时,也使用了关键字 final,这禁止将 BluefinTuna 类用作基类,因
此下面的代码将导致错误:
class FailedDerivation:public BluefinTuna
{
};
11.4 可将拷贝构造函数声明为虚函数吗
根本不可能实现虚拷贝构造函数,因为在基类方法声明中使用关键字 virtual 时,表示它将被派生类的实现覆盖,这种多态行为是在运行阶段实现的。而构造函数只能创建固定类型的对象,不具备多态性,因此 C++不允许使用虚复制构造函数。
虽然如此,但存在一种不错的解决方案,就是定义自己的克隆函数来实现上述目的:
class Fish
{
public:
virtual Fish* Clone() const = 0; // pure virtual function
};
class Tuna:public Fish
{
// ... other members
public:
Tuna * Clone() const // virtual clone function
{
return new Tuna(*this); // return new Tuna that is a copy of this
}
};
虚函数 Clone 模拟了虚复制构造函数,但需要显式地调用,
11.5 总结
-
覆盖时,基类要将函数声明为虚函数,派生类可以使用关键字 override来检测基类是否将函数声明为虚函数。
-
别忘了给基类提供一个虚析构函数
-
纯虚函数导致类变成抽象基类,且在派生类中必须提供纯虚函数的实现。
-
别忘了,在菱形继承层次结构中,虚继承旨在确保只有一个基类实例。
第 12 章 运算符类型与运算符重载
12.1 单目运算符
运算符 | 名称 | 运算符 | 名称 |
---|---|---|---|
++ | 递增 | & | 取址 |
- - | 递减 | ~ | 求反 |
* | 解除引用 | + | 正 |
-> | 成员选择 | − | 负 |
! | 逻辑非 | 转换运算符 | 转换为其他类型 |
12.1.1 单目递增与单目递减运算符
单目运算符没有参数,因为它们使用的唯一参数是当前类实例(*this)。
#include <iostream>
class Date{
private:
int date,month,year;
public:
Date(int myDate, int myMonth, int myYear)
: date(myDate),month(myMonth),year(myYear){}
Date& operator++(){
date ++ ;
return *this;
}
//后缀 使用占位符标识
Date& operator++(int){
date ++ ;
return *this;
}
void show(){
std::cout << year << "-" << month << "-" << date << std::endl;
}
};
int main(){
Date myDate(23,11,2000);
myDate++;
++myDate;
myDate.show();
return 0;
}
输出:2000-11-25
12.1.2 转换运算符
cout 能够很好地显示 const char *,要让 cout 能够显示 Date 对象,只需添加一个返回 const char*的运算符
#include <iostream>
#include <sstream>
using namespace std;
class Date{
private:
int date,month,year;
public:
Date(int myDate, int myMonth, int myYear)
: date(myDate),month(myMonth),year(myYear){}
Date& operator++(){
date ++ ;
return *this;
}
//后缀 使用占位符标识
Date& operator++(int){
date ++ ;
return *this;
}
operator const char *(){
ostringstream oss;
oss << year << "-" << month << "-" << date << endl;
return oss.str().c_str();
}
};
int main(){
Date myDate(23,11,2000);
myDate++;
++myDate;
cout << myDate;
return 0;
}
operator const char *()实现了Date 转换为 const char*的运算符。
现在,可在 cout 语句中直接使用 Date 对象,因为 cout 能够理解 const char*。编译器自动将合适运算符(这里只有一个)的返回值提供给 cout,从而在屏幕上显示日期。
12.2 双目运算符
12.2.1 双目运算符的类型
运算符 | 名称 | 运算符 | 名称 |
---|---|---|---|
, | 逗号 | < | 小于 |
!= | 不等于 | << | 左移 |
% | 求模 | <<= | 左移并赋值 |
%= | 求模并赋值 | <= | 小于或等于 |
& | 按位与 | = | 赋值、复制赋值和移动赋值 |
&& | 逻辑与 | == | 等于 |
&= | 按位与并赋值 | > | 大于 |
* | 乘 | >= | 大于或等于 |
*= | 乘并赋值 | >> | 右移 |
+ | 加 | >>= | 右移并赋值 |
+= | 加并赋值 | ^ | 异或 |
− | 减 | ^= | 异或并赋值 |
−= | 减并赋值 | | | 按位或 |
->* | 指向成员的指针 | |= | 按位或并赋值 |
/ | 除 | || | 逻辑或 |
/= | 除并赋值 | [] | 下标运算符 |
12.2.2 双目加法与双目减法运算符
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
public:
int m_A;
int m_B;
};
//全局函数实现 + 号运算符重载
//Person operator+(const Person& p1, const Person& p2) {
// Person temp(0, 0);
// temp.m_A = p1.m_A + p2.m_A;
// temp.m_B = p1.m_B + p2.m_B;
// return temp;
//}
//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
void test() {
Person p1(10, 10);
Person p2(20, 20);
//成员函数方式
Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
Person p4 = p3 + 10; //相当于 operator+(p3,10)
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
int main() {
test();
system("pause");
return 0;
}
12.2.3 关系运算符重载
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
};
bool operator==(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}
bool operator!=(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return false;
}
else
{
return true;
}
}
string m_Name;
int m_Age;
};
void test01()
{
//int a = 0;
//int b = 0;
Person a("孙悟空", 18);
Person b("孙悟空", 18);
if (a == b)
{
cout << "a和b相等" << endl;
}
else
{
cout << "a和b不相等" << endl;
}
if (a != b)
{
cout << "a和b不相等" << endl;
}
else
{
cout << "a和b相等" << endl;
}
}
int main() {
test01();
system("pause");
return 0;
}
12.2.4 赋值运算符重载
c++编译器至少给一个类添加4个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=, 对属性进行值拷贝
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}
//重载赋值运算符
Person& operator=(Person &p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
//编译器提供的代码是浅拷贝
//m_Age = p.m_Age;
//提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);
//返回自身
return *this;
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
//年龄的指针
int *m_Age;
};
void test01()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; //赋值操作
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
}
int main() {
test01();
//int a = 10;
//int b = 20;
//int c = 30;
//c = b = a;
//cout << "a = " << a << endl;
//cout << "b = " << b << endl;
//cout << "c = " << c << endl;
system("pause");
return 0;
}
12.3 函数调用运算符重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
#include "hao.h"
class HaoPrint{
public:
void operator()(string str){
cout << str << endl;
}
};
class HaoAdd{
public:
int operator()(int a, int b){
return a+b;
}
};
int main(){
HaoPrint print;
print("HAOhaoHao");
HaoAdd add;
int c = add(12,12);
cout << c << endl;
return 0;
}
12.4 用于高性能编程的移动构造函数和移动赋值运算符
移动构造函数的声明语法如下:
class Sample
{
private:
Type* ptrResource;
public:
Sample(Sample&& moveSource) // Move constructor, note &&
{
ptrResource = moveSource.ptrResource; // take ownership, start move
moveSource.ptrResource = NULL;
}
Sample& operator= (Sample&& moveSource)//move assignment operator, note &&
{
if(this != &moveSource)
{
delete [] ptrResource; // free own resource
ptrResource = moveSource.ptrResource; // take ownership, start move
moveSource.ptrResource = NULL; // free move source of ownership
}
}
Sample(); // default constructor
Sample(const Sample& copySource); // copy constructor
Sample& operator= (const Sample& copySource); // copy assignment
};
从上述代码可知,相比于常规赋值构造函数和复制赋值运算符的声明,移动构造函数和移动赋值运算符的不同之处在于,输入参数的类型为 Sample&&。另外,由于输入参数是要移动的源对象,因此不能使用 const 进行限定,因为它将被修改。返回类型没有变,因为它们分别是构造函数和赋值运算符的重载版本。
在需要创建临时右值时,遵循 C++的编译器将使用移动构造函数(而不是复制构造函数)和移动赋值运算符(而不是复制赋值运算符)。移动构造函数和移动赋值运算符的实现中,只是将资源从源移到目的地,而没有进行复制。
12.5 用户定义的字面量
C++增大了对字面量的支持力度,让您能够自定义字面量。例如,编写热力学方面的科学应用程序时,对于所有的温度,您都可能想以卡尔文为单位来存储和操作它们。为此,您可使用类似于下面的语法来声明所有的温度:
Temperature k1 = 32.15_F;
Temperature k2 = 0.0_C;
要自定义字面量,可像下面这样定义 operator “”:
ReturnType operator "" YourLiteral(ValueType value)
{
// conversion code here
}
【注意】
参数 ValueType 只能是下面几个之一,具体使用哪个取决于用户定义字面量的性质。
unsigned long long int:用于定义整型字面量。
long double:用于定义浮点字面量。
char、wchar_t、char16_t 和 char32_t:用于定义字符字面量。
const char*:用于定义原始字符串字面量。
const char和 size_t:用于定义字符串字面量。
const wchar_t和 size_t:用于定义字符串字面量。
const char16_t和 size_t:用于定义字符串字面量。
const char32_t和 size_t:用于定义字符串字面量。
0: #include <iostream>
1: using namespace std;
2:
3: struct Temperature
4: {
5: double Kelvin;
6: Temperature(long double kelvin) : Kelvin(kelvin) {}
7: };
8:
9: Temperature operator"" _C(long double celcius)
10: {
11: return Temperature(celcius + 273);
12: }
13:
14: Temperature operator "" _F(long double fahrenheit)
15: {
16: return Temperature((fahrenheit + 459.67) * 5 / 9);
17: }
18:
19: int main()
20: {
21: Temperature k1 = 31.73_F;
22: Temperature k2 = 0.0_C;
23:
24: cout << "k1 is " << k1.Kelvin << " Kelvin" << endl;
25: cout << "k2 is " << k2.Kelvin << " Kelvin" << endl;
26:
27: return 0;
28: }
12.6 不能重载的运算符
运算符 | 名称 | 运算符 | 名称 |
---|---|---|---|
. | 成员选择 | ?: | 条件三目运算符 |
.* | 指针成员选择 | sizeof | 获取对象/类类型的大小 |
:: | 作用域解析 |
第 13 章 类型转换运算符
13.1 C++类型转换运算符
4 个 C++类型转换运算符如下:
- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
这 4 个类型转换运算符的使用语法相同:
destination_type result = cast_operator<destination_type> (object_to_cast);
13.2 static_cast
static_cast 用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换—这种转换原本将自动或隐式地进行。用于指针时,static_cast 实现了基本的编译阶段检查,确保指针被转换为相关类型。
这改进了 C 风格类型转换,在 C 语言中,可将指向一个对象的指针转换为完全不相关
的类型,而编译器不会报错。
使用 static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型
//将 Derived*转换为 Base*被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换:
Derived objDerived;
Base* objBase = &objDerived; // ok!
//将 Base*转换为 Derived*被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换:
Derived objDerived;
Base* objBase = &objDerived; // Upcast -> ok!
Derived* objDer = objBase; // Error: Downcast needs explicit cast
然而,static_cast 只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast 编写如下代码,而编译器不会报错:
Base* objBase = new Base();
Derived* objDer = static_cast<Derived*>(objBase); // Still no errors!
其中 objDer 实际上指向一个不完整的 Derived 对象,因为它指向的对象实际上是 Base()类型。由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 objDer ->DerivedFunction()能够通过编译,但在运行阶段可能导致意外结果。
除用于向上转换和向下转换外,static_cast 还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读者的注意:
double Pi = 3.14159265;
int num = static_cast<int>(Pi); // Making an otherwise implicit cast, explicit
在上述代码中,使用 num = Pi 将获得同样的效果,但使用 static_cast 可让代码阅读者注意到这里使用了类型转换,并指出(对知道 static_cast 的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。
对于使用关键字 explicit 声明的转换运算符和构造函数,要使用它们,也必须通过 static_cast。有关如何使用关键字 explicit 来避免隐式转换,已经在第 9 讨论过。
13.3 dynamic_cast
与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。
使用 dynamic_cast 运算符的典型语法如下:
destination_type* Dest = dynamic_cast<class_type*>(Source);
if(Dest) // Check for success of the casting operation
Dest->CallFunc ();
例如:
Base* objBase = new Derived();
// Perform a downcast
Derived* objDer = dynamic_cast<Derived*>(objBase);
if(objDer) // Check for success of the cast
objDer->CallDerivedFunction ();
在上述示例代码中,目标对象的类型显然是 Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将 Derived传递给接受 Base参数的函数时。该函数可使用 dynamic_cast 判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用 dynamic_cast 在运行阶段判断类型,并在安全时使用转换后的指针。
程序清单 13.1 使用了一个您熟悉的继承层次结构—Tuna 和 Carp 类从基类 Fish 派生而来,其中的函数 DetectFishtype( )动态地检查 Fish 指针指向的对象是否是 Tuna 或 Carp。
0: #include <iostream>
1: using namespace std;
2:
3: class Fish
4: {
5: public:
6: virtual void Swim()
7: {
8: cout << "Fish swims in water" << endl;
9: }
10:
11: // 基类的析构函数一定是虚函数
12: virtual ~Fish() {}
13: };
14:
15: class Tuna: public Fish
16: {
17: public:
18: void Swim()
19: {
20: cout << "Tuna swims real fast in the sea" << endl;
21: }
22:
23: void BecomeDinner()
24: {
25: cout << "Tuna became dinner in Sushi" << endl;
26: }
27: };
28:
29: class Carp: public Fish
30: {
31: public:
32: void Swim()
33: {
34: cout << "Carp swims real slow in the lake" << endl;
35: }
36:
37: void Talk()
38: {
39: cout << "Carp talked Carp!" << endl;
40: }
41: };
42:
43: void DetectFishType(Fish* objFish)
44: {
45: Tuna* objTuna = dynamic_cast <Tuna*>(objFish);
46: if (objTuna) // check success of cast
47: {
48: cout << "Detected Tuna. Making Tuna dinner: " << endl;
49: objTuna->BecomeDinner();
50: }
51:
52: Carp* objCarp = dynamic_cast <Carp*>(objFish);
53: if(objCarp)
54: {
55: cout << "Detected Carp. Making carp talk: " << endl;
56: objCarp->Talk();
57: }
58:
59: cout << "Verifying type using virtual Fish::Swim: " << endl;
60: objFish->Swim(); // calling virtual function Swim
61: }
62:
63: int main()
64: {
65: Carp myLunch;
66: Tuna myDinner;
67:
68: DetectFishType(&myDinner);
69: cout << endl;
70: DetectFishType(&myLunch);
71:
72: return 0;
73: }
输出:
Detected Tuna. Making Tuna dinner:
Tuna became dinner in Sushi
Verifying type using virtual Fish::Swim:
Tuna swims real fast in the sea
Detected Carp. Making carp talk:
Carp talked Carp!
Verifying type using virtual Fish::Swim:
Carp swims real slow in the lake
务必检查 dynamic_cast 的返回值,看它是否有效。如果返回值为 NULL,说明转换失败
13.4 reinterpret_cast
reinterpret_cast 是 C++中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用强制重新解释类型。
这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为 API(应用程序编程接口)能够接的简单类型(例如,有些 OS 级 API 要求提供的数据为 BYTE 数组,即 unsigned char*):
SomeClass* object = new SomeClass();
// Need to send the object as a byte-stream...
unsigned char* bytesFoAPI = reinterpret_cast<unsigned char*>(object);
上述代码使用的类型转换并没有改变源对象的二进制表示,但让编译器允许程序员访问SomeClass对象包含的各个字节。由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用 reinterpret_cast 来执行不安全(不可移植)的转换。
【警告】
应尽量避免在应用程序中使用 reinterpret_cast,因为它让编译器将类型 X 视为不相关的 类型 Y,这看起来不像是优秀的设计或实现。
13.5 const_cast
const_cast 让程序员能够关闭对象的访问修饰符 const。
class SomeClass
{
public:
// ...
void DisplayMembers(); //problem - display function isn't const
};
在下面的函数中,以 const 引用的方式传递 object 显然是正确的。毕竟,显示函数应该是只读的,不应调用非 const 成员函数,即不应调用能够修改对象状态的函数。然而,成员函数DisplayMembers()本应为 const的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对 DisplayMembers()进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast将是您的救星。
void DisplayAllData (const SomeClass& object)
{
object.DisplayMembers (); // 编译错误
// 原因: 调用了一个 non-const 成员函数
}
在这种情况下,调用 DisplayMembers()的语法如下:
void DisplayAllData (const SomeClass& object)
{
SomeClass& refData = const_cast<SomeClass&>(object);
refData.DisplayMembers(); // Allowed!
}
除非万不得已,否则不要使用 const_cast 来调用非 const 函数。一般而言,使用 const_cast 来修改const 对象可能导致不可预料的行为。
13.6 C++类型转换运算符存在的问题
并非所有人都喜欢使用 C++类型转换,即使那些 C++拥趸也如此。其理由很多,从语法繁琐而不够直观到显得多余。
来比较一下下面的代码:
double Pi = 3.14159265;
// C++ style cast: static_cast
int num = static_cast <int>(Pi); // result: Num is 3
// C-style cast
int num2 = (int)Pi; // result: num2 is 3
// leave casting to the compiler
int num3 = Pi; // result: num3 is 3. No errors!
在这 3 种方法中,程序员得到的结果都相同。在实际情况下,第 2 种方法可能最常见,其次是第 3种,但几乎没有人使用第 1 种方法。无论采用哪种方法,编译器都足够聪明,能够正确地进行类型转换。这让人觉得类型转换运算符将降低代码的可读性。
Bjarne Stroustrup 准确地描述了这种境况:“由于 static_cast 如此拙劣且难以输入,因此您在使用它之前很可能会三思。这很不错,因为类型转换在现代 C++中是最容易避免的。”
再来看其他运算符。在不能使用 static_cast 时,可使用 reinterpret_cast 强制进行转换;同样,可以使用 const_cast 修改访问修饰符 const。因此,在现代 C++中,除 dynamic_cast 外的类型转换都是可以避免的。
仅当需要满足遗留应用程序的需求时,才需要使用其他类型转换运算符。在这种情况下,程序员通常倾向于使用 C 风格类型转换而不是 C++类型转换运算符。重要的是,应尽量避免使用类型转换;而一旦使用类型转换,务必要知道幕后发生的情况。
【注意】
- 请牢记,将 Derived转换为 Base被称为向上转换;这种转换是安全的。
- 请牢记,将 Base转换为 Derived被称为向下转换;除非使用 dynamic_cast 并核实转换成功,否则这种转换不安全。
- 请牢记,创建继承层次结构时,应尽量将函数声明为虚函数。这样通过基类指针调用这些函数时,如果该指针指向的是派生类对象,将调用相应类的函数版本。
- 使用 dynamic_cast 时,别忘了对转换得到的指针进行检查,看其是否有效。
第 14 章 宏和模板简介
14.1 预处理器
预处理器在编译器之前运行,换句话说,预处理器根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以#打头。
一是使用#define 定义常量,二是使用#define定义宏函数。这两个编译指令都告诉编译器,将每个宏实例其定义的值。
14.2 使用#define 定义常量
使用#define 定义常量的语法非常简单:#define identifier value
例如,要定义将被替换为 25 的常量 ARRAY_LENGTH,可使用如下代码:
#define ARRAY_LENGTH 25
这样,每当预处理器遇到标识符 ARRAY_LENGTH 时,都会将其替换为 25。
int numbers [ARRAY_LENGTH] = {0};
double radiuses [ARRAY_LENGTH] = {0.0};
std::string names [ARRAY_LENGTH];
对于上述三行代码,预处理器运行完毕后,编译器看到的代码如下:
int numbers [25] = {0}; // an array of 25 integers
double radiuses [25] = {0.0}; // an array of 25 doubles
std::string names [25]; // an array of 25 std::strings
替换将在所有代码中进行,包括下面这样的 for 循环:
for(int index = 0; index < ARRAY_LENGTH; ++index)
numbers[index] = index;
编译器看到的上述循环如下:
for(int index = 0; index < 25; ++index)
numbers[index] = index;
【注意】
预处理器进行死板的文本替换,而不检查替换是否正确,但编译器总是会检查。
另外,对于使用宏定义的常量 PI(#define PI 3.1416),您没有太大的控制权:其类型是 double 还是 float?答案是都不是。在预处理器看来,PI 就是 3.1416,根本不知道其数据类型。定义常量时,更好的选择是使用关键字 const 和数据类型
14.3 头文件使用宏避免多次包含
C++程序员通常在.H 文件(头文件)中声明类和函数,并在.CPP 文件中定义函数,因此需要在.CPP文件中使用预处理器编译指令#include
如果在头文件 class1.h 中声明了一个类,而这个类将 class2.h 中声明的类作为其成员,则需要在 class1.h 中包含 class2.h。如果设计非常复杂,即第二个类需要第一个类,则在 class2.h 中也需要包含 class1.h!
然而,在预处理器看来,两个头文件彼此包含对方会导致递归问题。为了避免这种问题,可结合使用宏以及预处理器编译指令#ifndef
和#endif
。
包含<header2.h>的 head1.h 类似于下面这样:
#ifndef HEADER1_H_ //多重包含保护:
#define HEADER1_H_ // 预处理器将读取 这一行开始至到后面#endif的内容 一次
#include <header2.h>
class Class1
{
// class members
};
#endif // end of header1.h
header2.h 与此类似,但宏定义不同,且包含的是<header1.h>:
#ifndef HEADER2_H_//多重包含保护:
#define HEADER2_H_//preprocessor will read this and following lines once
#include <header1.h>
class Class2
{
// class members
};
#endif // end of header2.h
【注意】
#ifndef 可读作 if-not-defined。这是一个条件处理命令,让预处理器仅在标识符未定义时才继续。
#endif 告诉预处理器,条件处理指令到此结束。
因此,预处理器首次处理 header1.h 并遇到#ifndef 后,发现宏 HEADER1_H_还未定义,因此继续处理。#ifndef 后面的第一行定义了宏 HEADER1_H_,确保预处理器再次处理该文件时,将在遇到包含#ifndef 的第一行时结束,因为其中的条件为 false。header2.h 与此类似。在 C++编程领域,这种简单的机制无疑是最常用的宏功能之一。
14.4 使用#define 编写宏函数
预处理器对宏指定的文本进行简单替换,因此也可以使用宏来编写简单的函数。
宏函数通常用于执行非常简单的计算。相比于常规函数调用,宏函数的优点在于,它们将在编译前就地展开,因此在有些情况下有助于改善代码的性能。
#define SQUARE(x) ((x) * (x))
#define PI 3.1416
#define AREA_CIRCLE(r) (PI*(r)*(r))
14.4.1 注意要使用括号
编写宏时使用了大量括号,而在函数中,同样的公式看起来完全不同。这是为什么呢?原因在于宏的计算方式—预处理器支持的文本替换机制。
请看下面的宏,它省略了大部分括号:
#define AREA_CIRCLE(r) (PI*r*r)
如果使用类似于下面的语句调用这个宏,结果将如何呢?
cout << AREA_CIRCLE (4+6);
展开后,编译器看到的语句如下:
cout << (PI*4+6*4+6); // not the same as PI*10*10
根据运算符优先级,将先执行乘法运算,再执行加法运算,因此编译器将这样计算面积:
cout << (PI*4+24+6); // 42.5664 (which is incorrect)
在省略了括号的情况下,简单的文本替换破坏了编程逻辑!使用括号有助于避免这种问题
#define AREA_CIRCLE(r) (PI*(r)*(r))
cout << AREA_CIRCLE (4+6);
经过替换后,编译器看到的表达式如下:
cout << (PI*(4+6)*(4+6)); // PI*10*10, as expected
14.4.2 assert 宏验证表达式
编写程序后,立即单步执行以测试每条代码路径很不错,但对大型应用程序来说可能不现实。比较现实的做法是,插入检查语句,对表达式或变量的值进行验证。
assert 宏让您能够完成这项任务。要使用 assert 宏,需要包含<assert.h>,其语法如下:
#include <iostream>
#include <assert.h>
using namespace std;
int main(){
string name = "";
assert(name != "");
return 0;
}
输出:
Assertion failed: name != "", file C:\。。。\11_assert.cpp, line 7
14.4.3 使用宏函数的优点和缺点
宏函数可用于不同的变量类型。再来看一下程序清单 14.2 中的下述代码行:
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
可将宏函数 MIN 用于整型:cout << MIN(25, 101) << endl;
也可将其用于双精度数:cout << MIN(0.1, 0.2) << endl;
如果 MIN( )为常规函数,必须编写两个不同的版本:MIN_INT( )和 MIN_DOUBLE( ),前者接受int 参数并返回一个 int 值,而后者接受 double 参数并返回一个 double 值。使用宏函数减少了代码行,这是一种细微的优势,诱使有些程序员使用宏来定义简单函数。宏函数将在编译前就地展开,因此简单宏的性能优于常规函数调用。这是因为函数调用要求创建调用栈、传递参数等,这些开销占用的 CPU时间通常比 函数执行的计算还多。
然而,宏不支持任何形式的类型安全,这是一个严重的缺点。另外,复杂的宏调试起来也不容易。如果需要编写独立于类型的泛型函数,又要确保类型安全,可使用模板函数,而不是宏函数。
【总结】
- 尽可能不要自己编写宏函数。
- 尽可能使用 const 变量,而不是宏常量。
- 请牢记,宏并非类型安全的,预处理器不执行类型检查。
- 在宏函数的定义中,别忘了使用括号将每个变量括起。
- 为了在头文件中避免多次包含,别忘了使用#ifndef、#define 和#endif。
- 别忘了在代码中大量使用 assert( ),它们在发行版本中将被禁用,但对提高代码的质量很有帮助。
14.5 模板
14.5.1 声明语法
模板声明以关键字 template
打头,接下来是类型参数列表。关键字 template 标志着模板声明的开始,该参数列表包含关键字typename
,定义了模板参数 。
模板函数
template <typename T1, typename T2 = T1>
bool TemplateFunction(const T1& param1, const T2& param2);
模板类
template <typename T1, typename T2 = T1>
class MyTemplate
{
private:
T1 member1;
T2 member2;
public:
T1 GetObj1() {return member1; }
// … other members
};
14.6 模板函数
14.6.1 模板函数
#include "hao.h"
template<typename T>
T myMax(T num1, T num2)
{
return num1 > num2 ? num1 : num2;
}
int main(){
//可显式地指定类型
cout << myMax<int>(11,12) << endl;
//也可以不指定
cout << myMax(12.1,13.2) << endl;
return 0;
}
注意事项:
自动类型推导,必须推导出一致的数据类型T,才可以使用
模板必须要确定出T的数据类型,才可以使用
14.6.2 普通函数与函数模板的区别
普通函数与函数模板区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
示例:
//普通函数
int myAdd01(int a, int b)
{
return a + b;
}
//函数模板
template<class T>
T myAdd02(T a, T b)
{
return a + b;
}
//使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
cout << myAdd01(a, c) << endl; //正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99
//myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换
myAdd02<int>(a, c); //正确,如果用显示指定类型,可以发生隐式类型转换
}
int main() {
test01();
system("pause");
return 0;
}
14.6.3 普通函数与函数模板的调用规则
调用规则如下:
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
//普通函数与函数模板调用规则
void myPrint(int a, int b)
{
cout << "调用的普通函数" << endl;
}
template<typename T>
void myPrint(T a, T b)
{
cout << "调用的模板" << endl;
}
template<typename T>
void myPrint(T a, T b, T c)
{
cout << "调用重载的模板" << endl;
}
void test01()
{
//1、如果函数模板和普通函数都可以实现,优先调用普通函数
// 注意 如果告诉编译器 普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
int a = 10;
int b = 20;
myPrint(a, b); //调用普通函数
//2、可以通过空模板参数列表来强制调用函数模板
myPrint<>(a, b); //调用函数模板
//3、函数模板也可以发生重载
int c = 30;
myPrint(a, b, c); //调用重载的函数模板
//4、 如果函数模板可以产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2); //调用函数模板
}
int main() {
test01();
system("pause");
return 0;
}
【总结】
既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
14.6.4 模板的实例化和具体化
模板类是创建类的蓝图,因此在编译器看来,仅当模板类以某种方式被使用后,其代码才存在。换言之,对于您定义了但未使用的模板类,编译器将忽略它。
对模板来说,实例化指的是使用一个或多个模板参数来创建特定的类型。
然而,在有些情况下,使用特定的类型实例化模板时,需要显式地指定不同的行为。这就是具体化模板,即为特定的类型指定行为。
例如:
template<class T>
void f(T a, T b)
{
if(a > b) { ... }
}
在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行。
因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
}
string m_Name;
int m_Age;
};
//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
if (a == b)
{
return true;
}
else
{
return false;
}
}
//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person &p1, Person &p2)
{
if ( p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
{
return true;
}
else
{
return false;
}
}
void test01()
{
int a = 10;
int b = 20;
//内置数据类型可以直接使用通用的函数模板
bool ret = myCompare(a, b);
if (ret)
cout << "a == b " << endl;
else
cout << "a != b " << endl;
}
void test02()
{
Person p1("Tom", 10);
Person p2("Tom", 10);
//自定义数据类型,不会调用普通的函数模板
//可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
bool ret = myCompare(p1, p2);
if (ret)
cout << "p1 == p2 " << endl;
else
cout << "p1 != p2 " << endl;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
14.7 模板类
14.7.1 模板类
//包含默认参数
template<typename nameType, typename ageType = int>
class Person{
private:
nameType name;
ageType age;
public:
Person(nameType name, ageType age):name(name), age(age){}
void showInfo(){
cout << "name:" << name << "\tage:" << age << endl;
}
};
int main(){
Person<string,int> person("chen",24);
Person person1("hao",24);
person.showInfo();
person1.showInfo();
return 0;
}
输出:
name:chen age:24
name:hao age:24
14.7.2 类模板中成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
14.7.3 类模板对象做函数参数
一共有三种传入方式:
- 指定传入的类型 — 直接显示对象的数据类型
- 参数模板化 — 将对象中的参数变为模板进行传递
- 整个类模板化 — 将这个对象类型 模板化进行传递
#include <string>
//类模板
template<class NameType, class AgeType = int>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
//1、指定传入的类型
void printPerson1(Person<string, int> &p)
{
p.showPerson();
}
void test01()
{
Person <string, int >p("孙悟空", 100);
printPerson1(p);
}
//2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{
p.showPerson();
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{
Person <string, int >p("猪八戒", 90);
printPerson2(p);
}
//3、整个类模板化
template<class T>
void printPerson3(T & p)
{
cout << "T的类型为: " << typeid(T).name() << endl;
p.showPerson();
}
void test03()
{
Person <string, int >p("唐僧", 30);
printPerson3(p);
}
int main() {
test01();
test02();
test03();
system("pause");
return 0;
}
14.7.4 类模板与继承
当类模板碰到继承时,需要注意一下几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
template<class T>
class Base
{
T m;
};
//class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son :public Base<int> //必须指定一个类型
{
};
void test01()
{
Son c;
}
//类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2>
{
public:
Son2()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
};
void test02()
{
Son2<int, char> child1;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
14.7.5 类模板成员函数类外实现
#include <string>
//类模板中成员函数类外实现
template<class T1, class T2>
class Person {
public:
//成员函数类内声明
Person(T1 name, T2 age);
void showPerson();
public:
T1 m_Name;
T2 m_Age;
};
//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}
void test01()
{
Person<string, int> p("Tom", 20);
p.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
14.7.6 类模板分文件编写
问题:
- 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决:
- 解决方式1:直接包含.cpp源文件
- 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
示例:
person.hpp中代码:
#pragma once
#include <iostream>
using namespace std;
#include <string>
template<class T1, class T2>
class Person {
public:
Person(T1 name, T2 age);
void showPerson();
public:
T1 m_Name;
T2 m_Age;
};
//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}
类模板分文件编写.cpp中代码
#include<iostream>
using namespace std;
//#include "person.h"
#include "person.cpp" //解决方式1,包含cpp源文件
//解决方式2,将声明和实现写到一起,文件后缀名改为.hpp
#include "person.hpp"
void test01()
{
Person<string, int> p("Tom", 10);
p.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
14.7.7 类模板与友元
全局函数类内实现 - 直接在类内声明友元即可
全局函数类外实现 - 需要提前让编译器知道全局函数的存在
//2、全局函数配合友元 类外实现 - 先做函数模板声明,下方在做函数模板定义,在做友元
template<class T1, class T2> class Person;
//如果声明了函数模板,可以将实现写到后面,否则需要将实现体写到类的前面让编译器提前看到
//template<class T1, class T2> void printPerson2(Person<T1, T2> & p);
template<class T1, class T2>
void printPerson2(Person<T1, T2> & p)
{
cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}
template<class T1, class T2>
class Person
{
//1、全局函数配合友元 类内实现
friend void printPerson(Person<T1, T2> & p)
{
cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}
//全局函数配合友元 类外实现
friend void printPerson2<>(Person<T1, T2> & p);
public:
Person(T1 name, T2 age)
{
this->m_Name = name;
this->m_Age = age;
}
private:
T1 m_Name;
T2 m_Age;
};
//1、全局函数在类内实现
void test01()
{
Person <string, int >p("Tom", 20);
printPerson(p);
}
//2、全局函数在类外实现
void test02()
{
Person <string, int >p("Jerry", 30);
printPerson2(p);
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
总结:建议全局函数做类内实现,用法简单,而且编译器可以直接识别
模板类和静态成员
如果将类成员声明为静态的,该成员将由类的所有实例共享。模板类的静态成员与此类似,由特定具体化的所有实例共享。也就是说,如果模板类包含静态成员,该成员将在针对 int 具体化的所有实例之间共享;同样,它还将在针对 double 具体化的所有实例之间共享,且与针对 int 具体化的实例无关。换句话说,可以认为编译器创建了两个版本的 x:x_int 用于针对 int 具体化的实例,而 x_double 针对 double 具体化的实例。
#include "hao.h"
template<typename T>
class Person{
public:
T age;
static int num;
};
//不可或缺,它初始化模板类的静态成员
template<typename T> int Person<T>::num;
int main(){
Person<int> person;
person.num = 30;
Person<double> person1;
person1.num = 40;
cout << "person.num =" << person.num << endl;
cout << "person1.num =" << person1.num << endl;
return 0;
}
输出:
person.num =30
person1.num =40
【注意】
对于模板类的静态成员,通用的初始化语法如下:
template StaticType ClassName::StaticVarName;
14.7.8 参数数量可变的模板
如果需要编写一个函数,能够计算任意数量值的和,就需要使用参数数量可变
的模板。参数数量可变的模板是 2014 年发布的 C++14 新增的,
#include "hao.h"
//不可缺少
template <typename ResType, typename ValType>
void getSum(ResType& result, ValType& val)
{
result = result + val;
}
//使用了省略号...,模板中的省略号告诉编译器,默认类或模板函数可接受任意数量的模板参数,
template<typename ResType, typename FirstValType, typename... Rest>
void getSum(ResType& res, FirstValType val , Rest... valn){
res = res + val;
return getSum(res,valn ...);
}
int main(){
int res = 0;
getSum(res,1,2,3,4,5,6,7,8,9);
cout << "res = " << res << endl;
string str = "";
getSum(str,"hello " ,"world");
cout << "str = " << str << endl;
return 0;
}
static_assert 是 C++11 新增的一项功能,让您能够在不满足指定条件时禁止编译。这好像不可思议,但对模板类来说很有用。例如,您可能想禁止针对 int 实例化模板类,为此可使用 static_assert,它是一种编译阶段断言,可用于在开发环境(或控制台中)显示一条自定义消息:
static_assert(expression being validated, "当前式值为假,就输出此消息");
要禁止针对类型 int 实例化模板类,可使用 static_assert( ),并将 sizeof(T)与 sizeof(int)进行比较,如果它们相等,就显示一条错误消息:
static_assert(sizeof(T) != sizeof(int), "No int please!");
template <typename T>
class EverythingButInt
{
public:
EverythingButInt()
{
static_assert(sizeof(T) != sizeof(int), "No int please!");
}
};
int main()
{
EverythingButInt<int> test; // template instantiation with int.
return 0;
}
输出:
error: No int please!
第 15 章 标准模板库简介
简单地说,标准模板库(STL)是一组模板类和函数,向程序员提供了:
• 用于存储信息的容器;
• 用于访问容器存储的信息的迭代器;
• 用于操作容器内容的算法。
15.1 STL 容器
容器是用于存储数据的 STL 类,STL 提供了两种类型的容器类:
• 顺序容器;
• 关联容器。
另外,STL 还提供了被称为容器适配器(Container Adapter)的类,它们是顺序容器和关联容器的变种,包含的功能有限,用于满足特殊的需求。
15.1.1 顺序容器
顺序容器按顺序存储数据,如数组和列表。顺序容器具有插入速度快但查找操作相对较慢的特征。
STL 顺序容器如下所示。
- std::vector:操作与动态数组一样,在最后插入数据;可将 vector 视为书架,您可在一端添加和拿走图书。
- std::deque:与 std::vector 类似,但允许在开头插入或删除元素。
- std::list:操作与双向链表一样。可将它视为链条,对象被连接在一起,您可在任何位置添加或删除对象。
- std::forward_list:类似于 std::list,但是单向链表,只能沿一个方向遍历。
STL vector 类与数组类似,允许随机访问元素,即可使用下标运算符([])指定元素在 vector 中的位置(索引),从而直接访问或操作元素。
另外,STL vector 是动态数组,因此能够根据应用程序在运行阶
段的需求自动调整长度。为保留数组能够根据位置随机访问元素的特征,大多数 STL vector 实现都将所有元素存储在连续的存储单元中,因此需要调整长度的 vector 通常会降低应用程序的性能,这取决于它包含的对象类型。
可将 STL list 类视为普通链表的 STL 实现。虽然 list 中的元素不能像 STL vector 中的元素那样随机访问,但 list 可使用不连续的内存块组织元素,因此它不像 std::vector 那样需要给内部数组重新分配内存,进而导致性能问题。
15.1.2 关联容器
关联容器按指定的顺序存储数据,就像词典一样。这将降低插入数据的速度,但在查询方面有很大的优势。
STL 提供的关联容器如下所示。
- std::set:存储各不相同的值,在插入时进行排序;容器的复杂度为对数。
- std::unordered_set:存储各不相同的值,在插入时进行排序;容器的复杂度为常数。这种容器是 C++11 新增的。
- std::map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。
- std::unordered_map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。这种容器是C++11 新增的。
- std::multiset:与 set 类似,但允许存储多个值相同的项,即值不需要是唯一的。
- std::unordered_multiset:与 unordered_set 类似,但允许存储多个值相同的项,即值不需要是唯一的。这种容器是 C++11 新增的。
- std::multimap:与 map 类似,但不要求键是唯一的。
- std::unordered_multimap:与 unordered_map 类似,但不要求键是唯一的。这种容器是 C++11新增的。
15.1.3 容器适配器
容器适配器(Container Adapter)是顺序容器和关联容器的变种,其功能有限,用于满足特定的需求。主要的适配器类如下所示。
- std::stack:以 LIFO(后进先出)的方式存储元素,让您能够在栈顶插入(压入)和删除(弹出)元素。
- std::queue:以 FIFO(先进先出)的方式存储元素,让您能够删除最先插入的元素。
- std::priority_queue:以特定顺序存储元素,因为优先级最高的元素总是位于队列开头。
15.2 STL 迭代器
最简单的迭代器是指针。给定一个指向数组中的第一个元素的指针,可递增该指针使其指向下一个元素,还可直接对当前位置的元素进行操作。
STL 中的迭代器是模板类,从某种程度上说,它们是泛型指针。这些模板类让程序员能够对 STL容器进行操作。注意,操作也可以是以模板函数的方式提供的 STL 算法,迭代器是一座桥梁,让这些模板函数能够以一致而无缝的方式处理容器,而容器是模板类。
STL 提供的迭代器分两大类。
- 输入迭代器:通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代器确保只能以只读的方式访问对象。
- 输出迭代器:输出迭代器让程序员对集合执行写入操作。最严格的输出迭代器确保只能执行写入操作。
上述两种基本迭代器可进一步分为三类。
- 前向迭代器:这是输入迭代器和输出迭代器的一种细化,它允许输入与输出。前向迭代器可以是 const 的,只能读取它指向的对象;也可以改变对象,即可读写对象。前向迭代器通常用于单向链表。
- 双向迭代器:这是前向迭代器的一种细化,可对其执行递减操作,从而向后移动。双向迭代器通常用于双向链表。
- 随机访问迭代器:这是对双向迭代器的一种细化,可将其加减一个偏移量,还可将两个迭代器相减以得到集合中两个元素的相对距离。随机访问迭代器通常用于数组。
15.3 STL 算法
查找、排序和反转等都是标准的编程需求,不应让程序员重复实现这样的功能。因此 STL 以 STL算法的方式提供这些函数,通过结合使用这些函数和迭代器,程序员可对容器执行一些最常见的操作。
最常用的 STL 算法如下所示。
- std::find:在集合中查找值。
- std::find_if:根据用户指定的谓词在集合中查找值。
- std::reverse:反转集合中元素的排列顺序。
- std::remove_if:根据用户定义的谓词将元素从集合中删除。
- std::transform:使用用户定义的变换函数对容器中的元素进行变换。
这些算法都是 std 命名空间中的模板函数,要使用它们,必须包含标准头文件<algorithm>
。
15.4 各种容器的优缺点
容器 | 优点 | 缺点 |
---|---|---|
std::vector(顺序容器) | 在末尾插入数据时速度快(时间固定)可以像访问数组一样进行访问 | 调整大小时将影响性能;搜索时间与容器包含的元素个数成反比;只能在末尾插入数据 |
std::deque(顺序容器) | 具备 vector 的所有优点,还可在容器开头插入数据,插入时间也是固定的 | 有 vector 的所有缺点;与 vector 不同的是,根据规范,deque不需要支持 reserve()函数,该函数让程序员能够给 vector 预留内存空间,以免频繁地调整大小,从而提高性能 |
std::list(顺序容器) | 在 list 开头、中间或末尾插入数据,所需时间都是固定的将元素从 list 中删除所需的时间是固定的,而不管元素的位置如何;插入或删除元素后,指向 list 中其他元素的迭代器仍有效 | 不能像数组那样根据索引随机访问元素;搜索速度比 vector 慢,因为元素没有存储在连续的内存单元中;搜索时间与容器中的元素个数成反比 |
std::forward_list(顺序容器) | 单向链表类,只能沿一个方向遍历 | 只能使用 push_front( )在链表开头插入元素 |
std::set(关联容器) | 搜索时间不是容器中的元素个数(而与元素个数的对数)成反比,因此搜索速度通常比顺序容器快得多 | 元素的插入速度比顺序容器慢,因为在插入时对元素进行排序 |
std::unordered_set(关联容器) | 搜索、插入和删除的速度几乎不受容器包含的元素个数的影响 | 由于元素未被严格排序,因此不能依赖于元素在容器中的相对位置 |
std::multiset (关联容器) | 需要存储非唯一的值时,应使用这种容器 | 插入速度可能比顺序容器慢,因为在插入时对(键-值对)进行排序 |
std::unordered_multiset(关联容器) | 需要存储非唯一的值时,应使用这种容器,而不是unorder_set;性能与 unordered_set 类似,即搜索、插入和删除元素的时间是固定的,不受容器长度的影响 | 由于元素未被严格排序,因此不能依赖于元素在容器中的相对位置 |
std::map(关联容器) | 用于存储键-值对的容器,搜索时间与元素个数的对数成反比,因此搜索速度通常比顺序容器快得多 | 插入时进行排序,因此插入速度比顺序容器慢 |
std::unordered_map(关联容器) | 搜索、插入和删除元素的时间是固定的,不受容器长度的影响 | 元素未被严格排序,不适合用于顺序很重要的情形 |
std::multimap(关联容器) | 在需要存储键-值且要求键不唯一时,应选择这种容器,而不是 std::map | 插入时进行排序,因此插入速度比顺序容器慢 |
std::unordered_multimap(关联容器) | 在需要存储键-值且要求键不唯一时,应选择这种容器,而不是 multimap;搜索、插入和删除元素的时间是固定的,不受容器长度的影响 | 元素未被严格排序,在需要依赖于元素的相对顺序时,不能使用它 |
15.5 STL 字符串类
STL 提供了一个专门为操纵字符串而设计的模板类:std::basic_string,该模板类的两个常用具体化如下所示。
- std::string:基于 char 的 std::basic_string 具体化,用于操纵简单字符串。
- std::wstring:基于 wchar_t 的 std::basic_string 具体化,用于操纵宽字符串,通常用于存储支持各种语言中符号的 Unicode 字符。
第 16 章 STL string 类
STL 字符串类 std::string 和 std::wstring 分别模拟了普通字符串和
宽字符串,可提供如下帮助:
- 减少了程序员在创建和操作字符串方面需要做的工作;
- 在内部管理内存分配细节,从而提高了应用程序的稳定性;
- 提供了复制构造函数和赋值运算符,可确保成员字符串得以正确复制;
- 提供了帮助执行截短、查找和删除等操作的实用函数;
- 提供了用于比较的运算符;
- 让程序员能够将精力放在应用程序的主要需求而不是字符串操作细节上。
16.1 使用 STL string 类
16.1.1 实例化和复制
string 类提供了很多重载的构造函数。
#include <iostream>
int main(){
std::string str("hello world");
std::string str1(str);
//数只接受输入字符串的前 n 个字符
std::string str2 (str,5);
std::string str3(10,'a'); //10个a组成字符串
std::cout << str << std::endl
<< str1 << std::endl
<< str2<< std::endl
<< str3 << std::endl;
}
输出:
hello world
hello world
world
aaaaaaaaaa
16.1.2 访问 std::string 的字符内容
要访问 STL string 的字符内容,可使用迭代器,也可采用类似于数组的语法并使用下标运算符([])提供偏移量。要获得 string 对象的 C 风格表示,可使用成员函数 c_str ()。
#include <iostream>
int main(){
std::string str("hello");
for (int i = 0; i < str.length(); ++i) {
std::cout << str[i] << std::endl;
}
std::string::const_iterator citerator;
for (citerator = str.cbegin(); citerator != str.cend(); ++citerator) {
std::cout << *citerator << std::endl;
}
std::cout << str.c_str() << std::endl;
}
使用auto
//std::string::const_iterator citerator;
for (auto citerator = str.cbegin(); citerator != str.cend(); ++citerator) {
std::cout << *citerator << std::endl;
}
16.1.3 拼接字符串
要拼接字符串,可使用运算符+=,也可使用成员函数 append()。
string str1("hello");
string str2(" string");
str1 += str2;
cout << str1 << endl; // hello string
str1.append(str2);
cout << str1 << endl; //hello string string
16.1.4 在 string 中查找字符或子字符串
STL string 类提供了成员函数 find(),该函数有多个重载版本,可在给定 string 对象中查找字符或子字符串。
//如果找到则返回下标,否则返回-1
string str1("hello string");
int i = str1.find("hello",0);
int j = str1.find('g',4);
cout << i << endl; //0
cout << j << endl; //11
16.1.5 截短 STL string
STL string 类提供了 erase()函数,具有以下用途。
- 在给定偏移位置和字符数时删除指定数目的字符。
string sampleStr ("Hello String! Wake up to a beautiful day!");
sampleStr.erase (13, 28); // Hello String! 删除包含下标13
- 在给定指向字符的迭代器时删除该字符。
sampleStr.erase (iCharS); // iterator points to a specific character
- 在给定由两个迭代器指定的范围时删除该范围内的字符。
sampleStr.erase (sampleStr.begin (), sampleStr.end ()); // erase from begin to end
16.1.6 字符串反转
有时需要反转字符串的内容。假设要判断用户输入的字符串是否为回文,方法之一是将其反转,再与原来的字符串进行比较。反转 STL string 很容易,只需使用泛型算法 std::reverse()。需要包含头文件
string sampleStr ("Hello String! We will reverse you!");
reverse (sampleStr.begin (), sampleStr.end ());
16.1.7 字符串的大小写转换
要对字符串进行大小写转换,可使用算法 std::transform(),它对集合中的每个元素执行一个用户指定的函数。在这里,集合是 string 对象本身。
string str1("hello string");
transform(str1.begin(), str1.end(), str1.begin(),::toupper);
cout << str1 << endl; //HELLO STRING
transform(str1.begin(), str1.end(), str1.begin(),::tolower);
cout << str1 << endl; //hello string
16.2 基于模板的 STL string 实现
前面说过,std::string 类实际上是 STL 模板类 std::basic_string 的具体化。容器类 basic_string
的模板声明如下:
template<class _Elem,
class _Traits,
class _Ax>
class basic_string
在该模板定义中,最重要的参数是第一个:_Elem,它指定了 basic_string 对象将存储的数据类型。因此,std::string 使用_Elem=char 具体化模板 basic_string 的结果,而 wstring 使用_Elem= wchar 具体化模板 basic_string 的结果。
换句话说,STL string 类的定义如下:
typedef basic_string<char, char_traits<char>, allocator<char> >
string;
而 STL wstring 类的定义如下:
typedef basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t> >
string;
因此,前面介绍的所有 string 功能和函数实际上都是 basic_string 提供的,它们也适用于 STL wstring 类。
16.3 C++14 新增的 operator “”s
string str1 ("hello \0 world");
string str2 ("hello \0 world"s);
cout << str1.length() << endl; //6
cout << str2.length() << endl; //13
第 17 章 STL 动态数组类
17.1 std::vector 的特点
vector 是一个模板类,提供了动态数组的通用功能,具有如下特点:
- 在数组末尾添加元素所需的时间是固定的,即在末尾插入元素的所需时间不随数组大小而异,在末尾删除元素也如此;
- 在数组中间添加或删除元素所需的时间与该元素后面的元素个数成正比;
- 存储的元素数是动态的,而 vector 类负责管理内存。
17.2 典型的 vector 操作
17.2.1 实例化 vector
vector<int> vector1{1,2,3,4,5,7,8,9};
//长度为10,后续可以增大,元素的值初始为0
vector<int> vector2(10);
//10个100
vector<int> vector3(10,100);
vector<int> copyVector (vector1);
//使用迭代器,复制vector1的5个元素
vector<int> partialCopy (vector1.cbegin(),vector1.cbegin() + 5);
17.2.2 push_back( )
实例化一个整型 vector 后,接下来需要在 vector 中插入元素(整数)。在 vector 中插入元素时,元素将插入到数组末尾,这是使用成员函数 push_back()完成的。
size ( )它返回 vector 中存储的元素数。
vector<int> vector1;
vector1.push_back(1);
vector1.push_back(2);
vector1.push_back(3);
cout << vector1.size() << endl; //3
17.2.3 列表初始化
vector<int> myVector{1,2,3,4,5};
vector<int> vector1 = {1,2,3,4,5};
17.2.4 insert( )
push_back()在 vector 末尾插入元素。如果要在中间插入元素,可以使用函数insert()。
很多 STL 容器(包括std::vector)都包含 insert( )函数,且有多个重载版本。
vector<int> vector1(10,0);
vector1.insert(vector1.begin(),100);
//够指定插入位置、要插入的元素数以及这些元素的值
vector1.insert(vector1.end(),2,100);
//可将另一个 vector 的内容插入到指定位置
vector<int> vector2{99,99,99};
vector1.insert(vector1.begin()+3, vector2.begin(), vector2.end());
17.2.5 使用数组语法访问 vector 中的元素
vector<int> vector1{1,2,3,4,5,6};
for (int i = 0; i < vector1.size(); ++i) {
cout << vector1[i] << endl;
}
17.2.6 使用指针语法访问 vector 中的元素
可使用迭代器以类似于指针的语法访问 vector 中的元素
vector<int> v{1,2,3,4,5,6,7,8,9,10};
for (auto i = v.begin(); i != v.end(); i++){
cout << *i << endl;
}
17.2.7 pop_back()
vector 支持使用 pop_back() 函数将末尾的元素删除。使用 pop_back()将元素从 vector 中删除所需的时间是固定的,即不随 vector 存储的元素个数而异。
vector<int> v{1,2,3,4,5,6,7,8,9,10};
v.pop_back();
cout << v.size() << endl; //9
17.3 大小容量
- vector 的大小指的是实际存储的元素数,要查询 vector 当前存储的元素数,可调用 size( )
- vector 的容量指的是在重新分配内存以存储更多元素前 vector 能够存储的元素数,要查询 vector 的容量,可调用 capacity( )
因此,vector 的大小小于或等于容量。
reserve 函数
reserve 函数的功能基本上是增加分配给内部数组的内存,以免频繁地重新分配内存。通过减少重新分配内存的次数,还可减少复制对象的时间,从而提高性能,这取决于存储在 vector 中的对象类型。
vector<int> v(3);
cout << v.size() << '\t' << v.capacity() << endl; // 3 3
v.push_back(5);
cout << v.size() << '\t' << v.capacity() << endl; // 4 6
v.reserve(100);
cout << v.size() << '\t' << v.capacity() << endl; // 4 100
17.4 STL deque 类
deque 是一个 STL 动态数组类,与 vector 非常类似,但支持在数组开头和末尾插入或删除元素。
deque 与 vector 极其相似,也支持使用函数 push_back( )和 pop_back( )在末尾插入和删除元素,也可以使用运算符[]以数组语法访问其元素。deque 与 vector 的不同之处在于,它还允许您使用 push_front 和 pop_front 在开头插入和删除元素。
deque<int> deque1(5);
deque1.push_back(100);
deque1.push_front(100);
for (int i = 0; i < deque1.size(); ++i) {
cout << deque1[i] << '\t';
}
cout << endl;
deque1.pop_front();
for (auto i = deque1.begin(); i != deque1.end(); i++){
cout << *i << '\t';
}
cout << endl;
输出:
100 0 0 0 0 0 100
0 0 0 0 0 100
17.5 clear()与empty()
- 要清空 vector 和 deque 等 STL 容器,即删除其包含的所有元素,可使用函数 clear()。
- vector 和 deque 还包含成员函数 empty(),这个函数在容器为空时返回 true
intDeque.clear();
if (intDeque.empty())
cout << "The container is now empty" << endl;
输出结果:
The container is now empty
第 18 章 STL list 和 forward_list
标准模板库(STL)以模板类 std::list 的方式向程序员提供了一个双向链表。双向链表的主要优点是,插入和删除元素的速度快,且时间是固定的。
从 C++11 起,您还可使用单向链表 std::forward_list,这种链表只能沿一个方向遍历。
18.1 std::list 的特点
链表是一系列节点,其中每个节点除包含对象或值外还指向下一个节点,即每个节点都链接到下一个节点和前一个节点,list 类的 STL 实现允许在开头、末尾和中间插入元素,且所需的时间固定。
18.2 基本的 list 操作
18.2.1 实例化 std::list 对象
//长度为10,每个元素值为0
list<int> list1(10);
//10个99
list<int> list2(10,99);
list<int> list3(list2);
list<int> list4(list2.begin(), list2.end());
18.2.2 在 list 开头或末尾插入元素
deque 类似,要在 list 开头插入元素,可使用其成员方法 push_front()。要在末尾插入,可使用成员方法push_back()。这两个方法都接受一个参数,即要插入的值:
list<int> list1(2);
list1.push_front(100);
list1.push_back(100);
for (auto i = list1.begin() ; i != list1.end(); i++) {
cout << *i << '\t';
}
cout << endl;
输出结果:
100 0 0 100
18.2.3 insert()
std::list 的特点之一是,在其中间插入元素所需的时间是固定的,这项工作是由成员函数 insert()完成的。成员函数 ==list::insert()==有 3 种版本。
- 第 1 种版本:
iterator insert(iterator pos, const T& x)
在这里,insert 函数接受的第 1 个参数是插入位置,第 2 个参数是要插入的值。该函数返回一个迭代器,它指向刚插入到 list 中的元素。
- 第 2 种版本:
void insert(iterator pos, size_type n, const T& x)
该函数的第 1 个参数是插入位置,第 2 个参数是要插入的元素个数,而最后一个参数是要插入的值。
- 3 种版本:
template <class InputIterator>
void insert(iterator pos, InputIterator f, InputIterator l)
该重载版本是一个模板函数,除一个位置参数外,它还接受两个输入迭代器,指定要将集合中相应范围内的元素插入到 list 中。注意,输入类型 InputIterator 是一种模板参数化类型,因此可指定任何集合(数组、vector 或另一个 list)的边界。
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
template<typename T>
void dispaly(list<T> list1){
for (auto i = list1.begin(); i != list1.end() ; ++i) {
cout << *i << '\t';
}
cout << endl;
}
int main(){
list<int> list1(2,99);
list1.insert(list1.begin(),100);
list1.insert(list1.end(),100);
dispaly(list1); //100 99 99 100
auto num = list1.insert(++list1.begin(),2,88);
cout << *num << endl; //88
dispaly(list1); //100 88 88 99 99 100
list<int> list2(2,0);
list1.insert(list1.end(),list2.begin(),list2.end());
dispaly(list1); //100 88 88 99 99 100 0 0
}
18.2.4 erase()
list 的成员函数 erase()有两种重载版本:一个接受一个迭代器参数并删除迭代器指向的元素;另一个接受两个迭代器参数并删除指定范围内的所有元素。
list<int> list1{1,2,3};
list1.erase(list1.begin());
list1.erase(--list1.end());
dispaly(list1); //2
list<int> list2{5,6,7};
list2.erase(++list2.begin(),list2.end());
dispaly(list2); //5
【提示】要清空 std::list 等 STL 容器,最简单、最快捷的方式是调用成员函数 clear()。
18.3 对 list 中的元素进行反转和排序
18.3.1 reverse( )
linkInts.reverse();
18.3.2 sort( )
list 的成员函数 sort()有两个版本
- 其中一个没有参数:
linkInts.sort(); // sort in ascending order
- 另一个接受一个二元谓词函数作为参数,让您能够指定排序标准:
bool SortPredicate_Descending (const int& lhs, const int& rhs)
{
// define criteria for list::sort: return true for desired order
return (lhs > rhs);
}
案例:
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
template<typename T>
void dispaly(list<T> list1){
for (auto i = list1.begin(); i != list1.end() ; ++i) {
cout << *i << '\t';
}
cout << endl;
}
bool sortWay(const int& i, const int& j){
//return i < j; //升序 默认情况
return i > j; //降序
}
int main(){
list<int> list1{2,3,5,1,6};
list1.sort();
dispaly(list1);//1 2 3 5 6
list<int> list2{2,3,5,1,7,6,4,8,9};
list2.sort(sortWay);
dispaly(list2); //9 8 7 6 5 4 3 2 1
}
18.3.3 对包含对象的 list 进行排序以及删除其中的元素
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
class Person{
public:
int age;
string name;
string toString;
Person(int age,string name):age(age),name(name)
{
toString = "age: " + to_string(age) + "\tname: " + name;
}
bool operator> (const Person& person)const{ //降序时使用
return this->age > person.age;
}
bool operator< (const Person& person) const{ //升序时使用
return this->age < person.age;
}
operator const char*() const {
return toString.c_str();
}
};
bool sortWay(const Person& person1, const Person& person2){
return person1 > person2; //降序
}
void showPersons(list<Person> persons){
for (auto p : persons){
cout << p << endl;
}
}
int main(){
Person person1(23,"hao");
Person person2(19,"rose");
Person person3(22,"cris");
Person person4(21,"lucas");
Person person5(28,"kai");
list<Person> personList{person1,person2,person3,person4,person5};
cout << "列表信息如下:" << endl;
showPersons(personList);
cout << endl <<"按年龄升序排序:" << endl;
personList.sort();
showPersons(personList);
cout << endl <<"按年龄降序排序:" << endl;
personList.sort(sortWay);
showPersons(personList);
}
输出结果:列表信息如下:
age: 23 name: hao
age: 19 name: rose
age: 22 name: cris
age: 21 name: lucas
age: 28 name: kai
按年龄升序排序:
age: 19 name: rose
age: 21 name: lucas
age: 22 name: cris
age: 23 name: hao
age: 28 name: kai
按年龄降序排序:
age: 28 name: kai
age: 21 name: lucas
age: 22 name: cris
age: 19 name: rose
age: 23 name: hao
18.4 C++11 引入的 std::forward_list
list是双向链表 ,而forward_list是一种单向链表。
【提示】
要使用 std::forward_list,需要包含头文件<forward_list>:
#include<forward_list>
forward_list 的用法与 list 很像,但只能沿一个方向移动迭代器,且插入元素时只能使用函数 push_front( ),而不能使用 push_back( )。当然,总是可以使用 insert( )及其重载版本在指定位置插入元素。
#include <iostream>
#include <forward_list>
using namespace std;
template<typename T>
void show(const T container){
for (auto i = container.begin(); i != container.end() ; ++i) {
cout << *i << '\t';
}
cout << endl;
}
int main(){
forward_list<int> myList{8,6,1,3,4};
myList.push_front(9);
show(myList);
myList.sort();
show(myList);
return 0;
}
第 19 章 STL 集合类
19.1 简介
容器 set 和 multiset 让程序员能够在容器中快速查找键,键是存储在一维容器中的值。set 和 multiset之间的区别在于,后者可存储重复的值,而前者只能存储唯一的值。
为了实现快速搜索,STL set 和 multiset 的内部结构像二叉树,这意味着将元素插入到 set 或 multiset时将对其进行排序,以提高查找速度。这还意味着不像 vector 那样可以使用其他元素替换给定位置的元素,位于 set 中特定位置的元素不能替换为值不同的新元素,这是因为 set 将把新元素同内部树中的其他元素进行比较,进而将其放在其他位置。
19.2 STL set 和 multiset 的基本操作
19.2.1 实例化 std::set 对象
鉴于 set 和 multiset 都是在插入时对元素进行排序的容器,如果您没有指定排序标准,它们将使用默认谓词 std::less,确保包含的元素按升序排列。
要创建二元排序谓词,可在类中定义一个 operator( ),让它接受两个参数(其类型与集合存储的数据类型相同),并根据排序标准返回 true。下面是一个这样的排序谓词,它按降序排列元素。
template <typename T>
struct SortDescending
{
bool operator()(const T& lhs, const T& rhs) const
{
return (lhs > rhs);
}
};
set <int, SortDescending<int>> setInts;
案例
#include <iostream>
#include <set>
using namespace std;
template<typename T>
struct SortDescending{
bool operator()(const T& l, const T& r) const
{
return l > r; //降序
}
};
int main(){
set<int> s1;
multiset<int> ms1;
set<int, SortDescending<int>> s2;
set<int> s3(s1);
set<int,SortDescending<int>> s4(s2);
set<int> s5(s1.begin(),s1.end());
return 0;
}
19.2.2 插入元素 insert()
set 和 multiset 的大多数函数的用法类似,它们接受类似的参数,返回类型也类似。例如,要在这两种容器中插入元素,都可使用成员函数== insert()==,这个函数接受要插入的值或容器的指定范围:
setInts.insert (-1);
msetInts.insert (setInts.begin (), setInts.end ());
==mulitset::count( )==确定 multiset 包含多少个这样的元素。
案例
#include <iostream>
#include <set>
using namespace std;
template<typename T>
struct SortDescending{
bool operator()(const T& l, const T& r) const
{
return l > r; //降序
}
};
template<typename T>
void dispaly(T container){
for (auto i = container.begin(); i != container.end() ; ++i) {
cout << *i << '\t';
}
cout << endl;
}
int main(){
set<int,SortDescending<int>> s1{100,6,8,-1000};
s1.insert(999);
dispaly(s1); //999 100 8 6 -1000
multiset<int> mset{1,2,3,1,2,1};
cout << mset.count(1) << endl; //3
return 0;
}
19.2.3 查找元素 find( )
能够根据给定的键来查找值.multiset 可包含多个值相同的元素,因此对于 multiset,这个
函数查找第一个与给定键匹配的元素。
find( )返回值是迭代器,若找得到则返回指向第一个元素的迭代器,否则返回end()。
set<int,SortDescending<int>> s1{100,6,8,-1000};
auto i = s1.find(-1000);
if (i != s1.end()){
cout << "get it, value = " << *i << endl;
}else{
cout << "nothing" << endl;
}
19.2.4 删除元素 erase( )
- 能够根据键删除值,若有重复值,则一并删除:
setObject.erase (key);
- erase()函数的另一个版本接受一个迭代器作为参数,并删除该迭代器指向的元素:
setObject.erase (element);
- 通过使用迭代器指定的边界,可将指定范围内的所有元素都从 set 或 multiset 中删除:
setObject.erase (iLowerBound, iUpperBound);
multiset<int> s{1,1,2,3,4,4,4,4};
s.erase(4);
dispaly(s); //1 1 2 3
19.3 使用 STL set 和 multiset 的优缺点
对需要频繁查找的应用程序来说,STL set 和 multiset 很有优势,因为其内容是经过排序的,因此查找速度更快。然而,为了提供这种优势,容器在插入元素时进行排序。因此,插入元素时有额外开销,因为需要对元素进行排序—如果应用程序将频繁使用 find( )等函数,则这种开销是值得的。
find( )利用了内部的二叉树结构。这种有序的二叉树结构使得 set 和 multiset 与顺序容器(如vector)相比有一个缺点:在 vector 中,可以使用新值替换迭代器(如 std::find( )返回的迭代器)指向的元素;但 set 根据元素的值对其进行了排序,因此不能使用迭代器覆盖元素的值,虽然通过编程可实现这种功能。
19.4 STL 散列集合实现 std::unordered_set 和 std::unordered_multiset
STL std::set 和 std::multiset 使用 std::less或提供的谓词对元素(同时也是键)进行排序。相对于vector 等未经排序的容器,在经过排序的容器中查找的速度更快,其 sort()的复杂度为对数。这意味着在 set 中查找元素时,所需的时间不是与元素数成正比,而是与元素数的对数成正比。
相比于未经排序的容器(查找时间与元素数成正比),这极大地改善了性能,但有时候这还不够。使用散列函数来计算排序索引。将元素插入散列集合时,首先使用散列函数计算出一个唯一的索引,再根据该索引决定将元素放到哪个桶(bucket)中。
从 C++11 起,STL 提供的容器类 std::unordered_set 就是基于散列的 set
unordered_set 的一个重要特征是,有一个负责确定排列顺序的散列函数
unordered_set<int> unorderedSet{1,2,3,4,5,6,7,8,9,10,11,12,13};
dispaly(unorderedSet);
cout << "the number of bucket: " << unorderedSet.bucket_count()<< endl;
cout << "the number of element: " << unorderedSet.size() << endl;
cout << endl;
unorderedSet.insert(999);
dispaly(unorderedSet);
cout << "the number of bucket: " << unorderedSet.bucket_count()<< endl;
cout << "the number of element: " << unorderedSet.size() << endl;
输出结果:
13 12 11 10 9 8 7 6 5 4 3 2 1
the number of bucket: 13
the number of element: 13
1 2 3 4 5 6 7 8 9 10 11 12 999 13
the number of bucket: 29
the number of element: 14
- unorderedSet.bucket_count() 是散列表的的大小
- edSet.size()是集合元素的个数
第 20 章 STL 映射类
20.1 STL 映射类简介
map 和 multimap 是键-值对容器,支持根据键进行查找。
map 和 multimap 之间的区别在于,后者能够存储重复的键,而前者只能存储唯一的键。
为了实现快速查找,STL map 和 multimap 的内部结构看起来像棵二叉树。这意味着在 map 或multimap 中插入元素时将进行排序;还意味着不像 vector 那样可以使用其他元素替换给定位置的元素,位于 map 中特定位置的元素不能替换为值不同的新元素,这是因为 map 将把新元素同二叉树中的其他元素进行比较,进而将它放在其他位置。
20.2 STL map 和 multimap 的基本操作
20.2.1 实例化 化 std::map 和 std::multimap
需要指定键和值的类型以及可选的谓词(它帮助 map 类对插入
的元素进行排序)。map <keyType, valueType, Predicate=std::less <keyType>> mapObj;
第三个模板参数是可选的。如果您值指定了键和值的类型,而省略了第三个模板参数,std::map和 std::multimap 将把 std::less<>用作排序标准。因此,将整数映射到字符串的 map 或 multimap 类似于std::map<int, string> mapIntToStr;
**案例 **
#include <iostream>
#include <map>
using namespace std;
template <typename keyType>
struct ReverseSort{
bool operator()(const keyType& key1, const keyType key2) const
{
return key1 > key2;
}
};
int main(){
map<int, string> m;
multimap<int,string,ReverseSort<int>> mm;
map<int,string> m1(m);
multimap<int,string,ReverseSort<int>> mm1(mm.cbegin(), mm.cend());
return 0;
}
20.2.2 在 STL map 或 multimap 中插入元素
可使用成员函数 insert( ) 搭配 make_pair():
std::map<int, std::string> mapIntToStr1;
// insert pair of key and value using make_pair function
mapIntToStr.insert (make_pair (-1, "Minus One"));
鉴于这两种容器包含的元素都是键-值对,因此也可直接使用 ==std::pair ==来指定要插入的键和值:
mapIntToStr.insert (pair <int, string>(1000, "One Thousand"));
另外,还可使用类似于数组的语法进行插入。这种方式对用户不太友好,是由下标运算符([])支持的:
mapIntToStr [1000000] = "One Million";
还可使用 map 来实例化 multimap:
std::multimap<int, std::string> mmapIntToStr(mapIntToStr.cbegin(),mapIntToStr.cend());
案例
#include <iostream>
#include <map>
using namespace std;
template <typename T>
void dispaly(const T& container){
for (auto element = container.begin(); element != container.end(); ++element) {
cout << element->first << " : " << element->second << endl;
}
}
template <typename keyType>
struct ReverseSort{
bool operator()(const keyType& key1, const keyType key2) const
{
return key1 > key2;
}
};
int main(){
map<int,string> m;
m.insert(make_pair(0,"hao"));
m.insert(pair<int,string>(1,"chen"));
m[2] = "shahao";
m.insert(map<int,string>::value_type(3,"chen"));
dispaly(m);
cout << endl;
multimap<int,string>mm(m.begin(), m.end());
dispaly(mm);
}
输出结果:
0 : hao
1 : chen
2 : shahao
3 : chen
0 : hao
1 : chen
2 : shahao
3 : chen
【注意】
element->first 的值为:迭代器指向元素的key值
<< element->second 的值为:迭代器指向元素的value值
20.2.3 查找元素 find( )
map 和 multimap 等关联容器都提供了成员函数 find(),它让您能够根据给定的键查找值。
find( )总是返回一个迭代器:
multimap <int, string>::const_iterator pairFound = mapIntToStr.find(key);
应首先检查该迭代器,确保 find( )已成功,再使用它来访问找到的值:
if (pairFound != mapIntToStr.end())
{
cout << "Key " << pairFound->first << " points to Value: ";
cout << pairFound->second << endl;
}
else
cout << "Sorry, pair with key " << key << " not in map" << endl;
案例
multimap<int, string> mm;
mm.insert(make_pair(1,"hao"));
mm.insert(make_pair(1,"kai"));
auto i = mm.find(1);
cout << i->first << " : " << i->second << endl; //1 : hao
multimap 中查找元素
multimap,容器可能包含多个键相同的键-值对,因此需要找到与指定键对应的所有值。为此,可使用 ==multimap::count( )==确定有多少个值与指定的键对应,再对迭代器递增,以访问这些相邻的值:
multimap<int, string> mm;
mm.insert(make_pair(1,"hao"));
mm.insert(make_pair(1,"kai"));
mm.insert(make_pair(1,"shahao"));
auto i = mm.find(1);
int num = mm.count(1);
if (i != mm.end()){
for (int j = 0; j < num; ++j) {
cout << i->first << " : " << i->second << endl;
i ++;
}
}
输出杰豪:
1 : hao
1 : kai
1 : shahao
20.2.5 删除元素 erase()
- erase()e 函数将键作为参数,这将删除包含指定键的所有键-值对。
mapObject.erase (key);
- 函数 erase()的另一种版本接受迭代器作为参数,并删除迭代器指向的元素:
mapObject.erase(element);
- 还可使用迭代器指定边界,从而将指定范围内的所有元素都从 map 或 multimap 中删除:
mapObject.erase (lowerBound, upperBound);
20.3 自定义的排序谓词
map 和 multimap 的模板定义包含第 3 个参数,该参数是确保 map 能够正常工作的排序谓词。如果没有指定这个参数(如前面的示例所示),将使用 std::less <>提供的默认排序标准,该谓词使用<运算符来比较两个对象。
template<typename keyType>
struct Predicate
{
bool operator()(const keyType& key1, const keyType& key2)
{
// your sort priority logic here
}
};
对于键类型为 std::string 的 map,默认排序谓词 std::less导致根据 std::string 类定义的<运算符进行排序,因此区分大小写。很多应用程序(如电话簿)要求执行插入和搜索操作时不区分大小写,为了满足这种需求,一种解决方案是在实例化 map 时提供一个排序谓词,它根据不区分大小写的比较结果返回 true 或 false。
#include <iostream>
#include <map>
#include <algorithm>
using namespace std;
template <typename T>
void dispaly(const T& container){
for (auto element = container.begin(); element != container.end(); ++element) {
cout << element->first << " : " << element->second << endl;
}
}
struct PredIgnoreCase{
bool operator()(const string& str1, const string& str2) const
{
string stringNoCase1(str1), stringNoCase2(str2);
/*
* 参数一二:要变化的内容的范围
* 参数三:存储结果
*/
transform(str1.begin(), str1.end(), stringNoCase1.begin(), ::tolower);
transform(str2.begin(), str2.end(), stringNoCase2.begin(), ::tolower);
return(stringNoCase1< stringNoCase2);
}
};
int main(){
//键值区分大小写
map<string,string> m1;
m1.insert(make_pair("hao","hao"));
m1.insert(make_pair("kai","hao"));
m1.insert(make_pair("HAO","hao"));
m1.insert(make_pair("KAI","hao"));
dispaly(m1);
cout << endl;
//键值不区分大小写
map<string,string,PredIgnoreCase> m2(m1.begin(),m1.end());
dispaly(m2);
return 0;
}
输出结果:
HAO : hao
KAI : hao
hao : hao
kai : hao
HAO : hao
KAI : hao
20.4 基于散列表的 STL 键-值对容器
从 C++11 起,STL 支持散列映射—std::unordered_map 类。要使用这个模板类,需要包含头文件<unordered_map>
20.4.1 散列表的工作原理
可将散列表视为一个键-值对集合,根据给定的键,可找到相应的值。散列表与简单映射的区别在于,散列表将键-值对存储在桶中,每个桶都有索引,指出了它在散列表中的相对位置(类似于数组)。这种索引是使用散列函数根据键计算得到的:Index = HashFunction(key, TableSize);
使用 find( )根据键查找元素时,将使用 HashFunction( )计算元素的位置,并返回该位置的值,就像数组返回其存储的元素那样。如果 HashFunction( )不佳,将导致多个元素的索引相同,进而存储在同一个桶中,即桶变成了元素列表。这种情形被称为冲突(collision),将降低查找速度,使查找时间不再是固定的。
20.4.2 使用 unordered_map 和 unordered_multimap
这两种实现散列表的容器是 C++11 引入的,与 std::map 和 std::multimap 差别不大,可以类似的方式执行实例化、插入和查找:
// instantiate unordered_map of int to string:
unordered_map<int, string> umapIntToStr;
// insert()
umapIntToStr.insert(make_pair(1000, "Thousand"));
// find():
auto pairFound = umapIntToStr.find(1000);
cout << pairFound->first << " - " << pairFound->second << endl;
// find value using array semantics:
cout << "umapIntToStr[1000] = " << umapIntToStr[1000] << endl;
然而,一个重要的特点是,unordered_map 包含一个散列函数,用于计算排列顺序:
unordered_map<int, string>::hasher hFn = umapIntToStr.hash_function();
要获悉键对应的索引,可调用该散列函数,并将键传递给它:
size_t hashingVal = hFn(1000);
鉴于 unordered_map 将键-值对存储在桶中,在元素数达到或接近桶数时,它将自动执行负载均衡:
cout << "Load factor: " << umapIntToStr.load_factor() << endl;
cout << "Max load factor = " << umapIntToStr.max_load_factor() << endl;
cout << "Max bucket count = " << umapIntToStr.max_bucket_count() << endl;
load_factor( ) 指出了 unordered_map 桶的填满程度。因插入元素导致 load_factor( ) 超 过max_load_factor( )时,unordered_map 将重新组织以增加桶数,并重建散列表。
#include <iostream>
#include <unordered_map>
using namespace std;
template <typename T>
void dispaly(const T& container){
// for (auto element = container.begin(); element != container.end(); ++element) {
// cout << element->first << " : " << element->second << "\t";
// }
// cout << endl;
cout << "Number of pairs, size() = " << container.size() << endl;
cout << "Bucket count = " << container.bucket_count() << endl;
cout << "Current load factor = " << container.load_factor() << endl;
cout << "Max load factor = " << container.max_load_factor() << endl;
}
int main(){
unordered_map<int, string> um;
um.insert(make_pair(1,"hao"));
um.insert(make_pair(999,"kai"));
um.insert(make_pair(3,"tin"));
um.insert(make_pair(-1000,"hui"));
um.insert(make_pair(95,"hon"));
um.insert(make_pair(125,"hon"));
um.insert(make_pair(-453,"hon"));
dispaly(um);
cout << endl << endl;
um.insert(make_pair(-555,"hao"));
um.insert(make_pair(-100,"hao"));
um.insert(make_pair(-45,"hao"));
um.insert(make_pair(987,"hao"));
um.insert(make_pair(78,"hao"));
um.insert(make_pair(-48,"hao"));
dispaly(um);
cout << endl << endl;
um.insert(make_pair(-8,"hao"));
dispaly(um);
return 0;
}
输出结果:
Number of pairs, size() = 7
Bucket count = 13
Current load factor = 0.538462
Max load factor = 1
Number of pairs, size() = 13
Bucket count = 13
Current load factor = 1
Max load factor = 1
Number of pairs, size() = 14
Bucket count = 29
Current load factor = 0.482759
Max load factor = 1
第 21 章 函数对象
21.1 函数对象与谓词的概念
函数对象是实现了 operator()的类的对象。虽然函数和函数指针也可归为函数对象,但实现了 operator()的类的对象才能保存状态(即类的成员属性的值),才能用于标准模板库(STL)算法。
C++程序员常用于 STL 算法的函数对象可分为下列两种类型。
- 一元函数:接受一个参数的函数,如 f(x)。如果一元函数返回一个布尔值,则该函数称为谓词。
- 二元函数:接受两个参数的函数,如 f(x, y)。如果二元函数返回一个布尔值,则该函数称为二元谓词。
返回布尔类型的函数对象通常用于需要进行判断的算法,如前面介绍的 find()和 sort()。组合两个函数对象的函数对象称为自适应函数对象。
21.2 函数对象的典型用途
21.2.1 一元函数
只对一个参数进行操作的函数称为一元函数。
template <typename elementType>
void FuncDisplayElement (const elementType& element)
{
cout << element << ' ';
};
该函数也可采用另一种表现形式,即其实现包含在类或结构的**operator()**中:
// Struct that can behave as a unary function
template <typename elementType>
struct DisplayElement
{
void operator () (const elementType& element) const
{
cout << element << ' ';
}
};
【提示】
DisplayElement 是一个结构,如果它是类,则必须给 operator( )指定访问限定符 public。
结构相当于成员默认为公有的类。
这两种实现都可用于 STL 算法 for_each(),将集合中的内容显示在屏幕上,每次显示一个元素。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename T>
struct DisplayElement{
void operator()(const T& element) const{
cout << element << "\t";
}
};
template <typename T>
void FuncDisplayElement(const T& element){
cout << element << "\t";
}
int main(){
vector<int> v{1,2,3,4,5};
for_each(v.begin(),v.end(),DisplayElement<int>());
cout << endl;
vector<char> v1{'a','b','c','d'};
for_each(v1.begin(),v1.end(),FuncDisplayElement<char>);
return 0;
}
输出结果:
1 2 3 4 5
a b c d
如果能够使用结构的对象来存储信息,则使用在结构中实现的函数对象的优点将显现出来。这是FuncDisplayElement 不像结构那么强大的地方,因为结构除 operator( )外还可以有成员属性。下面是一个稍做修改的版本,它使用了成员属性:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename T>
struct DisplayElement{
int count;
DisplayElement():count(0){}
void operator()(const T& element)
{
cout << element << "\t";
++count;
}
};
int main(){
DisplayElement<int> de;
vector<int> v{1,2,3,4,5};
de = for_each(v.begin(),v.end(),DisplayElement<int>());
cout << de.count << endl; //5
}
21.2.2 一元谓词
返回布尔值的一元函数是谓词。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename T>
struct IsMultiple{
T Divisor;
IsMultiple(const T& divisor):Divisor(divisor){}
bool operator()(const T& element) const
{
return ((element % Divisor) == 0);
}
};
int main(){
vector<int> v{1,2,3,4,5,6,7,8,9,10};
auto element = find_if(v.begin(), v.end(), IsMultiple<int>(2));
if (element != v.end ())
{
cout << *element << endl;
}
return 0;
}
find_if()使用了一元谓词。这里将函数对象IsMutilple 初始化为用户提供的除数, find_if() 对指定范围内的每个元素调用一元谓词IsMutilple::operator( )。当 operator( )返回 true(即元素可被用户提供的除数整除)时,find_if 返回一个指向该元素的迭代器。然后,将 find_if( )操作的结果与容器的 end( )进行比较,以核实是否找到了满足条件的元素。
21.2.3 二元函数
如果函数 f(x, y)根据输入参数返回一个值,它将很有用。这种二元函数可用于对两个操作数执行
运算,如加、减、乘、除等。下面的二元函数返回输入参数的积:
template <typename elementType>
class Multiply
{
public:
elementType operator () (const elementType& elem1, const elementType& elem2)
{
return (elem1 * elem2);
}
};
同样,在上述实现中最重要的是 operator( ),它接受两个参数并返回它们的积。在 std::transform()等算法中,可使用该二元函数计算两个容器内容的乘积。程序清单 21.5 演示了如何在 std::transform()中使用该二元函数。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template<typename T>
class Multiply{
public:
T operator()(const T& x, const T& y){
return x*y;
}
};
int main(){
vector<int> xs{0,1,2,3,4,5};
vector<int> ys{100,101,102,103,104,105};
vector<int> res;
res.resize(xs.size());
/**
* 参数一二:乘数一的范围
* 参数三:乘数二的起始
* 参数四:存储结果
* 参数五:二元函数对象
*/
transform(xs.begin(),xs.end(),ys.begin(),res.begin(),Multiply<int>());
for (int index = 0; index < xs.size(); ++ index)
cout << xs [index] << "\t";
cout << endl;
for (int index = 0; index < ys.size(); ++ index)
cout << ys [index] << "\t";
cout << endl;
for (int index = 0; index < res.size(); ++ index)
cout << res [index] << "\t";
cout << endl;
}
输出结果:
0 1 2 3 4 5
100 101 102 103 104 105
0 101 204 309 416 525
21.2.4 二元谓词
接受两个参数并返回一个布尔值的函数是二元谓词。
这种函数用于诸如 std::sort( )等 STL 函数中,下列程序是一个二元谓词,它将两个字符串都转换为小写,再对其进行比较。这个谓词可用于对字符串 vector 进行不区分大小写的排序。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class CompareStringNoCase{
public:
bool operator()(const string& str1, const string& str2) const{
string str1LowerCase;
str1LowerCase.resize(str1.size());
transform (str1.begin (), str1.end (), str1LowerCase.begin (), ::tolower);
string str2LowerCase;
str2LowerCase.resize(str2.size());
transform (str2.begin (), str2.end (), str2LowerCase.begin (), ::tolower);
return (str1LowerCase < str2LowerCase);
}
};
template <typename T>
void DisplayContents (const T& container){
for (auto element = container.cbegin(); element != container.cend (); ++ element )
cout << *element << "\t";
cout << endl;
}
int main(){
vector <string> names;
names.push_back ("jim");
names.push_back ("Jack");
names.push_back ("Sam");
names.push_back ("Anna");
DisplayContents(names);
sort(names.begin(), names.end());
DisplayContents(names);
sort(names.begin(), names.end(), CompareStringNoCase());
DisplayContents(names);
return 0;
};
输出结果:
jim Jack Sam Anna
Anna Jack Sam jim
Anna Jack jim Sam
因为第三次排序使用二元谓词,所以排序不区分大小写,因此Jack与jim排序在一起。
第 22 章 lambda 表达式
lambda 表达式是一种定义匿名函数对象的简洁方式,这是 C++11 新增的。
22.1 lambda 表达式是什么
可将 lambda 表达式视为包含公有 operator( )的匿名结构(或类),从这种意义上说,lambda 表达式属于第 21 章介绍的函数对象。
// struct that behaves as a unary function
template <typename elementType>
struct DisplayElement
{
void operator () (const elementType& element) const
{
cout << element << ' ';
}
};
这个函数对象使用 cout 将 element 显示到屏幕上,通常用于 std::for_each()等算法中:
// Display every integer contained in a vector
for_each (numsInVec.cbegin (), // Start of range
numsInVec.cend (), // End of range
DisplayElement <int> ()); // Unary function object
如果使用 lambda 表达式,可将上述代码(包括函数对象的定义)简化为下述 3 行:
// Display every integer contained in a vector using lambda exp.
for_each (numsInVec.cbegin (), // Start of range
numsInVec.cend (), // End of range
[](const int& element) {cout << element << ' '; } );
编译器见到下述 lambda 表达式时:
[](const int& element) {cout << element << ' '; }
自动将其展开为类似于结构 DisplayElement的表示:
struct NoName
{
void operator () (const int& element) const
{
cout << element << ' ';
}
};
22.2 定义 lambda 表达式
lambda 表达式的定义必须以方括号([])打头。这些括号告诉编译器,接下来是一个 lambda 表达式。方括号的后面是一个参数列表,该参数列表与不使用 lambda 表达式时提供给 operator( )的参数列表相同。
22.3 一元函数对应的 lambda 表达式
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
vector<int> v{1,2,3,4,5,6,7,8,9,10};
for_each(v.begin(),
v.end(),
[](auto i){cout << i << "\t";}
);
cout << endl;
return 0;
}
这个 lambda 表达式通过关键字 auto 利用了编译器的类型自动推断功能。遵循 C++14 的
编译器都支持这种对 lambda 表达式的改进,也就是说,编译器将这样解读上述 lambda
表达式:[](const int& i) {cout << i << "\t"; }
22.4 一元谓词对应的 lambda 表达式
一元谓词是返回 bool 类型(true 或 false)的一元表达式。lambda 表达式也可返回值,例如,下面的 lambda 表达式在 num 为偶数时返回 true:
[](int& num) {return ((num % 2) == 0); }
在这里,返回值的性质让编译器知道该 lambda 表达式的返回类型为 bool。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
vector<int> v{1,3,5,7,8,9};
auto num_i = find_if(v.begin(),
v.end(),
[](auto i){return ((i % 2) == 0);}
);
if(num_i != v.end())
cout << *num_i << endl; //8
return 0;
}
22.5 通过捕获列表接受状态变量的 lambda 表达式
让 lambda 表达式接受该“状态”。一系列以状态变量的方式传递的参数([…])也被称为 lambda 表达式的捕获列表
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
int divisor = 5;
vector<int> v{1,3,5,7,8,9};
auto num_i = find_if(v.begin(),
v.end(),
[divisor](auto i){return ((i % divisor) == 0);});
if(num_i != v.end())
cout << *num_i << endl; //5
return 0;
}
22.6 lambda 表达式的通用语法
lambda 表达式总是以方括号打头,并可接受多个状态变量,为此可在捕获列表([…])中指定这些状态变量,并用逗号分隔:
[stateVar1, stateVar2](Type& param) { // lambda code here; }
如果要在 lambda 表达式中修改这些状态变量,可添加关键字multable:
[stateVar1, stateVar2](Type& param) mutable { // lambda code here; }
这样,便可在 lambda 表达式中修改捕获列表([])中指定的变量,但离开 lambda 表达式后,这些修改将无效。要确保在 lambda 表达式内部对状态变量的修改在其外部也有效,应按引用传递它们:
[&stateVar1, &stateVar2](Type& param) { // lambda code here; }
lambda 表达式还可接受多个输入参数,为此可用逗号分隔它们:
[stateVar1, stateVar2](Type1& var1, Type2& var2) { // lambda code here; }
如果要向编译器明确地指定返回类型,可使用->,如下所示:
[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{ return (value or expression ); }
最后,复合语句({})可包含多条用分号分隔的语句,如下所示:
[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{
Statement 1;
Statement 2;
return (value or expression);
}
22.7 二元函数对应的 lambda 表达式
二元函数接受两个参数,还可返回一个值。与之等价的 lambda 表达式如下:
[...](Type1& param1Name, Type2& param2Name) { // lambda code here; }
案例
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
vector<int> v1{1,2,3,4};
vector<int> v2{100,200,300,400};
vector<int> res;
res.resize(v1.size());
transform(
v1.begin(),
v1.end(),
v2.begin(),
res.begin(),
[](const int& a,const int&b){return a*b;}
);
for (auto i = res.begin(); i != res.end(); ++i) {
cout << *i << "\t";
}
cout << endl;
}
输出结果:
100 400 900 1600
22.8 二元谓词对应的 lambda 表达式
返回 true 或 false、可帮助决策的二元函数被称为二元谓词。这种谓词可用于 std::sort( )等排序算法中,这些算法对容器中的两个值调用二元谓词,以确定将哪个放在前面。与二元谓词等价的 lambda 表达式的通用语法如下:
[...](Type1& param1Name, Type2& param2Name) { // return bool expression; }
案例
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
vector<string> v{"hao","jim","HAA","joun","chen"};
sort(v.begin(), v.end(),
[](const string& str1, const string& str2)->bool
{
string string1;
string1.resize(str1.size());
string string2;
string2.resize(str2.size());
transform(str1.begin(),str1.end(),string1.begin(),::tolower);
transform(str2.begin(),str2.end(),string2.begin(),::tolower);
return string1 < string2; //升序
}
);
for (auto i = v.begin(); i != v.end() ; ++i) {
cout << *i << "\t";
}
cout << endl;
return 0;
};
输出结果:
chen HAA hao jim joun
22.9 总结
- 请牢记,lambda 表达式总是以[]或 [state1,state2,…]打头。
- 请牢记,除非使用关键字
mutable
进行指定,否则不能修改捕获列表中指定的状态变量。 - 别忘了,lambda 表达式是实现了 operator( )的匿名类(或结构)。
- 编写 lambda 表达式时,别忘了使用 const 对参数进行限定
- lambda 表达式的语句块({})包含多条语句时,别忘了显式地指定返回类型。
[...](...)->bool{...}
第 23 章 STL 算法
23.1 什么是 STL 算法
查找、搜索、删除和计数是一些通用算法,其应用范围很广。STL 通过通用的模板函数提供了这些算法以及其他的很多算法,可通过迭代器对容器进行操作。要使用 STL 算法,程序员必须包含头文件。
23.2 STL 算法的分类
STL 算法分两大类:非变序算法与变序算法。
23.2.1 非变序算法
不改变容器中元素的顺序和内容的算法称为非变序算法。
23.2.2 变序算法
变序算法改变其操作的序列的元素顺序或内容。
23.3 使用 STL 算法
23.3.1 find( )和 find_if( )
STL 算法 find( )和 find_if( )用于在 vector 等容器中查找与值匹配或满足条件的元素。
find( )的用法如下:
auto element = find (numsInVec.cbegin(), // Start of range
numsInVec.cend(), // End of range
numToFind); // Element to find
// Check if find() succeeded
if (element != numsInVec.cend ())
cout << "Result: Value found!" << endl;
find_if( )的用法与此类似,但需要通过第三个参数提供一个一元谓词(返回 true 或 false 的一元函数):
auto evenNum = find_if (numsInVec.cbegin(), // Start of range
numsInVec.cend(), // End of range
[](int element) { return (element % 2) == 0; } );
if (evenNum != numsInVec.cend())
cout << "Result: Value found!" << endl;
这两个函数都返回一个迭代器,您需要将其同容器的 end( )或 cend( )进行比较,检查查找操作是否成功。如果成功,便可进一步使用该迭代器。程序清单 23.1 使用 find( )在 vector 中查找一个值,并使用 find_if( )找到第一个偶数。
23.3.2 count( )和 count_if( )
算法 std::count( )和 count_if( )计算给定范围内的元素数。
std:: count( )计算包含给定值(使用相等运算符==进行测试)的元素数:
size_t numZeroes = count (numsInVec.cbegin (), numsInVec.cend (), 0);
cout << "Number of instances of '0': " << numZeroes << endl;
std::count_if( )计算这样的元素数,即满足通过参数传递的一元谓词(可以是函数对象,也可以是lambda 表达式):
// Unary predicate:
template <typename elementType>
bool IsEven (const elementType& number)
{
return ((number % 2) == 0); // true, if even876543221
}
...
// Use the count_if algorithm with the unary predicate IsEven:
size_t numEvenNums = count_if (numsInVec.cbegin (),
numsInVec.cend (), IsEven <int> );
cout << "Number of even elements: " << numEvenNums << endl;
23.3.3 search( )和 search_n( )
search( )用于在一个序列中查找另一个序列:
auto range = search (numsInVec.cbegin(), // Start range to search in
numsInVec.cend(), // End range to search in
numsInList.cbegin(), // start range to search
numsInList.cend() ); // End range to search for
search_n( )用于在容器中查找 n 个相邻的指定值:
auto partialRange = search_n (numsInVec.cbegin(), // Start range
numsInVec.cend(), // End range
3, // num items to be searched for
9); // value to search for
这两个函数都返回一个迭代器,它指向找到的第一个模式;使用该迭代器之前,务必将其与 end( )进行比较。
案例
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;
int main(){
vector<int> v1{100,99,1,2,3,44,44,44};
list<int> v2{1,2,3};
//用于在v1序列中查找另一个序列v2
auto i = search(v1.cbegin(),v1.cend(),v2.cbegin(),v2.cend());
if (i != v1.end()){
cout << *i << endl; //1
cout << distance (v1.cbegin(), i) << endl;//2
}
auto j = search_n(v1.cbegin(),v1.cend(),3,44);
if (j != v1.end()){
cout << *j << endl; //44
cout << distance (v1.cbegin(), j) << endl;//5
}
return 0;
}
23.3.4 fill( )和 fill_n( )
STL 算法 fill( )和 fill_n( )用于将指定范围的内容设置为指定值。
fill( )将指定范围内的元素设置为指定值:
vector <int> numsInVec (3);
// fill all elements in the container with value 9
fill (numsInVec.begin (), numsInVec.end (), 9);
顾名思义,fill_n( )将 n 个元素设置为指定的值,接受的参数包括起始位置、元素数以及要设置的值:
fill_n (numsInVec.begin () + 3, /*count*/ 3, /*fill value*/ -9);
23.3.5 std::generate( )将元素设置为运行阶段生成的值
函数 fill( )和 fill_n( )将集合的元素设置为指定的值,而 generate( )和 generate_n( )等 STL 算法用于将集合的内容设置为一元函数返回的值。
可使用 generate( )将指定范围内的元素设置为生成器函数返回的值:
generate (numsInVec.begin (), numsInVec.end (), // range
rand); // generator function
generate_n( )与 generate( )类似,但您指定的是要设置的元素数,而不是闭区间:
generate_n (numsInList.begin (), 5, rand);
因此,可使用这两种算法将容器设置为文件的内容或随机值。
案例
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <ctime>
using namespace std;
int main(){
vector<int> v(10);
generate(v.begin(),v.end(),rand);
for(auto i = v.begin(); i != v.end(); i++){
cout << *i << "\t";
}
cout << endl;
list <int> l (5);
generate_n(l.begin(),3,rand);
for(auto i = l.begin(); i != l.end(); i++){
cout << *i << "\t";
}
return 0;
}
输出结果:
41 18467 6334 26500 19169 15724 11478 29358 26962 24464
5705 28145 23281 0 0
23.3.6 for_each( )
算法 for_each( )对指定范围内的每个元素执行指定的一元函数对象,其用法如下:
fnObjType retValue = for_each (start_of_range,end_of_range,
unaryFunctionObject);
也可使用接受一个参数的 lambda 表达式代替一元函数对象。
返回值表明,for_each( )返回用于对指定范围内的每个元素进行处理的函数对(functor)。这意味着使用结构或类作为函数对象可存储状态信息,并在 for_each( )执行完毕后查询这些信息。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template<typename valType>
class DisplayElementKeepcount{
public:
int count;
DisplayElementKeepcount():count(0){}
void operator () (const valType& element){
++ count;
cout << element << "\t";
}
};
int main(){
vector<int> v{1,2,3,4,5,6,7,8,9};
DisplayElementKeepcount<int> functor = for_each(v.begin(),v.end(),DisplayElementKeepcount<int>());
cout << endl;
cout << functor.count << endl; //9
int numChars = 0;
string str("hello world");
for_each(str.begin(),str.end(),
[&numChars](char c)->void{
++ numChars;
cout << c << "\t";
});
cout << endl;
cout << numChars << endl; //11
return 0;
}
输出结果:
1 2 3 4 5 6 7 8 9
9
h e l l o w o r l d
11
23.3.7 std::transform( )对范围进行变换
std::for_each( )和 std::transform( )很像,都对源范围内的每个元素调用指定的函数对象。然而,std::transform( )有两个版本,第一个版本一个接受一元函数,常用于将字符串转换为大写或小写(使用的一元函数分别是toupper( )和 tolower( )):
string str ("THIS is a TEst string!");
transform (str.cbegin(), // start source range
str.cend(), // end source range
strLowerCaseCopy.begin(), // start destination range
::tolower); // unary function
第二个版本接受一个二元函数,让 transform( )能够处理一对来自两个不同范围的元素:
// sum elements from two vectors and store result in a deque
transform (numsInVec1.cbegin(), // start of source range 1
numsInVec1.cend(), // end of source range 1
numsInVec2.cbegin(), // start of source range 2
sumInDeque.begin(), // store result in a deque
plus<int>()); // binary function plus
不像 for_each( )那样只处理一个范围,这两个版本的 transform( )都将指定变换函数的结果赋给指定的目标范围。
案例一
string str1("hello string");
transform(str1.begin(), str1.end(), str1.begin(),::toupper);
cout << str1 << endl; //HELLO STRING
transform(str1.begin(), str1.end(), str1.begin(),::tolower);
cout << str1 << endl; //hello string
案例二
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template<typename T>
class Multiply{
public:
T operator()(const T& x, const T& y){
return x*y;
}
};
int main(){
vector<int> xs{0,1,2,3,4,5};
vector<int> ys{100,101,102,103,104,105};
vector<int> res;
res.resize(xs.size());
/**
* 参数一二:乘数一的范围
* 参数三:乘数二的起始
* 参数四:存储结果
* 参数五:二元函数对象
*/
transform(xs.begin(),xs.end(),ys.begin(),res.begin(),Multiply<int>());
for (int index = 0; index < xs.size(); ++ index)
cout << xs [index] << "\t";
cout << endl;
for (int index = 0; index < ys.size(); ++ index)
cout << ys [index] << "\t";
cout << endl;
for (int index = 0; index < res.size(); ++ index)
cout << res [index] << "\t";
cout << endl;
}
输出结果:
0 1 2 3 4 5
100 101 102 103 104 105
0 101 204 309 416 525
23.3.8 复制和删除操作
STL 提供了三个重要的复制函数:copy( )、copy_if( )和 copy_backward( )。
copy 沿向前的方向将源范围的内容赋给目标范围:
auto lastElement = copy (numsInList.cbegin(), // start source range
numsInList.cend(), // end source range
numsInVec.begin()); // start dest range
copy_if( )是 C++11 新增的,仅在指定的一元谓词返回 true 时才复制元素:
// copy odd numbers from list into vector
copy_if (numsInList.cbegin(), numsInList.cend(),
lastElement, // copy position in dest range
[](int element){return ((element % 2) == 1);});
copy_backward( )沿向后的方向将源范围的内容赋给目标范围:
copy_backward (numsInList.cbegin (),
numsInList.cend (),
numsInVec.end ());
remove( )将容器中与指定值匹配的元素删除:
// Remove all instances of '0', resize vector using erase()
auto newEnd = remove (numsInVec.begin (), numsInVec.end (), 0);
numsInVec.erase (newEnd, numsInVec.end ());
remove_if( )使用一个一元谓词,并将容器中满足该谓词的元素删除:
// Remove all odd numbers from the vector using remove_if
newEnd = remove_if (numsInVec.begin (), numsInVec.end (),
[](int num) {return ((num % 2) == 1);} ); //predicate
numsInVec.erase (newEnd, numsInVec.end ()); // resizing
案例
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;
template<typename T>
void display(T& container){
for (auto i = container.cbegin(); i != container.cend() ; ++i) {
cout << *i << "\t";
}
cout << endl;
}
int main(){
list<int> l{1,2,3,4,5,6,7,8,9,10};
list<int> l1{100,100,102,102,104};
vector<int> v(10);
auto lastElement = copy_if(l.begin(),l.end(),v.begin(),
[](int a)->bool {return a%2==0;});
display(v);
//2 4 6 8 10 100 100 102 102 104
copy(l1.begin(),l1.end(),lastElement);
display(v);
//2 4 6 8 10 100 100 102 102 104
//删除本质是后面元素前移覆盖被删除元素
auto testElement = remove(v.begin(),v.end(),100);
display(v);
//2 4 6 8 10 102 102 104 102 104
cout << *testElement << endl; //102 后移是104
v.erase(testElement,v.end());
display(v);
//2 4 6 8 10 102 102 104
auto testElement1 = remove_if(v.begin(),v.end(),[](int a)->bool{return (a<100);});
display(v);//102 102 104 8 10 102 102 104
v.erase(testElement1,v.end());
display(v);//102 102 104
return 0;
}
23.3.9 替换值以及替换满足给定条件的元素
STL算法replace( )与replace_if( )分别用于替换集合中等于指定值和满足给定条件的元素。replace( )根据比较运算符==的返回值来替换元素:
cout << "Using 'std::replace' to replace value 5 by 8" << endl;
replace (numsInVec.begin (), numsInVec.end (), 5, 8);
replace_if( )需要一个用户指定的一元谓词,对于要替换的每个值,该谓词都返回 true:
cout << "Using 'std::replace_if' to replace even values by -1" << endl;
replace_if (numsInVec.begin (), numsInVec.end (),
[](int element) {return ((element % 2) == 0); }, -1);
23.3.10 排序、在有序集合中搜索以及删除重复元素
在实际的应用程序中,经常需要排序以及在有序范围内(出于性能考虑)进行搜索。经常需要对一组信息进行排序,为此可使用 STL 算法 sort( ):
sort (numsInVec.begin (), numsInVec.end ()); // ascending order
这个版本的 sort( )将 std::less<>用作二元谓词,而该谓词使用 vector 存储的数据类型实现的运算符<。
您可使用另一个重载版本,以指定谓词,从而修改排列顺序:
sort (numsInVec.begin (), numsInVec.end (),
[](int lhs, int rhs) {return (lhs > rhs);} ); // descending order
同样,在显示集合的内容前,需要删除重复的元素。要删除相邻的重复值,可使用 unique():
auto newEnd = unique (numsInVec.begin (), numsInVec.end ());
numsInVec.erase (newEnd, numsInVec.end ()); // to resize
要进行快速查找,可使用 STL 算法 binary_search( ),这种算法只能用于有序容器:(binary_search( )算法只能用于经过排序的容器)
bool elementFound = binary_search (numsInVec.begin (), numsInVec.end (), 2011);
if (elementFound)
cout << "Element found in the vector!" << endl;
23.3.11 将范围分区 partition( )
std::partition( )将输入范围分为两部分:一部分满足一元谓词;另一部分不满足:
bool IsEven (const int& num) // unary predicate
{
return ((num % 2) == 0);
}
...
partition (numsInVec.begin(), numsInVec.end(), IsEven);
然而,std::partition( )不保证每个分区中元素的相对顺序不变。在相对顺序很重要,需要保持不变时,应使用 std::stable_partition( ):
stable_partition (numsInVec.begin(), numsInVec.end(), IsEven);
案例
0: #include <algorithm>
1: #include <vector>
2: #include <iostream>
3: using namespace std;
4:
5: bool IsEven (const int& num) // unary predicate
6: {
7: return ((num % 2) == 0);
8: }
9:
10: template <typename T>
11: void DisplayContents(const T& container)
12: {
13: for (auto element = container.cbegin();
14: element != container.cend();
15: ++ element)
16: cout << *element << ' ';
17:
18: cout << "| Number of elements: " << container.size() << endl;
19: }
20:
21: int main ()
22: {
23: vector <int> numsInVec{ 2017, 0, -1, 42, 10101, 25 };
24:
25: cout << "The initial contents: " << endl;
26: DisplayContents(numsInVec);
27:
28: vector <int> vecCopy (numsInVec);
29:
30: cout << "The effect of using partition():" << endl;
31: partition (numsInVec.begin (), numsInVec.end (), IsEven);
32: DisplayContents(numsInVec);
33:
34: cout << "The effect of using stable_partition():" << endl;
35: stable_partition (vecCopy.begin (), vecCopy.end (), IsEven);
36: DisplayContents(vecCopy);
37:
38: return 0;
39: }
输出:
The initial contents:
2017 0 -1 42 10101 25 | Number of elements: 6
The effect of using partition():
42 0 -1 2017 10101 25 | Number of elements: 6
The effect of using stable_partition():
0 42 2017 -1 10101 25 | Number of elements: 6
23.3.12 在有序集合中插入元素 lower_bound( )和 upper_bound( )
将元素插入到有序集合中时,将其插入到正确位置很重要。为了满足这种需求,STL 提供了lower_bound( )和 upper_bound( )等函数:
auto minInsertPos = lower_bound (names.begin(), names.end(),
"Brad Pitt");
// alternatively:
auto maxInsertPos = upper_bound (names.begin(), names.end(),
"Brad Pitt");
lower_bound( )和 upper_bound( )都返回一个迭代器,分别指向在不破坏现有顺序的情况下,元素可插入到有序范围内的最前位置和最后位置。
0: #include <algorithm>
1: #include <list>
2: #include <string>
3: #include <iostream>
4: using namespace std;
5:
6: template <typename T>
7: void DisplayContents(const T& container)
8: {
9: for (auto element = container.cbegin();
10: element != container.cend();
11: ++ element)
12: cout << *element << endl;
13: }
14:
15: int main ()
16: {
17: list<string> names{ "John", "Brad", "jack", "sean", "Anna" };
18:
19: cout << "Sorted contents of the list are: " << endl;
20: names.sort ();
21: DisplayContents(names);
22:
23: cout << "Lowest index where \"Brad\" can be inserted is: ";
24: auto minPos = lower_bound (names.begin (), names.end (), "Brad");
25: cout << distance (names.begin (), minPos) << endl;
26:
27: cout << "The highest index where \"Brad\" can be inserted is: ";
28: auto maxPos = upper_bound (names.begin (), names.end (), "Brad");
29: cout << distance (names.begin (), maxPos) << endl;
30:
31: cout << endl;
32:
33: cout << "List after inserting Brad in sorted order: " << endl;
34: names.insert (minPos, "Brad");
35: DisplayContents(names);
36:
37: return 0;
38: }
输出:
Sorted contents of the list are:
Anna
Brad
John
jack
sean
Lowest index where "Brad" can be inserted is: 1
The highest index where "Brad" can be inserted is: 2
List after inserting Brad in sorted order:
Anna
Brad
Brad
John
jack
sean
第 24 章 栈和队列
24.1 栈和队列的行为特征
24.1.1 栈
栈是 LIFO(后进先出)系统,只能从栈顶插入或删除元素。泛型 STL 容器 std::stack 模拟了栈的这种行为。
【提示】要使用 std::stack,必须包含头文件:
#include<stack>
24.1.2 队列
队列是 FIFO(先进先出)系统,元素被插入到队尾,最先插入的元素最先删除。
【提示】要使用 std::queue,必须包含头文件:
#include<queue>
24.2 使用 STL stack 类
STL stack 是一个模板类,要使用它,必须包含头文件。它是一个泛型类,允许在顶部插入和删除元素,而不允许访问中间的元素。
24.2.1 实例化 stack
在有些 STL 实现中,std::stack 的定义如下:
template <
class elementType,
class Container=deque<Type>
> class stack;
参数 elementType 是 stack 存储的对象类型。第二个模板参数 Container 是 stack 使用的默认底层容器实现类。stack 默认在内部使用 std::deque 来存储数据,但可指定使用 vector 或 list 来存储数据。因此,实例化整型栈的代码类似于下面这样:
std::stack <int> numsInStack;
要创建存储类(如 Tuna)对象的栈,可使用下述代码:
std::stack <Tuna> tunasInStack;
要创建使用不同底层容器的栈,可使用如下代码:
std::stack <double, vector <double>> doublesStackedInVec;
24.2.2 stack 的成员函数
stack 改变了另一种容器(如 deque、list 或 vector)的行为,通过限制元素插入或删除的方式实现其功能,从而提供严格遵守栈机制的行为特征。表 24.1 解释了 stack 类的公有成员函数并演示了如何将这些函数用于整型栈。
函数 | 描述 |
---|---|
push() | 在栈顶插入元素。numsInStack.push (25); |
pop() | 删除栈顶的元素。numsInStack.pop ( ); |
empty() | 检查栈是否为空并返回一个布尔值。 |
size() | 返回栈中的元素数。size_t numElements = numsInStack.size ( ); |
top() | 获得指向栈顶元素的引用。cout << "Element at the top = " << numsInStack.top ( ); |
stack 的公有成员函数只提供了这样的方法,即插入或删除元素的位置符合栈的行为特征。也就是说,虽然底层容器可能是 deque、vector 或 list,但禁用了这些容器的有些功能,以实现栈的行为特征。
#include <iostream>
#include <stack>
#include <list>
using namespace std;
int main(){
stack<int,list<int>> myStack;
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.push(5);
while(myStack.size() != 0){
cout << myStack.top() << "\t";
myStack.pop();
}
//5 4 3 2 1
cout << endl;
return 0;
}
24.3 使用 STL queue 类
STL queue 是一个模板类,要使用它,必须包含头文件。queue 是一个泛型类,只允许在末尾插入元素以及从开头删除元素。queue 不允许访问中间的元素,但可以访问开头和末尾的元素。
24.3.1 实例化 queue
std::queue 的定义如下:
template <
class elementType,
class Container = deque<Type>
> class queue;
其中 elementType 是 queue 对象包含的元素的类型。Container 是 std::queue 用于存储其数据的集合类型,可将该模板参数设置为 std::list、vector 或 deque,默认为 deque。实例化整型 queue 的最简单方式如下:
std::queue <int> numsInQ;
如果要创建这样的 queue,即其元素类型为 double,并使用 std::list(而不是默认的 queue)存储这些元素,可以像下面这样做:
std::queue <double, list <double>> dblsInQInList;
与 stack 一样,也可使用一个 queue 来实例化另一个 queue:
std::queue<int> copyQ(numsInQ);
24.3.2 queue 的成员函数
与 std::stack 一样,std::queue 的实现也是基于 STL 容器 vector、list 或 deque 的。queue 提供了几个成员函数来实现队列的行为特征。
函数 | 描述 |
---|---|
push() | 在队尾(即最后一个位置)插入一个元素。numsInQ.push (25); |
pop() | 将队首(即最开始位置)的元素删除。numsInQ.pop ( ); |
front() | 返回指向队首元素的引用。cout << "Element at front: " << numsInQ.front ( ); |
back() | 返回指向队尾元素(即最后插入的元素)的引用 |
empty() | 检查队列是否为空并返回一个布尔值 |
size() | 返回队列中的元素数。size_t nNumElements = numsInQ.size ( ); |
#include <iostream>
#include <queue>
#include <list>
using namespace std;
int main(){
queue<int,list<int>> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
cout << "q.front() = " << q.front() << endl; //1
cout << "q.back() = " << q.back() << endl; //5
while(q.size() != 0){
cout << q.front() << "\t";
q.pop();
}
//1 2 3 4 5
return 0;
}
24.4 使用 STL 优先级队列
STL priority_queue 是一个模板类,要使用它,也必须包含头文件。priority_queue 与 queue的不同之处在于,包含最大值(或二元谓词认为是最大值)的元素位于队首,且只能在队首执行操作。
24.4.1 实例化 priority_queue 类
std::priority_queue 类的定义如下:
template <
class elementType,
class Container=vector<Type>,
class Compare=less<typename Container::value_type>
>
class priority_queue
其中 :
- elementType 是一个模板参数,指定了优先级队列将包含的元素的类型。
- 第二个模板参数指定priority_queue 在内部将使用哪个集合类来存储数据,
- 第三个参数让程序员能够指定一个二元谓词,以帮助队列判断哪个元素应位于队首。如果没有指定二元谓词,priority_queue 类将默认使用 std::less,它使用运算符<比较对象。
使用运算符<比较对象。要实例化整型 priority_queue,最简单的方式如下:
std::priority_queue <int> numsInPrioQ;
如果要创建一个这样的 priority_queue,即其元素类型为 int,且按小到大的顺序存储在 std::deque中,则可这样做:
[为使用 std::greater<>,程序包含了标准头文件。]
priority_queue <int, deque <int>, greater <int>> numsInDescendingQ;
与 stack 一样,也可使用一个 priority_queue 来实例化另一个 priority_queue:
std::priority_queue <int> copyQ(numsInPrioQ);
24.4.2 priority_queue 的成员函数
函数 | 描述 |
---|---|
push() | 在优先级队列中插入一个元素。numsInPrioQ.push (10); |
pop() | 删除队首元素,即最大的元素。numsInPrioQ.pop ( ); |
top() | 返回指向队列中最大元素(即队首元素)的引用 |
empty() | 检查优先级队列是否为空并返回一个布尔值 |
size() | 返回优先级队列中的元素个数 |
案例
通过使用谓词将值最小的元素放在 priority_queue 开头
#include <queue>
#include <iostream>
#include <functional>
using namespace std;
int main(){
priority_queue<int,vector<int>,greater<int>> q;
q.push(4);
q.push(3);
q.push(8);
q.push(6);
q.push(7);
while (!q.empty ())
{
cout << "Deleting topmost element " << q.top () << endl;
q.pop ();
}
return 0;
}
输出结果
Deleting topmost element 3
Deleting topmost element 4
Deleting topmost element 6
Deleting topmost element 7
Deleting topmost element 8
第 25 章 使用 STL 位标志
25.1 bitset 类
std::bitset 是一个 STL 类,用于处理以位和位标志表示的信息。std::bitset 不是 STL 容器类,因为它不能调整长度。这是一个实用类,针对处理长度在编译阶段已知的位序列进行了优化。
【提示】要使用 std::bitset 类,必须包含头文件:
#include<bitset>
25.1.1 实例化 std::bitset
实例化这个模板类时,必须通过一个模板参数指定实例需要管理的位数:
bitset <4> fourBits; // 4 bits initialized to 0000
还可将 bitset 初始化为一个用字符串字面量(char*)表示的位序列:
bitset <5> fiveBits("10101"); // 5 bits 10101
使用一个 bitset 来实例化另一个 bitset 非常简单:
bitset <8> fiveBitsCopy(fiveBits);
案例
0: #include <bitset>
1: #include <iostream>
2: #include <string>
3:
4: int main ()
5: {
6: using namespace std;
7:
8: bitset <4> fourBits; // 4 bits initialized to 0000
9: cout << "Initial contents of fourBits: " << fourBits << endl;
10:
11: bitset <5> fiveBits ("10101"); // 5 bits 10101
12: cout << "Initial contents of fiveBits: " << fiveBits << endl;
13:
14: bitset <6> sixBits(0b100001); // C++14 binary literal
15: cout << "Initial contents of sixBits: " << sixBits << endl;
16:
17: bitset <8> eightBits (255); // 8 bits initialized to long int 255
18: cout << "Initial contents of eightBits: " << eightBits << endl;
19:
20: // instantiate one bitset as a copy of another
21: bitset <8> eightBitsCopy(eightBits);
21:
23: return 0;
24: }
输出:
Initial contents of fourBits: 0000
Initial contents of fiveBits: 10101
Initial contents of sixBits: 100001
Initial contents of eightBits: 11111111
25.2 使用 std::bitset 及其成员
bitset 类提供了很多成员函数,可用于在 bitset 中插入位、设置(重置)内容、读取内容(将内容写入到流中)。它还提供了一些运算符,用于显示位序列、执行按位逻辑运算等。
25.2.1 std:bitset 的运算符
std::bitset 提供一些很有用的运算符,如表所示,这些运算符让 bitset 使用起来非常容易。
25.2.2 std::bitset 的成员方法
25.3 vector
25.3.1 实例化 vector
实例化 vector的方式与实例化 vector 类似,有一些方便的重载构造函数可供使用:
vector <bool> boolFlags1;
例如,可创建一个这样的 vector,即它最初包含 10 个布尔元素,且每个元素都被初始化为 1(即 true):
vector <bool> boolFlags2 (10, true);
还可使用一个 vector创建另一个 vector:
vector <bool> boolFlags2Copy (boolFlags2);
25.3.2 vector的成员函数和运算符
vector提供了函数 flip( ),用于将序列中的布尔值取反,这与函数 bitset<>::flip( )很像。除这个方法外,vector与 std::vector 极其相似,例如,可使用 push_back 将标志位插入到序列中。
0: #include <vector>
1: #include <iostream>
2: using namespace std;
3:
4: int main ()
5: {
6: vector <bool> boolFlags(3); // instantiated to hold 3 bool flags
7: boolFlags [0] = true;
8: boolFlags [1] = true;
9: boolFlags [2] = false;
10:
11: boolFlags.push_back (true); // insert a fourth bool at the end
12:
13: cout << "The contents of the vector are: " << endl;
14: for (size_t index = 0; index < boolFlags.size (); ++ index)
15: cout << boolFlags [index] << ' ';
16:
17: cout << endl;
18: boolFlags.flip ();
19:
20: cout << "The contents of the vector are: " << endl;
21: for (size_t index = 0; index < boolFlags.size (); ++ index)
22: cout << boolFlags [index] << ' ';
23:
24: cout << endl;
25:
26: return 0;
27: }
输出:
The contents of the vector are:
1 1 0 1
The contents of the vector are:
0 0 1 0
第 26 章 理解智能指针
26.1 什么是智能指针
简单地说,C++智能指针是包含重载运算符的类,其行为像常规指针,但智能指针能够及时、妥善地销毁动态分配的数据,并实现了明确的对象生命周期,因此更有价值。
26.1.1 常规(原始)指针存在的问题
与其他现代编程语言不同,C++在内存分配、释放和管理方面向程序员提供了全面的灵活性。不幸的是,这种灵活性是把双刃剑,一方面,它使 C++成为一种功能强大的语言,另一方面,它让程序员能够制造与内存相关的问题,如动态分配的对象没有正确地释放时将导致内存泄露。
26.1.2 智能指针有何帮助
鉴于使用常规指针以及常规的内存管理方法存在的问题,当 C++程序员需要管理堆(自由存储区)中的数据时,并非一定要使用它们,而可在程序中使用智能指针,以更智能的方式分配和管理内存:
smart_pointer<SomeClass> spData = anObject.GetData ();
// Use a smart pointer like a conventional pointer!
spData->Display ();
(*spData).Display ();
// Don't have to worry about de-allocation
// (the smart pointer's destructor does it for you)
智能指针的行为类似常规指针(这里将其称为原始指针),但通过重载的运算符和析构函数确保动态分配的数据能够及时地销毁,从而提供了更多有用的功能。
26.2 智能指针是如何实现的
这个问题暂时可以简化为:“智能指针 spData 是如何做到像常规指针那样的?”答案如下:智能指针类重载了解除引用运算符(*)和成员选择运算符(->),让程序员可以像使用常规指针那样使用它们。
另外,为了让您能够在堆中管理各种类型,几乎所有良好的智能指针类都是模板类,包含其功能的泛型实现。由于是模板,它们是通用的,可以根据要管理的对象类型进行具体化。
程序清单 26.1 智能指针类最基本的组成部分
0: template <typename T>
1: class smart_pointer
2: {
3: private:
4: T* rawPtr;
5: public:
6: smart_pointer (T* pData) : rawPtr(pData) {} // constructor
7: ~smart_pointer () {delete rawPtr;}; // destructor
8:
9: // copy constructor
10: smart_pointer (const smart_pointer & anotherSP);
11: // copy assignment operator
12: smart_pointer& operator= (const smart_pointer& anotherSP);
13:
14: T& operator* () const // dereferencing operator
15: {
16: return *(rawPtr);
17: }
18:
19: T* operator-> () const // member selection operator
20: {
21: return rawPtr;
22: }
23: };
分析:
该智能指针类实现了两个运算符:*和->,如第 14~17 行及第 19~22 行所示,它们让这个类能够用作常规意义上的“指针”。例如,如果有一个 Tuna 类,则可这样对该类型的对象使用智能指针:
smart_pointer <Tuna> smartTuna (new Tuna);
smartTuna->Swim();
// Alternatively:
(*smartTuna).Swim ();
这个 smart_pointer 类还没有实现使其非常智能,从而胜于常规指针的功能。构造函数(如第 6 行所示)接受一个指针,并将其保存到该智能指针类内部的一个指针对象中。析构函数释放该指针,从而实现了自动内存释放。
26.3 智能指针类型
内存资源管理(即实现的内存所有权模型)是智能指针类与众不同的地方。智能指针决定在复制和赋值时如何处理内存资源。最简单的实现通常会导致性能问题,而最快的实现可能并非适合所有应用程序。因此,在应用程序中使用智能指针前,程序员应理解其工作原理。
智能指针的分类实际上就是内存资源管理策略的分类,可分为如下几类:
- 深复制;
- 写时复制(Copy on Write,COW);
- 引用计数;
- 引用链接;
- 破坏性复制。
26.3.1 深复制
在实现深复制的智能指针中,每个智能指针实例都保存一个它管理的对象的完整副本。每当智能指针被复制时,将复制它指向的对象(因此称为深复制)。每当智能指针离开作用域时,将(通过析构函数)释放它指向的内存。
虽然基于深复制的智能指针看起来并不比按值传递对象优越,但在处理多态对象时,其优点将显现出来。如下所示,使用智能指针可避免切除(slicing)问题:
void MakeFishSwim (Fish aFish) // note parameter type
{
aFish.Swim(); // virtual function
}
// ... Some function
Carp freshWaterFish;
MakeFishSwim (freshWaterFish); // Carp will be 'sliced' to Fish
// Slicing: only the Fish part of Carp is sent to MakeFishSwim()
Tuna marineFish;
MakeFishSwim(marineFish); // Slicing again
程序清单 26.2 使用基于深复制的智能指针将多态对象作为基类对象进行传递
0: template <typename T>
1: class deepcopy_smart_ptr
2: {
3: private:
4: T* object;
5: public:
6: //... other functions
7:
8: // copy constructor of the deepcopy pointer
9: deepcopy_smart_ptr (const deepcopy_smart_ptr& source)
10: {
11: // Clone() is virtual: ensures deep copy of Derived class object
12: object = source->Clone ();
13: }
14:
15: // copy assignment operator
16: deepcopy_smart_ptr& operator= (const deepcopy_smart_ptr& source)
17: {
18: if (object)
19: delete object;
20:
21: object = source->Clone ();
22: }
23: };
分析:
可以看到,deepcopy_smart_ptr 在第 9~13 行实现了一个复制构造函数,使得能够通过函数 Clone( )函数对多态对象进行深复制—类必须实现函数 Clone( )。另外,它还实现了复制赋值运算符,如第16~22 行所示。为了简单起见,这里假设基类 Fish 实现的虚函数为 Clone()。通常,实现深复制模型的智能指针通过模板参数或函数对象提供该函数。
下面是 deepcopy_smart_ptr 的一种用法:
deepcopy_smart_ptr<Carp> freshWaterFish(new Carp);
MakeFishSwim (freshWaterFish); // Carp will not be 'sliced'
构造函数实现的深复制将发挥作用,确保传递的对象不会出现切除问题—虽然从语法上说,目标函数 MakeFishSwim( )只要求基类部分。
26.3.2 写时复制机制
写时复制机制(Copy on Write,COW)试图对深复制智能指针的性能进行优化,它共享指针,直到首次写入对象。首次调用非 const 函数时,COW 指针通常为该非 const 函数操作的对象创建一个副本,而其他指针实例仍共享源对象。
COW 深受很多程序员的喜欢。实现 const 和非 const 版本的运算符*和->,是实现 COW 指针功能的关键。非 const 版本用于创建副本。
重要的是,选择 COW 指针时,在使用这样的实现前务必理解其实现细节。否则,复制时将出现复制得太少或太多的情况。
26.3.3 引用计数智能指针
引用计数是一种记录对象的用户数量的机制。当计数降低到零后,便将对象释放。因此,引用计数提供了一种优良的机制,使得可共享对象而无法对其进行复制。如果读者使用过微软的 COM 技术,肯定知道引用计数的概念。这种智能指针被复制时,需要将对象的引用计数加 1。至少有两种常用的方法来跟踪计数:
- 在对象中维护引用计数;
- 引用计数由共享对象中的指针类维护。
前者称为入侵式引用计数,因为需要修改对象以维护和递增引用计数,并将其提供给管理对象的智能指针。COM 采取的就是这种方法。后者是智能指针类将计数保存在自由存储区(如动态分配的整型),复制时复制构造函数将这个值加 1。因此,使用引用计数机制,程序员只应通过智能指针来处理对象。
在使用智能指针管理对象的同时让原始指针指向它是一种糟糕的做法,因为智能指针将在它维护的引用计数减为零时释放对象,而原始指针将继续指向已不属于当前应用程序的内存。
引用计数还有一个独特的问题:如果两个对象分别存储指向对方的指针,这两个对象将永远不会被释放,因为它们的生命周期依赖性导致其引用计数最少为 1。
###26.3.4 引用链接智能指针
引用链接智能指针不主动维护对象的引用计数,而只需知道计数什么时候变为零,以便能够释放对象。
之所以称为引用链接,是因为其实现是基于双向链表的。通过复制智能指针来创建新智能指针时,新指针将被插入到链表中。当智能指针离开作用域进而被销毁时,析构函数将把它从链表中删除。与引用计数的指针一样,引用链接指针也存在生命周期依赖性导致的问题。
26.3.5 破坏性复制
破坏性复制是这样一种机制,即在智能指针被复制时,将对象的所有权转交给目标指针并重置原来的指针。
destructive_copy_smartptr <SampleClass> smartPtr (new SampleClass ());
SomeFunc (smartPtr); // Ownership transferred to SomeFunc
// Don't use smartPtr in the caller any more!
虽然破坏性复制机制使用起来并不直观,但它有一个优点,即可确保任何时刻只有一个活动指针指向对象。因此,它非常适合从函数返回指针以及需要利用其“破坏性”的情形。
程序清单 26.3 是一种破坏性复制指针的实现,它没有采用推荐的标准 C++编程方法。
0: template <typename T>
1: class destructivecopy_ptr
2: {
3: private:
4: T* object;
5: public:
6: destructivecopy_ptr(T* input):object(input) {}
7: ~destructivecopy_ptr() { delete object; }
8:
9: // copy constructor
10: destructivecopy_ptr(destructivecopy_ptr& source)
11: {
12: // Take ownership on copy
13: object = source.object;
14:
15: // destroy source
16: source.object = 0;
17: }
18:
19: // copy assignment operator
20: destructivecopy_ptr& operator= (destructivecopy_ptr& source)
21: {
22: if (object != source.object)
23: {
24: delete object;
25: object = source.object;
26: source.object = 0;
27: }
28: }
29: };
30:
31: int main()
32: {
33: destructivecopy_ptr<int> num (new int);
34: destructivecopy_ptr<int> copy = num;
35:
36: // num is now invalid
37: return 0;
38: }
分析:
程序清单 26.3 演示了基于破坏性复制的智能指针实现。第 10~17 行和第 20~28 行分别是复制构造函数和赋值运算符。这些函数实际上使源指针在复制后失效,即复制构造函数在复制后将源指针设置为 NULL,这就是“破坏性复制”的由来。赋值运算符亦如此。因此在第 34 行被赋给另一个指针后,num 就不再有效,这种行为不符合赋值操作的目的。
26.3.6 使用 std::unique_ptr
std::unique_ptr 是 C++11 新增的,与 auto_ptr 稍有不同,因为它不允许复制和赋值。
【提示】要使用 std:unique_ptr,必须包含头文件:
#include
unique_ptr 是一种简单的智能指针,但其复制构造函数和赋值运算符被声明为私有的,因此不能复制它,即不能将其按值传递给函数,也不能将其赋给其他指针。
0: #include <iostream>
1: #include <memory> // include this to use std::unique_ptr
2: using namespace std;
3:
4: class Fish
5: {
6: public:
7: Fish() {cout << "Fish: Constructed!" << endl;}
8: ~Fish() {cout << "Fish: Destructed!" << endl;}
9:
10: void Swim() const {cout << "Fish swims in water" << endl;}
11: };
12:
13: void MakeFishSwim(const unique_ptr<Fish>& inFish)
14: {
15: inFish->Swim();
16: }
17:
18: int main()
19: {
20: unique_ptr<Fish> smartFish (new Fish);
21:
22: smartFish->Swim();
23: MakeFishSwim(smartFish); // OK, as MakeFishSwim accepts reference
24:
25: unique_ptr<Fish> copySmartFish;
26: // copySmartFish = smartFish; // error: operator= is private
27:
28: return 0;
29: }
输出:
Fish: Constructed!
Fish swims in water
Fish swims in water
Fish: Destructed!
如果删除第 13 行的引用符号&,将出现编译错误,因为复制构造函数是私有的。同样,不能像第 26 行那样将一个 unique_ptr 对象赋给另一个 unique_ptr 对象,因为复制赋值运算符是私有的。
unique_ptr 比 C++11 已摒弃的 auto_ptr 更安全,因为复制和赋值不会导致源智能指针对象无效。它在销毁时释放对象,可帮助您进行简单的内存管理。