C++基础笔记

C++和C++11初始化赋值

int a=(15);  // 声明变量a,初始化值为15。
int b(20);   // 声明变量b,初始化值为20。

把值写在花括号中,等于号也可以省略(C++11标准),统一初始化列表。

int a={15};  // 声明变量a,初始化值为15。
int b{20};   // 声明变量b,初始化值为20。
注意:在Linux平台下,编译需要加-std=c++11参数。

作用域

作用域是指程序中变量存在(或生效)的区域,超过该区域变量就不能被访问。
变量分全局变量和局部变量两种,全局变量在整个程序中都可以访问,局部变量只能在函数或语句块的内部才能访问。

C++中定义变量的场景主要有五种:

1)在全部函数外面定义的是全局变量。
2)在头文件中定义的是全局变量。
3)在函数和语句块内部定义的是局部变量。
4)函数的参数是该函数的局部变量。
5)函数内部用static修饰的是静态局部变量。

1)全局变量

在整个程序生命周期内都是有效的,在定义位置之后的任意函数中都能访问。
全局变量在主程序退出时由系统收回内存空间。

2)局部变量

在函数或语句块内部的语句使用,在函数或语句块外部是不可用的。
局部变量在函数返回或语句块结束时由系统收回内存空间。

3)静态局部变量

用static修饰的局部变量生命周期和程序相同,并且只会被初始化一次。
其作用域为局部,当定义它的函数或语句块结束时,其作用域随之结束。
当程序想要使用全局变量的时候应该先考虑使用static(考虑到数据安全性)。

4)注意事项

  • 全局变量和静态局部变量自动初始化为0。
  • 局部变量不会自动初始化,其值是不确定的,程序中应该有初始化局部变量的代码,否则编译可能会报错(不同的编译器不一样)。
  • 局部变量和全局变量的名称可以相同,在某函数或语句块内部,如果局部变量名与全局变量名相同,就会屏蔽全局变量而使用局部变量,如果想使用全局变量,可以在变量名前加两个冒号(::)。
  • for循环初始化语句中定义的变量的作用域是for语句块。

进制

十进制是d表示,二进制是b,十六进制是h,在编写程序时是不能用的那只是为了方便书写

二进制

二进制由 0 和 1 两个数字组成,书写时必须以0b或0B(不区分大小写)开头。

以下是合法的二进制:
  int a = 0b101;      // 换算成十进制为 5
  int b = -0b110010;  // 换算成十进制为 -50
  int c = 0B100001;   // 换算成十进制为 33
以下是非法的二进制:
  int m = 101010;  // 无前缀 0B,相当于十进制
  int n = 0B410;    // 4不是有效的二进制数字

注意,C++标准并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字

八进制

八进制由 0~7 八个数字组成,书写时必须以0开头(注意是数字 0,不是字母 o)

以下是合法的八进制数:
  int a = 015;      // 换算成十进制为 13
  int b = -0101;    // 换算成十进制为 -65
  int c = 0177777;  // 换算成十进制为 65535
以下是非法的八进制:
  int m = 256;  // 无前缀 0,相当于十进制
  int n = 03A2;  // A不是有效的八进制数字
十六进制

十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,书写时必须以0x或0X(不区分大小写)开头。

以下是合法的十六进制:
  int a = 0X2A;   // 换算成十进制为 42
  int b = -0XA0;  // 换算成十进制为 -160
  int c = 0xffff;   // 换算成十进制为 65535
以下是非法的十六进制:
  int m = 5A;    // 没有前缀 0X,是一个无效数字
  int n = 0X3H;  // H不是有效的十六进制数字

C++11的原始字面量

原始字面量(值)可以直接表示字符串的实际含义,不需要转义和连接。

语法:R"(字符串的内容)"
R"xxx(字符串的内容)xxx"

string path = "C:\\Program Files\\Microsoft OneDrive\\tail\\nation";
string path1 = R"abcd(C:\Program Files\Microsoft OneDrive\tail\nation)abcd"; 
// 输出同样结果

字符串

C++风格字符串:
string 变量名="字符串的内容" ;
赋值:变量名="字符串的内容" ;
拼接:变量名=变量名+"字符串的内容一"+"字符串的内容一"+......+"字符串的内容n" ;
比较:支持==、!=、>和<关系运算符,常用的是==和!=。
C风格字符串:
char 变量名[]="字符串的内容" ;

C风格字符串的本质是字符数组,C++风格字符串的本质是类,它封装了C风格字符串。

C语言约定:如果字符型(char)数组的末尾包含了空字符\0(也就是0),那么该数组中的内容就是一个字符串。没有的话就是一个字符数组
因为字符串需要用0结尾,所以在声明字符数组的时候,要预留多一个字节用来存放0。

char name[11];                 // 可以存放10个字符,没有初始化,里面是垃圾值。
char name[11] = "hello";         // 初始内容为hello,系统会自动添加0。
char name[]   = { "hello" };      // 初始内容为hello,系统会自动添加0,数组长度是6。
char name[11] = { "hello" };      // 初始内容为hello,系统会自动添加0。
char name[11]   { "hello" };      // 初始内容为hello,系统会自动添加0。C++11标准。
char name[11] = { 0 };          // 把全部的元素初始化为0。

