通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组(虚函数表)的指针,虚函数表中存储了为类对象进行声明的虚函数的地址,例如:
class Scientist { ... char name[40]; public: virtual void show_name(); virtual void show_all(); }; class Physicist : public Scientist { ... char field[40]; public: void show_all(); //重写父类方法 virtual void show_field(); ... };类Scientist有一个隐藏的数据成员,里面存储了一个指向一张虚函数表的指针,该虚函数表记录了类Scientist定义的虚函数的地址;同理类Physicist也同样有一个隐藏的数据成员,里面存储了一个指向一张虚函数表的指针,该虚函数表记录了类Physicist继承自Scientist的虚函数的地址和自己新定义虚函数的地址。
调用虚函数时,程序将查看存储在对象中的虚函数表地址,然后通过查询虚函数表转向相应的虚函数,所以虚函数相比于非虚函数存在一些缺点(凡是都具有两面性):
每个对象都将增大,增大量为存储地址的空间;对于每个类,编译器都创建一个虚函数地址表(数组);对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。
虚函数注意事项:
在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括派生类派生出来的类)中是虚的;
如果使用指向对象的引用或指针来调用虚方法,程序将使用对象类型定义的方法,而不使用为引用或指针类型定义的方法,即非虚看引用或指针类型,虚看对象类型;
如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的;
构造函数不能是虚函数;
析构函数应当是虚函数,除非类不用做基类
class Singer : public Employee { ... } Employee * pe = new Singer; delete pe;如果使用静态联编 ,delete pe;将根据指针类型调用类Employee的析构函数~Employee(),这样只会释放类Singer从类Employee中继承过来的那一部分,自身的那一部分因为没有调用自身的析构函数而不会被释放;
如果使用动态联编,delete pe;将根据对象类型先调用类Singer的析构函数~Singer(),释放属于类Singer的那一部分,然后再调用类Employee的析构函数~Employee(),释放属于类Employee的那一部分;
友元函数不能是虚函数,因为友元函数不是类成员,而只有成员才能是虚函数,如果因为这个原因引起的设计问题,可以通过让友元函数使用虚成员函数来解决;
如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的;
重新定义将隐藏方法
class Dweling { public: virtual void showperks(int a) const; ... }; class Hovel : public Dwelling { public: virtual void showperks() const; } Hovel trump; trump.showperks(); //valid trump.showperks(5); //invalid新定义将showperks()定义为一个不接受任何参数的函数,重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。
因此这引出了两条经验规则:
如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(注意:这种例外只适应于返回值,而不适用于参数)
class Dweling { public: virtual Dweling & showperks(int a) const; ... }; class Hovel : public Dwelling { public: virtual Hovel & showperks(int a) const; }如果基类声明被重载了,则应在派生类中重新定义所有的基类版本
class Dweling { public: virtual void showperks(int a) const; virtual void showperks(double x) const; virtual void showperks() const; ... }; class Hovel : public Dwelling { public: virtual void showperks(int a) const; virtual void showperks(double x) const; virtual void showperks() const; }如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本。