1. 原始自变量

定义原始字符串的方式为:R “xxx(原始字符串)xxx”,其中()两边的字符串可以省略(不省略的时候就要求一样)。原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做转义或连接等操作。

用途:主要是防止一些特殊字符,如\t,\n等对想输出的字符串产生影响。

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
string str1 = R"hello(D:\hello\world\test.text)hello"; //后面的hello为world就会报错
cout << str1 << endl;
string str2 = R"(<html>
<head>
<title>
海贼王
</title>
</head>
<body>
<p>
我是要成为海贼王的男人
</p>
</body>
</html>)";
cout << str2 << endl;
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:\hello\world\test.text
<html>
<head>
<title>
海贼王
</title>
</head>
<body>
<p>
我是要成为海贼王的男人
</p>
</body>
</html>

2. 指针空值类型nullptr

nullptr无法隐式转换为整形,但是可以隐式匹配指针类型。在c++11标准下,相比NULL和0,使用nullptr初始化空指针可以令编写的程序更加健壮。

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func(char* p) {
cout << "void func(char *p)" << endl;
}
void func(int p) {
cout << "void func(int p)" << endl;
}

int main() {
int* ptr1 = NULL;
char* ptr2 = NULL;
void* ptr3 = nullptr;
func(10);
func(NULL); //在c中,NULL是(void*)0--->对0强制转换;而在c++中,NULL就是0
func(nullptr); //c++中,对指针初始化用nullptr,它可以隐式转换为各种类型指针
}

输出结果:

1
2
3
void func(int p)
void func(int p)
void func(char *p)

3. constexpr修饰常量表达式

C++ 程序从编写完毕到执行分为四个阶段:预处理、 编译、汇编和链接4个阶段,得到可执行程序之后就可以运行了。需要额外强调的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

注:const和constexpr是等价的,都可以在程序的编译阶段计算出结果。

测试程序:

1
2
3
4
5
6
7
8
9
struct T {          //对结构体用constexpr是不行的,只能在初始化一个结构体时才可以用该常量
int a;
};
int main() {
constexpr int a = 13;
//a = 12; 报错,不能对常量进行修改了
constexpr T t{ 13 };
//t.a = 12; 报错
}

4. 自动类型推导

当变量不是指针或者引用类型时,推导的结果中不会保留const、volatile关键字。
当变量是指针或者引用类型时,推导的结果中会保留const、volatile关键字。

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
auto a = 3;
auto b = 3.14;
auto c = 'f';

int tmp = 250;
const auto a1 = tmp; //a1是const int类型 ---> auto相当于是int
auto a2 = a1; //因为a2既没有指针也没有引用,所以a2是int类型,不是const int类型(const被消除了)
//保留赋值的const方法(加解引用或指针)
auto& a3 = a1; //a3是const int类型
auto* pt1 = &a1; //pt1是const int*类型
}

不允许使用auto的四个场景:

1.不能作为函数参数使用,因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。

1
2
3
int func(auto x, auto y) {                    //错误的
cout << "x: " << x << "y: " << y << endl;
}

2.不能用于类的非静态成员变量的初始化

1
2
3
4
5
class Test {
auto v1 = 0; //错误
static auto v2 = 0; //错误,类的静态非常量成员不允许在类的内部直接初始化
static const auto v3 = 10; //正确
};

3.不能使用auto关键字定义数组

1
2
3
4
5
6
int func() {
int array[] = { 1,2,3,4,5 }; //定义数组
auto t1 = array; //正确,t1被推导为int*类型
auto t2[] = array; //错误,t2相当于是重新定义数组,是无法成功的
auto t3[] = { 1,2,3,4,5 }; //错误,auto无法定义数组
}

4.无法使用auto推导出模板参数

1
2
3
4
5
6
7
8
template <typename T>
struct Test {};

int func() {
Test<double> t;
Test<auto>t1 = t; //错误,无法推导出模板类型
return 0;
}

5. decltype类型推导

它的作用是在编译器编译的时候推导出一个表达式的类型,如decltype (表达式);

测试程序:

1
2
3
4
int a = 10;
decltype(a) b = 99; // b -> int
decltype(a+3.14) c = 52.13; // c -> double
decltype(a+b*c) d = 520.1314; // d -> double

decltype的应用多数出现在泛型编程中,下面编写一个类模板,在里边添加遍历容器的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <list>
#include <iostream>
using namespace std;

template <class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
cout << *m_it << " ";
}
cout << endl;
}
private:
//T::iterator m_it; // 这里不能确定迭代器类型
//这样就能够推导出对应的T容器它的迭代器类型,基于这个类型定义出了它的迭代器变量
decltype(T().begin()) m_it; //通过T()来得到一个对象,调用它的begin()方法
};

int main()
{
list<int> st1{ 1,2,3,4,5,6,7,8,9 };
Container<list<int>> c;
c.func(st1);

//遍历常量的迭代器容器(只读迭代器)
const list<int> st2{ 1,2,3,4,5,6,7,8,9 };
Container<const list<int>> c2;
c2.func(st2);
return 0;
}

decltype返回值类型后置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename R,typename T, typename U>           //模板函数1
R add1(T t, U u) {
return t + u;
}
template <typename T, typename U> //模板函数2
auto add2(T t, U u) -> decltype(t + u) { //auto可以通过后面的decltype来判断
return t + u;
}

int main() {
int t = 3;
double u = 3.14;
//模板函数1:可以编译,但不合理,外部调用的人,一般不会知道模板函数内部的代码内容,即不知道最后返回值是t+u
auto a = add1<decltype(t + u), int, double>(t , u);
cout << a << endl;
//模板函数2:
auto b = add2(t, u);
cout << b << endl;
}

6. final和overrid关键字的使用

6.1 final

final关键字来限制某个类不能被继承,或者某个虚函数不能被重写。

如果用final来修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了。

解释:当test()是基类中的一个虚函数时,在子类中重写了这个方法,但是不希望孙子类中继续重写这个方法了,因此在子类中将test()方法标记为final,孙子类中对这个方法就只能使用,而不能进行重写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base
{
public:
virtual void test() //父类虚函数
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
void test() final //用final修饰从父类继承下来的虚函数,表示之后再继承Child类时,不能重写Test
{
cout << "Child class...";
}
};
class GrandChild : public Child
{
public:
void test() // 语法错误, 不允许重写
{
cout << "GrandChild class...";
}
};

如果使用final关键字来修饰类的话,表示该类是不允许被继承的,也就是说这个类不能有派生类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base                                 //父类
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child final: public Base //子类继承父类,同时加上了final关键字
{
public:
void test()
{
cout << "Child class...";
}
};
class GrandChild : public Child //语法错误,不能在继承Child类了
{
public:
};

6.2 override

override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的名字,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base
{
public:
virtual void test() //虚函数
{
cout << "Base class...";
}
};

class Child : public Base
{
public:
void test() override //重写从父类继承下来的虚函数(当函数名不和父类的虚函数名一样时,会提示错误)
{
cout << "Child class...";
}
};

7.委托构造函数和继承构造函数

7.1 委托构造函数

委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//子类也可以通过以下方式来继承父类的构造函数
class test {
public:
test() {};
test(int num) {
cout << "一个参数:" << num << endl;
}
test(int num, int sum) :test(num){ //调用同一个类中的另一个构造函数
//cout << "一个参数:" << num << endl;
cout << "两个参数:" << sum << endl;
}
test(int num, int sum, int tum) :test(num, sum) { //调用同一个类中的另一个构造函数
//cout << "一个参数:" << num << endl;
//cout << "二个参数:" << sum << endl;
cout << "三个参数:" << tum << endl;
}
};

7.2 继承构造函数

继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class base {
public:
base(int num) :a(num){}
base(int num, int sum) :a(num), b(sum) {}
base(int num, int sum, int tum) :a(num),b(sum),c(tum) {}
int a;
int b;
int c;
};
class child :public base {
public:
/*
child(int num) :base(num){}
child(int num, int sum) :base(num,sum) {}
child(int num, int sum, int tum) :base(num, sum,tum) {}*/
using base::base; //可以直接这样写(相当于上面3行代码)
};
int main() {
child x(10, 20, 30);
cout << x.a << x.b << x.c << endl;
}

另外如果在子类中隐藏了父类中的同名函数,也可以通过using的方式在子类中使用基类中的这些父类函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class base {
public:
base(int num) :a(num) {}
base(int num, int sum) :a(num), b(sum) {}
base(int num, int sum, int tum) :a(num), b(sum), c(tum) {}
void func(int x) {
cout << x << endl;
}
void func(int x, int y) {
cout << x << "and" << y << endl;
}
int a;
int b;
int c;
};
class child :public base {
public:
using base::base;
using base::func; //也可以通过这行来解决隐藏了父类同名函数的情况
void func() { //写了一个与父类中的同名函数
cout << "lxx" << endl;
}
};
int main() {
child c(250); //这里必须是带参的构造函数,因为父类中没有默认构造函数
c.base::func(39,93); //通过加定义域的方式来得到与父类相同的同名函数
c.func(39,93);
}

在C++中,如果基类没有默认构造函数,子类也不能自动生成默认构造函数。这是因为子类对象的构造需要先构造基类的部分,如果基类没有默认构造函数,子类在构造时就无法调用基类的构造函数来初始化基类部分。

8 .可调用对象包装器和绑定器

8.1 可调用对象

在c++中有四种可调用对象的定义

1.是一个函数指针

1
2
3
4
5
6
int print(int x, double y)
{
cout << x << y << endl;
return 0;
}
int (*func)(int, double) = &print; // 定义函数指针,指向刚定义的函数

2.是一个具有operator()成员函数的类对象(仿函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Test
{
// ()操作符重载
void operator()(string msg)
{
cout << "msg: " << msg << endl;
}
};
int main(void)
{
Test t;
t("lxxlxxlxx"); // 仿函数
return 0;
}

3.是一个可被转换为函数指针的类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using func_ptr = void(*)(int, string);
struct Test
{
static void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}

// 将类对象转换为函数指针
operator func_ptr()
{
//这里返回的必须是静态的成员函数,因为静态的是属于类,非静态的属于对象,在还没有定义对象之前,非静态的成员函数是不存在的,而静态成员函数是存在的。
return print;
}
};

int main(void)
{
Test t;
// 对象转换为函数指针, 并调用
t(19, "Monkey D. Luffy");
return 0;
}

4.是一个类成员函数指针或者类成员指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//using func_ptr = void(*)(int, string);
struct Test
{
void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}
int m_num;
};

int main(void)
{
// 定义类成员函数指针指向类成员函数
void (Test::*func_ptr)(int, string) = &Test::print;
// 类成员指针指向类成员变量
int Test::*obj_ptr = &Test::m_num;

Test t;
// 通过类成员函数指针调用类成员函数
(t.*func_ptr)(19, "Monkey D. Luffy");
// 通过类成员指针初始化类成员变量
t.*obj_ptr = 1;
cout << "number is: " << t.m_num << endl;
return 0;
}

8.2 可调用对象包装器

可调用对象的包装器是std::function。它是一个类模板,可以容纳除了类(非静态)成员(函数)指针之外的所有可调用对象。使用std::function,必须包含头文件functional

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int add(int a, int b)
{
cout << a << " + " << b << " = " << a + b << endl;
return a + b;
}

class T1
{
public:
static int sub(int a, int b)
{
cout << a << " - " << b << " = " << a - b << endl;
return a - b;
}
};

class T2
{
public:
int operator()(int a, int b)
{
cout << a << " * " << b << " = " << a * b << endl;
return a * b;
}
};

int main(void)
{
// 绑定一个普通函数
function<int(int, int)> f1 = add; //打包之后的名字是f1,包装的函数是add
// 绑定一个类的静态函数
function<int(int, int)> f2 = T1::sub; //包装的函数需要指明具体的类和函数名
// 绑定一个仿函数
T2 t;
function<int(int, int)> f3 = t;
// 函数调用
f1(9, 3);
f2(9, 3);
f3(9, 3);
return 0;
}

因为回调函数本身就是通过函数指针实现的,使用对象包装器可以取代函数指针的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void print(int num, string name) {             //普通函数
cout << num << "\t" << name << endl;
}
using funcptr = void(*)(int, string); //声明了一个函数指针别名,返回值是void,参数是int和string

class Test {
public:
void operator()(string msg) {
cout << "仿函数:" << msg << endl;
}
static void world(int a, string s) {
cout << a << "\t" << s << endl;
}
};

class A {
public:
//构造函数参数是一个包装器对象(可以给构造函数传入相同类型的可调用对象,然后通过可调用对象包装器进行打包,保存在了callback里面)
A(const function<void(int, string)>& f) :callback(f) {}
//相当于回调操作,通过函数指针就可以进行回调(传入一个函数指针的地址给另一个函数,在另一个函数里面就可以调用该指针,执行对应的函数体)
void notify(int id, string name) {
callback(id, name); //调用通过构造函数得到的函数指针
}

private:
function<void(int, string)>callback;
};

int main() {
A aa(print); //传入一种可调用对象
aa.notify(1, "ace");

A ab(Test::world); //传入一种可调用对象
ab.notify(2, "sabo");
}

如果在编写程序的时候要用到多种类型的可调用对象,那么就可以通过可调用对象包装器对它们进行打包。因为只要给参数指定为可调用对象包装器类型,只要传进任意一种可调用对象类型(参数和返回值要求是相同类型),它都会进行隐式的类型转换。通过可调用对象的包装器把这些不同类型的可调用对象封装成一种类型,这样程序就显得更加简洁和灵活。

8.3 绑定器

std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void output(int x, int y){
cout << x << " " << y << endl;
}
int main(void){
//使用绑定器绑定可调用对象和参数,并调用得到的仿函数
bind(output, 1, 2)(); //绑定了普通函数output,指定了2个参数,()表示调用了该仿函数
bind(output, placeholders::_1, 2)(3); //第一个参数指定了占位符,是后面()中的3
bind(output, 2, placeholders::_1)(9); //第二个参数指定了占位符,是后面()中的9

// error, 调用时没有第二个参数
// bind(output, 2, placeholders::_2)(10); //占位符_2会去找()里面的第二个实参,会出问题,应该是_1
// 调用时第一个参数10被吞掉了,没有被使用
bind(output, 2, placeholders::_2)(10, 20); //这种情况是参数1会用2,而不是会用10

bind(output, placeholders::_1, placeholders::_2)(10, 20);
bind(output, placeholders::_2, placeholders::_1)(10, 20);
return 0;
}

可调用对象包装器std::function是不能实现对类成员函数指针或者类成员指针的包装的,但是通过绑定器std::bind的配合之后,就可以完美的解决这个问题了。

在下面程序中,使用绑定器函数bind()绑定了某一个可调用对象,最终得到一个仿函数f或f1,其实这个仿函数对应的还是绑定的那个可调用对象(参数1),绑定的时候可以给它指定固定的参数(参数2和参数3),固定的参数可以是一个变量也可以是一个常量。如果绑定的时候不给它指定一个固定的数值,可以指定占位符,当指定占位符后,它就需要从仿函数调用时候的参数列表里面去读对应的数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//绑定器和包装器结合
void testFunc(int x, int y, const function<void(int, int)>& f) {
if (x % 2 == 0) {
f(x, y);
}
}
void output_add(int x, int y) {
cout << "x=" << x << "\t y=" << y << "\t x+y=" << x + y << endl;
}
int main(void){
for(int i=0; i<10; i++){
//当绑定后,就得到一个仿函数f了 ---> 也是可调用对象的一种
auto f = bind(output_add, i+100, i+200); //参2和参3已经指定了函数output_add的具体参数
testFunc(i,i,f); //这里的i和i参数不会影响output_add的参数了
auto f1 = bind(output_add, placeholders::_1, placeholders::_2); //参2和参3已经指定的是占位符
testFunc(i,i,f1); //这里的i和i参数会影响output_add的参数了
}
}

