近日学习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]

输出例如以下图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveHV3ZWlxdW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">


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;
}

写本文之前阅读參考了下面文章:
对于这篇文章中提到的 虚函数表在(Windows XP+VS2003)的末尾是个 NULL值,但笔者用(vs2003和vs2013 +win7 debug,release)測试后 末尾并不是一定是NULL。值不确定。




ATL中宏定义offsetofclass的分析的更多相关文章

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

    C语言中宏定义(#define)时do{}while(0)的价值 最近在新公司的代码中发现到处用到do{...}while(0),google了一下,发现Stack Overflow上早有很多讨论,总 ...

  2. c 语言中宏定义和定义全局变量的区别

    宏定义和定义全局变量的区别: 1 作用时间不同. 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以. 2 本质类型不同. 宏定义的只是一段字符,在编译的时候被替换到引用的位置.在运行中是没有 ...

  3. Linux内核导出符号宏定义EXPORT_SYMBOL源代码分析

    资源: <include/linux/moudule.h> --. #ifndef MODULE_SYMBOL_PREFIX #define MODULE_SYMBOL_PREFIX &q ...

  4. C语言中宏定义与C++中的内联函数

    一,宏定义:在预处理的时候把宏定义的内容替换到代码中,正常编译. 1,无参数宏定义和有参数宏定义 (1)宏定义不能加分号,比如:#define  PI 3.24;错的,#define  PI 3.24 ...

  5. C语言-宏定义与使用分析

    1.C语言中的宏定义 #define是预处理器处理的单元实体之— #define定义的宏可以出现在程序的任意位置 #define定义之后的代码都可以使用这个宏 2.定义宏常量 #define定义的宏常 ...

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

    最近在新公司的代码中发现到处用到do{...}while(0),google了一下,发现Stack Overflow上早有很多讨论,总结了一下讨论,加上自己的理解,do{...}while(0)的价值 ...

  7. c/c++中宏定义##连接符 和#符的使用

    C语言中如何使用宏C(和C++)中的宏(Macro)属于编译器预处理的范畴,属于编译期概念(而非运行期概念).下面对常遇到的宏的使用问题做了简单总结.关于#和##在C语言的宏中,#的功能是将其后面的宏 ...

  8. c语言中宏定义和常量定义的区别

    他们有共同的好处就是"一改全改,避免输入错误"哪两者有不同之处吗?有的. 主要区别就在于,宏定义是在编译之前进行的,而const是在编译阶段处理的 宏定义不占用内存单元而const ...

  9. Makefile中宏定义

    实际上是gcc命令支持-D宏定义,相当于C中的全局#define: gcc -D name gcc -D name=definition Makefile中可以定义变量(和宏很像),但是是给make解 ...

随机推荐

  1. UVA-12083 Guardian of Decency 二分图 最大独立集

    题目链接:https://cn.vjudge.net/problem/UVA-12083 题意 学校组织去郊游,选择最多人数,使得任意两个人之间不能谈恋爱 不恋爱条件是高差大于40.同性.喜欢的音乐风 ...

  2. HDU-1043 Eight八数码 搜索问题(bfs+hash 打表 IDA* 等)

    题目链接 https://vjudge.net/problem/HDU-1043 经典的八数码问题,学过算法的老哥都会拿它练搜索 题意: 给出每行一组的数据,每组数据代表3*3的八数码表,要求程序复原 ...

  3. /etc/rsyncd.conf

    [root@backup ~]# cat /etc/rsyncd.conf #Rsync server#created by oldboy ##rsyncd.conf start##uid = rsy ...

  4. MySQL中将数据库表名修改成大写的存储过程

    原文:MySQL中将数据库表名修改成大写的存储过程 MySQL中将数据库表名修改成大写的存储过程 创建存储过程的代码: DROP PROCEDURE IF EXISTS uppercaseTablen ...

  5. 素数计数函数$\pi(x)\sim \Theta(\frac{x}{\log{x}})$的一个初等方法——素数定理的估计

    $\DeclareMathOperator{\lcm}{lcm}$ 本文的方法来源于GTM 190:"Problems in Algebraic Number Theory",给出 ...

  6. LibSVM-windows

    本系列文章由 @YhL_Leo 出品,转载请注明出处. 文章链接: http://blog.csdn.net/yhl_leo/article/details/50112477 官方Web: https ...

  7. 【转】Hook钩子C#实例

    [转]Hook钩子C#实例 转过来的文章,出处已经不知道了,但只这篇步骤比较清晰,就贴出来了. 一.写在最前 本文的内容只想以最通俗的语言说明钩子的使用方法,具体到钩子的详细介绍可以参照下面的网址: ...

  8. reactor模式与java nio

     Reactor是由Schmidt, Douglas C提出的一种模式,在高并发server实现中广泛採用. 改模式採用事件驱动方式,当事件出现时,后调用对应的事件处理代码(Event Handl ...

  9. 集团公司(嵌入ETL工具)財务报表系统解决方式

    集团公司(嵌入ETL工具)財务报表系统解决方式 一.项目背景: 某集团公司是一家拥有100多家子公司的大型集团公司,旗下子公司涉及各行各业,包含:金矿.铜矿.房产.化纤等.因为子公司在业务上的差异.子 ...

  10. Android 购物车的实现

    实现了购物车的全选 全不选  选中删除   选中状态下数量添加时总价随之添加等基本功能. watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L ...