数据类型的别名

typedef

语法:typedef 原数据类型名 别名;

// 创建与平台无关的数据类型,提高程序的兼容性。
// 在VS中,short是两个字节,int是四个字节,long也是四个字节,long long是八个字节。
typedef short         int16_t;    // 16位的整数。
typedef int             int32_t;    // 32位的整数。
typedef long long int64_t;    // 64位的整数。
// 在Linux中,short是两个字节,int是四个字节,long也是八个字节,long long也是八个字节。
typedef short         int16_t;    // 16位的整数。
typedef int             int32_t;    // 32位的整数。
typedef long          int64_t;    // 64位的整数。

// 在程序源代码中,只使用别名int16_t、int32_t、int64_t,不使用原名。

using

语法:using 别名=原数据类型名

指针

指针也是变量,是变量就要占用内存空间。

32位系统不管是什么类型的指针,占用的内存都是4字节,寻址范围就是2的32次方,有4294967296个内存地址,4294967296个地址X8位空间=34359738368bit(位)/8=4294967296Byte/1024=4194304KB/1024=4096MB/1024=4G。这样就得出了32位系统最高支持4GB内存。

在64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节。

常量指针:指针指向可以改,指针指向的值不可以更改。

const 数据类型 *变量名

如果不希望在函数中修改结构体变量的值,可以对形参加const约束
void func(const st_girl* pst)

指针常量:指针指向不可以改,指针指向的值可以更改。

数据类型 * const 变量名

常指针常量:指针指向不可以改,指针指向的值不可以更改。

const 数据类型 * const 变量名;

记忆秘诀:*表示指针,指针在前先读指针;指针在前指针就不允许改变

C++内存模型

地址从低到高

  • 代码段:存储可执行程序的代码和常量(例如字符常量),此存储区不可修改。
  • 数据段:存储全局变量和静态变量。
  • 堆:存储动态开辟内存的变量。 堆向上增长,以升序分配内存地址
  • 栈:存储局部变量、函数参数和返回值。栈向下增长,以降序分配内存地址,一般只有8M(可以修改系统参数)
  • 内核空间:不直接与用户交互

new 数据类型(初始值); // C++11支持{}

野指针
就是指针指向的不是一个有效(合法)的地址。

出现野指针的情况:

  • 指针在定义的时候,如果没有进行初始化,它的值是不确定的(乱指一气)。
  • 如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已失效。
  • 指针指向的变量已超越变量的作用域(变量的内存空间已被系统回收),让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋给了指针。

规避方法:

  • 指针在定义的时候,如果没地方指,就初始化为nullptr。
  • 动态分配的内存被释放后,将其置为nullptr。
  • 函数不要返回局部变量的地址。

注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能会造成程序的崩溃。是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。

函数指针
#include <iostream> 
bool (*pfc)(int);
pfc是函数指针名,必须用括号,否则就成了返回指针的函数。
void func(int no, string str){}

int main()
{
	void (*pfunc)(int, string);           // 声明表白函数的函数指针。
	pfunc = func;                              // 对函数指针赋值,语法是函数指针名=函数名。
	pfunc(bh, message);                  // 用函数指针名调用函数。 C++
	(*pfunc)(bh, message);              // 用函数指针名调用函数。 C语言
}

函数指针作为参数

#include <iostream>          

void zs(int a){}

void ls(int a){}       

void show(void (*pf)(int),int b)
{
	pf(b);                                                              
}
int main()
{
	show(zs, 3);          
	show(ls, 4);       
}

