RAII

了解智能指针前,我们先了解下C++编程中常用的RAII的思想

RAII全称为Resource Acquisition is Initialization,直译就是资源获取即初始化。

它的思想是将必须在使用前获取的资源(堆内存,线程,文件,互斥锁等)与一个对象的声明周期绑定,常用做法是在构造函数中请求资源,在析构函数中释放资源

以上由C++的语言机制保证,当对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数

智能指针就是使用RAII思想的例子

智能指针概述

智能指针本质是模板类独享,其行为类似指针。其最主要的功能是帮助管理动态内存,防止发生内存泄漏

C++提供了四种智能指针

  • auto_ptr(C++11废弃,C++17移除)
  • unique_ptr(C++11):独占智能指针
  • shared_ptr(C++11):引用计数智能指针
  • weak_ptr(C++11):弱引用智能指针,解决shared_ptr的循环引用,不能访问直接访问对象
1
2
3
4
{
auto_ptr<double> p(new double(10.0));
// p析构时自动释放内存
}

auto_ptr是C++98提供的,现已废弃,应使用unique_ptr或shared_ptr替代

考虑如下情况,将p赋值给p2会发生什么?

按照常规指针的做法,p2和p会指向同一个对象,但是如果这样,p2和p在析构时会释放两次内存,这当然是不可行的

1
2
3
4
5
{
auto_ptr<double> p(new double(10.0));
auto_ptr<double> p2;
p2 = p; // 会发生什么
}

针对以上问题,有如下解决方案

  • 执行深拷贝,但这会造成两个指针指向不同对象
  • 建立所有权(ownership)的概念,仅可以让一个智能指针拥有该对象,该智能指针析构时执行内存释放操作。赋值操作将所有权转移。
    • auto_ptr和unique_ptr都采用此策略,unique_ptr的策略更加严格
  • 使用引用计数,记录有多少个智能指针指向该对象,当引用计数变为0时,执行内存释放操作
    • shared_ptr采用此策略

auto_ptr使用

  • 不要将同一个原生指针赋值给多个智能指针
  • auto_ptr对象的复制构造和赋值会转移所有权
  • 作为参数不要按值传递
  • auto_ptr不支持数组
  • 无法作为STL容器的元素,auto_ptr的复制构造和赋值要求参数左值引用
  • 仅支持new分配的内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto_ptr<double> p(new double(10.0));
auto_ptr<double> p2;
p2 = p; // p的所有权被转移给p2,p不再有效
*p = 10.0; // bad

// 按值传递,调用时会将所有权转移给临时对象p3,函数结束时将内存释放
// 这里应采用按引用传递
void test(auto_ptr<double> p3) {
printf("%.2f\n", *p3);
}
test(p2);
*p2 = 10.0; // bad

auto_ptr<double[]> pa(new double[100]()); // bad

auto_ptr实现

  • auto_ptr的复制构造函数和赋值运算符要求左值引用
  • 需要考虑右值的构造和赋值,通过AutoPtrRef的方式实现
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
template <typename T>
struct AutoPtrRef {
explicit AutoPtrRef(T* p) : ptr(p) {}
T* ptr;
};

template <typename T>
class AutoPtr {
public:
using element_type = T;

explicit AutoPtr(element_type* data = nullptr) : data_(data) {}
AutoPtr(AutoPtr& other) : data_(other.release()) {}
// 为了实现右值的初始化
AutoPtr(AutoPtrRef<element_type> ref) : data_(ref.ptr) {}
template <typename Y>
AutoPtr(AutoPtr<Y>& other) : data_(other.release()) {}
~AutoPtr() { Destroy(); }

AutoPtr& operator=(AutoPtr& other) {
printf("calling AutoPtr::operator=(AutoPtr& other)\n");
reset(other.release());
return *this;
}
template <typename Y>
AutoPtr& operator=(AutoPtr<Y>& other) {
printf("calling AutoPtr::operator=(AutoPtr<Y>& other)\n");
reset(other.release());
return *this;
}
template <typename Y>
AutoPtr& operator=(AutoPtrRef<Y> other) {
printf("calling AutoPtr::operator=(AutoPtrRef<Y> other)\n");
reset(other.ptr);
return *this;
}

element_type* get() const { return data_; }
element_type* operator->() const { return get(); }
element_type& operator*() const { return *get(); }

template <typename Y>
operator AutoPtrRef<Y>() {
return AutoPtrRef<Y>(release());
}
template <typename Y>
operator AutoPtr<Y>() {
return AutoPtr<Y>(release());
}

void reset(element_type* data = nullptr) {
if (data != data_) {
Destroy();
data_ = data;
}
}
element_type* release() {
element_type* ret = data_;
data_ = nullptr;
return ret;
}

private:
void Destroy() {
if (data_) {
delete data_;
data_ = nullptr;
}
}
element_type* data_;
};

