虚函数表的数量与位置:编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。编译器将虚函数表存放在了目标文件或者可执行文件的常量段,即代码区。
虚函数表指针(vptr)的数量与位置:如果1个类中存在一个虚函数,那么第一个地址永远都是指向虚函数列表的指针。子类没有vptr,子类的虚函数存放在第一个父类的虚函数表的最后,如果有覆盖,则覆盖掉相应父类的虚函数。
lass Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; Base b;
虚函数表地址:(int*)(&b)
虚函数表 第一个函数地址,即f()地址: (int*)*(int*)(&b)
g()地址:(int*)*(int*)(&b)+1
h()地址:(int*)*(int*)(&b)+2
注意:在虚函数表的最后有一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束。
一般继承(无虚函数覆盖)
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
对于实例:Derive d; 的虚函数表如下:
一般继承(有虚函数覆盖)
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive(); b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中,子类自己没有虚函数列表的指针。(所谓的第一个父类是按照声明顺序来判断的)
对于子类实例中的虚函数表,是下面这个样子:
多重继承(有虚函数覆盖)
三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。
下面是对于子类实例中的虚函数表的图:
虚析构函数的作用
class A { public: A(){cout << "In A constructor" << endl;} ~A(){cout << "In A destructor" << endl;} }; class B : public A { public: B() { cout << "In B constructor" << endl; m_p = new char[10]; } ~B() { cout << "In B destructor" << endl; if (m_p) delete [] m_p; } private: char *m_p; }; int main(int argc, char* argv[]) { A *p = new B; delete p; return 0; }
输出结果:
In A constructor
In B constructor
In A destructor
并没有调用B的析构函数,new出来的内存没有及时回收造成内存泄漏。解决的方法是将~A()定义为虚析构函数,那么像其它虚构函数一样,~B()重定义(overridden)了~A(),这样指向派生类的指针就能根据运行时的状态调用B的析构函数了。这里又有一个问题:为什么还会调用A的析构函数呢?我只能理解为析构函数是一个特殊的函数,由系统维护其机制。
所以使用虚析构函数的目的是:为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。