c++ 虛函數


我們先來討論一個類(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;[Page]
 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) :

[NextPage]

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; // 打算改變 的值
 *(pInt+1) = 200; // 打算改變 的值

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

 return 0;
}
這個程序最重要的地方是:
int* pInt = (int*)&objPoint;
*(pInt+0) = 100; // 打算改變 的值
*(pInt+1) = 200; // 打算改變 的值

這裏我們把對象的地址保存到一個int類型的指針,然後把它當成整型指針。[Page]
程序輸出:
X = 200
Y = 10
當然,這不是我們想要的結果!程序說明,這時的200是保存到x_ix,而不是x_iy。也就是說對象第一個數據成員是從內存的第二個地址開始的

,而不是第一個。換句話說,第一個內存地址保存的是虛函數的地址,然後其他保存的都是類的數據成員。我們來改一下下面兩行代碼。
int* pInt = (int*)&objPoint;
*(pInt+1) = 100; // 打算改變 的值
*(pInt+2) = 200; // 打算改變 的值

這時我們得到了所期待的結果。

程序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; // 想要改變 的值
         *(pInt+2) = 200; // 想要改變 的值

         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;[Page]
         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 \"

[NextPage]

             << (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.

添加多一個虛函數到類裏面會怎麼樣。我們現在想要訪問虛函數地址表裏第二個成員。查看以下程序,看看虛函數地址表裏的值。[Page]

程序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;[Page]
         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;

[NextPage]


         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);[Page]

         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);[Page]

         pFun();

        

         // calling 2nd virtual function of Base1

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

         pFun();

[NextPage]

       // 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;

}

[NextPage]

這是 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()



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章