搜索
bottom↓
回复: 31

MCU C++/CPP虚函数

[复制链接]

出0入0汤圆

发表于 2012-4-7 16:53:21 | 显示全部楼层 |阅读模式
本帖最后由 uc_cpp 于 2012-4-7 17:00 编辑

好像提到虚函数,大家就会联想到效率低。

我却是不明白这两个概念为何联系在一起。

C++虚函数,其实与C语言里面的函数指针数组类似。

具有虚函数的类,在构造对象的时候,会将虚函数表指针指向 正确 的虚函数表。
在调用虚函数的时候,通过这个虚函数表来运行 对应的函数。

因此,类的对象一旦创建好,其指向的虚函数表也就固定。
虚函数运行时多态是在构造时确定的。

不过,其实有办法,在对象已经构造完毕后,把对象的虚函数表指针的值偷换掉,
某种情况下,虚函数表指针可以当做 指向类成员函数的指针来用,不过这种用法一定要小心。

另外,关于虚函数调用的效率问题,
虚函数的调用相当于 C里面通过函数指针表来调用函数,效率也不算太低。
在C里面,通过函数指针表来调用函数也是很常见的,

不过虚函数 会增加代码量,主要是构造函数初始化虚函数表指针,另外还有虚函数表也要占用空间,
虚函数调用也会额外多一些指令,主要是通过查表来运行函数,比一般的成员函数调用会多费些指令。

