虚函数与虚函数表:
1、每个类只有一个虚函数表
如:
// cout<<(int *)*((int *)&bb1)<<endl;
// cout<<(int *)*((int *)&bb2)<<endl;
其中对于// cout<<(int *)*((int *)&bb1)<<endl;
&bb1是取得对象的首地址,因为含有虚函数的对象中的首地址保存的是虚指针的地址,所以&bb1就是虚指针的地址
(int *)&bb1是将虚函数的地址解释为int类型
*((int *)&bb1)是取得虚函数指针所指向的虚函数表的首地址
(int *)*((int *)&bb1)即将这个虚函数表的首地址解释为int类型输出
这样即bb1和bb2 的虚函数表的地址都相等,这个值就是虚函数表所在的位置。
2、含有虚函数的类都拥有一个虚函数表,这个表的表项中存放着所有的虚函数,用一个虚指针来指向这个虚函数表的入口地址,在调用虚函数的时候,先利用虚指针找到虚函数表,确定虚函数的入口地址的位置,然后通过偏移量来获得虚函数的入口地址来完成调用,我们可以通过虚函数的虚指针来获得虚函数表中各个函数的入口地址:
可以看到在一个含有虚函数的类中,对象指向的第一个地址就是虚函数的虚指针的地址,因为虚指针是指向一个类似一维数组的首地址,所以对虚指针解引用之后得到的就是指向第一个函数的地址,最后通过对得到的地址进行强制转换为函数地址即可将函数地址赋值给f函数指针,最后通过函数的调用从而实现对虚函数表中的虚函数进行手动调用。同时可以看到获得的地址和通过内联汇编方式得到的地址是一致的。
通过本例也可看出,对于虚函数表的各个函数的位置的存放,首先存放的是父类的虚函数,该函数的位置按照在类中声明的位置进行排序,对于子类中的虚函数,如果有对父类的虚函数进行覆盖的,就将子类的虚函数替换父类的虚函数,这也就是为什么能够实现动态调用的原因,
Base *b;
b=&d1;
b->f2();
对于d1,首先生成了虚函数表,然后将d1的地址赋值给b,也就是将b中的虚函数表中的f2函数的地址赋值为Derived类中f2的地址,所以实现了动态的调用,如果没有虚函数表,那么就不会出现这样对函数入口地址的覆盖,但是如果用b去调用在Derived中新定义的函数,而Base中没有的函数就不会将原来的函数覆盖,同时如果子类中没有对父类中的函数进行重新定义的话也不会进行覆盖,如:
b->f1();
b->f4();//错误
用b去调用f1,因为子类没有对其进行重新定义,所以调用的f1还是Base的f1,然而对于b去调用f4,因为父类Base中并没有实现对f4的声明,并不是Base的成员,所以出错。
3、多重继承
对于多重继承的含有虚函数的类来说,这个类并没有自己生成一个虚函数表而是用了父类的虚函数表,并且将自己的虚函数加到第一个父类的虚函数表中,如:
得出的结果是
BB:8
B1:8
B2:8
DD:28
即每个父类都含有一个虚函数指针,指向虚函数表的首地址,在DD类中,是由DD类生成一个虚函数表,同时将BB的虚函数加进来,而B1和B2则保留着原来的虚函数表和虚函数指针。
4、不要在构造函数或者析构函数中调用虚函数
最后输出的结果是:
in A
in A
说明并没有发生动态调用。
从这生成B的对象b的内敛汇编中可以看到,首先会先调用A的构造函数然后再生成B的虚函数表,在A的构造函数中先生成A的虚函数表,用this指向虚函数表的首地址,此时并没有生成B的虚函数表,所以此时调用A的show函数
对于析构函数中
首先,在return 0之后,调用B的析构函数
进入B的析构函数之后,首先先释放虚函数表
最后再调用A的析构函数
当进入A的析构函数的时候,B的虚函数表也就不存在了,所以发生函数调用是实调用。
最后再总结一个知识点就是,虚函数表是放在应用程序的常量区,和字符串常量一样。