您的位置:首页 > 编程语言 > C语言/C++

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

2010-04-10 12:14 1131 查看
注:本文所有观点纯属推测,请勿盲目信任

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

//Base.h

#pragma once
#include <iostream>
using namespace std;

struct Base1
{
virtual int __stdcall Base1Func1(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
virtual int  __stdcall Base1Func2(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
int   __stdcall NoVFunc(int a,int b)
{
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
int m_iData;
};
struct Base2
{
virtual int  __stdcall Base2Func1(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
virtual int  __stdcall Base2Func2(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
int m_iData;
};

struct D:public Base1,public Base2
{
virtual int  __stdcall D1Func(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
virtual int __stdcall Base1Func1(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
virtual int  __stdcall Base2Func2(int a,int b){
cout<<__FUNCTION__<<"/tm_iData="<<m_iData<<"/ta="<<a<<"/tb="<<b<<endl;
return 0;
}
int m_iData;
};

template<class T>
struct MemFuncT
{
typedef int (__stdcall T::* T_MemFuncT)(int,int);
typedef int (T::* T_MemDataT);
};

typedef int ( __stdcall * pMemFunc)(void*,int,int);


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

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

Base1 obj;
obj.Base1Func1(10,10);
//实际上看成是
Base1 obj;
Base1Func1(&obj,10,10);


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

//main.cpp

#include <GetVptr.hxx>
#include <typeinfo>
#include "Base.h"
#include <PrintLayout.hxx>
int main(int argc, char* argv[])
{
Base1 obj;
obj.m_iData =100;

D * pD = new D;
pD->Base1::m_iData =100;
pD->Base2::m_iData =200;
pD->m_iData = 300;
Base1 * pB1 = pD;
Base2 * pB2 = pD;

//Non-inheritance
cout<<"//Non-inheritance"<<endl;
pMemFunc func = ReinterpretCast<pMemFunc>(&Base1::Base1Func1);
func(&obj,1000,200);
func = ReinterpretCast<pMemFunc>(&Base1::Base1Func2);
func(&obj,1000,200);
func = ReinterpretCast<pMemFunc>(&Base1::NoVFunc);
func(&obj,1000,200);

//with inheritance
cout<<"/n//with inheritance"<<endl;
pMemFunc funcB1 = ReinterpretCastEx<pMemFunc>(&D::Base1Func1);  //override sizeof(&D::Base1Func1) == 8
func = ReinterpretCast<pMemFunc>(&D::Base1Func2);				//not override
pMemFunc funcB2 = ReinterpretCastEx<pMemFunc>(&D::Base2Func2);  //override sizeof(&D::Base2Func2) == 8

funcB1(pD,1000,200);
func(pD,1000,200);
funcB2(pD,1000,200);  //P1.输出有错,我们接下来会分析其原因--

return 0;
}

/*
//Non-inheritance
Base1::Base1Func1       m_iData=100     a=1000  b=200
Base1::Base1Func2       m_iData=100     a=1000  b=200
Base1::NoVFunc  m_iData=100     a=1000  b=200

//with inheritance
D::Base1Func1   m_iData=300     a=1000  b=200
Base1::Base1Func2       m_iData=100     a=1000  b=200
Base1::Base1Func2       m_iData=100     a=1000  b=200
请按任意键继续. . .
*/


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

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

Base1::vptr

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

//main.cpp

#include <GetVptr.hxx>
#include <typeinfo>
#include "Base.h"
#include <PrintLayout.hxx>
#include <iomanip>

template<typename T>
void PrintfMemFunc(T const & func,char const * const str)
{
cout<<setw(20)<<str;
int * p = ReinterpretCast<int*>(&func);
for (int i = 0;i<sizeof(T)/sizeof(int);++i)
{
cout<<"  "<<ReinterpretCast<void*>(p[i]);
}
cout<<endl;
}
#define PRINT_MEMFUNC(func) PrintfMemFunc(&func, #func)
int main(int argc, char* argv[])
{
D obj;
obj.Base1::m_iData = 100;
obj.Base2::m_iData = 200;
obj.m_iData = 300;
PrintLayout(obj);
cout<<endl;
cout<<"class Base1"<<endl;
PRINT_MEMFUNC(Base1::Base1Func1);
PRINT_MEMFUNC(Base1::Base1Func2);
PRINT_MEMFUNC(Base1::NoVFunc);

cout<<"/nclass Base2"<<endl;
PRINT_MEMFUNC(Base2::Base2Func1);
PRINT_MEMFUNC(Base2::Base2Func2);

cout<<"/nclass D"<<endl;
PRINT_MEMFUNC(D::Base1Func1);  //override
PRINT_MEMFUNC(D::Base1Func2);
PRINT_MEMFUNC(D::Base2Func1);
PRINT_MEMFUNC(D::Base2Func2);  //override
PRINT_MEMFUNC(D::D1Func);

return 0;
}

/*
class Base1
Base1::Base1Func1  004111C7
Base1::Base1Func2  0041112C
Base1::NoVFunc  004111D6

class Base2
Base2::Base2Func1  004111C7
Base2::Base2Func2  0041112C

class D
D::Base1Func1  004111C7  00000000
D::Base1Func2  0041112C
D::Base2Func1  004111C7
D::Base2Func2  0041112C  00000008
D::D1Func  004112BC  00000000
请按任意键继续. . .
*/


从输出我们发现.

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++模拟一下):

//VCallImpl.h

#pragma once
#include <GetVptr.hxx>
#include "Base.h"

inline int VCallImpl(void * pThis,int a,int b,int i)
{
long * pl = static_cast<long*>(pThis);
pMemFunc * vptr = ReinterpretCast<pMemFunc *>(GetVptr(*pl));
return (vptr[i])(pThis,a,b);
}
inline int VCallImpl1(void * pThis,int a,int b)
{
return VCallImpl(pThis,a,b,0);
}
inline int VCallImpl2(void * pThis,int a,int b)
{
return VCallImpl(pThis,a,b,1);
}
inline int VCallImpl3(void * pThis,int a,int b)
{
return VCallImpl(pThis,a,b,2);
}


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

//main.cpp

#include <GetVptr.hxx>
#include <typeinfo>
#include "Base.h"
#include <PrintLayout.hxx>
#include <iomanip>
#include "VCallImpl.h"

#define IMPL_CALLL( pObj,BaseClass,Num)			/
pObj->BaseClass##Func##Num(1000,2000);		/
VCallImpl##Num((BaseClass*)pObj,1000,2000);	/
cout<<endl

int main(int argc, char* argv[])
{
D d;
D * pObj  = &d;
pObj->Base1::m_iData = 100;
pObj->Base2::m_iData = 200;
pObj->m_iData = 300;

IMPL_CALLL(pObj,Base1,1);
IMPL_CALLL(pObj,Base1,2);
IMPL_CALLL(pObj,Base2,1);
IMPL_CALLL(pObj,Base2,2);
return 0;
}

/*
D::Base1Func1   m_iData=300     a=1000  b=2000
D::Base1Func1   m_iData=300     a=1000  b=2000

Base1::Base1Func2       m_iData=100     a=1000  b=2000
Base1::Base1Func2       m_iData=100     a=1000  b=2000

Base2::Base2Func1       m_iData=200     a=1000  b=2000
Base2::Base2Func1       m_iData=200     a=1000  b=2000

D::Base2Func2   m_iData=300     a=1000  b=2000
D::Base2Func2   m_iData=300     a=1000  b=2000

请按任意键继续. . .
*/


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

对于前面的结论3,大家不妨自己去验证一下,对于结论2,为什么派生类的函数指针需要额外的四个直接用以保存对应的vptr到首地址的偏移,在这里百思不得其解
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: