14.Boost-内存管理(笔记)


内存管理一直是令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_ptrscoped_arrayshared_ptrshared_arrayweak_ptrintrusive_ptr,从各个方面来增强std::auto_ptr,而且是异常安全的。库中的两个类——shared_ptrweak_ptr已被收入到c++新标准的TR1库中。

接下来详细介绍scoped_ptrscoped_arrayshared_ptrshared_array,简要介绍另两个组件weak_ptrintrusive_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_ptroperator*()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_ptrauto_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_arrayscoped_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;

这样的代码完全可以通过编译ÿ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值