C++ 理解智能指针(手写一个简单的智能指针)

简单手写一个智能指针,不会过多关注具体功能上的实现,而是思想上的实现。

比如指针上的诸多行为++,--这里不会多次提到,而使一些关于智能指针特性和思想的实现。

RAII与手写智能指针

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
class shape
{
public:

shape()
{
cout<<"new shape"<<endl;
}
};

class shape_wrapper
{
public:
explicit shape_wrapper(shape* ptr = nullptr):ptr_(ptr){}
~shape_wrapper()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
shape* get() const {return ptr_;}
private:
shape* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
shape_wrapper(new shape());

return 0;
}

我们继续扩展:

  • 让智能指针不止受限于shape
  • 添加指针行为

模板封装

首先让智能指针不止受限于shape,利用模板即可

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

shape()
{
cout<<"new shape"<<endl;
}
};

template<typename T>
class smart_ptr
{
public:
explicit smart_ptr(T* ptr = nullptr):ptr_(ptr){}
~smart_ptr()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
T* get() const {return ptr_;}
private:
T* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
smart_ptr<shape>(new shape());

return 0;
}

运算符重载模拟指针行为

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
class shape
{
public:

shape()
{
cout<<"new shape"<<endl;
}
};

template<typename T>
class smart_ptr
{
public:
explicit smart_ptr(T* ptr = nullptr):ptr_(ptr){}
~smart_ptr()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
T* get() const {return ptr_;}
//指针行为
T& operator*() const {return *ptr_;}
T* operator->() const {return ptr_;}
operator bool() const {return ptr_;}
private:
T* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
smart_ptr<shape>ptr(new shape());


return 0;
}

添加拷贝构造和赋值

这是个很难定义行为的问题:

我们是否应该禁用这种行为:

1
2
smart_ptr<shape> ptr1{new shape()};
smart_ptr<shape>ptr2{ptr1};

这会导致ptr2 ptr1释放同一片区域的内存两次造成崩溃,因此我们暂时考虑禁用这种行为: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
32
33
34
35
36
37
38
39
40
41
class shape
{
public:

shape()
{
cout<<"new shape"<<endl;
}
};

template<typename T>
class smart_ptr
{
public:
explicit smart_ptr(T* ptr = nullptr):ptr_(ptr){}
//删除拷贝构造函数和赋值构造函数
smart_ptr(const smart_ptr&) = delete;
smart_ptr& operator=(const smart_ptr&) = delete;
//析构函数释放指针
~smart_ptr()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
T* get() const {return ptr_;}
//指针行为
T& operator*() const {return *ptr_;}
T* operator->() const {return ptr_;}
operator bool() const {return ptr_;}
private:
T* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
smart_ptr<shape>ptr(new shape());

return 0;
}

也许我们可以更好的解决这个问题,不再禁用拷贝构造和赋值构造,而是转交指针的的所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
//拷贝构造函数和赋值构造函数 转交 指针拥有权
smart_ptr(smart_ptr& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr& rhs)
{
//下面这一句话做了两件事情:
//1. smart_ptr(rhs) ,拷贝构造出一个匿名变量,rhs的资源被release掉,rhs的指针拥有权转交给了这个匿名变量
//2. 匿名变量调用了swap函数,this获得了这个所有权,匿名变量走出作用域,自己消失了。
smart_ptr(rhs).swap(*this);
return *this
}

这有个疑问: 赋值函数中不都会加上 if (this != &rhs)吗?

那种用法更啰嗦,而且异常安全性不够好,如果在赋值过程中发生异常的话,this 对象的内容可能已经被部分破坏了,对象不再处于一个完整的状态。
目前这种惯用法(参考资料)则保证了强异常安全性:赋值分为拷贝构造和交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this 对象完全不受任何影响。无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。

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
class shape
{
public:

shape()
{
cout<<"new shape"<<endl;
}
};

