ATL offsetofclass 的工作原理

 ATL深入浅出[1]

作者:ErIM Creator
日期:2007-7-20
参考:http://www.codeproject.com/atl/atl_underthehood_.asp

在这一系列的文章里,我将和大家一起讨论ATL的底层工作和ATL所使用到的技术。如果你只想尝试写个普通的ATL控件,这篇文章对你一点帮助

都没有;如果你想更好的学会使用ATL,认真往下看把。

我们先来讨论一个类(Class)的存储配置。首先来写一个类,这个类没有任何数据成员,然后来看看他的内存结构。

程序1:
#include <iostream>

using namespace std;

class Class{
};

int main()
{
 Class objClass;
 
 cout << "Size of object is = " << sizeof(Class) << endl;
 cout << "Address of object is = " << &objClass << endl;

 return 0;
}

程序输出:
Size of object is = 1
Address of object is = 0012FF7C

现在,如果我们往类里添加数据成员,他的大小会将等于所有数据成员所占内存的和。在模版类(template class)中,也是这样。现在,让我

们来看看模版类Point。

程序2:
#include <iostream>

using namespace std;

template <typename T>
class CPoint{
 T m_x;
 T m_y;
};

int main()
{
 CPoint<int> objPoint;
 
 cout << "Size of object is = " << sizeof(objPoint) << endl;
 cout << "Address of object is = " << &objPoint << endl;

 return 0;
}
程序输出:
Size of object is = 8
Address of object is = 0012FF78

现在,我们也添加一个继承类到这个程序,我们用Point3D类来继承Point类,同时来看看程序的内存结构。

程序3:
#include <iostream>

using namespace std;

template <typename T>
class CPoint{
 T m_x;
 T m_y;
};

template <typename T>
class CPoint3D : public CPoint<T>
{
 T m_z;
};
int main()
{
 CPoint<int> objPoint;
 
 cout << "Size of Point is = " << sizeof(objPoint) << endl;
 cout << "Address of Point is = " << &objPoint << endl;

 CPoint3D<int> objPoint3D;

 cout << "Size of Point3D is = " << sizeof(objPoint3D) << endl;
 cout << "Address of Point3D is = " << &objPoint3D << endl;
 
 return 0;
}
程序输出:
Size of Point is = 8
Address of Point is = 0012FF78
Size of Point3D is = 12
Address of Point3D is = 0012FF6C

这个程序说明了派生类的内存结构。派生类所占用的内存总数是基类的所有数据成员和该派生类所有数据成员所占内存的和。


如果把虚函数也添加进来,那这个问题就更有趣了。我们来看看这个程序。
程序4:
#include <iostream>

using namespace std;

class Class
{
 virtual void fun()
 {
  cout << "Class::fun" << endl;
 }
};

void main()
{
 Class objClass;
 cout << "size of class = " << sizeof(objClass) << endl;
 cout << "Address of class = " << & objClass << endl;
}
程序输出:
size of class = 4
Address of class = 0012FF7C

如果我们添加一个以上的需函数,那么情况会变得更有去。

程序5:
#include <iostream>

using namespace std;

class Class
{
 virtual void fun1()
 {
  cout << "Class::fun" << endl;
 }
 virtual void fun2()
 {
  cout << "Class::fun2" << endl;
 }
 virtual void fun3()
 {
  cout << "Class::fun3" << endl;
 }
};

void main()
{
 Class objClass;
 cout << "size of class = " << sizeof(objClass) << endl;
 cout << "Address of class = " << & objClass << endl;
}
程序的输出和上面一样。我们来做多点实验,使我们能更好地理解他。

程序6:
#include <iostream>

using namespace std;

class CPoint
{
 int m_x;
 int m_y;
public:
 virtual ~CPoint(){
 };
};

void main()
{
 CPoint objPoint;
 cout << "size of Point = " << sizeof(objPoint) << endl;
 cout << "Address of Point = " << &objPoint << endl;
}

程序输出:
size of Point = 12
Address of Point = 0012FF68

这个程序的输出告诉我们,无论你在类里添加多少个虚函数,他的大小只增加一个int数据类型所占的内存空间,例如在Visual C++ 他增加4字

节。他表示有三块内存给这个类的整数,一个给m_x,一个给m_y,还有一个用来处理被调用的虚函数的虚指针。首先来看一看新的一块内存,也

就是虚函数指针所占内存,在该对象的最开始头(或者最后)。我们可以通过直接访问对象所占的内存块来调用虚函数。只要把对象的地址保

