浅入浅出了解智能指针

智能指针和他背后的故事

为什么需要智能指针

看一个很简单的指针的例子。

1
2
3
4
5
6
7
void func()
{
ObjectType* temp_ptr = new ObjectType();
temp_ptr->execute();
delete temp_ptr;
temp_ptr = nullptr;
}

    使用到指针就会想到如果没有回收这个指针,就会生成一个野指针,也就是说这个指针现在指向的内存就不受程序员的控制,也会非常容易造成内存泄漏。
但实际上,如果 temp_ptr 在执行 execute 函数时如果抛出异常,也不会执行 delete temp_ptr 也不会执行,也会造成内存泄露的情况。

划重点,如果我们希望函数终止时能够自动回收,不管他是为什么终止的

    猫猫灵机一动,发现其实析构函数就有这个功能,析构函数,在对象析构时就能够释放它所指向的内存,但是我们这个定义的指针是一个常规指针,并不是一个有析构函数的类对象指针,怎么办呢,脑门一拍,我们把他封装成类对象指针不就行啦。

RAII思想了解一下

    RAII 的全称是 Rescourse Acquisition is Initialization ,翻译一下就是资源获取即初始化。

    由于系统的资源不具有自动释放的功能,而 C++ 中的类具有自动调用析构函数的功能,如果我们把资源用类封装起来,在析构函数中进行释放资源,当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用我们显示的去调用释放资源的操作了。
一个栗子:

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 RAII
{
public:
RAII()
{
new_array = new int[10];
}
void init()
{
for(int i = 0;i < 10; i++)
{
*(new_array + i) = i;
}
}
void show()
{
for(int i = 0;i < 10; i++)
{
cout << new_array[i] << endl;
}
}
~RAII()
{
if(new_array != nullptr)
{
delete(new_array);
new_array = nullptr;
}
}
private:
int *new_array;
};

    在这里我们把数组的创建和对象的创建绑定在一起,那么我们就不需要手动的去处理 int 指针 的回收,当对象的生命周期结束的时候,就会自动的调用对象析构函数回收指针指向的内存。

对象的生命周期啥时候结束呢?

    按照常理对象的生命周期在他的作用域结束以后应该就结束了,然鹅,并不是这样的,这就牵扯到一个神奇的东西叫做引用计数Cplusplus不生产辣鸡,所以Cplusplus没有垃圾回收机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int func()
{
shared_ptr <int> p(new int);
// 声明以后这个对象的引用计数就是1
{
// 进入了一个局部作用域哦
shared_ptr <int> pp = p;
// 因为pp和p都能访问这块内存,所以引用计数为2
}
// 超出pp的作用域了,现在只有p可以访问这块内存
// 此时引用计数为1,那么pp所指的对象会被回收吗? 答案是不会
// 因为还有指针指向这块内存,引用计数不为0,所以不会被回收
}
// func函数结束,所有的局部变量都被回收

当当当当,智能指针,时代的光明w

    所以我们现在明白了,智能指针就是基于 RAII 思想的一种封装过的指针,他的本质就是引用计数。
上一个栗子中,这个指针好像没有见过, shared_ptr 是啥呢,就是我们大多数人理解的智能指针。

shared_ptr 多个指针指向相同的对象时,每一次shared_ptr的拷贝会将内部的引用计数 +1,每次析构都会将引用计数 -1,当引用计数为 0 时,自动回收所指向的堆内存。

  • 我觉得 reference counting 为多个指针时,是共享同一个数据结构的ww,但是没有找到相关资料(因为我懒得看源码)。

时代的光明拥有了眼泪

环状引用,引用计数的泪水

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 parent;
class children;
using parent_ptr = shared_ptr<parent>;
using children_ptr = shared_ptr<children>;

class parent
{
public:
~parent() { std::cout <<"destroying parent\n"; }

public:
children_ptr children;
};

class children
{
public:
~children() { std::cout <<"destroying children\n"; }

public:
parent_ptr parent;
};

void test()
{
children_ptr son(new children);
parent_ptr father(new parent());
son->parent = parent_ptr(father);
father->children = son;
}

    我们发现一个问题,当 test 函数执行结束以后,什么都没有输出,也就是说,两个构造函数都没有被调用。
嗯?咋肥四,和预期不符。我们仔细瞧瞧,发现,欸?为什么他们的引用计数都不是 0,原来是出现了环状引用,爸爸指向了儿子,所以儿子不为 0,儿子又指向了爸爸,爸爸也不为 0,欸?引用计数哭了,你呢。

我们可以使用 weak_ptr 来解决这个问题。 weak_ptr 是一种弱引用,即使存在 weak_ptr 的引用,如果没有 shared_ptr 的引用,对象依然会被析构,环就被打破了,环状引用导致无法析构的问题就被解决了。

  • 浅入浅出,不贴例子了,自己试试叭吼

  • 最后的最后,虽然 weak_ptr 可以有效的解除循环引用,但是这必须是程序员能够预见会出现循环引用的情况下才能使用,没辽。