天天看點

c++成員函數指針詳解

      今天看代碼突然遇到成員函數指針的使用,以前沒有用過,比較生疏,是以google了一下,發現此文,費勁的翻譯了下,友善以後查詢

原文連結:http://www.goingware.com/tips/member-pointers.html

内容:

1. 概述

2. 介紹

3. 成員函數指針不僅僅是簡單的位址

4. 緩存抉擇的結果

5. 成員函數指針的效率

6. 詳細說明使用成員函數指針

1. 概述

      成員函數指針是c++中衆多比較少用的特性之一,甚至很有經驗的開發者也不能很好的了解,這可以諒解,因為他們的文法确實比較難用和難以了解。

      盡管成員函數指針應用不是很廣泛,但有時候他們對于解決特定的問題是很有幫助的而且他們确實是最好的選擇,因為他們能夠提高效率并且使代碼更加合理。他們非常适合緩存那種抉擇的結果,用來實作一個不同的排序算法

(這句不會翻譯,了解着來吧:They work very well to cache the result of a frequently made decision, and to implement a different sort of polymorphism.)

      我将要讨論什麼是成員函數指針,如何聲明和使用他們,并且給出一些應用他們很好地解決問題的例子

2. 介紹

      我不能确切說出成員函數指針的使用頻率。當我在usenet和郵件清單裡看到一些人提及成員函數指針的時候,我也确實發現和我一起工作的有些人在代碼裡使用他們,是以我的看法是他們不是很普遍的使用。

      成員函數指針重要是因為他們提供了一種有效的方法去緩存是哪個成員函數被調用。在有些情況下這能節省時間,他們提供了另外:一種設計上的選擇:避免使用記憶體配置設定去緩存那樣的抉擇(比如是哪個成員函數被調用)。在下面我會深入介紹這些。

      成員函數指針允許我們間接地調用對象的一組擁有共同特征成員函數中的一個。

(Member function pointers allow one to call one of several of an object's member functions indirectly.

Each of the functions whose "address" is stored must share the same signature. )

      我把位址用引号括起來是因為儲存在成員函數指針裡的不簡單是成員函數代碼的起始位址,從概念上來講,他隻是一個在類内部定義的函數清單的偏移量,就虛函數表來說,成員函數指針包含的是真正的“虛函數指針表”的偏移。

      成員函數指針不能被直接使用他們自身解引用(比如 *pfnCall() 這樣的函數調用),他們必須和一些對象一起使用,也就讓這些對象提供"this"指針去調用它們的成員函數。

      為了說明如何聲明和調用一個成員函數指針,我開始先聲明并解引用一個普通的非成員函數指針。你可以通過給定一個指針能指向的函數類型來聲明一個函數指針,把函數名用“ (*pointerName)”。正常的函數指針在c和c++中有相同的文法:

void Foo(int anInt, double aDouble);

void Bar()

{

void (*funcPtr)(int, double) = &Foo;

(*funcPtr)(1, 2.0);

}

對于正常的函數指針取位址符"&"是可選的,但對于成員函數這是必須的。g++将編譯保留他的源碼,但會給出一個警告

      為了聲明一個成員函數指針,你必須像前面那樣給出一個要指向的函數類型,但函數名被一個域指針的構造替代---給出他要指向的類的成員函數名,比如(ClassName::*pointerName)。注意一個成員函數指針隻能指向他聲明的那個類的成員函數,他不能被使用在不同的類上,即使這個類有相同的成員函數特征。

      你可以在左側提供一個解引用或者指向對象的指針,在右側指定函數指針,使用" .* "或者" ->* " 解引用一個成員函數指針,這有一個簡單的例子:

class Foo

{

public:

double One(long inVal);

double Two(long inVal);

};

void main(int argc, char **argv)

{

double (Fool::*funcPtr)(long) = &Foo::One;

Fool aFoo;

double result = (aFoo.*funcPtr)(2);

return 0;

}

      除非你暫時使用成員函數指針,否則像上面那樣聲明一個成員函數指針是很難懂的并且很難正确的運作。像我下面的例子那樣使用typedef而不是每次都使用整個函數類型去聲明是很有幫助的。

3. 成員函數指針不僅僅是簡單的位址

      大多數c和c++程式員都知道假設一個指針和int有相同的大小這種做法是很糟糕的風格,然而這種情況卻經常出現。不同類型的指針的大小可能不同,這種差别可能不太明顯。比如,在x86的16位程式設計中的近指針和遠指針可能有不同的大小,遠指針由段和偏移構成,而近指針僅僅有偏移。 成員函數指針通常是一個小結構, 這個結構給出了是否虛函數,多繼承等資訊。

      在下面的例子中,使用g++ 2.95.2 在PowerPC G3 Mac OS X iBook上編譯,我發現我建立的成員函數指針是8個位元組。

      這會令使用者感到驚奇。例如,vc6允許程式員做一個優化(預設是開啟的),這可能導緻相同類型的成員函數指針在不同的編譯條件下産生不同的大小。使用錯誤的設定可能導緻臃腫的代碼産生bug,因為一個由函數傳回的成員函數指針大小很可能不是函數的本來期望的大小,這會導緻棧上的資料被虛假的資料覆寫。

