RAII

在了解智能指针之前,我们需要先要了解RAII,那么什么是RAII呢?RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络链接、互斥量等)的简单技术

具体来说,是在对象构造时获取资源,对资源的控制管理在整个对象的生命周期内都保持有效,并在对象析构时释放资源,也就是将资源的管理托管给一个对象,这有着一些好处:

  • 不用显示释放资源
  • 对象所需的资源在其整个生命周期内始终保持有效

RAII-引入

下边是一段异常相关的代码,main函数调用fun,fun函数先new了块空间,然后调用div函数,但是div函数如果出现除0错误,会抛出异常,直接被main函数捕获,那么fun中的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
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw "除0错误";
return a / b;
}


void fun()
{
int* p = new int(1);
cout << div() << endl;

delete p;
}


int main()
{
try
{
fun();
}
catch (const char* str)
{
cout << str << endl;
}
return 0;
}

针对上边代码,是有一些方法进行处理的,但是这里可以用RAII的技术解决这个问题,具体看下方代码:

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
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}

~SmartPtr()
{
if(_ptr)
delete _ptr;
cout << "SmartPtr:申请的资源已经释放" << endl;
}
private:
T* _ptr;
};


int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw "除0错误";
return a / b;
}


void fun()
{
SmartPtr<int>sp = new int(1);
cout << div() << endl;
}

针对上边场景我们可以设计一个SmartPtr类,这个类构造函数接收一个资源的管理权,析构函数释放这份资源,那么当创建的对象的生命周期结束后,就自动调用析构函数并释放资源。

智能指针

上边的SmartPtr类,就是RAII,但是只有管理资源释放的功能,并没有指针解引用和->的操作,不能对管理的资源进行控制,那么我们如何让SmartPtr支持像指针一样的行为呢?这里我们将引入智能指针的概念,并且简要的模拟四种库的智能指针,理解原理

智能指针的原理如下:

  1. RAII特性
  2. 重载operator * 和opertaor->,具有像指针一样的行为。

我们之前实现的SmartPtr其实还存在着其他的问题:就是不能进行拷贝,如果进行拷贝,会出现资源重复释放的问题。

比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A
{
public:
int _a = 0;
int _b = 0;
};
int main()
{
SmartPtr<A>ap1(new A);

SmartPtr<A>ap2(ap1);

return 0;
}

运行结果也不出意外的报错了。

auto_ptr

针对上边拷贝的问题,auto_ptr提供了它的方案,auto_ptr的实现原理:管理权转移的思想,但是这种做法并不太好,你一旦拷贝构造,被拷贝的对象就无法使用了,容易出错。

看一下库的auto_ptr,如果对进行拷贝构造,其实是进行资源的转移

我们对auto_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
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}

// 拷贝构造要进行管理权的转移
auto_ptr(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}

auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr) // 判断_ptr是否为空
delete _ptr;

_ptr = ap._ptr;
ap._ptr = nullptr;
}

return *this;
}

~auto_ptr()
{
if (_ptr)
delete ptr;
}

T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _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
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& up) = delete;

unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

~unique_ptr()
{
if (_ptr)
delete ptr;
}

T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};

shared_ptr

下边代码有一些开始写成了share_ptr,少了个d,后补上了,导致有一些截图和代码少一个d

auto_ptr和unique_ptr都针对智能指针的拷贝问题上有各自的处理方式,但是都比较呆,很多时候我们是有拷贝指针的需求的,那么这时候share_ptr就登场了。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

如何根据shared_ptr的原理,进行引用计数功能的实现呢?

方案1、静态成员变量 count计数

这里是否可行呢?我们试着实现一下,并测试效果:

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<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
_count++;
}

shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
{
_count++;
}

T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}


~shared_ptr()
{
if (--_count == 0)
{
delete _ptr;
}
}
private:
T* _ptr;
static int _count;
};

template<class T>
int shared_ptr<T>::_count = 0;

运行测试:

使用静态成员变量确实可以解决拷贝构造导致的重复释放资源的问题,但是,这也会引发新的问题。因为静态成员变量不属于某一个对象,它属于整个类。如果我们再创建一个对象就会引发资源泄漏的问题。

这里创建了个sp3,按理说有了两份资源,但是只析构了一次,原因如下。

我们其实期望的是对一份资源的管理,这一份资源要有独立的引用计数,但是用静态成员变量做不到这一点,无论多少份的资源,它们会共享一份引用计数,这样就会导致资源泄漏问题。

