65.9K
CodeProject 正在变化。 阅读更多。
Home

ATL 内部机制 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (99投票s)

2002年1月28日

CPOL

6分钟阅读

viewsIcon

377768

在本系列教程中,我将讨论 ATL 的一些内部工作原理以及 ATL 使用的技术。

引言

在本系列教程中,我将讨论 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

现在,如果我们添加一些数据成员,则类的 size 将是所有单个成员变量存储的总和。模板也是如此。现在,让我们看一下 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 的大小。即,在 Visual C++ 中,它会增加 4 个字节。这意味着这个类中有 3 个整数槽,一个用于 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;	// 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
我们在其中将对象视为整数指针,之后将其地址存储在整数指针中。此程序的输出是
X = 200
Y = 10

当然,这不是我们期望的结果。这表明当 200 存储在 m_ix 数据成员驻留的位置时。这意味着 m_ix,即第一个成员变量,从内存的第二个位置开始,而不是第一个。换句话说,第一个成员是虚指针,然后其余的是对象的成员数据。只需更改以下两行:

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

这清楚地表明,每当我们向类中添加虚函数时,虚指针就会被添加到内存结构的第一位置。


现在问题出现了:虚指针中存储了什么?看看下面的程序,对这个有所了解。

程序 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'd 函数指针。
	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

现在自然会产生一个问题。编译器如何知道 vtable 的长度?答案是 vtable 的最后一个条目是 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

此程序的输出表明 vtable 的最后一个条目是 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

此程序表明派生类的虚函数存储在第一个 vptr 的 vtable 中。


我们可以使用 static_cast 来获取派生类 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)

此宏返回基类 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 的 Essential COM 中,他也使用了类似的宏。稍微更改程序,并将 ATL 宏替换为 Box 的宏。

程序 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 的其他神秘之处。

© . All rights reserved.