【Effective C++】资源管理

所谓资源就是,一旦用了它,将来必须还给系统。C++程序中常见的资源有:

  • 动态分配内存
  • 文件描述器
  • 互斥锁
  • 图形界面的字型和笔刷
  • 数据库连接
  • 网络sockets

如何管理资源?

手动管理资源

假设有一个基类BaseCamera,各式各样的相机类继承自它,通过工厂函数供应某个特定的相机对象:

BaseCamera* pCam = createCamera();//返回指针,指向BaseCamera继承体系的动态分配对象。调用者有责任删除它。这里为了简化刻意不写参数。
...
delete pCam;//释放所指对象

这看起来妥当,但是当“...”区域内有一个过早的return语句,或者异常,delete会被略过,我们泄漏的不仅是内含相机对象的那块内存,还有相机对象所保存的任何资源。

当然啦,谨慎的编写代码可以防止这一错误,但是单纯依赖delete语句是行不通的

利用现成的资源管理类

把资源放进对象内,我们便可以依赖C++的“析构函数自动调用机智”确保资源被释放。许多资源被动态分配于heap内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。

1. auto_ptr

auto_ptr是“类指针对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete

auto_ptr<BaseCamera> pCam(createCamera());//一如以往地调用工厂函数,使用pCam
...
//经由auto_ptr的析构函数自动删除pCam

以上代码中,工厂函数返回的资源被当做其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源取得实际便是初始化时机”(Resource Acquisition Is Initialization;RAII),因为我们总是在获得一笔资源后在同一语句内以它初始化某个管理对象。管理对象会用析构函数确保资源被释放。注意:auto_ptr被销毁时会自动删除它所指之物,如果多个auto_ptr同时指向同一对象,对象会被删除一次以上,但,为了预防这个问题,auto_ptr有个特别的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。

auto_ptr<BaseCamera> pCam1(createCamera());//pCam1指向函数返回对象
auto_ptr<BaseCamera> pCam2(pCam1);//pCam2指向对象,pCam1为null
pCam1=pCam2;//pCam1指向对象,pCam2为null

2. tr1::shared_ptr

当类需要正常的复制行为时,就容不得auto_ptr,可以使用“引用计数型智慧指针”shared_ptr,其持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。

3. boost::scoped_array和boost::shared_array

auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[],所以不能在动态分配而得的array上使用auto_ptr和tr1::shared_ptr,但这样做能通过编译。

auto_ptr<string> p1 (new string[10]);//会用上错误的delete形式
tr1::shared_ptr<int> p2(new int[1024]);//同上

没有什么针对“C++动态分配数组”而设计的类似auto_ptr和tr1::shared_ptr的东西,因为vector和string几乎可以取代动态分配的数组。

Boost中的boost::scoped_array和boost::shared_array,可以提供这样的行为。

制作自己的资源管理类

对于不是heap-based的资源,智能指针往往不适合作为资源掌控者,因此有时需要建立自己的资源管理类。例如我们使用C API函数处理类型为BaseCamera的相机对象,共有open和close两个函数可用:

void open(BaseCamera* cam); //打开cam所指的相机
void close(BaseCamera* cam); //关闭cam所指的相机

为确保不会忘记将一个打开的相机关闭,你可能会希望建立一个class来管理,这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”:

class  Person{
public:
    explicit Person(BasicCamera* pm):camera(pm){
       open(camera) ;
    ~Person(){close(camera);}
private:
    BaseCamera *camera;
};

在使用的时候:

Basecamera cam;
...
{
    Person p(&cam);//打开相机
    ...
}//自动关闭相机

1. 在资源管理类中要小心copying行为

普遍而常见的RAIIclass的copying行为有:

