详解C++三类智能指针基础
通过内存泄漏的危害及其产生方式引出智能指针,详细介绍C++11引入的std::unique_ptr、std::shared_ptr与std::weak_ptr三类智能指针的特性与基本使用
详解C++三类智能指针基础
一、引入智能指针
由于C++没有隔壁C#的垃圾回收机制GC,我们常需手动清理动态分配内存的堆区对象, 而C++11引入的智能指针可以自动完成垃圾清理工作,即自动销毁动态分配内存的对象
1.1 内存泄漏
- 内存泄漏(Memory Leak)指程序在用
new
或malloc
等方式动态分配内存后未能正确释放,导致这些内存无法被回收和重用,这会逐渐耗尽系统内存资源,从而导致程序性能下降乃至最终崩溃,这在如服务器应用等长期运行的程序中尤其危险
1.2 泄露原因
1.2.1 忘记释放
- 程序员动态分配了内存却未调用
delete
或free
等释放内存,是常见的内存泄漏原因
1
2
3
4
5
6
void example()
{
//开辟内存在堆区
int* ptr = new int(42);
//忘记调用delete ptr,导致内存泄漏
}
1.2.2 指针丢失
- 在开辟了内存到某个指针上后,在释放内存前就将该指针指向了其他方向而未进行备份的话,就会导致丢失原堆区内存地址,从而无法进行内存释放
1
2
3
4
5
6
7
8
9
void Example()
{
//开辟内存在堆区
int* ptr = new int(42);
//指向原内存的指针被覆盖
ptr = new int(43);
//释放新分配的内存,而原来的内存无法被释放,导致内存泄漏
delete ptr;
}
1.2.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
#include <iostream>
//前向声明,以便A中使用B类型作为成员
class B;
class A
{
public:
//A对象持有B类型指针
B* b;
//析构函数,本应用于释放内存,此处仅进行输出检测是否会被调用
~A() { std::cout << "A Destroyed\n"; }
};
class B
{
public:
//B对象持有A类型指针
A* a;
~B() { std::cout << "B Destroyed\n"; }
};
int main()
{
//开辟对象在堆区
A* _a = new A();
B* _b = new B();
//_a持有_b
_a->b = _b;
//_b持有_a,形成循环引用
_b->a = _a;
//没有任何输出信息,说明程序结束时_a和_b都未被释放,导致内存泄漏
}
1.2.4 异常中断
- 下述例程中,如果我们输入了$0$作为分母则会触发异常中断程序,在这种特殊情况下会导致
Func
函数末尾的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
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
#include <iostream>
class Test
{
private:
int data;
public:
//默认无参构造函数与有参构造函数
Test() : data(0) {};
Test(int _data) : data(_data) {};
//析构函数
~Test() { std::cout << "Test Entity " << data << " Destructed\n"; }
};
int Div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
throw std::invalid_argument("Invalid Operation: Divide 0");
return a / b;
}
void Func()
{
//开辟在堆区的两个数组,分别使用默认构造函数和有参构造函数
Test* p1 = new Test[2]();
Test* p2 = new Test[2]{ Test(55), Test(66) };
//开辟在堆区的一个使用默认构造函数的对象
Test* p3 = new Test();
std::cout << Div() << "\n";
//释放内存
delete[] p1;
delete[] p2;
delete p3;
}
int main()
{
//捕获异常
try
{
Func();
}
catch (std::exception& e)
{
std::cout << e.what() << "\n";
}
}
//100
//25
//4
//Test Entity 0 Destructed
//Test Entity 0 Destructed
//Test Entity 66 Destructed
//Test Entity 55 Destructed
//Test Entity 0 Destructed
//100
//0
//Invalid Operation: Divide 0
1.3 智能指针
- 除了结合良好习惯和工具检测进行手动管理外,还可用智能指针来预防上述内存泄漏问题,注意需要引入头文件
<memory>
std::unique_ptr
:独占所有权,超出作用域时自动释放内存std::shared_ptr
:共享所有权,引用计数为零时自动释放内存std::weak_ptr
:解决std::shared_ptr
的循环引用问题
- 以前文的异常中断例程为例,若我们使用
std::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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
//支持智能指针
#include <memory>
class Test
{
private:
int data;
public:
//默认无参构造函数与有参构造函数
Test() : data(0) {};
Test(int _data) : data(_data) {};
//析构函数
~Test() { std::cout << "Test Entity " << data << " Destructed\n"; }
};
int Div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
throw std::invalid_argument("Invalid Operation: Divide 0");
return a / b;
}
void Func()
{
//Test* p1 = new Test[2]();
std::unique_ptr<Test[]> p1(new Test[2]());
//Test* p2 = new Test[2]{ Test(55), Test(66) };
std::unique_ptr<Test[]> p2(new Test[2]{ Test(55), Test(66) });
//Test* p3 = new Test();
std::unique_ptr<Test> p3(new Test());
std::cout << Div() << "\n";
}
int main()
{
//捕获异常
try
{
Func();
}
catch (std::exception& e)
{
std::cout << e.what() << "\n";
}
}
//100
//4
//25
//Test Entity 0 Destructed
//Test Entity 66 Destructed
//Test Entity 55 Destructed
//Test Entity 0 Destructed
//Test Entity 0 Destructed
1.4 RAll思想
- 资源获取即初始化(RAII, Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(例如内存、文件句柄、互斥量等)的技术
- 在获取资源后初始化一个对象,将资源托管给该对象,控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,无需我们显式释放资源
- 我们基于RAII思想设计一个简易的智能指针,并在前文中的例程上测试
- 可以看到即便触发了异常,内存依然被正确释放了,这是因为程序终止后栈区的
SmartPtr
类对象会被释放(继而带动资源的内存释放),而无需依赖函数体内书写的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
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
#include <iostream>
//实现一个最简易的智能指针
template<class T>
class SmartPtr
{
private:
T* ptr;
public:
//构造函数,将接收的资源挂载到内部指针上
SmartPtr(T* _ptr = nullptr) : ptr(_ptr) {}
//析构函数,对象析构时自动释放所管理的资源
~SmartPtr()
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
std::cout << "SmartPtr Deleted" << "\n";
}
};
class Test
{
private:
int data;
public:
//默认无参构造函数与有参构造函数
Test() : data(0) {};
Test(int _data) : data(_data) {};
//析构函数
~Test() { std::cout << "Test Entity " << data << " Destructed\n"; }
};
int Div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
throw std::invalid_argument("Invalid Operation: Divide 0");
return a / b;
}
void Func()
{
//Test* p3 = new Test();
SmartPtr<Test> p(new Test());
std::cout << Div() << "\n";
}
int main()
{
//捕获异常
try
{
Func();
}
catch (std::exception& e)
{
std::cout << e.what() << "\n";
}
}
//100
//2
//50
//Test Entity 0 Destructed
//SmartPtr Deleted
//100
//0
//Test Entity 0 Destructed
//SmartPtr Deleted
//Invalid Operation: Divide 0
- 但此处的
SmartPtr
还不能完全称为智能指针,因其还不具备指针的行为,即可通过->
访问所指向空间内容、且可通过*
解引用,为此我们还需重载*
与->
运算符
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
#include <iostream>
//实现一个最简易的智能指针
template<class T>
class SmartPtr
{
private:
T* ptr;
public:
//构造函数,将接收的资源挂载到内部指针上
SmartPtr(T* _ptr = nullptr) : ptr(_ptr) {}
//析构函数,对象析构时自动释放所管理的资源
~SmartPtr()
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
std::cout << "SmartPtr Deleted" << "\n";
}
//指针解引用
T& operator*() const
{
return *ptr;
}
//获取类型成员
T* operator->() const
{
return ptr;
}
};
- 除此之外,若对此时的
SmartPtr
对象使用拷贝操作,由于我们未提供拷贝构造和拷贝赋值运算符,故会使用默认拷贝操作(浅拷贝),导致析构时对同一块内存空间多次释放从而报错
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
//堆区指针int
SmartPtr<int> sp1(new int());
//使用SmartPtr的默认拷贝构造函数(浅拷贝)
SmartPtr<int> sp2(sp1);
//使用SmartPtr的默认拷贝赋值运算符(浅拷贝)
SmartPtr<int> sp3();
sp3
}
- 所以我们还需要重写
SmartPtr
类的两个拷贝函数,实现深拷贝
二、智能指针详解
C++11引入的
<memory>
库中的智能指针封装了原始指针,可自动管理和释放所指向的对象,减少手动内存管理带来的复杂性和错误风险
2.1 std::unique_ptr
2.1.1 类型特性
- 独占所有权(给定资源只允许存在唯一
std::unique_ptr
指向它,避免多个指针指向同一块内存而可能导致的多次释放内存),即不允许拷贝构造,但支持移动语义转移所有权 - 具备RAII特性,当
std::unique_ptr
智能指针对象的作用域结束时,其自动析构并释放其指向对象的内存
2.1.2 创建方式
- 可通过
std::unique_ptr<T> ptrName(new T(...))
创建堆区动态对象,并交由动态指针接管
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
//支持智能指针
#include <memory>
int main()
{
//创建一个指向整数10的std::unique_ptr
std::unique_ptr<int> ptr(new int(111));
std::cout << "Value=" << *ptr << "\n";
//Value=111
//作用域结束时,ptr对象自动析构,释放整型数据的内存
}
- 在C++14以后还可使用
std::make_unique<T>
创建std::unique_ptr<T>
,其由于避免了new
关键字的使用而更为安全
1
2
3
4
5
6
7
8
9
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(2727);
std::cout << "Value=" << *ptr << "\n";
//Value=2727
}
- 可通过
std::unique_ptr<iT[]>
创建并管理类型T
的动态数组
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>
int main()
{
//创建一个指向包含5个整数的数组的std::unique_ptr
std::unique_ptr<int[]> arr(new int[5]);
//为数组元素逐个赋值
for (int i = 0; i < 5; ++i)
arr[i] = i * 10;
//输出数组元素
for (int i = 0; i < 5; ++i)
std::cout << arr[i] << "\n";
//arr超出作用域时自动释放内存
}
//0
//10
//20
//30
//40
2.1.3 所有权独占与转移
- 试图调用
std::unique_ptr
的拷贝操作时会报错提示拷贝相关函数已被删除
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(2727);
std::cout << "Value=" << *ptr << "\n";
//Value=2727
//试图调用拷贝构造,运行报错
//use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]'
std::unique_ptr<int> copy = ptr;
}
- 智能指针
std::unique_ptr
不允许被拷贝,但可以通过std::move
转移所有权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <memory>
int main()
{
//创建一个指向整数111的std::unique_ptr
auto ptr1 = std::make_unique<int>(111);
//转移所有权给ptr2
std::unique_ptr<int> ptr2 = std::move(ptr1);
//ptr1现在为空
if (!ptr1)
std::cout << "Pointer ptr1 Is Empty" << "\n";
std::cout << "Value=" << *ptr2 << "\n";
//作用域结束,ptr2自动释放内存
}
//Pointer ptr1 Is Empty
//Value=111
2.2 std::shared_ptr
2.2.1 类型特性
- 共享所有权,允许多个指针拥有同一个对象
- 具备RAII特性,且通过检测引用计数释放内存,当指向某对象的最后一个
std::shared_ptr
销毁时,被指对象的内存被自动释放
2.2.2 创建方式
- 可以通过构造函数或
std::make_shared<T>
创建shared_ptr<T>
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <memory>
int main()
{
//使用构造函数创建std::shared_ptr
std::shared_ptr<int> ptr1(new int(111));
std::cout << "Value=" << *ptr1 << "\n";
//Value=111
//使用std::make_shared创建std::shared_ptr
std::shared_ptr<int> ptr2 = std::make_shared<int>(222);
std::cout << "Value=" << *ptr2 << "\n";
//Value=222
}
2.2.3 所有权共享
- 多个
std::shared_ptr<T>
可以通过=
指向同一个堆区资源,用其中任一指针的use_count()
方法可查询其所指资源上挂载的指针数量,即引用计数 - 当最后一个
std::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
#include <iostream>
#include <memory>
class Test
{
public:
int data;
public:
//默认无参构造函数与有参构造函数
Test() : data(0) {};
Test(int _data) : data(_data) {};
//析构函数
~Test() { std::cout << "Test Entity " << data << " Destructed\n"; }
};
int main()
{
//创建Test*类型的堆区指针,传给智能指针ptr1
std::shared_ptr<Test> ptr1 = std::make_shared<Test>(111);
//创建ptr2共享ptr1所指向的对象
std::shared_ptr<Test> ptr2 = ptr1;
std::cout << "ptr1=" << ptr1->data << "\n";
//ptr1=30
std::cout << "ptr2=" << ptr2->data << "\n";
//ptr2=30
//查询引用计数
std::cout << "Use Count=" << ptr1.use_count() << "\n";
//Use Count=2
//Test Entity 111 Destructed
}
2.2.4 手动销毁
- 可通过
reset()
函数手动销毁std::shared_ptr
智能指针对象,使得其所指的对象的引用计数减一(并非直接销毁所指对象,除非被销毁的智能指针是最后一个)
1
2
std::shared_ptr<T> ptr(new T());
ptr.reset();
2.2.5 循环引用
std::shared_ptr
可能导致循环引用而造成内存泄漏,如下例程就不会输出"~Node"
字符串
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 <memory>
struct Node
{
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
~Node()
{
std::cout << "~Node\n";
}
};
int main()
{
//指向Node类型的智能指针,Node内又嵌套智能指针
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
//n1->n2
n1->next = n2;
//n1<-n2
n2->prev = n1;
//n1与n2所指Node对象内部循环引用,导致作用域结束时两指针都无法销毁,两个堆区Node对象的内存也就无法被释放
}
- 上例中的两个
std::shared_ptr<Node>
智能指针指向的Node*
指针的内部成员(也是同类型智能指针)间产生了循环引用,外部作用域结束后两个智能指针销毁,但内部两个智能指针仍因对方未销毁(引用计数不为$0$)而无法销毁,导致了内存泄漏
- 可以使用
std::weak_ptr
打破循环引用,详见后文
2.3 std::weak_ptr
2.3.1 类型特性
std::weak_ptr
可指向一个由std::shared_ptr
指向的对象而不影响其生命周期(即不增加引用计数),用于在某些场合替换std::shared_ptr
以解决其循环引用导致的内存泄漏- 其诞生目的就是为了辅助解决
std::shared_ptr
的循环引用问题,其本身不具备RAII特性(即其自身并不直接管理对象,其销毁与否不影响其间接管理的对象的内存释放与否),故其只能从一个std::shared_ptr
或另一个已存在的std::weak_ptr
对象构造
2.3.2 打破循环引用
- 前文的
std::shared_ptr
循环引用例程中,Node
结构体内的两个std::shared_ptr<Node>
智能指针成员是导致引用计数永远不为$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
#include <iostream>
#include <memory>
struct Node
{
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
~Node()
{
std::cout << "~Node\n";
}
};
int main()
{
//指向Node类型的智能指针,Node内又嵌套智能指针
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
//n1->n2
n1->next = n2;
//n1<-n2
n2->prev = n1;
//n1与n2所指Node对象内部循环引用,导致作用域结束时两指针都无法销毁,两个堆区Node对象的内存也就无法被释放
}
- 解决该问题的方法之一即使用
std::weak_ptr
代替Node
任一方向上的std::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
#include <iostream>
#include <memory>
struct Node
{
//std::shared_ptr<Node> next;
//std::shared_ptr<Node> prev;
std::weak_ptr<Node> next;
std::weak_ptr<Node> prev;
~Node()
{
std::cout << "~Node\n";
}
};
int main()
{
std::shared_ptr<Node> p1(new Node);
std::shared_ptr<Node> p2(new Node);
p1->next = p2;
p2->prev = p1;
}
//~Node
//~Node
2.3.3 访问引用对象
std::weak_ptr
不能直接访问其所指对象,必须通过lock()
将其转为std::shared_ptr
类型- 转换时若发现其所指对象已被销毁,则
lock()
返回空指针 - 转换过后其所指对象引用计数加一,若此时原
std::shared_ptr
被销毁,则其所指对象的生命期将被延长至该新转换的std::shared_ptr
同样被销毁为止
- 转换时若发现其所指对象已被销毁,则
- 可以使用
std::weak_ptr
的成员函数expired()
检测其所观察的对象是否已释放,即检测其关联的std::shared_ptr
所指对象的引用计数是否为$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
#include <iostream>
#include <memory>
int main()
{
//一个std::shared_ptr智能指针,维护一个堆区int数据,数据引用计数为1
std::shared_ptr<int> sharedPtr = std::make_shared<int>(100);
//一个std::weak_ptr智能指针,指向上述数据,数据引用计数此时仍为1
std::weak_ptr<int> weakPtr = sharedPtr;
//将std::weak_ptr转换为std::shared_ptr,并检测是否成功
std::shared_ptr<int> promoted = weakPtr.lock();
if (promoted)
std::cout << "Value=" << *promoted << "\n";
else
std::cout << "Pointer Is Expired When Trying Convert\n";
//手动释放sharedPtr,减少引用计数
sharedPtr.reset();
//expired函数检查std::weak_ptr观察的对象是否已释放(即关联的std::shared_ptr所指对象的引用计数是否为0)
if (weakPtr.expired())
std::cout << "Flag1\n";
promoted.reset();
if (weakPtr.expired())
std::cout << "Flag2\n";
}
//Value=100
//Flag2
本文由作者按照 CC BY-NC-SA 4.0 进行授权