C++智能指针

智能指针,从其本质上说,就是要控制对象的销毁时机。换句话讲就是何时调用对象的析构函数。从C++11开始引入三个智能指针(unique_ptr,shared_ptr,weak_ptr),准确的说是四种(还有auto_ptr),但从C++17开始auto_ptr被移除了。所以就剩下上述三种了。所有的智能指针都包含在memory头文件中。

首先,很久以前的C++是没有智能指针的,用户创建在堆上的内存,智能自己显示的释放,如果没有释放就会造成内存泄漏。这种特点导致C++的使用成本很高,为了降低成本,引入了智能指针unique_ptrshared_ptr

unique_ptr

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
class Simple {
private:
int m_a;
public:
void Show() {
printf("Hello Simple %d\n", m_a);
}
void showfunc() {
printf("Hello Simple func \n");
}
Simple(int n) {
m_a = n;
printf("Simple Construct\n");
}
Simple(const Simple& p) {
printf("Simple Copy Construct\n");
}
~Simple() {
printf("Simple Destroy\n");
}
};

void unique_test() {
// unique ptr
auto s_ptr = std::make_unique<Simple>(1);
s_ptr->Show();
std::unique_ptr<Simple> s_copy_ptr = std::move(s_ptr);
s_copy_ptr->Show();
//std::unique_ptr<Simple> s_ptr_2(s_ptr); // 无法编译通过
//std::unique_ptr<Simple> s_ptr_2 = s_ptr; // 无法编译通过
s_ptr->showfunc();
//s_ptr->Show(); // 当所有权变更后就不该再用之前的指针访问对象资源。
}
  • unique_ptr禁用拷贝构造
    由于unique_ptr禁用了拷贝构造函数unique_ptr(const unique_ptr&) = delete;,所以一切试图触发拷贝构造函数的操作都会引发编译错误。
  • unique_ptr所有权一旦变更就不能使用原指针访问对象资源
    上述代码中有两处使用了原指针访问对象资源,第一处s_ptr->showfunc();没有报错,可以正常打印;s_ptr->Show();中使用到了成员变量m_a所以会导致报错“this空指针”。这是由于std::move的赋值操作触发了unique_ptr中的Move构造函数(unique_ptr(unique_ptr&&) = default;),从而将s_ptr中的成员清空。所以再次访问原对象指针,就会出错。

shared_ptr

shared_ptr

shared_ptr采用的是引用计数的机制来控制对象的销毁时机。如其名字所示,其对象是共享的。当计数器等于0是,调用对象的析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void shared_test_inner(std::shared_ptr<Simple> *steal_ptr) {
// share ptr
auto s_ptr = std::make_shared<Simple>(1);
s_ptr->Show();
std::shared_ptr<Simple> s_copy_ptr(s_ptr);
s_copy_ptr->Show();
std::shared_ptr<Simple> s_ptr_2 = s_ptr;
s_ptr_2->Show();
// try steal
*steal_ptr = s_ptr;
}

void shared_test() {
shared_test_leak();
std::shared_ptr<Simple> s_ptr;
shared_test_inner(&s_ptr);
s_ptr->Show();
}

尽可能使用make_shared创建shared_ptr,如果使用std::shared_ptr<Simple> s_ptr(new Simple(1))创建shared_ptr,需要分配两次内存,一次是new Simple(1);另一次是shared_ptr的引用计数。make_shared只分配一次。

现在创建一个SimpleBack类,然后再让SimpleSimpleBack循环引用。

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
class SimpleBack;

class Simple {
private:
int m_a;
public:
void Show() {
printf("Hello Simple %d\n", m_a);
}
void showfunc() {
printf("Hello Simple func \n");
}
Simple(int n) {
m_a = n;
printf("Simple Construct\n");
}
Simple(const Simple& p) {
printf("Simple Copy Construct\n");
}
~Simple() {
printf("Simple Destroy\n");
}
std::shared_ptr<SimpleBack> m_sb;
};

class SimpleBack {
public:
std::shared_ptr<Simple> m_s;
~SimpleBack() {
printf("SimpleBack Destroy\n");
}
};

void shared_test_leak() {
auto s = std::make_shared<Simple>(2);
auto sb = std::make_shared<SimpleBack>();

s->m_sb = sb;
sb->m_s = s;
// 通过打印语句可以看到 s和sb的析构函数并没有调用
}

通过析构函数函数的打印语句可以看出ssb并没有被析构,这说明ssb泄漏了。

为了解决shared_ptr在循环依赖中内存泄漏的问题,推出了weak_ptr

weak_ptr

weak_ptr不会增加引用计数,不能直接操作对象的内存(需要先调用lock接口),需要和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
class SimpleBack;

class Simple {
private:
int m_a;
public:
void Show() {
printf("Hello Simple %d\n", m_a);
}
void showfunc() {
printf("Hello Simple func \n");
}
Simple(int n) {
m_a = n;
printf("Simple Construct\n");
}
Simple(const Simple& p) {
printf("Simple Copy Construct\n");
}
~Simple() {
printf("Simple Destroy\n");
}
std::shared_ptr<SimpleBack> m_sb;
};

class SimpleBack {
public:
// 将循环引用的其中一个改成weak_ptr
std::weak_ptr<Simple> m_s;
~SimpleBack() {
printf("SimpleBack Destroy\n");
}
};

void shared_test_leak() {
auto s = std::make_shared<Simple>(2);
auto sb = std::make_shared<SimpleBack>();

s->m_sb = sb;
sb->m_s = s;
}

通过析构函数的打印语句可以看出,ssb的析构函数在shared_test_lead()调用结束后被调用。

那么,weak_ptr的使用是不是也像shared_ptr一样呢?不是的。weak_ptr需要与shared_ptr配合使用,看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void weak_test() {
std::weak_ptr<Simple> w;
{
auto s = std::make_shared<Simple>(3);
w = s;
auto s2 = w.lock();
if(s2 != nullptr) {
s2->Show();
}
}
if(w.expired()) {
printf("object 's' is destroied. \n");
}
}
  • lock
    若对象已被析构,则返回一个空的shared_ptr;否则返回实际的shared_ptr
  • expired
    若对象已被析构,则返回true;否则返回false

参考&鸣谢