ATL技術內幕 第一部分

前言:這一系列博客翻譯自 The Code Project 上的文章,作者是Zeeshan amjad。

題目:ATL Under the Hood -  Part 1

原文鏈接:http://www.codeproject.com/Articles/1769/ATL-Under-the-Hood-Part-1

 

介紹

這個系列我們將討論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;
}
這個程序的輸出和上面的“程序5”輸出結果一樣。讓我們再深入實驗。請看下面的代碼。

程序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的值
我們將對象的地址放入int型指針內部,然後通過操作這個int型指針來操作對象。程序輸出如下:
X = 200
Y = 10
這不是我們想要的結果。結果顯示,200存入了m_ix所在的內存中。這意味着m_ix,也就是這個對象的第一個成員變量從存放在這個對象起始內存的第二個位置,而不是第一個位置。起始,第一個位置放置的便是虛指針,然後才存放對象的數據成員。修改下面兩行



       *(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*,這個地址便是虛函數表的地址了,爲了得到這個位置的值,也就是該類第一個虛函數的地址,我們還要用一次解引用操作符,然後再強制轉化爲合適的(也就是我們定義的Fun)函數指針類型。所以
        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
現在讓我們看看多重繼承的情況。先看下面的簡單例子:

Program 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
#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;
}
ATLATLDEF.H中定義了宏offsetofclass,這個宏返回了派生類模型中基類的虛表地址。
#define offsetofclass(base, derived) \
       ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)
//以下這段爲自己加註的
在用這個宏之前首先解釋一下上面的這個程序。由程序15可以判斷出,基類的虛指針分別存在派生類的最開頭,而且都佔用4個字節,在這裏Base1的虛表地址位於起始地址0處,Base2的虛表地址位於起始地址偏移4個字節處,Base3的虛表地址位於起始地址偏移8個字節處。下面我們看這個關鍵運算:
(DWORD)static_cast<Base1*>((Drive*)SOME_VALUE)-SOME_VALUE
首先:(Drive*)SOME_VALUE將1強制轉換爲了一個地址,0x00000001(這個位數有計算機的地址空間決定)。
(DWORD)static_cast<Base1*>((Drive*)SOME_VALUE)將0x00000001這個地址進行static_cast轉換,由於Base1本身位於Drive的起始位置,古轉換後的結果仍然不變。最後將0x00000001減掉,便是Base1的偏移量0。同樣的道理,對Base2的static_cast轉換將得到原地址偏移4的地址,最後減掉源地址,便是偏移量,Base3類似。
//以上這段爲自己加註的
接下來讓我們再看一個例子。

程序 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
這個程序輸出顯示這個宏返回了特定基類的虛指針在派生類的偏移量。在DonBoxEssential 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;

        // 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(
 
本部分完。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章