免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 5500 | 回复: 12

c++ 虚函数 栈 [复制链接]

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 16:05 |显示全部楼层
5可用积分
比如:
class base{
   virtual test();
}
class sbase{
   virtual test()
}

int main()
{
    sbase *p=new sbase;
    p->test();
    delete p;
}

上面会生成一个虚函数vtable数组,里面vtable[0]保存了test()函数的地址

vprt应该是放在堆里了

但:

这个vtable数组放在栈哪里?

论坛徽章:
2
青铜圣斗士
日期:2015-11-26 06:15:59数据库技术版块每日发帖之星
日期:2016-07-24 06:20:00
发表于 2010-04-14 16:35 |显示全部楼层
一般的实现方式是:

vptr是根对象在一起的。
上面的代码中, 对象在free store里, vptr也在free store里。

vtable是每个class一份, 该class的所有对象共享, 在静态存储区里。

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 16:58 |显示全部楼层
恩,vptr是对象的一员,所以放在堆里

那么说vtable是编译阶段就确定了它的位置,运行时就加载到静态存储区了?

如果是这样,那么在运行阶段,movl vptr  0x123(这个就是vtable地址) 就把vptr指向了vtable了

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 17:01 |显示全部楼层
另外,如果vtable是放在静态存储区,那就只能等到进程结束才能抹去了

我还以为放在栈里面的,就是说对象所属的那个栈里面的,

论坛徽章:
0
发表于 2010-04-14 17:05 |显示全部楼层
以前读过的文章:

引言:近日CSDN的"C/C++语言"版的一个问题引 起了我的注意:"请问虚函数表放在哪里?"。我也曾经思考过这个问题,零零散散也有一定的收获,这次正好趁这个机会把我对这一部分的理解整理一下。 首先值得声明的是,本文的编译环境是VS2002+WinXP。C++标准并没有对虚函数的实现作出任何的说明,甚至都没有提到虚函数的实现需要用虚表来 实现,只不过主流的C++编译器的虚函数机制都是通过虚表来实现的,所以用虚表来实现虚函数就成了"不是标准的标准"。但是这并不代表所有编译器在实现细 节上的处理都是完全一致的,它们或多或少都存在一定的个体差异。所以,本文的结论不一定适用于其他的编译情况。

虚函数/虚表的基础知识
一 个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字 节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。