template<typename T>
class smart_ptr
{
public:
explicit smart_ptr(T* ptr = nullptr):ptr_(ptr){}
//拷贝构造函数和赋值构造函数 转交 指针拥有权
smart_ptr(smart_ptr& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr& rhs)
{
//下面这一句话做了两件事情:
//1. smart_ptr(rhs) ,拷贝构造出一个匿名变量,rhs的资源被release掉,rhs的指针拥有权转交给了这个匿名变量
//2. 匿名变量调用了swap函数,this获得了这个所有权,匿名变量走出作用域,自己消失了。
smart_ptr(rhs).swap(*this);
return *this
}

~smart_ptr()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
T* get() const {return ptr_;}
T* release()
{
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
}
//指针行为
T& operator*() const {return *ptr_;}
T* operator->() const {return ptr_;}
operator bool() const {return ptr_;}
private:
T* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
smart_ptr<shape>ptr(new shape());


return 0;
}

到此为止,这就是C++98给出auto_ptr的定义。 不过在C++17已经把auto_ptr删除掉了, 因为这个有很大的问题就是:程序员常常会不小心把smart_ptr传送给另一个,你就不在拥有这个对象了。这里的问题就是你赋值/拷贝时到底希不希望移交所有权,如果不希望移交,只希望多个同时拥有那就是shared_ptr,如果希望移交那就是unique_ptr,那怎么在unique_ptr移交时给一个程序员提醒呢? 利用左右值,当右值时才可以移交权力。

添加移动语义: unque_ptr() 雏形

  • 把拷贝构造函数中的参数类型smart_ptr&改成了 smart_ptr&&, 现在它成了移动构造函数

  • 把赋值函数中的参数类型 smart_ptr& 改成了smart_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。

1
2
3
4
5
6
7
8
9
smart_ptr(smart_ptr&& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs)
{
rhs.swap(*this);
return *this
}

根据 C++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用, 因此我们可以得到一个基本的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
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
class shape
{
public:

shape()
{
cout<<"new shape"<<endl;
}
};

template<typename T>
class smart_ptr
{
public:
explicit smart_ptr(T* ptr = nullptr):ptr_(ptr){}

smart_ptr(smart_ptr&& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs)
{
rhs.swap(*this);
return *this;
}

~smart_ptr()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
T* get() const {return ptr_;}
T* release()
{
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
}
//指针行为
T& operator*() const {return *ptr_;}
T* operator->() const {return ptr_;}
operator bool() const {return ptr_;}
private:
T* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
smart_ptr<shape>ptr1(new shape());

smart_ptr<shape>ptr2{ptr1};// error: use of deleted function 'constexpr smart_ptr<shape>::smart_ptr(const smart_ptr<shape>&)'|

smart_ptr<shape>ptr3;
ptr3 = ptr1;// error: use of deleted function 'constexpr smart_ptr<shape>::smart_ptr(const smart_ptr<shape>&)'|

ptr3 = std::move(ptr1);//ok

smart_ptr<shape> ptr4{std::move(ptr3)};//ok

return 0;
}

小细节:子类指针向基类指针的转换

一个circle* 是可以隐式转换成shape*的,但上面的smart_ptr<circle>却无法自动转换成 smart_ptr<shape>。这个行为显然还是不够“自然”。

我们只需要改变一下移动构造函数即可:再加一个模板即可

1
2
3
4
5
template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.release();
}

我们自然而然利用了指针的转换特性:现在 smart_ptr<circle> 可以移动给smart_ptr<shape>,但不能移动给smart_ptr<triangle>。不正确的转换会在代码编译时直接报错。

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
class shape
{
public:

shape()
{
cout<<"new shape"<<endl;
}
};

class circle:public shape
{
public:
circle()
{
cout<<"new circle"<<endl;
}
};

template<typename T>
class smart_ptr
{
public:
explicit smart_ptr(T* ptr = nullptr):ptr_(ptr){}

template<typename U>
smart_ptr(smart_ptr<U>&&other)
{
ptr_ = other.release();
}

smart_ptr& operator=(smart_ptr rhs)
{
rhs.swap(*this);
return *this;
}

~smart_ptr()
{
delete ptr_;
cout<<"delete ptr_"<<endl;
}
T* get() const {return ptr_;}
T* release()
{
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
}
//指针行为
T& operator*() const {return *ptr_;}
T* operator->() const {return ptr_;}
operator bool() const {return ptr_;}
private:
T* ptr_;
};

int main()
{

//采用RAII帮助我们封装指针,当离开作用域自动调用析构函数,防止内存泄漏
smart_ptr<shape>ptr1(new shape());

smart_ptr<circle>circle_ptr(new circle());
ptr1 = std::move(circle_ptr);

return 0;
}