存到一个int类型指针,然后使用指针算法的魔术(The magic of pointer arithmetic)就能够调用他了。

程序7:
#include <iostream>

using namespace std;

class CPoint {
 
public:
 
 int m_ix;
 int m_iy;
 CPoint(const int p_ix = 0, const int p_iy = 0) :
 m_ix(p_ix), m_iy(p_iy) {
 }
 int getX() const {
  return m_ix;
 }
 int getY() const {
  return m_iy;
 }
 virtual ~CPoint() { };
};
int main() {
 
 CPoint objPoint(5, 10);

 int* pInt = (int*)&objPoint;
 *(pInt+0) = 100; // 打算改变 x 的值
 *(pInt+1) = 200; // 打算改变 y 的值

 cout << "X = " << objPoint.getX() << endl;
 cout << "Y = " << objPoint.getY() << endl;

 return 0;
}
这个程序最重要的地方是:
int* pInt = (int*)&objPoint;
*(pInt+0) = 100; // 打算改变 x 的值
*(pInt+1) = 200; // 打算改变 y 的值

这里我们把对象的地址保存到一个int类型的指针,然后把它当成整型指针。
程序输出:
X = 200
Y = 10
当然,这不是我们想要的结果!程序说明,这时的200是保存到x_ix,而不是x_iy。也就是说对象第一个数据成员是从内存的第二个地址开始的

,而不是第一个。换句话说,第一个内存地址保存的是虚函数的地址,然后其他保存的都是类的数据成员。我们来改一下下面两行代码。
int* pInt = (int*)&objPoint;
*(pInt+1) = 100; // 打算改变 x 的值
*(pInt+2) = 200; // 打算改变 y 的值

这时我们得到了所期待的结果。

程序8:
#include <iostream>

using namespace std;

class CPoint {
public:
         int m_ix;
         int m_iy;

         CPoint(const int p_ix = 0, const int p_iy = 0) :
                 m_ix(p_ix), m_iy(p_iy) {
         }

         int getX() const {
                 return m_ix;
         }
         int getY() const {
                 return m_iy;
         }

         virtual ~CPoint() { };

};

int main() {

         CPoint objPoint(5, 10);

         int* pInt = (int*)&objPoint;
         *(pInt+1) = 100; // 想要改变 x 的值
         *(pInt+2) = 200; // 想要改变 y 的值

         cout << "X = " << objPoint.getX() << endl;
         cout << "Y = " << objPoint.getY() << endl;

         return 0;

}

程序的输出:
X = 100
Y = 200

这里很明确的告诉我们,无论什么时候我们添加虚函数到类里面,虚指针都存放在内存的第一个位置。

现在问题出现了:虚指针里存放的是什么?来看看以下程序,我们就能知道它是什么概念。
程序9:
#include <iostream>

using namespace std;

class Class {

         virtual void fun() { cout << "Class::fun" << endl; }

};

int main() {

         Class objClass;

         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;
         cout << "Value at virtual pointer " << (int*)*(int*)(&objClass+0) << endl;

         return 0;

}
程序输出:

Address of virtual pointer 0012FF7C
Value at virtual pointer 0046C060

虚指针保存一张叫做虚表的地址。而虚表保存着整个类的虚函数地址。换句话说,虚表是一个存放虚函数地址的数组。让我们看一下下面的程

序来理解这种思想。

程序10:
#include <iostream>

using namespace std;

 

class Class {

         virtual void fun() { cout << "Class::fun" << endl; }

};

 

typedef void (*Fun)(void);

 

int main() {

         Class objClass;

 

         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;

         cout << "Value at virtual pointer i.e. Address of virtual table "

                  << (int*)*(int*)(&objClass+0) << endl;

         cout << "Value at first entry of virtual table "

                  << (int*)*(int*)*(int*)(&objClass+0) << endl;

 

         cout << endl << "Executing virtual function" << endl << endl;

         Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);
         pFun();
         return 0;

}
这个程序有些不常用的间接类型转换。这个程序最重要的地方是

Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);

这里 Fun 是一个typeded 函数指针。

typdef void (*Fun)(void):

我们来详细研究这种不常用的间接转换。
(int*)(&objClass+0)给出类第一个入口的虚函数的指针然后把他类型转换成int*.我们使用间接操作符(也就是 *)来获取这个地址的值然后再

次类型转换成 int* 也就是(int*)*(int*)(&objClass+0).这将会给出虚函数地址表的入口地址.获得这个位置的值,也就是得到类的第一个虚

函数的指针,再次使用间接操作符再类型转换成相应的函数指针类型。像这样:
Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);
表示获取虚函数地址表第一个入口的值然后类换成Fun类型后保存到pFun.

