引用计数式智能指针

既然手动内存管理这么麻烦,那么为什么来一个自动的内存管理呢


c++当中,对于一个new,就要对应一个delete,进行相对应的内存释放。只要成对的出现分配与释放,那自然就不会引起任何内存泄漏的问题。但是,说很简单,但是当做起来的时候就未必了。比如对于一个函数返回的指针,我们需不需要对其进行内存的释放呢?而对于一个传入的指针,我们又需不需要对其进行内存的分配呢?还有,下面这种情况

1
2
3
4
char *p = new char[10];
char *p1 = p;
......
p5 = p4 = p3 = p2 = p1 = p

当我们有很多变量都需要p的时候,我们应不应该释放pn?这个时候,我们就需要利用一些东西去让我们自动的管理指针了。

我们面对的问题无非就是怎么样让指针在应该释放的时候释放,不应该释放的时候不会释放,以及一定要指针得到释放。首先,其实我们可以对于每一个内存块都建立一个计数器,一旦有指针指向这个内存块,我们就使得计数器增加,当有一个指针被释放的时候,我们就减少我们的计数器,当计数器为0的时候才真正的去释放这一块的内存。这个方法,就叫做引用计数法。

还有一个问题就是,怎么样才能让指针不再使用的时候就能够得到释放呢?

其实这个问题,我们可以参考一下栈上的临时变量,当函数调用的时候,栈上的空间被申请,而里面的临时变量都会同时被申请了出来,但是,当执行完函数的时候,临时变量也同样会被析构。因此,我们可以利用这个特点,将指针封装到一个对象上去,那就是智能指针了。

RCObject

在实现指针之前,我们有一个东西要处理,那就是计数器。按照我们之前的想法,所有内存都使用一个计数器去管理,因此,我们首先来实现这个计数器。

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 RCObject
{
public:

void addReference();
void removeReference();
void markUnshareable();

bool isShareable() const;
bool isShared() const;

protected:

RCObject();
RCObject(const RCObject& rhs);
RCObject& operator=(const RCObject& rhs);
virtual ~RCObject() = 0;

private:

int refCount;
bool shareable;

};

当有一个指针进来引用同一块内存的时候,我们就增加计数器,当有指针被销毁的时候减少引用,当然,这些都是智能指针应该去干的事情,这里只是一个计数器。当然,计数器同时也要拥有一个判断当前内存块是不是被共享的方式,至于具体原因之后再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RCObject::RCObject()
:refCount(0),shareable(true)
{}

RCObject::RCObject(const RCObject& rhs)
:refCount(0),shareable(true)
{}

RCObject& RCObject::operator=(const RCObject& rhs)
{
return *this;
}

//虽然是纯虚函数,但是必须要提供一个实现,因为这是一个析构函数,派生类调用自身的析构的时候一定会调用它的
RCObject::~RCObject(){}

void RCObject::addReference(){++refCount;}
void RCObject::removeReference(){if(--refCount == 0) delete this;}
void RCObject::markUnshareable(){shareable = false;}

bool RCObject::isShareable() const { return shareable;}
bool RCObject::isShared() const {return refCount > 1;}

RCPtr

首先,先思考一下这个东西要有什么样的效果。首先能够自动对内存进行管理那当然是必须的。然后,还需要能够兼容指针的操作,换句话说就是像指针一样的使用方式,那么*,->这些操作符肯定要重载的,还有就是那些指针的判空操作比如if(ptr),当然,我们可以假如一个isEmpty()的方法,但是,这样不是很够自然,因此我们需要一些能够隐式转换类型的方式,那么说,operator bool()是要去建立的。还有就是感叹号的!,这样也可以去兼容if(!ptr)这种判断方式。

另外还有一个问题就是,当我们对指针进行操作的时候,由于我们都是指向同一个内存的,但是,我的想法肯定不是想要

1
2
ptrb = ptra;
ptrb->doSomething();

之后,就改变了ptra的内容,因此,我们需要在对ptrb进行操作的时候,对其进行写时复制。将指针的复制操作延缓到真的要实际操作的时候才进行复制,这样对于需要引用大量同样的对象却使用比较少的情况有比较少的优化效果。当然,假如你想同时操作两个对象,那么这个就不太适合这种情况了。而之前所说的判断空间是否被分享的作用也就于此。

既然需要实现的内容都清楚了,我们就可以去实现内容了。

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
template<typename T>
class RCPtr
{
public:
RCPtr(T* realPtr = 0)
:pointee(realPtr)
{ init(); }
RCPtr(RCPtr& rhs)
:pointee(rhs.pointee)
{ init(); }
~RCPtr(){ holder->removeReference(); }

RCPtr<T>& operator=(RCPtr<T>& rhs);

//用const与否来判断接下来的对象是否需要用到写时复制
T* operator->();
T& operator*();

T* operator->() const;
T& operator*() const;

bool operator!() const;
operator bool();

private:
T *pointee;

void init();
void makeCopy();
};

template<typename T>
RCPtr<T>::operator bool()
{
return pointee;
}

template<typename T>
bool RCPtr<T>::operator!() const
{
return pointee == 0;
}

template<typename T>
T& RCPtr<T>::operator*() const
{
return *pointee;
}

template<tyepname T>
T* RCPtr<T>::operator->() const
{
return pointee;
}

template<typename T>
T* RCPtr<T>::operator->()
{
makeCopy();
return pointee;
}

template<typename T>
T& RCPtr<T>::operator*()
{
makeCopy();
return *pointee;
}

template<tyepname T>
RCPtr<T>& RCPtr<T>::operator=(RCPtr<T>& rhs)
{
if(pointee != rhs.pointee)
{
if(pointee)
{
pointee->removeReference();
}
pointee = rhs.pointee;
pointee->addReference();
}
}

template<typename T>
void RCPtr<T>::makeCopy()
{
if(pointee->isShared())
{
T *oldValue = pointee;
pointee->removeReference();
pointee = new T(*oldValue);
}

pointee->addReference();
}

template<typename T>
void RCPtr<T>::init()
{
if(pointee == 0)
return;

if(pointee->isShareable() == false)
{
pointee = new T(*pointee);
}

pointee->addReference();
}

在这里,我们仍然面临着两个问题,首先,就是派生类的问题

1
2
3
4
5
6
7
8
9
10
11
class A;
class B:public A;
class C:public A:

megre(const RCPtr<A>& a1,const RCPtr<A>& a2);

RCPtr<B> b;
RCPtr<C> c;

//错误
megre(b,c);

虽然在指针上,这两个拥有派生类的家伙能够用隐式转换的方式进行正确的传递。但是,这个在RCPtr<T>之中就完全不行了,因为,虽然泛型的参数是有继承关系的,但是两个类实际上毫无关联。因此,为了解决这个问题,我们就需要使用类型转换的重载了。

1
2
3
4
5
template<class newType>
operator RCPtr<newType>()
{
return RCPtr<newType>(pointee);
}

虽然,这里使用了泛型重载了类型变换,但是,构造函数的指针赋值操作会为我们检查这个操作是否合法,也就是说只有属于同一条继承链上的人才会得到正确的转换

RCHolder

接下来就是一个很重要的问题,那就是,我用这个智能指针还必须要参数对象T要继承RCObject才能操作是不是很不方便???

没错,确实十分不方便,因此,我们现在就来解决这个问题。有句话说过,计算机当中的所有问题都能够通过增加一个层来解决,因此,同样的,我们可以为这个指针增加一个中间层,专门用来处理计数器的

我们现在的情况是这样的。智能指针记录了指针,指针身上有一个计数器,智能指针控制指针身上的计数器,当计数器归零的时候,就会销毁指针。那么,其实可以看出来,计数器对于指针来说并不是一个必须的东西,我们可以在中间加一个holder,变成这样的方式

1
2
3
4
             RCObject
|
|
RCPtr<T> --> Holder --> pointee

计数器管理Holder,对智能指针的操作都会反映到holder身上,而holder持有pointee,当计数器归零的时候,holder会销毁,而holder所持有的pointee也会顺势销毁,这样,指针和计数器之间的耦合关系就被解除了。

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
template<typename T>
class RCPtr
{
public:
RCPtr(T* realPtr = 0)
:holder(new RCHolder())
{ holder->pointee = realPtr; init(); }
RCPtr(RCPtr& rhs)
:holder(rhs.holder)
{ init(); }
~RCPtr(){ holder->removeReference(); }

RCPtr<T>& operator=(RCPtr<T>& rhs);

T* operator->();
T& operator*();

T* operator->() const;
T& operator*() const;

bool operator!() const;
operator bool();

template<class newType>
operator RCPtr<newType>();

private:
struct RCHolder : public RCObject
{
T *pointee;
~RCHolder(){ delete pointee; pointee = 0;}
}

RCHolder *holder;

void init();
void makeCopy();
};

template<T>
template<newType>
RCPtr<T>::operator RCPtr<newType>()
{
return RCPtr<newType>(holder->pointee);
}

template<typename T>
RCPtr<T>::operator bool()
{
return holder->pointee;
}

template<typename T>
bool RCPtr<T>::operator!() const
{
return holder->pointee == 0;
}

template<typename T>
T& RCPtr<T>::operator*() const
{
return *(holder->pointee);
}

template<tyepname T>
T* RCPtr<T>::operator->() const
{
return holder->pointee;
}

template<typename T>
T* RCPtr<T>::operator->()
{
makeCopy();
return holder->pointee;
}

template<typename T>
T& RCPtr<T>::operator*()
{
makeCopy();
return *(holder->pointee);
}

template<tyepname T>
RCPtr<T>& RCPtr<T>::operator=(RCPtr<T>& rhs)
{
if(holder != rhs.holder)
{
holder->removeReference();
holder = rhs.holder;
init();
}

return *this;
}

template<typename T>
void RCPtr<T>::makeCopy()
{
if(holder->isShared())
{
T *oldValue = holder->pointee;
holder->removeReference();
holder = new RCHolder();
holder->pointee = new T(*oldValue);
holder->addReference();
}
}

template<typename T>
void RCPtr<T>::init()
{
if(!holder->isShareable())
{
T *oldValue = holder->pointee;
holder = new RCHolder;
holder->pointee = new T(*oldValue);
}

holder->addReference();
}

最终就会是这个样子了

与share_ptr

stl当中的share_ptr与这个的设计都是使用了引用计数的方式,但是不同的一点在与,share_ptr对于一个被复制的指针保留了原有的指针的效果,也就说不会写时复制,当另一个指针修改了内容之后,这个内存块的位置也会被修改。从设计上来说,感觉share_ptr与原生的指针的操作会更加的相似一些,而我这里的操作会对于每一个指针的持有者都会有独一无二的内存空间,也就没有了共享指针这种操作了。

参考书籍《more effective c++》,这里也是里面的RCPtr的设计

  • 本文作者: ShinyGX
  • 本文链接: https://ShinyGX.github.io/posts/dbb7ef5e/
  • 版权声明: 本博客所有文章除特别声明外,均采用 https://creativecommons.org/licenses/by-nc-sa/3.0/ 许可协议。转载请注明出处!
0%