在vc的工程設定裡有一個标簽 "representation",這個标簽有一個選擇: “best  case always”和"most general always",如果你在vc++中使用成員函數指針,請檢查這些設定的文檔說明,做一個适合的選擇,如果可疑的話,就選擇"most general always"。

(我找了半天才發現這個選項。。。vc6的)

c++成員函數指針詳解

4. 緩存抉擇的結果

      一個最好的使用成員函數指針的例子就是緩存在不同的環境下應該使用哪個成員函數這個抉擇的結果。以成員函數指針的形式儲存可以節省時間,特别是當這種情況發生在一個循環裡的時候。

這裡有一個很可笑的程式(但希望能簡單)顯示了使用成員函數指針解決儲存抉擇結果的問題,他也說明了如何使用typedef:

#include <stdlib.h>

#include <iostream>

class Test

{

public:

Test( long inVal )

: mVal( inVal ){}

long TimesOne() const;

long TimesTwo() const;

long TimesThree() const;

private:

long mVal;

};

typedef long (Test::*Multiplier)() const;

int main( int argc, char **argv )

{

using std::cerr;

using std::endl;

using std::cout;

if ( argc != 3 ){

cerr << "Usage: PtrTest value factor" << endl;

return 1;

}

Multiplier funcPtr;

switch( atol( argv[ 2 ] ) ){

case 1:

funcPtr = &Test::TimesOne;

break;

case 2:

funcPtr = &Test::TimesTwo;

break;

case 3:

funcPtr = &Test::TimesThree;

break;

default:

cerr << "PtrTest: factor must range from 1 to 3" << endl;

return 1;

}

cout << "sizeof( funcPtr )=" << sizeof( funcPtr ) << endl;

Test myTest( atol( argv[ 1 ] ) );

cout << "result=" << (myTest.*funcPtr)() <<endl;

return 0;

}

long Test::TimesOne() const

{

return mVal;

}

long Test::TimesTwo() const

{

return 2 * mVal;

}

long Test::TimesThree() const

{

return 3 * mVal;

}

現在我給出不太一樣的一個例子,因為他能在一個循環裡執行選擇很多次,總是到達相同的選擇。他是使用成員函數指針進行重構的一個很好的說明。

#include <exception>

class Test

{

public:

Test( long inFactor )

: mFactor( inFactor ){}

long TimesOne( long inToMultiply ) const;

long TimesTwo( long inToMultiply ) const;

long TimesThree( long inToMultiply ) const;

long MultiplyIt( long inToMultiply ) const;

private:

long mFactor;

};

long Test::MultiplyIt( long inToMultiply ) const

{

switch( mFactor ){ // decision made repeatedly that always yields the same result

case 1:

return TimesOne( inToMultiply );

break;

case 2:

return TimesTwo( inToMultiply );

break;

case 3:

return TimesThree( inToMultiply );

break;

default:

throw std::exception();

}

}

void MultiplyThem( long inFactor )

{

Test myTest( 2 );

long product;

// Call a function that makes the same decision many times

for ( long i = 0; i < 1000000; ++i )

product = myTest.MultiplyIt( i );

}

在大多數情況下在這個循環裡都會有相同的選擇,一個更好的重構代碼是使抉擇在循環的外面,并且循環的每個分支都有一個重複的循環(或者被子函數包裹着)

void Foo( long value )

{

for ( long i = 0; i < 1000000; ++i ){

switch( value ){ // BAD CODE: always reaches the same decision

case 1:

//...

break;

case 2:

//...

break;

case 3:

//...

break;

}

}

}

//Instead we place the switch outside the loop:

void Foo( long value )

{

switch( value ){ // BETTER CODE: decision made only once

case 1:

for ( long i = 0; i < 1000000; ++i ){

//...

}

break;

case 2:

for ( long i = 0; i < 1000000; ++i ){

//...

}

break;

//...

}

}

如果你想避免每個分支裡的循環實作,使代碼更簡單,可以把它們放到一個子函數裡。

      如果這種重構方式不實用,成員函數指針是最好的解決方案。一個原因可能是因為在代碼裡循環和抉擇屬于不同的類,并且你不想暴露出做出抉擇的類的實作。這裡有使用成員指針對MultiplyIt 代碼的重構:

#include <exception>

class Test

{

public:

Test( long inFactor );

long TimesOne( long inToMultiply ) const;

long TimesTwo( long inToMultiply ) const;

long TimesThree( long inToMultiply ) const;

long MultiplyIt( long inToMultiply ) const;

private:

typedef long (Test::*Multiplier)( long inToMultiply ) const;

long mFactor;

Multiplier mMultFuncPtr;

static Multiplier GetFunctionPointer( long inFactor );

};

Test::Test( long inFactor )

: mFactor( inFactor ), mMultFuncPtr( GetFunctionPointer( mFactor ) )

{

return;

}