编译模块内部虚表存放的位置
如 果一个模块定义了拥有虚表的类,那么这个类的虚表存放在那里呢?要回答这个问题,我们还是需要用汇编代码入手,我首先建立了一个简单的Win32 Console Application,然后定义了一个带虚函数的类,在相应的汇编代码中,我找到了重要的破解虚表存放位置的重要线索:
CONST    SEGMENT
  ??_7CDerived@@6B@                                     ; CDerived::`vftable'
      DD FLATfoobar@CDerived@@UAEXXZ
      DD FLATcallMe@CDerived@@UAEXXZ
  ; Function compile flags: /Odt /RTCsu /ZI
CONST    ENDS

以上的汇编代码给了我们这样的信息:
1> 虚表存放的位置应该实在模块的常量段中;
2> 这个类有两个虚函数,它们分别是?foobar@CDerived@@UAEXXZ和?callMe@CDerived@@UAEXXZ。

外部模块虚表存放的位置
当一个模块导出了一个带虚表的类,而另外一个模块又使用了这个导出类,这时候情况又是什么样的呢?这里存在两种很自然的处理方式:
1。维护一份虚表。虚表放在定义导出类的那个模块,任何使用这个导出类的其他模块都要通过这个模块来使用导出类。
2。维护多份虚表。这时候每一个使用导出类的模块都会有一份虚表的拷贝。
VS2002是使用那一种情况呢?在假设存在多份虚表的前提下,我们可以使用这样的策略来判断VS2002使用那种方式:
1。在类定义模块中创建一个类对象,并在另外一个模块中使用这个类对象。在类定义模块中创建类对象保证编译器用类定义模块中的虚表来初始化类对象。
2。在模块(非类定义模块)中创建并类对象并使用它。这样就保证编译器会用模块中的虚表来初始化类对象。
3。分别获取两种情况下两个类对象的虚表指针。如果它们的值相等,就说明只存在一份虚表;如果它们的值不等,就说明存在多份虚表。
4。如果两个虚表指针的值相等,则虚表来自于两个模块中的一个模块,判断这个虚表来自于那个模块。

应用上面的策略,我们首先建立一个Win32 DLL工程导出一个带虚表的类,再建立一个Win32 Consle Application使用这个导出类。在Win32 Consle Application的主函数中,我写了以下的代码:
    CDllInDepth* pObjInAnotherDLL = createObject();
    int vTableAdress = *reinterpret_cast<int*>(pObjInAnotherDLL);
    int vFuncAddress = *reinterpret_cast<int*>(vTableAdress);
    pObjInAnotherDLL->dumpMe();

    CDllInDepth* pObjInMyApp = new CDllInDepth;
    int vTableAdress2 = *reinterpret_cast<int*>(pObjInMyApp);
    int vFuncAddress2 = *reinterpret_cast<int*>(vTableAdress);
    pObjInMyApp->dumpMe();

对这段代码做如下的解释:
1。createObject()是DLL导出了一个全局函数。这个全局函数实现的功能就是生成一个类对象并将类对象的地址传出。这样做的目的就是为了在类定义模块中生成一个类对象。
2。 获得虚表指针和虚函数的代码可以这样分析:由于虚表指针存放在类对象的前4个字节中,我们首先需要将类对象的首地址转化成int型指针,并通过这个int 型指针获得前4个字节的内容,这个内容就是虚表的地址。接着我们将这个虚表的地址再转化成int型指针,并通过这个int型指针获得虚表的前4个字节的内 容,这个内容就是虚表的第一项的值,也就是一个虚函数的地址。

通过调试,我们得出这样的结果:
vTableAdress  = 0x1001401C        vFuncAddress  = 0x1001103C
vTableAdress2 = 0x1001401C        vFuncAddress2 = 0x1001103C

比较vTableAdress和vTableAdress2的值我们发现它们的值是完全一样的,这就说明我们的假设是不正确的,这里是存在一份虚 表。那最后的一个问题就是这个虚表是来自于那个模块呢?这个答案我们需要通过比较虚表的地址以及模块所占的内存空间来解答。在调试状态下,打开"模块"窗 口,我们就可以找到模块的地址:
  TestApp.exe            00400000-00417000
  DllInDepth.dll         10000000-10019000
其中的DllInDepth.dll模块就是定义导出类的模块,而TestApp.exe就是使用这个类的模块。通过比较不难发现,虚表的地址落在DllInDepth.dll的地址范围内,这就说明了虚表来在于类定义的模块。

到 了现在,关于虚表存放的问题基本上都得到了圆满的解决,但是我又有了一个新的问题:为什么会是这样的情况呢?我想,大概应该是这样的原因吧:类对象虚表指 针的初始化应该发生在构造函数被调用的时候,更具体的说应该实在进入到构造函数"{"之前,这个时机就是通常所说的构造函数"初始化列表"被执行的时候。 由于构造函数是在类定义模块中执行的,当然虚表也应该是类定义模块的虚表,对于其他的模块而言就是导入函数的调用,这样就没有必要维护多份虚表了。

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 17:23 |显示全部楼层
好文,看看

c++很多内在机制太不明朗了,蛋疼

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 17:56 |显示全部楼层
本帖最后由 chenzhanyiczy 于 2010-04-14 17:58 编辑

在linux + g++下做了一下试验:

using namespace std;


class base
{
  public :
  virtual void test(){char a;}
};

class sbase
{
  public :
  virtual void test() {int b;}
};

int main()
{
  base *p=new base;
  p->test();
  delete p;
  sbase *k=new sbase;
  k->test();
  delete k;
}


objdump -D test

08048636 <main>:
8048636:       8d 4c 24 04             lea    0x4(%esp),%ecx
804863a:       83 e4 f0                and    $0xfffffff0,%esp
804863d:       ff 71 fc                pushl  0xfffffffc(%ecx)
8048640:       55                      push   %ebp
8048641:       89 e5                   mov    %esp,%ebp
8048643:       53                      push   %ebx
8048644:       51                      push   %ecx
8048645:       83 ec 20                sub    $0x20,%esp
8048648:       c7 04 24 04 00 00 00    movl   $0x4,(%esp)
804864f:       e8 78 fe ff ff          call   80484cc <_Znwj@plt>
8048654:       89 c3                   mov    %eax,%ebx
8048656:       89 1c 24                mov    %ebx,(%esp)
8048659:       e8 70 00 00 00          call   80486ce <_ZN4baseC1Ev>

804865e:       89 5d f0                mov    %ebx,0xfffffff0(%ebp)
8048661:       8b 45 f0                mov    0xfffffff0(%ebp),%eax
8048664:       8b 00                   mov    (%eax),%eax
8048666:       8b 10                   mov    (%eax),%edx
8048668:       8b 45 f0                mov    0xfffffff0(%ebp),%eax
804866b:       89 04 24                mov    %eax,(%esp)
804866e:       ff d2                   call   *%edx
8048670:       8b 45 f0                mov    0xfffffff0(%ebp),%eax
8048673:       89 04 24                mov    %eax,(%esp)
8048676:       e8 11 fe ff ff          call   804848c <_ZdlPv@plt>
804867b:       c7 04 24 04 00 00 00    movl   $0x4,(%esp)
8048682:       e8 45 fe ff ff          call   80484cc <_Znwj@plt>
8048687:       89 c3                   mov    %eax,%ebx
8048689:       89 1c 24                mov    %ebx,(%esp)
804868c:       e8 4d 00 00 00          call   80486de <_ZN5sbaseC1Ev>
8048691:       89 5d f4                mov    %ebx,0xfffffff4(%ebp)
8048694:       8b 45 f4                mov    0xfffffff4(%ebp),%eax
8048697:       8b 00                   mov    (%eax),%eax
8048699:       8b 10                   mov    (%eax),%edx
804869b:       8b 45 f4                mov    0xfffffff4(%ebp),%eax
804869e:       89 04 24                mov    %eax,(%esp)
80486a1:       ff d2                   call   *%edx
80486a3:       8b 45 f4                mov    0xfffffff4(%ebp),%eax
80486a6:       89 04 24                mov    %eax,(%esp)
80486a9:       e8 de fd ff ff          call   804848c <_ZdlPv@plt>
80486ae:       b8 00 00 00 00          mov    $0x0,%eax
80486b3:       83 c4 20                add    $0x20,%esp
80486b6:       59                      pop    %ecx
80486b7:       5b                      pop    %ebx
80486b8:       5d                      pop    %ebp
80486b9:       8d 61 fc                lea    0xfffffffc(%ecx),%esp
80486bc:       c3                      ret
80486bd:       90                      nop


080486ce <_ZN4baseC1Ev>:
80486ce:       55                      push   %ebp
80486cf:       89 e5                   mov    %esp,%ebp
80486d1:       ba d0 87 04 08          mov    $0x80487d0,%edx
80486d6:       8b 45 08                mov    0x8(%ebp),%eax
80486d9:       89 10                   mov    %edx,(%eax)
80486db:       5d                      pop    %ebp
80486dc:       c3                      ret
80486dd:       90                      nop

080486de <_ZN5sbaseC1Ev>:
80486de:       55                      push   %ebp
80486df:       89 e5                   mov    %esp,%ebp
80486e1:       ba f0 87 04 08          mov    $0x80487f0,%edx
80486e6:       8b 45 08                mov    0x8(%ebp),%eax
80486e9:       89 10                   mov    %edx,(%eax)
80486eb:       5d                      pop    %ebp
80486ec:       c3                      ret
80486ed:       90                      nop
80486ee:       90                      nop
80486ef:       90                      nop



可以看到粗体的部分,编译器已经直接找到了要调用的函数,并call 它了,并没有去找vtable表

看来每个编译器实现还真不一样

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 21:23 |显示全部楼层
本帖最后由 chenzhanyiczy 于 2010-04-14 21:24 编辑

再看了一下上面的代码汇编,好像根本没有发现vtable数组的痕迹,难道g++根本没用vtable来实现虚函数?

论坛徽章:
1
2015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2010-04-14 23:19 |显示全部楼层
再看了一下上面的代码汇编,好像根本没有发现vtable数组的痕迹,难道g++根本没用vtable来实现虚函数?
chenzhanyiczy 发表于 2010-04-14 21:23


上面的汇编我没看
g++肯定是用vtable来实现虚函数的,有可能g++把一个虚函数调用优化掉了。

有个最简单的方法可以看到vptr,
class Concrete {
public:
  virtual ~Concrete() {}
  int i;
};

class Virtual {
public:
  int i;
};

比较sizeof(Concrete)和sizeof(Virtual)就能发现Virtual里多一个vptr

论坛徽章:
1
申猴
日期:2014-02-11 14:50:31
发表于 2010-04-14 23:26 |显示全部楼层
上面的汇编我没看
g++肯定是用vtable来实现虚函数的,有可能g++把一个虚函数调用优化掉了。

有个最 ...
koolcoy 发表于 2010-04-14 23:19



首先,我没有用到任何优化选项,命令如下:
g++ -o test test.cpp -g
至于g++默认会不会优化掉,这个就不知道了

其次,不是问vptr,问的是vtable,兄弟你可以回去看看 1,7,8楼
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP