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类

主要成员函数

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; // 释放派生类对象。

}

}
posted @ 2023-09-23 11:40  游客0721  阅读(277)  评论(0)    收藏  举报