  • 禁止复制

当复制动作对RAII类并不合理,应该禁止之。将copying操作声明为private。

class  Person: private Uncopyable{
public:
...
};
  • 对底层资源使用“引用计数法”

有时候我们希望保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该讲资源的“被引用数”递增。通常只要含有一个tr1::shared_ptr成员变量,RAIIclass就可以实现引用计数的复制行为

class  Person{
public:
    explicit Person(BasicCamera* pm):camera(pm,close){//以close函数作为删除器
       open(camera.
    shared_ptr<BaseCamera> camera;//使用shared_ptr代替raw指针
};

tr1::shared_ptr的缺省行为时“当引用计数为0时删除其所有物”,而我们想要的是关闭相机而不是删除相机。可以指定tr1::shared_ptr的删除器为close函数。class的析构函数会自动调用器non-static成员变量camera的析构函数。而camera的析构函数会在引用次数为0时自动调用删除器close函数。

  • 复制底部资源

当想对一份资源拥有其任意数量的复件时,进行深度拷贝,同时复制对象包含的指针和heap内存。

  • 转移底部资源的拥有权

像auto_ptr一样。

2. 在资源管理类中提供对原始资源的访问

之前我们使用auto_ptr或者 shared_ptr保存工厂函数的返回资源:

auto_ptr<BaseCamera> pCam1(createCamera());

此时有个函数需要处理BaseCamera对象,像这样:

void fun(const BaseCamera* cam);

你想这么调用它:

fun(pcam1);

但却通不过编译,因为fun函数需要的是BaseCamera*指针,你传给它的却是一个类型为auto_ptr<BaseCamera>的对象。这时需要一个函数将RAII class对象转换为其所内含的原始资源。有两个做法可以达成目标:

  • 显式转换

auto_ptr和tr1::shared_ptr两者都提供了一个get成员函数用来执行显式转换,也就是它能返回智能指针内部的原始指针(的复件):

fun(pcam1.get());
  • 隐式转换

auto_ptr和tr1::shared_ptr都重载了指针取值操作符(operate->和operate*),它们允许隐式转换至底部原始指针。

auto_ptr<BaseCamera> pCam1(createCamera());
p1->fun1();//经由operate->访问资源
(*p1).fun2();//经由operate*访问资源

以上是获取智能指针指向资源的方法。如果是自己的资源管理类,也可以提供显式的转换函数,像get一样,或者提供隐式的转换函数,像重写操作符()。

APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其管理之资源”的办法。对原始资源的访问可能是经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换比较方便。

new的注意事项

1. 成对使用new和delete时要采取相同形式

当你使用new动态生成一个对象,有两件事发生:1)内存被分配出来 2)会有构造函数被调用。当你使用delete,也有两件事发生:1)析构函数被调用 2)内存被释放。delete的最大问题是:即将被删除的内存之内究竟存有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。

string* p1 = new string;
string* p2 = new string[10];
delete p1;
delete[] p2;

这个问题可以更简单些:即将被删除的那个指针,所指的是单一对象或对象数组?当你对一个指针使用delete,它便认定指针指向单一对象,而加上[],delete便认定指针指向一个数组。当你对p1使用delete[],delete会读取若干内存并将它解释为“数组大小”,然后多次调用析构函数,浑然不知它删除的那块内存不但不是数组,而且都不一定是string对象。当你对p2使用delete,也会导致调用的析构函数太少。

2. 以独立语句将new好的对象置入智能指针

假设有这样的一段代码:

int getNum();
void fun(shared_ptr<BaseCamera> pw, int num);
...
fun(shared_ptr<BaseCamera>(new BaseCamera), getNum());//调用

编译器需要做以下3件事:

1)调用getNum

2)执行new BaseCamera

3)调用shared_ptr构造函数

其中2)一定在3)的前面,但getNum的调用不一定,如果getNum调用异常,而new BaseCamera返回的对象没有置入shared_ptr内,资源就会泄漏。因此使用分离语句:1)创建BaseCamera 2)将它置于智能指针内 3)再把指针传给fun函数。

shared_ptr<BaseCamera> pw(new BaseCamera);
fun(pw, getNum());//调用

 

整理自《Effective C++》第三章资源管理 P61-P77

 

posted @ 2022-05-16 22:06  湾仔码农  阅读(74)  评论(0编辑  收藏  举报