最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
1 多态的基类需要虚构函数
class TimeKeeper{ //计时器类,用来当做基类
public:
TimeKeeper(); //这是构造函数
~TimeKeeper(); //这是析构函数
......
};
class AtomicClock : public TimeKeeper{...}; //原子钟是一种计时器
class WaterClock : public TimeKeeper{...}; //水钟也是一种计时器
TimeKeeper* getTimeKeeper(){...} //用来返回一个动态分配的基类对象,工厂类
TimeKeeper* ptk = getTimeKeeper(); //使用这个基类指针操作它的子类,假设这个子类是WaterClock
.....
delete ptk; //使用完毕,释放资源
上述代码的基类没有虚析构函数,直接 delete
基类指针指向的派生类(delete ptk;
),会导致派生类 WaterClock
中的基类成分( 即 TimeKeeper
的成员变量)被释放,而专属于派生类 WaterClock
内的成员变量没被释放,且它的析构函数也未能执行,于是造成了一个诡异的局部销毁对象,这也是造成内存泄漏、败坏数据结构、在调试器上浪费时间的原因之一。
解决方法:给多态的基类的析构函数添加 virtual
关键字
class TimeKeeper{ //计时器类,用来当做基类
public:
TimeKeeper(); //这是构造函数
virtual ~TimeKeeper(); //这是虚析构函数
......
};
此后 delete
基类指针指向的派生类,派生类中的所有成员变量都会被释放。
2 虚函数的工作原理
虚函数是用来在运行时(runtime),自动把编译时未知的对象,比如用户输入的对象,和它所对应的函数绑定起来并调用。当一个类中有虚函数时,编译器会给这个类添加一个隐藏变量 vptr
,即虚函数表指针(virtual table pointer),vptr
指向一个由函数指针构成的数组 vtbl
,即虚函数表(virtual table)。当对象调用某一虚函数时,具体调用哪个函数取决于该对象的 vptr
所指的那个 vtbl
——编译器在其中寻找适当的函数指针。
如果类内有虚函数,其对象的体积会增加,因为 vptr
、vtbl
都需要占用空间的,所有不要无谓声明虚析构函数。
3 不要继承标准库中的类
class SpecialString : public std::string{...}; //某个继承自标准字符串的类
SpecialString* pss = new SpecialString("Hi");
std::string* ps;
...
ps = pss;
delete ps; //使用完后从基类删除内存
这样的写法会导致内存泄漏,因为标准库的字符串并没有把析构函数定义为虚函数,它们并不是用来拿去继承的,所以不能随便继承,包括 STL
。虽然C++不像Java有final和C#有 sealed
来阻止某些类被继承的机制,我们也要拒绝这种写法。
4 抽象类
抽象类是包含至少一个纯虚函数的类,而且它们不能被实例化,只能通过指针来操作,是纯粹被用来当做多态的基类的。
因为多态的基类需要有虚析构函数,抽象类又需要有纯虚函数,那么在抽象类中就要把析构函数声明为纯虚函数,如下述代码
class AWOV{
public:
virtual ~AWOV() =0; //"=0"只是一个关键字,用来声明纯虚函数,并不把任何东西设为0
};
/*
析构函数的调用顺序:首先是调用派生类的析构函数,然后是其每一个基类的析构函数被调用,因此作为析构函数调用的终点,要保证有一个定义,否则链接器会报错。
*/
AWOV::~AWOV(){
//纯虚析构函数的定义
}
Note
- 带多态性质的基类应该声明一个虚析构函数;如果
class
内至少有一个虚函数,应该为它声明虚析构函数- 不是所有基类都需要声明虚析构函数的,如条款06中的
Uncopyable
类,只是为了实现一个功能,而不具有多态性质。