本文共 12144 字,大约阅读时间需要 40 分钟。
C++的虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖(override)的问题,保证其能真实的反应实际的函数。这样,在有虚函数的类的实例中这张表被分配在了这个实例的内存中,所以当我们用父类的指针操作一个子类的时候,这张虚函数表就显得尤为重要了,他就像一个地图一样,指明了实际所应该调用的函数。
说明:虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数 亦或 是 纯虚函数,亦或是 派生类中隐式声明的这些虚函数都会 生成这张虚函数表。
虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。
首先了解下这张虚函数表:虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。
1、无继承情况
#includeusing namespace std;class Base{public: Base(){cout<<"Base construct"<
注意:在上面这个图中,虚函数表中最后一个节点相当于字符串的结束符,其标志了虚函数表的结束,在Codeblocks下打印为0。
Base::~Base()在Base::h()后边
2、继承,无虚函数覆盖的情形
#includeusing namespace std;class Base {public: virtual void f() { cout << "Base::f()" << endl; } virtual void g() { cout << "Base::g()" << endl; } virtual void h() { cout << "Base::h()" << endl; }};class Derive: public Base { virtual void f1() { cout << "Derive::f1()" << endl; } virtual void g1() { cout << "Derive::g1()" << endl; } virtual void h1() { cout << "Derive::h1()" << endl; }};int main(){ typedef void (*Fun)(); Base *b = new Derive; cout << *(int*)b << endl; Fun funf = (Fun)(*(int*)*(int*)b); Fun fung = (Fun)(*((int*)*(int*)b + 1)); Fun funh = (Fun)(*((int*)*(int*)b + 2)); Fun funf1 = (Fun)(*((int*)*(int*)b + 3)); Fun fung1 = (Fun)(*((int*)*(int*)b + 4)); Fun funh1 = (Fun)(*((int*)*(int*)b + 5)); funf(); // Base::f() fung(); // Base::g() funh(); // Base::h() funf1(); // Derive::f1() fung1(); // Derive::g1() funh1(); // Derive::h1() cout << (Fun)(*((int*)*(int*)b + 6)); return 0;}
从表上可以看出
1、虚函数按照声明的顺序放在表中。
2、父类的虚函数在子类的虚函数前面。
3. 继承,虚函数覆盖的情形
#includeusing namespace std;class Base {public: virtual void f() { cout << "Base::f()" << endl; } virtual void g() { cout << "Base::g()" << endl; } virtual void h() { cout << "Base::h()" << endl; }};class Derive: public Base { virtual void f() { cout << "Derive::f()" << endl; } virtual void g1() { cout << "Derive::g1()" << endl; } virtual void h1() { cout << "Derive::h1()" << endl; }};int main(){ typedef void (*Fun)(); Base *b = new Derive; cout << *(int*)b << endl; Fun funf = (Fun)(*(int*)*(int*)b); Fun fung = (Fun)(*((int*)*(int*)b + 1)); Fun funh = (Fun)(*((int*)*(int*)b + 2)); Fun fung1 = (Fun)(*((int*)*(int*)b + 3)); Fun funh1 = (Fun)(*((int*)*(int*)b + 4)); funf(); // Derive::f() fung(); // Base::g() funh(); // Base::h() fung1(); // Derive::g1() funh1(); // Derive::h1() cout << (Fun)(*((int*)*(int*)b + 5)); return 0;}
从表上可以看出:
1、覆盖的 f() 函数被放到虚函数表中原来父类虚函数的位置。
2、没有被覆盖的函数依旧。
3、可通过获取成员函数指针来调用成员函数(即时是private类型的成员函数),这就出现一定的安全问题。
4、多继承情况
#includeusing namespace std;class Base1 {public: virtual void f() { cout << "Base1::f()" << endl; } virtual void g() { cout << "Base1::g()" << endl; } virtual void h() { cout << "Base1::h()" << endl; }};class Base2 {public: virtual void f() { cout << "Base2::f()" << endl; } virtual void g() { cout << "Base2::g()" << endl; } virtual void h() { cout << "Base2::h()" << endl; }};class Base3 {public: virtual void f() { cout << "Base3::f()" << endl; } virtual void g() { cout << "Base3::g()" << endl; } virtual void h() { cout << "Base3::h()" << endl; }};class Derive: public Base1,public Base2, public Base3 { virtual void f() { cout << "Derive::f()" << endl; } virtual void g1() { cout << "Derive::g1()" << endl; }};int main(){ typedef void (*Fun)(); Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g() Fun b1fun = (Fun)(*(int*)*(int*)b1); Fun b2fun = (Fun)(*(int*)*((int*)b1+1)); Fun b3fun = (Fun)(*(int*)*((int*)b1+2)); b1fun(); // Derive::f() b2fun(); // Derive::f() b3fun(); // Derive::f() return 0;}
从表上可以看出:
1、每个父类都有自己的虚函数表。
2、子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明的顺序来确定的)
3、对于多继承无虚函数覆盖的情况,布局与上图类似(Derive的位置对应Base)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
感谢博主:
一般来说,对于开发者我们只需要知道虚函数的使用方法,以及虚函数表的存在即可。但面试时往往会遇到更细节的问题,比如让你实现一个虚函数机制,虽然不太实用,总归了解些底层知识也是件好事。但如果有人苦苦相逼一定要拿这个刷人,你就去骂他吧,你才是写编译器的,你们全家都是写编译器的。唉,我有些失态了...
1. 虚函数与虚函数表基本知识
这里有一篇介绍,只需看前两页,各种配图,很形象:
这篇文章则更精练,只需看第一段就好:
总的来说,每一个拥有virtual function的类实例化对象时,都会额外申请一块内存存储虚函数表存储所有虚函数地址,并在对象某个位置存储一个vptr指针指向该表起始地址。这个指针具体放在什么位置,虚函数表怎么组织,怎么索引各个虚函数,这些都是编译器在编译期间决定的,在不同编译环境下不见得相同。
2. 多态子类的调用顺序 -- 为什么不要在构造函数中调用虚函数
原因是,在子类的构造函数执行时,虚函数表还没有被子类覆盖,换句话说,此时调用的函数是当前类的函数,虚函数机制在构造函数中无法触发。其原因在于子类构造时各个初始化步骤的调用顺序:
全部推演过程见此:
直接摘录构造顺序:
1.构造子类构造函数的参数
2.子类调用基类构造函数
3.基类设置vptr
4.基类初始化列表内容进行构造
5.基类函数体调用
6.子类设置vptr
7.子类初始化列表内容进行构造
8.子类构造函数体调用
(注意一点,初始化列表内的数据不按书写顺序,而是按类内部的定义顺序)
析构的顺序恰好相反,所以也不要在析构函数中调用虚函数,那样也是没有意义的。
3. 如何去验证虚函数表的存在
其实在第一个链接里已经有了示例程序。
如果你看不懂函数指针,请看这里:
4. 为什么构造函数不能是虚函数
1、从设计理念上说,构造函数不需要是虚函数;
2、从当前vptr的实现机制上说,无法实现虚的构造函数。
详细可见这里:
原文:
原文: