0 生活鸡汤
偶然看到一篇文章,每天前进一点点,积累下来,人生就能有所改变。已经有一段时间没有更新这个系列,今天争取再往前走一点点。
1 提出问题
宠物医院提供收养服务,其中,主要收养对象是小狗(Dog)小猫(Cat)。收养需要走一定流程,具体流程我们不必关心。上面的情景可用下面代码描述。
class Animal {
public:
virtual void processAdoption() = 0;
}
class Cat : public Animal {
public:
virtual void processAdoption();
}
class Dog : public Animal {
public:
virtual void processAdoption();
}
void batchAdoption(istream& dataSource) {
while (dataSource) {
Animal *animal = readAnimal(dataSource);
animal->processAdoption();
delete animal;
}
}
但是,如果上面的batchAdoption()方法中,readAnimal(), processAdoption(), 都可能抛出异常, 程序中断,从而导致delete animal无法执行,内存泄漏发生。
2 解决途径
由于“防止内存泄漏”时本书的一个重要主题,为了一步步揭示思维过程,和书中内容保持一致,下面将给出逐步优化过程。
1 利用“异常捕获”
考虑到上面readAnimal()和processAdoption()都有可能出现异常,因此可以将两个语句放入try中。
这样的代码比较冗余,因此我们是否可以进一步将delete操作集中到一处进行处理?
void batchAdoption(istream& dataSource) {
while (dataSource) {
Animal *animal = readAnimal(dataSource); // 不能放入try中,否则animal对外部不可见
try {
animal->processAdoption();
} catch (...) {
delete animal;
throw;
}
delete animal;
}
}
2 将指针用对象抱起来
我们可以将readAnimal()返回的指针,作为构造参数,放入一个类对象中;将delete animal的动作,放到类对象的析构函数中。此时,一旦退出类对象所在作用域,其析构函数被调用,那么delete animal就会被执行。
STL提供了auto_ptr模板类来实现上面的设计。其可能的实现如下。
template <class T>
class auto_ptr {
public:
auto_ptr(T *p = 0) : m_ptr(p) {}
~auto_ptr() { delete m_ptr;}
private:
T *m_ptr;
};
这样,上面的函数可以实现为下面的代码。
void batchAdoption(istream& dataSource) {
while (dataSource) {
auto_ptr animal(readAnimal(dataSource));
animal->processAdoption();
// 无需调用语句delete animal,出了作用域即调用析构函数
}
}
3 进一步应用
至此,我们的核心观念已经提出:
1,利用“作用域”和“生存周期”来控制heap中对象的生存周期。
2,进一步,利用“作用域”和“生存周期”,来控制函数局部内的行为。
根据上面的道理,进一步应用到现实场景。在窗口显示信息的过程中,出现异常,则窗口指针w将无法被销毁。因此,我们可以用类对象将w封装,从而保证无论什么情况下,w总能被销毁。
class WindowHandle {
public:
WindowHandle(Window_Handle handle) : w(handle){}
~WindowHandle() {destroyWindow(w);}
operator Window_Handle() { return w;} //隐式类型转换函数
private:
Window_Handle w;
WindowHandle(const WindowHandle&); // 屏蔽复制构造函数
WindowHandle& operator=(const WindowHandle& ); // 屏蔽复制构造函数
4 提出新问题
后面条款10和条款11将分别讨论如下两个问题:
1,当初始化包装heap指针的时候,抛出异常。
2,当析构包装heap指针的时候,抛出异常。