首先解释一下本节两个主要知识点:析构函数(destructor)与多态(polymorphism)
析构函数(destructor):用来释放对象所占用的资源。当对象的使用周期结束后,例如当某对象的范围(scope)结束时,或者是动态分配的对象被delete关键字解除资源时,对象的析构函数会被自动调用,对象所占用的资源就会被释放。假如在你的类中不声明析构函数,编译器也会为你自动生成一个。
多态(polymorphism):则是C++面向对象的基本思想之一,即抽象(abstraction),封装(encapsulation),继承(inheritance),多态(polymorphism)。如果我们希望仅仅通过基类指针就能操作它所有的子类对象,那这就是多态。
多态基类范例:
class TimeKeeper{ // 计时器类,用来当做基类
public:
TimeKeeper(); // 这是构造函数
~TimeKeeper(); // 这是析构函数
......
};
class AtomicClock : public TimeKeeper{...}; // 原子钟是一种计时器
class WaterClock : public TimeKeeper{...}; // 水钟也是一种计时器
TimeKeeper* getTimeKeeper(){...} // 用来返回一个动态分配的基类对象
TimeKeeper* ptk = getTimeKeeper();
..... // 使用这个指针操作它的子类
delete ptk; // 使用完毕,释放资源
以往情况下,我们通常认为 new 与 delete 相对应使用就不会发生内存泄漏(memory leak)。但是实际上多态中也可能发生内存泄漏,即使你及时使用了 delete。
上述代码存在的主要问题:
当你通过基类指针使用子类,使用完毕后却只从基类删除。同时这个基类的析构函数并不是虚函数(virtual),也就是不允许子类有自己版本的析构函数,这样就只能删除子类中基类的部分,而子类衍生出来的变量和函数所占用的资源并没有被释放,这就造成了这个对象只被释放了一部分资源的现象,依然会导致内存泄漏。
解决方法:
给基类一个虚的析构函数,这样子类就允许拥有自己的析构函数,就能保证被占用的所有资源都会被释放。
class TimeKeeper{
public:
virtual ~TimeKeeper();
....
};
注意:
其实作为一个多态的基类,不仅仅析构函数要声明为虚函数,如果想让不同的子类用不同的方法实现同一个函数,这个函数也要被声明为虚。换言之,大多数情况下,如果没有虚函数,这个类就不应该被用作一个基类。但也有少数例外会在后面提到。
另外若一个类不被用作基类,则不需要将其析构函数声明为虚函数,会产生不好的影响。
虚函数的工作原理:
虚函数是用来在运行时(runtime),自动把编译时未知的对象,比如用户输入的对象,和它所对应的函数绑定起来并调用。当一个类包含虚函数时,编译器会给这个类添加一个隐藏变量,即虚函数表指针(virtual table pointer),用来指向一个包含函数指针的数组,即虚函数表(virtual table)。当一个虚函数被调用时,具体调用哪个函数就可以从这个表里找了。
另外注意:
这个地址变量也是要占空间。例如在32位系统里,一个地址占32位,那么这个变量就要占32位,而在64位系统就要占用64位。
例如:
class Point{
public:
Point(...);
~Point();
private:
int x;
int y;
};
注意:
这样一个Point的类包含两个整型,因此一个对象要占64位。但如果把析构函数声明为虚函数,在32位系统里就要多占32位,在64位系统里就要多占64位,那么它所占用的空间直接增大了50%到100%。这样一来,对象就刚好不能用一个64位的寄存器装下了。
另外,别的语言并没有C++这样的函数表指针,不知道怎么处理这个变量,所以就不能把这个对象从C++传到别的语言的程序里了。因此盲目声明虚函数也会给多语言项目带来不必要的麻烦。
错误案例:
class SpecialString : public std::string{...}; //某个继承自标准字符串的类
SpecialString* pss = new SpecialString("ID");
std::string* ps;
...
ps = pss;
delete ps; //使用完后从基类删除内存。未有定义!现实中*ps的SpecialString资源会泄露,因为SpecialString析构函数没有被调用
这样的写法同样会导致一开始讲的内存泄漏,因为标准库的字符串并没有把析构函数定义为虚函数,它们并不是用来拿去继承的,所以不能随便继承,包括STL。虽然C++不像java有final和C#有sealed来阻止某些类被继承的机制,我们也要拒绝这种写法。
抽象类(abstract class):抽象类是包含至少一个纯虚函数的类(pure virtual function),而且它们不能被实例化,只能通过指针来操作,是纯粹被用来当做多态的基类的。
具体类(concrete class):虽然它们都可以通过父类指针来操作子类,但抽象类有更高一层的抽象,从设计的角度上能更好概括某些类的共同特性,比如"狗"相对于"边牧",“柴犬”,“斗牛”,把"狗"当做基类显然要好过把某个品种当做基类。
因为多态的基类需要有虚析构函数,抽象类又需要有纯虚函数,那么在抽象类中就要把析构函数声明为纯虚函数:
class AWSL{
public:
virtual ~AWSL() =0; //"=0"只是一个关键字,用来声明纯虚函数,并不把任何东西设为0
};
同时注意:当在继承层级中某一类的析构函数被调用时,它下一级类的析构函数会被随后调用,最后一直到基类的析构函数,因此作为析构函数调用的终点,要保证有一个定义,否则链接器会报错。
AWSL::~AWSL(){} //基类的析构函数要有一个空的定义
一般来讲,我们使用基类都是为了实现多态,那么这些基类就需要虚的析构函数,比如我们的TimeKeeper类,就可以通过TimeKeeper的指针来操作例如AtomicClock这样的子类。
但并不是所有的基类都是被用来实现多态的,比如我们在上一章讲过的Uncopyable类,单纯只是为了实现某个功能,而不是希望通过它的指针来操作某个对象,那么就不需要将析构函数声明为虚函数。以及某些类就不是用来当做基类的,比如标准库的string类和STL容器类,也不需要将析构函数声明为虚函数。
总结: