在C++中使用虚函数的过程中,进行了小小的总结,大概要注意到以下的几个方面吧。
一、包含虚函数的类指针列表会增大
让我们先来看一段程序:
#include <iostream> using namespace std; class A{ public: void aa(){} void bb(){} }; int main() { cout << sizeof(A) << endl; return 0; }
代码运行结果为:
程序的运行结果为1,这个结果不出我们的意料。在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址。
我们再将上述程序进行如下修改:
程序的运行结果变为了4。这是因为包含虚函数的类都有一个一维的虚函数表叫作虚表,这个类的每个对象都包含一个指向这个虚表首地址的虚指针。所以导致了包含虚函数类的指针列表增大。
二、虚析构函数
析构函数的作用是在对象撤销之前做必要的“清理现场”的工作。当派生类的对象从内存中撤销的时候,会先先调用派生类的析构函数然后再调用基类的析构函数。当我们new一个临时对象时,若基类中包含析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生什么样的情况呢?我们来看看下述程序的运行:
#include <iostream> using namespace std; class A{ public: ~A(){ cout << "based class end;" << endl; } }; class AA :public A{ ~AA(){ cout << "derived class end;" << endl; } }; int main() { A* a = new AA; delete a; return 0; }
程序运行结果:
程序结果表明,delete撤销对象时只调用了基类的析构函数,而没有执行派生类的析构函数。对此,我们将基类的析构函数声明为虚函数。代码如下:
#include <iostream> using namespace std; class A{ public: virtual ~A(){ cout << "based class end;" << endl; } }; class AA :public A{ public: ~AA(){ cout << "derived class end;" << endl; } }; int main() { A* a = new AA; delete a; return 0; }
程序运行结果:
正如我们所愿,delete在撤销对象时,不仅调用了基类的析构函数而且调用了派生类的析构函数。当基类中的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不同。所以,我们要为多态基类声明virtual析构函数。
三、构造函数不能声明为虚函数
构造函数不能声明为虚函数。如果声明为虚函数,编译器会自动报出:
四、不在析构或者构造过程中调用虚函数
在析构函数或者是构造函数中,我们绝对不能调用虚函数。即使,我们在构造函数或者析构函数中调用虚函数,也不会下降至派生类中调用函数。我们来看如下代码:
class A{ public: int a; A(){ cout << b() << endl; } virtual int b() const=0; virtual int c() const=0; virtual ~A(){ cout << c() << endl; } }; class AA :public A{ public: AA() :A(){} int b(){ return 11; } int c(){ return 22; } }; int main() { AA a; return 0; }
上述程序在编译阶段即会报错。AA a;语句执行时,AA构造函数会被先调用,但是A的构造函数会被更早的调用。A构造函数会调用虚函数b(),也正是这里产生编译问题。这个时候,A构造函数调用的A类中的成员函数b(),而不是派生类AA中的b()。因此,基类构造期间,虚函数不会下降到派生类层中。我们可以说:在基类构造期间,virtual函数不是virtual函数。
要做到绝不在构造和析构过程中调用virtual函数。