C++ 巩固进阶之四

大纲

C++ 智能指针深入理解

智能指针的几种类型

在 C++ 中,智能指针的类型有以下几种:

  • (1) 带引用计数的智能指针:shared_ptr
  • (2) 不带引用计数的智能指针:auto_ptrscoped_ptrunique_ptr
  • (3) 特殊的智能指针:weak_ptr(不增加引用计数,可以用于避免 shared_ptr 发生循环引用)
智能指针所有权带引用计数适用场景核心特性
auto_ptr独占(拷贝时转移)⚠ 已废弃,建议改用 unique_ptr独占所有权,在复制或赋值时会转移所有权,导致原指针变为空(nullptr
scoped_ptr独占生命周期受限于作用域,适用于简单的场景,避免资源泄漏独占所有权,不可复制或赋值,不支持移动语义,即不可以使用 std::move() 函数转移所有权
unique_ptr独占资源独占,生命周期明确独占所有权,不可复制(拷贝构造和赋值),但可以移动(移动构造和移动赋值),即支持使用 std::move() 函数转移所有权
shared_ptr共享资源共享,生命周期不固定共享所有权(允许多个智能指针管理同一个资源)
weak_ptr观察 shared_ptr避免 shared_ptr 发生循环引用不增加引用计数,用于避免 shared_ptr 发生循环引用,可以通过 lock() 函数转换为 shared_ptr

智能指针的基础使用

不带引用计数的智能指针

auto_ptr 智能指针
概念介绍

auto_ptr 是 C++ 98 引入的智能指针,具有独占所有权,在复制或赋值时会转移所有权,导致原指针变为空(nullptr)。它可以自动释放资源,但由于容易导致空指针问题,在 C++ 11 被弃用,推荐使用 unique_ptrshared_ptr 代替。auto_ptr 的核心特性如下:

  • 独占所有权(所有权转移):

    • 不能有多个 auto_ptr 共享同一资源。
    • 复制或赋值时,所有权会从原指针转移到新指针,原指针会变为空(nullptr)。
  • 自动释放资源:

    • auto_ptr 离开作用域时,会自动调用 delete 释放资源,防止内存泄漏。
  • 已被 C++ 11 弃用:

    • 由于所有权转移容易导致空指针问题,auto_ptr 在 C++ 11 中已经被 unique_ptr 或者 shared_ptr 取代。
使用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>

using namespace std;

void test01() {
auto_ptr<int> ptr1(new int(20)); // ptr1 拥有这个动态分配的 int(20)
auto_ptr<int> ptr2(ptr1); // 可复制,ptr1 的所有权被 ptr2 接管,ptr1 变为空
*ptr2 = 10; // 通过 ptr2 修改资源的值
// cout << *ptr1 << endl; // 程序运行出错,试图解引用 ptr1,会导致未定义行为
cout << *ptr2 << endl; // 正常输出 10
}

void test02() {
auto_ptr<int> ptr1(new int(20)); // ptr1 拥有这个动态分配的 int(20)
auto_ptr<int> ptr2(new int(30)); // 可赋值,ptr2 拥有这个动态分配的 int(30)
ptr2 = ptr1; // ptr1 的所有权被 ptr2 接管,ptr1 变为空
// cout << *ptr1 << endl; // 程序运行出错,试图解引用 ptr1,会导致未定义行为
cout << *ptr2 << endl; // 正常输出 20
}
scoped_ptr 智能指针
概念介绍

scoped_ptr 是 Boost 库提供的一种简单智能指针,独占所有权,不可复制或赋值,不支持移动语义,即不可以使用 std::move() 函数转移所有权,在离开作用域时自动释放资源。类似于 unique_ptr,但不支持移动语义,适用于 RAII(资源管理即初始化) 场景。在 C++ 11 之后,推荐使用 unique_ptr 代替。scoped_ptr 的核心特性如下:

  • 独占所有权:

    • 只能有一个 scoped_ptr 拥有资源,不可复制或赋值。
  • 自动释放资源:

    • 离开作用域时自动调用 delete 释放资源,防止内存泄漏。
  • 不支持移动语义:

    • 不能使用 std::move() 函数转移所有权(但相比 unique_ptr,功能更简单)。
  • 适用于 RAII(资源管理即初始化):

    • 适合管理动态分配的对象,确保作用域结束时资源被释放。

提示

阅读 scoped_ptr 的底层源码,可以发现它的拷贝构造函数和赋值运算符重载函数都被 delete 掉,所以它才不支持复制或赋值(转移)。

使用案例
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
#include <iostream>
#include <boost/scoped_ptr.hpp>

using namespace std;
using namespace boost;

void test01() {
boost::scoped_ptr<int> ptr1(new int(10));
// boost::scoped_ptr<int> ptr2(ptr1); // 编译出错,不可复制
}

void test02() {
boost::scoped_ptr<int> ptr1(new int(10));
boost::scoped_ptr<int> ptr2(new int(30));
// ptr2 = ptr1; // 编译出错,不可赋值
}

void test03() {
boost::scoped_ptr<int> ptr1(new int(10));
// boost::scoped_ptr<int> ptr2(move(ptr1)); // 编译出错,不支持移动构造
}

void test04() {
boost::scoped_ptr<int> ptr1(new int(10));
boost::scoped_ptr<int> ptr2(new int(30));
// ptr2 = move(ptr1); // 编译出错,不支持移动赋值
}
unique_ptr 智能指针
概念介绍

unique_ptr 是 C++ 11 引入的智能指针,独占所有权,不可复制(拷贝构造和赋值),但可以移动(移动构造和移动赋值),即支持使用 std::move() 函数转移所有权。它在离开作用域时自动释放资源,适用于 RAII(资源管理即初始化),是 auto_ptrscoped_ptr 的现代替代方案。unique_ptr 的核心特性如下:

  • 独占所有权:

    • 只能有一个 unique_ptr 拥有同一资源,不可复制。
  • 支持移动语义:

    • 可通过 std::move() 函数转移所有权。
  • 自动释放资源:

    • 离开作用域时自动调用 delete 释放资源,防止内存泄漏。
  • 支持自定义删除器:

    • 可指定自定义删除器,如 std::default_delete 或 lambda。
  • 轻量且性能优越:

    • 不会有额外的引用计数(相比 shared_ptr 而言),适用于 RAII 管理单一对象。

提示

  • 阅读 unique_ptr 的底层源码,可以发现它使用了带右值引用参数的拷贝构造函数和赋值运算符重载函数,所以它才可以通过 std::move() 函数转移所有权。
使用案例
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
#include <iostream>
#include <memory>

using namespace std;

void test01() {
unique_ptr<int> ptr1(new int(30));
// unique_ptr<int> ptr2(ptr1); // 编译出错,不可复制
}

void test02() {
unique_ptr<int> ptr1(new int(30));
unique_ptr<int> ptr2(new int(10));
// ptr2 = ptr1; // 编译出错,不可赋值
}

void test03() {
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2(move(ptr1)); // 支持移动构造,将 ptr1 的所有权转移到 ptr2,ptr1 变为空
// cout << *ptr1 << endl; // 程序运行出错,试图解引用 ptr1,会导致未定义行为
cout << *ptr2 << endl; // 正常输出 10
}

void test04() {
unique_ptr<int> ptr1(new int(30));
unique_ptr<int> ptr2(new int(10));
ptr2 = move(ptr1); // 支持移动赋值,将 ptr1 的所有权转移到 ptr2,ptr1 变为空
// cout << *ptr1 << endl; // 程序运行出错,试图解引用 ptr1,会导致未定义行为
cout << *ptr2 << endl; // 正常输出 30
}

带引用计数的智能指针

C++ 11 带引用计数的智能指针允许多个指针共享同一个资源。当最后一个指向资源的智能指针被销毁时,资源才会被释放。它通过引用计数来跟踪有多少个智能指针指向同一资源,确保资源在不再使用时自动释放,避免内存泄漏。

  • C++ 11 带引用计数的智能指针有两种,分别是:
    • shared_ptr:强引用智能指针,可以改变资源的引用计数
    • weak_ptr:弱引用智能指针,不会改变资源的引用计数,主要用于避免 shared_ptr 智能指针的循环引用
shared_ptr 智能指针
概念介绍

shared_ptr 是 C++ 11 引入的智能指针,允许多个指针共享同一资源。它通过引用计数来跟踪有多少个指针指向同一对象,当最后一个指向该资源的 shared_ptr 被销毁时,资源会自动释放。适用于需要多个所有者共享同一资源的场景,即需要多个智能指针管理同一个资源。shared_ptr 的核心特性如下:

  • 共享所有权
    • 多个 shared_ptr 可以同时指向同一对象,共享资源的所有权。
  • 引用计数
    • 每个 shared_ptr 内部维护一个引用计数,表示当前有多少个指针引用该资源。
  • 自动管理内存
    • 当引用计数归零时,资源被自动释放。
使用案例

在创建 shared_ptr 智能指针时,建议使用 make_shared() 函数来创建,而不是直接 new,其原因如下:

  • 使用 make_shared() 函数

    • 写法:shared_ptr<Person> sp1 = make_shared<Person>();
    • 只进行一次内存分配(对象数据和控制块在同一块内存中),可以减少开销。
    • 资源的内存分配和 shared_ptr 创建是原子操作,可以避免内存泄漏和异常安全问题。
  • 使用 new 关键字

    • 写法:shared_ptr<Person> sp1(new Person());
    • 需要进行两次内存分配(一次是给 Person,另一次是给 shared_ptr 的控制块),性能稍低。
    • newshared_ptr 绑定是分离的,如果 shared_ptr 还没成功创建,但 new 已经分配内存,这可能会导致内存泄漏。
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
#include <iostream>
#include <memory>

using namespace std;

class Person {

public:
Person() {
cout << "Person()" << endl;
}

~Person() {
cout << "~Person()" << endl;
}

void show() {
cout << "Using Person" << endl;
}

};

int main() {
// 创建一个 shared_ptr sp1,并管理 Person 对象
shared_ptr<Person> sp1 = make_shared<Person>();

// 不建议这样写
// shared_ptr<Person> sp1(new Person());

// 打印引用计数
cout << "Reference count: " << sp1.use_count() << endl;

{
// 创建一个 shared_ptr sp2,共享 sp1 的资源
shared_ptr<Person> sp2 = sp1;
// 打印引用计数
cout << "Reference count: " << sp1.use_count() << endl;
// 访问资源
sp2->show();
} // 作用域结束后,sp2 自动析构,引用计数减少

// 打印引用计数
cout << "Reference count: " << sp1.use_count() << endl;

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
Person()
Reference count: 1
Reference count: 2
Using Person
Reference count: 1
~Person()
weak_ptr 智能指针
概念介绍

weak_ptr 是 C++ 11 的智能指针之一,主要用于避免 shared_ptr 智能指针之间的循环引用问题。它是 shared_ptr 智能指针的弱引用,不会增加引用计数,不能直接访问资源,但可以通过 lock() 函数将其转换为 shared_ptr 智能指针来访问资源。当资源已被释放时,lock() 函数会返回空指针,可用于判断对象是否仍然存在。weak_ptr 的核心特性如下:

  • 不增加引用计数
    • 只持有 shared_ptr 智能指针的弱引用,不影响对象生命周期。
  • 防止循环引用
    • 可以解决 shared_ptr 智能指针循环引用导致的内存泄漏问题。
  • 检查资源状态
    • 可以通过 expired() 函数判断资源是否仍然存在。
  • 访问资源
    • 可以通过 lock() 函数转换为 shared_ptr 智能指针,若资源已释放,则返回空指针。
  • 可重置与交换
    • 支持使用 reset() 函数释放关联,还支持使用 swap() 函数交换指针内容。
模拟 shared_ptr 循环引用问题

本节将演示在使用 shared_ptr 智能指针时,怎样才会发生循环引用的问题。在下述代码中,会发生智能指针循环引用的问题,最终导致堆上的对象 A 和 对象 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
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <memory>

using namespace std;

// 模拟 shared_ptr 智能指针发生循环引用的问题

class B;

class A {

public:
A() {
cout << "A()" << endl;
}

~A() {
cout << "~A()" << endl;
}

shared_ptr<B> _ptrB;

};

class B {

public:
B() {
cout << "B()" << endl;
}

~B() {
cout << "~B()" << endl;
}

shared_ptr<A> _ptrA;
};

int main() {
shared_ptr<A> ptrA(new A());
shared_ptr<B> ptrB(new B());

// 这里会造成智能指针的循环引用,导致程序结束运行后资源无法正常被析构(释放内存空间)
ptrA->_ptrB = ptrB;
ptrB->_ptrA = ptrA;

// 打印智能指针的引用计数
cout << ptrA.use_count() << endl;
cout << ptrB.use_count() << endl;

return 0;
}

程序运行输出的结果如下:

1
2
3
4
A()
B()
2
2
解决 shared_ptr 循环引用问题

为了解决 shared_ptr 智能指针使用不当导致的循环引用问题,可以在定义对象的时候,使用强引用智能指针(shared_ptr);而在引用对象的时候,使用弱引用智能指针(weak_ptr)。

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
// 解決 shared_ptr 智能指针的循环引用问题

#include <iostream>
#include <memory>

using namespace std;

// 声明 B 的存在,但未定义 B
class B;

class A {

public:
A() {
cout << "A()" << endl;
}

~A() {
cout << "~A()" << endl;
}

// 引用对象时,使用弱引用智能指针 (weak_ptr)
weak_ptr<B> _ptrB;

};

class B {

public:
B() {
cout << "B()" << endl;
}

~B() {
cout << "~B()" << endl;
}

// 引用对象时,使用弱引用智能指针 (weak_ptr)
weak_ptr<A> _ptrA;
};

int main() {
// 定义对象时,使用强引用智能指针 (shared_ptr)
shared_ptr<A> ptrA(new A());
shared_ptr<B> ptrB(new B());

// 这里不会造成智能指针循环引用的问题
ptrA->_ptrB = ptrB;
ptrB->_ptrA = ptrA;

// 打印智能指针的引用计数
cout << ptrA.use_count() << endl;
cout << ptrB.use_count() << endl;

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
A()
B()
2
2
~B()
~A()
weak_ptr 访问对象资源

weak_ptr 智能指针是 shared_ptr 智能指针的弱引用,不会增加引用计数,不能直接访问资源,但可以通过 lock() 函数转换为 shared_ptr 智能指针来访问资源。当资源已被释放时,lock() 函数会返回空指针,可用于判断对象是否仍然存在。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// weak_ptr 通过 lock() 函数访问对象资源

#include <iostream>
#include <memory>

using namespace std;

// 声明 B 的存在,但未定义 B
class B;

class A {

public:
A() {
cout << "A()" << endl;
}

~A() {
cout << "~A()" << endl;
}

// 声明 func 方法的存在,但未定义 func 方法
void func();

// 引用对象时,使用弱引用智能指针
weak_ptr<B> _ptrB;

};

class B {

public:
B() {
cout << "B()" << endl;
}

~B() {
cout << "~B()" << endl;
}

// 声明 func 方法的存在,但未定义 func 方法
void func();

// 引用对象时,使用弱引用智能指针
weak_ptr<A> _ptrA;
};

void A::func() {
cout << "A::func()" << endl;
// 必须将弱引用智能指针转换为强引用智能指针,这样才能访问被管理对象的资源
shared_ptr<B> ptrB = _ptrB.lock();
if (ptrB != nullptr) {
ptrB->func();
}
}

void B::func() {
cout << "B::func()" << endl;
}

int main() {
// 定义对象时,使用强引用智能指针
shared_ptr<A> ptrA(new A());
shared_ptr<B> ptrB(new B());

// 这里不会造成智能指针循环引用的问题
ptrA->_ptrB = ptrB;
ptrA->func();
ptrB->_ptrA = ptrA;

// 打印智能指针的引用计数
cout << ptrA.use_count() << endl;
cout << ptrB.use_count() << endl;

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
A()
B()
A::func()
B::func()
1
1
~B()
~A()

自定义智能指针的删除器

删除器的介绍

C++ 智能指针的删除器(Deleter)是一个可自定义的回调函数,用于在智能指针释放资源时自动执行自定义的删除操作。

  • unique_ptr 智能指针的删除器可以是函数指针或仿函数(函数对象),允许自定义释放方式,例如调用 delete[] 代替 delete
  • shared_ptr 智能指针的删除器可以确保所有引用计数归零后执行自定义删除操作。
删除器的使用场景

在以下使用情况下,C++ 需要自定义智能指针的删除器。

  • (1) 使用非 new 分配的资源。当对象的创建方式不是 new,不能用默认 delete 释放时,需要自定义删除器。例如,使用 malloc() 分配的内存必须用 free() 释放。
1
2
3
4
5
6
#include <memory>
#include <cstdlib>

int main() {
unique_ptr<int, decltype(&free)> ptr((int*)malloc(sizeof(int)), free);
}
  • (2) 管理文件、网络、数据库等系统资源。智能指针默认用 delete 释放资源,但某些系统资源(如文件、数据库连接、网络套接字)需要调用特定 API 释放资源。
1
2
3
4
5
6
#include <memory>
#include <cstdio>

int main() {
unique_ptr<FILE, decltype(&fclose)> file(fopen("data.txt", "w"), fclose);
}
  • (3) 数组(new[])资源的正确释放。默认的 unique_ptr 只调用 delete,而不是 delete[],所以管理动态数组时,需要自定义删除器。
1
2
3
4
5
6
#include <memory>
#include <cstdio>

int main() {
unique_ptr<int[], void(*)(int*)> arr(new int[10], [](int* p) { delete[] p; });
}

或者:

1
2
3
4
5
6
#include <memory>
#include <cstdio>

int main() {
unique_ptr<int[]> arr(new int[10]); // C++ 11 允许直接用 unique_ptr<int[]>
}
  • (4) 需要额外的清理操作。对象释放时,需要额外的清理步骤,比如日志记录、计数等。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <memory>

using namespace std;

void customDeleter(int* p) {
cout << "Deleting pointer: " << p << endl;
delete p;
}

int main() {
unique_ptr<int, decltype(&customDeleter)> ptr(new int(42), customDeleter);
}
  • (5) 使用 shared_ptr 时需要优化删除器大小。shared_ptr 允许自定义删除器,但它需要存储删除器对象,因此通常使用函数对象(仿函数)或 Lambda 来优化删除器大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>

using namespace std;

class CustomDeleter {

public:
void operator()(int* p) const {
cout << "Deleting shared ptr: " << p << endl;
delete p;
}

};

int main() {
shared_ptr<int> sp(new int(10), CustomDeleter());
}

或者:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <memory>

using namespace std;

int main() {
auto deleter = [](int* p) { delete p; };
shared_ptr<int> sp(new int(20), deleter);
}
删除器的使用案例
删除器的案例代码一

通过模板类单独定义了两个函数对象(仿函数),来实现 unique_ptr 智能指针的删除器。

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
#include <iostream>
#include <memory>

using namespace std;

// 自定义数组的删除器(函数对象)
template<typename T>
class ArrayDeleter {

public:
void operator()(T * arrayPtr) const {
cout << "Deleting array pointer" << endl;
if (arrayPtr != nullptr) {
delete[] arrayPtr;
}
}
};

// 自定义文件的删除器(函数对象)
template<typename T>
class FileDeleter {

public:
void operator()(T* filePtr) const {
cout << "Deleting file pointer" << endl;
if (filePtr != nullptr) {
fclose(filePtr);
}
}
};

int main() {
// 创建一个智能指针管理整型数组,并指定删除器
unique_ptr<int, ArrayDeleter<int>> ptr(new int[100]);

// 创建一个智能指针管理文件,并指定删除器
unique_ptr<FILE, FileDeleter<FILE>> ptr2(fopen("data.txt", "w"));

return 0;
}

程序运行输出的结果如下:

1
2
Deleting file pointer
Deleting array pointer
删除器的案例代码二

在上述的案例代码一中,通过模板类单独定义了两个函数对象(仿函数)来实现智能指针的删除器,这略显得冗余,因为删除器在定义之后只使用了一次。为了使代码更简洁,可以使用 Lambda 表达式来定义智能指针的删除器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <memory>
#include <functional>

using namespace std;

int main() {
// 创建一个智能指针管理整型数组,并通过Lambda表达指定删除器
unique_ptr<int, function<void(int*)>> ptr(new int[100], [](int* p) {
cout << "Deleting array pointer by lambda" << endl;
delete[] p;
});


// 创建一个智能指针管理文件,并通过Lambda表达指定删除器
unique_ptr<FILE, function<void(FILE*)>> ptr2(fopen("data.txt", "w"), [](FILE* p) {
cout << "Deleting file pointer by lambda" << endl;
fclose(p);
});

return 0;
}

程序运行输出的结果如下:

1
2
Deleting file pointer by lambda
Deleting array pointer by lambda
删除器的案例代码三

除了上面介绍的删除器定义方式之外,还可以使用函数指针作为 unique_ptr 智能指针的删除器。通过 std::decltype() 来获取 arrayDeleter() 函数的类型,并将其作为 unique_ptr 智能指针的删除器类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <memory>

using namespace std;

void arrayDeleter(int* ptr) {
cout << "Deleting array pointer: " << ptr << endl;
delete[] ptr;
}

int main() {
// 创建一个智能指针管理文件,并通过函数指针指定删除器
unique_ptr<int, decltype(&arrayDeleter)> ptr(new int[10], arrayDeleter);

// 等效写法之一
// using DeleterType = void(*)(int*); // 明确指定删除器类型
// unique_ptr<int, DeleterType> ptr(new int[100], arrayDeleter);

// 等效写法之二
// unique_ptr<int, void(*)(int*)> ptr(new int(10), arrayDeleter);
}

程序运行输出的结果如下:

1
Deleting array pointer: 000001B05684F800

多线程访问共享对象的问题

在多个线程同时访问同一个对象(共享对象)时,往往会产生线程安全问题,下面将介绍如何使用 C++ 11 的 shared_ptrweak_ptr 智能指针来解决线程安全问题。

thread::detach () 介绍

这里先简单介绍一下 thread::detach() 函数,有助于理解后面给出的多线程案例代码。

  • thread::detach() 函数的作用

    • detach() 提供了一种非阻塞方式来运行线程
    • 线程一旦 detach(),就会变成守护线程(Daemon Thread),独立于 thread 对象运行。
    • thread 对象与底层线程资源分离,thread 对象销毁时不会影响已分离的线程。
    • 适用于不需要等待线程结束的场景,例如后台任务或日志记录等。
  • thread::detach() 函数的注意事项

    • 不要访问已销毁的资源:调用 detach() 后,线程对象的生命周期由操作系统管理,后续不能再通过 thread 访问它,否则会导致未定义行为。
    • 避免主线程提前退出main() 退出时,所有仍在运行的分离线程会被终止,这可能导致任务未完成或资源泄漏。
    • 不能 detach() 两次:如果 thread 已经 detach(),再次调用 detach() 会导致错误。
  • thread::detach()thread::join() 的区别

    • join():阻塞主线程,直到子线程执行完毕,适用于必须等待子线程完成的任务(如并行计算)。
    • detach():让子线程独立运行,主线程不会等待它执行完毕,适用于不关心子线程何时完成的任务(如日志记录、异步 I/O 任务)。
线程安全问题的产生

下述这段 C++ 代码会产生线程安全问题,因为主线程和子线程同时访问共享对象 Task,但是主线程在子线程访问共享对象 Task 之前,将共享对象析构掉,这会导致子线程在后续访问共享对象时产生不可预知的行为。

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
#include <iostream>
#include <thread>

using namespace std;

// 线程安全问题的产生

class Task {

public:
Task() {
cout << "Task()" << endl;
}

~Task() {
cout << "~Task()" << endl;
}

void run() {
cout << "invoke Task::run()" << endl;
}

};

void executTask(Task *task) {
// 子线程等待 5 秒
this_thread::sleep_for(chrono::seconds(5));
// 执行任务方法
task->run();
}

int main() {
Task *task = new Task();

// 创建子线程
thread t(executTask, task);

// 将子线程与 thread 对象分离,使其在后台独立运行,直到子线程执行完毕
t.detach();

// 主线程在子线程调用 Task::run() 函数之前析构 Task 对象(共享对象),这样就会产生线程安全问题
delete task;

// 等待子线程执行完成
t.join();

return 0;
}
线程安全问题的解决

这里利用 weak_ptr 智能指针可以观察 shared_ptr 智能指针状态的特性,来解决多线程访问共享对象的线程安全问题。

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
#include <iostream>
#include <memory>
#include <thread>

using namespace std;

// 线程安全问题的解决

class Task {

public:
Task() {
cout << "Task()" << endl;
}

~Task() {
cout << "~Task()" << endl;
}

void run() {
cout << "invoke Task::run()" << endl;
}

};

void executTask(weak_ptr<Task> wp) {
// 子线程等待 5 秒
this_thread::sleep_for(chrono::seconds(5));

// 子线程访问共享对象时,先检查共享对象是否存活
shared_ptr<Task> sp = wp.lock();
if (sp != nullptr) {
sp->run();
} else {
cout << "Failed to invoke Task:run()";
}
}

int main() {
{
// 创建强引用智能指针
shared_ptr<Task> task = make_shared<Task>();

// 创建子线程
thread t(executTask, weak_ptr<Task>(task));

// 将子线程与 thread 对象分离,使其在后台独立运行,直到子线程执行完毕
t.detach();

} // 共享对象 task 出了作用域后会自动释放

// 主线程等待子线程执行完成
this_thread::sleep_for(chrono::seconds(10));

return 0;
}

程序运行输出的结果如下:

1
2
3
Task()
~Task()
Failed to invoke Task:run()

智能指针的模拟实现

本节将使用 C++ 的类模板来简单模拟实现智能指针。

提示

智能指针的本质是利用栈上的对象出了作用域(比如函数作用域)后自动析构的特性,以此来实现资源的自动释放。

不带引用计数的智能指针

本节将使用类模板、右值引用、移动语义(move )来简单模拟实现 C++ 11 中的 unique_ptr 智能指针。

扩展阅读

更多关于 C++ 11 右值引用和移动语义(move )的介绍请看 这里

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <iostream>
#include <stdexcept>

using namespace std;

// 模拟实现不带引用计数的智能指针,类似 unique_ptr 智能指针
template<typename T>
class CSmartPtr {

public:
// 构造函数
CSmartPtr(T *ptr = nullptr) : _ptr(ptr) {
cout << "CSmartPtr(T *ptr)" << endl;
}

// 析构函数
~CSmartPtr() {
cout << "~CSmartPtr()" << endl;
delete _ptr;
}

// 显式删除带左值引用参数的拷贝构造函数
CSmartPtr(const CSmartPtr<T> &) = delete;

// 显式删除带左值引用参数的赋值运算符重载函数
CSmartPtr<T> &operator=(const CSmartPtr<T> &) = delete;

// 带右值引用参数的拷贝构造函数,支持移动构造(即支持 move 移动语义)
CSmartPtr(CSmartPtr<T> &&src) noexcept : _ptr(src._ptr) {
cout << "CSmartPtr(CSmartPtr<T> &&src)" << endl;
src._ptr = nullptr;
}

// 带右值引用参数的赋值运算符重载函数,支持移动赋值(即支持 move 移动语义)
CSmartPtr<T> &operator=(CSmartPtr<T> &&src) noexcept {
cout << "CSmartPtr<T> &operator=(CSmartPtr<T> &&src)" << endl;
// 避免自赋值
if (this == &src) {
return *this;
}

// 先存储当前的指针,避免删除自己后 src._ptr 变为 nullptr
T *old_ptr = _ptr;

// 转移所有权
_ptr = src._ptr;
src._ptr = nullptr;

// 释放旧资源
delete old_ptr;

return *this;
}

// * 运算符重载函数
T &operator*() {
if (!_ptr) {
throw runtime_error("Dereferencing a null pointer!");
}
return *_ptr;
}

// -> 运算符重载函数
T *operator->() {
if (!_ptr) {
throw runtime_error("Accessing member of a null pointer!");
}
return _ptr;
}

// 释放所有权,不删除对象
T *release() noexcept {
T *temp = _ptr;
_ptr = nullptr;
return temp;
}

// 重新分配资源
void reset(T *ptr = nullptr) noexcept {
// 避免 delete 自己的对象
if (_ptr == ptr) {
return;
}

delete _ptr;
_ptr = ptr;
}

// 获取原始指针
T *get() const noexcept {
return _ptr;
}

private:
T *_ptr;
};


void test01() {
CSmartPtr<int> ptr1(new int(30));
// CSmartPtr<int> ptr2(ptr1); // 编译出错,不可复制
}

void test02() {
CSmartPtr<int> ptr1(new int(30));
CSmartPtr<int> ptr2(new int(10));
// ptr2 = ptr1; // 编译出错,不可赋值
}

void test03() {
CSmartPtr<int> ptr1(new int(10));
CSmartPtr<int> ptr2(move(ptr1)); // 支持移动构造,将 ptr1 的所有权转移到 ptr2,ptr1 变为空
// cout << *ptr1 << endl; // 程序运行出错,试图解引用 ptr1,会导致未定义行为
cout << *ptr2 << endl; // 正常输出 10
}

void test04() {
CSmartPtr<int> ptr1(new int(30));
CSmartPtr<int> ptr2(new int(10));
ptr2 = move(ptr1); // 支持移动赋值,将 ptr1 的所有权转移到 ptr2,ptr1 变为空
// cout << *ptr1 << endl; // 程序运行出错,试图解引用 ptr1,会导致未定义行为
cout << *ptr2 << endl; // 正常输出 30
}

void test05() {
CSmartPtr<int> p1(new int(10));
int *rawPtr = p1.release(); // p1 不再管理该资源
delete rawPtr; // 需要手动释放

CSmartPtr<int> p2;
p2.reset(new int(20)); // 重新分配资源
}

int main() {
test01();
test02();
test03();
test04();
test05();
}

C++ 11 及更高版本中的特殊成员函数行为

  • 如果用户通过右值引用参数声明了移动构造函数或移动赋值运算符重载函数,而没有显式声明拷贝构造函数或赋值运算符重载函数(带左值引用参数),则:
  • (1) 拷贝构造函数(带左值引用参数)会被隐式删除(= delete)。
  • (2) 赋值运算符重载函数(带左值引用参数)会被隐式删除(= delete)。
是否定义了用户自定义的… 默认拷贝构造默认拷贝赋值默认移动构造默认移动赋值
没有移动构造和移动赋值✅ 自动生成✅ 自动生成不会自动生成不会自动生成
定义了移动构造或移动赋值隐式删除隐式删除✅ 存在✅ 存在

带引用计数的智能指针

本节将使用类模板来简单模拟实现 C++ 11 中的 shared_ptr 智能指针,暂时不考虑线程安全问题。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <iostream>

using namespace std;

// 模拟实现带引用计数的智能指针(不考虑线程安全问题),类似 shared_ptr 智能指针
template<typename T>
class RefCount {

public:
RefCount(T *ptr = nullptr) : _ptr(ptr) {
// 在堆上初始化引用计数,为了让智能指针在复制或赋值时共享同一个引用计数
if (_ptr == nullptr) {
_count = new int(0);
} else {
_count = new int(1);
}
}

~RefCount() {
// 释放堆上的引用计数
delete _count;
}

// 引用计数自增
void increDef() {
++(*_count);
}

// 引用计数自减
int decreRef() {
if (*_count > 0) {
return --(*_count);
}
return 0;
}

// 获取当前引用计数
int getCount() const {
return *_count;
}

private:
T *_ptr; // 指向资源的指针
int *_count; // 指向堆上存储的引用计数
};

template<typename T>
class CSmartPtr {

public:
// 构造函数
CSmartPtr(T *ptr = nullptr) : _ptr(ptr) {
cout << "CSmartPtr(T* ptr)" << endl;
// 初始化引用计数对象
_refCount = new RefCount<T>(_ptr);
}

// 析构函数
~CSmartPtr() {
cout << "~CSmartPtr()" << endl;
// 减少引用计数,并且当引用计数为零时,才释放智能指针管理的资源
if (0 == _refCount->decreRef()) {
delete _ptr;
delete _refCount;
}
}

// 拷贝构造函数
CSmartPtr(const CSmartPtr<T> &src) : _ptr(src._ptr), _refCount(src._refCount) {
cout << "CSmartPtr(const CSmartPtr<T>& src)" << endl;
// 增加引用计数
if (_ptr != nullptr) {
_refCount->increDef();
}
}

// 赋值运算符重载函数
CSmartPtr<T> &operator=(const CSmartPtr<T> &src) {
cout << "CSmartPtr<T> operator=(const CSmartPtr<T>& src)" << endl;
// 避免自赋值
if (this == &src) {
return *this;
}

// 释放旧的资源
if (0 == _refCount->decreRef()) {
delete _ptr;
delete _refCount;
}

// 共享新的资源
_ptr = src._ptr;
_refCount = src._refCount;

// 增加引用计数
_refCount->increDef();

return *this;
}

// * 运算符重载函数
T &operator*() {
return *_ptr;
}

// -> 运算符重载函数
T *operator->() {
return _ptr;
}

private:
T *_ptr; // 指向资源的指针
RefCount<T> *_refCount; // 指向资源引用计数对象的指针
};

class Person {

public:
Person(int age = 0) : _age(age) {
cout << "Person(int age)" << endl;
}

~Person() {
cout << "~Person()" << endl;
}

void print() {
cout << "age: " << _age << endl;
}

private:
int _age;
};

int main() {
// 测试多个智能指针管理同一个对象资源
CSmartPtr<Person> ptr1(new Person(20));

{
CSmartPtr<Person> ptr2(ptr1); // 可复制
ptr2->print();

CSmartPtr<Person> ptr3;
ptr3 = ptr2; // 可赋值
ptr3->print();
} // 出了作用域后,ptr2、ptr3 析构,但不影响 ptr1,也不影响对象资源的析构

cout << "Leaving inner scope..." << endl;

ptr1->print(); // 仍然有效

} // 出了作用域后,ptr1 析构,并析构对象资源

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Person(int age)
CSmartPtr(T* ptr)
CSmartPtr(const CSmartPtr<T>& src)
age: 20
CSmartPtr(T* ptr)
CSmartPtr<T> operator=(const CSmartPtr<T>& src)
age: 20
~CSmartPtr()
~CSmartPtr()
Leaving inner scope...
age: 20
~CSmartPtr()
~Person()