unique_ptr使用

相比于auto_ptr,unique_ptr有如下优点

  • 不要将同一个原生指针赋值给多个智能指针
  • 禁用拷贝构造函数和赋值运算符
  • 使用移动语义std::move转移所有权
  • 使用移动语义可以作为STL容器的元素,但某些需要拷贝和赋值的算法操作会受限
  • 支持数组,并提供operator[],使用delete[]释放内存
  • 支持自定义deleter
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
{
std::string name = "student1";
double score = 100.0;
gtl::unique_ptr<Student> p(gtl::make_unique<Student>(name, score));
auto* p_bak = p.get();
EXPECT_NE(p.get(), nullptr);
EXPECT_EQ(name, p->name());
EXPECT_EQ(score, p->score());
gtl::unique_ptr<Person> p2(std::move(p));
EXPECT_EQ(p.get(), nullptr);
EXPECT_EQ(p2.get(), p_bak);
EXPECT_EQ(name, p2->name());
p2 = std::move(p2);
EXPECT_EQ(p2.get(), p_bak);
EXPECT_EQ(name, p2->name());

gtl::unique_ptr<Person> p3;
p3 = std::move(p2);
}
{
// 支持数组指针即使用new[]/delete[]
int n = 10;
gtl::unique_ptr<Person[]> p = gtl::make_unique<Person[]>(n);
Person* p_bak = p.get();
EXPECT_NE(p.get(), nullptr);
EXPECT_EQ(bool(p), true);
p = std::move(p);
EXPECT_NE(p.get(), nullptr);
EXPECT_EQ(bool(p), true);

for (int i = 0; i < n; ++i) {
p[i].set_name(std::to_string(i));
EXPECT_EQ(std::to_string(i), p[i].name());
}
}

unique_ptr实现

  • unique_ptr存在针对对象和数组的两个版本
    • 分别使用new/delete和new[]/delete[]
  • 需要支持自定义deleter
  • 禁用拷贝构造函数和赋值运算符
  • 定义移动构造函数和移动复制运算符
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
template <typename T, typename Deleter = std::default_delete<T>>
class UniquePtr {
public:
using pointer = T*;
using element_type = T;
using deleter_type = Deleter;

explicit UniquePtr(pointer data = nullptr, deleter_type deleter = deleter_type()) : data_(data), deleter_(deleter) {}
UniquePtr(const UniquePtr& other) = delete;
UniquePtr(UniquePtr&& other) : data_(other.release()), deleter_(std::forward<deleter_type>(other.deleter_)) {}
template <typename Y>
UniquePtr(UniquePtr<Y>&& other) : data_(other.release()), deleter_(std::forward<deleter_type>(other.get_deleter())) {}
~UniquePtr() { Destroy(); }

UniquePtr& operator=(const UniquePtr& other) = delete;
UniquePtr& operator=(UniquePtr&& other) {
reset(other.release());
deleter_ = std::forward<deleter_type>(other.deleter_);
return *this;
}
template <typename Y>
UniquePtr& operator=(UniquePtr<Y>&& other) {
reset(other.release());
deleter_ = std::forward<deleter_type>(other.deleter_);
return *this;
}

pointer operator->() const { return get(); }
element_type& operator*() const { return *get(); }

explicit operator bool() const { return data_ != nullptr; }
deleter_type& get_deleter() { return deleter_; }
const deleter_type& get_deleter() const { return deleter_; }

pointer get() const { return data_; }
void reset(pointer data = nullptr) {
if (data != data_) {
Destroy();
data_ = data;
}
}
pointer release() {
pointer ret = data_;
data_ = nullptr;
return ret;
}

void swap(UniquePtr& other) {
std::swap(data_, other.data_);
std::swap(deleter_, other.deleter_);
}

private:
void Destroy() {
if (data_) {
deleter_(data_);
data_ = nullptr;
}
}
pointer data_;
deleter_type deleter_;
};

