cpp智能指针详解

cpp是一种没有垃圾回收的语言,给了程序员十足的控制感,同时也为内存泄漏问题留下了重大的隐患。Modern Cpp不推荐程序员完全用new和delete手动管理内存,而是应该使用智能指针。这一节我们就来梳理下智能指针的特性与应用场景。

auto_ptr

我们从std::auto_ptr说起。在C++ 98/03中,C++引入了std::auto_ptr帮助管理内存。随着cpp新标准的出现, std::auto_ptr已被彻底废弃了。被废弃的原因是因为auto_ptr在operator=赋值和复制构造过程中具有的控制权自动转移特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <memory>

using namespace std;

int main(){
auto_ptr<int> p1(new int(1));

auot_ptr<int> p2;
p2.reset(new int(2));

auto_ptr<int> p3 = p1 // p1将变成Null, 控制权转移到了p3

auto_ptr<int> p4(p2) // p2将变成Null, 控制权转移到了p4
}

如上,将p1赋值给p3将导致p1将所持有的堆内存转移给p3,之后p1的原始指针将等于Null。通过p2来构造p4也是相同结果。不完善的控制权转移特性被认为是auto_ptr的重大设计缺陷,使得其不被社区所接受。这种默认的控制权转移机制在Rust语言中得到了很好的发展,Rust语言提供了完善的机制来管理变量的控制权和生命周期。

unique_ptr

std::unique_ptrstd::auto_ptr的替代者。unique_ptr对自己持有的堆内存具有唯一的控制权,其禁止复制语义。在unique_ptr的实现中,其operator=运算符和复制构造函数被标记为了delete,如下:

1
2
3
4
5
6
7
template<typename T>
class unique_ptr {
...
...
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
}

这使得将一个unique_ptr对象赋值给另一个unique_ptr,或者用一个unique_ptr初始化另一个unique_ptr对象的代码无法通过编译。

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <memory>
using namespace std;

int main(){
unique_ptr<int> p1 = make_unique<int>(1);
unique_ptr<int> p2 = p1; // 无法通过编译
unique_ptr<int> p3(p1); //无法通过编译
}

这里有一个例外,当从一个函数返回一个unique_ptr时,是可以的:

1
2
3
4
5
6
7
8
9
10
unique_ptr<int> func_return_unique(){
auto p = make_unique<int>(1);
return p;
}


int main(){
auto up=func_return_unique();
printf("%d\n",*up);
}

既然禁止了复制语义,那这里又是怎样实现的呢?答案是移动。unique_ptr实现了移动赋值运算符和移动构造函数

1
2
unique_ptr(const unique_ptr&&)
unique_ptr& operator=(const unique_ptr&&)

这使得unique_ptr可以通过std::move()或者在函数中返回来转移其控制权。

1
2
3
4
5
6
7
8
#include <memory>
#include <iostream>
using namespace std;
int main(){
unique_ptr<int> p1 = make_unique<int>(3);
uinque_ptr<int> p2 = std::move(p1);
unique_ptr<int> p3(std::move(p2));
}

当讲p1 move给p2后,p1就变成了一个空的智能指针对象。注意并不是所有对象的std::move操作都有意义,一定是实现了移动构造函数和移动赋值运算符的对象才行。

shared_ptr

unique_ptr独享其所占有的堆内存,而shared_ptr则允许其所占有的堆内存被多个变量共享,其使用了引用计数,每当shared_ptr被赋值,构造,从函数返回等,其引用计数加一;每一个指向其共享资源的shared_ptr析构时,其引用计数减一;当最后一个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
#include <memory>
#include <iostream>
using namespace std;

int main() {
auto sp1 = make_shared<int>(3);
cout<< sp1->use_count()<<endl; // 1

auto sp2 = sp1;
cout <<sp1->use_count()<<endl; // 2

sp2.reset();
cout<<sp1->use_count()<<endl; // 1

{
sp3 = sp1;
cout <<sp1->use_count()<<endl; // 2
}

cout<<sp1->use_count()<<endl; // 1

return 0;
}

为了接着往下介绍weak_ptr,这里我们需要介绍std::enable_shared_from_this模板对象。我们在cpp中经常需要将指向对象本身的指针返回给外部使用,如果是普通指针,直接返回this即可,但如果是shared_ptr呢?把this抢转成shared_ptr? cpp不支持我们进行这种强转,其实我们只需要继承std::enable_shared_from_this模板对象即可,如下:

1
2
3
4
5
6
7
8
class C: public std::enable_shared_from_this<C>{
...
...

shared_ptr<C> self(){
return shared_from_this();
}
}

调用self方法,即可获得C类型对象的shared_ptr指针。

1
2
3
4
5
6
int main(){
shared_ptr<C> c (new C());
shared_ptr<C> sp = c->self();
cout<<sp->use_count()<<endl; // 2
return 0;
}

weak_ptr

1
2
3
4
5
6
7
8
9
10
11
12
class C: public std::enable_shared_from_this<C>{
...
...

public:
void self(){
m = shared_from_this();
}

private:
shared_ptr<C> _m
}

考虑上面的代码,类型C的对象c调用self()成员方法之后,导致成员变量_m与c之间循环引用。这使得shared_ptr的引用计数永远不能为0,从而导致内存泄漏。也就是说一个资源对象的生命周期可以交给一个智能指针对象管理,但是该智能指针不能再交给这个资源对象来管理。

weak_ptr是一个不获取资源生命周期的智能指针,是一种对资源对象的弱引用。它只提供了对其引用资源的访问手段,引入它的目的是协助std::shared_ptr工作。weak_ptr是用来解决上面的循环引用问题。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class C: public std::enable_shared_from_this<C>{
...
...

public:
void self(){
m = shared_from_this();
}

private:
weak_ptr<C> _m
}

weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造,一个weak_ptr可以调用lock()方法来获得shared_ptr.weak_ptr的构造和析构不会引起引用计数的增加或减少。

因为weak_ptr不管理其所引用对象的生命周期,所以其引用的对象在何时被销毁对weak_ptr不可见,所以要访问其所引用的对象,需要先通过其expired()方法检测对象是否被销毁,检测还存在,然后才能通过lock()方法获取shared_ptr对象,常见的访问代码如下:

1
2
3
4
5
if (wp.expried()) return;
auot sp =wp.lock();
if (sp){
// do sth with sp ...
}

weak_ptr没有实现operator->operator*opeator!方法,所以不能直接访问其所引用的资源对象,不能解引用,不能用!符号判断引用的资源是否存在。

使用场景

auto_ptr已经被彻底废弃,无论何种情况都不应使用.如果你的对象不需要被共享应该优先使用unique_ptr,如果需要共享则使用shared_ptr,如果不需要管理资源的生命周期则使用unique_ptr.