C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例。然后通过父类的指针调用是实际子类的成员函数。这种技术可以让父类的指针有多种形态。

虚函数

虚函数通过 virtual关键字来声明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class CPerson{
public:
    virtual void hello(){
        cout<<"I'm a person"<<endl;
    }
};
class CMan: public CPerson{
public:
    // 子类中不必声明virtual
    void hello(){
        cout<<"I'm a man"<<endl;
    }
};
CPerson *p = new CMan();
p->hello();
// I'm a man

上述代码中,通过基类指针调用虚函数时,子类的同名函数得到了执行。多态在C++中有三种形态:

  1. 通过基类指针调用基类和子类的同名虚函数时,会调用对象的实际类型中的虚函数。
  2. 通过基类引用调用基类和子类的同名虚函数时,会调用对象的实际类型中的虚函数。
  3. 基类或子类的成员函数中调用基类和子类的同名虚函数,会调用对象的实际类型中的虚函数。

虚函数表

纯虚函数

虚函数的声明以 =0结束,便可将它声明为纯虚函数。包含纯虚函数的类不允许实例化,称为抽象类。 事实上纯虚函数提供了面向对象中接口的功能。当然,这样的接口是以继承的方式实现的。

class CPerson{
public:
    virtual void hello() = 0;
};
CPerson p;  // compile error

注意空方法、纯虚函数、方法声明的区别。类声明中的空方法给出了方法声明+方法定义。 只声明但没有定义的方法将会产生链接错,无论是否被调用过。

class CPerson{
public:
    void empty(){};
    void declare();
};
CPerson::declare(){
    // ...
};

访问级别

虚函数的调用会在运行时动态匹配当前类型,然而成员函数的访问性检查是语法检查的一部分,在编译期完成。 如果虚函数在父类中是Private,即使在子类中是Public,也不可以通过父类指针调用它:

class CPerson{
    virtual void hello(); 
};
class CMan: public CPerson{
public:
    virtual void hello(); 
};

CPerson* p = new CMan;
p->hello(); // 编译错

虚析构函数

虚函数的机制使得我们可以通过更加通用的基类指针来操作对象。然而使用基类指针来 delete对象则面临着问题:

CPerson *p = new CMan();
delete p;

上述代码只会回收 CManCPerson部分所占用的内存,执行了 CPerson的析构函数,却没有执行 CMan的虚构函数。 解决办法很直观:将析构函数设为 virtual。更多讨论见Effective C++: Item 7

构造函数不允许是虚函数,编译错。

class CPerson{
public: 
    virtual ~CPerson(){};
};
class CMan: public CPerson{
public:
    ~CMan(){}; 
};
CPerson *p = new CMan();
delete p;

这样,delete时会先调用 ~CMan()在调用 ~CPerson()

构造函数调用虚函数

当执行构造函数时,当前对象的类型为构造函数所属在的类。 所以在构造函数中调用虚函数和调用普通函数是一样的,不会动态联编, 被调用的函数来自自己或者基类。

class CPerson{
public:
    virtual void hello(){
        cout<<"I'm a person"<<endl;
    }
    virtual void bye(){
        cout<<"Bye, person"<<endl;
    }
};
class CMan: public CPerson{
public:
    CMan(){
        hello();
        bye();
    }
    void hello(){
        cout<<"I'm a man"<<endl;
    }
};
class CReek: public CMan{
public:
    void hello(){
        cout<<"I'm a reek"<<endl;
    }
    void bye(){
        cout<<"Bye, reek"<<endl;
    }
};

int main(){
    CReek r;
    return 0;
}

上述的调用结果是:

I'm a man
Bye, person

hellobye都是虚函数,其中 hello三个层级都有定义,但被执行的是当前类 CMan中的定义; bye在上下两个层级有定义,被执行的是上一级类 CPerson中的定义。 可见,构造函数执行时当前对象的类型是定义构造函数的类。

问题

  1. C++的构造函数可以为虚函数吗? 不能,因为C++对象的虚函数表是在构造函数中初始化的,如果连构造函数都是虚函数了,这就是一个鸡生蛋蛋生鸡的问题。这个在编译时酒不能通过。
  2. 为什么C++的析构函数一般为虚函数? 为了解决用基类指针指向派生类对象时,并用基类的指针删除派生类对象。如果不声明为虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。

Reference