所以,静态成员变量这种方案是不行的,那么应该怎么做呢?我们引入第二种的方案。

方案2、在堆上申请一块空间做计数

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
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}

shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
(*_pcount)++;
}

shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
// share_ptr<A>sp1(new A)
// share_ptr<A>sp2(sp1)
// sp1 = sp2
// 为了防止上边场景,用_ptr判断更好
if (_ptr == sp._ptr)
return *this;

// --被赋值对象的计数,如果是最后一个对象要释放资源
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}

// 共管新资源,++计数
_ptr = sp._ptr;
_pcount = sp._pcount;

(*_pcount)++;

return *this;
}

T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}


~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
private:
T* _ptr;
int* _pcount;
};

运行结果如下:可以看到解决了上边的问题。

weak_ptr

我们的shared_ptr看起来已经很厉害了,那这个weak_ptr又有什么作用呢?这里就要看shared_ptr潜在的一个问题了。

循环引用

问题的引入:我们有个节点类,用shared_ptr会发生什么样的问题,我们来看一下下面

上边说share_ptr的对象不能给Node*,我们可以更改一个Node类,将前后指针改成智能指针。

经过更改后发现没有任何的问题,shared_ptr好像完美的胜任了节点的指针这个角色

我们继续看一个场景:

当我们加上sp2->_pre=sp1时,就出现问题了,运行结果是两个节点都没有释放,这是为啥呢,非常奇怪的问题。

这里我们就要引出,循环引用的概念了。

这时候就出现了循环引用的问题,导致了两边的资源都没有没释放,这时候就引入了weak_ptr来解决这个问题。

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

sp1-> _ next = sp2;sp2->_ pre = sp1;时weak_ptr的_next和_ prev不会增加sp1和sp2的引用计数。

我们再看一下运行结果,发现问题已经被解决。

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
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{

}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}

weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}

weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}

T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}

private:
T* _ptr;
};

切换成st的命名空间,结果如下:

也实现了库中weak_ptr的效果,解决了循环引用的问题。

定制删除器

定制删除器是针对智能指针管理的类型定制专用的删除器,如果没有制删除器可能会出现一些错误。

比如下边代码,用了库里面的shared_ptr

1
2
3
4
5
6
int main()
{
std::shared_ptr<int>s1(new int[5]);
std::shared_ptr<Node>sp2(new Node[5]);
return 0;
}

运行结果如下:

可以看到程序直接崩溃了,其实第一个还没有崩,第二个Node[5]那里才崩溃掉

由于释放的位置不对导致程序崩溃掉。

可以通过定制删除器解决这个问题,也就是传个函数对象。

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
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};

template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "free" << ptr << endl;
free(ptr);
}
};

void test_del()
{
// 仿函数对象
std::shared_ptr<Node> n1(new Node[5], DeleteArray<Node>());

std::shared_ptr<Node> n2(new Node);

std::shared_ptr<int> n3(new int[5], DeleteArray<int>());

std::shared_ptr<int> n4((int*)malloc(sizeof(12)), Free<int>());

// lambda
std::shared_ptr<Node> m1(new Node[5], [](Node* ptr){delete[] ptr; });
std::shared_ptr<Node> m2(new Node);

std::shared_ptr<int> m3(new int[5], [](int* ptr){delete[] ptr; });

std::shared_ptr<int> m4((int*)malloc(sizeof(12)), [](int* ptr){free(ptr); });
std::shared_ptr<FILE> m5(fopen("test.txt", "w"), [](FILE* ptr){fclose(ptr); });

主要unique是在模板参数这里
//std::unique_ptr<Node, DeleteArray<Node>> up(new Node[5]);
}

模拟完善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
template<class T, class D = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
{}

void Release()
{
if (--(*_pCount) == 0)
{
D()(_ptr);
delete _pCount;
}
}

~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this == &sp)
if (_ptr == sp._ptr)
{
return *this;
}

Release();

// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;

(*_pCount)++;

return *this;
}

T& operator*()
{
return *_ptr;
}

T* operator->()
{
return _ptr;
}

int use_count()
{
return *_pCount;
}

T* get() const
{
return _ptr;
}

private:
T* _ptr;

// 引用计数
int* _pCount;
};

这里的模拟定制删除器的功能,是简单版的通过模板参数控制