多态的内幕–(C++, C)语言两个版本

本文通过分析C++编译器生成的汇编代码,分析多态的机制。并实现了一个C语言版本。

在编译性语言里面,多态真的是一个伟大的发明。它可以现在写好代码,编译好,并且可以调用未来的代码。这多少有了点动态的感觉。

很多人,也在脚本语言里面抱怨,为什么不提供多态的功能啊。脚本语言里面,一个函数参数,可以传递任何类型,甚至可以通过函数名的字符串调用函数,

这样多态的作用就小了很多。对于面向对象来说,最重要的两个概念莫过于 继承 和 多态。继承可以减少代码重复,多态可以减少大量的条件判断,if else switch

如果在代码中太多,你的程序应该不怎么面向对象。

废话不说了,先给一个用于分析的程序:

多态的内幕--(C++, C)语言两个版本多态的内幕--(C++, C)语言两个版本代码#include<iostream>

usingnamespacestd;



classBase

{

public:

virtualvoidvfun1() {cout<<"Base::vfun1()"<<endl;}

virtualvoidvfun2() {cout<<"Base::vfun2()"<<endl;}

virtualvoidvfun3() {cout<<"Base::vfun3()"<<endl;}

};



classConcrete:publicBase

{

public:

voidvfun1() {cout<<"Concrete::vfun1()"<<endl;}

voidvfun2() {cout<<"Concrete::vfun2()"<<endl;}

};



voidoverride_demo2(Base&obj)

{

obj.vfun1();

obj.vfun2();

obj.vfun3();

}



typedef
longpoint_t;//32 位系统 和 64 位系统上 都表示标准指针的长度,但是可能不兼容16位系统,在编译的时候修改一下

typedefvoid(func)();



inline
void
getvfptr(voidp,intoffset)

{

point_t
q=(point_t)(point_t)p;

//cout << q[0] << endl

//<< q[1] << endl

//<< q[2] << endl;

return(void
)(q[offset]);

}



voidoverride_demo(Base&obj)

{

func f;

f
=(func)getvfptr(&obj,0);

f();

f
=(func)getvfptr(&obj,1);

f();

f
=(func)getvfptr(&obj,2);

f();

}



intmain()

{

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语言的多态的方法。比如你定义一个基结构,它是一个函数指针列表,然后,定义几个子结构,子结构是和基结构一样排序

的函数指针列表。下面是一个例子:
多态的内幕--(C++, C)语言两个版本多态的内幕--(C++, C)语言两个版本代码#include<stdio.h>

#include
<stdlib.h>



typedef
voidfunc();

structBase

{

func
vfun1;

func
vfun2;

func
vfun3;

};



structChild

{

func
vfun1;

func
vfun2;

func
vfun3;

charhello;

};





voidbase_vfunc1()

{

printf(
"base_vfunc1n");

}



voidbase_vfunc2()

{

printf(
"base_vfunc2n");

}



voidbase_vfunc3()

{

printf(
"base_vfunc3n");

}



structBase
init_base()

{

staticstructBase base_vtable;

base_vtable.vfun1
=base_vfunc1;

base_vtable.vfun2
=base_vfunc2;

base_vtable.vfun3
=base_vfunc3;

return&base_vtable;

}



voidchild_vfunc3()

{

printf(
"child_vfunc3n");

}

structChildinit_child()

{

structChild
child;

structBasebase_vtable;

child
=malloc(sizeof(structChild));

base_vtable
=init_base();

child
->vfun1=base_vtable->vfun1;

child
->vfun2=base_vtable->vfun2;

child
->vfun3=child_vfunc3;

child
->hello="hello world";

returnchild;

}



free_child(
structChild
ch)

{

if(ch) free(ch);

ch
=NULL;

}



voidoverride_demo(voidp)

{

structBase
base;

base=(structBase)p;

base->vfun1();

base->vfun2();

base->vfun3();

}



intmain()

{

structChild
ch=init_child();

structBase*base=init_base();

printf(
"basen");

override_demo(
base);

printf(
"childn");

override_demo(ch);

printf(ch
->hello);

free_child(ch);

}

这样,你写一个函数,可以调用不同的代码了。

当然,可能没有面向对象这样直观了。

原文链接: https://www.cnblogs.com/niniwzw/archive/2011/01/20/1940159.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/20277

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月7日 下午9:44
下一篇 2023年2月7日 下午9:47

相关推荐