结构体

  • 在C++中,struct关键字可以不写。
  • 可以在定义结构体的时候创建结构体变量。
  • 在C++11中,定义结构体的时候可以指定缺省值。
  • C++11可以不写等于号来创建结构体,如果大括号内未包含任何东西或只写一个0,全部的成员都将被设置为0。(
    struct 结构体名 结构体变量名={0}😉
  • 如果结构体中的指针指向的是动态分配的内存地址:对结构体用sizeof运算可能没有意义,对结构体用memset()函数可能会造成内存泄露。
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

struct st_pet                // 宠物结构体。
{
    char name[21];       // 宠物的名字。
    char type[21];         // 宠物的物种。
};

struct st_girl                // 超女基本信息结构体st_girl,存放了超女全部的数据项。
{
    char name[21];        // 姓名。
    int age;                     // 年龄。
    double weight;        // 体重(kg)。
    char sex;                   // 性别:X-女;Y-男。
    bool yz;                    // 颜值:true-漂亮;false-不漂亮。
    struct st_pet pet;     // 宠物。
};

int main()
{
    st_girl girl = { "西施",23,50.5,'X',true,{"宝宝","鸭子"} };
    cout << "姓名:" << girl.name << "的宠物是一只" << girl.pet.type << ",名字叫" << girl.pet.name << "。" << endl;

    girl = { "西瓜",25,51.5,'X',true,{"贝贝","天鹅"} };     // C++11标准的语法。
    cout << "姓名:" << girl.name << "的宠物是一只" << girl.pet.type << ",名字叫" << girl.pet.name << "。" << endl;
    
    girl.pet = {"小白","狗"};    // C++11标准的语法。
    cout << "姓名:" << girl.name << "的宠物是一只" << girl.pet.type << ",名字叫" << girl.pet.name << "。" << endl;
}

链表

如果结构体中有一个本结构体的指针,它就是链表。

struct st_girl
{
	int no;
	string name;
	struct st_girl *next;
};

引用

声明/创建引用的语法:数据类型 &引用名=原变量名;

int a = 3;          // 声明普通的整型变量。
int& ra = a;      // 创建引用ra,ra是a的别名。
  • 引用变量是C++新增的复合类型。
  • 引用是已定义的变量的别名。
  • 引用的主要用途是用作函数的形参和返回值。
  • 引用的数据类型要与原变量名的数据类型相同。
  • 引用名和原变量名可以互换,它们值和内存单元是相同的。
  • 必须在声明引用的时候初始化,初始化后不可改变。
  • C和C++用&符号来指示/取变量的地址,C++给&符号赋予了另一种含义。
  • 引用是指针常量的伪装。
    引用是编译器提供的一个有用且安全的工具,去除了指针的一些缺点,禁止了部分不安全的操作.
  • 引用的底层机制实际上是和指针一样的。不要相信有别名,不要认为引用可以节省一个指针的空间,因为这一切不会发生,编译器还是会把引用解释为指针。
    引用和指针本质上没有区别。
引用用于函数的参数

引用的本质是指针,传递的是变量的地址,在函数中,修改形参会影响实参。

  • 传引用的代码更简洁。
  • 传引用不必使用二级指针。
  • 引用的属性和特别之处。
void func1(int** p)      // 传地址,实参是指针的地址,形参是二级指针。
{
	*p = new int(3);       // p是二级指针,存放指针的地址。
	cout << "func1内存的地址是:" << *p << ",内存中的值是:" << **p << endl;
}

void func2(int*& p)     // 传引用,实参是指针,形参是指针的别名。
{
	p = new int(3);         // p是指针的别名。
	cout << "func2内存的地址是:" << p << ",内存中的值是:" << *p << endl;
}

int main()
{
	int* p = nullptr;    // 存放在子函数中动态分配内存的地址。

	func1(&p);      // 传地址,实参填指针p的地址。
	//func2(p);      // 传引用,实参填指针p。

	cout << "main 内存的地址是:" << p << ",内存中的值是:" << *p << endl;

	delete p;
}

将引用形参声明为const的理由有三个:

  • 使用const引用可以避免无意中修改数据的编程错误。
  • 使用const引用使函数能够处理const和非const实参,否则将只能接受非const实参。
  • 使用const引用,函数能正确生成并使用临时变量。

左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针。

非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式。

引用用于函数的返回值

如果返回的是一个结构体,将把整个结构体拷贝到临时的位置,然后赋值。
如果返回引用不会拷贝内存。

注意:

  • 如果返回局部变量的引用,其本质是野指针,后果不可预知。
  • 可以返回函数的引用形参、类的成员、全局变量、静态变量。
  • 返回引用的函数是被引用的变量的别名,将const用于引用的返回类型
const int &func2(int &ra)    // 返回的是引用。
{
	ra++;
	cout << "ra的地址是:" << &ra << ",ra=" << ra << endl;
	return ra;
}
int a = 3;
const int& b = func2(a);      // 返回的是引用。 
func2(a) = 10;             // 返回引有的函数是被引用的变量的别名。

类的成员有三种访问权限:public、private和protected,分别表示公有的、私有的和受保护的。
在类的内部(类的成员函数中),无论成员被声明为 public还是private,都可以访问。
在类的外部(定义类的代码之外),只能访问public成员,不能访问 private、protected成员。
在一个类体的定义中,private 和 public 可以出现多次。
结构体的成员缺省为public,类的成员缺省为private。
private的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public

1)类的成员函数可以直接访问该类其它的成员函数(可以递归)。
2)类的成员函数可以重载,可以使用默认参数。
3)类指针的用法与结构体指针用法相同。
4)类的成员可以是任意数据类型(类中枚举)。
5)可以为类的成员指定缺省值(C++11标准)。
6)类可以创建对象数组,就像结构体数组一样。
7)对象可以作为实参传递给函数,一般传引用。
8)可以用new动态创建对象,用delete释放对象。
9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。数据隐藏是面向对象编程的思想之一。
10)对象一般不用memset()清空成员变量,可以写一个专用于清空成员变量的成员函数。
11)对类和对象用sizeof运算意义不大,一般不用。
12)用结构体描述纯粹的数据,用类描述对象。
13)在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了inline限定符,也是内联函数。
14)为了区分类的成员变量和成员函数的形参,把成员变量名加m_前缀或_后缀,如m_name或name_。
15)类的分文件编写。
  • 类的创建
CGirl girl = CGirl("西施"20);  // 显式创建对象。
CGirl girl;                   // 创建对象。不设置任何初始值
girl = CGirl("西施"20);        // 创建匿名对象,然后给现有的对象赋值。

C++11支持使用统一初始化列表
CGirl girl = {"西施"20};
CGirl girl  {"西施"20};
CGirl* girl = new CGirl{ "西施"20 };
拷贝构造函数