到此一个有基本功能unique_ptr就完成了

shared_ptr引用计数实现

image-20220715112222225

我们需要搞一个类表示共享计数模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class shared_count
{
public:
shared_count():count_(1) {}
void add_count() ++count_;
long long reduce_count() --count_;
long long get_count() const
{
return count_;
}

private:
long long count_;
};

初始化shared_ptr需要构造一个shared_count

析构函数在看到 ptr_非空时(此时根据代码逻辑,shared_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
template <typename T>
class smart_ptr {
public:
explicit smart_ptr(T* ptr = nullptr)
: ptr_(ptr)
{
if (ptr) {
shared_count_ =
new shared_count();
}
}
~smart_ptr()
{
if (ptr_ &&
!shared_count_
->reduce_count()) {
delete ptr_;
delete shared_count_;
}
}

private:
T* ptr_;
shared_count* shared_count_;
};

同理swap函数也应该swap掉引用计数模块:

1
2
3
4
5
6
7
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
swap(shared_count_,
rhs.shared_count_);
}

赋值函数可以跟前面一样,保持不变(这是因为赋值函数就是利用拷贝构造和swap实现的),但拷贝构造和移动构造函数是需要更新一下:更新计数功能

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
smart_ptr(const smart_ptr& other)
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& other)
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}

拷贝构造函数为什么有一个泛型版本 还有一个非泛型版本 但是函数体内容又一模一样 不是代码冗余的吗 是有什么特殊设计意图吗?

这是一个很特殊的、甚至有点恼人的情况。如果没有非泛型版本,编译器看到没有拷贝构造函数,会生成一个缺省的拷贝构造函数。这样,同样类型的smart_ptr的拷贝构造会是错误的。“子类指针向基类指针的转换”这一节里我也提到了这点。这不是我讲智能指针想讲的内容,所以就淡化了。

shared_ptr去除release(),改用use_count()

release()unique_ptr特有的产物,我们不需要在shared_ptr中存在这种东西,我们更需要的是看下引用计数器有多少个: 添加use_count()

1
2
3
4
5
6
7
8
9
long use_count() const
{
if (ptr_) {
return shared_count_
->get_count();
} else {
return 0;
}
}

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
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#include <utility>  // std::swap

class shared_count {
public:
shared_count() noexcept
: count_(1) {}
void add_count() noexcept
{
++count_;
}
long reduce_count() noexcept
{
return --count_;
}
long get_count() const noexcept
{
return count_;
}

private:
long count_;
};

template <typename T>
class smart_ptr {
public:
template <typename U>
friend class smart_ptr;

explicit smart_ptr(T* ptr = nullptr)
: ptr_(ptr)
{
if (ptr) {
shared_count_ =
new shared_count();
}
}
~smart_ptr()
{
if (ptr_ &&
!shared_count_
->reduce_count()) {
delete ptr_;
delete shared_count_;
}
}

smart_ptr(const smart_ptr& other)
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}

template <typename U>
smart_ptr(const smart_ptr<U>& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_->add_count();
shared_count_ = other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& other,
T* ptr) noexcept
{
ptr_ = ptr;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
smart_ptr&
operator=(smart_ptr rhs) noexcept
{
rhs.swap(*this);
return *this;
}

T* get() const noexcept
{
return ptr_;
}
long use_count() const noexcept
{
if (ptr_) {
return shared_count_
->get_count();
} else {
return 0;
}
}
void swap(smart_ptr& rhs) noexcept
{
using std::swap;
swap(ptr_, rhs.ptr_);
swap(shared_count_,
rhs.shared_count_);
}

T& operator*() const noexcept
{
return *ptr_;
}
T* operator->() const noexcept
{
return ptr_;
}
operator bool() const noexcept
{
return ptr_;
}

private:
T* ptr_;
shared_count* shared_count_;
};

template <typename T>
void swap(smart_ptr<T>& lhs,
smart_ptr<T>& rhs) noexcept
{
lhs.swap(rhs);
}

template <typename T, typename U>
smart_ptr<T> static_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = static_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}

template <typename T, typename U>
smart_ptr<T> reinterpret_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = reinterpret_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}

template <typename T, typename U>
smart_ptr<T> const_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = const_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}

template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = dynamic_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}