现在的位置: 首页 > 综合 > 正文

深入理解C++对象模型-成员函数的本质以及虚函数的实现(非虚继承)

2013年08月03日 ⁄ 综合 ⁄ 共 6447字 ⁄ 字号 评论关闭

注:本文所有观点纯属推测,请勿盲目信任

 

 

前言:本文是前一篇文章的续篇,在阅读本文之前请先阅读前一篇文章<<深入理解C++对象模型-对象的内存布局,vptr,vtable >>.在开始本文讨论之前,先给出一段代码,后面将基于这段代码进行讨论.

 

1.成员函数的本质与实现:

大家都知道,非静态成员函数都有一个隐含的this指针,那这this指针是怎么传进来的呢?相信看过Inside the C++ Object Model的人都知道,每个成员函数都有一个隐含的参数,this指针就是从这个参数传进来的,拿int  Base1::Base1Func1(int a,int b)为例

 

也就是说,对于任何一个非静态成员函数 ReturnT Class:memFunc(params....) 都可以看成是ReturnT memFunc(Class* this,params...);下面我们将通过代码来进行验证.(注意,在默认情况下成员函数的调用约定方式为__thiscall,__thiscall只能用在成员函数中,不能用在普通函数,而普通函数又没有任何一种调用约定方式跟__thiscall兼容,这就是为我们我在前面的代码中显式地将成员函数的调用约定方式设为__stdcall).

2.虚函数的本质与实现:

首先我们先给出D对象的内存布局

Base1::vptr

Base1::m_iData

Base2::vptr

Base2::m_iData

D::m_iData

接下来我们通过打印成员函数指针的地址以及指向的内容来研究成员函数的实现

 

从输出我们发现.

1.无论是基类还是派生类,不同类的第N个虚函数的地址都一样,比如Base1::Base1Func1==Base2::Base2Func1==0041135C

2.派生类里面出现的新函数的函数指针(无论是新添加的还是重写基类的)大小都是8,其中该指针第二个DWORD保存着其对应子对象中vptr相对首地址的偏移(对于D::Base2Func2,它对应的基类是Base2,vptr相对首地址偏移为8 )

3.意外发现的一个现象:对于非虚拟继承情况下,派生类对象的内存布局模型,并非是按照声明顺序来排列的,而是先将有vptr的类按照声明顺序放在对象的最前面,接下来是按照声明顺序存放不含有vptr的类.这样处理的好处之一是,假设这种情况,class D:public A, public B,其中A没有vptr而B有,假如仅仅是按照声明顺序来排列,那么D的内存布局就会是如下所示

D::vptr

A::datas

B::vptr

B::datas

D::datas

而使用编译器现在所用的策略,那么D::vptr跟B::vptr就可以合并了,这样可以节省一个指针的大小.

 

从第1点中得到启发:VS应该会给虚函数提供一个proxy,也就是说VS中应该会保存着这样一堆l类似功能的函数(为了效率以及通用性,VS应该是使用thunk技术来实现,这里只是用C++模拟一下):

 

下面我们将通过一段测试代码来验证前面结论的正确性

你可能留意套前面在IMPL_CALLL里面做了一个指针强制转换,这个是为了调整vptr的,对于编译器,他可以很清楚地知道,你要调用的函数要做什么样的调整,比如说当你要调用一个从Base1基类继承过来的函数,它会首先将this调整为指向Base1的subobject,然后在调用这个函数.这就是在1.成员函数的本质与实现:里面有一个例子会输出错误的原因,看注释://P1.输出有错,我们接下来会分析其原因--

对于前面的结论3,大家不妨自己去验证一下,对于结论2,为什么派生类的函数指针需要额外的四个直接用以保存对应的vptr到首地址的偏移,在这里百思不得其解

 

抱歉!评论已关闭.