技术: C++ 智能指针

集中说说Boost和标准库中的智能指针

智能指针的引入并没有完全解决资源自动回收的问题, 或者你不能做刷手掌柜, 但是也很大程度上简化了相关问题的处理.本文集中讲讲Boost或者标准库中的几种智能指针, 以 用好 为宗旨.
期间可能会遇到 Noncopyable 的智能指针 装入容器 问题(比如 unique_ptr ).

引子

你平时用么? 看项目要求.

C++(包括C语言中已存的一些关于内存的问题):

  • 空指针异常
  • 越界访问
  • 野指针
  • 内存泄露

本质上都是因为编程能力造成,但是如果有自动回收,智能管理之类的(就像Java的垃圾回收机制)策略是不是更方便呢?

Boost给出了相关的方案,智能指针smart pointers.
(当然STL野给出了auto_ptr,但是受到了限制比较多,而且存在一些问题,例如不支持拷贝构造函数以及赋值等,一般不推荐使用了)

后来C++11也从Boost引入了3个智能指针:(本质上就是Boost的那一套)

  • unique_ptr
    smart pointer with unique object ownership semantics
  • shared_ptr
    smart pointer with shared object ownership semantics
  • weak_ptr
    weak reference to an object managed by std::shared_ptr

现在用起来更加方便了.

正文

Boost智能指针

官方文档时这么说的:

Smart pointers are objects which store pointers to dynamically allocated (heap) objects. They behave much like built-in C++ pointers except that they automatically delete the object pointed to at the appropriate time.
(到底是什么时候回收呢? appropriate time)

Smart pointers are particularly useful in the face of exceptions as they ensure proper destruction of dynamically allocated objects. They can also be used to keep track of dynamically allocated objects shared by multiple owners.

1
2
#include <boost/smart_ptr.hpp>
using namespace boost;

大致上给了6种:

  1. scoped_ptr Simple sole ownership of single objects. Noncopyable.
  2. scoped_array Simple sole ownership of arrays. Noncopyable.
  3. shared_ptr Object ownership shared among multiple pointers.
  4. shared_array Array ownership shared among multiple pointers.
  5. weak_ptr Non-owning observers of an object owned by shared_ptr.
  6. intrusive_ptr Shared ownership of objects with an embedded reference count.

scope_ptr, Noncopyable也就是说不可以拷贝和赋值; 相比而言 shared_ptr则要好很多.
(array和普通指针的区别在于负责new还是new [])

Additionally, the smart pointer library provides efficient factory functions for creating smart pointer objects:
(直接new创建智能指针消耗比较大, 这里可以用一下工厂方法)

1
2
3
make_shared,  allocate_shared for objects	<boost/make_shared.hpp>	Efficient creation of shared_ptr objects.
make_shared, allocate_shared for arrays <boost/make_shared.hpp> Efficient creation of shared_ptr arrays.
make_unique <boost/make_unique.hpp> Creation of unique_ptr objects and arrays.

<boost/make_shared.hpp><boost/make_unique.hpp> , 大致像下面这样的用法:

1
2
shared_ptr<std::string> strp = make_shared<std::string>("make_test");
shared_ptr<std::vector<int> > vecp = make_shared< std::vector<int> >(10, 3);

挨个说一下(重点放在 shared_ptrweak_ptr, 其他不常用的不说)

scoped_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
/* Smart_Ptr.cpp : 定义控制台应用程序的入口点*/

#include <boost/smart_ptr.hpp>
#include <iostream>

class Test
{
public:
Test(int x = 0);
virtual ~Test();

int getX() const;

private:
int x;

};

Test::Test(int x)
{
this->x = x;
std::cout << "constructor called" << std::endl;
}

Test::~Test()
{
std::cout << "deconstructor called" << std::endl;
}

int Test::getX() const
{
return this->x;
}

int main(void)
{
std::cout << "Main Function start" << std::endl;


//basic type: int
boost::scoped_ptr<int> int_ptr(new int(100));
++*int_ptr;
std::cout << *int_ptr << std::endl;


//class type: Test
boost::scoped_ptr<Test> class_ptr(new Test(1));
std::cout << class_ptr->getX() << std::endl;
class_ptr.reset();

std::cout << "Main Function end" << std::endl;

return 0;
}

运行结果如下:

1
2
3
4
5
6
Main Function start
101
constructor called
1
deconstructor called
Main Function end

不用手动调用delete ptr确实不错,但是也有限制:

  • scoped_ptr 只限于作用域内使用(出了定义智能指针的范围,管不了)
  • 指针管理权不可转移,不支持拷贝构造函数与赋值操作

