智能指针

总体介绍

智能指针是一个类似指针的类,提供了内存管理的功能,当指针不再被使用时,它指向的内存会自动被释放,这就比原生指针要好,原生指针有可能会因为忘记释放所申请的空间,而造成内存泄漏,而用智能指针就没这个顾虑。C++11支持shared_ptr, weak_ptr, unique_ptr,auto_ptr(被弃用)。这些智能指针位于<memory>

  • auto_ptr采取所有权模式,可以被拷贝(构造or赋值)时,原auto_ptr指为nullptr,即auto_ptr被其他auto_ptr剥夺(转移),所以很容易引起内存泄露(粗心的程序员可能仍然会解引用原auto_ptr)
  • unique_ptr是独占式拥有,解决了auto_ptr被剥夺的问题,unique_ptr禁止了拷贝(构造or赋值),保证同一时间内只有一个智能指针可以指向该对象,如果真的需要转移,可以使用借助move实现移动构造,原unique_ptr置为nullptr(但粗心的程序员可能仍然会解引用原来的unique_ptr)
  • shared_ptr是共享,允许拷贝(构造or赋值),允许多个智能指针可以指向相同对象,每当,每次有一个shared_ptr关联到某个对象上时(拷贝构造or拷贝赋值),计数值就加上1;相反,每次有一个shared_ptr析构时,相应的计数值就减去1。当计数值减为0的时候,就执行对象的析构函数,此时该对象才真正被析构!如果用了移动(构造or赋值),那么原shared_ptr为空,并且指向对象的引用计数不会改变(相当于-1+1=0)
  • weak_ptr是一种弱引用,指向shared_ptr(强引用)所管理的对象,可从一个shared_ptr或另一个weak_ptr来构造,它的构造和析构不会引起引用计数的增加或减少。weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。weak_ptr提供了expired()与lock()成员函数,前者用于判断weak_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
24
25
26
27
28
29
30
{
auto_ptr<string> ps1(new string("Hello, auto_ptr!"));
auto_ptr<string> ps2;
ps2 = ps1;
//【E1】下面这行注释掉才可正确运行,因为ps1被ps2剥夺,ps1此时指向null,解引用当然会报错
//cout << "ps1: " << *ps1 << endl;
cout << "ps2: " << *ps2 << endl;
}

{
unique_ptr<string> ps1(new string("Hello, unique_ptr!"));
// unique_ptr<string> ps2(ps1);// 编译将会出错!因为禁止拷贝构造
// unique_ptr<string> ps2 = ps1;// 编译将会出错!因为禁止拷贝赋值
unique_ptr<string> ps2 = move(ps1); //编译通过,ps1被转移了,此时ps1指向null
}

{
shared_ptr<string> ps1(new string("Hello, shared_ptr!"));
shared_ptr<string> ps3(ps1); // 允许拷贝构造
shared_ptr<string> ps2 = ps1; // 允许拷贝赋值
cout << "Count is: " << ps1.use_count() << ", " << ps2.use_count() << ", " << ps3.use_count() << endl; // Count is: 3, 3, 3
cout << "ps1 is: " << *ps1 << ", ptr value is: " << ps1.get() << endl;
cout << "ps2 is: " << *ps2 << ", ptr value is: " << ps2.get() << endl;
cout << "ps3 is: " << *ps3 << ", ptr value is: " << ps3.get() << endl;

shared_ptr<string> ps4 = move(ps1); // 注意ps1在move之后,就“失效”了
cout << "Count is: " << ps1.use_count() << ", " << ps2.use_count() << ", " << ps3.use_count() << ", " << ps4.use_count() << endl; // Count is: 0, 3, 3, 3
cout << "ps1 is: " << ps1.get() << endl;
cout << "ps4 is: " << *ps4 << ", ptr value is: " << ps4.get() << endl;
}

shared_ptr与new

  • 接受指针参数的智能指针构造函数是explicit的,因为不能将内置指针隐式转换成智能指针,必须使用直接初始化形式
  • reset方法以改变shared_ptr
1
2
3
4
5
6
7
shared_ptr<int> p1 = new int(1024); // 错误,必须使用直接初始化
shared_ptr<int> p2(new int(1024)); // 正确,使用直接初始化形式

// 若p是唯一指向其对象的shared_ptr,reset会释放对象,若传递了可选参数内置指针q,则令p指向q,若传递了d,则会调用d而不是delete来释放q
p.reset()
p.reset(q)
p.reset(q, d)
  • 不要混合使用智能指针和普通指针
  • 若发生异常,普通指针不会自动销毁对象,但智能指针可以
  • 如果用智能指针管理的资源不是new分配的内存,记得传递一个删除器

unique_ptr

  • 一般使用release切断unique_ptr与它原来所管理的对象间的联系
  • unique_ptr在函数返回时会执行一种特殊的拷贝
1
2
3
4
5
6
7
8
9
u.release() // 放弃对指针的控制权,返回指针,并将u置空
u.reset() // 释放u指向的对象
u.reset() // 如果提供内置指针q,令u指向这个对象;否则将u置空

unique_ptr<string> p2(p1.release()); // 所有权转移,p1置空
p2.reset(p3.release()) // reset释放了p2原来指向的内存

p2.release() // 错误,p2不会释放内存,而且丢失了指针
auto p = p2.release // 正确,但记得delete(p)