出0入0汤圆

 楼主| 发表于 2012-4-7 17:41:40 | 显示全部楼层
  1. #include <iostream>

  2. class V
  3. {
  4. public:
  5.   virtual void vfunc(void) =0;       //纯虚类
  6. };


  7. class A0:public V
  8. {
  9. public :
  10.   int X;
  11.   A0():X(0)
  12.   {
  13.   }
  14.   virtual void vfunc(void)
  15.   {
  16.      X=0xa0;
  17.      cout<<"vfunc in A0"<<endl;
  18.   }
  19. };

  20. class A1:public A0
  21. {
  22. public :
  23.   virtual void vfunc(void)
  24.   {
  25.      X=0xa1;
  26.      cout<<"vfunc in A1"<<endl;
  27.   }
  28. };

  29. class B0:public V
  30. {
  31. public :
  32.   int Y;
  33.   B0():Y(0)
  34.   {
  35.   }
  36.   virtual void vfunc(void)
  37.   {
  38.     Y=0xb0;
  39.     cout<<"vfunc in B0"<<endl;
  40.   }
  41. };

  42. class B1:public B0
  43. {
  44. public :
  45.   virtual void vfunc(void)
  46.   {
  47.     Y=0xb1;
  48.     cout<<"vfunc in B1"<<endl;
  49.   }
  50. };

  51. A0 a0;
  52. A1 a1;
  53. B0 b0;
  54. B1 b1;


  55. int main(void)
  56. {
  57. //---------------------------------------------
  58. //直接运行虚函数
  59.    a0.vfunc();  //vfunc in A0
  60.    a1.vfunc();  //vfunc in A1
  61.    b0.vfunc();  //vfunc in B0
  62.    b1.vfunc();  //vfunc in B1

  63. //---------------------------------------------
  64. //强制转换成基类,运行虚函数   
  65.    (*(V *)(void *)&a0).vfunc();    //vfunc in A0   
  66.    (*(V *)(void *)&a1).vfunc();    //vfunc in A1
  67.    (*(A0 *)(void *)&a1).vfunc();   //vfunc in A1
  68.    
  69.    (*(V *)(void *)&b0).vfunc();    //vfunc in B0
  70.    (*(V *)(void *)&b1).vfunc();    //vfunc in B1  
  71.    (*(B0 *)(void *)&b1).vfunc();   //vfunc in B1
  72.    
  73. //---------------------------------------------
  74. //交换a0,b0函数表,通过基类V运行虚函数
  75.    void * temp_addr;              //局部变量,存储虚函数表地址
  76.    temp_addr=*(void **)&a0;
  77.    *(void **)&a0=*(void **)&b0;   
  78.    *(void **)&b0=temp_addr;      

  79.    (*(V *)(void *)&a0).vfunc();    //vfunc in B0
  80.    (*(V *)(void *)&b0).vfunc();    //vfunc in A0

  81. //----------------------------------------------------------------------
  82. //a0,b0虚函数表还原
  83.    temp_addr=*(void **)&a0;
  84.    *(void **)&a0=*(void **)&b0;   
  85.    *(void **)&b0=temp_addr;      
  86.    
  87. //---------------------------------------------
  88. //交换a0,a1虚函数表,通过基类V运行虚函数
  89.    temp_addr=*(void **)&a0;
  90.    *(void **)&a0=*(void **)&a1;   
  91.    *(void **)&a1=temp_addr;      

  92.    (*(V *)(void *)&a0).vfunc();    //vfunc in A1
  93.    (*(V *)(void *)&a1).vfunc();    //vfunc in A0

  94. //----------------------------------------------------------------------
  95. //a0,a1虚函数表还原   
  96.     temp_addr=*(void **)&a0;
  97.    *(void **)&a0=*(void **)&a1;   
  98.    *(void **)&a1=temp_addr;      
  99.    
  100. //---------------------------------------------
  101. //交换b0,b1虚函数表,通过基类V运行虚函数
  102.    temp_addr=*(void **)&b0;
  103.    *(void **)&b0=*(void **)&b1;   
  104.    *(void **)&b1=temp_addr;      

  105.    (*(V *)(void *)&b0).vfunc();    //vfunc in B1
  106.    (*(V *)(void *)&b1).vfunc();    //vfunc in B0   

  107. //----------------------------------------------
  108. //b0,b1虚函数表还原   
  109.    temp_addr=*(void **)&b0;
  110.    *(void **)&b0=*(void **)&b1;   
  111.    *(void **)&b1=temp_addr;      

  112. //---------------------------------------------
  113. //交换a0,b1虚函数表,通过基类V运行虚函数
  114.    temp_addr=*(void **)&a0;
  115.    *(void **)&a0=*(void **)&b1;   
  116.    *(void **)&b1=temp_addr;      

  117.    (*(V *)(void *)&a0).vfunc();    //vfunc in B1
  118.    (*(V *)(void *)&b1).vfunc();    //vfunc in A0   

  119. //----------------------------------------------
  120. //a0,b1虚函数表还原   
  121.    temp_addr=*(void **)&a0;
  122.    *(void **)&a0=*(void **)&b1;   
  123.    *(void **)&b1=temp_addr;     
  124.       

  125. //---------------------------------------------
  126. //交换a1,b0虚函数表,通过基类V运行虚函数
  127.    temp_addr=*(void **)&a1;
  128.    *(void **)&a1=*(void **)&b0;   
  129.    *(void **)&b0=temp_addr;      

  130.    (*(V *)(void *)&a1).vfunc();    //vfunc in B0
  131.    (*(V *)(void *)&b0).vfunc();    //vfunc in A1   

  132. //----------------------------------------------
  133. //a1,b0虚函数表还原   
  134.    temp_addr=*(void **)&a1;
  135.    *(void **)&a1=*(void **)&b0;   
  136.    *(void **)&b0=temp_addr;  
  137.    
  138.   while(1);
  139. }
复制代码

出0入0汤圆

 楼主| 发表于 2012-4-7 17:48:28 | 显示全部楼层
本帖最后由 uc_cpp 于 2012-4-7 17:49 编辑

