C++之智能指针

智能指针是C++11引进的新特性,使用智能指针可以弥补野指针、内存泄露、二次释放等问题,这部分主要讨论C++11引进的智能指针的用法。


智能指针

智能指针可以通过下面三个层次来理解:
1、从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
2、智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
3、智能指针还有一个作用是把值语义转换成引用语义。
在C++98标准中,引进了auto_ptr类模板,是最早出现的智能指针。但auto_ptr也很多缺陷,从C++11之后,引进了shared_ptr、unique_ptr以及weak_ptr,包含在头文件中。

类别 功能
auto_ptr C++早期的智能指针,不能共享对象,不能用于指向数组,存在浅拷贝的问题
shared_ptr 可共享指针对象,可以赋值给shared_ptr或weak_ptr,指针所指对象在所有相关联的shared_ptr生命周期结束时结束,是强引用
unique_ptr 独占指针对象,并保证指针所指对象生命周期一致
weak_ptr 它不能决定对象的生命周期,引用所指对象时,需要lock()成shared_ptr才能使用

shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化:智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,其构造函数是explicit。例如std::shared_ptr p4 = new int(1);的写法是错误的
  • 拷贝和赋值:拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • 获取指针:使用get函数
  • 注意不要用一个原始指针初始化多个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
    #include <iostream>
    #include <memory>
    using namespace std;
    int main() {
    int a = 10;
    shared_ptr<int> pinta = make_shared<int>(a);
    shared_ptr<int> pinta2(pinta);
    int b = 20;
    shared_ptr<int> pintb = make_shared<int>(b);
    cout << pinta.use_count() << endl;
    cout << pintb.use_count() << endl;
    pinta2 = pintb;
    int *pa = pinta.get(); // 获取原始指针
    cout << pinta.use_count() << endl;
    cout << pintb.use_count() << endl;
    return 0;
    }

unique_str

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。
unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> uptr(new int(10));
// unique_ptr<int> uptr2 = uptr; // 不能赋值
// unique_ptr<int> uptr2(uptr2); // 不能拷贝
unique_ptr<int> uptr2 = move(uptr);
cout << uptr.get() << endl;
cout << uptr2.get() << endl;
cout << *uptr2 << endl;
uptr2.release();
return 0;
}


weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> sptr = make_shared<int>(10);
cout << sptr.use_count() << endl;
weak_ptr<int> wptr(sptr);
cout << wptr.use_count() << endl;
if (!wptr.expired()) {
shared_ptr<int> sptr2 = wptr.lock();
*sptr2 = 100;
cout << sptr2.use_count() << endl;
}
return 0;
}


循环引用问题

考虑一个简单的对象模型 - 家长与子女:Parent has Child,Child knows his/her Parent

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
#include <iostream>
#include <memory>
using namespace std;
class Parent;
class Child;
class Parent {
public:
Parent() {}
void setChild(Child *child) {
if (this->m_child != NULL) {
delete m_child;
}
m_child = child;
}
~Parent() {
if (this->m_child != NULL) {
delete m_child;
m_child = NULL;
}
}
private:
Child *m_child;
};
class Child {
public:
Child() {}
void setParent(Parent *parent) {
if (this->m_parent != NULL) {
delete m_parent;
}
m_parent = parent;
}
~Child() {
if (this->m_parent != NULL) {
delete m_parent;
m_parent = NULL;
}
}
private:
Parent *m_parent;
};
int main() {
Parent *p = new Parent;
Child *c = new Child;
p->setChild(c);
c->setParent(p);
delete p; // only delete one
return 0;
}

如果用原始指针会出现野指针问题,c指针指向的内存已经释放,如果delete c就会出现二次释放,不释放就会出现野指针问题。
使用引用对象:

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
#include <iostream>
#include <memory>
using namespace std;
class Parent;
class Child;
class Parent {
public:
Parent() {}
void setChild(shared_ptr<Child> child) {
m_child = child;
}
~Parent() {}
private:
weak_ptr<Child> m_child;
};
class Child {
public:
Child() {}
void setParent(shared_ptr<Parent> parent) {
m_parent = parent;
}
~Child() {}
private:
shared_ptr<Parent> m_parent;
};
int main() {
weak_ptr<Parent> wp;
weak_ptr<Child> wc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setParent(p);
wp = p;
wc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 1
}
cout << wp.use_count() << endl; // 0
cout << wc.use_count() << endl; // 0
return 0;
}


参考资料:
C++智能指针梳理
C++11中智能指针的原理、使用、实现
C++智能指针auto_ptr详解
c++11智能指针解析——揭开底层面纱,完整理解智能指针