文章目录
内存管理一直是令c++程序员最头疼的工作,c++继承了c那高效而又灵活的指针,使用起来稍微不小心就会导致内存泄漏(
memory leak
)、“野”指针(
wildpointer
)、越界访问(
access denied
)等问题。曾几何时,c++程序员曾经无限地向往Java、C#等语言的垃圾回收机制。虽然c++标准提供了智能指针
std::auto_ptr
,但并没有解决所有问题。
阅读完本章,你会了解到高效的内存管理方法,彻底忘记"栈"(Stack
)、“堆”(Heap
)等内存分配相关的术语,并且还会发现,Boost为c++提供的解决方案可能要比Java和C#等其他语言更好。
1 smart_ptr 库概述
计算机系统中资源又很多种,内存是我们最常用到的,此外还有文件描述符、socket
、操作系统handler
、数据库连接等等,程序中申请这些资源后必须及时归还系统,否则就会产生难以预料的后果。
1.1 RAII机制
为了管理内存等资源,c++程序员通常采用RAII机制(资源获取即初始化,Resource Acquisition Is Initialization
),在使用该资源的类的构造函数中申请资源,然后使用,最后在析构函数中释放资源。
如果对象是用声明的方式在栈上创建的(一个局部对象),那么RAII机制会工作正常,当离开作用域时对象会自动销毁从而调用析构函数释放资源。但如果对象是用new操作符在堆上创建的,那么它析构函数不会自动调用,程序员必须明确地用对应的delete操作符销毁它才能释放资源。这就存在着资源泄漏的隐患,因为这时没有任何对象对已经获取的资源负责,如果因某些以外导致程序未能执行delete语句,那么内存等资源就永久地丢失了。例如:
int *p = new class_need_resource; //对象创建,获取资源
.. //可能发生异常导致资源泄漏
delete p; //析构释放资源
new、delete 以及指针的不恰当运用是c++中造成资源获取/释放问题的根源,能够正确而明智地运用delete是区分c++新手与熟手的关键所在。但是很多人—即使是熟练的c++程序员,也经常会忘记调用delete。
1.2 智能指针
智能指针(smart pointer)是c++群体中热门的议题,围绕它有很多价值的讨论和结论。它实践了代理模式,代理了原始"裸"指针的行为,为它添加了更多更有用的特性。
c++引入异常机制后,智能指针由一种技巧升级为一种非常重要的技术,因为如果没有智能指针,程序员必须保证new对象能在正确的实际delete,四处编写异常捕获代码以释放资源,而智能指针则可以退出作用域时——不管是正常流程离开或是因异常离开——总调用delete来析构在堆上动态分配的对象。
存在很多种智能指针,其中最有名的应该是c++98标准中的“自动指针”std::auto_ptr
,它部分地解决了获取资源自动释放的问题,例如:
int main()
{
//离开作用域,p1、p2自动析构从而释放内存等资源
auto_ptr<class_need_resource> p1(new class_need_resource);
auto_ptr<demo_class> p2(factory.create());
...
}
auto_ptr
的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但因为重载了operator*和operator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时(离开函数main()或者发生异常),c++语言会保证auto_ptr
对象销毁,调用auto_ptr的析构函数,进而使用delete操作符删除原始指针释放资源。
auto_ptr
很好用,被包含在c++标准库中令它在全世界范围内广泛使用,使智能指针的思想、用法深入人心。但标准库并没有覆盖智能指针的全部领域,尤其是最重要的引用计数型智能指针。
boost.smart_ptr
库是对c++98标准的一个绝佳补充。它提供了六种智能指针,包括scoped_ptr
、scoped_array
、shared_ptr
、shared_array
、weak_ptr
和intrusive_ptr
,从各个方面来增强std::auto_ptr
,而且是异常安全的。库中的两个类——shared_ptr
和weak_ptr
已被收入到c++新标准的TR1库中。
接下来详细介绍scoped_ptr
、scoped_array
、shared_ptr
和shared_array
,简要介绍另两个组件weak_ptr
和intrusive_ptr
。它们都是很轻量级的对象,速度与原始指针相差无几,对于所指的类型T也有一个很小且很合里的要求:类型T的析构函数不能抛出异常。
这些智能指针都位于名字空间boost
,为了使用smart_ptr
组件,需要包含头文件<boost/smart_ptr.hpp>,即:
#include <boost/smart_ptr.hpp>
using namespace boost;
2 scoped_ptr
scoped_ptr
是一个很类似auto_ptr
的智能指针,它包装了new
操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但scoped_ptr
的所有权更加严格,不能转让,一旦scoped_ptr
获取了对象的管理权,你就无法再从它那里取回来。
scoped_ptr
拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让。
2.1 类摘要
scoped_ptr
的类摘要如下:
template<class T>
class scoped_ptr{ //noncopyable
private:
T* px;
scoped_ptr(scoped_ptr const &);
scoped_ptr* operator=(scoped_ptr const &);
public:
explicit scoped_ptr(T* p=0);
~scoped_ptr();
void reset(T* p = 0);
T& operator*() const;
T* operator->() const;
T* get() const;
operator unspecified-bool-type() const;
void swap(scoped_ptr& b);
};
2.2 操作函数
scoped_ptr
的构造函数接受一个类型为T*
的指针p,创建出一个scoped_ptr
对象,并在内部保存指针参数p。p必须是一个new表达式动态分配的结果,或者是个空指针(0)。当scoped_ptr
对象的生命期结束时,析构函数~scoped_ptr
会使用delete操作符自动销毁所保存的指针对象,从而正确地回收资源。
scoped_ptr
同时把拷贝构造函数和赋值操作符都声明为私有的,禁止对智能指针的复制操作,保证了被它管理的指针不能被转让所有权。
成员函数reset()
的功能是重置scoped_ptr
;它删除原来保存的指针,再保存新的指针值p。如果p是空指针,那么scoped_ptr
将不会持有任何指针。一般情况下reset()
不应该被调用,因为它违背了scoped_ptr
的本意——资源应该一直有scoped_ptr
自己自动管理。
scoped_ptr
用operator*()
和operator->()
重载了解引用操作符*和箭头操作符->,以模仿被代理的原始指针的行为,因此可以把scoped_ptr
对象如同指针一样使用。如果scoped_ptr
保存空指针,那么这两个操作的行为未定义。
scoped_ptr
不支持比较操作,不能在两个scoped_ptr
之间、scoped_ptr
和原始指针或空指针之间进行相等或者不相等测试,我们也无法为它编写额外的比较函数,因为它已经将operator==
和operator!=
两个操作符重载都声明为私有的。但scoped_ptr
提供一个可以在bool
语境(context
)中自动转换成bool
值(如if的条件表达是)的功能,用来测试scoped_ptr
是否持有一个有效的指针(为空)。他可以代替与空指针的比较操作,而且写法更简单。
成员函数swap()
可以交换两个scoped_ptr
保存的原始指针。它是高效的操作,被用于实现reset()
函数,也可以被boost:swap()
所利用。
最后是成员函数get()
,它返回scoped_ptr
内部保存的原始指针,可以用在某些要求必须是原始指针的场景(如底层的c接口)。但是使用时必须小心,这将使原始指针脱离scoped_ptr
的控制!不能对这个指针做delete
操作,否则scoped_ptr
析构时会对已经删除的指针在进行删除操作,发送未定义行为(通常是程序崩溃,这可能是最好的结果,因为它说明你的程序存在bug)。
2.3 用法
scoped_ptr
的用法很简单:在原本使用指针变量接受new表达式结果的地方该用scoped_ptr
对象,然后去掉哪些多余的try/catch和delete操作就可以了。像这样:
scoped_ptr<string> sp(new string("text"));
scoped_ptr
是一种"智能指针",因此其行为与普通指针基本相同,可以使用非常熟悉的*和->操作符:
cout << *sp << endl; //取字符串的内容
cout << sp->size() << endl; //取字符串的长度
但记住:不再需要delete
操作,scoped_ptr
会自动地帮助我们释放资源。如果我们对scoped_ptr
执行delete
会得到一个编译错误:因为scoped_ptr
是一个行为类似指针的对象,而不是指针,对一个对象应用delete是不允许的。
scoped_ptr
不允许拷贝、赋值,只能在scoped_ptr
被声明的作用域内使用。除了*和->外scoped_ptr
也没有定义其他的操作符(不能对scoped_ptr
进行++或者–等指针算术操作)。与普通指针相比它只有很小的接口,因此使指针的使用更加安全,更容易使用同时更不容易被误用。下面的代码都是scoped_ptr
的错误用法:
sp++; //错误,scoped_ptr未定义递增操作符
scoped_ptr<string> sp2 = sp; //错误,scoped_ptr不能拷贝构造
使用scoped_ptr
会带来两个好处:
- 一是使代码变得清晰简单,而简单意味着更少的错误;
- 二是它并没有增加多余的操作,安全的同时保证了效率,可以获得与原始指针同样的速度。
示范scoped_ptr
用法的另一段代码如下:
#include <boost/smart_ptr.hpp>
using namespace boost;
struct posix_file //一个示范性质的文件类
{
posix_file(const char* file_name) //构造函数打开文件
{cout << "open file:" << file_name << endl; }
~posix_file() //析构函数关闭文件
{cout << "close file" << endl;}
};
int main()
{
scoped_ptr<int> p(new int); //一个int指针的scoped_ptr
if(p) //在bool语境中测试指针是否有效
{
*p = 100; //可以像普通指针一样使用解引用操作符*
cout << *p << endl;
}
p.reset(); //reset()置空scoped_ptr,仅仅是演示
assert(p == 0); //p不持有任何指针
if(!p) //在bool语境中测试,可以用!操作符
{cout << "scoped_ptr == null" << endl; }
//文件类的scoped_ptr
//将在离开作用域时自动析构,从而关闭文件释放资源
scoped_ptr<posix_file> fp(new posix_file("/tmp/a.txt"));
} //在这里发生scoped_ptr的析构
//p和fp管理的指针自动被删除
2.4 与auto_ptr的区别
scoped_ptr
的用法与auto_ptr
几乎一样,大多数情况下它可以与auto_ptr
相互替换,它也可以从一个auto_ptr
获得指针的管理权(同时auto_ptr
失去管理权)。
scoped_ptr
也具有auto_ptr
同样的“缺陷”——不能用作容器的元素,但原因不同:
auto_ptr
是因为它的转移语义scoped_ptr
是因为不支持拷贝和赋值,不符合容器对元素类型的要求。
scoped_ptr
与auto_ptr
的根本性区别在于指针的所有权:
-
auto_ptr
特意被设计为指针的所有权是可转移的,可以在函数之间传递,同一时刻只能有一个auto_ptr
管理指针。它的用意是好的,但转移语义太过于微妙,不熟悉auto_ptr
特性的初学者很容易误用引发错误。 -
scoped_ptr
是把拷贝函数和赋值函数都声明为私有的,拒绝了指针所有权的转让——除了scoped_ptr
自己,其他任何人都无权访问被管理的指针,从而保证了指针的绝对安全。
实例代码:
auto_ptr<int> ap(new int(10)); //一个int自动指针
scoped_ptr<int> sp(ap); //从auto_ptr获得原始指针
assert(ap.get == 0); //原auto_ptr不再拥有指针
ap.reset(new int(20)); //auto_ptr拥有新的指针
cout << *ap << "," << *sp << endl;
auto_ptr<int> ap2;
ap2 = ap; //ap2从ap获得原始指针,发送所有权转移
assert(ap.get() == 0); //ap不再拥有指针
scoped_ptr<int> sp2; //另一个scoped_ptr
sp2 = sp; //赋值操作,无法通过编译!!!
3 scoped_array
scoped_array
很像scoped_ptr
,它包装了new[]
操作符(不是单纯的new)在堆上分配的动态数组,为动态数组提供了一个代理,保证可以正确的释放内存。
scoped_array
弥补了标准库中没有指向数组的智能指针的缺憾。
3.1 类摘要
scoped_array
的类摘要如下:
template<class T> class scoped_array //noncopyable
{
public:
explicit scoped_array(T* p = 0);
~scoped_array();
void reset(T* p = 0);
T& operator[](std::ptrdiff_t i) const;
T* get() const;
operator unspecified-bool-type() const;
void swap(scoped_array& b);
};
scoped_array
的接口和功能几乎是与scoped_ptr
是相同的(甚至还要少一些),主要特点如下:
- 构造函数接受的指针p必须是 new[] 的结果,而不能是new表达式的结果;
- 没有*、->操作符重载,因为
scoped_array
特有的不是一个普通指针; - 析构函数使用 delete[] 释放资源,而不是 delete;
- 提供
operator[]
操作符重载,可以像普通数组一样用下标访问元素; - 没有
begin()
、end()
等类似容器的迭代器操作函数。
3.2 用法
scoped_array
与scoped_ptr
源于相同的设计思想,故而用法非常相似:它只能在被声明的作用域内使用,不能拷贝、赋值。唯一不同的是scoped_array
包装的是 new[] 产生的指针,并在析构时调用delete[],因为它管理的是动态数组,而不是单个动态对象。
通常scoped_array
的创建方式是这样的:
scoped_array<int> sa(new int[100]); //包装动态数组
scoped_array
重载了operator[]
,因此它用起来就像是一个普通的数组,但因为它不提供指针运算,所以不能用"数组首地址+N"的方式访问数组元素:
sa[0] = 10; //正确用法,使用operator[]
*(sa+1) = 20; //错误用法,不能通过编译
在使用重载的operator[]
时要小心,scoped_array不提供数组索引的范围检查,如果使用了超过动态数组大小的索引或者是负数索引将引发未定义行为。
下面代码进一步示范了scoped_array
的用法:
#include <boost/smart_ptr.hpp>
using namespace boost;
int main()
{
int *pArr = new int[100]; //一个整数的动态数组
scoped_array<int> sa(arr); //scoped_array 对象代理原始动态数组
fill_n(&sa[0],100,5); //可以使用标注库算法赋值数据
sa[10] = sa[20] + sa[30]; //用起来就像是个普通数组
} //这里 scoped_array 被自动析构,
//释放动态数组资源
3.3 使用建议
scoped_array
没有给程序增加额外的负担,用起来很方便轻巧。它的速度与原始数组同样快,很适合哪些习惯于用new操作符在堆上分配内存的程序员。但scoped_array
的功能很有限,不能动态增长,也没有迭代器支持,不能搭配STL
算法,仅有一个纯粹的"裸"数组接口。而且,我们应该尽量避免使用new[]操作符,它比new更可怕,是许多错误的来源,因为
int *p = new int[10];
delete p;
这样的代码完全可以通过编译ÿ