用一个已存在的对象创建新的对象,不会调用(普通)构造函数,而是调用拷贝构造函数。

类名 新对象名(已存在的对象名);
类名 新对象名=已存在的对象名;

拷贝构造函数的语法:

类名(const 类名& 对象名){......}
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

class CGirl                 // 超女类CGirl。
{
public:
    string    m_name;                                  // 姓名属性。
    int         m_age;                                     // 年龄属性。
    int*       m_ptr;                                       // 指针成员,计划使用堆内存。

    // 没有参数的普通构造函数。  
    CGirl() { m_name.clear(); m_age = 0;  m_ptr = nullptr;  cout << "调用了CGirl()构造函数。\n"; }

    // 没有重载的拷贝构造函数(默认拷贝构造函数)。  
    CGirl(const CGirl& gg) 
    { 
        m_name = gg.m_name; m_age = gg.m_age;  
        m_ptr = new int;         // 分配内存。
        // *m_ptr = *gg.m_ptr;   // 拷贝数据。
        memcpy(m_ptr, gg.m_ptr, sizeof(int));   // 拷贝数据。
        cout << "调用了CGirl(const CGirl &gg)拷贝构造函数。\n"; 
    }

    // 析构函数。  
    ~CGirl() { delete m_ptr; m_ptr = nullptr; cout << "调用了~CGirl()\n";  }

    // 超女自我介绍的方法,显示姓名和年龄。
    void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << ",m_ptr="<< m_ptr<<",*m_ptr="<<*m_ptr<<endl; }
};

int main()
{
    CGirl g1;
    g1.m_name = "西施"; g1.m_age = 23; g1.m_ptr = new int(3);
    g1.show();

    CGirl g2(g1);  *g2.m_ptr = 8;
    g1.show();
    g2.show();
}
const修饰成员函数

在类的成员函数后面加const关键字,表示在成员函数中保证不会修改调用对象的成员变量。

注意:

1)mutable可以突破const的限制,被mutable修饰的成员变量,将永远处于可变的状态,在const修饰的函数中,mutable成员也可以被修改。
2)非const成员函数可以调用const成员函数和非const成员函数。
3)const成员函数不能调用非const成员函数。
4)非const对象可以调用const修饰的成员函数和非const修饰的成员函数。
5)const对象只能调用const修饰的成员函数,不能调用非cosnt修饰的成员函数。
class CGirl                 // 超女类CGirl。
{
public:
    mutable string    m_name;                     // 姓名属性。
    int         m_age;                         // 年龄属性。

    // 两个参数的普通构造函数。
    CGirl(const string &name, int age) 
    { m_name = name; m_age = age;  cout << "调用了CGirl(name,age)构造函数。\n"; }
    
    // 超女自我介绍的方法,显示姓名、年龄。
    void show1() const
    { 
        m_name = "西施show2";
    }
}
const CGirl g1("冰冰",18);    
g1.show1();
静态成员
类的静态成员包括静态成员变量和静态成员函数。
用静态成员可以变量实现多个对象之间的数据共享,比全局变量更安全性。
用 static 关键字把类的成员变量声明为静态,表示它在程序中(不仅是对象)是共享的。
静态成员变量不会在创建对象的时候初始化,必须在程序的全局区用代码清晰的初始化(用范围解析运算符 ::)。
静态成员使用类名加范围解析运算符 :: 就可以访问,不需要创建对象。
如果把类的成员声明为静态的,就可以把它与类的对象独立开来(静态成员不属于对象)。
静态成员变量在程序中只有一份(生命周期与程序运行期相同,存放在静态存储区的),不论是否创建了类的对象,也不论创建了多少个类的对象。
在静态成员函数中,只能访问静态成员,不能访问非静态成员。
静态成员函数中没有this指针。
在非静态成员函数中,可以访问静态成员。
私有静态成员在类外无法访问。
const静态成员变量可以在定义类的时候初始化。

C++类中有两种数据成员:nonstatic、static,三种函数成员:nonstatic、static、virtual。

对象内存的大小包括:

1)所有非静态数据成员的大小;
2)由内存对齐而填补的内存大小;
3)为了支持virtual成员而产生的额外负担。
  • 静态成员变量属于类,不计算在对象的大小之内。
  • 成员函数是分开存储的,不论对象是否存在都占用存储空间,在内存中只有一个副本,也不计算在对象大小之内。
  • 用空指针可以调用没有用到this指针的非静态成员函数。
  • 对象的地址是第一个非静态成员变量的地址,如果类中没有非静态成员变量,编译器会隐含的增加一个1字节的占位成员。
友元
在友元类所有成员函数中,都可以访问另一个类的所有成员。
友元类的注意事项:
友元关系不能被继承。
友元关系是单向的,不具备交换性。
若类B是类A的友元,类A不一定是类B的友元。B是类A的友元,类C是B的友元,类C不一定是类A的友元,要看类中是否有相应的声明。
在友元成员函数中,可以访问另一个类的所有成员。
class CGirl       // 超女类CGirl。
{
    friend int main();
    friend void func();
    .....
}

void func()
{
    CGirl g;
    g.showname();
    g.showxw();
}

int main()
{
    func();
}
class CGirl       // 超女类CGirl。
{
    friend class CBoy;
    ....
}

