C++基础教程(从入门到精通)
本文是一份全面的 C++ 基础教程,涵盖从环境搭建到高级特性的完整内容。适合初学者系统学习,也适合有经验的开发者查阅参考。
📚 目录
第一部分:入门基础
- 1. Windows开发环境的准备
- 2. Linux开发环境
- 3. 第一个C++程序
- 4. C++输出数据
- 5. 程序的注释
- 6. 使用变量
- 7. 使用常量
- 8. 标识符的命名
- 9. 输入数据
第二部分:运算符与表达式
- 10. 算术运算
- 11. 自增和自减
- 12. 赋值运算
- 13. C++11初始化赋值
- 14. 关系运算
- 15. 逻辑运算
- 16. 逗号运算
- 17. 运算的优先级
第三部分:流程控制
- 18-24. if语句与条件判断
- 25-30. 循环语句
- 24. switch语句
第四部分:函数
第五部分:数据类型深入
第六部分:指针与内存
- 50-60. 指针基础、const指针、函数指针等
第七部分:数组与字符串
第八部分:结构体与自定义类型
第九部分:函数高级特性
第十部分:面向对象编程
第十一部分:继承与多态
第十二部分:模板编程
第十三部分:STL标准模板库
第十四部分:文件操作
第十五部分:异常处理
第十六部分:C++11新特性
- 220-229. 新标准、lambda、右值引用、移动语义
第十七部分:多线程编程
- 240-244. 线程、互斥锁、条件变量、原子操作
第十八部分:高级主题
1、Windows开发环境的准备
1)下载Visual Studio 2022安装文件
从微软官方网站下载,网址:https://visualstudio.microsoft.com/zh-hans/
选中Community 2022,它是Visual Studio 2022官方免费社区版,无需注册,不需要破解和盗版。(请不要从国内的其它网站下载安装文件,有病毒和插件)
下载后的文件名是VisualStudioSetup.exe。
2)安装Visual Studio 2022
双击安装文件VisualStudioSetup.exe。
2、Linux开发环境
本课程适用于各种Linux发行版,建议采用CentOS7.0至CentOS7.9之间的版本,它是目前企业在实际项目开发中最广泛采用的Linux版本。
Linux基础知识的学习,请见https://space.bilibili.com/627875426/channel/seriesdetail?sid=720603
用root登录,执行以下命令,将安装gcc和g++编译器以及gdb调试器。
yum -y install gcc gcc-c++ gdb
几十秒即可安装完成。
3、第一个C++程序
VS2022太方便了,它可以帮我们生成一个最简单的C++程序。
1)用VS生成最简单的C++程序
鼠标双击Visual Studio 2002,运行它。
点击“创建新项目”。
选择“控制台应用”,再点“下一步”。
点击“创建”后将生成一个最简单的C++程序。
2)运行C++程序
选择“调试->开始执行“菜单,或快捷键Ctrl+F5运行程序。
3)示例
// 包含头文件。
- #include <iostream>
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- // 在控制台输出一首诗。
- std::cout << "\n\n 我是一只傻傻鸟\n";
- std::cout << " 生活美好如鲜花,不懂享受是傻瓜;\n";
- std::cout << " 傻呀傻呀傻呀傻,不如小鸟和乌鸦。\n";
- std::cout << " 芳草地啊美如画,谁要不去是傻瓜;\n";
- std::cout << " 我是一只傻傻鸟,独在枯枝丫上趴。\n";
- // std::cout 向控制台输出内容的指令,只能小写,不能用大写。
- // << 输出的运算符。
- // "" 字符串内容的边界符,半角。
- // Hello World! 输出字符串的内容,可以是中文、英文和任意符号,半角的双引号除外。
- // \n 输出一个换行。
- // ; C++语句结束的标志,半角。
- }
复制代码
4、C++输出数据
数据是信息的载体,写程序的目的就是为了处理数据。
1)数据的分类
数据有数字、字符和字符串三种类型。
- 数字:直接书写,如:100、18.52、0、9;
- 字符:用半角的单引号包含的一个符号,如:‘A’、‘Z’、‘0’、‘9’、‘~’、‘+’,汉字和全角的标点符号不是字符;
- 字符串:用半角的双引号包含的多个符号,如:“Hello World”、“我是一只傻鸟”、“西施”、“100”、“X”。
2)输出数据
- std::cout可以输出各种类型的数据;
- <<操作符可以拼接多个数据项;
- std::endl也可以用于换行;
- using namespace std;指定缺省的命名空间。
3)示例
- #include <iostream>
- // 指定缺省的命名空间。
- using namespace std;
- int main()
- {
- // 用多个数据项输出超女信息,姓名:西施;年龄:25;体重:48.5;性别:X
- // std::cout << "姓名:西施;年龄:25;体重:48.5;性别:X\n";
- cout << "姓名:" << "西施" << ";年龄:" << 25 << ";体重:" << 48.5
- << ";性别:" << 'X' << endl;
- }
复制代码
5、程序的注释
在程序中添加的说明文字,对代码的流程或功能加以解释,方便自己和其他的程序员阅读和理解代码。
编译器在编译源代码的时候,会忽略注释。
1)单行注释
用两根反斜线打头,一般放在代码的上方,或者一行语句的末尾。
注意:字符串内容中的两根反斜线是内容的一部分,不是注释。
2)多行注释
从/*开始,到*/结束,把一些内容夹住。
注意:a)字符串内容中的/*和*/是内容的一部分,不是注释;b)/*和*/可以出现在一行代码的中间。
3)注释的注释
单行注释可以注释多行注释,多行注释也可以注释单行注释,但是,不建议使用。
4)VS中的快捷键
添加注释:Ctrl+k+c
取消注释:Ctrl+k+u
5)示例
// 包含头文件。
- #include <iostream>
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- // 在控制台输出一首诗。
- cout << "\n\n 我是一只傻傻鸟\n";
- cout << " 生活美好如鲜花,不懂享受是傻瓜;\n";
- cout << " 傻呀傻呀傻呀傻,不如小鸟和乌鸦。\n";
- cout << " 芳草地啊美如画,谁要不去是傻瓜;\n";
- cout << " 我是一只傻傻鸟,独在枯枝丫上趴。\n";
- cout << "姓名:" << "西施" << /\*";年龄:" << 25 << \*/";体重:" << 48.5 << "。" << endl;
- /\*
- std::cout 向控制台输出内容的指令,只能小写,不能用大写。
- << 输出的运算符。
- "" 字符串内容的边界符,半角。
- Hello World! 输出字符串的内容,可以是中文、英文和任意符号,半角的双引号除外。
- \n 输出一个换行。
- ; C++语句结束的标志,半角。
- \*/
- }
复制代码
6、使用变量
变量是内存变量的简称,用于存放数据。
1)声明变量
变量在使用之前要先声明。
语法:数据类型 变量名
C++常用的数据类型有五种:整数(int)、浮点数(float和double)、字符(char)、字符串(string)和布尔(bool)。
变量在声明的时候可以赋初始值。
语法:数据类型 变量名=值
2)变量的赋值
语法:变量名=值
3)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- // 整数(int)、浮点数(float和double)、字符(char)、字符串(string)和布尔(bool)。
- // 超女基本信息:姓名、年龄、体重、性别、颜值。
- // 声明变量,存放超女基本信息的数据项。
- string name = "西施"; // 姓名。
- int age = 25; // 年龄。
- double weight = 48.6; // 体重(kg)。
- char sex = 'X'; // 性别:X-女;Y-男。
- bool yz = false; // 颜值:true-漂亮;false-不漂亮。
- cout << "姓名:" << name << ",年龄:" << age << ",体重:" << weight
- << ",性别:" << sex << ",颜值:" << yz << endl;
- name = "冰冰"; // 字符串有双引号包含。
- age = 23; // 整数直接书写。
- weight = 50.5; // 浮点数直接书写。
- sex = 'X'; // 字符用单引号包含。
- yz = true; // 布尔型取值只能是true和false,或1和0。
- cout << "姓名:" << name << ",年龄:" << age << ",体重:" << weight
- << ",性别:" << sex << ",颜值:" << yz << endl;
- }
复制代码
注意,布尔型变量输出的时候,如果值是true,将显示1,false显示0。
7、使用常量
常量是程序中固定不变的数据。
1)宏常量
一般在main函数的上面声明,用大写命名。
语法:#define 常量名 值
2)const修饰的变量
在程序的任何地方都可以声明。
语法:const 数据类型 常量名=值
3)常量的特点
程序中不允许改变常量的值,否则编译的时候会报错。
4)示例
- #include <iostream> // 包含头文件。
- #define MONTHS 12 // 一年中的月份数。
- #define PI 3.14159 // 圆周率。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- const int days = 7; // 一个星期中的天数。
- cout << "一年有" << MONTHS << "个月。" << endl;
- cout << "圆周率的值是:" << PI << endl;
- cout << "一个星期有" << days << "天。\n";
- }
复制代码
8、标识符的命名
1)C++命名规则
C++规定给标识符(变量、常量、函数、结构体、类等)命名时,必须遵守以下规则。
- 在名称中只能使用字母字符、数字和下划线;
- 名称的第一个字符不能是数字;
- 名称区分大写字符与小写字符;
- 不能将C++关键字用作名称;
- 以下划线和大写字母打头的名称被保留给编译器及其使用的资源使用,如果违反了这一规则,会导致行为的不确定性。
- C++对名称的长度没有限制,但有些平台可能有长度限制(64字符)。
C++提倡有一定含义的名称(望名知义)。
2)C++关键字
关键字也叫保留字,是C++预先保留的标识符。
每个C++关键字都有特殊的含义,用于声明类型、对象、函数、命名空间等,程序中不能声明与关键字同名的标识符。
| | | | |
|---|
| asm | do | if | return | typedef |
| auto | double | inline | short | typeid |
| bool | dynamic_cast | int | signed | typename |
| break | else | long | sizeof | union |
| case | enum | mutable | static | unsigned |
| catch | explicit | namespace | static_cast | using |
| char | export | new | struct | virtual |
| class | extern | operator | switch | void |
| const | false | private | template | volatile |
| const_cast | float | protected | this | wchar_t |
| continue | for | public | throw | while |
| default | friend | register | true | |
| delete | goto | reinterpret_cast | try | |
9、输入数据
程序输入数据的方式有多种。
- 从控制台的界面中输入(网页、PC桌面程序、APP程序);
- 从文件中读取;
- 从数据库中读取;
- 从网络中读取。
1)用std::cin输入数据
语法:std::cin>>变量名;
注意:
- a)布尔型变量的值在计算机内部用1(true)和0(false)存储;b)程序中可以书写true和false,也可以书写1和0,其它值将强制转换成1;c)用cin输入时可以填1和0,其它值也强制转换成1;d)用cout输出时只显示1和0,不显示true和false。
- 如果输入的数据与变量的数据类型不匹配,会导致行为的不确定性。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- string name; // 姓名。
- cout << "请输入超女的姓名:";
- cin >> name;
- cout << "输入的超女姓名是:" << name << endl;
- int age; // 年龄。
- cout << "请输入超女的年龄:";
- cin >> age;
- cout << "输入的超女年龄是:" << age << endl;
- double weight; // 体重(kg)。
- cout << "请输入超女的体重(kg):";
- cin >> weight;
- cout << "输入的超女体重是:" << weight << endl;
- char sex; // 性别:X-女;Y-男。
- cout << "请输入超女的性别(X-女;Y-男):";
- cin >> sex;
- cout << "输入的超女性别是:" << sex << endl;
- bool yz; // 颜值:true-漂亮;false-不漂亮。
- cout << "请问输入超女的颜值(1-漂亮;0-不漂亮):";
- cin >> yz;
- cout << "输入的超女颜值是:" << yz << endl;
- }
复制代码
10、算术运算
| |
|---|
| 运算符 | 描述 |
| + | 两个数相加。 |
| - | 一个数减另一个数。 |
| * | 两个数相乘。 |
| / | 分子除以分母。 |
| % | 取模运算,整除后的余数。 |
注意:
- 整数进行除法运算时,如果分母为0,程序将异常退出;
- 浮点数进行除法运算时,如果分母为0.0,将得到inf(infinite,无穷大);
- 两个整数进行除法运算时,将舍去小数部分,得到一个整数;
- 整数与浮点数进行除法运算时,得到的结果是浮点数;
- 进行除法运算时,在整数前面加(float)或(double)可以将整数转换为float或double类型;
- 取模运算只能用于整数(分母也不能为0)。
11、自增和自减
| |
|---|
| 运算符 | 描述 |
| ++变量名 | 先把变量的值加1,然后再执行表达式。 |
| 变量名++ | 先执行表达式,再把变量的值加1。 |
| –变量名 | 先把变量的值减1,然后再执行表达式。 |
| 变量名– | 先执行表达式,再把变量的减1。 |
12、赋值运算
1)赋值运算
| | |
|---|
| 运算符 | 示例 | 描述 |
| = | c = a + b; 将把a + b的值赋给c。 | 把右边操作数的值赋给左边操作数。 |
| += | c += a; 相当于 c = c + a; | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数。 |
| -= | c -= a; 相当于 c = c - a; | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数。 |
| *= | c *= a; 相当于 c = c * a; | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数。 |
| /= | c /= a; 相当于 c = c / a; | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数。 |
| %= | c %= a; 相当于 c = c % a; | 求余数且赋值运算符,求两个操作数的模赋值给左边操作数,浮点数不适用取余数。 |
注意:
- 字符串(string)只能使用等号(=)赋值,不能使用其它的赋值运算符;
- 浮点数不能使用%=运算符;
- 等号(=)赋值运算符可以连续使用;
- 如果赋值运算符两边操作数的数据类型不同,C++将自动转换数据类型,可能会损失精度,也可能超出取值范围,如果转换不成功,编译时会报错。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- int a = 10;
- int c = 20;
- // c += a; // c=c+a;
- // c -= a; // c=c-a;
- // c \*= a; // c = c \* a;
- // c /= a; // c = c / a;
- //c %= a; // c = c % a;
- // cout << "c=" << c << endl;
- a = c = 30;
- cout << "a=" << a<<endl;
- cout << "c=" << c << endl;
- }
复制代码
13、C++11初始化赋值
1)C++和C++11初始化赋值
int a=(15); // 声明变量a,初始化值为15。
int b(20); // 声明变量b,初始化值为20。
- 把值写在花括号中,等于号也可以省略(C++11标准),统一初始化列表。
int a={15}; // 声明变量a,初始化值为15。
int b{20}; // 声明变量b,初始化值为20。
注意:在Linux平台下,编译需要加-std=c++11参数。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- int a = 10;
- int b = (10);
- int c(10);
- int e = {10};
- int f{ 10 };
- cout << "a=" << a << endl;
- cout << "b=" << b << endl;
- cout << "c=" << c << endl;
- cout << "e=" << e << endl;
- cout << "f=" << f << endl;
- }
复制代码
14、关系运算
用于比较两个表达式的值,运算的结果为1-true和0-false。
1)关系运算
| | |
|---|
| 关系 | 数学的表示 | C++的表示 |
| 等于 | = | == |
| 不等于 | ≠ | != |
| 小于 | < | < |
| 小于等于 | ≤ | <= |
| 大于 | > | > |
| 大于等于 | ≥ | >= |
注意:
- 关系运算符的两边可以是数值,也可以是表达式;
- 用std::cout输出关系运算表达式时,关系表达式要加括号;
- 关系运算也适用于字符串(string),字符串常用的关系运算是==和!=,其它的基本上不用。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- int a = 7;
- int b = 7;
- bool result;
- result = a <= b-3; // a和b先做比较(关系)运算,然后再赋值给result。
- cout << "result=" << result << endl; // 关系运算表达式需要括号,算术运算表达式可以不用括号。
- string str1 = "西施";
- string str2 = "西瓜";
- cout << (str1 ==> str2) << endl;
- }
复制代码
15、逻辑运算
1)逻辑运算
根据给定的逻辑因子(表达式或值)返回一个新的逻辑因子。
| | | |
|---|
| 运算符 | 术语 | 示例 | 结果 |
| && | 逻辑与 | a&&b; | 如果a和b都为真,则结果为真,否则为假。 |
| | | 逻辑或 |
| ! | 逻辑非(反) | !a; | 如果a为假,则!a为真; 如果a为真,则!a为假。 |
注意:
- 逻辑运算符的两边可以是数值,也可以是表达式;
- 用std::cout输出逻辑运算表达式时,逻辑表达式要加括号;
- 在实际开发中,逻辑运算的多重组合是重点。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- bool a = false;
- bool b = true;
- // bool c = a && b; // 如果a和b都为真,则结果为真,否则为假。
- // bool c = a || b; // 如果a和b中有一个为真,则结果为真,二者都为假时,结果为假。
- bool c = !a; // 如果a为假,则!a为真; 如果a为真,则!a为假。
- // cout << "c=" << c << endl;
- // cout << "a&&b=" << (a && b) << endl;
- // cout << "a||b=" << (a || b) << endl;
- cout << "!a=" << c << endl;
- }
- ///////////////////////////////////////////////////////////////////////////
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- // 超女必须满足四个条件:年龄25岁以下,身高165以上、体重50公重以下,颜值要漂亮。
- int age = 23;
- int height = 168;
- double weight = 48.8;
- bool yz = true;
- cout << "result="<<(age<25&&height>165&&weight<50&&yz==true) << endl;
- }
复制代码
16、逗号运算
1)逗号运算
把一行语句中的多个表达式连接起来,程序将从左到右执行表达式。
语法:表达式一,表达式二,……,表达式n;
逗号运算常用于声明多个变量。
int a,b; // 声明变量a和b。
int a=10,b=20; // 声明变量a和b并初始化。
也可以用于其它语句中,但是,逗号运算符是所有运算符中级别最低的,以下两个表达式的效果是不同的。
int a,b;
b=a=2,a*2;
b=(a=2,a*2);
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- /\*int a =10, b=20;
- cout << "a=" << a << endl;
- cout << "b=" << b << endl;\*/
- int a, b;
- //b = a = 2, a \* 2;
- b = (a = 2, a \* 2);
- cout << "a=" << a << endl;
- cout << "b=" << b << endl;
- }
复制代码
17、运算的优先级
1)运算的优先级
一个表达式可以包含多个运算符,运算符的优先级决定了表达式各部分的执行顺序。
例如,按照运算规则, *的优先级比+高,所以的b * c将先执行:
a + b * c;
如果想让a + b先执行,则必须使用括号:
(a + b) * c;
如果一个表达式中操作符的优先级相同,那么它们的结合律(associativity)决定了它们的执行顺序(从左到右或从右到左)。例如,算术运算的组合方式是从左到右,赋值运算则是从右到左。如下:
| | |
|---|
| 表达式 | 结合律 | 组合方式 |
| a/b%c | 从左到右 | (a/b)%c |
| a=b=c | 从右到左 | a=(b=c) |
下表是全部运算符的优先级和结合律:
| | | | |
|---|
| 优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 |
| 1 | [] | 下标 | 地址[表达式] | 左到右 |
| () | 圆括号 | (表达式)/函数名(形参表) | | |
| . | 成员选择(对象) | 对象.成员名 | | |
| -> | 成员选择(指针) | 对象指针->成员名 | | |
| 2 | - | 负号运算符 | -表达式 | 右到左 |
| (类型) | 强制类型转换 | (数据类型)表达式 | | |
| ++ | 前置自增运算符 | ++变量名 | | |
| ++ | 后置自增运算符 | 变量名++ | | |
| – | 前置自减运算符 | –变量名 | | |
| – | 后置自减运算符 | 变量名– | | |
| * | 取值运算符 | *指针变量 | | |
| & | 取地址运算符 | &变量名 | | |
| ! | 逻辑非运算符 | !表达式 | | |
| ~ | 按位取反运算符 | ~表达式 | | |
| sizeof | 长度运算符 | sizeof(表达式) | | |
| 3 | / | 除 | 表达式/表达式 | 左到右 |
| * | 乘 | 表达式*表达式 | | |
| % | 余数(取模) | 整型表达式/整型表达式 | | |
| 4 | + | 加 | 表达式+表达式 | 左到右 |
| - | 减 | 表达式-表达式 | | |
| 5 | << | 左移 | 变量 | 左到右 |
| >> | 右移 | 变量>>表达式 | | |
| 6 | > | 大于 | 表达式>表达式 | 左到右 |
| >= | 大于等于 | 表达式>=表达式 | | |
| < | 小于 | 表达式 | | |
| <= | 小于等于 | 表达式 | | |
| 7 | == | 等于 | 表达式==表达式 | 左到右 |
| != | 不等于 | 表达式!= 表达式 | | |
| 8 | & | 按位与 | 表达式&表达式 | 左到右 |
| 9 | ^ | 按位异或 | 表达式^表达式 | 左到右 |
| 10 | | | 按位或 | 表达式 |
| 11 | && | 逻辑与 | 表达式&&表达式 | 左到右 |
| 12 | | | | 逻辑或 |
| 13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 |
| 14 | = | 赋值运算符 | 变量=表达式 | 右到左 |
| /= | 除后赋值 | 变量/=表达式 | | |
| *= | 乘后赋值 | 变量*=表达式 | | |
| %= | 取模后赋值 | 变量%=表达式 | | |
| += | 加后赋值 | 变量+=表达式 | | |
| -= | 减后赋值 | 变量-=表达式 | | |
| <<= | 左移后赋值 | 变量 | | |
| >>= | 右移后赋值 | 变量>>=表达式 | | |
| &= | 按位与后赋值 | 变量&=表达式 | | |
| ^= | 按位异或后赋值 | 变量^=表达式 | | |
| = | 按位或后赋值 | 变量 | =表达式 |
| 15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 |
注意:
- 如果不确定运算符的优先级,可以加括号;
- 多用括号,让代码的可读性更好。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- int a, b, c;
- c = 10; // 赋值表达式的值为赋值符号右边的值。
- cout << "(c = 10)=" << (c = 10) << endl;
- a = b = c = 10; // 赋值运算的结合律是从右到左。
- }
- ///////////////////////////////////////////////////////////////////
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- // main函数,程序从这里开始执行,每个程序只能有一个main函数。
- int main()
- {
- int a, b, c, d;
- a = 4, b = 2, c = 5;
- d = a \* (b / c); // 算术运算的结合律是从左到右。
- cout << "d=" << d << endl;
- }
复制代码
18、if语句的基本结构
语法:
if (表达式)
{
// 表达式为真时执行的语句。
}
else
{
// 表达式为假时执行的语句。
}
注意:
- if (表达式)之间的空格可以不写。
- 表达式必须使用括号。
- 表达式可以是具体的值。
- 表达式一般是关系运算和逻辑运算表达式,也可以是赋值运算或其它的表达式。
- 不要把if(变量名==值)写成if(变量名=值)。
- 整个if语句可以没有else分支。
- if或else分支花括号内的代码如果只有一行,花括号可以不写;如果一行也没有,花括号一定要写;如果有多行,花括号一定要写,如果不写,除了第一行,其它的代码将不是if语句的一部分。
- if (表达式)和else后不能加分号,因为分号表示空语句。(C++用分号作为一条语句结束的标志。如果在C++程序中不小心多写了一个分号,那么该分号会被当成一条空语句。)
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- int main()
- {
- // C++用分号作为一条语句结束的标志。
- // 如果在C++程序中不小心多写了一个分号,那么该分号也许会被视作一个空语句。
- // 判断超女的颜值,如果漂亮,在控制台输出“晋级下一轮”,否则输出“冲关失败”。
- // 1)声明一个bool型变量,用于存放超女的颜值数据。
- bool yz;
- // 2)显示输入超女颜值的提示信息。
- cout << "请输入超女的颜值(1-漂亮;0-不漂亮):";
- // 3)输入超女的颜值数据,存放在变量中。
- cin >> yz;
- // 4)用if语句判断超女的颜值,如果漂亮,显示“晋级下一轮”,否则显示“冲关失败”。
- if (yz == true)
- {
- cout << "小姐姐很漂亮哟!" << endl;
- cout << "恭喜您,晋级下一轮!" << endl;
- }
- else
- {
- cout << "对不起,冲关失败!" << endl;
- cout << "推荐一个美容院,......" << endl;
- }
- }
复制代码
19、嵌套使用if语句
1)嵌套使用if语句
if语句可以嵌套使用,实现多级(层)的条件判断(最多127层)。
语法:
if (表达式一)
{
if (表达式二)
{
// 表达式二为真时执行的语句。
}
else
{
// 表达式二为假时执行的语句。
}
}
else
{
// 表达式一为假时执行的语句。
}
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- int main()
- {
- // 超女选秀:1)性别(X-女,Y-男);2)颜值(1-漂亮,0-不漂亮);3)身材(1-火辣,2-丰满,3-苗条)。
- // 声明三个变量,存放超女的性别、颜值和身材数据。
- char sex; // 性别(X-女,Y-男)。
- bool yz; // 颜值(1-漂亮,0-不漂亮)。
- int sc; // 身材(1-火辣,2-丰满,3-苗条)。
- // 显示“请输入超女的性别(X-女,Y-男):”的提示文字。
- cout << "请输入超女的性别(X-女,Y-男):";
- // 输入超女的性别,存放在变量中。
- cin >> sex;
- // 判断超女的性别,如果是女,流程继续,否则程序结束。
- if (sex == 'X')
- {
- // 显示“请输入超女的颜值(1-漂亮,0-不漂亮):”的提示文字。
- cout << "请输入超女的颜值(1-漂亮,0-不漂亮):";
- // 输入超女的颜值,存放在变量中。
- cin >> yz;
- // 判断超女的颜值,如果是漂亮,流程继续,否则程序结束。
- if (yz == true) // 漂亮。
- {
- // 显示“请输入超女的身材(1-火辣,2-丰满,3-苗条):”的提示文字。
- cout << "请输入超女的身材(1-火辣,2-丰满,3-苗条):";
- // 输入超女的身材,存放在变量中。
- cin >> sc;
- // 判断超女的身材,如果是火辣,显示“晋级成功”,如果是丰满,显示“待定”,程序结束。
- if (sc == 1) // 1-火辣。
- {
- cout << "晋级成功!" << endl;
- }
- else
- {
- if (sc == 2) // 2-丰满
- {
- cout << "待定!" << endl;
- }
- }
- }
- }
- }
复制代码
20、嵌套if语句的坑
嵌套使用if语句时候,会出现if与else的配对问题。
C++编译器处理该问题的原则是:else总是与前面最近if 配对。
如果要改变这种配对关系,可以使用花括号来确定新的配对关系。
if (sex == ‘X’)
if (yz == true)
cout << “sexX,yztrue\n”;
else
cout << “sexY,yztrue or false\n”;
以上代码中的else与第二个if配对,如果想让它与第一个if配对,代码如下:
if (sex == ‘X’)
{
if (yz == true)
cout << “sexX,yztrue\n”;
}
else
cout << “sexY,yztrue or false\n”;
21、多条件的if语句
1)多条件的if语句
语法:
if (表达式一)
{
// 表达式一为真时执行的语句。
}
else if (表达式二)
{
// 表达式二为真时执行的语句。
}
else if (表达式三)
{
// 表达式三为真时执行的语句。
}
……
else if (表达式n)
{
// 表达式n为真时执行的语句。
}
else
{
// 全部表达式都不为真时执行的语句。
}
注意:
- 多条件的if语句本质上是嵌套的if语句。
- 最多只能有127个条件分支。
- 最后一个else可以没有。
2)示例
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- int main()
- {
- // 超女的身材:1-火辣;2-丰满;3-苗条;4-强壮;5-肥胖;>5-未知 。
- // 声明存放超女身材数据的变量。
- int sc;
- // 显示“请输入身材的代码(1-火辣;2-丰满;3-苗条;4-强壮;5-肥胖;>5-未知):”的提示文字。
- cout << "请输入身材的代码(1-火辣;2-丰满;3-苗条;4-强壮;5-肥胖;其它表示未知):";
- // 输入超女身材的代码,存放在变量中。
- cin >> sc;
- // 用多条件的if语句,判断身材代码,显示身材的中文描述。
- if (sc == 1) cout << "火辣!\n";
- else if (sc == 2) cout << "丰满!\n";
- else if (sc == 3) cout << "苗条!\n";
- else if (sc == 4) cout << "强壮!\n";
- else if (sc == 5) cout << "肥胖!\n";
- else cout << "未知!\n";
- }
复制代码
22、if语句中的逻辑表达式
采用括号、对齐、空格、换行有助于更清晰的表达复杂的逻辑表达式。
示例:
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- int main()
- {
- // 超女的数据:年龄(16-50岁),身高(150cm-190cm),身材(火辣;丰满;苗条)
- // 颜值(漂亮;一般;歪瓜裂枣)。
- // 晋级的标准:年龄25-30岁,身高165cm-175cm,身材火辣,颜值漂亮或者一般。
- // 已准备好的超女数据。
- int age = 28;
- int height = 170;
- string sc = "火辣";
- string yz = "漂亮";
- if ( (age > 25 && age < 30 ) && // 年龄
- (height > 165 && height < 175 ) && // 身高
- (sc == "火辣" ) && // 身材
- (yz == "漂亮" || yz == "一般" ) ) // 颜值
- {
- cout << "晋级成功!\n";
- }
- }
复制代码
23、三目运算
三目运算也叫条件运算或三元运算,可以实现简单if语句的功能,但是书写更简洁。
语法: 表达式一 ? 表达式二 : 表达式三
先计算表达式一的值,如果为真,整个表达式的结果为表达式二的值,如果为假,整个表达式的结果为表达式三的值。
int a,b,c;
a=7;
b=6;
c=(a>b)?a:b;
等同于
if (a>b) c=a;
else c=b;
三目运算表达式可以嵌套使用,但是,过于复杂的三目运算表达式不方便理解。
比如判断year是不是闰年,是则返回1,不是返回0。
int year;
year=(year%1000)?(year%4000?1:0):(year%4==0?1:0)
- 、普通年能被4整除且不能被100整除的为闰年。
- 、世纪年能被400整除的是闰年。
- 、对于数值很大的年份,这年如果能整除3200,并且能整除172800则是闰年。如172800年是闰年,86400年不是闰年。
24、switch语句
switch也是一种选择结构的语句,可以代替简单的多条件的if语句。
语法:
switch (表达式)
{
case 值一:
语句一;
break;
case 值二:
语句二;
break;
…
case 值n:
语句n;
break;
default:
上述条件都不满足时执行的语句;
}
注意:
- case后面必须是整数和字符,或者是结果为整数和字符的表达式,但不能使用变量。
- default不是必须的,当没有default时,如果全部的case匹配失败,那么就什么都不执行。
- 每个分支不要漏写break;语句。
25、while循环语句
语法:
while (表达式)
{
语句块
}
先计算表达式的值,如果为真就执行语句块,执行完语句块后,回到循环首部再次计算表达式的值,如果为真又执行一次语句块……,这个过程会一直重复,直到表达式的值为假时不再执行语句块。
注意:
- 如果表达式的值永远为真,那么将进入死循环,所以在循环中应该有改变表达式的值的方法。
- 如果循环体中的语句块只有一行代码,大括号可以不书写。
- 有疑问先放一边,在以后的课程中,会介绍循环的各种使用方法。
示例:
- #include <iostream> // 包含头文件。
- using namespace std; // 指定缺省的命名空间。
- int main()
- ... [OUTPUT TRUNCATED - 1039 chars omitted out of 51039 total] ...
- cin >> sc;
- // 6)判断超女的身材,如果火辣,显示“晋级成功”。
- if (sc == true) cout << "晋级成功\n";
- }
- }
复制代码
27、for循环语句
语法:
for (语句一 ; 表达式 ; 语句二)
{
语句块
}
1)循环开始的时候,先执行语句一,在整个循环过程中语句一只会被执行一次。
2)计算表达式的值,如果为真,就执行
… [OUTPUT TRUNCATED - 265245 chars omitted out of 315245 total] …
d%H%M%S") << std::endl;
stringstream ss; // 创建stringstream对象ss,需要包含头文件。
ss << std::put_time(tm_now, “%Y-%m-%d %H:%M:%S”); // 把时间输出到对象ss中。
string timestr = ss.str(); // 把ss转换成string的对象。
cout << timestr << endl;
}
三、计时器
steady_clock类相当于秒表,操作系统只要启动就会进行时间的累加,常用于耗时的统计(精确到纳秒)。
- #include <iostream>
- #include <chrono>
- using namespace std;
- int main()
- {
- // 静态成员函数chrono::steady_clock::now()获取开始的时间点。
- auto start = chrono::steady_clock::now();
- // 执行一些代码,让它消耗一些时间。
- cout << "计时开始 ...... \n";
- for (int ii = 0; ii < 1000000; ii++) {
- // cout << "我是一只傻傻鸟。\n";
- }
- cout << "计时完成 ...... \n";
- // 静态成员函数chrono::steady_clock::now()获取结束的时间点。
- auto end = chrono::steady_clock::now();
- // 计算消耗的时间,单位是纳秒。
- auto dt = end - start;
- cout << "耗时: " << dt.count() << "纳秒("<<(double)dt.count()/(1000\*1000\*1000)<<"秒)";
- }
复制代码
240、C++11线程
在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。
一、创建线程
头文件:#include
线程类:std::thread
构造函数:
1)thread() noexcept;
默认构造函,构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
2)template< class Function, class… Args >
explicit thread(Function&& fx, Args&&… args );
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数。
3)thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
4)thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数:
thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。
注意:
- 先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。
- 线程的任务函数返回后,子线程将终止。
- 如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。
示例:
- #include <iostream>
- #include <thread> // 线程类头文件。
- #include <windows.h> // Sleep()函数需要这个头文件。
- using namespace std;
- // 普通函数。
- void func(int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- Sleep(1000); // 休眠1秒。
- }
- }
- // 仿函数。
- class mythread1
- {
- public:
- void operator()(int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- Sleep(1000); // 休眠1秒。
- }
- }
- };
- // 类中有静态成员函数。
- class mythread2
- {
- public:
- static void func(int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- Sleep(1000); // 休眠1秒。
- }
- }
- };
- // 类中有普通成员函数。
- class mythread3
- {
- public:
- void func(int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- Sleep(1000); // 休眠1秒。
- }
- }
- };
- int main()
- {
- // 用普通函数创建线程。
- //thread t1(func, 3, "我是一只傻傻鸟。");
- //thread t2(func, 8, "我有一只小小鸟。");
- // 用lambda函数创建线程。
- auto f = [](int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- Sleep(1000); // 休眠1秒。
- }
- };
- //thread t3(f, 3, "我是一只傻傻鸟。");
- // 用仿函数创建线程。
- //thread t4(mythread1(), 3, "我是一只傻傻鸟。");
- // 用类的静态成员函数创建线程。
- //thread t5(mythread2::func, 3, "我是一只傻傻鸟。");
- // 用类的普通成员函数创建线程。
- mythread3 myth; // 必须先创建类的对象,必须保证对象的生命周期比子线程要长。
- thread t6(&mythread3::func, &myth, 3, "我是一只傻傻鸟。"); // 第二个参数必须填对象的this指针,否则会拷贝对象。
- cout << "任务开始。\n";
- for (int ii = 0; ii < 10; ii++) {
- cout << "执行任务中......\n";
- Sleep(1000); // 假设执行任务需要时间。
- }
- cout << "任务完成。\n";
- //t1.join(); // 回收线程t1的资源。
- //t2.join(); // 回收线程t2的资源。
- //t3.join(); // 回收线程t3的资源。
- //t4.join(); // 回收线程t4的资源。
- //t5.join(); // 回收线程t5的资源。
- t6.join(); // 回收线程t6的资源。
- }
复制代码
二、线程资源的回收
虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。
回收子线程的资源有两种方法:
1)在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
2)在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。
示例:
- #include <iostream>
- #include <thread> // 线程类头文件。
- #include <windows.h> // Sleep()函数需要这个头文件。
- using namespace std;
- // 普通函数。
- void func(int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- Sleep(1000); // 休眠1秒。
- }
- }
- int main()
- {
- // 用普通函数创建线程。
- thread t1(func, 3, "我是一只傻傻鸟。");
- thread t2(func, 8, "我有一只小小鸟。");
- t1.detach(); t2.detach(); // 分离子线程。
- //cout << "任务开始。\n";
- //for (int ii = 0; ii < 12; ii++) {
- // cout << "执行任务中......\n";
- // Sleep(1000); // 假设执行任务需要时间。
- //}
- //cout << "任务完成。\n";
- //t1.join(); // 回收线程t1的资源。
- //t2.join(); // 回收线程t2的资源。
- Sleep(12000);
- }
复制代码
三、this_thread的全局函数
C++11提供了命名空间this_thread来表示当前线程,该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。
1)get_id()
thread::id get_id() noexcept;
该函数用于获取线程ID,thread类也有同名的成员函数。
2)sleep_for() VS Sleep(1000) Linux sleep(1)
template
void sleep_for (const chrono::duration& rel_time);
该函数让线程休眠一段时间。
3)sleep_until() 2022-01-01 12:30:35
template
void sleep_until (const chrono::time_point& abs_time);
该函数让线程休眠至指定时间点。(可实现定时任务)
4)yield()
void yield() noexcept;
该函数让线程主动让出自己已经抢到的CPU时间片。
5)thread类其它的成员函数
void swap(std::thread& other); // 交换两个线程对象。
static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。
The interpretation of this value is system- andimplementation- specific, and may not be exact, but just an approximation.
Note that this does not need to match the actualnumber of processors or cores available in the system: A system can supportmultiple threads per processing unit, or restrict the access to its resourcesto the program.
If this value is not computable or well defined,the function returns 0.
示例:
- #include <iostream>
- #include <thread> // 线程类头文件。
- using namespace std;
- // 普通函数。
- void func(int bh, const string& str) {
- cout << "子线程:" << this_thread::get_id() << endl;
- for (int ii = 1; ii <= 3; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
- }
- }
- int main()
- {
- // 用普通函数创建线程。
- thread t1(func, 3, "我是一只傻傻鸟。");
- thread t2(func, 8, "我有一只小小鸟。");
- cout << "主线程:" << this_thread::get_id() << endl;
- cout << "线程t1:" << t1.get_id() << endl;
- cout << "线程t2:" << t2.get_id() << endl;
- t1.join(); // 回收线程t1的资源。
- t2.join(); // 回收线程t2的资源。
- }
复制代码
四、call_once函数
在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。
在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次。
头文件:#include
template< class callable, class… Args >
void call_once( std::once_flag& flag, Function&& fx, Args&&… args );
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。
示例:
- #include <iostream>
- #include <thread> // 线程类头文件。
- #include <mutex> // std::once_flag和std::call_once()函数需要包含这个头文件。
- using namespace std;
- once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
- // 在线程中,打算只调用一次的函数。
- void once_func(const int bh, const string& str) {
- cout << "once_func() bh= " << bh << ", str=" << str << endl;
- }
- // 普通函数。
- void func(int bh, const string& str) {
- call_once(onceflag,once_func,0, "各位观众,我要开始表白了。");
- for (int ii = 1; ii <= 3; ii++)
- {
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
- }
- }
- int main()
- {
- // 用普通函数创建线程。
- thread t1(func, 3, "我是一只傻傻鸟。");
- thread t2(func, 8, "我有一只小小鸟。");
- t1.join(); // 回收线程t1的资源。
- t2.join(); // 回收线程t2的资源。
- }
复制代码
五、native_handle函数
C++11定义了线程标准,不同的平台和编译器在实现的时候,本质上都是对操作系统的线程库进行封装,会损失一部分功能。
为了弥补C++11线程库的不足,thread类提供了native_handle()成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。
示例:
- #include <iostream>
- #include <thread>
- #include <pthread.h> // Linux的pthread线程库头文件。
- using namespace std;
- void func() // 线程任务函数。
- {
- for (int ii=1;ii<=10;ii++)
- {
- cout << "ii=" << ii << endl;
- this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
- }
- }
- int main()
- {
- thread tt(func); // 创建线程。
- this_thread::sleep_for(chrono::seconds(5)); // 休眠5秒。
- pthread_t thid= tt.native_handle(); // 获取Linux操作系统原生的线程句柄。
- pthread_cancel(thid); // 取消线程。
- tt.join(); // 等待线程退出。
- }
复制代码
六、线程安全
示例:
- #include <iostream>
- #include <thread> // 线程类头文件。
- using namespace std;
- int aa = 0; // 定义全局变量。
- // 普通函数,把全局变量aa加1000000次。
- void func() {
- for (int ii = 1; ii <= 1000000; ii++)
- aa++;
- }
- int main()
- {
- // 用普通函数创建线程。
- thread t1(func); // 创建线程t1,把全局变量aa加1000000次。
- thread t2(func); // 创建线程t2,把全局变量aa加1000000次。
- t1.join(); // 回收线程t1的资源。
- t2.join(); // 回收线程t2的资源。
- cout << "aa=" << aa << endl; // 显示全局变量aa的值。
- }
复制代码
242、互斥锁
C++11提供了四种互斥锁:
- mutex:互斥锁。
- timed_mutex:带超时机制的互斥锁。
- recursive_mutex:递归互斥锁。
- recursive_timed_mutex:带超时机制的递归互斥锁。
包含头文件:#include
一、mutex类
1)加锁lock()
互斥锁有锁定和未锁定两种状态。
如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。
如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。
2)解锁unlock()
只有持有锁的线程才能解锁。
3)尝试加锁try_lock()
如果互斥锁是未锁定状态,则加锁成功,函数返回true。
如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)
示例:
- #include <iostream>
- #include <thread> // 线程类头文件。
- #include <mutex> // 互斥锁类的头文件。
- using namespace std;
- mutex mtx; // 创建互斥锁,保护共享资源cout对象。
- // 普通函数。
- void func(int bh, const string& str) {
- for (int ii = 1; ii <= 10; ii++)
- {
- mtx.lock(); // 申请加锁。
- cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
- mtx.unlock(); // 解锁。
- this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
- }
- }
- int main()
- {
- // 用普通函数创建线程。
- thread t1(func, 1, "我是一只傻傻鸟。");
- thread t2(func, 2, "我是一只傻傻鸟。");
- thread t3(func, 3, "我是一只傻傻鸟。");
- thread t4(func, 4, "我是一只傻傻鸟。");
- thread t5(func, 5, "我是一只傻傻鸟。");
- t1.join(); // 回收线程t1的资源。
- t2.join(); // 回收线程t2的资源。
- t3.join(); // 回收线程t3的资源。
- t4.join(); // 回收线程t4的资源。
- t5.join(); // 回收线程t5的资源。
- }
复制代码
二、timed_mutex类
增加了两个成员函数:
bool try_lock_for(时间长度);
bool try_lock_until(时间点);
三、recursive_mutex类
递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。
示例:
- #include <iostream>
- #include <mutex> // 互斥锁类的头文件。
- using namespace std;
- class AA
- {
- recursive_mutex m_mutex;
- public:
- void func1() {
- m_mutex.lock();
- cout << "调用了func1()\n";
- m_mutex.unlock();
- }
- void func2() {
- m_mutex.lock();
- cout << "调用了func2()\n";
- func1();
- m_mutex.unlock();
- }
- };
- int main()
- {
- AA aa;
- //aa.func1();
- aa.func2();
- }
复制代码
四、lock_guard类
lock_guard是模板类,可以简化互斥锁的使用,也更安全。
lock_guard的定义如下:
template
class lock_guard
{
explicit lock_guard(Mutex& mtx);
}
lock_guard在构造函数中加锁,在析构函数中解锁。
lock_guard采用了RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。
243、条件变量-生产消费者模型
条件变量是一种线程同步机制。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
C++11的条件变量提供了两个类:
condition_variable:只支持与普通mutex搭配,效率更高。
condition_variable_any:是一种通用的条件变量,可以与任意mutex搭配(包括用户自定义的锁类型)。
包含头文件:
一、condition_variable类
主要成员函数:
1)condition_variable() 默认构造函数。
2)condition_variable(const condition_variable &)=delete 禁止拷贝。
3)condition_variable& condition_variable::operator=(const condition_variable &)=delete 禁止赋值。
4)notify_one() 通知一个等待的线程。
5)notify_all() 通知全部等待的线程。
6)wait(unique_lock lock) 阻塞当前线程,直到通知到达。
7)wait(unique_lock lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。
8)wait_for(unique_lock lock,时间长度)
9)wait_for(unique_lock lock,时间长度,Pred pred)
10)wait_until(unique_lock lock,时间点)
11)wait_until(unique_lock lock,时间点,Pred pred)
二、unique_lock类
template class unique_lock是模板类,模板参数为互斥锁类型。
unique_lock和lock_guard都是管理锁的辅助类,都是RAII风格(在构造时获得锁,在析构时释放锁)。它们的区别在于:为了配合condition_variable,unique_lock还有lock()和unlock()成员函数。
示例1:
244、原子类型atomic
C++11提供了atomic模板类(结构体),用于支持原子类型,模板参数可以是bool、char、int、long、long long、指针类型(不支持浮点类型和自定义数据类型)。
原子操作由CPU指令提供支持,它的性能比锁和消息传递更高,并且,不需要程序员处理加锁和释放锁的问题,支持修改、读取、交换、比较并交换等操作。
头文件:#include
构造函数:
atomic() noexcept = default; // 默认构造函数。
atomic(T val) noexcept; // 转换函数。
atomic(const atomic&) = delete; // 禁用拷贝构造函数。
赋值函数:
atomic& operator=(const atomic&) = delete; // 禁用赋值函数。
常用函数:
void store(const T val) noexcept; // 把val的值存入原子变量。
T load() noexcept; // 读取原子变量的值。
T fetch_add(const T val) noexcept; // 把原子变量的值与val相加,返回原值。
T fetch_sub(const T val) noexcept; // 把原子变量的值减val,返回原值。
T exchange(const T val) noexcept; // 把val的值存入原子变量,返回原值。
T compare_exchange_strong(T &expect,const T val) noexcept; // 比较原子变量的值和预期值expect,如果当两个值相等,把val存储到原子变量中,函数返回true;如果当两个值不相等,用原子变量的值更新预期值,函数返回false。CAS指令。
bool is_lock_free(); // 查询某原子类型的操作是直接用CPU指令(返回true),还是编译器内部的锁(返回false)。
原子类型的别名:
![图片待上传]
注意:
- atomic模板类重载了整数操作的各种运算符。
- atomic模板类的模板参数支持指针,但不表示它所指向的对象是原子类型。
- 原子整型可以用作计数器,布尔型可以用作开关。
- CAS指令是实现无锁队列基础。
示例:
- #include <iostream>
- #include <atomic> // 原子类型的头文件。
- using namespace std;
- int main()
- {
- atomic<int> a = 3; // atomic(T val) noexcept; // 转换函数。
- cout << "a=" << a.load() << endl; // 读取原子变量a的值。输出:a=3
- a.store(8); // 把8存储到原子变量中。
- cout << "a=" << a.load() << endl; // 读取原子变量a的值。 输出:a=8
- int old; // 用于存放原值。
- old = a.fetch_add(5); // 把原子变量a的值与5相加,返回原值。
- cout << "old = " << old <<",a = " << a.load() << endl; // 输出:old=8,a=13
- old = a.fetch_sub(2); // 把原子变量a的值减2,返回原值。
- cout << "old = " << old << ",a = " << a.load() << endl; // 输出:old=13,a=11
- atomic<int> ii = 3; // 原子变量
- int expect = 4; // 期待值
- int val = 5; // 打算存入原子变量的值
- // 比较原子变量的值和预期值expect,
- // 如果当两个值相等,把val存储到原子变量中;
- // 如果当两个值不相等,用原子变量的值更新预期值。
- // 执行存储操作时返回true,否则返回false。
- bool bret = ii.compare_exchange_strong(expect, val);
- cout << "bret=" << bret << endl;
- cout << "ii=" << ii << endl;
- cout << "expect=" << expect << endl;
- }
复制代码
250、可调用对象
在C++中,可以像函数一样调用的有:普通函数、类的静态成员函数、仿函数、lambda函数、类的非静态成员函数、可被转换为函数的类的对象,统称可调用对象或函数对象。
可调用对象有类型,可以用指针存储它们的地址,可以被引用(类的成员函数除外)
一、普通函数
普通函数类型可以声明函数、定义函数指针和函数引用,但是,不能定义函数的实体。
示例:
- #include <iostream>
- using namespace std;
- using Fun = void (int, const string&); // 普通函数类型的别名。
- Fun show; // 声明普通函数。
- int main()
- {
- show(1, "我是一只傻傻鸟。"); // 直接调用普通函数。
- void(\*fp1)(int, const string&) = show; // 声明函数指针,指向普通函数。
- void(&fr1)(int, const string&) = show; // 声明函数引用,引用普通函数。
- fp1(2, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
- fr1(3, "我是一只傻傻鸟。"); // 用函数引用调用普通函数。
- Fun\* fp2 = show; // 声明函数指针,指向普通函数。
- Fun& fr2 = show; // 声明函数引用,引用普通函数。
- fp2(4, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
- fr2(5, "我是一只傻傻鸟。"); // 用函数引用调用普通函数。
- }
- // 定义普通函数
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- // 以下代码是错误的,不能用函数类型定义函数的实体。
- //Func show1 {
- // cout << "亲爱的" << bh << "," << message << endl;
- //}
复制代码
二、类的静态成员函数
类的静态成员函数和普通函数本质上是一样的,把普通函数放在类中而已。
示例:
- #include <iostream>
- using namespace std;
- using Fun = void (int, const string&); // 普通函数类型的别名。
- struct AA // 类中有静态成员函数。
- {
- static void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- int main()
- {
- AA::show(1, "我是一只傻傻鸟。"); // 直接调用静态成员函数。
- void(\*fp1)(int, const string&) = AA::show; // 用函数指针指向静态成员函数。
- void(&fr1)(int, const string&) = AA::show; // 引用静态成员函数。
- fp1(2, "我是一只傻傻鸟。"); // 用函数指针调用静态成员函数。
- fr1(3, "我是一只傻傻鸟。"); // 用函数引用调用静态成员函数。
- Fun\* fp2 = AA::show; // 用函数指针指向静态成员函数。
- Fun& fr2 = AA::show; // 引用静态成员函数。
- fp2(4, "我是一只傻傻鸟。"); // 用函数指针调用静态成员函数。
- fr2(5, "我是一只傻傻鸟。"); // 用函数引用调用静态成员函数。
- }
复制代码
三、仿函数
仿函数的本质是类,调用的代码像函数。
仿函数的类型就是类的类型。
示例:
- #include <iostream>
- using namespace std;
- struct BB // 仿函数。
- {
- void operator()(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- int main()
- {
- BB bb;
- bb(11, "我是一只傻傻鸟。"); // 用对象调用仿函数。
- BB()(12, "我是一只傻傻鸟。"); // 用匿名对象调用仿函数。
- BB& br = bb; // 引用函数
- br(13, "我是一只傻傻鸟。"); // 用对象的引用调用仿函数。
- }
复制代码
四、lambda函数
lambda函数的本质是仿函数,仿函数的本质是类。
- #include <iostream>
- using namespace std;
- int main()
- {
- // 创建lambda对象。
- auto lb = [](int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- };
- auto& lr = lb; // 引用lambda对象。
- lb(1, "我是一只傻傻鸟。"); // 用lambda对象调用仿函数。
- lr(2, "我是一只傻傻鸟。"); // 用lambda对象的引用调用仿函数。
- }
复制代码
五、类的非静态成员函数
类的非静态成员函数有地址,但是,只能通过类的对象才能调用它,所以,C++对它做了特别处理。
类的非静态成员函数只有指针类型,没有引用类型,不能引用。
示例:
- #include <iostream>
- using namespace std;
- struct CC // 类中有普通成员函数。
- {
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- int main()
- {
- CC cc;
- cc.show(14, "我是一只傻傻鸟。");
- void (CC::\* fp11)(int, const string&) = &CC::show; // 定义类的成员函数的指针。
- (cc.\*fp11)(15, "我是一只傻傻鸟。"); // 用类的成员函数的指针调用成员函数。
- using pFun = void (CC::\*)(int, const string&); // 类成员函数的指针类型。
- pFun fp12 = &CC::show; // 让类成员函数的指针指向类的成员函数的地址。
- (cc.\*fp12)(16, "我是一只傻傻鸟。"); // 用类成员函数的指针调用类的成员函数。
- }
复制代码
六、可被转换为函数指针的类对象
类可以重载类型转换运算符operator 数据类型() ,如果数据类型是函数指针或函数引用类型,那么该类实例也将成为可调用对象。
它的本质是类,调用的代码像函数。
在实际开发中,意义不大。
示例:
- #include <iostream>
- using namespace std;
- // 定义函数
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- struct DD // 可以被转换为函数指针的类。
- {
- using Fun = void (\*)(int, const string&);
- operator Fun() {
- return show; // 返回普通函数。
- }
- };
- int main()
- {
- DD dd;
- dd(17, "我是一只傻傻鸟。"); // 可以被转换为函数指针的类对象。
- }
复制代码
251、包装器function
std::function模板类是一个通用的可调用对象的包装器,用简单的、统一的方式处理可调用对象。
template
class function……
_Fty是可调用对象的类型,格式:返回类型(参数列表)。
包含头文件:#include
注意:
- 重载了bool运算符,用于判断是否包装了可调用对象。
- 如果std::function对象未包装可调用对象,使用std::function对象将抛出std::bad_function_call异常。
示例:
- #include <iostream>
- #include <functional>
- using namespace std;
- // 普通函数
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- struct AA // 类中有静态成员函数。
- {
- static void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- struct BB // 仿函数。
- {
- void operator()(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- struct CC // 类中有普通成员函数。
- {
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- struct DD // 可以被转换为普通函数指针的类。
- {
- using Fun = void (\*)(int, const string&); // 函数指针的别名。
- operator Fun() {
- return show; // 返回普通函数show的地址。
- }
- };
- int main()
- {
- using Fun = void(int, const string&); // 函数类型的别名。
- // 普通函数。
- void(\*fp1)(int, const string&) = show; // 声明函数指针,指向函数对象。
- fp1(1, "我是一只傻傻鸟。"); // 用函数指针调用普通函数。
- function<void(int, const string&)> fn1 = show; // 包装普通全局函数show。
- fn1(1, "我是一只傻傻鸟。"); // 用function对象调用普通全局函数show。
- // 类的静态成员函数。
- void(\*fp3)(int, const string&) = AA::show; // 用函数指针指向类的静态成员函数。
- fp3(2, "我是一只傻傻鸟。"); // 用函数指针调用类的静态成员函数。
- function<void(int, const string&)> fn3 = AA::show; // 包装类的静态成员函数。
- fn3(2, "我是一只傻傻鸟。"); // 用function对象调用类的静态成员函数。
- // 仿函数。
- BB bb;
- bb(3, "我是一只傻傻鸟。"); // 用仿函数对象调用仿函数。
- function<void(int, const string&)> fn4 = BB(); // 包装仿函数。
- fn4(3, "我是一只傻傻鸟。"); // 用function对象调用仿函数。
- // 创建lambda对象。
- auto lb = [](int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- };
- lb(4, "我是一只傻傻鸟。"); // 调用lambda函数。
- function<void(int, const string&)> fn5 = lb; // 包装lamba函数。
- fn5(4, "我是一只傻傻鸟。"); // 用function对象调用lamba函数。
- // 类的非静态成员函数。
- CC cc;
- void (CC:: \* fp11)(int, const string&) = &CC::show; // 定义类成员函数的指针。
- (cc.\*fp11)(5, "我是一只傻傻鸟。"); // 用类成员函数的指针调用类的成员函数。
- function<void(CC&,int, const string&)> fn11 = &CC::show; // 包装成员函数。
- fn11(cc,5, "我是一只傻傻鸟。"); // 用function对象调用成员函数。
- // 可以被转换为函数指针的类对象。
- DD dd;
- dd(6, "我是一只傻傻鸟。"); // 用可以被转换为函数指针的类对象调用普通函数。
- function<void(int, const string&)> fn12 = dd; // 包装可以被转换为函数指针的类。
- fn12(6, "我是一只傻傻鸟。"); // 用function对象调用它。
- function<void(int, const string&)> fx=dd;
- try {
- if (fx) fx(6, "我是一只傻傻鸟。");
- }
- catch (std::bad_function_call e) {
- cout << "抛出了std::bad_function_call异常。";
- }
- }
复制代码
252、适配器bind
std::bind()模板函数是一个通用的函数适配器(绑定器),它用一个可调用对象及其参数,生成一个新的可调用对象,以适应模板。
包含头文件:#include
函数原型:
template< class Fx, class… Args >
function<> bind (Fx&& fx, Args&…args);
Fx:需要绑定的可调用对象(可以是前两节课介绍的那六种,也可以是function对象)。
args:绑定参数列表,可以是左值、右值和参数占位符std::placeholders::_n,如果参数不是占位符,缺省为值传递,std:: ref(参数)则为引用传递。
std::bind()返回std::function的对象。
std::bind()的本质是仿函数。
示例一(bind的基本用法):
- #include <iostream>
- #include <functional>
- using namespace std;
- // 普通函数
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "号," << message << endl;
- }
- int main()
- {
- function<void(int, const string&)> fn1 = show;
- function<void(int, const string&)> fn2 = bind(show, placeholders::_1, placeholders::_2);
- fn1(1, "我是一只傻傻鸟。");
- fn2(1, "我是一只傻傻鸟。");
- function<void(const string&, int)> fn3 = bind(show, placeholders::_2, placeholders::_1);
- fn3("我是一只傻傻鸟。", 1);
- function<void(const string&)> fn4 = bind(show, 3, placeholders::_1);
- fn4("我是一只傻傻鸟。");
- function<void(int, const string&,int)> fn5 = bind(show, placeholders::_1, placeholders::_2);
- fn5(1, "我是一只傻傻鸟。", 88);
- }
- 示例二(绑定六种可调用对象):
- #include <iostream>
- #include <functional>
- using namespace std;
- // 普通函数
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- struct AA // 类中有静态成员函数。
- {
- static void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- struct BB // 仿函数。
- {
- void operator()(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- struct CC // 类中有普通成员函数。
- {
- void show(int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- }
- };
- struct DD // 可以被转换为普通函数指针的类。
- {
- using Fun = void (\*)(int, const string&); // 函数指针的别名。
- operator Fun() {
- return show; // 返回普通函数show的地址。
- }
- };
- int main()
- {
- // 普通函数。
- function<void(int, const string&)> fn1 = bind(show, placeholders::_1, placeholders::_2); // 绑定普通全局函数show。
- fn1(1, "我是一只傻傻鸟。"); // 用function对象调用普通全局函数show。
- // 类的静态成员函数。
- function<void(int, const string&)> fn3 = bind(AA::show, placeholders::_1, placeholders::_2); // 绑定类的静态成员函数。
- fn3(2, "我是一只傻傻鸟。"); // 用function对象调用类的静态成员函数。
- // 仿函数。
- function<void(int, const string&)> fn4 = bind(BB(), placeholders::_1, placeholders::_2); // 绑定仿函数。
- fn4(3, "我是一只傻傻鸟。"); // 用function对象调用仿函数。
- // 创建lambda对象。
- auto lb = [](int bh, const string& message) {
- cout << "亲爱的" << bh << "," << message << endl;
- };
- function<void(int, const string&)> fn5 = bind(lb, placeholders::_1, placeholders::_2); // 绑定lamba函数。
- fn5(4, "我是一只傻傻鸟。"); // 用function对象调用lamba函数。
- // 类的非静态成员函数。
- CC cc;
- //function<void(CC&, int, const string&)> fn11 = bind(&CC::show, placeholders::_1, placeholders::_2, placeholders::_3); // 绑定成员函数。
- //fn11(cc, 5, "我是一只傻傻鸟。"); // 用function对象调用成员函数。
- function<void(int, const string&)> fn11 = bind(&CC::show,&cc,placeholders::_1, placeholders::_2); // 绑定成员函数。
- fn11(5, "我是一只傻傻鸟。"); // 用function对象调用成员函数。
- // 可以被转换为函数指针的类对象。
- DD dd;
- function<void(int, const string&)> fn12 = bind(dd, placeholders::_1, placeholders::_2); // 绑定可以被转换为函数指针的类。
- fn12(6, "我是一只傻傻鸟。"); // 用function对象调用它。
- }
复制代码
253、可变函数和参数
写一个函数,函数的参数是函数对象及参数,功能和thread类的构造函数相同。
示例:
- #include <iostream>
- #include <thread>
- #include <functional>
- using namespace std;
- void show0() { // 普通函数。
- cout << "亲爱的,我是一只傻傻鸟。\n";
- }
- void show1(const string& message) { // 普通函数。
- cout << "亲爱的," << message << endl;
- }
- struct CC // 类中有普通成员函数。
- {
- void show2(int bh, const string& message) {
- cout << "亲爱的" << bh << "号," << message << endl;
- }
- };
- template<typename Fn, typename...Args>
- auto show(Fn&& fn, Args&&...args) -> decltype(bind(forward<Fn>(fn), forward<Args>(args)...))
- {
- cout << "表白前的准备工作......\n";
- auto f = bind(forward<Fn>(fn), forward<Args>(args)...);
- f();
- cout << "表白完成。\n";
- return f;
- }
- int main()
- {
- show(show0);
- show(show1,"我是一只傻傻鸟。");
- CC cc;
- auto f = show(&CC::show2,&cc, 3,"我是一只傻傻鸟。");
- f();
- //thread t1(show0);
- //thread t2(show1,"我是一只傻傻鸟。");
- //CC cc;
- //thread t3(&CC::show2,&cc, 3,"我是一只傻傻鸟。");
- //t1.join();
- //t2.join();
- //t3.join();
- }
复制代码
254、回调函数的实现
在消息队列和网络库的框架中,当接收到消息(报文)时,回调用户自定义的函数对象,把消息(报文)参数传给它,由它决定如何处理。
示例:
- #include <iostream>
- #include <string>
- #include <thread> // 线程类头文件。
- #include <mutex> // 互斥锁类的头文件。
- #include <deque> // deque容器的头文件。
- #include <queue> // queue容器的头文件。
- #include <condition_variable> // 条件变量的头文件。
- #include <functional>
- using namespace std;
- void show(const string& message) { // 处理业务的普通函数
- cout << "处理数据:" << message << endl;
- }
- struct BB { // 处理业务的类
- void show(const string& message) {
- cout << "处理表白数据:" << message << endl;
- }
- };
- class AA
- {
- mutex m_mutex; // 互斥锁。
- condition_variable m_cond; // 条件变量。
- queue<string, deque<string>> m_q; // 缓存队列,底层容器用deque。
- function<void(const string&)> m_callback; // 回调函数对象。
- public:
- // 注册回调函数,回调函数只有一个参数(消费者接收到的数据)。
- template<typename Fn, typename ...Args>
- void callback(Fn && fn, Args&&...args) {
- m_callback = bind(forward<Fn>(fn), forward<Args>(args)..., std::placeholders::_1); // 绑定回调函数。
- }
- void incache(int num) // 生产数据,num指定数据的个数。
- {
- lock_guard<mutex> lock(m_mutex); // 申请加锁。
- for (int ii = 0; ii < num; ii++)
- {
- static int bh = 1; // 超女编号。
- string message = to_string(bh++) + "号超女"; // 拼接出一个数据。
- m_q.push(message); // 把生产出来的数据入队。
- }
- //m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程。
- m_cond.notify_all(); // 唤醒全部被当前条件变量阻塞的线程。
- }
- void outcache() { // 消费者线程任务函数。
- while (true) {
- // 把互斥锁转换成unique_lock<mutex>,并申请加锁。
- unique_lock<mutex> lock(m_mutex);
- // 1)把互斥锁解开;2)阻塞,等待被唤醒;3)给互斥锁加锁。
- m_cond.wait(lock, [this] { return !m_q.empty(); });
- // 数据元素出队。
- string message = m_q.front(); m_q.pop();
- cout << "线程:" << this_thread::get_id() << "," << message << endl;
- lock.unlock(); // 手工解锁。
- // 处理出队的数据(把数据消费掉)。
- if (m_callback) m_callback(message); // 回调函数,把收到的数据传给它。
- }
- }
- };
- int main()
- {
- AA aa;
- // aa.callback(show); // 把普通函数show()注册为回调函数。
- BB bb;
- aa.callback(&BB::show, &bb); // 把类成员函数BB::show()注册为回调函数。
- thread t1(&AA::outcache, &aa); // 创建消费者线程t1。
- thread t2(&AA::outcache, &aa); // 创建消费者线程t2。
- thread t3(&AA::outcache, &aa); // 创建消费者线程t3。
- this_thread::sleep_for(chrono::seconds(2)); // 休眠2秒。
- aa.incache(2); // 生产2个数据。
- this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。
- aa.incache(5); // 生产5个数据。
- t1.join(); // 回收子线程的资源。
- t2.join();
- t3.join();
- }
复制代码
255、如何取代虚函数
C++虚函数在执行过程中会跳转两次(先查找对象的函数表,再次通过该函数表中的地址找到真正的执行地址),这样的话,CPU会跳转两次,而普通函数只跳转一次。
CPU每跳转一次,预取指令要作废很多,所以效率会很低。(百度)
为了管理的方便(基类指针可指向派生类对象和自动析构派生类),保留类之间的继承关系。
示例:
- #include <iostream> // 包含头文件。
- #include <functional>
- using namespace std;
- struct Hero { // 英雄基类
- //virtual void show() { cout << "英雄释放了技能。\n"; }
- function<void()> m_callback; // 用于绑定子类的成员函数。
- // 注册子类成员函数,子类成员函数没有参数。
- template<typename Fn, typename ...Args>
- void callback(Fn&& fn, Args&&...args) {
- m_callback = bind(forward<Fn>(fn), forward<Args>(args)...);
- }
- void show() { m_callback(); } // 调用子类的成员函数。
- };
- struct XS :public Hero { // 西施派生类
- void show() { cout << "西施释放了技能。\n"; }
- };
- struct HX :public Hero { // 韩信派生类
- void show() { cout << "韩信释放了技能。\n"; }
- };
- int main()
- {
- // 根据用户选择的英雄,施展技能。
- int id = 0; // 英雄的id。
- cout << "请输入英雄(1-西施;2-韩信。):";
- cin >> id;
- // 创建基类指针,将指向派生类对象,用基类指针调用派生类的成员函数。
- Hero\* ptr = nullptr;
- if (id == 1) { // 1-西施
- ptr = new XS;
- ptr->callback(&XS::show, static_cast<XS\*>(ptr)); // 注册子类成员函数。
- }
- else if (id == 2) { // 2-韩信
- ptr = new HX;
- ptr->callback(&HX::show, static_cast<HX\*>(ptr)); // 注册子类成员函数。
- }
- if (ptr != nullptr) {
- ptr->show(); // 调用子类的成员函数。
- delete ptr; // 释放派生类对象。
- }
- }
复制代码