输出的结果为:

1
2
3
4
5
6
7
8
9
10
x=100    y=200   x+y=300
x=0 y=0 x+y=0
x=102 y=202 x+y=304
x=2 y=2 x+y=4
x=104 y=204 x+y=308
x=4 y=4 x+y=8
x=106 y=206 x+y=312
x=6 y=6 x+y=12
x=108 y=208 x+y=316
x=8 y=8 x+y=16

下面程序是对类的成员函数以及类的成员变进行绑定和封装。因为可调用包装器std::function不能对类成员函数和变量进行包装,但可以对仿函数进行包装,所以可以先通过绑定器bind来对类成员函数和变量进行绑定,然后可以得到对应的一个仿函数。这样包装器就可以间接的对其进行包装了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test {
public:
void output(int x, int y) {
cout << x << "\t" << y << endl;
}
int m_number = 100;
};
int main(){
//成员函数绑定
Test t;
auto f2 = bind(&Test::output, &t, 520, placeholders::_1); //&t是output所属对象的地址,得到一个仿函数f2
function<void(int, int)>f22 = bind(&Test::output, &t, 520, placeholders::_1); //用包装器对可调用对象f2进行包装(间接包装)
f2(1314); //通过绑定,将一个二元函数变成一元函数
//成员变量绑定(因为成员变量没有参数,所以bind就没有参3和参4)
auto f3 = bind(&Test::m_number, &t); //f3是一个仿函数,而下面的f33是一个将仿函数进行包装的包装器类型
function<int& (void)>f33 = bind(&Test::m_number, &t); //如果要f33是可读可写,就使用取地址符&
cout << f3() << endl; //大于f3的值,它代表的是绑定的变量
f3() = 666; //修改成员变量的值
cout << f3() << endl;
}

上面程序中,f3和f33的类型是不一样的,绑定器bind绑定完后得到的是一个仿函数,而f33它是把仿函数进行了包装,得到一个包装器类型,所以这两个不是等价的。需要了解的是,f33这一行是做了隐式的类型转换,f3这一行是做了自动的类型推导。

9. lambda表达式

lambda表达式的捕获列表可以捕获一定范围内的变量,具体如下:

  • [] 不捕抓任何变量
  • [&] 捕获外部作用域所有的变量,并作为引用在函数体内使用(按引用捕获)
  • [=] 捕获外部作用域所有的变量,并作为副本在函数体内使用(按值捕获) —>拷贝的副本在匿名函数体内部是只读的
  • [=,&foo] 按值捕获外部作用域中的所有变量,并按引用捕获外部变量foo
  • [bar] 按值捕获bar变量,同时不捕获其他变量
  • [&bar] 按引用捕获bar变量,同时不捕获其他变量
  • [this] 捕获当前类中的this指针

lambda表达式的使用:

1
2
3
4
5
6
7
8
9
10
11
void func(int x, int y) {
int a = 7;
int b = 9;
[=, &x]() mutable {
int c = a; //拷贝得来的
int d = x; //引用得来的
b++; //如果没有加上mutable,则只能读外部变量
cout << b << endl;
}(); //不加()只是被定义,而没有被调用(如果上面()里面有参数,则这里也要相应的加上参数)
cout << b << endl; //lambda内部修改了值但不影响外面,因为是拷贝进去的
}

关于通过值拷贝的方式捕获的外部变量是只读的原因:

  1. lambda表达式的类型在C++11中会被看做是一个带operator()的类,即仿函数。
  2. 按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量值的。

所以mutable选项的作用就在于取消operator()的const属性。

因为lambda表达式在C++中会被看做是一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void func(int x, int y){
int a;
int b;
using ptr = void(*)(int);
//1.当[]里面为空时,该匿名函数可以看成是一个函数指针
ptr p1 = [](int x){
cout << "x: " << x << endl;
};
p1(3);
/*2.当[]里面有捕获外部变量时,该匿名函数不能看成是一个函数指针,它是一个仿函数
ptr p2 = [=](int x){ //这里是有问题的,一个函数指针指的是仿函数
cout << "x: " << x << endl;
};
p2(9) */
//3.用包装器包装lambda表达式(直接包装)
function<void(int)> fff = [=](int x){
cout << "x: " << x << endl;
}
fff(10);
//4.用绑定器绑定lambda表达式(通过绑定器间接包装)
function<void(int)> fff1 = bind([=](int x){ //返回的是一个仿函数,包装到包装器对象里面
cout << "x: " << x << endl;
},placeholders::_1);
fff1(9);
}

10. 右值引用

在C++11中增加了一个新的类型,即右值引用,标记为 &&。

  • 左值是指存储在内存中、有明确存储地址(可取地址)的数据

  • 右值是指可以提供数据值的数据(不可取地址)

区别方法:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(){
//左值
int num = 9;
//左值引用
int& a = num; //a不占用额外的内存地址,它是num的别名
//右值
//右值引用
int&& b = 8; //必须是使用右值来初始化,左值不行
//常量右值引用
const int&& d = 6;
//常量左值引用可以通过同类型的左值、同类型的右值引用、同类型的常量右值引用、同类型的常量右值引用都可以初始化
const int& c = num; //c只能是num的别名,因为它是一个常量
const int& f = b;
const int& g = d;
const int& h = d;

//const int&& e = b; //错误,常量的右值引用不能通过右值引用来初始化
//int&& f = b; //错误。普通的右值引用也不能通过右值引用来初始化
}

通过上面程序可以得出结论:右值引用只能通过右值来初始化;常量的左值引用是一个万能的引用类型,可以通过同类型的各种引用来初始化左值引用。

在下面程序中,是通过右值引用来模拟浅拷贝。移动构造(右值引用)是把临时对象的指针成员移动走了,临时对象析构的时候析构了一个空指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Test
{
public:
//无参构造
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
cout << "m_num的地址: " << &m_num << endl;
}
//拷贝构造(作用是防止浅拷贝)
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
//移动构造函数 -> 复用其它对象中的资源(堆内存) ---> 移动构造就是要使用浅拷贝
Test(Test&& a) : m_num(a.m_num) { //让新对象的m_num指向了a里面的m_num(浅拷贝)
a.m_num = nullptr; //令a的m_num指向空,这样a析构的时候,释放的就是空
cout << "move construct..." << endl;
}
~Test() {
cout << "destruct Test class ....." << endl;
delete m_num;
}
int *m_num;
};

Test getObj()
{
Test t;
return t;
}

int main()
{
//结论:如果给某一个类指定了移动构造函数,在赋值的时候就有两种方式了,即直接赋值和通过右值引用的方式赋值(不管是哪种方式,要求右边的对象是临时对象)
Test t = getObj(); //只打印1次地址(默认构造时打印)
Test&& t1 = getObj(); //打印的1次地址(默认构造时打印)
cout << "m_num的地址: " << &t1.m_num << endl; //地址不变,与上面t1打印的地址一样
return 0;
}

使用不带参数的构造函数和移动构造都可以实现浅拷贝,区别在于:不带参的构造函数使用浅拷贝,指针资源不会转移,是两个对象的指针指向同一块内存,析构就会出问题。而使用移动构造是实实在在的对资源进行了转移,转移完了之后,原来这个对象就不拥有这块资源了。