class CBoy    // 超女的男朋友类
{
public:
    void func(const CGirl& g)
    {
        cout << "我女朋友的姓名是:" << g.m_name << endl;
        cout << "我女朋友的胸围是:" << g.m_xw << endl;
        g.showxw();
    }
};
运算符重载

运算符重载函数的语法:返回值 operator运算符(参数列表);

运算符重载函数的返回值类型要与运算符本身的含义一致。

非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同;

成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象。

如果同时重载了非成员函数和成员函数版本,会出现二义性。如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。

class CGirl       // 超女类CGirl。
{
    friend CGirl& operator+(CGirl& g, int score);
    friend CGirl& operator+(int score, CGirl& g);
    friend CGirl& operator+(CGirl& g1, CGirl& g2);
    ....
}

CGirl& operator+(CGirl& g, int score)    // 给超女加分的函数。
{
    g.m_score = g.m_score + score;
    return g;
}
CGirl& operator+(int score,CGirl& g)    // 给超女加分的函数。
{
    g.m_score = g.m_score + score;
    return g;
}
CGirl& operator+(CGirl& g1, CGirl& g2)    // 给超女加分的函数。
{
    g1.m_score = g1.m_score + g2.m_score;
    return g1;
}
class CGirl       // 超女类CGirl。
{
     bool operator==(const CGirl& g1)      // 相等==
    {
    }
    bool operator>(const CGirl& g1)      // 大于>
    {
    }
    bool operator<(const CGirl& g1)      // 小于<
    {
    }
}

自动推导类型auto

在C语言和C++98中,auto关键字用于修饰变量(自动存储的局部变量)。
在C++11中,赋予了auto全新的含义,不再用于修饰变量,而是作为一个类型指示符,指示编译器在编译时推导auto声明的变量的数据类型。
语法:auto 变量名 = 初始值;
在Linux平台下,编译需要加-std=c++11参数。
注意:

1)auto声明的变量必须在定义时初始化。
2)初始化的右值可以是具体的数值,也可以是表达式和函数的返回值等。
3)auto不能作为函数的形参类型。
4)auto不能直接声明数组。
5)auto不能定义类的非静态成员变量。

不要滥用auto,auto在编程时真正的用途如下:

1)代替冗长复杂的变量声明。
2)在模板中,用于声明依赖模板参数的变量。
3)函数模板依赖模板参数的返回值。
4)用于lambda表达式中。

#define

#是字符串化的意思,出现在宏定义中的#是把跟在后面的参数转成一个字符串;

#define  strcpy__(dst, src)      strcpy(dst, #src)   
strcpy__(buff,abc)  相当于 strcpy__(buff,“abc”)

##是连接符号,把参数连接在一起

#define FUN(arg)     my##arg
则     FUN(ABC)
等价于  myABC
再看一个具体的例子


#include <iostream>
         
using namespace std;
         
#define  OUTPUT(A) cout<<#A<<":"<<(A)<<endl;
         
int main()
{
    int a=1,b=2;
         
    OUTPUT(a);     //a:1
    OUTPUT(b);	   //b:2
    OUTPUT(a+b);   //a+b:3
         
    return 1;
}

左值与右值

在C++中,所有的值不是左值,就是右值。有名字的对象都是左值,右值没有名字。

左值:

左值是指表达式结束后依然存在的持久化对象,能对表达式取地址

右值:

右值是指表达式结束后就不再存在的临时对象,不能对表达式取地址

左值与右值

通常来说你确实可以使用一个值在表达式中的位置信息来判断这个值的左右值类型,比如说在下面这段代码中:

int x = 10;
int y = 20;
int sum = x + y;

最后一行当中,位于 " = " 左侧的sum就是一个左值,而右侧的 " x + y " 就是一个右值。
因为我们可以对sum进行取地址操作,而不能对等号右侧的x+y进行取地址操作。

C++11扩展了右值的概念,将右值分为了纯右值和将亡值。

纯右值:

  • 非引用返回的临时变量;
  • 运算表达式产生的结果;
  • 字面常量(C风格字符串除外,它是地址)。
  • lambda表达式

将亡值:

  • 与右值引用相关的表达式

例如:将要被移动的对象、T&&函数返回的值、std::move()的返回值、转换成T&&的类型的转换函数的返回值。

示例:

class AA {
    int m_a;
};

AA getTemp()
{
    return AA();
}

int ii = 3;       // ii是左值,3是右值。
int jj = ii+8;    // jj是左值,ii+8是右值。
AA aa = getTemp();   // aa是左值 ,getTemp()的返回值是右值(临时变量)。

左值引用、右值引用

C++98中的引用很常见,就是给变量取个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。

  • 右值引用就是给右值取个名字。

    语法:数据类型&& 变量名=右值;

    示例:

    #include <iostream>
    using  namespace std;
    
    class AA {
    public:
        int m_a=9;
    };
    
    AA getTemp()
    {
        return AA();
    }
    
    int main()
    {
        int&& a = 3;         // 3是右值。
    
        int b = 8;               // b是左值。
        int&& c = b + 5;   //  b+5是右值。
    
        AA&& aa = getTemp();   // getTemp()的返回值是右值(临时变量)。
    
        cout << "a=" << a << endl;
        cout << "c=" << c << endl;
        cout << "aa.m_a=" << aa.m_a << endl;
    }
    

    getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。

  • 引入右值引用的主要目的是实现移动语义。

