介绍
在本系列的教程中,我要讨论一些ATL的内部工作方式以及它所使用的技术。
在讨论的开始,让我们先看看一个程序的内存分布。首先,编写一个简单的程序,它没有任何的数据成员,你可以看看它的内存结构。
程序1.
这个程序的输出为:
现在,如果我们向类中添加一些数据成员,那么这个类的大小就会是各个成员的大小之和。对于模板,也依然是这样:
程序2.
现在程序的输出为:
那么,再向程序中添加继承。现在我们使Point3D类继承自Point类,然后来看看程序的内存结构:
程序3.
程序的输出为:
这一程序演示了派生类的内存结构,它表明派生类的对象所占据的内存为它本身的数据成员和它基类的成员之和。
当虚函数加入到这个派对中的时候,一切就变得都有意思了。请看下面的程序:
程序4.
程序的输出为:
并且,在我们添加了多于一个的虚函数之后,会变得更加有趣:
程序5.
这个程序的输出和前一个程序一模一样,让我们再做一个实验来更好地弄懂这件事吧。
程序6.
程序的输出为:
这些程序的输出表明,当你向类中添加了虚函数之后,那么它的尺寸就会增加一个int的大小。在Visual C++中也就是增加4个字节。这就意味着这个类中有三个整数大小的位置,一个是x,一个是y,另一个是处理虚函数之用的虚函数表指针。首先,让我们来看看这个新的位置,也就是这个位于对象首部(或末尾)的虚函数表指针。要这么做的话,我们需要直接存取对象所占用的内存。我们可以使用神奇的指针技术,即用一个指向int的指针来存储一个对象的地址。
程序7.
这个程序中最重要的东西是:
其中,我们在把对象的地址存入一个整型指针之后,就可以把这个对象看作一个整型的指针了。程序的输出为:
当然,这并不是我们想要的结果,它表明200存储在了m_ix数据成员的位置。这就意味着m_ix,也就是第一个成员变量,是起始于内存中第二个位置的,而不是第一个。换句话说,第一个成员是虚函数表指针,其它的才是对象的数据成员。那么,只需要修改以下的两行:
这样我们就会获得想要的结果了,以下为完整程序:
程序8.
并且,程序的输出为:
下图清楚地示范了当我们向类中添加了虚函数之后,虚函数表指针就会被添加在内存结构中的第一个位置。
现在问题出现了:虚函数表指针中到底存储了什么?那么看看下面的程序:
程序9.
程序的输出为:
虚函表数指针存储了一个虚函数表的地址。并且,虚函数表存储了类中所有虚函数的地址。换句话说,虚函数表是一个数组,这个数组的元素为虚函数指针的地址。让我们看看下面这个程序来考虑吧。
程序10.
这个程序中有一些使用了类型转换的间接调用,其中最重要的一行是:
在这里,Fun为一个由typedef定义的函数指针类型:
让我们来剖析一下这个冗长的间接调用。(int*)(&objClass+0)给出了虚函数表指针的地址,这个虚函数表指针是类的第一个入口,我们将它转换为了int*。要获得这个地址的值,我们需要使用间接调用运算符(也就是*),然后将它再次转换为int*,也就是(int*)*(int*)(&objClass+0)。这就会给出虚函数表的第一个入口。要获得这个位置的值,也就是获得类中第一个虚函数的地址,我们需要再次使用间接调用运算符,并将其转换为合适的函数指针类型,所以 Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);
就表示从虚函数表的第一个入口获得值,作Fun的类型转换,并将其存入pFun之中。
那么,当类中含有多于一个的虚函数,又会怎么样呢?现在我们希望存取虚函数表中的第二个虚函数,请看下面的程序:
程序11.
程序的输出为:
那么,有一个问题很自然地出现了——编译器是如何知道虚函数表的长度的呢?答案是:虚函数表的最后一个入口为NULL。你可以把程序改一改来考虑这个问题。
程序12.
程序的输出为:
这个程序的输出示范了虚函数表的最后一个入口为NULL。就让我们来用已有的知识来调用虚函数吧:
程序13.
程序的输出为:
现在我们来看看多重继承的情况。先看一个多重继承最简单的情况:
程序14.
程序的输出为:Size is = 12
这个程序示范了当你从多个基类继承一个类的时候,这个派生类就会拥有所有基类的虚函数表指针。
那么,当派生类也有虚函数表指针的时候会发生什么事情呢?让我们看看下面的程序来弄懂关于带有虚函数的多重继承的概念吧。
程序15.
程序的输出为:
我们可以通过使用static_cast获得派生类虚函数表指针的偏移量,请看以下程序:
程序16.
ATL使用了一个定义在ATLDEF.H中的宏offsetofclass来做这件事,这个宏被定义为:
这个宏返回了在派生类对象模型中基类虚函数表指针的偏移量,让我们来看看下面这个例子:
程序17.
派生类的内存布局为:
程序的输出为:
这个程序的输出示范了这个宏返回指定基类的虚函数表指针偏移量。在Don Box的《COM本质论》中,它使用了一个简单的的宏,你可以修改这个程序,用Box的宏替换ATL的宏。
程序18.
这一程序的目的和输出与前一个程序完全相同。
现在让我们用这个宏来做些特别的东西,事实上我们可以通过获得派生类内存结构中基类虚函数表指针的偏移量的方法来调用指定基类中的虚函数。
程序19.
程序的输出为:
在本章教程中,我尝试着解释了ATL中offsetofclass宏的工作方式。我希望在下一篇文章中,继续探究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 |
现在,如果我们向类中添加一些数据成员,那么这个类的大小就会是各个成员的大小之和。对于模板,也依然是这样:
程序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 |
那么,再向程序中添加继承。现在我们使Point3D类继承自Point类,然后来看看程序的内存结构:
程序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的大小。在Visual C++中也就是增加4个字节。这就意味着这个类中有三个整数大小的位置,一个是x,一个是y,另一个是处理虚函数之用的虚函数表指针。首先,让我们来看看这个新的位置,也就是这个位于对象首部(或末尾)的虚函数表指针。要这么做的话,我们需要直接存取对象所占用的内存。我们可以使用神奇的指针技术,即用一个指向int的指针来存储一个对象的地址。
程序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的值 |
其中,我们在把对象的地址存入一个整型指针之后,就可以把这个对象看作一个整型的指针了。程序的输出为:
X = 200 Y = 10 |
当然,这并不是我们想要的结果,它表明200存储在了m_ix数据成员的位置。这就意味着m_ix,也就是第一个成员变量,是起始于内存中第二个位置的,而不是第一个。换句话说,第一个成员是虚函数表指针,其它的才是对象的数据成员。那么,只需要修改以下的两行:
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为一个由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; // 调用第一个虚函数 pFun = (Fun)*((int*)*(int*)(&objClass+0)+0); pFun(); // 调用第二个虚函数 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; // 调用Base1的第一个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+0); pFun(); // 调用Base1的第二个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+1); pFun(); // 调用Base2的第一个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+1)+0); pFun(); // 调用Base2的第二个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+1)+1); pFun(); // 调用Base3的第一个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+2)+0); pFun(); // 调用Base3的第二个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+2)+1); pFun(); // 调用派生类的第一个虚函数 pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+2); pFun(); // 调用派生类的第二个虚函数 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 { }; // 任意的非0值,因为0乘任何数都得0 #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使用了一个定义在ATLDEF.H中的宏offsetofclass来做这件事,这个宏被定义为:
#define offsetofclass(base, derived) / ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING) |
这个宏返回了在派生类对象模型中基类虚函数表指针的偏移量,让我们来看看下面这个例子:
程序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 |
这个程序的输出示范了这个宏返回指定基类的虚函数表指针偏移量。在Don Box的《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; } |
这一程序的目的和输出与前一个程序完全相同。
现在让我们用这个宏来做些特别的东西,事实上我们可以通过获得派生类内存结构中基类虚函数表指针的偏移量的方法来调用指定基类中的虚函数。
程序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; // 调用Base1的函数 pVoid = (char*)&d + offsetofclass(Base1, Drive); ((Base1*)(pVoid))->f(); // 调用Base2的函数 pVoid = (char*)&d + offsetofclass(Base2, Drive); ((Base2*)(pVoid))->f(); // 调用Base3的函数(译注:原文为Base1) pVoid = (char*)&d + offsetofclass(Base3, Drive); ((Base3*)(pVoid))->f(); return 0; } |
程序的输出为:
Base1::f() Base2::f() Base3::f() |
在本章教程中,我尝试着解释了ATL中offsetofclass宏的工作方式。我希望在下一篇文章中,继续探究ATL中其它的秘密。