近日学习ATL,通过对宏定义
offsetofclass的解惑过程,顺便分析下虚函数表,以及通过虚函数表调用函数的问题。
1
解开ATL中宏定义offsetofclass的疑惑
#define
_ATL_PACKING 8
#define
offsetofclass(base, derived)
((
unsigned
long
)(
static_cast
<base*>((derived*)
_ATL_PACKING
))-
_ATL_PACKING
)
分析如下:(base 基类 , derived 子类)
- (derived*) 8 就是把指针指向地址8,这样就不用自己新创建类的对象。
- 此后又static_cast <base*>,将指针转为基类指针,这个过程,指针的值实际发生了变化,如果有偏移,那么此时已经指到新的地址,比如12或16(32位系统指针为4字节)
- 12减去8 就是最后得到的偏移量4
- 可以看出,_ATL_PACKING 实际上可以是任意非0值,它只是一个地址值,只要不是0,正负均可。
由此得出
offsetofclass 用来计算基类(base)指针在子类(
derived
)对象中的偏移量,也可以理解为基类虚函数表在子类对象中的偏移量。
因为虚函数表指针就在所有对象的开头位置。
此时大家多有疑问,为什么不通过类对象来计算?
有一个问题,如果
子类是个虚类,它根本就不能创建类对象,所以就没法计算,这个方法解决了虚类的问题,它只是用了下这个地址,并没有修改数据。
(这样任意指向内存地址,不知道有何风险?)
如有两个基类,就有两个虚函数表指针。
class D
erived
: public Base1 ,public Base2
offsetofclass(Base1,
D
erived
)
计算出
Base1
在
D
erived
的实例对象中偏移0 字节
offsetofclass(Base2,
D
erived
)
计算出
Base2
在
D
erived
的实例对象中偏移4 字节
2
通过偏移来指定基类在子类对象中的地址
Derived d;
Derived *pD= &d; //
pD地址0x0018fe98
Base2 * pB2 = pD; //传递给pB2,地址为0x0018fe9c,偏移了4个字节
pB2 =
(
Base2
*)((
int
)(&d) + 4); // 通过偏移也可以得到Base2
pB2 和pD地址并不相同
,而指针判断却相等。
if(pD == pB)
{
// 两个指针的比较
// pD地址
0x0018fe98,pB地址
0x0018fe9c
// 为什么还是相等呢,pD,pB指向的类型不同,pD先转换成基类
pB
类型,再进行比较。
}
如下图:
3 通过虚函数表来调用父类或子类中成员函数
虚函数表,几个基类分支,就有几个虚函数表指针
class D
erived: public Base1,public Base2
所以D
erived有两个虚函数表指针
如下图:
D
erived覆盖了基类的相同虚函数,自己的虚函数放在第一个表中。
清楚了虚函数表,就可以通过地址来调用函数了.
typedef
void
(*Fun)(
void
); //函数指针
Derived d;
int
**pVtab = (
int
**)&d;
Fun pFun = (Fun)pVtab[0][0];
//等同于 调用 Fun pFun = (Fun)*((int*)*(int*)((int*)&b + 0) + 0);
pFun();
以此类推,调用
pVtab[0][1],
pVtab[0][2],
pVtab[1][0],
pVtab[1][1]
输出如下图:
4 如下为全部代码部分
// 测试分3次进行,进行测试1时,请注释掉其他部分,以此类推。
#include
<iostream>
using
namespace
std;
#define
_ATL_PACKING 8 // 尝试修改下 非0 即可
#define
offsetofclass(base, derived) ((
unsigned
long
)(
static_cast
<base*>((derived*)
_ATL_PACKING
))-
_ATL_PACKING
)
typedef
void
(*Fun)(
void
);
class Base1
{
public
:
virtual
void
f() { cout <<
"Base1::f"
<< endl; }
virtual
void
g() { cout <<
"Base1::g"
<< endl; }
};
class Base2
{
public
:
virtual
void
f() { cout <<
"Base2::f"
<< endl; }
virtual
void
g() { cout <<
"Base2::g"
<< endl; }
// void h(){ cout << "Base2::h" << endl; } // 测试2使用: 非虚函数,此函数不在虚表中
};
class D
erived
: public Base1 ,public Base2
{
public
:
virtual
void
f() { cout <<
"
D
erived
::f"
<< endl; }
virtual
void
g1() { cout <<
"
D
erived
::g1"
<< endl; }
// virtual void h() = 0; //测试1使用:子类为虚类时,计算偏移
};
int
main(
int
argc,
char
* argv[])
{
//测试1:子类为虚类时,计算偏移
unsigned
long
nOffset1 =0,
nOffset2=0
;
nOffset1
= offsetofclass(Base1,Derived); // 计算后
nOffset1
=0
nOffset2
= offsetofclass(Base2,Derived); // 计算后
nOffset2
= 4
//测试2:创建对象
unsigned
long
nOffset1 =0,
nOffset2=0
;
nOffset1
= offsetofclass(Base1,Derived); // 计算后
nOffset1
=0
nOffset2
= offsetofclass(Base2,Derived); // 计算后
nOffset2
= 4
Derived d;
Derived *pD= &d; //pD地址0x0018fe98
Base2 * pB2 = pD; // 传递给pB2,地址为0x0018fe9c,偏移了4个字节
pB2 =
(
Base2
*)((
int
)(&d) +
nOffset2
); // 通过偏移也可以得到Base2
// 测试3 通过虚函数表调用 函数
Derived d;
Derived *pD= &d;
int
**pVtab = (
int
**)&d;
Fun pFun = (Fun)pVtab[0][0];
//等同于 调用 Fun pFun = (Fun)*((int*)*(int*)((int*)&d+ 0) + 0);
pFun();
pFun = (Fun)pVtab[0][1];
//等同于 调用 pFun = (Fun)*((int*)*(int*)((int*)&d + 0 ) + 1);
pFun();
pFun = (Fun)pVtab[0][2];
//等同于 调用 pFun = (Fun)*((int*)*(int*)((int*)&d + 0) + 2);
pFun();
pFun = (Fun)pVtab[1][0];
//等同于 调用 pFun = (Fun)*((int*)*(int*)((int*)&d + 1) + 0);
pFun();
pFun = (Fun)pVtab[1][1];
//等同于 调用 pFun = (Fun)*((int*)*(int*)((int*)&d + 1) + 1);
pFun();
int
nWait=0;
cin >> nWait;
}
写本文之前阅读参考了以下文章:
1: http://blog.csdn.net/haoel/article/details/1948051/
对于这篇文章中提到的 虚函数表在(
Windows XP+VS2003)的末尾是个 NULL值,但笔者用(vs2003和vs2013 +win7 debug,release)测试后 末尾并非一定是NULL,值不确定。
2: http://blog.csdn.net/wishfly/article/details/2046361
3: http://c.chinaitlab.com/basic/748017.html