C++中虚函数的诞生,就是为了多态的实现。当子类对父类的虚函数进行了重写,在父类指针调用重写的虚函数时,如果父类指针(或引用)指向了父类的对象,则调用父类的虚函数,如果父类指针(或引用)指向了子类对象,则调用子类的虚函数。
要想了解多态的实现,就必须要知道虚函数表的构成。
【注:文章中代码测试环境为Win7 64位 VS2013】
首先,我们讨论单个含有虚函数的类,即不存在继承关系。
当我们的类中含有虚函数时,类实例化出来的对象,他的成员除了自己的成员变量外,还会多出一个指针。这个指针我们称为虚表指针,他所指向的是我们类对象所维护的虚表。虚表中保存的是类中所有虚函数的地址。代码如下:
class B { public: B() :_val(1){} virtual void fun1() { cout << "void B::fun1()" << endl; } virtual void fun2() { cout << "void B::fun2()" << endl; } void fun3() { cout << "void B::fun3()" << endl; } private: int _val; }; int main() { B b; cout << sizeof(b) << endl;//输出结果为8 system("pause"); return 0; }
代码说明:构造函数给类成员变量赋值为1,方便内存查看,成员函数有两个虚函数,一个非虚函数,断点打在return 0;处。
然后我们切换到监视窗口,<如下>可以发现,对象b实际上维护了两个成员,"__vfptr"和"_val",在内存窗口中"&b",得到的是 0012cc74 ,即 __vfptr,下一个是 00000001,即我们的成员 _val。这也解释了为什么 sizeof(b) 的输出结果是8。不管有多少虚函数,类中只保留了一个虚表指针,添加再多的虚函数,也不会改变sizeof(b)的值
另外,可以看到的是虚表只保存虚函数地址,非虚函数,依旧是属于整个类而非对象。
我们再次查看 __vfptr 指向的空间
前两个是我虚函数的地址,也就是说,他们之间存在关系如下
为了验证,这里我重新封装一个函数指针,通过偏移,看能不能输出cout里面的内容。代码如下:
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { for (int i = 0;/* ppfun[i] != NULL*/i<2; i++) { ppfun[i](); } } void test() { B b; Print_fun((p_fun*)(*(int*)&b)); }
测试是可以输出我们函数内容的。多说一句,虚表中前面保存的都是虚函数的地址,最后结束项在不同编译器下是不一样的,在VS2013环境下,最后一项保存的地址是不可访问的,VS2008环境下,最后是以0x00000000结尾,即是NULL。所以打印函数Print_fun()中for循环条件我做了修改。当然可以更加直接的这样调用函数。
// ((p_fun*)(*(int*)&b))[0](); // ((p_fun*)(*(int*)&b))[1]();
接下来,我们看看包含虚函数重写的单继承中的虚表
这里给出单继承的测试代码
class A { public: A() :_a_val(1){} virtual void fun1() { cout << "void A::fun1()" << endl; } virtual void fun2() { cout << "void A::fun2()" << endl; } protected: int _a_val; }; class B:public A { public: B() :_b_val(2){} virtual void fun1() { cout << "virtual B::fun1()" << endl; } virtual void fun3() { cout << "virtual B::fun3()" << endl; } virtual void fun4() { cout << "virtual B::fun4()" << endl; } protected: int _b_val; }; void test() { B b; }
代码说明:父类 A 包含两个虚函数 fun1() 、fun2(),一个成员变量 _a_val ,构造成员变量为 1 ;子类 B 共有继承了 A ,重写函数 fun1() ,同时添加两个自己的虚函数 fun3()、fun4(),成员变量 _b_val ,构造为 2 。
接下来切换到调试窗口<如下图>,可以看到类B实例化对象 b 实际上维护了三个成员,"__vfptr"、"_a_val"、"_b_val",在内存窗口中“&b”,得到的是0x009bcd48,对应到监视窗口,即 __vfptr ,接下来是0x00000001,0x00000002,即成员变量_a_val,_b_val。如果在这里去sizeof(b),得到的结果应该是12。
接下来查看 __vfptr 指向的空间
监视中看到的是只有两个函数地址,但内存窗口中可以看到,前四个在内存中是在一起的,或者说很近。为了确认,使用刚刚的打印函数,不过需要改变一下循环次数,受编译器的限制,这里只能手动修改循环次数,看最多打印多少次是正常结束,而非程序崩溃。
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { for (int i = 0;/* ppfun[i] != NULL*/i<4; i++) { ppfun[i](); } } void test() { B b; // ((p_fun*)(*(int*)&b))[0](); // ((p_fun*)(*(int*)&b))[1](); // ((p_fun*)(*(int*)&b))[2](); // ((p_fun*)(*(int*)&b))[3](); Print_fun((p_fun*)(*(int*)&b)); }
测试得到,最多可以打印四次,打印结果如下:
*可以看到fun1()函数被子类 B 重写,fun2()函数继承自父类。得到结果监视窗口未显示虚函数fun3()和fun4()地址,但实际上子类新创建的虚函数地址也会保存到虚表当中,而且在单继承过程中,子类的虚函数和父类的虚函数是保存在同一虚表当中,并未对子类的虚函数创建独立的虚表。
即有下图关系:
接下来的多继承中的对象模型
首先给出测试代码,如下
class A { public: A() :_a_val(1){} virtual void test1() { cout << "A::test1()" << endl; } virtual void test2() { cout << "A::test2()" << endl; } protected: int _a_val; }; class B { public: B() :_b_val(2){} virtual void test1() { cout << "B::test1()" << endl; } virtual void test3() { cout << "B::test3()" << endl; } protected: int _b_val; }; class C { public: C() :_c_val(3){} virtual void test1() { cout << "C::test1()" << endl; } virtual void test4() { cout << "C::test4()" << endl; } protected: int _c_val; }; class D:public A,public B,public C { public: D() :_d_val(4){} virtual void test1() { cout << "D::test1()" << endl; } virtual void test5() { cout << "D::test5()" << endl; } protected: int _d_val; }; void test() { D d; }
代码说明:首先创造三个基类,类 A 包含两个虚函数fun1()、fun2(),类包含成员变量 _a_val ,构造为1;类 B 包含两个虚函数 fun1()、fun3(),类包含成员变量 _b_val,构造为2;类 C 包含两个虚函数 fun1()、fun4(),类包含成员变量 _c_val,构造为3;创建第四个类,作为派生类 D ,同时共有继承类A、类B、类C,包含虚函数fun1(),fun2(),fun1()函数对子类中的fun1()进行重写,同时包含成员变量_d_val。
接下来切换到调试窗口<如下图>,可以看到类 D 实例化对象 d 这里维护了七个成员,由于继承了三个类,因此这里有三个虚表指针"__vfptr"、同时包含继承自三个类的成员变量"_a_val"、"_b_val"、"_c_val"和自己本身的成员变量"_d_val"。在内存窗口中“&d”,得到的是0x013bdd04,对应到监视窗口,即继承的第一个类的虚表指针 __vfptr ,接下来是0x00000001,即成员变量_a_val,接下来依次类推,得到第二个类的虚表指针,和继承自第二个类的成员变量,第三个类的虚表指针,和继承自第三个类的成员变量,最后一项是子类 D 的成员变量。如果在这里去 sizeof(b),得到的结果应该是28。
不过这里有个问题,是子类 D 的虚函数地址在哪里。。这里我们打开多个内存窗口,同时把各个虚表指针指向的内容列出来。<如图>
可以看到,尽管监视窗口中,A的虚表指针下只有两项,但对应到内存中却有三项,可以推测,子类单独的虚函数地址是保存在了第一继承子类的虚函数表中,未覆盖的虚函数不会单独创建一块虚函数表。
除此之外,还应该可以看到,子类每继承一个含有虚函数的父类,就会多一个虚表指针,可能会同时维护多个虚表。
换句话说,存在如下图对应关系。
多提一点,子类继承了多个父类,父类虚表的地址不一定是连续的
这里依旧使用函数指针的方式去调用我的成员函数来加以验证。代码如下
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { for (int i = 0; ppfun[i] != NULL; i++) { ppfun[i](); } } void test() { D d; Print_fun((p_fun*)(*(int*)&d)); cout << "-----------------------------------------" <<endl; Print_fun((p_fun*)(*((int*)&d+2))); cout << "-----------------------------------------" << endl; Print_fun((p_fun*)(*((int*)&d+4))); cout << "-----------------------------------------" << endl; }
打印结果如下:
由打印结果可见,子类专有的虚函数test5()的函数地址放在了第一个继承的虚表中,test1()函数均被子类 D 重写。
我们通过虚函数表理解C++ 中的对象模型,了解多态实际上是用虚函数实现覆盖,但通过上面的测试,可以发现,实现多态的同时,无疑会带来效率的下降(通过两次指针解引用才可以访问)。
除此之外应该看到的一点是,多态实现的过程是不安全的,尽管虚函数表的内容我们不能够随意修改,但永远可以被直接访问,这是不安全的一种直接表现。
关于菱形继承的对象模型和菱形虚拟继承的对象模型,会在下一篇中提到。
------------------------------------------muhuizz------------------------------------------
http://11331490.blog.51cto.com
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。