虚函数的主要功能是用来实现C++的多态性,C++有两种多态性:
1. 编译时多态:通过函数重载实现;
2. 运行时多态:通过虚函数实现;
一个类存在虚函数,那么编译器就会为这个类生成一个虚表,虚表里存放的是这个类所有虚函数的地址。当生成类对象(实例化)的时候,编译器会自动将类对象的前四个字节设置为虚表地址,这四个字节就可以看作是一个指向虚表的指针(虚指针)。
虚函数的调用过程:this指针 -> 虚指针(vptr) -> 虚函数表(vtbl) -> 虚函数。
虚函数是为了实现动态联编,目的是通过基类类型指针指向不同对象时,自动调用相应的,和基类同名的函数。
只有当我们删除的是一个指向派生类对象的基类指针时,才需要虚析构函数。
哪些函数不能是虚函数?
- 构造函数
首先, 虚表是存储在类对象的内存空间中,若构造函数为虚,就需要通过虚表来调用构造函数,然而此时对象还没有实例化,即没有分配内存空间,也就不存在虚表,故矛盾;其次,如果构造函数是虚函数,那么在创建派生类对象时,将调用派生类的构造函数而不是基类的构造函数,然后派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。 - 内联函数
内联函数是在编译阶段进行函数体的替换操作,而虚函数是在运行期间才进行类型确定; - 静态函数
静态函数不属于任何对象,没有this指针; - 友元函数
友元函数不属于类的成员函数,不能被继承; - 普通函数
普通函数不属于类的成员函数,不能被继承且没有this指针。
Effective C++中提到:
-
如果class不含virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当作base class,令其析构函数为virtual往往是个馊主意;
-
无端地将所有classes的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。
C++ Primer Plus中提到:
BrassPlus ophelia; // derived-class object
Brass * bp; // base-class pointer
bp = &ophelia; // Brass pointer to BrassPlus Object
bp->ViewAcct(); // which version?
在上述代码中,如果在基类中没有将
ViewAcct()
声明为虚的,则bp->ViewAcct()
将调用Brass::ViewAcct()
,指针类型在编译时已知,可以将ViewAcct()
关联到Brass::ViewAcct()
。总之,编译器对非虚方法使用静态联编。
然而,如果在基类中将ViewAcct()
声明为虚的,则bp->ViewAcct()
根据对象类型(BrassPlus)调用BrassPlus::ViewAcct()
,编译器生成的代码将在程序执行时,根据对象类型将ViewAcct()
关联到Brass::ViewAcct()
或BrassPlus::ViewAcct()
。总之,编译器对虚方法使用动态联编。
第四行代码的操作就叫:基类指针指向派生类对象。
总结:
-
如果基类与派生类都定义了同名函数(非虚函数),那么通过对象指针调用成员函数时,到底调用哪个函数要根据指针的类型(基类指针或派生类指针)来确定,而不是根据指针实际指向的对象类型来确定;
-
如果基类和派生类定义了同名函数(虚函数),那么通过对象指针调用成员函数时,到底调用哪个函数要根据指针实际指向的对象类型(基类对象或派生类对象)来确定,而不是根据指针的类型来确定。