一、虚函数表指针位置分析
一个类若有虚函数,这个类就会产生一个虚函数表。当类创建对象的时候,对象内就会维护一个虚函数表指针,该指针(vptr)会指向这个虚函数表的开始地址。接下来借助代码分析一下虚函数表指针的位置。
1 | class A |
通过测试,我们发现虚函数表指针在对象内存的开头处。
二、继承关系作用下虚函数的手工调用
1 | //父类 |
从上面的运行结果来看(注释中已标出),对于子类对象来说,子类中要是有和父类同名的虚函数,则子类中的虚函数就会把父类中的同名虚函数覆盖,同时虚函数表中对应的虚函数地址必然会发生变化(变成子类中虚函数的地址)。但是如果是父类对象,则还是会调用父类中的虚函数(即不会被覆盖)。
接着看下面的代码:
1 | typedef void(*Func)(void); //定义一个函数指针类型 |
可以看出,当用子类对象初始化父类对象的时候,编译器给咱们做了一个选择,显然derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值。
总结:
(1)一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同。
(2)父类中有虚函数就等于子类中有虚函数。话句话来说,父类中有虚函数表,则子类中肯定有虚函数表。因为你是继承父类的。并且只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。但如果子类只继承自一个父类,则不管是父类还是子类,它们内部都只会有一个虚函数表。
(3)如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同。但,仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。
(4)虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),则该表项所执行的该函数的地址应该相同。
(5)超出虚函数表部分的内容不可知。
三、多重继承虚函数表分析
1 | //基类1 |
(1)一个对象,如果它的类有多个基类则有多个虚函数表指针(注意是多个虚函数表指针,而不是多个虚函数表)。
(2)在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr);
(3)如上程序中,子类对象ins有两个虚函数表指针,vptr1,vptr2。类Derived有两个虚函数表,因为它继承自两个基类。
(4)子类和第一个基类公用一个vptr(因为vptr指向一个虚函数表,所以也可以说子类和第一个基类共用一个虚函数表vtbl),因为我们注意到了类Derived的虚函数表1里边的5个函数,而g()正好是base1里边的函数。
(5)子类中的虚函数覆盖了父类中的同名虚函数。比如derived::f(),derived::i();
四、vptr、vtbl创建时机
(1)vptr(虚函数表指针)跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来。即运行的时候。实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加为vptr赋值的代码,这是在编译期间编译器为构造函数增加的。当程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了。
(2)虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容。然后也是在编译期间在相应的类构造函数中添加给vptr赋值的代码,这样程序运行的时候,当运行到创建类对象的代码时,会调用类的构造函数,执行到类的构造函数中的给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了。
五、普通类包含虚函数时引发的虚函数调用问题
1 | class X |
(1)如果一个普通类中包含了虚函数,那么在构造函数中使用如上所示的memset或者拷贝构造函数中使用如上所示的memcpy方法,那么就会出现程序崩溃的情形。
(2)某些情况下,编译器会往类内部增加一些我们看不见但真实存在的成员变量(隐藏成员变量),比如你类中增加了虚函数,系统默认往类对象中增加虚函数表指针,这个虚函数表指针就是隐藏的成员变量。有了这种变量的类,就不是普通的类了。同时,这种隐藏的成员变量的增加(使用)或者赋值的时机,往往都是在执行构造函数或者拷贝构造函数的函数体之前进行。那么你如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了。
(3)静态联编是指:我们编译的时候就能确定调用哪个函数。把调用语句和被调用函数绑定到一起。动态联编是在程序运行时,根据时机情况,动态的把调用语句和被调用函数绑定到一起,动态联编一般旨有在多态和虚函数情况下才存在。
(4)对多态,虚函数,父类,子类。虚函数主要解决的问题父类指针指向子类对象这种情况。如果一个类中只有虚函数,没有继承,那么虚函数和普通函数没有区别,就算虚函数表指针被置空,仍然可通过对象正常调用,因为这是静态联编,不是多态。但是如果用new出来的对象调用,就会失败,因为虚函数表指针为空,找不到虚函数表。