添加多一个虚函数到类里面会怎么样。我们现在想要访问虚函数地址表里第二个成员。查看以下程序,看看虚函数地址表里的值。

程序11:
#include <iostream>

using namespace std;
class Class {
         virtual void f() { cout << "Class::f" << endl; }
         virtual void g() { cout << "Class::g" << endl; }
};

int main() {

         Class objClass;
         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;
         cout << "Value at virtual pointer i.e. Address of virtual table "
                 << (int*)*(int*)(&objClass+0) << endl;

         cout << endl << "Information about VTable" << endl << endl;
         cout << "Value at 1st entry of VTable "
                 << (int*)*((int*)*(int*)(&objClass+0)+0) << endl;

         cout << "Value at 2nd entry of VTable "
                 << (int*)*((int*)*(int*)(&objClass+0)+1) << endl;

         return 0;
}
程序输出:
Address of virtual pointer 0012FF7C
Value at virtual pointer i.e. Address of virtual table 0046C0EC
Information about VTable
Value at 1st entry of VTable 0040100A
Value at 2nd entry of VTable 0040129E

现在我们心理产生一个问题。编译器怎么知道虚函数地址表的长度呢?答案是:虚函数地址表最后的入口等于NULL。把程序做些小改动来理解

它。

程序12:
#include <iostream>

using namespace std;

class Class {
         virtual void f() { cout << "Class::f" << endl; }
         virtual void g() { cout << "Class::g" << endl; }
};

int main() {
         Class objClass;

         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;
         cout << "Value at virtual pointer i.e. Address of virtual table "
                  << (int*)*(int*)(&objClass+0) << endl;
         cout << endl << "Information about VTable" << endl << endl;
         cout << "Value at 1st entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+0) << endl;
         cout << "Value at 2nd entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+1) << endl;
         cout << "Value at 3rd entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+2) << endl;
         cout << "Value at 4th entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+3) << endl;
         return 0;
}

程序输出:
Address of virtual pointer 0012FF7C
Value at virtual pointer i.e. Address of virtual table 0046C134

Information about VTable

Value at 1st entry of VTable 0040100A
Value at 2nd entry of VTable 0040129E
Value at 3rd entry of VTable 00000000
Value at 4th entry of VTable 73616C43

这个程序的输出显示了虚函数地址表的最后一个入口等于NULL.我们用我们所了解的知识来调用虚函数

程序13:
#include <iostream>

using namespace std;

class Class {
         virtual void f() { cout << "Class::f" << endl; }
         virtual void g() { cout << "Class::g" << endl; }
};

typedef void(*Fun)(void);

int main() {

         Class objClass;

         Fun pFun = NULL;

         // calling 1st virtual function

         pFun = (Fun)*((int*)*(int*)(&objClass+0)+0);

         pFun();

         // calling 2nd virtual function

         pFun = (Fun)*((int*)*(int*)(&objClass+0)+1);

         pFun();

         return 0;
}

这个程序的输出是:

Class::f
Class::g

现在我们来看一下多层继承的情况。以下是个简单的多层继承的情况。

程序14:
#include <iostream>
using namespace std;
class Base1 {
public:
         virtual void f() { }

};
class Base2 {
public:
         virtual void f() { }
};
class Base3 {
public:
         virtual void f() { }
};
class Drive : public Base1, public Base2, public Base3 {
};
int main() {

         Drive objDrive;
         cout << "Size is = " << sizeof(objDrive) << endl;

         return 0;
}

程序输出:
Size is = 12

程序表明,当你的 drive 类具有一个以上的基类时,drive 类就具有所有基类的虚函数指针。
如果 drive 类也具有虚函数那会怎么样呢。我们来看看这个程序以便更好的了解多继承虚函数的概念。

程序15:
#include <iostream>

using namespace std;

 

class Base1 {

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

         virtual void g() { cout << "Base1::g" << endl; }

};

 

class Base2 {

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

         virtual void g() { cout << "Base2::g" << endl; }

};

 

class Base3 {

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

         virtual void g() { cout << "Base3::g" << endl; }

};

 

class Drive : public Base1, public Base2, public Base3 {

public:

         virtual void fd() { cout << "Drive::fd" << endl; }

         virtual void gd() { cout << "Drive::gd" << endl; }

};

 

typedef void(*Fun)(void);

 

