0%

C++ 智能指针简介与错误使用情况

智能指针是为了更方便的管理内存而设计的,设计思想就是让使用者不再管理内存,而是由智能指针来进行管理。
换句话说以后不用再考虑new出来的对象什么时候需要delete,智能指针能帮你管理内存。
智能指针分为三种: std::shared_ptrstd::weak_ptrstd::unique_ptr

std::unique_ptr

std::unique_ptr指针拥有其管理对象的所有权,该智能指针不能被复制,只能被移动。当std::unique_ptr智能指针被析构,则其管理的对象也会被析构。
举一个简单的例子,高中宿管大爷,早六点开灯,晚十点关灯,其他人没有办法参与开关灯的事情。这里开关灯指的就是内存的分配与释放。

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <memory>
int main()
{
{
int* p = new int(0); ///< 分配一个int的内存,其值为0
std::unique_ptr<int> up = std::unique_ptr<int>(p); ///< 使用指针p来创建一个智能指针对象
/*
/// 由于std::unique_ptr对象不可复制,所以下面三句话编译不过
std::unique_ptr<int> up_copy1(up);
std::unique_ptr<int> up_copy2;
up_copy2 = up;
*/
} ///< 在这个地方智能指针up被析构,up的析构函数对指针p执行delete操作,从而达到智能回收内存的作用

{
auto up = std::make_unique<int>(0); ///< 这句话等价于上面的两句话
}
}

下面看一下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
template<typename OBJECT>
class unique_ptr {
public:
/// 构造函数
unique_ptr(OBJECT* p) : p_(p) {}
/// 析构函数
~unique_ptr(){
if(p_ != nullptr){
delete p_;
}
}
/// 删除拷贝构造函数, 标识这个类不可拷贝
unique_ptr(const unique_ptr<OBJECT>& other) = delete;
/// 删除拷贝构造赋值符, 标识这个类不可拷贝
unique_ptr<OBJECT>& operator=(const unique_ptr<OBJECT>& other) = delete;
/// 移动构造函数
unique_ptr(unique_ptr&& other) {
p_ = other.p_;
other.p_ = nullptr;
}

/// 移动赋值符
unique_ptr<OBJECT>& unique_ptr(unique_ptr&& other) {
if (&other == this) {
return *this;
}
delete p_;
p_ = other.p_;
other.p_ = nullptr;
}
private:
OBJECT* p_ = nullptr;
};

见示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class object {
public:
object() {
std::cout << "object()" << std::endl;
}
~object() {
std::cout << "~object()" << std::endl;
}
};

int main() {
{
unique_ptr<object> p(new object);
unique_ptr<object> other(std::move(p)); ///< 调用移动构造函数
/// 此时other智能指针拥有对象的管理权,而p失去了该对象的管理权
}
return 0;
}

运行结果:

Snipaste_2020-12-03_15-42-50

std::shared_ptr

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
37
38
39
40
template <typename OBJECT>
class shared_ptr {
public:
/// 构造函数
shared_ptr(OBJECT* object):p_(object), count_(new int(0)) {
++(*count_);
}

/// 析构函数
~shared_ptr() {
if(--(*count_) == 0) { ///< 如果自己是最后一个管理该对象的人,自己被析构时负责做善后工作即delete对象
delete p_;
delete count_;
}
}

/// 拷贝构造函数
shared_ptr(const shared_ptr& other) {
count_ = other.count_; ///< 把引用计数的指针复制过来
++(*count_); ///< 所有shared_ptr中的count_都自加一下
p_ = other.p_; ///< 复制管理对象的指针
}

/// 拷贝赋值运算符
shared_ptr<OBJECT>& operator=(const shared_ptr& other) {
if (&other == this) {
return &this; ///< 如果复制的对象是自己,则直接返回
}
count_ = other.count_; ///< 把引用计数的指针复制过来
++(*count_); ///< 所有shared_ptr中的count_都自加一下
delete p_;
p_ = other.p_; ///< 复制管理对象的指针
return &this;
}

private:
OBJECT* p_ = nullptr; ///< 指向管理对象的指针
int* count_ = nullptr; ///< 引用计数, 之所以使用指针是为了让管理同一个对象的引用计数
};

运行一下例子

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 object {
public:
object() {
std::cout << "object()" << std::endl;
}

~object() {
std::cout << "~object()" << std::endl;
}
};

int main() {
{
shared_ptr<object> sp(new object);

auto sp1(sp);
auto sp2(sp);
auto sp3(sp);
auto sp4(sp);
auto sp5(sp);
auto sp6(sp);
}
return 0;
}

运行结果:

Snipaste_2020-12-03_10-47-05
\resource\C++智能指针简介与错误使用情况

可以看到该对象被构造了一次,又被析构了一次。

上面的实现虽然让内存管理变得简单,但也带来了一些麻烦。
由于智能指针拥有了对象的管理权,万一两个智能指针管理同一对象,那么这两个智能指针在析构时会对同一对象执行两次delete, 从而造成崩溃。见下面代码举例。

1
2
3
4
5
6
7
8
int main() {
{
object* p = new object();
shared_ptr<object> sp1(p);
shared_ptr<object> sp2(p);
}
return 0;
}

上方代码运行会崩溃。因为两个截然不同的智能指针sp1sp2同时管理了同一个object对象, 它们分别析构时会对指针pdelete两次。
所以我们更加推荐使用std::make_shared来代替使用裸指针初始化智能指针。见下面举例。

