ATL Under the Hood - Part 1(翻译)
作者:Zeeshan Amjad
译者:姜江
QQ:457283
E-mail:jzsnmail@163.net
原文地址:http://www.codeproject.com/atl/atl_underthehood_.asp
介绍
在这一系列的教程中我准备讨论一些关于ATL内部工作原理和ATL使用技术。
让我们以一个程序的内存布局开始我们的讨论。让我们写一个简单的程序,它没有包含任何数据成员,考虑一下它的内存结构。
程序1
#include <iostream> using namespace std;
class Class { };
int main() { Class objClass; cout << "Size of object is = " << sizeof(objClass) << endl; cout << "Address of object is = " << &objClass << endl; return 0; } |
程序的输出是:
Size of object is = 1 Address of object is = 0012FF7C |
现在如果我们准备加入一些数据成员,那么类的大小是所有单独存储的成员变量的存储总和。在模版类中也是这样。现在让我们看一下模版类Point。
程序2
#include <iostream> using namespace std;
template <typename T> class CPoint { public: 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 |
现在我们在程序中加入继承。我们准备从Point类派生出Point3D,然后观察程序结构。
程序3
#include <iostream> using namespace std;
template <typename T> class CPoint { public: T m_x; T m_y; };
template <typename T> class CPoint3D : public CPoint<T> { public: T m_z; };
int main() { CPoint<int> objPoint; cout << "Size of object Point is = " << sizeof(objPoint) << endl; cout << "Address of object Point is = " << &objPoint << endl;
CPoint3D<int> objPoint3D; cout << "Size of object Point3D is = " << sizeof(objPoint3D) << endl; cout << "Address of object Point3D is = " << &objPoint3D << endl;
return 0; } |
程序输出为:
Size of object Point is = 8 Address of object Point is = 0012FF78 Size of object Point3D is = 12 Address of object Point3D is = 0012FF6C |
这个程序展示了派生类的内存结构。这个对象占用的内存大小是它的数据成员加上它的基类成员的总和。
当一个虚函数加入这个程序中事情变得有趣了,让我们看看下面这个程序。
程序4
#include <iostream> using namespace std;
class Class { public: virtual void fun() { cout << "Class::fun" << endl; } };
int main() { Class objClass; cout << "Size of Class = " << sizeof(objClass) << endl; cout << "Address of Class = " << &objClass << endl; return 0; } |
程序的输出结果是:
Size of Class = 4 Address of Class = 0012FF7C |
当我们加入一个以上的虚函数的时候事情变得更加有趣了。
程序5
#include <iostream> using namespace std;
class Class { public: virtual void fun1() { cout << "Class::fun1" << endl; } virtual void fun2() { cout << "Class::fun2" << endl; } virtual void fun3() { cout << "Class::fun3" << endl; } };
int main() { Class objClass; cout << "Size of Class = " << sizeof(objClass) << endl; cout << "Address of Class = " << &objClass << endl; return 0; } |
程序的输出的结果跟上面哪个程序一样。让我们多做一些试验来理解它。
程序6
#include <iostream> using namespace std;
class CPoint { public: int m_ix; int m_iy; virtual ~CPoint() { }; };
int main() { CPoint objPoint; cout << "Size of Class = " << sizeof(objPoint) << endl; cout << "Address of Class = " << &objPoint << endl; return 0; } |
程序的输出结果是:
Size of Class = 12 Address of Class = 0012FF68 |
这些程序的输出结果说明了当你在类中加入任何虚函数,该类增加了一个int类型的大小。也就是在Virtual C++中它增加4个字节。也就是说在这个类中有3个整型大小的位置,一个给x、一个给y还有一个给处理虚函数的指针,它被叫做虚函数指针(Virtual Pointer)。首先观察一下这个新的位置,也就是虚函数指针,它在对象的开始或者结尾。我们准备直接访问被这个对象占用的内存来看看这个虚函数指针。通过用一个巧妙的指针算法来做这个实验。
程序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; // want to change the value of x *(pInt+1) = 200; // want to change the value of y
cout << "X = " << objPoint.getX() << endl; cout << "Y = " << objPoint.getY() << endl;
return 0; } |
这个程序最重要的东西:
int* pInt = (int*)&objPoint; *(pInt+0) = 100; // want to change the value of x *(pInt+1) = 200; // want to change the value of y |
Y = 10 |
当然,这不是我们需要的结果。这个说明200存储在m_ix数据成员驻留的位置。也就是说明第一个成员变量m_ix开始于第二个内存位置而不是第一个。换句话说就是第一个成员是虚函数指针(Virtual Pointer),然后剩下的是对象的数据成员。改变下面两行程序:
int* pInt = (int*)&objPoint; *(pInt+1) = 100; // want to change the value of x *(pInt+2) = 200; // want to change the value of 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; // want to change the value of x *(pInt+2) = 200; // want to change the value of y
cout << "X = " << objPoint.getX() << endl; cout << "Y = " << objPoint.getY() << endl;
return 0; } |
程序的输出结果是:
X = 100 Y = 200 |
这清楚的说明了无论我们什么时候在类里加入虚函数,虚函数指针(Virtual Pointer)都是加在内存结构的第一个位置上。
现在问题出现了:虚函数指针里存储的是什么?看看下面的程序来理解它。
程序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 |
虚函数指针存储了被叫做虚函数表(Virtual Table)的地址。虚函数表存储了类中所有的虚函数的地址。换句话说虚函数表是一个虚函数地址的数组。让我们观察下面的程序来理解它。
程序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是typedef定义的函数指针
typedef 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 |
这个程序说明当你从多个基类派生,这个派生类有所有基类的虚函数指针。
当派生类也有序函数的时候会发生什么呢?让我们看看这个程序,更好的理解多继承的虚函数概念。
程序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 |
这个程序说明继承的虚函数在第一个虚函数指针所指向的虚函数表中。
通过static_cast我们可以获得派生类的偏移量。让我们观察下面的程序,更好的理解它。
程序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) |
该宏返回派生类对象模块中的基类虚函数指针(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; } |
派生类的内存布局是:
这个程序的输出结果是:
0 4 8 |
这个程序的输出结果说明这个宏返回基类虚函数指针(vptr)的偏移量。在Don Box的COM本质论(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; } |
程序输出结果和用途跟前面的那个程序是一样的。
让我们在程序中使用这个宏来做一些实际的事情。事实上我们可以通过在派生类内存结构中获得的基函数虚函数指针(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中其他神秘的东西。
翻译过程中难免出现错误,欢迎批评指正!!!