1. 程式人生 > >成員函式指標與高效能的C委託(中篇)

成員函式指標與高效能的C委託(中篇)

成員函式指標——為什麼那麼複雜?
  類的成員函式和標準的C函式有一些不同。與被顯式宣告的引數相似,類的成員函式有一個隱藏的引數this,它指向一個類的例項。根據不同的編譯器,this或者被看作內部的一個正常的引數,或者會被特別對待(比如,在VC++中,this一般通過ECX暫存器來傳遞,而普通的成員函式的引數被直接壓在堆疊中)。this作為引數和其他普通的引數有著本質的不同,即使一個成員函式受一個普通函式的支配,在標準C++中也沒有理由使這個成員函式和其他的普通函式(ordinary function)的行為相同,因為沒有thiscall關鍵字來保證它使用像普通引數一樣正常的呼叫規則。成員函式是一回事,普通函式是另外一回事(Member functions are from Mars, ordinary functions are from Venus)。
  
  你可能會猜測,一個成員函式指標和一個普通函式指標一樣,只是一個程式碼指標。然而這種猜測也許是錯誤的。在大多數編譯器中,一個成員函式指標要比一個普通的函式指標要大許多。更奇怪的是,在Visual C++中,一個成員函式指標可以是4、8、12甚至16個位元組長,這取決於它所相關的類的性質,同時也取決於編譯器使用了怎樣的編譯設定!成員函式指標比你想象中的要複雜得多,但也不總是這樣。
  
  讓我們回到二十世紀80年代初期,那時,最古老的C++編譯器CFront剛剛開發完成,那時C++語言只能實現單一繼承,而且成員函式指標剛被引入,它們很簡單:它們就像普通的函式指標,只是附加了額外的this作為它們的第一個引數,你可以將一個成員函式指標轉化成一個普通的函式指標,並使你能夠對這個額外新增的引數產生足夠的重視。
  
  這個田園般的世界隨著CFront 2.0的問世被擊得粉碎。它引入了模版和多重繼承,多重繼承所帶來的破壞造成了成員函式指標的改變。問題在於,隨著多重繼承,呼叫之前你不知道使用哪一個父類的this指標,比如,你有4個類定義如下:
  
  class A {
  
  public:
  
  virtual int Afunc() { return 2; };
  
  };
  
  class B {
  
  public:
  
  int Bfunc() { return 3; };
  
  };
  
  // C是個單一繼承類,它只繼承於A
  
  class C: public A {
  
  public:
  
  int Cfunc() { return 4; };
  
  };
  
  // D 類使用了多重繼承
  
  class D: public A, public B {
  
  public:
  
  int Dfunc() { return 5; };
  
  };
  
  假如我們建立了C類的一個成員函式指標。在這個例子中,Afunc和Cfunc都是C的成員函式,所以我們的成員函式指標可以指向Afunc或者Cfunc。但是Afunc需要一個this指標指向C::A(後面我叫它Athis),而Cfunc需要一個this指標指向C(後面我叫它Cthis)。編譯器的設計者們為了處理這種情況使用了一個把戲(trick):他們保證了A類在物理上儲存在C類的頭部(即C類的起始地址也就是一個A類的一個例項的起始地址),這意味著Athis == Cthis。我們只需擔心一個this指標就夠了,並且對於目前這種情況,所有的問題處理得還可以。
  
  現在,假如我們建立一個D類的成員函式指標。在這種情況下,我們的成員函式指標可以指向Afunc、Bfunc或Dfunc。但是Afunc需要一個this指標指向D::A,而Bfunc需要一個this指標指向D::B。這時,這個把戲就不管用了,我們不可以把A類和B類都放在D類的頭部。所以,D類的一個成員函式指標不僅要說明要指明呼叫的是哪一個函式,還要指明使用哪一個this指標。編譯器知道A類佔用的空間有多大,所以它可以對Athis增加一個delta = sizeof(A)偏移量就可以將Athis指標轉換為Bthis指標。
  
  如果你使用虛擬繼承(virtual inheritance),比如虛基類,情況會變得更糟,你可以不必為搞懂這是為什麼太傷腦筋。就舉個例子來說吧,編譯器使用虛擬函式表(virtual function table——“vtable”)來儲存每一個虛擬函式、函式的地址和virtual_delta:將當前的this指標轉換為實際函式需要的this指標時所要加的位移量。
  
  綜上所述,為了支援一般形式的成員函式指標,你需要至少三條資訊:函式的地址,需要增加到this指標上的delta位移量,和一個虛擬函式表中的索引。對於MSVC來說,你需要第四條資訊:虛擬函式表(vtable)的地址。
  
  成員函式指標的實現

  那麼,編譯器是怎樣實現成員函式指標的呢?這裡是對不同的32、64和16位的編譯器,對各種不同的資料型別(有int、void*資料指標、程式碼指標(比如指向靜態函式的指標)、在單一(single-)繼承、多重(multiple-)繼承、虛擬(virtual-)繼承和未知型別(unknown)的繼承下的類的成員函式指標)使用sizeof運算子計算所獲得的資料:
  

  注:
  
  # 表示使用__single/__multi/__virtual_inheritance關鍵字的時候代表4、8或12。
  
  這些編譯器是Microsoft Visual C++ 4.0 to 7.1 (.NET 2003), GNU G++ 3.2 (MingW binaries, http://www.mingw.org/), Borland BCB 5.1 (http://www.borland.com/), Open Watcom (WCL) 1.2 (http://www.openwatcom.org/), Digital Mars (DMC) 8.38n (http://www.digitalmars.com/), Intel C++ 8.0 for Windows IA-32, Intel C++ 8.0 for Itanium, (http://www.intel.com/), IBM XLC for AIX (Power, PowerPC), Metrowerks Code Warrior 9.1 for Windows (http://www.metrowerks.com/), 和 Comeau C++ 4.3 (http://www.comeaucomputing.com/). Comeau的資料是在它支援的32位平臺(x86, Alpha, SPARC等)上得出的。16位的編譯器的資料在四種DOS配置(tiny, compact, medium, 和 large)下測試得出,用來顯示各種不同程式碼和資料指標的大小。MSVC在/vmg的選項下進行了測試,用來顯示“成員指標的全部特性”。(如果你擁有在列表中沒有出現的編譯器,請告知我。非x86處理機下的編譯器測試結果有獨特的價值。)
  
  看著表中的資料,你是不是覺得很驚奇?你可以清楚地看到編寫一段在一些環境中可以執行而在另一些編譯器中不能執行的程式碼是很容易的。不同的編譯器之間,它們的內部實現顯然是有很大差別的;事實上,我認為編譯器在實現語言的其他特性上並沒有這樣明顯的差別。對實現的細節進行研究你會發現一些奇怪的問題。
  
  一般,編譯器採取最差的,而且一直使用最普通的形式。比如對於下面這個結構:
  
  // Borland (預設設定) 和Watcom C++.
  
  struct {
  
  FunctionPointer m_func_address;
  
  int m_delta;
  
  int m_vtable_index; //如果不是虛擬繼承,這個值為0。
  
  };
  
  // Metrowerks CodeWarrior使用了稍微有些不同的方式。
  
  //即使在不允許多重繼承的Embedded C++的模式下,它也使用這樣的結構!
  
  struct {
  
  int m_delta;
  
  int m_vtable_index; // 如果不是虛擬繼承,這個值為-1。
  
  FunctionPointer m_func_address;
  
  };
  
  // 一個早期的SunCC版本顯然使用了另一種規則:
  
  struct {
  
  int m_vtable_index; //如果是一個非虛擬函式(non-virtual function),這個值為0。
  
  FunctionPointer m_func_address; //如果是一個虛擬函式(virtual function),這個值為0。
  
  int m_delta;
  
  };
  
  //下面是微軟的編譯器在未知繼承型別的情況下或者使用/vmg選項時使用的方法:
  
  struct {
  
  FunctionPointer m_func_address;
  
  int m_delta;
  
  int m_vtordisp;
  
  int m_vtable_index; // 如果不是虛擬繼承,這個值為0
  
  };
  
  // AIX (PowerPC)上IBM的XLC編譯器:
  
  struct {
  
  FunctionPointer m_func_address; // 對PowerPC來說是64位
  
  int m_vtable_index;
  
  int m_delta;
  
  int m_vtordisp;
  
  };
  
  // GNU g++使用了一個機靈的方法來進行空間優化
  
  struct {
  
  union {
  
  FunctionPointer m_func_address; // 其值總是4的倍數
  
  int m_vtable_index_2; // 其值被2除的結果總是奇數
  
  };
  
  int m_delta;
  
  };
  
  對於幾乎所有的編譯器delta和vindex用來調整傳遞給函式的this指標,比如Borland的計算方法是:
  
  adjustedthis = *(this + vindex -1) + delta // 如果vindex!=0
  
  adjustedthis = this + delta // 如果vindex=0
  
  (其中,“*”是提取該地址中的數值,adjustedthis是調整後的this指標——譯者注)
  
  Borland使用了一個優化方法:如果這個類是單一繼承的,編譯器就會知道delta和vindex的值是0,所以它就可以跳過上面的計算方法。
  
  GNU編譯器使用了一個奇怪的優化方法。可以清楚地看到,對於多重繼承來說,你必須檢視vtable(虛擬函式表)以獲得voffset(虛擬函式偏移地址)來計算this指標。當你做這些事情的時候,你可能也把函式指標儲存在vtable中。通過這些工作,編譯器將m_func_address和m_vtable_index合二為一(即放在一個union中),編譯器區別這兩個變數的方法是使函式指標(m_func_address)的值除以2後結果為偶數,而虛擬函式表索引(m  

相關推薦

成員函式指標高效能C委託中篇

成員函式指標——為什麼那麼複雜?  類的成員函式和標準的C函式有一些不同。與被顯式宣告的引數相似,類的成員函式有一個隱藏的引數this,它指向一個類的例項。根據不同的編譯器,this或者被看作內部的一個正常的引數,或者會被特別對待(比如,在VC++中,this一般通過ECX暫

成員函式指標高效能C++委託上,中,下

成員函式指標與高效能的 C++委託(上篇) Member Function Pointers and the Fastest Possible C++ Delegates 撰文: Don Clugston 翻譯:周翔 引子 標準 C++中沒有真正的面

成員函式指標高效能C++委託下篇

Member Function Pointers and the Fastest Possible C++ Delegates 撰文: 翻譯:周翔 (接中篇) 委託(delegate) 和成員函式指標不同,你不難發現委託的用處。最重要的,使用委託可以很容易地實現一個 現

好文轉載:成員函式指標高效能C++委託

委託(delegate)和成員函式指標不同,你不難發現委託的用處。最重要的,使用委託可以很容易地實現一個Subject/Observer設計模式的改進版[GoF, p. 293]。Observer(觀察者)模式顯然在GUI中有很多的應用,但我發現它對應用程式核心的設計也有很大的作用。委託也可用來實現策略(St

成員函式指標高效能C++委託(中篇)

成員函式指標與高效能的C++委託(中篇) Member Function Pointers and the Fastest Possible C++ Delegates 撰文:Don Clugston 翻譯:周翔 (接上篇) 成員函式指標——為什麼那麼複雜? 類

成員函式指標高效能C++委託

Member Function Pointers and the Fastest Possible C++ Delegates 撰文:Don Clugston 翻譯:周翔 引子 標準C++中沒有真正的面向物件的函式指標。這一點對C++來說是不幸的,因為面向物件的指標(也

C藝術篇 3-1 指標一維陣列1

我們先來看指標與一維陣列的關係,例題如下: 從輸出結果得知,arr是陣列名,它是指標常量,而ptr是指標變數。 arr表示此陣列第一個元素的地址,即arr等同於&arr[0]。 arr可以使用指標變數的*表示符號,如*arr等同於arr[0],*(arr+1)等同於arr[1],依次

c#委託Delegates--匿名方法,Lambda表示式

以下通過程式碼對比,委託+方法、匿名方法、Lambda表示式的區別。一.委託+方法這裡用上一篇中的例項:namespace Func { public delegate int MyDel(int num);//宣告一個自定義委託 class Pr

To B業務To C業務產品,使用者需求

 To B 就是 To business,面向企業或者特定使用者群體的面商類產品;  To C 就是 To customer,產品面向消費者。 B端產品經理與C端產品經理- http://blog.csdn.net/qq_37697037/article/details/7

F28335的InitSysCtrl()DSP2833x_SysCtrl.c檔案1

開發DSP除了CCS之外,TI還推出了一個controlSUITE,專門針對C2000系列,主要是官網資源的集中和分類。軟體免費只需要下載安裝,由於不喜歡被別人加工註釋了程式碼與工程,所以controlSUITE是個不錯的選擇。而且裡邊是TI原裝的東西,所以應該是最精華的!開

C/C++ 指標小結——指標其它資料型別陣列、字串、函式、結構體的關係

一、指標與陣列和字串 1、指標與陣列 當宣告數時,編譯器在連續的記憶體空間分配基本地址和足夠的儲存空間,以容納陣列的所有元素。基本地址是陣列第一個元素(索引為0)的儲存位置。編譯器還把陣列名定義為指向第一個元素的常量指標。 元素的地址是通過索引和資料型別的比例因子來計算的;例如: x[3

函式指標指標函式C++工廠設計最喜歡用這個

在看開源專案的時候,發現C++搞工廠設計都喜歡用這個。下面來給出這方面的例子(大學裡面沒學過)函式指標:型別一:程式碼如下:#include <iostream> using namespace std; int max(int x, int y){ retu

成員函式指標結構+普通函式指標之間的轉換

通過記憶體拷貝(memcpy等)可以實現任意指標 間的強制轉換,但不能保證可以正常使用。 通過網上查詢發現: 函式成員指標其實與普通成員指標不同,它除了包含函式本身地址以外還包含其他資訊(例如是否為虛擬函式等),所以不能簡單的理解成員函式指標就是普通指標那樣一般佔4位元組

C/C++開發】函式指標回撥函式

C++很多類庫都喜歡用回撥函式,MFC中的定時器,訊息機制,hook機制等待,包括現在在研究的cocos2d-x中也有很多的回撥函式。 1.回撥函式 什麼是回撥函式呢?回撥函式其實就是一個通過函式指標呼叫的函式!假如你把A函式的指標當作引數傳給B函式,然後在B函式中通過A函式傳進來的這個指標

C++過載2:通過成員函式和友元函式過載

分別通過成員函式和友元函式完成過載 #include <iostream> using namespace std; class Complex { public: Complex(double real =0,double imag=0):real(real),imag(i

C++組合聚合C結構體中包含函式

C++組合(聚合)與C結構體中包含函式 今天突然想到C++的聚合,以前一直沒有注意,今天想到就寫下來,做個筆記; C++的類與我們的C語言中的結構體特別像,但是有有些不太一樣,這裡不多累贅了不能,大家學過的都知道。 C++組合(聚合) 我們知道的都是C++的類的物件,

成員函式過載函式指標

在有成員函式過載的情況下該如何使用函式指標呢 class l { public: void func(); void func(int, int); }; void l::func() { cout << "func()" << endl; }

C++函式指標 C++11 function 函式物件對比

轉自:https://blog.csdn.net/skillart/article/details/52336303 1.函式指標 函式指標:是指向函式的指標變數,在C編譯時,每一個函式都有一個入口地址,那麼這個指向這個函式的函式指標便指向這個地址。函式指標主要由以下兩方面的用途:呼叫函式和

C++筆記——類3:const修飾成員函式

一、const修飾的成員函式         由於成員函式可以任意訪問類內的任何資料成員,但當我們不願意讓成員函式修改資料成員時,我們可以用const修飾類的成員函式,一般形式為: class CName { private: ......

C/C++函式指標指標函式

前面說的話 面試的時候,經常有面試官問這個問題,在Linux核心裡面也是經常被使用的,在看很多大神的程式碼裡面,我們也經常遇到函式指標與指標函式,一樣,如果你自己沒問題了,就不用往下看了。   定義 我們看個程式碼 int *func(int a,int b)