多态的内幕--(C++, C)语言两个版本
本文通过分析C++编译器生成的汇编代码,分析多态的机制。并实现了一个C语言版本。
在编译性语言里面,多态真的是一个伟大的发明。它可以现在写好代码,编译好,并且可以调用未来的代码。这多少有了点动态的感觉。
很多人,也在脚本语言里面抱怨,为什么不提供多态的功能啊。脚本语言里面,一个函数参数,可以传递任何类型,甚至可以通过函数名的字符串调用函数,
这样多态的作用就小了很多。对于面向对象来说,最重要的两个概念莫过于 继承 和 多态。继承可以减少代码重复,多态可以减少大量的条件判断,if else switch
如果在代码中太多,你的程序应该不怎么面向对象。
废话不说了,先给一个用于分析的程序:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
using namespace std;
class Base
{
public :
virtual void vfun1() {cout << " Base::vfun1() " << endl;}
virtual void vfun2() {cout << " Base::vfun2() " << endl;}
virtual void vfun3() {cout << " Base::vfun3() " << endl;}
};
class Concrete: public Base
{
public :
void vfun1() {cout << " Concrete::vfun1() " << endl;}
void vfun2() {cout << " Concrete::vfun2() " << endl;}
};
void override_demo2(Base & obj)
{
obj.vfun1();
obj.vfun2();
obj.vfun3();
}
typedef long point_t; // 32 位系统 和 64 位系统上 都表示标准指针的长度,但是可能不兼容16位系统,在编译的时候修改一下
typedef void ( * func)();
inline void * getvfptr( void * p, int offset)
{
point_t * q = (point_t * ) * (point_t * )p;
// cout << q[0] << endl
// << q[1] << endl
// << q[2] << endl;
return ( void * )(q[offset]);
}
void override_demo(Base & obj)
{
func f;
f = (func)getvfptr( & obj, 0 );
f();
f = (func)getvfptr( & obj, 1 );
f();
f = (func)getvfptr( & obj, 2 );
f();
}
int main()
{
Base base_obj;
Concrete concrete_obj;
cout << " override_demo: " << endl;
override_demo(base_obj);
override_demo(concrete_obj);
cout << " override_demo2: " << endl;
override_demo2(base_obj);
override_demo2(concrete_obj);
}
这基本上是一个最简单的多态的演示了。我们先来看看 override_demo 这个函数。这个函数没有使用系统使用的多态功能,但是也实现了多态。
通过仔细分析可以发现,这个代码的原理是取出 Base 类的地址,如果,Base 定义了 虚函数,那么会在Base的头部自动插入一个指针,指向虚表数组。
函数调用是通过函数的地址,编译器会在Base类里面插入这个虚表,里面填上按照顺序填上虚函数的地址,在子类中,会复制一份Base的虚表数组,
如果函数被重新定义,那么替换这个虚表中的函数地址,否则就用Base 类里面的地址,在调用虚函数的地方,把obj.vfunc1() 改成 调用虚表中的第一个函数。
这样,即时子类指针转换成了父类指针,但是子类地址指针指向的虚表还是子类的,所以,会调用子类虚表中的第一个函数。
上面的解释太抽象,可以看看汇编的代码:
这是 override_demo2 的汇编代码,很能说明问题:
void override_demo2(Base &obj)
{
00411950 push ebp
00411951 mov ebp,esp
00411953 sub esp,0C0h
00411959 push ebx
0041195A push esi
0041195B push edi
0041195C lea edi,[ebp-0C0h]
00411962 mov ecx,30h
00411967 mov eax,0CCCCCCCCh
0041196C rep stos dword ptr es:[edi]
obj.fun1();
0041196E mov eax,dword ptr [obj] //取出obj的地址,就是getvfptr 中p的值
00411971 mov edx,dword ptr [eax] //取出obj第一个元素的值,也就是 getvfptr 中的 *(ponit_t *)p , 取出指针所指向的地址
00411973 mov esi,esp
00411975 mov ecx,dword ptr [obj]
00411978 mov eax,dword ptr [edx] //取出obj第一个元素的指针,指向的第一个元素,也就是 getvfptr 中的 q[0]
0041197A call eax //调用函数
0041197C cmp esi,esp
0041197E call @ILT+470(__RTC_CheckEsp) (4111DBh)
obj.fun2();
00411983 mov eax,dword ptr [obj]
00411986 mov edx,dword ptr [eax]
00411988 mov esi,esp
0041198A mov ecx,dword ptr [obj]
0041198D mov eax,dword ptr [edx+4] //第二个函数
00411990 call eax
00411992 cmp esi,esp
00411994 call @ILT+470(__RTC_CheckEsp) (4111DBh)
obj.fun3();
00411999 mov eax,dword ptr [obj]
0041199C mov edx,dword ptr [eax]
0041199E mov esi,esp
004119A0 mov ecx,dword ptr [obj]
004119A3 mov eax,dword ptr [edx+8] //第三个函数
004119A6 call eax
004119A8 cmp esi,esp
004119AA call @ILT+470(__RTC_CheckEsp) (4111DBh)
}
这样看来,调用虚函数的代码并不是很高,但是可以发现,虚函数是不可能内联的,因为,调用它必须通过地址。而且,在之类中必须声明为 virtual,
否则,这个函数不会放入虚表中,也就不能产生多态了。
依照这个思路,你可以改造成一个C语言的多态的方法。比如你定义一个基结构,它是一个函数指针列表,然后,定义几个子结构,子结构是和基结构一样排序
的函数指针列表。下面是一个例子:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include < stdlib.h >
typedef void func();
struct Base
{
func * vfun1;
func * vfun2;
func * vfun3;
};
struct Child
{
func * vfun1;
func * vfun2;
func * vfun3;
char * hello;
};
void base_vfunc1()
{
printf( " base_vfunc1\n " );
}
void base_vfunc2()
{
printf( " base_vfunc2\n " );
}
void base_vfunc3()
{
printf( " base_vfunc3\n " );
}
struct Base * init_base()
{
static struct Base base_vtable;
base_vtable.vfun1 = base_vfunc1;
base_vtable.vfun2 = base_vfunc2;
base_vtable.vfun3 = base_vfunc3;
return & base_vtable;
}
void child_vfunc3()
{
printf( " child_vfunc3\n " );
}
struct Child * init_child()
{
struct Child * child;
struct Base * base_vtable;
child = malloc( sizeof ( struct Child));
base_vtable = init_base();
child -> vfun1 = base_vtable -> vfun1;
child -> vfun2 = base_vtable -> vfun2;
child -> vfun3 = child_vfunc3;
child -> hello = " hello world " ;
return child;
}
free_child( struct Child * ch)
{
if (ch) free(ch);
ch = NULL;
}
void override_demo( void * p)
{
struct Base * base ;
base = ( struct Base * )p;
base -> vfun1();
base -> vfun2();
base -> vfun3();
}
int main()
{
struct Child * ch = init_child();
struct Base * base = init_base();
printf( " base\n " );
override_demo( base );
printf( " child\n " );
override_demo(ch);
printf(ch -> hello);
free_child(ch);
}
这样,你写一个函数,可以调用不同的代码了。
当然,可能没有面向对象这样直观了。