总结一下:

scoped_ptr<T> ptr_t(new T); //创建了一个智能指针维护空间
ptr_t.reset(); //调用delete指针所指向空间
ptr_t->get(); //返回内部管理的指针, 但禁止在get()出来的指针上执行delete

shared_ptr

这个是重点, 可以看做auto_ptr的替代品, 内置引用计数, 支持拷贝和赋值, 支持 *-> 运算
RCSP: reference-counting smart pointer. 引用计数机制. (避免了浅拷贝的危险性, 只有引用计数为0的时候, 才真正去释放资源)

scoped_ptr 的基础上还要注意:

  • 不要循环引用(这个 weak_ptr 出面解决)
  • 不要用于函数的匿名参数(特别注意)–以独立语句将newed对象置入智能指针
  • ptr.use_count() 可以查看当前引用计数
  • ptr.unique() 查看引用是否唯一

例如:

1
2
3
4
void bad() 
{
f( shared_ptr(new int(2)), g() );
}

应该写成:

1
2
3
4
5
void ok() 
{
shared_ptr p(new int(2));
f(p, g());
}

这一点 Effective C++ 中反复强调了: 以独立语句将newed对象置入智能指针, 原因很简单: 在异常发生的同时, shared_ptr可能没有完全构造.
这样的话, 原始对象指针可能将会遗失. (也就是说, 尽可能独立语句完成 shared_ptr 的构造).

大师的书中, 也强调了shared_ptr最好结合Raii手法一起使用, 举了这样的例子:
不用智能指针时, 实现可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
class Lock
{
public:
explicit Lock(Mutex* pm): mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock() {unlock(mutexPtr);}
private:
Mutex *mutexPtr;
};

即用对象管理资源(管理指针其实就是管理背后的资源)

之后使用智能指针(shared_ptr 是允许指定析构行为的函数的, 即指定释放资源的函数)