在上面程序中,getObj()函数里面创建了一个临时的Test对象并返回它。由于t是一个左值(即它有一个持久的名字),编译器会调用Test类的拷贝构造函数来创建t。而t1是一个右值引用,因此,编译器会调用Test类的移动构造函数来创建t1

右值可以分为两种:一个是将亡值,另一个是纯右值。

  • 纯右值:非引用返回的临时变量,运算表达式产生的临时变量、原始字面量和lambda表达式等。
  • 将亡值:与右值引用相关的表达式,如T&&类型函数的返回值、std::move的返回值等。

10.1 &&的特性

在C++中,并不是所有情况下&&都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下&&被称作未定的引用类型。另外还有一点需要额外注意const T&&表示一个右值引用,不是未定引用类型(是不需要推导的)。

在C++11中引用折叠的规则如下:

  • 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型

  • 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//例子1
template<typename T>
void f(T&& param); //这种是需要根据传进来的参数进行推导的
void f1(const T&& param); //这种是不需要根据传进来的参数进行推导,就一定为右值引用类型
f(10); //传入10(右值),推导出为右值引用类型
int x = 10;
f(x); //传入x(左值),推导出为左值引用类型
f1(10); // 不需要推导,就为右值引用类型

//例子2
int main()
{
int x = 520, y = 1314; //x和y都为左值
auto&& v1 = x; //推导出v1为左值引用类型
auto&& v2 = 250; //推导出v2为右值引用类型
decltype(x)&& v3 = y; //可以得出decltype(x)为int,那么就为int&&,是int型的右值引用(不需要推导),给它赋了一个左值,该语法是错误的
cout << "v1: " << v1 << ", v2: " << v2 << endl;
return 0;
};

//例子3
int&& a1 = 5; //a1为右值引用
auto&& bb = a1; //因为a1本身是一个左值,得到bb是一个int型的左值引用(int& bb)
auto&& bb1 = 5; //bb1是一个右值引用

int a2 = 5; //a2是左值
int &a3 = a2; //a3是左值引用
auto&& cc = a3; //cc是一个左值引用
auto&& cc1 = a2; //cc1是左值引用

const int& s1 = 100; //s1是常量左值引用
const int&& s2 = 100; //s2是常量的右值引用
auto&& dd = s1; //dd是常量的左值引用
auto&& ee = s2; //ee是常量的左值引用

const auto&& x = 5; //常量的右值引用

还有一种情况需要注意,例如在下面程序中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void printValue(int &i)                       //参数在左值引用
{
cout << "l-value: " << i << endl;
}

void printValue(int &&i) //参数是右值引用
{
cout << "r-value: " << i << endl;
}

void forward(int &&k) //参数是右值引用
{
//传进来右值后,k为右值引用,但如果对它进行传递,那么它就是左值引用(k为它的名字)
printValue(k); //用右值引用进行传递,会被看成是左值引用
}

int main()
{
int i = 520; //i是左值
printValue(i); //调用的是左值引用的函数
printValue(1314); //调用的是右值引用的函数
forward(250); //传入参数右值
return 0;
};

输出的结果:

1
2
3
l-value: 520
r-value: 1314
l-value: 250

结论:

  • 左值和右值是独立于他们的类型的,右值引用类型可能是左值也可能是右值。
  • 编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。
  • auto&&或者函数参数类型自动推导的T&&是一个未定的引用类型,它可能是左值引用也可能是右值引用类型,这取决于初始化的值类型(上面有例子)。
  • 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,其余都是左值引用类型。

11. 转移和完美转发

11.1 move方法

std::move可以给右值引用进行初始化,把一些左值转换为右值;还有就是可以进行资源的转移,如果某一个对象后面不再被使用了,并且需要拷贝这个对象里面的数据到另一个对象中,这种情况下就可以进行资源转移,减少拷贝的次数,提高析构的允许效果。

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//move作用1
class Test
{
public
Test(){}
......
}
int main()
{
Test t; //左值
Test && v1 = t; // 通过左值对右值引用进行初始化是错误的,会报错
Test && v2 = move(t); // 通过move,将左值转换为右值,再对右值引用进行初始化,是正确的
return 0;
}

//move作用2
int main(){
int a = 39;
int b = move(a); //a失效了
}

11.2 forward方法

std::forward()函数实现的功能称之为完美转发。因为当一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,就可以使用forward()方法。

std::forward(t);

当T为左值引用类型时,t将被转换为T类型的左值
当T不是左值引用类型时,t将被转换为T类型的右值

测试程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<typename T>
void printValue(T& t)
{
cout << "l-value: " << t << endl;
}

template<typename T>
void printValue(T&& t)
{
cout << "r-value: " << t << endl;
}

template<typename T>
void testForward(T && v)
{
printValue(v);
printValue(move(v));
printValue(forward<T>(v));
cout << endl;
}

int main()
{
testForward(520);
int num = 1314;
testForward(num);
testForward(forward<int>(num));
testForward(forward<int&>(num));
testForward(forward<int&&>(num));
return 0;
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
1.对于testForward(520);            //传入的是右值
void testForward(T && v) //右值引用,如果用它来传递的话,就会变为左值引用
{
printValue(v); //实参为左值 --->打印左
printValue(move(v)); //通过move将左值转为了右值,实参为右值 --->打印右
printValue(forward<T>(v)); //通过forward进行了类型转换,T为右值引用,实参就为右值 --->打印右
}

2.对于testForward(num); //传入的是左值
void testForward(T && v) //左值引用
{
printValue(v); //实参为左值引用 --->打印左
printValue(move(v)); //通过move将左值引用转为了右值,实参为右值 --->打印右
printValue(forward<T>(v)); //通过forward进行了类型转换,T为左值引用,实参就为左值 --->打印左
}

3. 对于testForward(forward<int>(num)); //因为int不是左值引用类型,参数就被转换为右值
void testForward(T && v) //右值引用,如果用它来传递的话,就会变为左值引用
{
printValue(v); //实参为左值引用 --->打印左
printValue(move(v)); //通过move将左值引用转为了右值,实参为右值 --->打印右
printValue(forward<T>(v)); //通过forward进行了类型转换,T为右值引用,实参就为右值 --->打印右
}

4.对于testForward(forward<int&>(num)); //因为int&是左值引用类型,参数就被转换为左值
void testForward(T && v) //左值引用
{
printValue(v); //实参为左值引用 --->打印左
printValue(move(v)); //通过move将左值引用转为了右值,实参为右值 --->打印右
printValue(forward<T>(v)); //通过forward进行了类型转换,T为左值引用,实参就为左值 --->打印左
}

5.testForward(forward<int&&>(num)); //因为int是右值引用类型,参数就被转换为右值
void testForward(T && v) //右值引用,如果用它来传递的话,就会变为左值引用
{
printValue(v); //实参为左值引用 --->打印左
printValue(move(v)); //通过move将左值引用转为了右值,实参为右值 --->打印右
printValue(forward<T>(v)); //通过forward进行了类型转换,T为右值引用,实参就为右值 --->打印右
}

12. 智能指针

智能指针能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。它的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。它的头文件是#include <memory>

C++11中提供了如下三种智能指针:

  1. std::shared_ptr:共享的智能指针
  2. std::unique_ptr:独占的智能指针
  3. std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

12.1 共享智能指针

共享智能指针shared_ptr 是一个模板类,它可以让多个智能指针同时管理同一块有效的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
shared_ptr<int> ptr1(new int(520)); // 使用智能指针管理一块 int 型的堆内存
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; //结果为1

shared_ptr<char> ptr2(new char[12]); // 使用智能指针管理一块字符数组对应的堆内存
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; //结果为1

shared_ptr<int> ptr3; // 创建智能指针对象, 不管理任何内存
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; //结果为0

shared_ptr<int> ptr4(nullptr); // 创建智能指针对象, 初始化为空
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; //结果为0

return 0;
}

