深入理解C++对象模型-成员函数的本质以及虚函数的实现(非虚继承)
2010-04-10 12:14
1131 查看
注:本文所有观点纯属推测,请勿盲目信任
前言:本文是前一篇文章的续篇,在阅读本文之前请先阅读前一篇文章<<深入理解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对象的内存布局
接下来我们通过打印成员函数指针的地址以及指向的内容来研究成员函数的实现
从输出我们发现.
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跟B::vptr就可以合并了,这样可以节省一个指针的大小.
从第1点中得到启发:VS应该会给虚函数提供一个proxy,也就是说VS中应该会保存着这样一堆l类似功能的函数(为了效率以及通用性,VS应该是使用thunk技术来实现,这里只是用C++模拟一下):
下面我们将通过一段测试代码来验证前面结论的正确性
你可能留意套前面在IMPL_CALLL里面做了一个指针强制转换,这个是为了调整vptr的,对于编译器,他可以很清楚地知道,你要调用的函数要做什么样的调整,比如说当你要调用一个从Base1基类继承过来的函数,它会首先将this调整为指向Base1的subobject,然后在调用这个函数.这就是在1.成员函数的本质与实现:里面有一个例子会输出错误的原因,看注释://P1.输出有错,我们接下来会分析其原因--
对于前面的结论3,大家不妨自己去验证一下,对于结论2,为什么派生类的函数指针需要额外的四个直接用以保存对应的vptr到首地址的偏移,在这里百思不得其解
前言:本文是前一篇文章的续篇,在阅读本文之前请先阅读前一篇文章<<深入理解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 |
从第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到首地址的偏移,在这里百思不得其解
相关文章推荐
- 深入探索C++对象模型之指向成员函数的指针
- C++ 类的继承,子类以及之类的对象 对父类成员函数的访问权限
- 【深入探索c++对象模型】c++中构造函数调用虚函数的讨论
- 学习《深入理解C++对象模型》小结
- C++中的继承关系、访问限定符,六个默认成员函数以及菱形继承和虚继承
- C++深入理解(4)------函数模板以及显式具体化(读书笔记)
- 深入理解C++对象模型之拷贝构造函数
- 【转载】C/C++杂记:深入理解数据成员指针、函数成员指针
- C++对象模型中数据成员与继承
- C++对象模型之函数成员(1)
- 深入理解C++面向对象机制(二)虚继承
- 深入理解C++对象模型-对象的内存布局,vptr,vtable
- 【深度探索c++对象模型】Function语义学之成员函数调用方式
- c++多重继承和虚继承及虚函数深入理解
- c++多重继承和虚继承及虚函数深入理解(转)
- C++对象模型之函数成员(2)
- 从C到C++看面相对象(深入了解C++的成员函数)
- C++对象模型的那些事儿之六:成员函数调用方式
- C++学习之成员函数的访问属性与继承属性对虚表构建的影响--个人理解
- 《深入理解C++对象模型》何时合成Default Constructor