最佳实践

  • 这个对象在对象或方法内部使用时优先使用unique_ptr

  • 这个对象需要被多个 Class 同时使用的时候优先使用shared_ptr

  • 当出现循环引用的时候,用weak_ptr代替一个类中对其他类的shared_ptr引用

错误用法

  • 使用智能指针托管的对象,尽量不要再使用原生指针(容易造成二次释放)

  • 不要把一个原生指针交给多个智能指针管理(会导致多次销毁)

  • 尽量不要使用 get()获取原生指针

  • 不要将 this 指针直接托管智能指针(造成二次释放)

  • 智能指针只能管理堆对象,不能管理栈上对象(造成二次释放)

make_shared的优缺点

1
2
3
auto p = new widget();
shared_ptr sp1{ p }, sp2{ sp1 }; // 分配两次内存,异常不安全
auto sp1 = make_shared<widge>(), sp2{ sp1 }; // 分配一次内存,异常安全

缺点:

  • 构造函数是保护或私有时,无法使用make_shared
  • 对象的内存可能无法及时回收,用shared_ptr时,当强引用为0自动回收,用make_shared时,当强引用与弱引用都为0才自动回收

智能指针也会发生内存泄漏吗;如果是,有什么手段避免

两个shared_ptr相互引用时会发生循环引用(“你中有我,我中有你”),使引用计数失效,从而导致内存泄露

weak_ptr弱指针可以解决这个问题,weak_ptr的构造和析构不会影响引用计数,它指向shared_ptr所管理的对象,也可以检测到所管理的对象是否已经被释放,从而避免非法访问。

weak_ptr也可以调用lock()函数,如果管理对象没有被释放,则提升为shared_ptr,如果管理对象已经释放,调用lock()函数也不会有异常

shared_ptr循环引用导致内存泄漏

考虑下面的例子,A类与B类都有一个指向对方的shared_ptr成员,创建a_obj与b_obj,首先把if语句块注释掉,先测试它们不循环引用的情况

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
#include <iostream>
#include <memory>
class B;
class A
{
public:
// weak_ptr<B> pb;
shared_ptr<B> pb;
~A()
{
cout << "kill A\n";
}
};

class B
{
public:
// weak_ptr<A> pa;
shared_ptr<A> pa;
~B()
{
cout <<"kill B\n";
}
};
int main()
{
A* a_obj = new A();
B* b_obj = new B();
shared_ptr<A> sa(a_obj);
shared_ptr<B> sb(b_obj);
cout<<"sa use count:"<<sa.use_count()<<endl;
// if(sa && sb)
// {
// ;
// }
cout<<"sa use count:"<<sa.use_count()<<endl;

没有循环引用,通过下面的输出,可以看到a_obj与b_obj都已经正确地调用了析构函数。

1
2
3
4
sa use count:1
sa use count:1
kill B
kill A

为了探究use count的返回情况,修改if语句:

1
2
3
4
5
if(sa && sb)
{
shared_ptr<A> tempsa(sa);
cout<<"sa use count:"<<sa.use_count()<<endl;
}

输出如下,证实了局部引用在退出作用域时取消引用,use count会减1。

1
2
3
4
5
sa use count:1
sa use count:2
sa use count:1
kill B
kill A

接下来修改if语句,探究a_obj与b_obj的shared_ptr互相引用对方时的场景

1
2
3
4
5
6
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
cout<<"sa use count:"<<sa.use_count()<<endl;
}

输出如下,结束if语句块时,use count没有-1,证明了引用计数失效;程序结束时也没有kill B、kill A的输出,说明a_obj与b_obj没有正确析构

1
2
3
sa use count:1
sa use count:2
sa use count:2

总结:循环引用导致引用计数失效,最后导致无法正确析构,造成内存泄漏

weak_ptr解决循环引用

只需要把class A与class B中的成员类型改为weak_ptr<B>weak_ptr<A>即可,输出如下,引用计数正常工作,两个对象也正确析构了

1
2
3
4
5
sa use count:1
sa use count:1
sa use count:1
kill B
kill A

手动实现引用计数型智能指针

这里简单地把引用计数设为int*,其实也可以构造一个类来代替int,在类中会有一个int成员变量

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
#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
SmartPtr(T *p) {
ptr = p;
use_count = new int(1);
}
~SmartPtr(){
// 只在最后一个对象引用ptr时才释放内存
if (--(*use_count) == 0)
{
delete ptr;
delete use_count;
ptr = nullptr; // delete指针后要让指针置空,不然就成了空悬指针/野指针
use_count = nullptr;
}
}
SmartPtr(const SmartPtr<T> &orig){ // 浅拷贝
ptr = orig.ptr;
use_count = orig.use_count; // 浅拷贝,指向同一块内存(同一个int型变量)
++(*use_count);
}
SmartPtr<T>& operator=(const SmartPtr<T> &rhs){ // 浅拷贝
// 必须先递增rhs的引用计数,为了防止自赋值时把自己给释放掉了
++(*rhs.use_count);
// 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象
if (--(*use_count) == 0)
{
delete ptr;
delete use_count;
}
ptr = rhs.ptr;
use_count = rhs.use_count;
return *this;
}
T& operator*(const SmartPtr<T> &rhs){ // 重载解引用操作符,注意返回引用,因为返回左值可修改
return *ptr;
}
private:
T *ptr; // 原始指针
int *use_count; // 为了方便对其的递增或递减操作
};