掀起你的盖头来——谈VC++对象模型
发布时间:2006-02-28 12:21:54 来源:blog 网友评论 0 条 成员函数
一个C++成员函数只是类范围内的又一个成员。X类每一个非静态的成员函数都会接受一个特殊的隐藏参数——this指针,类型为X* const。该指针在后台初始化为指向成员函数工作于其上的对象。同样,在成员函数体内,成员变量的访问是通过在后台计算与this指针的偏移来进行。

| struct P { int p1; void pf(); // new virtual void pvf(); // new }; |
P有一个非虚成员函数pf(),以及一个虚成员函数pvf()。很明显,虚成员函数造成对象实例占用更多内存空间,因为虚成员函数需要虚函数表指针。这一点以后还会谈到。这里要特别指出的是,声明非虚成员函数不会造成任何对象实例的内存开销。现在,考虑P::pf()的定义。
| void P::pf() { // void P::pf([P *const this]) ++p1; // ++(this->p1); } |
这里P:pf()接受了一个隐藏的this指针参数,对于每个成员函数调用,编译器都会自动加上这个参数。同时,注意成员变量访问也许比看起来要代价高昂一些,因为成员变量访问通过this指针进行,在有的继承层次下,this指针需要调整,所以访问的开销可能会比较大。然而,从另一方面来说,编译器通常会把this指针缓存到寄存器中,所以,成员变量访问的代价不会比访问局部变量的效率更差。
译者注:访问局部变量,需要到SP寄存器中得到栈指针,再加上局部变量与栈顶的偏移。在没有虚基类的情况下,如果编译器把this指针缓存到了寄存器中,访问成员变量的过程将与访问局部变量的开销相似。
1、覆盖成员函数
和成员变量一样,成员函数也会被继承。与成员变量不同的是,通过在派生类中重新定义基类函数,一个派生类可以覆盖,或者说替换掉基类的函数定义。覆盖是静态(根据成员函数的静态类型在编译时决定)还是动态(通过对象指针在运行时动态决定),依赖于成员函数是否被声明为“虚函数”。
Q从P继承了成员变量和成员函数。Q声明了pf(),覆盖了P::pf()。Q还声明了pvf(),覆盖了P::pvf()虚函数。Q还声明了新的非虚成员函数qf(),以及新的虚成员函数qvf()。

| struct Q : P { int q1; void pf(); // overrides P::pf void qf(); // new void pvf(); // overrides P::pvf virtual void qvf(); // new }; |
对于非虚的成员函数来说,调用哪个成员函数是在编译时,根据“->”操作符左边指针表达式的类型静态决定的。特别地,即使ppq指向Q的实例,ppq->pf()仍然调用的是P::pf(),因为ppq被声明为“P*”。(注意,“->”操作符左边的指针类型决定隐藏的this参数的类型。)
| P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q; pp->pf(); // pp->P::pf(); // P::pf(pp); ppq->pf(); // ppq->P::pf(); // P::pf(ppq); pq->pf(); // pq->Q::pf(); // Q::pf((P*)pq); (错误!) pq->qf(); // pq->Q::qf(); // Q::qf(pq); |
译者注:标记“错误”处,P*似应为Q*。因为pf非虚函数,而pq的类型为Q*,故应该调用到Q的pf函数上,从而该函数应该要求一个Q* const类型的this指针。
对于虚函数调用来说,调用哪个成员函数在运行时决定。不管“->”操作符左边的指针表达式的类型如何,调用的虚函数都是由指针实际指向的实例类型所决定。比如,尽管ppq的类型是P*,当ppq指向Q的实例时,调用的仍然是Q::pvf()。
| pp->pvf(); // pp->P::pvf(); // P::pvf(pp); ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq); pq->pvf(); // pq->Q::pvf(); // Q::pvf((P*)pq); (错误!) |
译者注:标记“错误”处,P*似应为Q*。因为pvf是虚函数,pq本来就是Q*,又指向Q的实例,从哪个方面来看都不应该是P*。
为了实现这种机制,引入了隐藏的vfptr成员变量。一个vfptr被加入到类中(如果类中没有的话),该vfptr指向类的虚函数表(vftable)。类中每个虚函数在该类的虚函数表中都占据一项。每项保存一个对于该类适用的虚函数的地址。因此,调用虚函数的过程如下:取得实例的vfptr;通过vfptr得到虚函数表的一项;通过虚函数表该项的函数地址间接调用虚函数。也就是说,在普通函数调用的参数传递、调用、返回指令开销外,虚函数调用还需要额外的开销。
回头再看看P和Q的内存布局,可以发现,VC++编译器把隐藏的vfptr成员变量放在P和Q实例的开始处。这就使虚函数的调用能够尽量快一些。实际上,VC++的实现方式是,保证任何有虚函数的类的第一项永远是vfptr。这就可能要求在实例布局时,在基类前插入新的vfptr,或者要求在多重继承时,虽然在右边,然而有vfptr的基类放到左边没有vfptr的基类的前面。
许多C++的实现会共享或者重用从基类继承来的vfptr。比如,Q并不会有一个额外的vfptr,指向一个专门存放新的虚函数qvf()的虚函数表。Qvf项只是简单地追加到P的虚函数表的末尾。如此一来,单继承的代价就不算高昂。一旦一个实例有vfptr了,它就不需要更多的vfptr。新的派生类可以引入更多的虚函数,这些新的虚函数只是简单地在已存在的,“每类一个”的虚函数表的末尾追加新项。
2、多重继承下的虚函数
如果从多个有虚函数的基类继承,一个实例就有可能包含多个vfptr。考虑如下的R和S类:

| struct R { int r1; virtual void pvf(); // new virtual void rvf(); // new }; |

| struct S : P, R { int s1; void pvf(); // overrides P::pvf and R::pvf void rvf(); // overrides R::rvf void svf(); // new }; |
这里R是另一个包含虚函数的类。因为S从P和R多重继承,S的实例内嵌P和R的实例,以及S自身的数据成员S::s1。注意,在多重继承下,靠右的基类R,其实例的地址和P与S不同。S::pvf覆盖了P::pvf()和R::pvf(),S::rvf()覆盖了R::rvf()。
| S s; S* ps = &s; ((P*)ps)->pvf(); // (*(P*)ps)->P::vfptr[0])((S*)(P*)ps) ((R*)ps)->pvf(); // (*(R*)ps)->R::vfptr[0])((S*)(R*)ps) ps->pvf(); // one of the above; calls S::pvf() |
译者注:
·调用((P*)ps)->pvf()时,先到P的虚函数表中取出第一项,然后把ps转化为S*作为this指针传递进去;
·调用((R*)ps)->pvf()时,先到R的虚函数表中取出第一项,然后把ps转化为S*作为this指针传递进去;
因为S::pvf()覆盖了P::pvf()和R::pvf(),在S的虚函数表中,相应的项也应该被覆盖。然而,我们很快注意到,不光可以用P*,还可以用R*来调用pvf()。问题出现了:R的地址与P和S的地址不同。表达式(R*)ps与表达式(P*)ps指向类布局中不同的位置。因为函数S::pvf希望获得一个S*作为隐藏的this指针参数,虚函数必须把R*转化为S*。因此,在S对R虚函数表的拷贝中,pvf函数对应的项,指向的是一个“调整块”的地址,该调整块使用必要的计算,把R*转换为需要的S*。
译者注:这就是“thunk1: this-= sdPR; goto S::pvf”干的事。先根据P和R在S中的偏移,调整this为P*,也就是S*,然后跳转到相应的虚函数处执行。
在微软VC++实现中,对于有虚函数的多重继承,只有当派生类虚函数覆盖了多个基类的虚函数时,才使用调整块。
- 推荐阅讯
- 使用VC6.0实现窗口的任意分割
- Visual C# 2005快速入门之运用作用域
- VC++内部COM插件的编程实现
- 漫谈Visual C#的组件设计方法
- VC利用boost库解析正则表达式
- VC++中利用/GS开关防止缓冲区溢出
- 利用Visual C#实现ICMP网络协议
- Visual C#2005快速入门之声明bool变量
- 解读VC++编程中的文件操作API和CFile类
- VC#2005快速入门之使用布尔操作符
- 阅读排行
- 1.VC++编程实现广告窗口自动关闭
- 2.深入浅出VC++串口编程之基于控件
- 3.解读VC++编程中的文件操作API和CFile类
- 4.利用Visual C#实现ICMP网络协议
- 5.深入浅出VC++串口编程之第三方类
- 6.掀起你的盖头来——谈VC++对象模型
- 7.Visual C#中用WMI控制远程计算机
- 8.深入浅出VC++串口编程之基于Win32 API
- 9.Visual C++2005中开发自定义绘图控件
- 10.深入浅出VC++串口编程之基本概念
- 专题教程
- Windows Server-Windows Server文档-Windows Server新闻-Windows Ser PostgreSQL-PostgreSQL文档-PostgreSQL新闻-PostgreSQL专家
- WebLogic-WebLogic文档-WebLogic新闻-WebLogic专家 FreeBSD-FreeBSD文档-FreeBSD新闻-FreeBSD专家
- Linux-内核 GUI KDE Gnome DNS FTP 安全 安装-Linux专区 Windows-AD IIS ServerCore 虚拟化 安全 HPC-Windows专区
- 大话G游 专题:手机病毒揭密
- ARP攻击防范与解决方案 路由故障处理手册
