Effective C++ 改善程序与设计的55个具体做法(三)
资源管理
13. 以对象管理资源
- 当你通过工厂函数生成一个指针的时候(如下),调用者应该要对其进行delete操作。但是往往函数调用还么有到delete就由于return等操作终止了,单纯依赖f()导致无法进行到delete操作。
Investment* createInvestment();
void f(){
Investment* PInv= createInvestment();
...
delete PInv;
}
- 我们可以将资源放进对象内,当控制流离开f(),该对象的析构函数会自动释放这些资源。标准库中提供的auto_ptr正是为这种形式而设计的。
void f(){
std::auto_ptr<Investment> pInv(createInvestment());
.....
}
- 这里主要两个思想:1.获得资源后立刻放入管理对象中,即资源取得时机便是初始化时机(RAII)。2. 管理对象运用析构函数确保资源被释放。
- auto_ptr删除对象后会调用析构删除保存的指针,并且他可以通过copy构造和赋值操作符复制,复制后原有的会变成null,复制后的指针获得位移控制权(控制权转移)。所以避免让多个auto_ptr指向同一对象。
auto_ptr<Investment> pInv1(createInvestment());
auto_ptr<Investment> pInv2(pInv1);
pInv1 = pInv2;
- auto_ptr还有个问题就是,当对一个指针设置两个auto_ptr来管理时,当程序结束,会出现重复调用析构的问题。
int* a = new int(5);
auto_ptr<int> pInv1(a);
auto_ptr<int> pInv2(a);
- 这意味着auto_ptr并非管理动态分配内存的神兵利器,**shared_ptr(引用计数型智能指针)**会解决这个问题。但是shared_ptr也会存在循环引用的问题。通过拷贝和赋值时会让相应的计数器加1。当计数器为0是调用析构函数。值得注意的是,auto_ptr和shared_ptr内部都是调用delete函数而非delete [],所以对于array使用这两个是个馊主意。 (也可以直接让工厂函数返回一个智能指针类型,从而解决这个问题。)
- 为防止资源泄漏,请使用RAII(资源取得就初始化)对象,他们在构造函数中获得资源并且在析构函数中释放。
- 两个常被使用的RAII classes分别为shared_ptr和auto_ptr。前者通常为较佳选择,因为其中的copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
14. 在资源管理类中小心copying行为
-假设定义一个类型为Mutex的互斥器对象,有lock和unlock两个函数可用:
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm){
lock(mutexPtr);
}
~Lock(){
unlock(mutexPtr);
}
private:
Mutex* mutexPtr;
};
客户合理的用法如下:
Mutex m;
...
{
Lock m1(&m);
...
}
上述操作时正常的,但是如果你对Lock 进行复制会发生什么?
Lock m1(&m);
Lock m2(m1);
所以你需要考虑一个RAII对象被复制时会发生什么的问题:
- 禁止复制。 将copying操作定义为private。对Lock而言看起来挺合适。
- 对底层资源祭出“引用计数器”。如同shared_ptr操作。幸运的是shared_ptr可以指定自己的删除器,所以你完全可以修改上述的代码
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm, unlock()){
lock(mutexPtr);
}
private:
shared_ptrL<Mutex> mutexPtr;
};
- 复制底部资源。使用深度拷贝,拷贝出一个附件,避免重复释放同一个空间的问题。
- 转移底部资源控制权。如同auto_ptr操作。
- 值得注意的是,copying函数编译器会自动生成,如果你想要的和编译器生成的效果不同,最好自己重写定义他。
复制RAII对象必须一并复制他们所管理的资源,所以资源的copying行为决定RAII对象的copying行为。 普遍而常见的RAII类copying行为是:抑制copying、施行引用计数器。不过其他行为也可能被实现(底层复制资源与控制权转移(如auto_ptr))。
15. 在资源管理类中提供对原始资源的访问
class Font{
public:
...
Fon他Handle get()const{ return f;}
operator FontHandle() const
{
return f;
}
private:
FontHandle f;
};
此时调用就方便多了:
Font f(getFont());
int newFontSize;
...
changeFontSize(f,newFontSize)
但是这样也会增加错误发生:
Font f1(getFont());
...
FontHandle f2 = f1;
上述操作会导致f1被销毁后,f2就被吊空了。所以通常显示的转换更安全。
APIs往往要求访问原始资源,所以对每一个RAII 类应该提供一个取得其所管理资源的方法。(shared_ptr中比如get()函数并且他也有重载->和*) 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
16. 成对使用new和delete时要采用相同的形式
- 对于new,当你使用它时,他会做两件事,一是通过operator new函数分配内存,而是针对次内存会有一个或者多个构造函数被调用。
- 当你使用delete时,针对该内存会有一个或者多个析构函数被调用,然后内存才会被(operator delete操作)释放。
- 对于数组,相较于单一对象,在头部多了一个数组大小的记录。当你对一个指针使用delete时候,唯一能告诉他是否为数组的只有你。当你加上[],他就会知道该指针是一个数组,并且释放前会读取数组大小,来决定析构函数调用的次数。
- 如果你对一个单一对象使用delete[],他就会读取若干内存解释为数组大小,开始多次调用析构,完全不在意所处理的内存是否为那个对象。
- 对于一个数组只使用delete,那他则认为他是单一对象,只调用一次析构,只对数组的首位元素进行析构,后续的内容则会造成内存的泄漏。
- 最好不要对数组形式做typedefs动作,否则还要查明其是否为数组,决定使用哪种delete。
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[],如果你在new表达式中不使用[],一定不要再相应的delete表达式中使用。
17. 以独立语句将newed对象置入智能指针。
int priority();
void processWidget(std::trl::shared_ptr<Widget> pw,int priority);
进行如下调用:
processWidget(new Widget,priority());
但是shared_ptr的构造函数是一个explicit,无法进行隐式转换,无法把一个Widget指针转为一个shared_ptr类型,所以该调用会报错。
processWidget(shared_ptr<Widget>(new Widget),priority());
有三件事要做:
- priority()
- new Widget
- shared_ptr() 构造函数
在VS编译环境,我测试过是从右到左。但是不同的编译环境弹性很大,如果执行顺序变换后变为2-1-3.那么如果在执行好申请内存后,priority()出现异常,导致shared_ptr() 构造函数么有进行执行,那么申请的内存没有得到管理,这就会导致内存的泄漏。
- 所以尽量将1和23分离开,先执行2,3,确保得到管理后,在和1一起执行。
shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
|