物是人非事事休,欲语泪先流|

游客0721

园龄:2年粉丝:4关注:2

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容器

  1. array(静态数组)

  2. array的大小是固定的,不像其它的模板类,但array有begin()和end()成员函数,程序员可以array对象使用STL算法。

  3. forward_list(单向链表)

  4. unordered_map、unordered_multimap、unordered_set、unordered_multiset(哈希表)

十七、新的STL方法(成员函数)

  1. C++11新增了的方法cbegin()、cend()、crbegin()、crend(),这些方法将元素视为const。

  2. iterator emplace (iterator pos, …); // 在指定位置插入一个元素,…用于构造元素,返回指向插入元素的迭代器。

  3. 更重要的是,除了传统的拷贝构造函数和赋值函数,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 );

形参说明

  1. str:需要要转换的string字符串。

  2. pos:传出参数,存放从哪个字符开始无法继续解析的位置,例如:123a45, 传出的位置将为3。

  3. base:若base为0,则自动检测数值进制:若前缀为0,则为八进制,若前缀为0x或0X,则为十六进制,否则为十进制。

  4. 注意: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=(类名&& 源对象)

注意

  1. 对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。

  2. 如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。

  3. C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。

  4. 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

示例

#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提供了以下方案:

  1. 如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。

  2. 提供了模板函数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的资源。
}

二、线程资源的回收

  • 虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。

回收子线程的资源有两种方法

  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类也有同名的成员函数。

  1. 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);

该函数让线程休眠至指定时间点。(可实现定时任务)

  1. yield()

void yield() noexcept;

该函数让线程主动让出自己已经抢到的CPU时间片。

  1. 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 );
  1. 第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。

  2. 第二个参数是需要执行的函数fx。

  3. 后面的可变参数是传递给函数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类

主要成员函数

1condition_variable() 默认构造函数。
2condition_variable(const condition_variable &)=delete 禁止拷贝。
3)condition_variable& condition_variable::operator=(const condition_variable &)=delete 禁止赋值。
4notify_one() 通知一个等待的线程。
5notify_all() 通知全部等待的线程。
6wait(unique_lock<mutex> lock) 阻塞当前线程,直到通知到达。
7wait(unique_lock<mutex> lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。
8wait_for(unique_lock<mutex> lock,时间长度)
9wait_for(unique_lock<mutex> lock,时间长度,Pred pred)
10wait_until(unique_lock<mutex> lock,时间点)
11wait_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 中国大陆许可协议进行许可。

posted @   游客0721  阅读(176)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 優しい光 水谷広実
  2. 2 ひだまりの中で SONO MAKERS,鈴丸
  3. 3 白い吐息 MANYO
  4. 4 夏の子守歌 折戸伸治
優しい光 - 水谷広実
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.