当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
shared_ptr<int> ptr1(new int(520)); // 使用智能指针管理一块 int 型的堆内存
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; //结果为1

shared_ptr<int> ptr2(ptr1); //调用拷贝构造函数
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; //结果为2

shared_ptr<int> ptr3 = ptr1; //调用拷贝赋值函数
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; //结果为3

shared_ptr<int> ptr4(std::move(ptr1)); //调用移动构造函数(ptr1失效了)
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; //结果为3

std::shared_ptr<int> ptr5 = std::move(ptr2); //调用移动构造函数(ptr2失效了)
cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl; //结果为3

return 0;
}

也可以通过reset方法,它既可以初始化,也可以重置:对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
// 使用智能指针管理一块 int 型的堆内存(通过make_shared来初始化智能指针)
shared_ptr<int> ptr1 = make_shared<int>(520); //引用计数为1了
shared_ptr<int> ptr2 = ptr1; //引用计数为2了
shared_ptr<int> ptr3 = ptr1; //引用计数为3了
shared_ptr<int> ptr4 = ptr1; //引用计数为4了
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; //结果为4
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; //结果为4
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; //结果为4
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; //结果为4

ptr4.reset(); //重置指针ptr4,原来指向的内存引用计数-1,现在ptr4没有指向任何内存,所以引用计数为0
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; //结果为3
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; //结果为3
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; //结果为3
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; //结果为0

shared_ptr<int> ptr5;
ptr5.reset(new int(250));
cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl; //结果为1

return 0;
}

初始化和使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Test {
public:
Test() { //默认构造函数
cout << "construct Test..." << endl;
}
Test(int x) : m_num(x) { //带参数int的构造函数
cout << "construct Test, x = " << x << endl;
}
Test(string str) { //带参数string的构造函数
cout << "construct Test, str = " << str << endl;
}
~Test() { //析构函数
cout << "destruct Test..." << endl;
}
void setValue(int v) { //赋值m_num
m_num = v;
}
void print() { //打印m_num的值
cout << "m_num:" << m_num << endl;
}

private:
int m_num;
};

int main() {
//通过构造函数初始化
shared_ptr<int> ptr1(new int(3));
cout << "ptr1 use_count:" << ptr1.use_count() << endl; //引用计数为1

//通过移动构造和拷贝函数初始化
shared_ptr<int>ptr2 = move(ptr1);
cout << "ptr1 use_count:" << ptr1.use_count() << endl; //移动资源了,ptr1失效了,引用计数为0
cout << "ptr2 use_count:" << ptr2.use_count() << endl; //引用计数为1

shared_ptr<int>ptr3 = ptr2;
cout << "ptr2 use_count:" << ptr2.use_count() << endl; //引用计数为2
cout << "ptr3 use_count:" << ptr3.use_count() << endl; //引用计数为2
//通过std::make_shared初始化
shared_ptr<int>ptr4 = make_shared<int>(8);
shared_ptr<Test>ptr5 = make_shared<Test>(8); //通过int型构造类对象
shared_ptr<Test>ptr6 = make_shared<Test>("hello, world"); //通过字符串构造类对象

//通过reset初始化
ptr6.reset(); //指针重置,现在ptr6引用计数为0,字符串构成的类对象调用析构函数(没有指针指向它),
cout << "ptr6 use_count:" << ptr6.use_count() << endl; //引用计数为0
ptr5.reset(new Test("hello")); //原来ptr5指向的对象会调用析构函数
cout << "ptr5 use_count:" << ptr5.use_count() << endl; //初始化指针了,引用计数为1,指向完main,

//通过智能指针对象取出原始地址,基于原始地址调用函数
Test* t = ptr5.get(); //这里是通过智能指针对象调用它所对应的类,该类是shared_ptr类里面提供的标准api,所以是加.
t->setValue(1000);
t->print();
//通过智能指针对象直接操作
ptr5->setValue(999); //这时通过智能指针对象去调用它管理的内存对应的类里面的api函数,就按照指针的方式使用对象,所以加->
ptr5->print();
}

指定删除器:当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。当然我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Test {
public:
Test() { //默认构造函数
cout << "construct Test..." << endl;
}
Test(int x) : m_num(x) { //带参数int的构造函数
cout << "construct Test, x = " << x << endl;
}
Test(string str) { //带参数string的构造函数
cout << "construct Test, str = " << str << endl;
}
~Test() { //析构函数
cout << "destruct Test..." << endl;
}
void setValue(int v) { //赋值m_num
m_num = v;
}
void print() { //打印m_num的值
cout << "m_num:" << m_num << endl;
}

private:
int m_num;
};

int main(){
shared_ptr<Test> pp(new Test(39),[](Test* t){ //在这里,不手动添加删除器也可以,系统会调用默认的删除器
//释放内存的操作
cout<<"-----------------------"<<endl;
delete t;
});
//shared_ptr<Test> p1(new Test[5]); //这样会报错,因为只构造5个对象了,但没有析构对象
shared_ptr<Test[]> p1(new Test[5]); //这样不会报错
shared_ptr<Test> p1(new Test[5], [](Test* t) { //正确
delete[] t;
});
//调用c++提供的默认删除器函数
shared_ptr<Test> p2(new Test[5], default_delete<Test[]>()); //这样也正确,默认删除器要指出删除的类型
}

注意:shared_ptr在通过指针对象去管理一块数组内存的时候,必须手动添加删除器,如果不是数组,智能指针默认提供的删除器就会删除这块内存。

12.2 独占智能指针

std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr

初始化和使用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(){
//通过构造函数初始化
unique_ptr<int> ptr1(new int(9));

//通过移动构造函数初始化
unique_ptr<int> ptr2 = move(ptr1); //ptr1失效了

//通过reset初始化
ptr2.reset(new int(8)); //ptr2原来指向的内存被析构了,重新指向了新的内存块

//使用 ---> 和共享智能指针一样
unique_ptr<Test> ptr3(new Test(1));
Test* pt = ptr3.get(); //获取原始指针(普通指针)
pt->setValue(3);

ptr3->setValue(9); //直接使用指针调用指向的内存块里面的api

}

删除器:unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以它不能像shared_ptr那样直接指定删除器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shared_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	  // 正确的,不需要指定删除器类型
unique_ptr<int> ptr1(new int(10), [](int*p) {delete p; }); // 错误的,没有指定删除器的类型

int main()
{
//情况1:删除器的[]里面为空,则对应的lambda是函数指针类型
using func_ptr = void(*)(int*); //定义一个函数指针类型
unique_ptr<int, func_ptr> ptr1(new int(10), [](int*p) {delete p; }); //需要在前面指定删除器类型

//情况2:删除器的[]里面添加了=,则对应的lambda是仿函数类型,就需要通过可调用对象包装器对其类型进行包装,
unique_ptr<int, func_ptr> ptr1(new int(10), [=](int*p) {delete p; }); //这种写法会报错
unique_ptr<int, function<void(int*)>> ptr1(new int(10), [](int*p) {delete p; }); //正确的

//独占的智能指针可以管理数组类型的地址,能够自动释放
unique_ptr<int[]> ptr5(new int[3]); //这是正确的
//在c++11中shared_ptr不支持下面的写法,c++11之后才支持的
shared_ptr<int[]> ptr5(new int[3]); //现在这是正确的
return 0;
}

回忆:对于lambda表达式,[]里面为空,则对应的lambda表达式为函数指针类型;否则为仿函数类型。

12.3 弱引用智能指针

std::weak_ptr可以看做是shared_ptr的助手,它不管理shared_ptr内部的指针。weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在。

初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() 
{
shared_ptr<int> sp(new int);

weak_ptr<int> wp1; //wp1没有被实例化
weak_ptr<int> wp2(wp1); //wp2也没有被实例化
weak_ptr<int> wp3(sp); //wp3是被实例化了

weak_ptr<int> wp4;
wp4 = sp; //直接通过赋值的方式来初始化(实例化)
weak_ptr<int> wp5;
wp5 = wp3; //实例化了
return 0;
}