左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。

但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

int a = 1;        
const int& ra = a;   // a是非常量左值。

const int b = 1;  
const int& rb = b;  // b是常量左值。

const int& rc = 1;   // 1是右值。

总结一下,其中T是一个具体类型:

  • 左值引用, 使用 T&, 只能绑定左值。
  • 右值引用, 使用 T&&, 只能绑定右值。
  • 已命名的右值引用是左值。
  • 常量左值,使用 const T&, 既可以绑定左值又可以绑定右值。

移动语义

如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。

实现移动语义要增加两个函数:移动构造函数和移动赋值函数。

移动构造函数的语法:
类名(类名&& 源对象){......}

移动赋值函数的语法:
类名& operator=(类名&& 源对象){……}

注意:

  • 对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
  • 如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
  • C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
  • 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

示例:

#include <iostream>
using namespace std;

class AA
{
public:
    int* m_data = nullptr;  // 数据成员,指向堆区资源的指针。

    AA() = default;             // 启用默认构造函数。

    void alloc() {                // 给数据成员m_data分配内存。
        m_data = new int;                       // 分配内存。
        memset(m_data, 0, sizeof(int));   // 初始化已分配的内存。
    }

    AA(const AA& a) {     // 拷贝构造函数。
        cout << "调用了拷贝构造函数。\n";            // 显示自己被调用的日志。
        if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
        memcpy(m_data, a.m_data, sizeof(int));     // 把数据从源对象中拷贝过来。
    }

    AA(AA&& a) {     // 移动构造函数。
        cout << "调用了移动构造函数。\n";            // 显示自己被调用的日志。
        if (m_data != nullptr) delete m_data;         // 如果已分配内存,先释放掉。
        m_data = a.m_data;                                   // 把资源从源对象中转移过来。
        a.m_data = nullptr;                                    // 把源对象中的指针置空。
    }

    AA& operator=(const AA& a) { // 赋值函数。
        cout << "调用了赋值函数。\n";                   // 显示自己被调用的日志。
        if (this == &a)   return *this;                      // 避免自我赋值。
        if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
        memcpy(m_data, a.m_data, sizeof(int));    // 把数据从源对象中拷贝过来。
        return *this;
    }

    AA& operator=(AA&& a) { // 移动赋值函数。
        cout << "调用了移动赋值函数。\n";            // 显示自己被调用的日志。
        if (this == &a)   return *this;                      // 避免自我赋值。
        if (m_data != nullptr) delete m_data;         // 如果已分配内存,先释放掉。
        m_data = a.m_data;                                   // 把资源从源对象中转移过来。
        a.m_data = nullptr;                                    // 把源对象中的指针置空。
        return *this;
    }

     ~AA() {                 // 析构函数。
         if (m_data != nullptr) {
             delete m_data; m_data = nullptr;
         }
    }
};

int main()
{
    AA a1;                  // 创建对象a1。
    a1.alloc();             // 分配堆区资源。
    *a1.m_data = 3;   // 给堆区内存赋值。
    cout << "a1.m_data=" << *a1.m_data << endl;

    AA a2 = a1;         // 将调用拷贝构造函数。
    cout << "a2.m_data=" << *a2.m_data << endl;

    AA a3;
    a3 = a1;              // 将调用赋值函数。
    cout << "a3.m_data=" << *a3.m_data << endl;

    auto f = [] { AA aa; aa.alloc(); *aa.m_data = 8; return aa; };   // 返回AA类对象的lambda函数。
    AA a4 = f();                // lambda函数返回临时对象,是右值,将调用移动构造函数。
    cout << "a4.m_data=" << *a4.m_data << endl;

    AA a6;
    a6 = f();              // lambda函数返回临时对象,是右值,将调用移动赋值函数。
    cout << "a6.m_data=" << *a6.m_data << endl;
}

std::move是C++11当中新引入的一个标准库函数,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。确切地说:它使一个值易于移动。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

接下来我们来看一下std::move的用武之地,通常我们需要交换两个变量的值时,我们会这样编写函数:

template<class T>
void swap(T left, T right)
{
	T temp = left;
	left = right;
	right = temp;
}

通过这种方法会存在三次复制的操作,当然如果数据类型较小的情况下,这种方法是可取的,但是倘若数据量很大的情况下,这种交换操作的代价是非常昂贵的,因此我们采用了std::move的方法:

// std::swap函数的实现
template<class T>
void swap(T &left, T& right)
{
	T temp = std::move(left);    // left为空
	left = std::move(right);     // right为空
	right = std::move(temp);     // temp为空
}

使用这种方法的好处是,我们不必再为中间对象temp重新分配内存空间,而是直接将left申请好的内存直接偷过来用,从而避免了大量内存分配的高昂代价。

问题:为什么拷贝构造函数的参数会使用常量左值引用类型(const T&)?或者说使用这个类型有什么样的好处呢?

首先来说明一下为什么拷贝构造函数的参数必须为引用类型:

