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