另外,Cortex-M3虚函数调用指令:
        LDR.N    R0,??DataTable1        //加载虚函数表指针地址
        LDR      R1,[R0, #+0]              //读取虚函数表指针
        LDR      R1,[R1, #+0]              //读取虚函数表的函数地址
        BLX      R1                               //运行虚函数

4条指令,算不上效率如何如何低。

出0入0汤圆

发表于 2012-4-7 21:30:19 | 显示全部楼层
大哥,出个MCU C++集锦吧,放一个帖子里面。

出0入0汤圆

发表于 2012-4-7 21:39:06 | 显示全部楼层
dosomething 发表于 2012-4-7 21:30
大哥,出个MCU C++集锦吧,放一个帖子里面。

哈哈,他就是一个集锦。

出0入0汤圆

发表于 2012-4-7 22:01:18 | 显示全部楼层
虚函数具有继承的逐级传递性,而函数指针是没有的。
这导致继承的级别越多,虚函数的效率越低,这是C++的软肋之一。

出0入0汤圆

发表于 2012-4-7 22:56:45 | 显示全部楼层
本帖最后由 theophilus 于 2012-4-7 23:02 编辑

查虚函数表是多次跳转过程,破坏流水线,一定是慢的。当然,比较下,如果用C来实现类似的效果,不会更快,但是会更丑(参见GTK的多态)
JUST FYI
大部分多态的需求其实可以用编译时多态来搞
http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
http://en.wikipedia.org/wiki/Tem ... tatic_polymorphisam

运行时可以用Pimpl,这个历史很久远了,但是由于其二进制兼容性是决定性的优点。
http://en.wikipedia.org/wiki/Opaque_pointer

出0入0汤圆

 楼主| 发表于 2012-4-8 06:58:08 | 显示全部楼层
虚函数只有一个,继承没关系,不会影响效率。

多个虚函数,虚函数表就会有多个虚函数,查表有偏移量,效率会更低些。

出0入0汤圆

发表于 2012-4-8 08:22:38 | 显示全部楼层
51访问虚函数代码:
  1.         PUSH    DPL
  2.         PUSH    DPH
  3.         MOV     R2,#(a0 & 0xff)
  4.         MOV     R3,#((a0 >> 8) & 0xff)
  5.         MOV     DPTR,#a0
  6.         MOVX    A,@DPTR
  7.         MOV     R0,A
  8.         INC     DPTR
  9.         MOVX    A,@DPTR
  10.         MOV     DPH,A
  11.         MOV     DPL,R0
  12.         MOVX    A,@DPTR
  13.         MOV     R0,A
  14.         INC     DPTR
  15.         MOVX    A,@DPTR
  16.         MOV     DPH,A
  17.         MOV     DPL,R0
  18.         LCALL   ?CALL_IND
  19.         POP     DPH
  20.         POP     DPL
  21.         RET
复制代码

出0入93汤圆

发表于 2012-4-8 08:35:47 | 显示全部楼层
打击一下楼主,您是否什么是虚函数都没有弄明白?虚函数具有多态性,多态性只能在堆上分配时才能体现出来,直接在栈上分配的行为直接就是普通函数,运算得出的结果是错的。

您在看看这句:
V* v = new B1;                  //声明的是基类类型的变量
v->vfunc();                       //这里调用的实际上是B1::vfunc,查询了N遍才会达到的。

反汇编得到什么样的结果,占用多大ROM、RAM、时间、效率。虚函数效率一定就得低,因为每一次继承都会引入额外的vTable,查询效率自然低了好多。你直接弄成普通函数方式去调用,是不可以的,根本就无法多态。

请查看多态的定义和实现。虚函数的目的就是为了实现基类访问子类而设置的,你要用子类直接声明的话,那完全可以把virtual去掉,直接override,还节省ROM和RAM。

出0入93汤圆

发表于 2012-4-8 08:39:34 | 显示全部楼层
C++中为了实现虚函数的效果而又不增加额外的开销,方法是有的,就是采用模板,微软的WTL库就是这个得来的,这个效率比MFC高得多,虽然WTL也有继承,但是继承的都不是虚函数,而是普通函数,通过模板模拟调用子类的方法。
WTL中不存在多态行为。

出0入0汤圆

发表于 2012-4-8 09:58:04 | 显示全部楼层
本帖最后由 uc_c++ 于 2012-4-8 10:02 编辑
takashiki 发表于 2012-4-8 08:35
打击一下楼主,您是否什么是虚函数都没有弄明白?虚函数具有多态性,多态性只能在堆上分配时才能体现出来, ...


每一次继承都会引入额外的vTable,查询效率自然低了好多。你直接弄成普通函数方式去调用
---------------------------------------------------------------------------------------------------------------------
每继承一次,就会增加一个VTAB,在IAR ARM上,这个VTAB是12个字节,
前8个字节都是0,不清楚有什么用途(或许是为了pmf,纯猜测),最后4个字节才是虚函数地址。

继承会增加代码量,虚函数表,子类重载的虚函数,构造函数,都会有不同程度的代码增加。

但是 虚函数调用速度不会有影响。(前提是子类不引入新的虚函数,只是继承虚函数,或者重载虚函数)
每个类都有自己的虚函数表,查表不需要层层查表,只需要查自己的虚函数表就行。

出0入0汤圆

 楼主| 发表于 2012-4-8 10:24:51 | 显示全部楼层
本帖最后由 uc_cpp 于 2012-4-8 10:27 编辑

虚函数具有多态性,多态性只能在堆上分配时才能体现出来,直接在栈上分配的行为直接就是普通函数,运算得出的结果是错的。

您在看看这句:
V* v = new B1;                  //声明的是基类类型的变量
v->vfunc();                       //这里调用的实际上是B1::vfunc
---------------------------------------------------------------------------------------------------------------

你的意思我明白。
多态性只能在堆上分配时才能体现,这却未必。

多态常用的方法是 作为参数传递把子类传递给基类(或者把子类转换成基类),通过基类来调用虚函数。
这与对象分配在那个空间(堆,栈,全局变量区),没有本质的区别。

但是,直接通过子类来访问虚函数,有时并不能达到调用虚函数的结果。
原因是编译器的优化。
编译器知道子类的虚函数具体是哪个函数,因此把虚函数优化成了 一般函数,而没有通过虚函数表来调用,这没有错。

为了避免编译器的这种优化,可以把子类强制转换成纯虚类的基类,调用虚函数,编译器无法优化成实函数,只能通过虚函数表来调用,达到多态效果。  
  (*(V *)(void *)&a0).vfunc();
或者:   
  ((V *)(void *)&a0)->vfunc();
//这里的(void *) 必须要有,作用是把 &a0转换成 纯数字,脱离与类A0的关系,再把数字转换成纯虚类的指针,
//这里也可以使用int,unsigned int转换(前提是指针与int占用存储空间 相同)。

出0入0汤圆

发表于 2012-4-8 10:41:15 | 显示全部楼层
我是来顶楼主的.....

出0入0汤圆

发表于 2012-4-8 10:53:36 | 显示全部楼层
dosomething 发表于 2012-4-7 21:30
大哥,出个MCU C++集锦吧,放一个帖子里面。

支持!~

出0入93汤圆

发表于 2012-4-8 11:28:34 | 显示全部楼层
uc_cpp 发表于 2012-4-8 10:24
虚函数具有多态性,多态性只能在堆上分配时才能体现出来,直接在栈上分配的行为直接就是普通函数,运算得出 ...

重新编译了你写的程序,还真是那么回事,无论是直接分配在栈上,还是分配在堆上,编译之后虚函数调用都只有那么4条指令,查表的过程被优化了,只是new过程加长了,看来RVCT编译器已经相当智能。
但测试的结果表示,无论分配在堆上还是栈上,程序都需要分配足够的堆空间,否则__vptr指针会乱指,程序跑飞,未分析原因。

出0入0汤圆

发表于 2012-4-8 13:16:51 | 显示全部楼层
takashiki 发表于 2012-4-8 11:28
重新编译了你写的程序,还真是那么回事,无论是直接分配在栈上,还是分配在堆上,编译之后虚函数调用都只 ...

基本上,我说知道C++编译器对于这种单继承链,都是这样调用的。
对于多重继承(菱形继承), 生成的代码要复杂些,需要加上一个Offset。

性能上,虚函数调用相对于大量的if else分支,性能要高上不少;switch case 要看情况,主要看switch的跳转表。

虚函数其实主要的性能损失倒不是那几条汇编代码,而是虚函数不可预测的分支跳转,在越先进的CPU中,性能损失反而越大(破坏流水线的损失,在有PGO优化的编译器中,此种情况可能不严重,因为有叫Virtual Call Speculation的玩意)。

出0入0汤圆

发表于 2012-4-9 16:35:46 | 显示全部楼层
JAVA也是面向对象的,但对虚函数的实现方式跟C++是不同的
当继承的次数多了(比如超过5),JAVA的效率明显要高于C++
但如果继承只有一级,C++的VTAB是有优势的
头像被屏蔽

出0入0汤圆

发表于 2012-4-24 12:43:15 | 显示全部楼层
cool !

出0入0汤圆

发表于 2012-4-24 12:45:38 | 显示全部楼层
在单片机上还没用过C++

出0入0汤圆

发表于 2012-4-24 12:48:54 | 显示全部楼层
这是个好贴

出0入0汤圆

发表于 2012-6-23 19:30:56 | 显示全部楼层
Keil 中用模板 很是让人蛋疼....

出0入0汤圆

发表于 2012-6-23 20:40:05 | 显示全部楼层
虚函数就是把原来得需要你switch的部分编译器给隐藏实现了...都得查表...只不过你不用写代码了...
C++包含C 和 ++....
C是程序员需要掌握的...
++是构架师需要掌握的...其设计也是为了方便UML的...

理论上的开发模式是 构架师用++写框架 程序员用C写程序...
实际上的是构架师根本不干活...

所以才给了程序员一个学习++的机会....


效率这个问题看你怎么看...没有飞机的时候 郑和也到非洲了...
你说飞机快...几个小时就到了...郑和跑了半年多...
实际上郑和比飞机快好几百年呢...

不要只看到程序的效率...要看工期....

虚函数的效率也是一样...你感觉效率高...一旦团队有人看不懂...效率就低很多很多....
推广全国通用的C++是开发人员的根本...把一些用不到的什么泛型 多态等去掉就行了....



出0入0汤圆

发表于 2012-6-24 18:57:36 | 显示全部楼层
看不懂的路过。。。

出0入0汤圆

发表于 2012-6-24 19:06:11 | 显示全部楼层
中断函数里声明的对象,中断结束时析构函数执行了没?

出0入17汤圆

发表于 2014-11-9 21:15:54 | 显示全部楼层
mark! 好贴

出0入0汤圆

发表于 2014-11-9 22:49:02 | 显示全部楼层
mark! .......

出0入0汤圆

发表于 2014-11-10 09:13:52 | 显示全部楼层
其实把派生类传给基类作为模版参数,也可以实现虚函数静态化的。

出0入0汤圆

发表于 2014-11-10 09:41:27 | 显示全部楼层
fish47 发表于 2014-11-10 09:13
其实把派生类传给基类作为模版参数,也可以实现虚函数静态化的。

你这是ATL/WTL的基础。

出0入0汤圆

发表于 2014-11-10 09:42:18 | 显示全部楼层
赞,好好学习,支持一下

出0入0汤圆

发表于 2014-11-10 09:55:55 | 显示全部楼层
C++用到MCU, 目前还比较少吧

出0入0汤圆

发表于 2014-11-10 14:16:22 | 显示全部楼层
这年头搞单片机都用C++了?老衲该退休了啊
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-5-5 04:51

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表