template <typename T, typename Deleter>
class UniquePtr<T[], Deleter> {
public:
using pointer = T*;
using element_type = T;
using deleter_type = Deleter;

explicit UniquePtr(pointer data = nullptr) : data_(data) {}
UniquePtr(const UniquePtr& other) = delete;
UniquePtr(UniquePtr&& other) : data_(other.release()), deleter_(std::forward<deleter_type>(other.deleter_)) {}
~UniquePtr() { Destroy(); }

UniquePtr& operator=(const UniquePtr& other) = delete;
UniquePtr& operator=(UniquePtr&& other) {
reset(other.release());
deleter_ = std::forward<deleter_type>(other.deleter_);
return *this;
}
template <typename Y>
UniquePtr& operator=(UniquePtr<Y>&& other) {
reset(other.release());
deleter_ = std::forward<deleter_type>(other.deleter_);
return *this;
}

element_type& operator[](std::size_t index) const { return data_[index]; }

explicit operator bool() const { return data_ != nullptr; }
deleter_type& get_deleter() { return deleter_; }
const deleter_type& get_deleter() const { return deleter_; }

pointer get() const { return data_; }
void reset(pointer data = nullptr) {
Destroy();
data_ = data;
}
pointer release() {
pointer ret = data_;
data_ = nullptr;
return ret;
}

void swap(UniquePtr& other) {
std::swap(data_, other.data_);
std::swap(deleter_, other.deleter_);
}

private:
void Destroy() {
if (data_) {
deleter_(data_);
data_ = nullptr;
}
}
pointer data_;
deleter_type deleter_;
};

shared_ptr和weak_ptr使用

  • 不要将同一个原生指针赋值给多个智能指针
  • shared_ptr是共享所有权的智能指针,使用上更像普通指针
  • 支持数组(C++17)
  • 注意循环引用——将任一shared_ptr替换为weak_ptr打破循环即可
  • weak_ptr无法访问其所管理的资源,其是为了解决循环引用的问题,weak_ptr不会增加引用计数
    • weak_ptr可以通过expired函数判断对应的资源是否已经释放
    • 访问所管理的对象必须通过lock函数返回一个shared_ptr对象(注意判断返回值是否为空,可能其所对应的资源已经释放)
  • 线程安全问题
    • 多个线程在不同shared_ptr的实例上调用所有成员函数(包含复制构造和赋值)是安全的,即使他们共享同一个对象的所有权
    • 多个线程只读方式访问同一个shared_ptr的实例是安全的
    • 多个线程访问同一个shared_ptr的实例的非const成员函数是不安全的,会出现数据竞争
    • 所拥有资源的线程安全由资源本身决定

循环引用的例子

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
class Student : public Person {
public:
// ...

private:
double score_;
SharedPtr<Teacher> teacher_;
};

class Teacher : public Person {
public:
// ...

private:
int id_;
SharedPtr<Student> student_;
WeakPtr<Student> student_weak_;
};

{
// teacher包含student,student包含teacher,会造成循环引用
// 析构时引用数都不会变为0,造成内存泄漏
gtl::shared_ptr<Teacher> teacher(gtl::make_shared<Teacher>("teacher", 10));
gtl::shared_ptr<Student> student(gtl::make_shared<Student>("student", 100.0));
printf("%s count %zu\n", teacher->name().c_str(), teacher.use_count());
printf("%s count %zu\n", student->name().c_str(), student.use_count());
EXPECT_EQ(teacher.use_count(), 1);
EXPECT_EQ(student.use_count(), 1);
teacher->set_student(student);
student->set_teacher(teacher);
printf("%s count %zu\n", teacher->name().c_str(), teacher.use_count());
printf("%s count %zu\n", student->name().c_str(), student.use_count());
EXPECT_EQ(teacher.use_count(), 2);
EXPECT_EQ(student.use_count(), 2);
}

{
gtl::WeakPtr<Student> student_weak_bak;
{
gtl::shared_ptr<Teacher> teacher_weak(gtl::make_shared<Teacher>("teacher_weak", 10));
gtl::shared_ptr<Student> student_weak(gtl::make_shared<Student>("student_weak", 100.0));
printf("%s count %zu\n", teacher_weak->name().c_str(), teacher_weak.use_count());
printf("%s count %zu\n", student_weak->name().c_str(), student_weak.use_count());
EXPECT_EQ(teacher_weak.use_count(), 1);
EXPECT_EQ(student_weak.use_count(), 1);
teacher_weak->set_student_weak(student_weak);
student_weak->set_teacher(teacher_weak);
printf("%s count %zu\n", teacher_weak->name().c_str(), teacher_weak.use_count());
printf("%s count %zu\n", student_weak->name().c_str(), student_weak.use_count());
EXPECT_EQ(teacher_weak.use_count(), 2);
EXPECT_EQ(student_weak.use_count(), 1);
student_weak_bak = student_weak;
EXPECT_EQ(student_weak_bak.use_count(), 1);
EXPECT_EQ(student_weak_bak.expired(), false);
EXPECT_EQ(bool(student_weak_bak.lock()), true);
printf("student_weak ptr %p\n", student_weak.get());
}
printf("weak test\n");
EXPECT_EQ(student_weak_bak.use_count(), 0);
EXPECT_EQ(student_weak_bak.expired(), true);
auto stucent_shared_ptr = student_weak_bak.lock();
printf("student_weak ptr %p\n", stucent_shared_ptr.get());
EXPECT_EQ(bool(stucent_shared_ptr), false);
}

shared_ptr和weak_ptr实现

  • shared_ptr包含指向对象的指针和引用计数的指针
    • 包含对象指针是为了不同的对象共享所有权
    • 包含引用计数的指针而不是对象,是为了共享所有权的shared_ptr得到的引用计数是相同的
  • 引用计数包含use_count和weak_count
    • 由于weak_ptr需要在所有的shared_ptr都释放时也能够判断引用计数的数量是否为0,因此shared_ptr释放时,如果weak_count>1,则仅可以释放所管理对象的资源,引用计数的资源不能释放
    • 当weak_ptr析构时,需要判断是否需要释放引用计数的资源
  • shared_ptr的use_count和weak_count如何变化
    • 以空指针构造,两个指针都为空
    • 以非空指针构造,use_count和weak_count都设置为1
    • 复制构造,若非空则use_count加1
    • 移动构造use_count不变,资源转移到新构造的shared_ptr,被移动的shared_ptr变为空
    • 赋值操作,左侧操作数use_count减1,右侧操作数use_count加1,并且赋值给左操作数,左右侧操作数指向同一对象
    • 移动赋值,左侧操作数use_count减1,右侧操作数use_count不变,并且赋值给左操作数,右侧操作数变为空
    • shared_ptr析构,use_count减1
    • use_count为0时,释放所管理对象的资源,weak_count减1(为0时释放引用计数的资源)
  • weak_ptr的use_count和weak_count如何变化
    • 默认构造,weak_ptr不管理任何对象
    • weak_ptr不接受普通指针构造
    • 复制构造,若非空则weak_count加1
    • 以shared_ptr构造,若非空则weak_count加1
    • 移动构造,weak_count不变,被移动weak_ptr变为空
    • 赋值操作,左侧操作数weak_count减1,右侧操作数weak_count加1,并且赋值给左操作数,左右侧操作数指向同一对象
    • 移动赋值,左侧操作数weak_count减1,右侧操作数weak_count不变,并且赋值给左操作数,右侧操作数变为空
    • weak_ptr析构,weak_count减1
    • weak_count为0时,释放引用计数的资源
    • 调用lock函数构造shared_ptr,use_count加1

具体实现可参考smart_pointers.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
struct RefCount {
// ...
std::atomic<std::size_t> use_count; // 有多少个shared_ptr指向这个对象
std::atomic<std::size_t> weak_count; // 有多少个weak_ptr指向这个对象
};

class SharedPtr {
public:
// ...

private:
RefCount* ref_count_;
T* data_;
}

参考