第三章:资源管理
所谓资源就是,一旦用了它,将来必须还给系统;否则,糟糕的事情就会发生。C++中常用的资源就是动态分配内存,如果未能及时释放归还它,将会导致内存泄露(该块内存区域将永远不能使用)。
其他的资源还包括:
- 文件描述器
- 互斥锁
- 图形界面中的字型和笔刷
- 数据库连接
- 网络sockets
条款13:以对象管理资源
假设我们使用一个用来塑模投资行为(例如股票、债券等)的程序库,其中各式各样的投资类型继承自一个root class Investment:
class Investment {...}; //"投资类型"继承体系中的root class
进一步假设,这个程序库系通过一个工厂函数(条款7)供应我们某特定的Investment对象:
Investment* createInvestment(); //返回指针,指向Investment继承体系内
//的动态分配对象。调用者有责任删除它
现在编写一个f函数履行删除该对象的责任:
void f(){
Investment* pInv = createInvestment(); //调用factory函数
...
delete pInv; //释放pInv所指对象
}
以上函数似乎是可行的,但某些情况下并不能很好的执行,因为"..."内可能存在return语句导致过早的返回,未能成功执行delete语句;另一可能就是"..."内可能会抛出异常,则同样无法执行delete语句。因此,我们泄露的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。
未确保资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源;即把资源放进对象内,依赖于C++的“析构函数自动调用机制”确保资源被释放。
标准库函数提供的auto_ptr正是针对这种形势设计的产品。它是个"类指针(pointer-like)对象",也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。例:
void f(){
std::auto_ptr<Investment> pInv(createInvestment());
//调用factory函数,一如既往的使用pInv,经由auto_ptr的析构函数自动删除pInv
}
以上即RAII -----> "资源取得时机便是初始化时机"。
【注】由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象;因为这会使得对象删除一次以上,导致“未定义的行为”。auto_ptr有一个不寻常的性质:若通过copy构造函数或拷贝分配操作符复制他们,他们会变成null,而复制所得的指针将取得资源的唯一使用权!
//pInv1指向createInvestment返回物
std::auto_ptr<Investment> pInv1(createInvestment());
//现在的pInv2指向对象,pInv1被设为null
std::auto_ptr<Investment> pInv2(pInv1);
//现在的pInv1指向对象,pInv2被设为null
pInv1 = pInv2;
以上均说明“受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它”。
auto_ptr的替代方案是“引用计数型智慧指针”,其持续追踪共有多少对象指向某笔资源,并在雾刃指向它时自动删除该资源;行为类似于垃圾回收,典型示例是tr1::shared_ptr,即:
void f(){
...
//调用factory函数
std::tr1::shared_ptr<Investment> pInv(createInvestment());
..。
//经由shared_ptr析构函数自动删除pInv
}
auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[ ]操作;这意味着在动态分配而得的array身上使用这两个智能指针是个馊主意,但其错误仍能通过编译。
条款14:在资源管理类中小心coping行为
上述条款阐明了“资源取得时机便是初始化时机”,并以此作为“资源管理类”的脊柱,也描述了auto_ptr和tr1::shared_ptr如何将这个观念表现在heap-based资源上。然后并非所有资源都是heap-based,对那种资源而言,像这两种指针往往不适合作为资源掌管者。因此,我们需要自己建立相应的资源管理类。
例如,假设我们使用C API函数处理类型为Mutex的互斥器对象,共有lock和unlock两函数可用:
void lock(Mutex* pm); //锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥器解除锁定
为确保绝不会忘记将一个被锁住的Mutex解锁,你可能会希望建立一个class用来管理机锁。这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”:
class Lock{
public:
explict Lock(Mutex* pm):mutexPtr(pm){
lock(mutexPtr); //获得资源
}
~Lock() { unlock(nutexPtr); } //释放资源
private:
Mutex* mutexPtr;
};
客户对Lock的用法符合RAII方式:
Mutex m; //定义需要的互斥器
...
{ //建立一个区块用来定义critical section
Lock m1(&m); //锁定互斥器
... //执行critical section内的操作
} //在区块最末尾,自动解除互斥器锁定
但如果Lock对象被复制,会发生什么事?
Lock m11(&m); //锁定m
Lock m12(m11); //将m11复制到m12身上,会发生什么事呢
对此,我们有以下两种选择:
- 禁止复制:将copying操作声明为private,即条款6;
- 对底层资源祭出“引用计数法”:直到最后一个使用者(某对象)被销毁,即类似shared_ptr智能指针;
- 复制底部资源:当你不再需要某个复件时确保它被释放,在此情况下复制资源管理对象,应该同时也复制其所包覆的资源;也就是进行了“深度拷贝”;
- 转移底部资源的拥有权:某些场合下可能希望确保永远只有一个RAII对象指向一个未加工资源,此时资源的拥有权会从被复制物转移到目标物;
条款15:在资源管理类中提供对原始资源的访问
使用智能指针如auto_ptr或者shared_ptr保存factory函数如createInvestment的调用结果:
std::tr1::shared_ptr<Investment> pInv(createInvestment());
假设希望某个函数来处理Investment对象,如:
int daysHeld(const Investment* pi); //返回投资天数
我们需要这么使用:
int days = daysHeld(pInv); //错误!!!
以上不能通过编译,因为daysHeld需要的是Investment* 指针,而我传递的却是个类型为tr1::shared_ptr<Investment>对象;这个时候需要一个函数将RAII class对象转换为其所内含之原始资源(Investment*)。有两个做法:
- 显示转换:
tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针,即:
int days = daysHeld(pInv.get());
- 隐式转换
就像(几乎)所有智能指针一样,这两个智能指针也重载了指针取值操作符(operator->和operator*):
class Investment{ //Investment继承体系的根类
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); //factory函数
//令shared_ptr管理一笔资源
std::tr1::shared_ptr<Investment> pi1(createInvestment());
//经由operator->访问资源
bool taxable1 = !(pi1->isTaxFree());
...
//令auto_ptr管理一笔资源
std::auto_ptr<Investment> pi2(createInvestment());
//经由operator*访问资源
bool taxable2 = !((&pi2).isTaxFree());
条款16:成对使用new和delete时要采取相同形式
std::string* stringArray = new std::string[100];
...
delete stringArray;
以上代码使用new创建了一个长度为100的string数组,同时也使用了delete删除该指针,但编译器仅可能删除string数组中的一个对象,其余99个不能被适当的删除,因为它们的析构函数很可能没有被调用。
当使用new创建对象,有两件事发生:
- 内存被分配出来,通常在堆(heap)上;
- 针对此内存会有一个(或更多)构造函数被调用;
当使用delete删除对象,也有两件事发生:
- 针对此内存会有一个(或更多)析构函数被调用;
- 内存释放;
实际上,都可以简化成一个问题:在删除指针的时候,所指的单一对象还是对象数组?
当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个“数组大小记录”的办法就是:由你来告诉它。如果你使用delete时加上中括号,delete便认定指针指向一个数组,否则便指向一个单一对象。
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete[] stringPtr2;
【注】如果你调用new时用了[ ],你必须在对应调用delete时也使用[ ];如果new时未使用[ ],则delete时也不该使用[ ]。
对于使用typedef的人来说,当程序员以new创建该种typedef类型对象时,该以哪一种delete形式删除之。考虑以下:
typedef std::string AddressLines[4]; //每个人地址有4行,每行一个string
由于AddressLines是个数组,如果这样使用new:
std::string* pal = new AddressLines;
//注意,“new AddressLines”返回一个string*,就像“new string[4]”一样
那就必须匹配“数组模式”的delete:
delete pal; //行为未有定义!
delete[] pal; //正常!
条款17:以独立语句将newd对象置入智能指针
假设我们有个函数来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);
由于“以对象管理资源”的智慧铭言,processWidget决定对其动态分配得来的Widget运用智能指针。
现在考虑调用processsWidget:
processWidget(new Widget,priority());
上述形式并不能通过编译。tr1::shared_ptr的构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,将得自“newWidget”的原始指针转换为processWidget所要求的tr1::shared_ptr。如果写成下述形式即可通过编译:
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
在调用
std::tr1::shared_ptr<Widget>(new Widget)
时,将有两部分构成:
- 执行 “new Widget” 表达式
- 调用tr1::shared_ptr构造函数
于是在调用processWidget之前,编译器必须创建代码,做以下三件事:
- 调用priority
- 执行“new Widget”
- 调用tr1::shared_ptr构造函数
但是,C++编译器以什么样的次序去完成这些事情呢?答案是未知的。目前可以确定创建对象的操作必定是第一位,但如果是按照以下顺序执行呢?
- 执行“new Widget”
- 调用priority
- 调用tr1::shared_ptr构造函数
万一priority函数的调用导致异常,会发生什么事呢?这个时候“new Widget”返回的指针将会遗失,未能够进入智能指针中,从而导致了资源泄露。
避免这类问题的方法也很简单,使用分离语句,分别写出
- 创建Widget
- 将它置入一个智能指针内,再传入processWidget
即:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());