C++11新标准(全)
来自视频课程学习笔记资料整理,有做删改,略处部分在其他c++章节中,记性不好记录查询使用,仅做参考
c++11标准(1)
一、long long类型
-
新增了类型long long和unsigned long long,以支持64位(或更宽)的整型。
-
在VS中,int和long都是4字节,long long是8字节。
-
在Linux中,int是4字节,long和long long是8字节。
二、char16_t和char32_t类型
新增了类型char16_t和char32_t,以支持16位和32位的字符。意义不大,好像没什么人用,连demo程序都找不到。
三、原始字面量
略。
四、统一的初始化(列表)
C++11丰富了大括号的使用范围,用大括号括起来的列表(统一的初始化列表)可以用于所有内置类型和用户自定义类型。
- 使用统一的初始化列表时,可以添加等号(=),也可以不添加:
int x={5}; double y{2.75}; short quar[5]{4,5,2,76,1};
- 统一的初始化列表也可以用于new表达式中:
int *ar=new int[4]{2,4,6,7};
- 创建对象时,也可以使用大括号(而不是圆括号)来调用构造函数:
class Girl { private: int m_bh; string m_name; public: Girl(int bh,string name) : m_bh(bh),m_name(name) {} }; Girl g1(3, "西施"); // C++98的风格。 Girl g2={5, "冰冰"}; // C++11的风格。 Girl g3{8, "幂幂"}; // C++11的风格。
STL容器提供了将initializer_list模板类作为参数的构造函数: vector<int> v1(10); // 把v1初始化为10个元素。 vector<int> v2{10}; // 把v2初始化为1个元素,这个元素的值是10。 vector<int> v2{3,5,8}; // 把v3初始化为3个元素,值分别是3、5、8。
- 头文件<initializer_list>提供了对模板类initializer_list的支持,这个类包含成员函数begin()和end()。除了用于构造函数外,还可以将initializer_list用于常规函数的参数:
#include <iostream> #include <initializer_list> double sum(std::initializer_list<double> il) { double total = 0; for (auto it = il.begin(); it != il.end(); it++) total = total + *it; return total; } int main() { // double total = sum( 3.14, 5.20, 8 ); // 错误,如果没有大括号,这是三个参数。 double total = sum({ 3.14, 5.20, 8 }); // 正确,有大括号,这是一个参数。 std::cout << "total=" << total << std::endl; }
五、自动推导类型auto
略。
六、decltype关键字
略。
七、函数后置返回类型
略
八、模板的别名
略
九、空指针nullptr
-
空指针是不会指向有效数据的指针。以前,C/C++用0表示空指针,这带来了一些问题,这样的话0既可以表示指针常量,又可以表示整型常量。
-
C++11新增了关键字nullptr,用于表示空指针;它是指针类型,不是整型类型。
-
为了向后兼容,C++11仍允许用0来表示空指针,因此表达式nullptr==0为true。
-
使用nullptr提供了更高的类型安全。例如,可以将0传递给形参为int的函数,但是,如果将nullptr传递给这样的函数,编译器将视为错误。
-
因此,出于清晰和安全考虑,请使用nullptr。
十、智能指针
略。
十一、异常规范方面的修改
略。
十二、强类型枚举(枚举类)
-
传统的C++枚举提供了一种创建常量的方式,但类型检查比较低级。还有,如果在同一作用域内定义的两个枚举,它们的成员不能同名。
-
针对枚举的缺陷,C++11 标准引入了枚举类,又称强类型枚举。
-
声明强类型枚举非常简单,只需要在enum后加上关键字 class。
例如∶
enum e1{ red, green }; enum class e2 { red, green, blue }; enum class e3 { red, green, blue, yellow };
-
使用强类型枚举时,要在枚举成员名前面加枚举名和::,以免发生名称冲突,如:e2::red,e3::blue
-
强类型枚举默认的类型为int,也可以显式地指定类型,具体做法是在枚举名后面加上:type,type可以是除wchar_t以外的任何整型。
例如:
enum class e2:char { red, green, blue };
十三、explicit关键字
- C++支持对象自动转换,但是,自动类型转换可能导致意外。为了解决这种问题,C++11引入了explicit关键字,用于关闭自动转换的特性。
十四、类内成员初始化
在类的定义中初始化成员变量。
class Girl { private: int m_bh=20; // 年龄。 string m_name="美女"; // 姓名。 char m_xb = 'X'; // 性别。 public: Girl(int bh, string name) : m_bh(bh), m_name(name) {} };
十五、基于范围的for循环
略
十六、新的STL容器
-
array(静态数组)
-
array的大小是固定的,不像其它的模板类,但array有begin()和end()成员函数,程序员可以array对象使用STL算法。
-
forward_list(单向链表)
-
unordered_map、unordered_multimap、unordered_set、unordered_multiset(哈希表)
十七、新的STL方法(成员函数)
-
C++11新增了的方法cbegin()、cend()、crbegin()、crend(),这些方法将元素视为const。
-
iterator emplace (iterator pos, …); // 在指定位置插入一个元素,…用于构造元素,返回指向插入元素的迭代器。
-
更重要的是,除了传统的拷贝构造函数和赋值函数,C++11新增了移动构造函数和移动赋值函数。
十八、摒弃export
- C++98新增了export关键字,C++11不再使用,但仍保留它作为关键字,供以后使用。
十九、嵌套模板的尖括号
-
为了避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开:
-
vector<list
> v1; // 两个>之间必须加空格。
C++11不再这样要求:
vector<list<int>> v2; // 两个>之间不必加空格。
二十、final关键字
-
final关键字用于限制某个类不能被继承,或者某个虚函数不能被重写。
-
final关键字放在类名或虚函数名的后面。
示例:
class AA { public: virtual void test() { cout << "AA class..."; } }; class BB : public AA { public: void test() final // 如果有其它类继承BB,test()方法将不允许重写。 { cout << "BB class..."; } }; class CC : public BB { public: void test() // 错误,BB类中的test()后面有final,不允许重写。 { cout << "CC class..."; } };
二十一、override关键字
-
在派生类中,把override放在成员函数的后面,表示重写基类的虚函数,提高代码的可读性。
-
在派生类中,如果某成员函数不是重写基类的虚函数,随意的加上override关键字,编译器会报错。
示例: class AA { public: virtual void test() { cout << "AA class..."; } }; class BB : public AA { public: void test() override { cout << "BB class..."; } };
二十二、数值类型和字符串之间的转换
-
传统方法用sprintf()和snprintf()函数把数值转换为char字符串;用atoi()、atol()、atof()把char字符串转换为数值。
-
C++11提供了新的方法,在数值类型和string字符串之间转换。
1、数值转换为字符串
使用to_string()函数可以将各种数值类型转换为string字符串类型,这是一个重载函数,在头文件
string to_string (int val); string to_string (long val); string to_string (long long val); string to_string (unsigned val); string to_string (unsigned long val); string to_string (unsigned long long val); string to_string (float val); string to_string (double val); string to_string (long double val);
2、字符转换为串数值
在C++中,数值类型包括整型和浮点型,针对于不同的数值类型提供了不同的函数在头文件
int stoi( const string& str, size_t* pos = nullptr, int base = 10 ); long stol( const string& str, size_t* pos = nullptr, int base = 10 ); long long stoll( const string& str, size_t* pos = nullptr, int base = 10 ); unsigned long stoul( const string& str, size_t* pos = nullptr, int base = 10 ); unsigned long long stoull( const string& str, size_t* pos = nullptr, int base = 10 ); float stof( const string& str, size_t* pos = nullptr ); double stod( const string& str, size_t* pos = nullptr ); long double stold( const string& str, size_t* pos = nullptr );
形参说明:
-
str:需要要转换的string字符串。
-
pos:传出参数,存放从哪个字符开始无法继续解析的位置,例如:123a45, 传出的位置将为3。
-
base:若base为0,则自动检测数值进制:若前缀为0,则为八进制,若前缀为0x或0X,则为十六进制,否则为十进制。
-
注意:string字符串转换为数值的函数可能会抛出异常,在《209、C++异常》中有详细介绍。
示例:
string str="123a45"; size_t pos; int val = stoi(str, &pos, 10); cout << "val=" << val << endl; // 输出123 cout << "pos=" << pos << endl; // 输出3
二十三、静态断言static_assert
略
二十四、常量表达式constexpr关键字
- const关键字从功能上来说有双重语义:只读变量和修饰常量。
示例:
void func(const int len1) { // len1是只读变量,不是常量。 int array1[len1]={0}; // VS会报错,Linux平台的数组长度支持变量,不会报错。 const int len2 = 8; int array2[len2]={0}; // 正确,len2是常量。 }
-
C++11标准为了解决const关键字的双重语义问题,保留了const表示“只读”的语义,而将“常量”的语义划分给了新添加的constexpr关键字。
-
所以,C++11 标准中,建议将const和constexpr的功能区分开,表达“只读”语义的场景用const,表达“常量”语义的场景用constexpr。
二十五、默认函数控制=default与=delete
在C++中自定义的类,编译器会默认生成一些成员函数:
-
无参构造函数
-
拷贝构造函数
-
拷贝赋值函数
-
移动构造函数
-
移动赋值函数
-
析构函数
-
=default表示启用默认函数。
-
=delete表示禁用默认函数。
示例:
#include <iostream> using namespace std; class Girl { private: int m_bh = 20; // 年龄。 string m_name = "美女"; // 姓名。 char m_xb = 'X'; // 性别。 public: Girl() = default; // 启用默认构造函数。 Girl(int bh, string name) : m_bh(bh), m_name(name) {} Girl(const Girl& g) = delete; // 删除拷贝构造函数。 void show() { cout << "bh=" << m_bh << ",m_name=" << m_name << endl; } }; int main() { Girl g1; g1.show(); // Girl g2 = g1; // 错误,拷贝构造函数已删除。 }
c++11((2)更常用)
一、委托构造和继承构造委托构造和继承构造
C++11标准新增了委托构造和继承构造两种方法,用于简化代码。
一、委托构造
在实际的开发中,为了满足不同的需求,一个类可能会重载多个构造函数。多个构造函数之间可能会有重复的代码。例如变量初始化,如果在每个构造函数中都写一遍,这样代码会显得臃肿。
委托构造就是在一个构造函数的初始化列表中调用另一个构造函数。
注意:
不要生成环状的构造过程。
一旦使用委托构造,就不能在初始化列表中初始化其它的成员变量。
示例:
#include <iostream> using namespace std; class AA { private: int m_a; int m_b; double m_c; public: // 有一个参数的构造函数,初始化m_c AA(double c) { m_c = c + 3; // 初始化m_c cout << " AA(double c)" << endl; } // 有两个参数的构造函数,初始化m_a和m_b AA(int a, int b) { m_a = a + 1; // 初始化m_a m_b = b + 2; // 初始化m_b cout << " AA(int a, int b)" << endl; } // 构造函数委托AA(int a, int b)初始化m_a和m_b AA(int a, int b, const string& str) : AA(a, b) { cout << "m_a=" << m_a << ",m_b=" << m_b << ",str=" << str << endl; } // 构造函数委托AA(double c)初始化m_c AA(double c, const string& str) : AA(c) { cout << "m_c=" << m_c << ",str=" << str << endl; } }; int main() { AA a1(10, 20, "我是一只傻傻鸟。"); AA a2(3.8, "我有一只小小鸟。"); }
二、继承构造
-
在C++11之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定。在《126、如何构造基类》中有详细介绍。
-
C++11推出了继承构造(Inheriting Constructor),在派生类中使用using来声明继承基类的构造函数。
示例:
#include <iostream> using namespace std; class AA // 基类。 { public: int m_a; int m_b; // 有一个参数的构造函数,初始化m_a AA(int a) : m_a(a) { cout << " AA(int a)" << endl; } // 有两个参数的构造函数,初始化m_a和m_b AA(int a, int b) : m_a(a), m_b(b) { cout << " AA(int a, int b)" << endl; } }; class BB :public AA // 派生类。 { public: double m_c; using AA::AA; // 使用基类的构造函数。 // 有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c BB(int a, int b, double c) : AA(a, b), m_c(c) { cout << " BB(int a, int b, double c)" << endl; } void show() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c << endl; } }; int main() { // 将使用基类有一个参数的构造函数,初始化m_a BB b1(10); b1.show(); // 将使用基类有两个参数的构造函数,初始化m_a和m_b BB b2(10,20); b2.show(); // 将使用派生类自己有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c BB b3(10,20,10.58); b3.show(); }
二、lambda函数
-
lambda函数是C++11标准新增的语法糖,也称为lambda表达式或匿名函数。
-
lambda函数的特点是:距离近、简洁、高效和功能强大。
示例:[](const int& no) -> void { cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; };
示例:
#include <iostream> #include <vector> #include <algorithm> using namespace std; // 表白函数。 void zsshow(const int & no) { cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; } // 表白仿函数。 class czs { public: void operator()(const int & no) { cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; } }; int main() { vector<int> vv = { 5,8,3 }; // 存放超女编号的容器。 // 第三个参数是普通函数。 for_each(vv.begin(), vv.end(), zsshow); // 第三个参数是仿函数。 for_each(vv.begin(), vv.end(), czs()); // 第三个参数是lambda表达式。 for_each(vv.begin(), vv.end(), [](const int& no) { cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; } ); }
一、参数列表
-
参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,()可以省略不写。
-
与普通函数的不同:
-
lambda函数不能有默认参数。
-
所有参数必须有参数名。
-
不支持可变参数。
二、返回类型
-
用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,编译器会根据函数体中的代码推断出来。
-
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。
三、函数体
- 类似于普通函数的函数体。
四、捕获列表
-
通过捕获列表,lambda函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,不能访问全局变量)。
-
捕获列表书写在[]中,与函数参数的传递类似,捕获方式可以是值和引用。
-
以下列出了不同的捕获列表的方式。
1.值捕获
-
与传递参数类似,采用值捕获的前提是变量可以拷贝。
-
与传递参数不同,变量的值是在lambda函数创建时拷贝,而不是调用时拷贝。
例如:
size_t v1 = 42; auto f = [ v1 ] { return v1; }; // 使用了值捕获,将v1拷贝到名为f的可调用对象。 v1 = 0; auto j = f(); // j为42,f保存了我们创建它是v1的拷贝。
-
由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值。
-
默认情况下,如果以传值方式捕获变量,则在lambda函数中不能修改变量的值。
2.引用捕获
- 和函数引用参数一样,引用变量的值在lambda函数体中改变时,将影响被引用的对象。
size_t v1 = 42; auto f = [ &v1 ] { return v1; }; // 引用捕获,将v1拷贝到名为f的可调用对象。 v1 = 0; auto j = f(); // j为0。
- 如果采用引用方式捕获变量,就必须保证被引用的对象在lambda执行的时候是存在的。
3.隐式捕获
-
除了显式列出我们希望使用的父作域的变量之外,还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
-
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。
int a = 123; auto f = [ = ] { cout << a << endl; }; //值捕获 f(); // 输出:123 auto f1 = [ & ] { cout << a++ << endl; }; //引用捕获 f1(); //输出:123(采用了后++) cout << a << endl; //输出 124
4.混合方式捕获
-
lambda函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。
-
混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获。
-
需要注意的是:显式捕获的变量必须使用和默认捕获不同的方式捕获。例如:
int i = 10; int j = 20; auto f1 = [ =, &i] () { return j + i; }; // 正确,默认值捕获,显式是引用捕获 auto f2 = [ =, i] () { return i + j; }; // 编译出错,默认值捕获,显式值捕获,冲突了 auto f3 = [ &, &i] () { return i +j; }; // 编译出错,默认引用捕获,显式引用捕获,冲突了
5.修改值捕获变量的值
-
在lambda函数中,如果以传值方式捕获变量,则函数体中不能修改该变量,否则会引发编译错误。
-
在lambda函数中,如果希望修改值捕获变量的值,可以加mutable选项,但是,在lambda函数的外部,变量的值不会被修改。
int a = 123; auto f = [a]()mutable { cout << ++a << endl; }; // 不会报错 cout << a << endl; // 输出:123 f(); // 输出:124 cout << a << endl; // 输出:123
6.异常说明
- lambda可以抛出异常,用throw(…)指示异常的类型,用noexcept指示不抛出任何异常。
五、lambda函数的本质
- 当我们编写了一个lambda函数之后,编译器将它翻译成一个类,该类中有一个重载了()的函数。
1.采用值捕获
- 采用值捕获时,lambda函数生成的类用捕获变量的值初始化自己的成员变量。
例如:
int a =10; int b = 20; auto addfun = [=] (const int c ) -> int { return a+c; }; int c = addfun(b); cout << c << endl; 等同于: class Myclass { int m_a; // 该成员变量对应通过值捕获的变量。 public: Myclass( int a ) : m_a(a){}; // 该形参对应捕获的变量。 // 重载了()运算符的函数,返回类型、形参和函数体都与lambda函数一致。 int operator()(const int c) const { return a + c; } };
- 默认情况下,由lambda函数生成的类是const成员函数,所以变量的值不能修改。如果加上mutable,相当于去掉const。这样上面的限制就能讲通了。
2.采用引用捕获
-
如果lambda函数采用引用捕获的方式,编译器直接引用就行了。
-
唯一需要注意的是,lambda函数执行时,程序必须保证引用的对象有效。
三、右值引用
一、左值、右值
-
在C++中,所有的值不是左值,就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。
-
还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
-
C++11扩展了右值的概念,将右值分为了纯右值和将亡值。
-
纯右值:a)非引用返回的临时变量;b)运算表达式产生的结果;c)字面常量(C风格字符串除外,它是地址)。
-
将亡值:与右值引用相关的表达式,例如:将要被移动的对象、T&&函数返回的值、std::move()的返回值、转换成T&&的类型的转换函数的返回值。
-
不懂纯右值和将亡值的区别其实没关系,统一看作右值即可,不影响使用。
示例:
class AA { int m_a; }; AA getTemp() { return AA(); } int ii = 3; // ii是左值,3是右值。 int jj = ii+8; // jj是左值,ii+8是右值。 AA aa = getTemp(); // aa是左值 ,getTemp()的返回值是右值(临时变量)。
二、左值引用、右值引用
-
C++98中的引用很常见,就是给变量取个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。
-
右值引用就是给右值取个名字。
-
语法:数据类型&& 变量名=右值;
示例
#include <iostream> using namespace std; class AA { public: int m_a=9; }; AA getTemp() { return AA(); } int main() { int&& a = 3; // 3是右值。 int b = 8; // b是左值。 int&& c = b + 5; // b+5是右值。 AA&& aa = getTemp(); // getTemp()的返回值是右值(临时变量)。 cout << "a=" << a << endl; cout << "c=" << c << endl; cout << "aa.m_a=" << aa.m_a << endl; }
-
getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。
-
引入右值引用的主要目的是实现移动语义。
-
左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。
-
但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
int a = 1; const int& ra = a; // a是非常量左值。 const int b = 1; const int& rb = b; // b是常量左值。 const int& rc = 1; // 1是右值。
总结一下,其中T是一个具体类型:
-
左值引用, 使用 T&, 只能绑定左值。
-
右值引用, 使用 T&&, 只能绑定右值。
-
已命名的右值引用是左值。
-
常量左值,使用 const T&, 既可以绑定左值又可以绑定右值。
四、移动语义
-
如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
-
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。
-
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
-
移动构造函数的语法:
-
类名(类名&& 源对象)
-
移动赋值函数的语法:
-
类名& operator=(类名&& 源对象)
注意:
-
对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
-
如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
-
C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
-
移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
示例:
#include <iostream> using namespace std; class AA { public: int* m_data = nullptr; // 数据成员,指向堆区资源的指针。 AA() = default; // 启用默认构造函数。 void alloc() { // 给数据成员m_data分配内存。 m_data = new int; // 分配内存。 memset(m_data, 0, sizeof(int)); // 初始化已分配的内存。 } AA(const AA& a) { // 拷贝构造函数。 cout << "调用了拷贝构造函数。\n"; // 显示自己被调用的日志。 if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。 memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。 } AA(AA&& a) { // 移动构造函数。 cout << "调用了移动构造函数。\n"; // 显示自己被调用的日志。 if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。 m_data = a.m_data; // 把资源从源对象中转移过来。 a.m_data = nullptr; // 把源对象中的指针置空。 } AA& operator=(const AA& a) { // 赋值函数。 cout << "调用了赋值函数。\n"; // 显示自己被调用的日志。 if (this == &a) return *this; // 避免自我赋值。 if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。 memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。 return *this; } AA& operator=(AA&& a) { // 移动赋值函数。 cout << "调用了移动赋值函数。\n"; // 显示自己被调用的日志。 if (this == &a) return *this; // 避免自我赋值。 if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。 m_data = a.m_data; // 把资源从源对象中转移过来。 a.m_data = nullptr; // 把源对象中的指针置空。 return *this; } ~AA() { // 析构函数。 if (m_data != nullptr) { delete m_data; m_data = nullptr; } } }; int main() { AA a1; // 创建对象a1。 a1.alloc(); // 分配堆区资源。 *a1.m_data = 3; // 给堆区内存赋值。 cout << "a1.m_data=" << *a1.m_data << endl; AA a2 = a1; // 将调用拷贝构造函数。 cout << "a2.m_data=" << *a2.m_data << endl; AA a3; a3 = a1; // 将调用赋值函数。 cout << "a3.m_data=" << *a3.m_data << endl; auto f = [] { AA aa; aa.alloc(); *aa.m_data = 8; return aa; }; // 返回AA类对象的lambda函数。 AA a4 = f(); // lambda函数返回临时对象,是右值,将调用移动构造函数。 cout << "a4.m_data=" << *a4.m_data << endl; AA a6; a6 = f(); // lambda函数返回临时对象,是右值,将调用移动赋值函数。 cout << "a6.m_data=" << *a6.m_data << endl; }
五、完美转发
-
在函数模板中,可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。
-
C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义还是移动语义。
-
为了支持完美转发,C++11提供了以下方案:
-
如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。
-
提供了模板函数std::forward
(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。
示例:
#include <iostream> using namespace std; void func1(int& ii) { // 如果参数是左值,调用此函数。 cout << "参数是左值=" << ii << endl; } void func1(int&& ii) { // 如果参数是右值,调用此函数。 cout << "参数是右值=" << ii << endl; } // 1)如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名, // 那么,函数既可以接受左值引用,又可以接受右值引用。 // 2)提供了模板函数std::forward<T>(参数) ,用于转发参数, // 如果参数是一个右值,转发之后仍是右值引用;如果 参数是一个左值,转发之后仍是左值引用。 template<typename TT> void func(TT&& ii) { func1(forward<TT>(ii)); } int main() { int ii = 3; func(ii); // 实参是左值。 func(8); // 实参是右值。 }
六、可变参数模板
- 可变参数模版是C++11新增的最强大的特性之一,它对参数进行了泛化,能支持任意个数、任意数据类型的参数。
示例:
#include <iostream> #include <thread> using namespace std; template <typename T> void show(T girl) // 向超女表白的函数,参数可能是超女编号,也可能是姓名,所以用T。 { cout << "亲爱的" << girl << ",我是一只傻傻鸟。\n"; } // 递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板相同。 void print() { cout << "递归终止。\n"; } // 展开参数包的递归函数模板。 template <typename T, typename ...Args> void print(T arg, Args... args) { //cout << "参数: " << arg << endl; // 显示本次展开的参数。 show(arg); // 把参数用于表白。 //cout << "还有" << sizeof...(args) << "个参数未展开。" << endl; // 显示未展开变参的个数。 print(args...); // 继续展开参数。 } template <typename...Args> void func(const string& str, Args...args) // 除了可变参数,还可以有其它常规参数。 { cout << str << endl; // 表白之前,喊句口号。 print(args...); // 展开可变参数包。 cout << "表白完成。\n"; } int main(void) { //print("金莲", 4, "西施"); //print("冰冰", 8, "西施", 3); func("我是绝世帅歌。", "冰冰", 8, "西施", 3); // "我是绝世帅歌。"不是可变参数,其它的都是。 }
七、时间操作chrono库
-
C++11提供了chrono模版库,实现了一系列时间相关的操作(时间长度、系统时间和计时器)。
-
头文件:#include
-
命名空间:std::chrono
一、时间长度
-
duration模板类用于表示一段时间(时间长度、时钟周期),如:1小时、8分钟、5秒。
-
duration的定义如下:
template<class Rep, class Period = std::ratio<1, 1>> class duration { …… };
- 为了方便使用,定义了一些常用的时间长度,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于std::chrono命名空间下,定义如下:
using hours = duration<Rep, std::ratio<3600>> // 小时 using minutes = duration<Rep, std::ratio<60>> // 分钟 using seconds = duration<Rep> // 秒 using milliseconds = duration<Rep, std::milli> // 毫秒 using microseconds = duration<Rep, std::micro> // 微秒 using nanoseconds = duration<Rep, std::nano> // 纳秒
注意:
- duration模板类重载了各种算术运算符,用于操作duration对象。
- duration模板类提供了count()方法,获取duration对象的值。
示例:
#include <iostream> #include <chrono> // chrono库的头文件。 using namespace std; int main() { chrono::hours t1(1); // 1小时 chrono::minutes t2(60); // 60分钟 chrono::seconds t3(60 * 60); // 60*60秒 chrono::milliseconds t4(60 * 60 * 1000); // 60*60*1000毫秒 chrono::microseconds t5(60 * 60 * 1000 * 1000); // 警告:整数溢出。 chrono::nanoseconds t6(60 * 60 * 1000 * 1000*1000); // 警告:整数溢出。 if (t1 == t2) cout << "t1==t2\n"; if (t1 == t3) cout << "t1==t3\n"; if (t1 == t4) cout << "t1==t4\n"; // 获取时钟周期的值,返回的是int整数。 cout << "t1=" << t1.count() << endl; cout << "t2=" << t2.count() << endl; cout << "t3=" << t3.count() << endl; cout << "t4=" << t4.count() << endl; chrono::seconds t7(1); // 1秒 chrono::milliseconds t8(1000); // 1000毫秒 chrono::microseconds t9(1000 * 1000); // 1000*1000微秒 chrono::nanoseconds t10(1000 * 1000 * 1000); // 1000*1000*1000纳秒 if (t7 == t8) cout << "t7==t8\n"; if (t7 == t9) cout << "t7==t9\n"; if (t7 == t10) cout << "t7==t10\n"; // 获取时钟周期的值。 cout << "t7=" << t7.count() << endl; cout << "t8=" << t8.count() << endl; cout << "t9=" << t9.count() << endl; cout << "t10=" << t10.count() << endl; }
二、系统时间
- system_clock类支持了对系统时钟的访问,提供了三个静态成员函数:
// 返回当前时间的时间点。 static std::chrono::time_point<std::chrono::system_clock> now() noexcept; // 将时间点time_point类型转换为std::time_t 类型。 static std::time_t to_time_t( const time_point& t ) noexcept; // 将std::time_t类型转换为时间点time_point类型。 static std::chrono::system_clock::time_point from_time_t( std::time_t t ) noexcept;
示例:
#define _CRT_SECURE_NO_WARNINGS // localtime()需要这个宏。 #include <iostream> #include <chrono> #include <iomanip> // put_time()函数需要包含的头文件。 #include <sstream> using namespace std; int main() { // 1)静态成员函数chrono::system_clock::now()用于获取系统时间。(C++时间) auto now = chrono::system_clock::now(); // 2)静态成员函数chrono::system_clock::to_time_t()把系统时间转换为time_t。(UTC时间) auto t_now = chrono::system_clock::to_time_t(now); // t_now = t_now + 24*60*60; // 把当前时间加1天。 // t_now = t_now + -1*60*60; // 把当前时间减1小时。 // t_now = t_now + 120; // 把当前时间加120秒。 // 3)std::localtime()函数把time_t转换成本地时间。(北京时) // localtime()不是线程安全的,VS用localtime_s()代替,Linux用localtime_r()代替。 auto tm_now = std::localtime(&t_now); // 4)格式化输出tm结构体中的成员。 std::cout << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S") << std::endl; std::cout << std::put_time(tm_now, "%Y-%m-%d") << std::endl; std::cout << std::put_time(tm_now, "%H:%M:%S") << std::endl; std::cout << std::put_time(tm_now, "%Y%m%d%H%M%S") << std::endl; stringstream ss; // 创建stringstream对象ss,需要包含<sstream>头文件。 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)<<"秒)"; }
八、C++11线程
-
在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。
-
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。
一、创建线程
-
头文件:#include
-
线程类:std::thread
构造函数:
thread() noexcept;
默认构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
template< class Function, class... Args > explicit thread(Function&& fx, Args&&... args );
-
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
-
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数。
thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数:
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的资源。 }
二、线程资源的回收
- 虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。
回收子线程的资源有两种方法:
-
在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
-
在主程序中,调用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类也有同名的成员函数。
- sleep_for() VS Sleep(1000) Linux sleep(1)
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
该函数让线程休眠一段时间。
1.sleep_until() 2022-01-01 12:30:35
template <class Clock, class Duration> void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
该函数让线程休眠至指定时间点。(可实现定时任务)
- yield()
void yield() noexcept;
该函数让线程主动让出自己已经抢到的CPU时间片。
- thread类其它的成员函数
void swap(std::thread& other); // 交换两个线程对象。 static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。
示例:
#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 <mutex> 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的值。 }
九、互斥锁
-
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 Mutex> class lock_guard { explicit lock_guard(Mutex& mtx); }
-
lock_guard在构造函数中加锁,在析构函数中解锁。
-
lock_guard采用了RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。
十、条件变量-生产消费者模型
- 条件变量是一种线程同步机制。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
C++11的条件变量提供了两个类:
-
condition_variable:只支持与普通mutex搭配,效率更高。
-
condition_variable_any:是一种通用的条件变量,可以与任意mutex搭配(包括用户自定义的锁类型)。
包含头文件:<condition_variable>
condition_variable类一、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<mutex> lock) 阻塞当前线程,直到通知到达。 7)wait(unique_lock<mutex> lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。 8)wait_for(unique_lock<mutex> lock,时间长度) 9)wait_for(unique_lock<mutex> lock,时间长度,Pred pred) 10)wait_until(unique_lock<mutex> lock,时间点) 11)wait_until(unique_lock<mutex> lock,时间点,Pred pred)
二、unique_lock类
-
template
class unique_lock是模板类,模板参数为互斥锁类型。 -
unique_lock和lock_guard都是管理锁的辅助类,都是RAII风格(在构造时获得锁,在析构时释放锁)。它们的区别在于:为了配合condition_variable,unique_lock还有lock()和unlock()成员函数。
示例1:
#include <iostream> #include <string> #include <thread> // 线程类头文件。 #include <mutex> // 互斥锁类的头文件。 #include <deque> // deque容器的头文件。 #include <queue> // queue容器的头文件。 #include <condition_variable> // 条件变量的头文件。 using namespace std; class AA { mutex m_mutex; // 互斥锁。 condition_variable m_cond; // 条件变量。 queue<string, deque<string>> m_q; // 缓存队列,底层容器用deque。 public: 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(); // 唤醒一个被当前条件变量阻塞的线程。 } void outcache() // 消费者线程任务函数。 { while (true) { string message; { // 把互斥锁转换成unique_lock<mutex>,并申请加锁。 unique_lock<mutex> lock(m_mutex); while (m_q.empty()) // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if m_cond.wait(lock); // 等待生产者的唤醒信号。 // 数据元素出队。 message = m_q.front(); m_q.pop(); } // 处理出队的数据(把数据消费掉)。 this_thread::sleep_for(chrono::milliseconds(1)); // 假设处理数据需要1毫秒。 cout << "线程:" << this_thread::get_id() << "," << message << endl; } } }; int main() { AA aa; 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(3); // 生产3个数据。 this_thread::sleep_for(chrono::seconds(3)); // 休眠3秒。 aa.incache(5); // 生产5个数据。 t1.join(); // 回收子线程的资源。 t2.join(); t3.join(); }
示例2:
#include <iostream> #include <string> #include <thread> // 线程类头文件。 #include <mutex> // 互斥锁类的头文件。 #include <deque> // deque容器的头文件。 #include <queue> // queue容器的头文件。 #include <condition_variable> // 条件变量的头文件。 using namespace std; class AA { mutex m_mutex; // 互斥锁。 condition_variable m_cond; // 条件变量。 queue<string, deque<string>> m_q; // 缓存队列,底层容器用deque。 public: 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); // 条件变量虚假唤醒:消费者线程被唤醒后,缓存队列中没有数据。 //while (m_q.empty()) // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if // m_cond.wait(lock); // 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(); // 手工解锁。 // 处理出队的数据(把数据消费掉)。 this_thread::sleep_for(chrono::milliseconds(1)); // 假设处理数据需要1毫秒。 } } }; int main() { AA aa; 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(); }
十一、原子类型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; }
十二、可调用对象
-
在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, "我是一只傻傻鸟。"); // 可以被转换为函数指针的类对象。 }
十三、包装器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异常。"; } }
十四、适配器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对象调用它。 }
十五、可变函数和参数
- 写一个函数,函数的参数是函数对象及参数,功能和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(); }
十六、如何取代虚函数
-
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; // 释放派生类对象。 } }
本文作者:游客0721
本文链接:https://www.cnblogs.com/Gal0721/p/17724097.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步