C++类模型漫谈(三)
系统基于32位,MSVC编译器,VS开发工具
1、上篇直接通过类型对象调用成员函数,这种方式无法实现多态。所谓多态意思对函数的调用呈现出不同的形态。
下面这个例子中 a_ptr为指向a_obj的指针,当调用虚函数时,不再像之前一样直接调用了,而是先从虚函数表获取函数地址间接调用。
而如果是调用非虚函数,没差别,不管是通过函数指针还是对象的方式调用,都是汇编代码直接生成了函数地址调用,效率高。
我们看到拥有虚函数的a_obj对象多出了一个__vfptr指针变量(放在对象首地址处),指向存放函数指针的虚函数表。32位环境下,指针大小通常为4个字节,所以这个时候输出sizeof(a_obj)为12个字节
|
2、类的单继承模式下,多态的实现。TypeB继承了TypeA, b_obj中自然也包含了TypeA子对象,按照先TypeA子对象,再TypeB自己定义的那部分b1、b2。
因为两个类都定义了虚函数,所以无脑的实现方式是,有两个__vfptr,分别指向对应的虚函数表。
TypeB重写了TypeA的TypeA_Method1,所以对应的TypeA子对象的的虚表中函数指针为&TypeB::TypeA_Method1,TypeB自己部分的虚表中存了一个&TypeB::TypeB_Method1。
不过编译器发现可以把两个虚表合在一起,所以实际情况只会出现一个虚表,一个__vfptr。
|
原来TypeA中的TypeA_Method1函数,被TypeB类中重写了,这个时候虚表存的是&TypeB::TypeA_Method1 a_ptr->TypeA_Method1() 查找__vfptr中虚表中的TypeA_Method1,得到&TypeB::TypeA_Method1 b_ptr->TypeA_Method1() 查找__vfptr中虚表中的TypeA_Method1,得到&TypeB::TypeA_Method1 b_ptr->TypeA_Method2() 查找__vfptr中虚表中的TypeA_Method2,得到&TypeA::TypeA_Method2 b_ptr->TypeB_Method1() 查找__vfptr中虚表中的TypeB_Method1,得到&TypeB::TypeB_Method1 如果a_ptr->TypeB_Method1()呢? 虽然此时看到好像虚表中也包含了TypeB_Method1, 但是该函数是属于TypeB类型中定义的,我们可以看到合并之前 并不存在于TypeA那部分的虚表中,所以这个时候调用是提示不存在的。
|
3、上面的例子是两个虚表进行了合并,现在设计一个合并不了的,让TypeC继承TypeA和TypeB。
c_obj中包含了TypeA子对象和TypeB子对象,按照顺序排下来,最后包含了TypeC类自己定义的c1、c2。
因为这三个类,都有自己的虚函数,所以正常无脑的做法,可以给每个类都安插一个__vfptr, 指向对应的虚表。
同样道理TypeA子对象部分其实就是c_obj的起始地址,所以把这两部分的虚函数合并一起,而TypeB子对象部分无法合并,单独一个虚表
所以实际c_obj内存中会出现两个__vfptr 对应两张虚表。
总结:不管是完整对象、或者是子对象,都会尝试把各自的__vfptr放到各自内存区域的起始地址处。因为TypeA子对象和TypeC对象起始地址相同,而__vfptr都想放在各自的起始位置处,所以直接合并用一个__vfptr,两个子虚表也合并一起了
可以想像成两个__vfptr上下重叠一起,对应的虚表也是重叠在一起。而TypeB子对象__vfptr也放在了起始位置,即&c_obj的值+12位置处。
|
TypeB* b_ptr=&c_obj c_obj的起始地址为0,而TypeB子对象部分的起始地址为12,所以这句代码的结果是编译器直接通过c_ptr+12的方式,得到的地址作为b_ptr指针的值。b_ptr指向了地址12开始的12-23空间范围的Type子对象。b_ptr->TypeB_Method() 编译器检测到TypeB_Method函数是TypeB类型中定义的,而这个时候b_ptr也是指向TypeB子对象,所以直接从12地址的__vfptr获取虚表地址,再从对应虚表中查到&TypeC::TypeB_Method1函数指针调用。这里是TypeC重写后的函数,调用该函数this需要c_obj对象的指针,但此时的b_ptr是指向TypeB子对象,因此需调调整。编译器有时候会生成一个thunk函数,函数里面先执行this+=12,用以调整this指针,再跳转到&TypeC::TypeB_Method1执行。因此这种情况下虚表里存的是&thunk指针。c_ptr->TypeB_Method() 编译器检测到TypeB_Method函数是TypeB类型中定义的,而此时c_ptr是指向TypeC对象,所以只能通过c_ptr+12的方式定位到TypeB子对象处,从定位到的地址拿到__vfptr,再找到对应的虚函数&TypeC::TypeB_Method1指针调用,传入c_ptr当做this指针。
|