其他常用方法:

  • 通过调用weak_ptr类提供的use_count()方法可以获得当前所观测资源的引用计数。
  • 通过调用weak_ptr类提供的expired()方法来判断观测的资源是否已经被释放,如果观察的资源的引用计数为0了,返回的就是true,否则返回false。
  • 通过调用weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象。
  • 通过调用weak_ptr类提供的reset()方法来清空对象,使其不监测任何资源。

演示程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
shared_ptr<int> sp1, sp2;
weak_ptr<int> wp;

sp1 = std::make_shared<int>(520); //给sp1管理了一块int类型的内存
wp = sp1; //初始化了wp,它可以观察sp1管理的内存了
sp2 = wp.lock(); //通过wp.lock()返回的共享指针的实例,初始化了sp2
cout << "use_count: " << wp.use_count() << endl; //现在就有2个共享指针指向那块内存了,打印为2

sp1.reset(); //sp1不管理该内存了
cout << "use_count: " << wp.use_count() << endl; //现在只有sp2管理该内存块,打印1

sp1 = wp.lock(); //因为wp检测了那块内存还没有被释放,所以还可以返回对应的共享指针对象,实例化sp1
cout << "use_count: " << wp.use_count() << endl; //现在是sp1和sp2两个共享指针管理该内存,打印2

cout << "*sp1: " << *sp1 << endl; //打印520
cout << "*sp2: " << *sp2 << endl; //打印520

return 0;
}

12.4 智能指针的注意事项

shared_ptr使用的注意事项:

1.不能使用一个原始地址初始化多个共享智能指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Test{
shared_ptr<Test> getSharedPtr(){
return shared_ptr<Test>(this);
}
~Test(){
cout<<"class Test is disstruct..."<<endl;
}
};

int main(){
//这种情况会报错,因为析构了两次
Test* t = new Test;
shared_ptr<Test>ptr1(t);
//shared_ptr<Test>ptr2(t); //这种情况会报错,因为析构了两次
shared_ptr<Test>ptr2 = ptr1; //这种就正确
}

2.函数不能返回管理了this的共享智能指针对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Test {
shared_ptr<Test> getSharedPtr() {
return shared_ptr<Test>(this); //返回一个共享指针对象,管理的是this,即外面new出来的一个对象
}
~Test() {
cout << "class Test is disstruct..." << endl;
}
};

int main() {
shared_ptr<Test>ptr1(new Test);
cout << ptr1.use_count() << endl; //打印1
shared_ptr<Test>ptr2 = ptr1->getSharedPtr(); //因为里面的this指的是ptr1初始化时的new Test,所以本质上是犯了注意事项1的问题,会报错
cout << ptr2.use_count() << endl; //打印1
}

在这个例子中使用同一个指针this构造了两个智能指针对象ptr1和ptr2,这二者之间是没有任何关系的,因为ptr2并不是通过ptr1初始化得到的实例对象。在离开作用域之后this将被构造的两个智能指针各自析构,导致重复析构的错误。

这个问题可以通过一个模板类叫做std::enable_shared_from_this<T>来解决,这个类中有一个方法叫做shared_from_this(),通过这个方法可以返回一个共享智能指针,在该函数的底层就是使用weak_ptr来监测this对象,并通过调用weak_ptrlock()方法返回一个shared_ptr对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Test : enable_shared_from_this<Test>{        //把当前的类型指定在模板类型里面
shared_ptr<Test> getSharedPtr() {
return shared_from_this(); //底层是通过弱引用类型对象返回一个share_ptr对象
}
~Test() {
cout << "class Test is disstruct..." << endl;
}
};
int main() {
//weak_ptr初始化的地方:在ptr1初始化时,new出了一块内存Test让ptr1来管理,而Test类继承了enable_shared_from_this,那么其里面的weak_ptr被实例化了,指向了ptr1
shared_ptr<Test>ptr1(new Test);
cout << ptr1.use_count() << endl;
shared_ptr<Test>ptr2 = ptr1->getSharedPtr();
cout << ptr2.use_count() << endl;
}

3.共享智能指针不能循环引用

13. POD类型

POD指的就是普通的旧数据 。它通常用于说明一个类型的属性,尤其是用户自定义类型的属性。

在C++11中将 POD划分为两个基本概念的合集,即∶平凡的(trivial)标准布局的(standard layout)

## 13
.1 平凡类型

一个平凡的类或者结构体应该符合以下几点要求:

  1. 拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)。
  2. 拥有平凡的拷贝构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)。
  3. 拥有平凡的拷贝赋值运算符(trivial assignment operator)和移动赋值运算符(trivial move operator)。
  4. 不包含虚函数以及虚基类。

13.2 非受限联合体

在C++11之前我们使用的联合体是有局限性的,主要有以下三点:

  • 不允许联合体拥有非POD类型的成员
  • 不允许联合体拥有静态成员
  • 不允许联合体拥有引用类型的成员

在新的C++11标准中,取消了关于联合体对于数据成员类型的限定,规定任何非引用类型都可以成为联合体的数据成员,这样的联合体称之为非受限联合体(Unrestricted Union)。

13.3 非受限联合体中静态成员的使用

补充知识:

  • 如果在联合体里面出现了静态成员变量,那么它的初始化要放到联合体的外面(类和结构体也一样的)
  • 静态成员是属于类的,而不是属于对象(类和结构体也一样的)
  • 在联合体里面的静态成员和非静态成员使用的不是同一块内存(类和结构体也一样的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
union Test {
//不允许出现静态成员(成员变量、成员函数)
static int num;
static void print() {}
int num1;
};
int Test::num = 3;

int main() {
Test t1;
t1.num1 = 100;
cout << "static num value: " << t1.num << endl; //结果为3 --->外部初始化的值
cout << "num1 value: " << t1.num1 << endl; //结果为100

Test t2;
t2.num = 50;
cout << "static num value: " << t2.num << endl; //结果为50
cout << "static num value: " << t1.num << endl; //结果为50,静态成员被对象t2修改,t1和t2的静态成员是同一个
}

13.4 非受限联合体中非POD类型成员的使用

在c++11里面规定,如果在非受限联合体中使用了非POD里面的成员,编译器就会自动的删除这个联合体的构造函数和析构函数

placement new:一般情况下,使用new申请空间时,是从系统的堆(heap)中分配空间,申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这种操作就叫做placement new即定位放置 new。

使用定位放置new申请内存空间:ClassName* ptr = new (定位的内存地址)ClassName;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Teacher {         //将该类定义为非TOB类型
public:
Teacher() {} //不加这个就是TOB类型的类
void setText(string s) {
name = s;
}
private:
string name;
};

union Student {
Student() { //手动创建一个构造函数
new (&name)string; //选择一块占用空间最大的变量来定义联合体的内存空间
}
~Student() {}
int id;
Teacher t;
string name;
};

int main() {
Student s;
s.name = "lxx";
s.t.setText("hello"); //占用的都是同一块空间
cout << "s.name=" << s.name << endl; //打印的是hello
}

13.5 标准布局的

标准布局类型主要主要指的是类或者结构体的结构或者组合方式。

标准布局类型的类应该符合以下五点定义,最重要的为前两条:

1.所有非静态成员有相同 的访问权限(public,private,protected)

  • 类成员拥有不同的访问权限(非标准布局类型)
  • 类成员拥有相同的访问权限(标准布局类型)

2.在类或者结构体继承时,满足以下两种情况之一∶


  • 派生类中有非静态成员,基类中包含静态成员(或基类没有变量)。
  • 基类有非静态成员,而派生类没有非静态成员。
1
2
3
4
5
6
7
8
9
10
struct Base { static int a;};
struct Child: public Base{ int b;}; // POD类型,既是平凡类型,也是标准布局类型
struct Base1 { int a;};
struct Child1: public Base1{ static int c;}; // POD类型
struct Child2:public Base, public Base1 { static int d;); // POD类型,子类和基类没有同时出现非静态成员变量
struct Child3:public Base1{ int d;}; // 不满足条件,子类和基类同时出现了非静态成员
struct Child4:public Base1, public Child // 不满足条件。继承的多个基类中,有多个基类有非静态成员
{
static int num;
};

结论:非静态成员只要同时出现在派生类和基类间,即不属于标准布局。对于多重继承,一旦非静态成员出现在多个基类中,即使派生类中没有非静态成员变量,派生类也不属于标准布局。

3.子类中第一个非静态成员的类型与其基类不同

1
2
3
4
5
struct Base { static int a;};
struct Child: public Base{
Base p; //Base p放到这里不是POD类型,如果放到int b之后,就是POD类型
int b;
};

4.没有虚函数和虚基类

5.所有非静态数据成员均符合标准布局类型,其基类也符合标准布局,这是一个递归的定义

14. 扩展的 friend 语法

friend关键字用于声明类的友元,友元可以无视类中成员的属性(public、protected 或是 private),友元类或友元函数都可以访问,这样虽然完全破坏了面向对象编程中封装性的概念。但有的时候friend关键字确实会让程序少写很多代码。

14.1 语法改进

声明一个类为另外一个类的友元时,不再需要使用class关键字,并且还可以使用类的别名(使用 typedef 或者 using 定义)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Tom;                   //先申明有该类
using Honey = Tom; //定义一个别名

class Jack {
//friend class Tom; //c++98标准
friend Tom; //c++11标准
private:
string name = "jack";
void print() {
cout << "my name is " << name << endl;
}
};

class Lucy {
friend Honey;
protected:
string name = "lucy";
void print() {
cout << "my name is " << name << endl;
}
};

class Tom {
public:
void print() {
cout << j.name << endl << l.name << endl;
j.print();
l.print();
}
private:
Jack j;
Lucy l;
};

int main() {
Tom t;
t.print();
}

14. 命名空间this_thread

std::this_thread是关于线程的命名空间,在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。

14. 1 get_id()

std::this_thread中的get_id()方法可以得到当前线程的线程ID。测试程序如下:

1
2
3
4
5
6
7
8
9
10
11
void func()
{
cout << "子线程: " << this_thread::get_id() << endl; //得到当前线程的线程ID(子线程)
}

int main()
{
cout << "主线程: " << this_thread::get_id() << endl; //得到当前线程的线程ID(主线程)
thread t(func); //指定的函数func()会在子线程中执行
t.join();
}

14.2 sleep_for()

与进程一样,线程被创建后也有五种状态:创建态就绪态运行态阻塞态(挂起态)退出态(终止态)

众所周知的,在计算机中启动的多个线程都需要占用CPU资源,但是CPU的个数是有限的并且每个CPU在同一时间点不能同时处理多个任务。为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到CPU时间片的情况)。

而sleep_for()是用于让线程休眠的函数,调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。这个函数的参数需要指定一个休眠时长,是一个时间段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func()
{
for (int i = 0; i < 10; ++i)
{
this_thread::sleep_for(chrono::seconds(1)); //休息1s
cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
}
}

int main()
{
thread t(func);
t.join();
}

需要注意的是:程序休眠完成之后,会从阻塞态重新变成就绪态就绪态的线程需要再次争抢CPU时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。

14.3 sleep_until()

sleep_until()也是一个休眠函数,它是指定线程阻塞到某一个指定的时间点(time_point类型),之后解除阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void func()
{
for (int i = 0; i < 10; ++i)
{
auto now = chrono::system_clock::now(); // 获取当前系统时间点
chrono::seconds sec(2); // 时间间隔为2s
this_thread::sleep_until(now + sec); // 在当前时间点之后休眠两秒
cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
}
}

int main()
{
thread t(func);
t.join();
}

14.4 yield()

当线程中调用这个函数yield()之后,如果它正处于运行态,那么它会主动让出自己已经抢到的CPU时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到CPU时间片了。需要注意的是,线程调用了yield()之后会主动放弃CPU资源,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况,这是概率问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void func()
{
for (int i = 0; i < 100000000000; ++i)
{
cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
this_thread::yield();
}
}

int main()
{
thread t(func);
thread t1(func);
t.join();
t1.join();
}

结论:

  • yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降
  • yield() 是让当前线程主动放弃了当前自己抢到的CPU资源,但是在下一轮还会继续抢

15. 线程类

可执行文件的运行过程:如果在磁盘上有一个可执行的文件a.exe(占用的是硬盘资源),双击该文件,就可以得到一个应用程序(占用内存和cpu资源),将该应用程序称为进程。这个进程在物理内存里面占一块虚拟地址空间,相对于应用程序来说只能算一个中转站,是我们把程序启动起来之后,程序里面所有的数据被加载到这块虚拟地址空间里面,然后通过cpu里面的MMU(cpu里面的内存管理单元),它会把虚拟地址空间里面的数据映射到物理内存的另外一个位置。

每一个启动起来的进程都对应一块虚拟空间,其由两部分组成,一部分是内核区,一部分是用户区。如果是多进程,每个进程都对应一个虚拟地址空间。比如说进程1创建了一个新的进程2,在操作系统里面就会额外的再次分配一个虚拟地址空间。每个进程都对应一个虚拟地址空间。如果是线程的话,不会创建额外的虚拟地址空间,即多个线程是共用同一个虚拟地址空间的。

虚拟内存空间是虚拟的,一个进程存储的数据都是存储在虚拟内存映射的那块物理内存中,作用就是保护数据,确保进程之间的隔离,提高系统的安全性和稳定性,其中磁盘作为辅助。具体来说,当进程访问虚拟地址时,操作系统会将这些地址映射到实际的物理内存地址中,物理内存满了的话,就会将物理内存的一些不常用的数据存到磁盘,从而空出多余的内存空间。我们通过代码打印出来的地址都是物理内存里面分配的虚拟空间地址。

虚拟内存地址到物理内存地址的映射是通过分页或分段等机制来实现的,当通过虚拟地址空间写入数据时,如果对应的虚拟页在物理内存中,则直接在物理内存中进行修改。如果物理内存满了,操作系统会将不常用的页面换出到磁盘,确保有足够的内存用于当前的写入操作。

15.1 线程初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void func()
{
cout << "我是子线程,叫做lxx..." << this_thread::get_id() << endl;
}

void func1(string name, int age)
{
cout << "我是子线程,叫做" << name << ",age:" << age << this_thread::get_id() << endl;
}


int main()
{
cout << "主线程id: " << this_thread::get_id() << endl;
//1.创建空的线程对象
thread t1;
//2.创建一个可用的子线程
thread t2(func);
//3.创建一个带参数的可用子线程
thread t3(func1, "罗罗诺亚", 19);
//4.创建一个匿名函数的子线程(带一个参数)
thread t4([=](int id) {
cout << "arg id:" << id << "thread id:" << this_thread::get_id() << endl;
},100);
//5.通过移动构造的方式创建子线程
thread t5 = move(t4);
t2.join();
t3.join();
t5.join();
return 0;
}

15.2 常用的成员函数

1.get_id():每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的。通过该函数可用获取线程的ID。

2.join():调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。

3.detach():因为在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。

4.joinable():该函数用于判断主线程和子线程是否处于关联(连接)状态,一般情况下,二者之间的关系处于关联状态,会返回一个true,否则返回一个false。

5.thread::hardware_concurrency():用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程。

15.3 类的成员函数作为子线程的任务函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Base {
public:
void showMsg(string name, int age) {
cout << "我的名字是:" << name << ",今年 " << age << "岁了..." << endl;
}
static void message() {
cout << "我是罗罗诺亚!!!" << endl;
}
};