这是由于,倘若拷贝构造函数的参数不是一个引用类型,而是诸如(const T)或(T)这样的传值类型,那么我们在调用拷贝构造函数的时刻,会采用传值(pass-by-value)的方式将实参的值传递给形参,而传值的方式又会调用该类的拷贝构造函数,从而造成无穷的递归调用拷贝构造函数,进而导致堆栈溢出,因此拷贝构造函数的参数必须为引用类型。

我们再来进一步分析为什么为常量类型:

这是由于,我们拷贝构造函数设计的初衷即:根据被拷贝对象的内容,来初始化对象本身。使用常量类型是为了防止在拷贝构造函数内部,通过引用形参修改被拷贝对象本身的内容。

完美转发

在函数模板中,可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。

C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定于该参数在传递过程使用的是拷贝语义还是移动语义

为了支持完美转发,C++11提供了以下方案:

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

  • 提供了模板函数std::forward(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。

示例:

#include <iostream>
using namespace std;

void func1(int& ii) {        // 如果参数是左值,调用此函数。
    cout << "参数是左值=" << ii << endl;
}

void func1(int&& ii) {     // 如果参数是右值,调用此函数。
    cout << "参数是右值=" << ii << endl;
}

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);  // "我是绝世帅歌。"不是可变参数,其它的都是。
}
#include <iostream>

void xprintf(){}
template <typename T, typename... Targs>
void xprintf(T value, Targs... Fargs)
{
std::cout << sizeof...(Fargs) << " " << value << std::endl;
xprintf(Fargs...);
}

int main()
{
xprintf("小明个人信息:", "小明", "男", 35, "程序员", 169.5);
return 0;
}
#define ALOGD(...)  LOGF(LOG_TAG, ##__VA_ARGS__)

template<typename... T>
static void LOGF(string tag, T&&... t) {
    printf(std::forward<T>(t)...);
}

C++11线程

在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。

一、创建线程

头文件:#include <thread>
线程类:std::thread

构造函数:

1)thread() noexcept;
默认构造函,构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
2)template< class Function, class... Args >
explicit thread(Function&& fx, Args&&... args );
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数。
3)thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
4)thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数:
thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。

注意:

  • 先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。
  • 线程的任务函数返回后,子线程将终止。
  • 如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。

示例:

#include <iostream>
#include <thread>                // 线程类头文件。
#include <windows.h>         // Sleep()函数需要这个头文件。
using namespace std;

// 普通函数。
void func(int bh, const string& str) {
	for (int ii = 1; ii <= 10; ii++)
	{
		cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
		Sleep(1000);   // 休眠1秒。
	}
}

// 仿函数。
class mythread1
{
public:
	void operator()(int bh, const string& str) {
		for (int ii = 1; ii <= 10; ii++)
		{
			cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
			Sleep(1000);   // 休眠1秒。
		}
	}
};

// 类中有静态成员函数。
class mythread2
{
public:
	static void func(int bh, const string& str) {
		for (int ii = 1; ii <= 10; ii++)
		{
			cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
			Sleep(1000);   // 休眠1秒。
		}
	}
};

// 类中有普通成员函数。
class mythread3
{
public:
	void func(int bh, const string& str) {
		for (int ii = 1; ii <= 10; ii++)
		{
			cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
			Sleep(1000);   // 休眠1秒。
		}
	}
};

int main()
{
	// 用普通函数创建线程。
	//thread t1(func, 3, "我是一只傻傻鸟。");
	//thread t2(func, 8, "我有一只小小鸟。");

	// 用lambda函数创建线程。
	auto f = [](int bh, const string& str) {
		for (int ii = 1; ii <= 10; ii++)
		{
			cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
			Sleep(1000);   // 休眠1秒。
		}
	};
	//thread t3(f, 3, "我是一只傻傻鸟。");

	// 用仿函数创建线程。
	//thread t4(mythread1(), 3, "我是一只傻傻鸟。");

	// 用类的静态成员函数创建线程。
	//thread t5(mythread2::func, 3, "我是一只傻傻鸟。");

	// 用类的普通成员函数创建线程。
	mythread3 myth;   // 必须先创建类的对象,必须保证对象的生命周期比子线程要长。
	thread t6(&mythread3::func, &myth, 3, "我是一只傻傻鸟。");  // 第二个参数必须填对象的this指针,否则会拷贝对象。

	cout << "任务开始。\n";
	for (int ii = 0; ii < 10; ii++) {
		cout << "执行任务中......\n";
		Sleep(1000);   // 假设执行任务需要时间。
	}
	cout << "任务完成。\n";

	//t1.join();         // 回收线程t1的资源。
	//t2.join();         // 回收线程t2的资源。
	//t3.join();         // 回收线程t3的资源。
	//t4.join();         // 回收线程t4的资源。
	//t5.join();         // 回收线程t5的资源。
	t6.join();         // 回收线程t6的资源。
}

二、线程资源的回收

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

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

  • 在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
  • 在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。

分离后的子线程不可join()。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。

示例:

#include <iostream>
#include <thread>                // 线程类头文件。
#include <windows.h>         // Sleep()函数需要这个头文件。
using namespace std;

