虚函数表的数量与位置:编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。编译器将虚函数表存放在了目标文件或者可执行文件的常量段,即代码区。
虚函数表指针(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
注意:在虚函数表的最后有一个结点,这是虚函数表的结束结点,就像字符串的结束符“ ”一样,其标志了虚函数表的结束。
一般继承(无虚函数覆盖)
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的析构函数呢?我只能理解为析构函数是一个特殊的函数,由系统维护其机制。
所以使用虚析构函数的目的是:为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
构造函数不能声明为虚函数,原因是:
virtual" allows us to call afunction knowing only an interfaces and not the exact type of theobject. To create an object you need complete information. Inparticular, you need to know the exact type of what you want tocreate. Consequently, a "call to a constructor" cannot bevirtual.