Test::Multiplier Test::GetFunctionPointer( long inFactor )

{

switch ( inFactor )

{ // Decision only made once!

case 1: return &Test::TimesOne;

break;

case 2: return &Test::TimesTwo;

break;

case 3: return &Test::TimesThree;

break;

default: throw std::exception();

}

}

long Test::MultiplyIt( long inToMultiply ) const

{

// Using cached decision result

return (this->*mMultFuncPtr)( inToMultiply );

}

void MultiplyThem( long inFactor )

{

Test myTest( 2 );

long product;

for ( long i = 0; i < 1000000; ++i )

product = myTest.MultiplyIt( i );

}

5. 成員函數指針的效率

       不幸的是,通過解引用成員函數指針調用成員函數比簡單的jmp到一個寄存器要複雜的多,這些指針實際上是一些小結構,結構中的一些bit用來查找實際要跳轉到的函數位址。我想如果我手上有g++的源代碼,我或許能給你示範一下具體實作。我通過跟蹤這些成員函數指針的調用發現每個調用都運作一小段彙編代碼,這是非常快的代碼,如果這段代碼儲存在cpu的一級緩存裡,在循環裡執行這些代碼也是非常快的。但是他不如簡單的比較和條件分支快。

      如果你的代碼抉擇在不斷地重複着,使用成員函數指針或許沒有什麼優勢。一個簡單的做法是用if語句比較兩個數字或者檢查bool值,或者用一個每個分支都包含一個小循環的switch語句(構造一個跳表對于編譯器是很容易的),可能比解引用成員函數指針更快。如果一個抉擇很複雜或者需要經過很長的代碼才能到達,比如字元串比較或者搜尋一些資料結構,使用成員函數指針可能更好一些。

6. 詳細說明使用成員函數指針

      如果你看到成員函數指針可以被各種有相同函數調用類型(參數,傳回值類型一樣),不同實作類型的函數位址指派(前面有virtual,static,inline等修飾),你可能已經了解了使用結構實作成員函數指針的原因。

class Different

{

public:

inline void InlineMember();

virtual void VirtualMember();

void OrdinaryMember();

static void StaticMember();

typedef void (Different::*FuncPtr)();

};

void Test()

{

Different::FuncPtr ptr = &Different::InlineMember;

ptr = &Different::VirtualMember;

ptr = &Different::OrdinaryMember;

}

      當你看到我建立一個指向inline函數的指針時,可能感到驚訝,但如果你想這樣做,确實是可以的,編譯器将會放置一個inline函數的函數實作版本,并把它的位址給你,是以函數指針根本不真正指向inline函數。

     盡管看上去一個靜态成員函數也可以做相同的轉換,但實際上根本不會,因為他沒有被傳遞給指針,他像其他參數一樣被傳遞給了你的成員函數,但他沒有明确指明函數類型你不能使用成員函數指針儲存一個靜态函數的位址(使用普通的,非成員函數指針去儲存)。

void Fails()

{

Different::FuncPtr ptr = &Different::StaticMember;

}

      指向虛函數的成員函數指針工作起來就像直接調用虛函數一樣,成員函數的類型被動态地從對象獲得,而不是成員函數指針所代表的靜态類型:

#include <iostream>

class Base

{

public:

virtual void WhoAmI() const;

typedef void (Base::*WhoPtr)() const;

};

class Derived: public Base

{

public:

virtual void WhoAmI() const;

};

void Base::WhoAmI() const

{

std::cout << "I am the Base" << std::endl;

}

void Derived::WhoAmI() const

{

std::cout << "I am the Derived" << std::endl;

}

int main( int argc, char **argv )

{

Base::WhoPtr func = &Base::WhoAmI;

Base theBase;

(theBase.*func)();

Derived theDerived;

(theDerived.*func)();

return 0;

}

運作上面的程式輸出結果如下:

I am the Base

I am the Derived

      多态被認為是通過繼承包含虛成員函數的類實作的。一個繼承類對象可以被指派給基類的指針或引用;當通過指針或引用調用虛成員函數的時候,函數指針會查找實際被配置設定的對象的虛函數表,而不是像聲明的基類指針或引用那樣的靜态類型。

      然而多态的概念可以有更普遍的意義,并且我在郵件清單裡看到建議他應該包含模闆的使用:允許相同的源代碼被應用于不同的沒有關系的對象類型。當一個vector被聲明的時候,他被認為是一個類型被參數化的多态容器。

      成員函數指針可以被用來實作各種不同的多态。在正常類型中,我們通過配置設定不同類型的對象來決定繼承樹中哪個相關的成員函數被調用,這是通過一個隐藏在對象裡的虛函數表實作的。

      在總是建立同一類型對象的情況下,可以通過指派給成員函數指針哪個成員函數位址決定哪個成員函數被調用,一個有趣的好處是你可以不必配置設定一個以普通繼承為基礎的多态類型的對象,卻可以在運作期改變對象的行為

(上面的意思就是不需要動态配置設定新的繼承樹中的對象就可以調用這個對象的成員函數)