// 普通函数。
void func(int bh, const string& str) {
	for (int ii = 1; ii <= 10; ii++)
	{
		cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
		Sleep(1000);   // 休眠1秒。
	}
}

int main()
{
	// 用普通函数创建线程。
	thread t1(func, 3, "我是一只傻傻鸟。");
	thread t2(func, 8, "我有一只小小鸟。");
	
	t1.detach(); t2.detach();  // 分离子线程。

	//cout << "任务开始。\n";
	//for (int ii = 0; ii < 12; ii++) {
	//	cout << "执行任务中......\n";
	//	Sleep(1000);   // 假设执行任务需要时间。
	//}
	//cout << "任务完成。\n";
	
	//t1.join();         // 回收线程t1的资源。
	//t2.join();         // 回收线程t2的资源。
	Sleep(12000);
}

三、this_thread的全局函数

C++11提供了命名空间this_thread来表示当前线程,

该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。

1)get_id()
thread::id get_id() noexcept;
该函数用于获取线程ID,thread类也有同名的成员函数。
2)sleep_for()  VS  Sleep(1000)   Linux sleep(1)
template <class Rep, class Period>
  void sleep_for (const chrono::duration<Rep,Period>& rel_time);
该函数让线程休眠一段时间。
3)sleep_until()          2022-01-01 12:30:35
template <class Clock, class Duration>
  void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
该函数让线程休眠至指定时间点。(可实现定时任务)
4)yield()
void yield() noexcept;
该函数让线程主动让出自己已经抢到的CPU时间片。
5)thread类其它的成员函数
void swap(std::thread& other);    // 交换两个线程对象。
static unsigned hardware_concurrency() noexcept;   // 返回硬件线程上下文的数量。

示例:

#include <iostream>
#include <thread>                // 线程类头文件。
using namespace std;

// 普通函数。
void func(int bh, const string& str) {
	cout << "子线程:" << this_thread::get_id() << endl;

	for (int ii = 1; ii <= 3; ii++)
	{
		cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
		this_thread::sleep_for(chrono::seconds(1));    // 休眠1秒。
	}
}

int main()
{
	// 用普通函数创建线程。
	thread t1(func, 3, "我是一只傻傻鸟。");
	thread t2(func, 8, "我有一只小小鸟。");

	cout << "主线程:" << this_thread::get_id() << endl;
	cout << "线程t1:" << t1.get_id() << endl;
	cout << "线程t2:" << t2.get_id() << endl;

	t1.join();         // 回收线程t1的资源。
	t2.join();         // 回收线程t2的资源。
}

call_once函数

在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。
在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次。

头文件:#include <mutex>
template< class callable, class... Args >
  void call_once( std::once_flag& flag, Function&& fx, Args&&... args );
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。

示例:

#include <iostream>
#include <thread>        // 线程类头文件。
#include <mutex>        // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;

once_flag onceflag;       // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str)  { 
	cout << "once_func() bh= " << bh << ", str=" << str << endl;
}

// 普通函数。
void func(int bh, const string& str) {
	call_once(onceflag,once_func,0, "各位观众,我要开始表白了。");

	for (int ii = 1; ii <= 3; ii++)
	{
		cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
		this_thread::sleep_for(chrono::seconds(1));    // 休眠1秒。
	}
}

int main()
{
	// 用普通函数创建线程。
	thread t1(func, 3, "我是一只傻傻鸟。");
	thread t2(func, 8, "我有一只小小鸟。");

	t1.join();         // 回收线程t1的资源。
	t2.join();         // 回收线程t2的资源。
}
class Test {
private:
    static  void once_xxx() {
        static std::once_flag oc;
        std::call_once(oc, [] { call_xxx;});
    }
};

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>

一、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类

主要成员函数:

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)
std::mutex m_mutex;
std::condition_variable m_cond;
unique_lock<mutex> lock(m_mutex);
std::cv_status status = std::cv_status::no_timeout;

auto now = std::chrono::system_clock::now();
auto until_time = now + std::chorono::seconds(10);

status = m_cond.wait_until(lock, until_time);

if (status == std::cv_status::timeout ) {
 // timeout 
}

二、unique_lock类

template <class Mutex> 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<T>模板类(结构体),用于支持原子类型,模板参数可以是bool、char、int、long、long long、指针类型(不支持浮点类型和自定义数据类型)。

原子操作由CPU指令提供支持,它的性能比锁和消息传递更高,并且,不需要程序员处理加锁和释放锁的问题,支持修改、读取、交换、比较并交换等操作。

头文件:#include <atomic>

构造函数:
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<T>模板类重载了整数操作的各种运算符。
atomic<T>模板类的模板参数支持指针,但不表示它所指向的对象是原子类型。
原子整型可以用作计数器,布尔型可以用作开关。
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 _Fty>
class function……
_Fty是可调用对象的类型,格式:返回类型(参数列表)。
包含头文件:#include <functional>
注意:
重载了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 <functional>

函数原型:
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();
}
  • 回调函数的实现

在消息队列和网络库的框架中,当接收到消息(报文)时,回调用户自定义的函数对象,把消息(报文)参数传给它,由它决定如何处理。

示例:

#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;			// 释放派生类对象。
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值