int main() {

         Drive objDrive;

 

         Fun pFun = NULL;

 

         // calling 1st virtual function of Base1

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+0);

         pFun();

        

         // calling 2nd virtual function of Base1

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+1);

         pFun();

 

         // calling 1st virtual function of Base2

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+1)+0);

         pFun();

 

         // calling 2nd virtual function of Base2

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+1)+1);

         pFun();

 

         // calling 1st virtual function of Base3

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+2)+0);

         pFun();

 

         // calling 2nd virtual function of Base3

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+2)+1);

         pFun();

 

         // calling 1st virtual function of Drive

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+2);

         pFun();

 

         // calling 2nd virtual function of Drive

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+3);

         pFun();

 

         return 0;

}

程序输出:
Base1::f

Base1::g

Base2::f

Base2::f

Base3::f

Base3::f

Drive::fd

Drive::gd

这个程序显示 drive 的虚函数保存在 vptr 的第一个 虚函数地址表.
我们在 static_cast 的帮助下我门能够获取 Drive 类 vptr 的偏移量.我们看一下以下的程序来更好地理解他。

程序16:
#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { }

};

 

class Base2 {

public:

         virtual void f() { }

};

 

class Base3 {

public:

         virtual void f() { }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

// any non zero value because multiply zero with any no is zero

#define SOME_VALUE        1

 

int main() {

         cout << (DWORD)static_cast<Base1*>((Drive*)SOME_VALUE)-SOME_VALUE << endl;

         cout << (DWORD)static_cast<Base2*>((Drive*)SOME_VALUE)-SOME_VALUE << endl;

         cout << (DWORD)static_cast<Base3*>((Drive*)SOME_VALUE)-SOME_VALUE << endl;

         return 0;

}

ATL 使用一个叫做offsetofclass 的宏来这么做,该宏定义在 ATLDEF.h 里。宏的定义是:
#define offsetofclass(base, derived) /
       ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)
这个宏返回在 drive 类对象模型里的基类的vptr的偏移量。我们来看一个例子了解这个概念。
程序17:
#include <windows.h>

#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { }

};

 

class Base2 {

public:

         virtual void f() { }

};

 

class Base3 {

public:

         virtual void f() { }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

#define _ATL_PACKING 8

 

#define offsetofclass(base, derived) /

         ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)

 

int main() {

         cout << offsetofclass(Base1, Drive) << endl;

         cout << offsetofclass(Base2, Drive) << endl;

         cout << offsetofclass(Base3, Drive) << endl;

         return 0;

}

这是 drive 类的内存层次
程序输出是:
0
4
8
程序的输出显示,这个宏返回了所要的基类vptr的偏移量。在 Don Box 的《Essential COM》,他使用一个类似的宏来这么做。把程序做些小改动来用Box的宏取代ATL 的宏。
程序18:
#include <windows.h>

#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { }

};

 

class Base2 {

public:

         virtual void f() { }

};

 

class Base3 {

public:

         virtual void f() { }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

#define BASE_OFFSET(ClassName, BaseName) /

         (DWORD(static_cast<BaseName*>(reinterpret_cast<ClassName*>/

         (0x10000000))) - 0x10000000)

 

int main() {

         cout << BASE_OFFSET(Drive, Base1) << endl;

         cout << BASE_OFFSET(Drive, Base2) << endl;

         cout << BASE_OFFSET(Drive, Base3) << endl;

         return 0;

}

程序的目的很输出和前面的程序一样。
我们用这个宏在我们的程序做些实用的事。事实上,我们可以通过获取在 drive的内存结构里基类的vptr来调用所要的基类的虚函数。

程序19:
#include <windows.h>

#include <iostream>

using namespace std;

 

class Base1 {

public:

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

};

 

class Base2 {

public:

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

};

 

class Base3 {

public:

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

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

#define _ATL_PACKING 8

 

#define offsetofclass(base, derived) /

         ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)

 

int main() {

         Drive d;

 

         void* pVoid = NULL;

 

         // call function of Base1

         pVoid = (char*)&d + offsetofclass(Base1, Drive);

         ((Base1*)(pVoid))->f();

 

         // call function of Base2

         pVoid = (char*)&d + offsetofclass(Base2, Drive);

         ((Base2*)(pVoid))->f();

 

         // call function of Base1

         pVoid = (char*)&d + offsetofclass(Base3, Drive);

         ((Base3*)(pVoid))->f();

 

         return 0;

}

这个程序的输出:

Base1::f()

Base2::f()

Base3::f()

在这个指南里,我设法解释 ATL 里 offsetofclass 宏的工作原理。我希望在下一篇文章探测其他神秘的ATL。

你可能感兴趣的:(工作,function,object,Class,iostream,fun)