1
2
3
std::shared_ptr<object> sp = std::make_shared<object>();
/// 代替下面的初始化的方法
/// std::shared_ptr<object> sp = std::shared_ptr<object>(new object);

特殊情况

我们有时会在代码里遇到这样一种情况,见下面代码。

1
2
3
4
5
6
class error_object {
public:
std::shared_ptr<error_object> get_sp() {
return std::shared_ptr<error_object>(this);
}
};

上面代码中get_sp()的函数,目的是想返回一个能够管理自己的智能指针,但是我们可以看到每调用一次get_sp(),我们都会用同一个指针this, 创建一个不同的智能指针
这意味着我们调用两次get_sp()函数后,程序运行时会崩溃。

为了应对这种情况标准库中设计了一个工具函数std::enable_shared_from_this, 具体用法如下。

1
2
3
4
5
6
7
8
9
class correct_object : public std::enable_shared_from_this<correct_object> {
};

int main() {
auto instance = std::make_shared<correct_object>();
std::shared_ptr<correct_object> sp1 = instance.shared_from_this();
std::shared_ptr<correct_object> sp2 = instance.shared_from_this();
return 0;
}

上面代码可以正常运行。
关于std::enable_shared_from_this的实现原理,见C++ enable_shared_from_this原理与简单实现

std::weak_ptr

std::weak_ptr不能被称为一个独立的智能指针,它是std::shared_ptr智能指针的一种扩展。

std::weak_ptr的功能是,观察一个被std::shared_ptr管理的对象, 但不会影响std::shared_ptr的引用计数。

std::weak_ptr可以观察一个对象有没有被释放,或是用来防止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
class object {
public:
object() {
std::cout << "object()" << std::endl;
}
~object() {
std::cout << "~object()" << std::endl;
}
};

int main()
{
std::weak_ptr<object> weak;
{
auto sp = std::make_shared<object>();
weak = sp;
std::shared_ptr<object> sp1 = weak.lock();
if(sp1){
sp1->doSomeThing();
}
}
if (weak.expired()) {
std::cout << "资源没有释放" << std::endl;
} else {
std::cout << "资源已经释放" << std::endl;
}
}

以下代码出现了引用回环,会导致智能指针对象被销毁了,但资源没有被销毁。

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 list_node {
public:
list_node() {
std::cout << "list_node()" << std::endl;
}

~list_node() {
std::cout << "~list_node()" << std::endl;
}
std::shared_ptr<list_node> next;
};

int main()
{
{
auto sp0 = std::make_shared<list_node>();
auto sp1 = std::make_shared<list_node>();
auto sp2 = std::make_shared<list_node>();
sp0->next = sp1;
sp1->next = sp2;
sp2->next = sp0;
}
return 0;
}

运行结果:

Snipaste_2020-12-03_14-50-26

我们可以看到该对象的析构函数一个也没有执行,说明内存没有释放。

我们现在换成std::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
class list_node {
public:
list_node() {
std::cout << "list_node()" << std::endl;
}

~list_node() {
std::cout << "~list_node()" << std::endl;
}
std::weak_ptr<list_node> next;
};

int main()
{
{
auto sp0 = std::make_shared<list_node>();
auto sp1 = std::make_shared<list_node>();
auto sp2 = std::make_shared<list_node>();
sp0->next = sp1;
sp1->next = sp2;
sp2->next = sp0;
}
return 0;
}

运行结果:
Snipaste_2020-12-03_15-00-25

现在指针形成的环路被std::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
#include <iostream>
#include <memory>
#include <string>
#include <vector>

/// 订阅者
class subscriber {
public:
/// 订阅者被观察器调用的函数
void read(const std::string& str) { std::cout << str << std::endl; }
};

/// 广播者
class boardcaster {
public:
/// 广播者注册订阅者
void regist(std::shared_ptr<subscriber> sp) { vec.push_back(sp); }

/// 广播事件
void boardcast(const std::string& str) {
for (auto it = vec.begin(); it != vec.end();) {
auto sp = it->lock();
if (sp) {
/// 让订阅者接受消息
sp->read(str);
it++;
} else {
std::cout << "subscriber is delete!" << std::endl;
it = vec.erase(it); ///< 擦除这个订阅者的指针
}
}
}

private:
std::vector<std::weak_ptr<subscriber>> vec; ///< 用于保存订阅者的智能指针
};

int main() {
boardcaster obj;
auto reader0 = std::make_shared<subscriber>();
obj.regist(reader0);
{
auto reader1 = std::make_shared<subscriber>();
obj.regist(reader1);
auto reader2 = std::make_shared<subscriber>();
obj.regist(reader2);

obj.boardcast("start boardcast!");
}

obj.boardcast("reader1 and reader2 is deleted, boardcast angin!");
return 0;
}

智能指针(现代 C++)

C/C++内存泄漏及检测

知乎 C++ 怎么检测内存泄露,怎么定位内存泄露?

1
2
3
4
5
6
7
8
9
10
11
#ifdef _WIN32
#include <crtdbg.h>
#ifdef _DEBUG

#define new new(_NORMAL_BLOCK,__FILE__,__LINE__)
#endif
#endif

#ifdef _WIN32
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)|_CRTDBG_LEAK_CHECK_DF);
#endif

使用 CRT 库查找内存泄漏