ATL中宏定义offsetofclass的使用

近日学习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 Derived:  public Base1 ,public Base2

offsetofclass(Base1,Derived)
 计算出 Base1 在Derived的实例对象中偏移0
字节

offsetofclass(Base2,Derived)
 计算出 Base2在Derived的实例对象中偏移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 Derived: public Base1,public Base2

所以Derived有两个虚函数表指针

如下图:

Derived覆盖了基类的相同虚函数,自己的虚函数放在第一个表中。

清楚了虚函数表,就可以通过地址来调用函数了.

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 Derived: 
public Base1 ,public Base2

{

public :

virtual void f()
{ cout << "Derived::f" <<
endl; }

virtual void g1()
{ cout << "Derived::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


时间: 2024-08-03 23:36:28

ATL中宏定义offsetofclass的使用的相关文章

ATL中宏定义offsetofclass的分析

近日学习ATL,通过对宏定义offsetofclass的解惑过程.顺便分析下虚函数表,以及通过虚函数表调用函数的问题. 1 解开ATL中宏定义offsetofclass的疑惑 #define _ATL_PACKING  8 #define offsetofclass(base, derived) ((unsigned long)(static_cast <base*>((derived*)_ATL_PACKING))-_ATL_PACKING) 分析例如以下:(base 基类 , derive

C语言中宏定义(#define)时do{}while(0)的价值(转)

C语言中宏定义(#define)时do{}while(0)的价值 最近在新公司的代码中发现到处用到do{...}while(0),google了一下,发现Stack Overflow上早有很多讨论,总结了一下讨论,加上自己的理解,do{...}while(0)的价值主要体现在: 1. 增加代码的适应性 下面的宏定义没有使用do{...}while(0) #define FOO(x) foo(x); bar(x); 这样宏定义,单独调用不会出现问题,例如: FOO(100) 宏扩展后变成: 1 f

C语言中宏定义使用方法详解

C语言中的宏替换详解 首先看一个问题: #include <stdio.h> #define    PRINT_CLINE()    printf("%d", ______) int main(void) { PRINT_CLINE(); PRINT_CLINE(); return 0; } 在横线处填上适当的代码,使得上面这段代码的输出为34. 我想一般人看到这个问题的时候头脑里都没有明确的思路来解答这个它.我看到这个问题的时候想出了各种办法来解答它,最终还是没有通过编译

C语言中宏定义之 ## 用于可变参数

GCC 支持复杂的宏,它使用一种不同的语法,使你可以给可变参数一个名字,如同其它参数一样,比如: 引用 #define debug(format, args...) fprintf(stderr, format, args) 这种定义可读性更强,也更容易描述.完整测试代码: 引用 #include <stdio.h> #define debug(format, args...) fprintf(stderr, format, args) int main(){    char a[20] = 

c语言中宏定义#和 ##的作用:

转载:http://www.cnblogs.com/cyttina/archive/2013/05/11/3072969.html 看了这篇文章后了解了,但是文章中的例子比较特别,我在这里加个注释好了. http://www.cnblogs.com/welkinwalker/archive/2012/03/30/2424844.html 单井号就是将后面的 宏参数 进行字符串操作,就是将后面的参数用双引号引起来 双井号就是用于连接. 比如文章中的例子: #define PRINT(NAME) p

解决C++中宏定义导致的名字污染

在编写一个 Graph 模板类的时候,为了使用户可以自定义 距离 的类型,比如 int 或者 double 甚至其他高精度有理数的封装类, 我将距离的类型定义为模板参数 T_DIST ,并使用了标准库中的库函数 std::numeric_limits<T_DIST>::max() 来定义最大距离 template <typename T_DIST> const typename TopologicalGraph<T_DIST>::Distance Topological

nginx中宏定义ngx_align(d, a)

nginx中的pool用到了这玩意: #define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1)) 其实这篇大部分不是解释这玩意啥用的...简单一句话就是得到一个a的倍数c,且这个倍数是d的最小弱上界,得到这个值啥好处,实际上就是得到一个pool的最小size罢了,尽量减少内存的使用. 一开始我一脸懵逼,后面搜了下这个表达式发现有人在百度知道上回答了,仔细看了下他的其实不太对,后面自己就在百度知道上添加了一些补充,现在发到博客上吧: 上面的zjuzzh

iOS开发分分钟搞定C语言 —— 宏定义和关键字

一.宏定义 概念:宏定义实质是一个预编译指令,在程序未运行之前将某些指令付给相应的变量.一般情况预处理指令都是以#号开头的,所以宏定义也是以#开发,关键字为#define(定义宏定义),#undef(结束宏定义). 定义格式及作用域 一般宏定义都定义在程序的首段: #define 宏名 值. 宏定义的作用域:从开始定义的那行起,一直到文件末尾,虽然默认情况下宏定义的作用域是从定义的那一行开始, 一直到文件末尾.但是我们也可以通过对应的关键字#under提前结束宏定义的作用域. 宏定义规范 一般情

[转载]c语言宏定义

一. #define是C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率,但学生在学习时往往不能理解该命令的本质,总是在此处产生一些困惑,在编程时误用该命令,使得程序的运行与预期的目的不一致,或者在读别人写的程序时,把运行结果理解错误,这对 C语言的学习很不利. 1 #define命令剖析 1.1   #define的概念 #define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为