void func() {
cout << "hello,欢迎来到新世界!!!" << endl;
}

int main() {
thread t1(func); //普通函数作为任务函数

//传递静态成员函数
thread t2(&Base::message); //只需要指明静态函数属于的类,传入其地址即可

//传递非静态成员函数
Base b;
thread t3(&Base::showMsg, b, "卡卡罗特", 23);
//thread t3(&Base::showMsg, &b, "卡卡罗特", 23); //也可用区b的地址
thread t4(bind(&Base::showMsg, b, "卡卡罗特", 23)); //通过仿函数传入,b也可用取地址

t1.join();
t2.join();
t3.join();
t4.join();
}

16. call_once

在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。特别注意的是,在使用call_once()的时候,需要一个once_flag作为call_once()的传入参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
once_flag g_flag;                  //定义一个全局的once_flag变量,保证所有的线程都能访问到它
void do_once(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}

void do_something(int age, string name)
{
static int num = 1;
call_once(g_flag, do_once, 19, "luffy"); //call_once的do_once()函数只会被调用1次
cout << "do_something() function num = " << num++ << endl; //每个线程都会调用这个多次
}

int main()
{
thread t1(do_something, 20, "ace");
thread t2(do_something, 20, "sabo");
thread t3(do_something, 19, "luffy");
t1.join();
t2.join();
t3.join();
return 0;
}

结果:

1
2
3
4
name: luffy, age: 19
do_something() function num = 1
do_something() function num = 2
do_something() function num = 3

应用:实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//编写一个单例模式的类 ---> 懒汉模式
once_flag g_flag;

class Base {
public:
Base(const Base& obj) = delete; //拷贝构造显示的删除(也可用设为私有)
Base& operator=(const Base& obj) = delete; //拷贝赋值显示的删除
//获取单例类的对象
static Base* getInstance() { //静态函数能够访问的成员只能是静态成员
//被call_once管理的这个匿名函数所对应的处理动作只会被调用1次
call_once(g_flag, [&]() {
obj = new Base;
cout << "实例对象被创建....." << endl;
});
return obj;
}

void setName(string name) {
this->name = name;
}

string getName() {
return name;
}
private:
Base() {};
static Base* obj; //将单例对象设为静态的
string name;
};
Base* Base::obj = nullptr; //静态成员的初始化要放到外面

void myFunc(string name) {
Base::getInstance()->setName(name);
cout << "my name is: " << Base::getInstance()->getName() << endl;
}
int main() {
thread t1(myFunc, "路飞");
thread t2(myFunc, "艾斯");
thread t3(myFunc, "萨博");
thread t4(myFunc, "索隆");
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}

17. C++线程同步之互斥锁

解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,也可以称为互斥量,其头文件是#include <mutex>,在C++11中一共提供了四种互斥锁:

  • std::mutex:独占的互斥锁,不能递归使用
  • std::timed_mutex:带超时的独占互斥锁,不能递归使用
  • std::recursive_mutex:递归互斥锁,不带超时功能
  • std::recursive_timed_mutex:带超时的递归互斥锁

17.1 std::mutex

成员函数:

1.lock():函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用。

2.try_lock():获取互斥锁的所有权并对互斥锁加锁,它与lock()区别在于try_lock()不会阻塞线程,lock()会阻塞线程。

  • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
  • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false

3.unlock():给锁定的互斥锁解锁,只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。

线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

特别需要注意的是:当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Base {
public:
void increment(int count) { //对成员变量进行++操作
for (int i = 0; i < count; i++) {
mx.lock();
++number;
cout << "+++current number: " << number << endl;
mx.unlock();
this_thread::sleep_for(chrono::milliseconds(500)); //休眠0.5s
}
}
void decrement(int count) { //对成员变量进行--操作
for (int i = 0; i < count; i++) {
mx.lock();
--number;
cout << "---current number: " << number << endl;
mx.unlock();
this_thread::yield(); //放弃抢夺时间片,开始下一轮的的抢夺
}
}
private:
int number = 999;
mutex mx;
};

int main() {
Base b;
thread t1(&Base::increment, &b, 10);
thread t2(&Base::decrement, &b, 10);
t1.join();
t2.join();
}

17.2 std::lock_guard

lock_guard在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。

使用:

1
2
3
4
5
6
7
8
9
10
void decrement(int count) {               //对成员变量进行--操作
for (int i = 0; i < count; i++) {
{
lock_guard<mutex> guard(mx); //创建了一个guard对象
--number;
cout << "---current number: " << number << endl;
} //声明周期结束,自动析构,解锁
this_thread::yield(); //放弃抢夺时间片,开始下一轮的的抢夺
}
}

17.3 std::recursive_mutex

递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Base {
public:
void increment(int count) {
for (int i = 0; i < count; i++) {
mx.lock();
++number;
cout << "+++current number: " << number << endl;
mx.unlock();
this_thread::sleep_for(chrono::milliseconds(500)); //休眠0.5s
}
}
void decrement(int count) {
for (int i = 0; i < count; i++) {
{
lock_guard<recursive_mutex>lock_guard(mx); //使用递归互斥锁(如果使用的不是递归互斥锁,该程序会报错)
increment(2);
--number;
cout << "---current number: " << number << endl;
}
this_thread::yield(); //放弃抢夺时间片,开始下一轮的的抢夺
}
}
private:
int number = 999;
recursive_mutex mx;
};

int main() {
Base b;
thread t1(&Base::increment, &b, 10);
thread t2(&Base::decrement, &b, 10);
t1.join();
t2.join();
}

17.4 std::timed_mutex

std::timed_mutex是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。

18. 原子变量atomic

C++11提供了一个原子类型std::atomic<T>,通过这个原子类型管理的内部变量就可以称之为原子变量,我们可以给原子类型指定boolcharintlong指针等类型作为模板参数。

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Base {
public:
Base(int n, string s) : age(n), name(s) {};

int age;
string name;
};

int main() {
//初始化
atomic<char>c('a');
atomic_char cc('b'); //效果与上面这种写法一样
atomic<int> b;
atomic_init(&b, 9); //对b进行初始化

//修改
cc = 'd';
b.store(88);
char ccc = c.exchange('e'); //c现在存储的是e,exchange返回原来存的内容a,即ccc是a

//获取
Base base(123, "luffy");
atomic<Base*>atc_base(&base); //原子类型的模板类可以是指针类型
cout << "c value: " << c << endl;
cout << "ccc value: " << ccc << endl;
cout << "b value: " << b.load() << endl;
Base* tmp = atc_base.load(); //取出地址
cout << "name: " << tmp->name << ",age: " << tmp->age << endl;
return 0;
}

局限性:使用原子变量的时候,它不能够保护复合类型,比如说atomic<Base*> base,现在base里面存储的就是Base类的指针,Base原子变量里面现在不能保护base这块指针指向的内存块里面的数据安全,它只能保护这个指针做算术运算时的线程安全,即将地址做移动时安全。

原子变量处理线程同步:使用原子变量或互斥量对公共资源进行处理,可以达到数据同步的效果,即以下程序最后可以刚好将number加到200,否则,可能小于200。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base {
public:
void increment() {
for (int i = 0; i < 100; i++) {
number++;
cout << "+++ increment thread id: " << this_thread::get_id() << ",number: " << number << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
}
void increment1() {
for (int i = 0; i < 100; i++) {
number++;
cout << "*** increment1 thread id: " << this_thread::get_id() << ",number: " << number << endl;
this_thread::sleep_for(chrono::milliseconds(50));
}
}
private:
atomic_int number = 0; //把一个整形数放到原子变量的类对象里面

};

int main() {
Base b;
thread t1(&Base::increment, &b);
thread t2(&Base::increment1, &b);
t1.join();
t2.join();
}