函數指針
定義方式
typedef 返回值類型(* 新類型名稱)(參數列表)
typedef char (*PTRFUN)(int); PTRFUN pFun; char glFun(int a){ return;} void main() { pFun = glFun; (*pFun)(2); }
調用方式:
- 直接把函數指針變量當作函數名,然后填入參數
- 將函數指針的反引用作為函數名,然后填入參數
上面的PTRFUN也可直接進行以下調用:
PTRFUN(2);
函數調用方式
函數名就是函數的地址,是一個編譯時的常量,“函數連接”的本質就是把函數地址綁定到函數的調用語句上。
靜態連接:一般的函數調用語句可以在編譯的時候就完成這個綁定,這就是靜態連接。
運行時連接&動態連接:
函數指針可以理解為一個新的類型,由其定義的變量,與其他變量一樣,在編譯期間是沒有值的,因此函數指針與函數體的綁定關系只有到了運行時才確定。
對於函數指針數組,此特征表現的尤為明顯,見《高質量程序設計指南》P135
class CTest { public: void f(void){cout<<"CTest::f()"<<endl;} //普通成員函數 static void g(void) {cout<<"CTest::g()"<<endl;} //靜態成員函數 virtual void h(void) {cout<<"CTest::h()"<<endl;} //虛擬成員函數 private: //.......... }; void main() { typedef void (*GFPtr)(void); //定義一個全局函數指針類型 GFPtr fp = CTest::g; //取靜態成員函數地址的方法和取一個全局函數的地址相似 fp(); //通過函數指針調用類靜態成員函數 typedef void (CTest::*MemFuncPtr)(void)//聲明類成員函數指針類型 MemFuncPtr mfp_1 = &CTest::f; //聲明成員函數指針變量並初始化 MemFuncPtr mfp_2 = &CTest::h; //注意獲取成員函數地址的方法 CTest theObj; (theObj.*mfp_1)(); //使用對象和成員函數指針調用成員函數 (theObj.*mfp_2)(); CTest* pTest = &theObj; (pTest->*mfp_1)(); //使用對象指針和成員函數指針調用成員函數 (pTest->*mfp_2)(); }
vtable與vptr類型問題與初始化問題
- 類型問題
vptr:是C++對象的隱含數據成員。多態類的每一個對象中安插一個vptr,其類型為指向函數指針的指針,它總是指向所屬類的vtable,也就是說:vptr當前所在的對象是什么類型的, 那么它就指向這個類型的vtable.
vtable: 就是一個函數指針的數組.按道理說只能在同一數組中存放統一類型的數據。可是一個class中可能有各式各樣的的虛函數,它們的原型都可能不一樣,因此也不可能是一種函數指針類型,不同class的虛函數就更不可能了。
那么虛函數表到底怎么定義呢?參考一下:MFC 消息映射、分派和傳遞
按照MFC的思路,我們設想C++編譯器構建vtable的方法是這樣的:
1、定義如下通用的虛函數指針類型(實際上是經過Name-Mangling處理后對應的全局函數指針類型)和vtable類型。
typedef void(__cdecl* PVFN)(void); //通用的虛函數指針類型 typedef struct { type_info * _pTypeInfo; PVFN _arrayOfPvfn[]; //虛函數個數由初始化語句確定 } VTABLE;
2、在每一個繼承分支中的第一個多態類中插入vptr,而在每一個多態類中都插入vtable的聲明,如下:
class Shape { PVFN *_vptr; static VTABLE _vtable; public: Shape() :m_color(0){} virtual ~Shape(){} float GetColor() const{ return m_color; } void SetColor(float color){ m_color = color; } virtual void Draw() = 0; private: float m_color;//顏色 }; class Rectangle :public Shape { static VTABLE _vtable;//在實現文件中初始化 public: Rectangle() :m_length(1), m_width(1){...} virtual ~Rectangle(){...} //.... virtual void Draw(){...} static unsigned int GetCount(){ return m_count; } protected: Rectangle(const Rectangle& copy){...} Rectangle& operator=(const Rectangle& assign){...} private: float m_length;//長 float m_width;// 寬 static unsigned int m_count;//對象計數,在實現文件中初始化 };
編譯器編譯完每一個class,都會進行如下工作:
1、把class中所有虛函數的指針(實際就是被轉換為全局函數后的地址)都強制轉換為PVFN類型,並用它們初始化_vtable中的_arrayOfPvfn[],就像ON_WM_XXX所做的那樣
2、_vptr則將被初始化為指向_vtable對象或者_vtable._arrayOfPvfn[0],具體指向哪里取決於編譯器的實現.
3、vptr的初始化和改寫在class的各個構造函數和析構函數中完成.
但是MFC中是枚舉了所有的可能的消息處理函數處理類型(AfxSig_xxx),C++編譯器顯然不可能如此,而是在遇到通過指針或者引用調用虛函數的語句時:
1、首先根據指針或引用的靜態類型來判斷所調用的虛函數是否屬於該class或者它的某個public基類
2、然后進行靜態類型檢查,例如:
Shape * pShape = new Rectangle; //pShape的靜態類型是 Shape* pShape->Draw(); // 根據Shape::Draw()執行類型檢查 delete pShape; //根據 Shape::~Shape()執行類型檢查
3、改寫虛函數調用語句,怎么改?
(*(p->_vptr[slotnum]))(p, arg-list);//指針當作數組來用,最后改寫為指針運算
其中,p是基類型指針,vptr是p指向的對象的隱含指針,而slotnum就是調用虛函數在vtable中的編號,這個數組元素的索引號在編譯時就確定了下來,並且不會隨着派生層次的增加而改變。arg-list是參數列表。
對應上面Shape則改寫為下面的樣子:
(*(pShape->_vptr[2]))(pShape); //pShape->Draw(); (*(pShape->_vptr[1]))(pShape); //delete pShape;
擴展:
以上也就是動態綁定技術的一個實現,由於在運行時才進行的函數尋址,以及各個虛函數的參數列表都不盡相同,編譯時根本無法對一個具體的虛函數調用執行靜態的參數類型檢查。
那么C++編譯器是如何對虛函數調用語句的參數類型進行檢查的呢?
派生類定義中的名字(對象或函數名)將義無反顧地遮蔽(即隱藏)掉基類中任何同名的對象或函數。
隱藏:如果派生類定義了一個與其基類的虛函數同名的虛函數,但是參數列表有所不同,那么這就不會被編譯器認為是對基類虛函數的改寫(Overrides),而是隱藏,所以不可能發生運行時綁定。
協變:要想達到運行時綁定,派生類和基類中同名的虛函數必須具有相同的原型,也即相同的Signature(參數列表),返回值類型可以不同,此為協變。
4、函數指針類型轉換:語句pShape->_vptr[n]從vtable中取出來的函數指針類型都是通用類型PVFN,與實際調用的虛函數的類型一般是不匹配的(編譯器知道我們定義的每一個虛函數的類型),所以還應該有一個反向類型強制轉換的過程。
以下僅作示意,待找到MFC的反向轉換過程,以MFC的代碼進行說明。
typedef void(__cdecl* PVFN_Draw)(void); typedef void(__cdecl* PVFN_~Shape)(void); (*(PVFN_Draw)(pShape->_vptr[2]))(pShape); //pShape->Draw(); (*(PVFN_Draw)(pShape->_vptr[1]))(pShape); //delete pShape;
以上的過程中並沒有派生類改寫的虛函數Rectangle::Draw()和Rectangle::~Rectangle()參與,那么怎么會調用到這兩個改寫的虛函數呢?奧妙就在vtable的構造及pShape當前實際執行的對象。
雖然pShape的靜態類型是Shape*,但在運行時卻指向一個Rectangle對象,而該對象的vptr成員指向Rectangle::_vtable,而不是Shape::_vtable;這個vtable中存放的也都是Rectangle改寫過的虛函數或者新增的虛函數的地址,除非有的虛函數Rectangle沒有改寫。
vtable中虛函數指針的排列順序,具體規則詳見《高質量程序設計指南-C++/C語言》P222.
1、vptr在哪里被初始化?
由於vptr並非static成員,因此只能在構造函數中初始化---在哪個類的構造函數中就被初始化為指向哪個類的vtable。
2、它的值會不會改變?
會
3、為什么需要改變?
4、在哪里改變?
類的每一個構造函數中。
具體如下:
由於一個派生類對象的構造函數會從每一個繼承分支的根類開始向下依次調用它的每一個基類的構造函數,所以除了在根類的構造函數中vptr是被初始化的外,在后來的基類構造函數中實際上都是在不斷地被改寫以指向當前構造函數對應的基類的vtable。編譯器必須保證在類的每一個構造函數中(包括拷貝構造函數)都要重新初始化或改寫vptr。
vptr必須隨着對象類型的變化而不斷的改變它的指向,以保證其值和當前對象的實際類型是一致的。
構造函數和析構函數中如何調用虛函數?
應盡量避免在構造函數和析構函數調用虛函數!!!
構造函數中
vptr肯定是第一個被初始化和改寫的成員---在所有用戶代碼前執行。
如果在多態類的構造函數中調用了某個虛函數,不過這個虛函數是新增的還是來自於多態基類里或是改寫自多態基類,因當程序執行到這個構造函數中的時候,可以肯定的是當前對象已經存在了,而且vptr已經被正確初始化了,只要保證這個虛函數用到的所有其他數據成員都在它被調用之前初始化好即可。
析構函數中
vptr則是在所有用戶代碼執行完之后被改寫為指向其直接基類的vtable,或者說在所有用戶代碼執行之前被重新改寫為指向當前類的vtable。這是必須的,因為一個對象在析構的過程中是沿着繼承分支向上依次退化為各個基類對象---直至根類對象的,所以析構函數中的虛函數調用不應該綁定到當前類的某個派生類的該虛函數的改寫版本上。
一句話就是:可以vptr指向當前類的vtable(所有用戶代碼之前),也可以指向直接基類(所有用戶代碼之后)的vtable,但不能指向派生類的vtable。
此處參考對象的構造和析構過程便可明白,如同網絡消息的打包和解包過程,如圖:
是否有必要在最終的二進制代碼(LIB,DLL和EXE)中為一個抽象基類產生實際的vtable呢?
沒有必要
因為抽象基類不可能實例化,也就不可能有指針或引用指向它的對象,也不可能有vptr指向它的vtable。