1
2
3
4
5
6
7
8
9
10
class Lock
{
public:
explicit Lock(Mutex *pm): mutexPtr(pm, unlock) //指定了释放函数
{
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr;
};

使用方式还是一样的:

1
2
3
4
5
Mutex m;

{
Lock ll(&m);
}

所有的指针, 请你都放到shared_ptr里面去吧, 用法和普通指针没有太多区别.

然后结合 pImpl手法, 可以更加简单的处理 move 语义

详细看一下:

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
shared_ptr<int> p0;

shared_ptr<int> p1(new int);
*p1=1;

/*---------------------------------------------------------------------------*/
shared_ptr<int>p2(p1); //p1引用计数+1; p1和p2同时管理一个内部指针
std::cout << "p1 count: " << p1.use_count() << " value: " << *p2 << std::endl;
*p2 = 13;
std::cout << "p1 count: " << p1.use_count() << " value: " << *p2 << std::endl;

p2.reset();
std::cout << "p1 count: " << p1.use_count() << std::endl; //p2解除之后引用计数-1

/*
Main Function start
p1 count: 2 value: 1
p1 count: 2 value: 13
p1 count: 1
Main Function end
*/
/*---------------------------------------------------------------------------*/

// shared_ptr(std::auto_ptr<Y> & r) 从 auto_ptr 获得指针管理权; 同时 auto_ptr 失去管理权
std::auto_ptr<int> p;
shared_ptr<int> p3(p);

// 从另外一个 shared_ptr 或 auto_ptr 获得指针管理权(拷贝构造函数)
shared_ptr<int> p4 = p1;

//shared_ptr<Y>(Y * p, D d) 类似于 shared_ptr<Y>(Y * p)
// 第一个参数是要被管理的指针;
// 第二个删除参数 d 告诉 shared_ptr 在析构时不使用 delete 来操作指针 p, 而使用 d 来操作;
// 把 delete p 换成调用 d(p) 函数对象.

其他的方法,可以利用 <boost/make_shared.hpp> 提供的工厂方法:
(创建效率高)

1
2
3
4
5
6
7
// include <boost/make_shared.hpp>

shared_ptr<std::string> strp = make_shared<std::string>("make_test");
std::cout << *strp << std::endl;

shared_ptr<std::vector<int> > vec_ptr = make_shared< std::vector<int> >(10, 3);
std::cout << vecp_ptr->size() << std::endl;

把智能指针放到容器中:(因为其实现了拷贝构造和赋值操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef std::vector<boost::shared_ptr<int>> ptr_vector;

ptr_vector vector(10);

int i = 0;
for (ptr_vector::iterator it = vector.begin(); it != vector.end(); ++it) {
*it = boost::make_shared<int>(i++);
std::cout << *(*it) << " ";
}
std::cout << std::endl;
std::cout << "Main Function end" << std::endl;

/*result
Main Function start
0 1 2 3 4 5 6 7 8 9
Main Function end
*/

如果想修改其中一个成员, 比如说vector中最后一个成员:

1
2
3
shared_ptr<int> ptest = vector[9];
*ptest = 100;
std::cout << *vector[9] << std::endl;

用于自定义类型时候:

如果一个类, 有成员是需要手动的分配空间的, 例如:

1
2
3
4
5
class Test
{
private:
int *data;
}

此时Test就要手动处理 拷贝构造赋值还有析构; 避免由于浅拷贝导致的重复析构.比如说分配独立的Heap(构造函数重新分配成员空间;赋值函数先删除本对象原来的,再重新分配,或者使用临时对象),即深拷贝来解决问题(说了太多遍,就不写例子了)

然而自从有了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
class ClassInt
{
private:
shared_ptr<int> ptr_data;

public:
ClassInt(int data_param) :ptr_data(new int)
{
init(data_param);
std::cout << "construct" << std::endl;
}


virtual ~ClassInt()
{
std::cout << "destructor, count = " << ptr_data.use_count() << std::endl;
}

int get_data() const
{
return *ptr_data;
}


long ptr_count() const
{
return ptr_data.use_count();
}


void init(int data_param)
{
*ptr_data = data_param;
}

};

int main(void)
{
ClassInt c1(10);
ClassInt c2(c1);
ClassInt c3 = c2;

std::cout << c1.ptr_count() << std::endl; //3
std::cout << c2.get_data() << std::endl; //10
}

ptr_count 的数量可以看出他们指向了同样的地址, 而使用了 shared_ptr, 所以销毁的时候, 不会重复析构(先对引用计数操作).

weak_ptr

weak_ptr 被设计为与 shared_ptr 共同工作,可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造,获得资源的观测权.
值得强调的是weak_ptr一定从shared_ptr初始化而来, 它就是shared_ptr的助手(不能像普通指针那样使用 ->* 操作), 用于解决循环引用问题.
但是 weak_ptr 没有共享资源,它的构造不会引起指针引用计数的增加,同时,在析构的时候也不会引起引用计数的减少.(但是却可以从weak_ptr还原得到一个shared_ptr)

主要用于打破循环引用, 不能两方都是用强引用, 要有一方使用weak_ptr,才能在相互引用的情况下, 保证引用计数可以减为1, 从未正确释放.

例子:

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
class parent;
class children;

typedef boost::shared_ptr<parent> parent_ptr;
typedef boost::shared_ptr<children> children_ptr;

class parent
{
public:
~parent()
{
std::cout << "destroying parent" << std::endl;;
}

public:
children_ptr children;
};

class children
{
public:
~children()
{
std::cout << "destroying children\n";
}

public:
parent_ptr parent;
};

int main()
{
parent_ptr father(new parent()); //father.use_count 1
children_ptr son(new children); //son.use_count 1

std::cout << "parent_ptr count " << father.use_count() << std::endl;
std::cout << "child_ptr count " << son.use_count() << std::endl;

std::cout << "------------------" << std::endl;

/*相互赋值, 导致循环引用*/
father->children = son;
son->parent = father;

std::cout << "parent_ptr count " << father.use_count() << std::endl;
std::cout << "child_ptr count " << son.use_count() << std::endl;

/*
parent_ptr count 1
child_ptr count 1
------------------
parent_ptr count 2
child_ptr count 2
Main Function end
*/

}

当你析构father的时候, father引用计数减少1;但是由于son还引用着呢, 所以引用计数没有减少到0; 所以father指针所指向的parent的析构函数调用失败(引用计数还剩1); son指针销毁children的情况类似.

解决方法,引入weak_ptr:

  • expired()用于检测所管理的对象是否已经释放
  • lock()用于获取所管理的对象的强引用指针
1
2
3
4
5
6
7
8
9
//parent 代码不变;
class children
{
public:
~children() { std::cout <<"destroying children\n"; }

public:
boost::weak_ptr<parent> 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
int main(void)
{
parent_ptr father(new parent()); //father.use_count 1
children_ptr son(new children); //son.use_count 1

std::cout << "parent_ptr count " << father.use_count() << std::endl;
std::cout << "child_ptr count " << son.use_count() << std::endl;

std::cout << "------------------" << std::endl;

father->children = son; //son.use_count 2
son->parent = father; //father.use_count 1

std::cout << "parent_ptr count " << father.use_count() << std::endl;
std::cout << "child_ptr count " << son.use_count() << std::endl;

/*
Main Function start
parent_ptr count 1
child_ptr count 1
------------------
parent_ptr count 1
child_ptr count 2
destroying parent
destroying children
Main Function end
*/
}

从上面看到,赋值给weak_ptr的仍旧是 boost::shared_ptr<parent>,即 shared_ptr.

标准库智能指针

C++11引入的3个智能指针全部都在 <memory> 头文件中, shared_ptrweak_ptr大致和Boost一样.

1
2
3
4
5
6
7
8
9
10
11
12
13
auto pShared = std::make_shared<T>(new T)
auto pWeak = std::make_shared<T>(pShared);

if(pWeak.expired()) {
std::cout << "The Obj is no longer exists" << std::endl;
}

std::shared_ptr<T> pNew(pWeak.lock()); //如果对象已经销毁则返回的是nullptr
if(pNew){
//doing sth.
} else {
std::cout << "The Obj is no longer exists" << std::endl;
}

注意 shared_ptr 没有权限转移行为, 因为他是基于引用计数的, 所以这样的代码是错误的:

1
2
3
4
auto old_ptr = std::make_unique<T>(new T);

//shared_ptr没有release()方法
std::shared_ptr<T> new_ptr(old_ptr.release());

下面主要说说 unique_ptr

unique_ptr

这个本质上和 scoped_ptr 有点儿像, 不能拷贝, 不能赋值, 所以你想把外部具名的 unique_ptr 实例放入容器是不能的了.
作为函数参数, 返回值, 值传递也是不可能的了, 但是下面的案例除外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unique_ptr<int> f(int i){
if (i == 0)
return unique_ptr<int>(new int(0));
else
return unique_ptr<int>(new int(1));
}

int main(void)
{
auto p = f(2);
/*std::unique_ptr<int> p;
p = f(2); //wrong
*/
return 0;
}

这里采用了匿名对象(临时对象), 直接初始化到容器内(相当于有人接收, 所以小三直接上位, 临时变量也不用销毁了), 但是注释起来的则产生的中间对象没有人接收, 行为不确定, 要看具体的编译器处理.(但是这样返回的临时对象的unique_ptr实在没有意义)

补充知识: 具名返回值优化
C++ 11确实可以用move构造编译通过, 即便没有move构造, 还有一种潜规则即 Named Return Value Optimization (具名返回值优化). 这种情况下由于返回的时一个临时对象,所以编译器会将接受返回值的对象的引用传进去直接进行构造.

下面的代码也是 ok 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::cout << "Runtime polymorphism demo\n";
{
std::unique_ptr<B> p = std::make_unique<D>(); // p is a unique_ptr that owns a D

// as a pointer to base
p->bar(); // virtual dispatch

std::vector<std::unique_ptr<B>> v; // unique_ptr can be stored in a container

v.push_back(std::make_unique<D>());
v.push_back(std::move(p));
//v.push_back(p); //error
v.emplace_back(new D);

for (auto& p : v) {
p->bar(); // virtual dispatch
}
} // ~D called 3 times

因为存入容器的, 全部是没有调用拷贝构造的(直接新生成的或者采用移动构造转移所有权的), 保证了 unique 的不共享特性.

v.push_back(std::make_unique<D>()); 临时的右值, 编译器允许. (就相当于一个中间对象)

其他使用(reset, release等不说了)

1
2
3
4
size_t len[10];
std::unique_ptr<int[]> pnumbers(new int[len]);
//or
std::unique_ptr<int[]> pnums = std::make_unique<int[]>(len);

控制权的转移:

  • std::move
  • reset(release())

(前一种方式其实是利用临时右值, 进行权限转移)

unique_ptr 不支持拷贝和复制, 所以只能唯一引用, 这和它的(RAII)实现有关.
(后面有时间再说)

尾巴

几种智能指针的使用还是比较简单的, 中间也会有一些坑, 稍微注意一下就好了.

但是智能指针的引入, 并没有完全解决相关回收泄露问题, 只是一定程度上缓解了, 简化了操作, 也就是说, 还是需要人为注意.

参考

  1. http://www.boost.org/doc/libs/1_60_0/libs/smart_ptr/smart_ptr.htm
  2. 《Boost程序库完全开发指南》
  3. Effective C++ 资源管理部分
文章目录
  1. 1. 引子
  2. 2. 正文
    1. 2.1. Boost智能指针
      1. 2.1.1. scoped_ptr
      2. 2.1.2. shared_ptr
      3. 2.1.3. weak_ptr
    2. 2.2. 标准库智能指针
      1. 2.2.1. unique_ptr
  3. 3. 尾巴
  4. 4. 参考
|