跳到主要內容

C++的靜態多型

class Foo : public Base<Foo>
{
};
在上例中,類別Foo繼承自類別Base,類別Base有一個Template參數,而在此例中是以類別Foo作為參數傳入。以上這個宣告是合法的,根據C++的標準規格,當類別Foo宣告之後,Foo這個名字就算是已被定義了,所以能夠立即被拿來餵給類別Base作為參數使用。
template<class T>
class B
{
public:
void sayHello()
{
T* pThis = static_cast<T*>(this);
pThis->hello();
}

// 可被子類別覆寫(overridable)
void hello()
{
cout << "B::hello" << endl;
}
};

class D1 : public B<D1>
{
};

class D2 : public B<D2>
{
public:
void hello()
{
cout << "D2::hello" << endl;
}
};

int main()
{
D1 d1;
D2 d2;

d1.sayHello(); // 印出 "B::hello"
d2.sayHello(); // 印出 "D2::hello"

return 0;
}
在上面的範例中我們宣告了一個類別B,還有二個繼承類別B的類別D1和類別D2。類別D1和類別D2以上面所提到的方式,以自己為參數繼承template類別B。類別B中有個sayHello公開函式,它會呼叫一個叫作hello的函式。hello函式在類別B中有一個預設的實作,會在畫面上列印出"B::hello"的文字訊息,而這個函式hello可以被子類別所覆寫。如果子類別有覆寫hello函式實作自己的版本,則在呼叫sayHello時子類別的實作就會被呼叫到,否則就會呼叫到預設的類別B的版本。

這裡使用到的關鍵技巧就在於static_cast<T*>(this)這一行的地方。

它把this指標轉型成參數T所指的型別。在這個例子中,參數T不是類別D1就是類別D2,而類別D1或類別D2都是類別B的子類別,所以這樣的轉型是合法且安全的。透過pThis指標來呼叫到的hello實作品會根據參數T的型別而定,如果參數T有一個自己的hello實作,則這個hello實作就會被呼叫,若沒有則會呼叫到類別B自己的hello實作。因為這裡面使用到泛型程式設計,所以這個動作是在編譯時期決定的。而這樣的行為和執行時期的多型非常類似,因為是在編譯時期決定,是靜態的,所以我們才會稱它作靜態多型。

但是如果參數T是有問題的參數呢?比如說在此例中我們不是傳入類別D1或類別D2呢?假如是這樣子的話,會有底下二種可能的情況發生。

1.傳入的參數T類別正好有個叫作hello函式,所以沒問題編譯可以過,只不過類別的行為是不是會變得異常就不得而知了。

2.傳入的參數T類別本身找不到一個叫作hello的函式(包含類別T的基礎類別),這時候編譯器就會直接告訴你一個找不到hello的定義的錯誤因而編譯失敗。所以這不構成什麼問題。

繼續回到原來的範例。類別D1只單純繼承類別B而沒有覆寫hello,所以物件d1在呼叫sayHello時會呼叫到類別B的hello,因此畫面上打印出"B::hello"文字訊息。而類別D2有自己的hello實作,所以物件d2在呼叫sayHello時就會呼叫到自己的hello函式,因此畫面上打印出"D2::hello"文字訊息。

使用靜態多型的技術(或把它叫作技巧)有以下幾個好處。

1.類別就不再需要額外的vtbl了,因為沒有任何虛擬函式。而因為不是執行期多型,所以呼叫更快,類別體積也較小。

2.在執行時期不可能透過一個null指標去呼叫靜態"虛擬函式",因為指標都是有效的this指標轉型而來的。

3.所有的呼叫都是在編譯時期決定,所以編譯器有更多機會作最佳化。

留言

這個網誌中的熱門文章

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...

單人撲克牌遊戲 - 蒙地卡羅

更多可在網頁玩的 單人撲克牌遊戲 ; 新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

關於C/C++的指標

我想應該還有不少人在使用指標上有些地方觀念不大清楚,比如說下面二個函式,那個是正確的?為什麼?像這樣的問題如果弄不清楚,寫出來的程式一定非常危險。 // 為簡化忽略檢查 void alloc_mem(char* p) // 版本1 {   p = new char[100]; } void alloc_mem(char** p) // 版本2 {   *p = new char[100]; } 如上,這個函式要配置大小是100個字元的記憶體並從傳入的參數p回傳,這二個版本除了輸入參數不一樣外大致上是一樣的;從第一個版本來看,參數是一個字元指標,記憶體配置出之後直接傳給p,如果觀念正確的人一定可以馬上指出這樣的寫法是錯誤的,第二個版本才是正確能work的。 現在就來說明為什麼,在這之前先要了解在C/C++中,函式的參數是如何傳遞的,在C/C++中函式的呼叫所傳入的參數是透過堆疊(Stack) 來傳入函式的,不懂什麼是堆疊也沒關係,就把它看成是另外一塊記憶體也行,當在程式中呼叫某個函式時,傳入的參數會先被複製到這塊記憶體中,當在函式中要使用這些參數時再從堆疊中去取出來。 以版本1的例子來說明,如下在程式中大概會這樣呼叫。 char* pp = NULL; alloc_mem(pp); pp一開始的初值是NULL,當呼叫alloc_mem時,pp的值會被複製到堆疊中(傳址),這種情況和以下的code事實上是對等的,只不過 p的值一開始被初始化成和pp的值一樣,p就好像一個區域變數一樣,一離開函式後這個變數就無效了,所以在外面的pp的值永遠都不會改變,同時在涵式中 new出來的記憶體也lost掉了。 void alloc_mem() {   char* p = new char[100]; } 再來看版本2,它的參數是一個指標的指標,這是什麼意思,我們先從實際使用上來看,如下。 char* pp = NULL; alloc_mem(&pp); 這次我們把pp這個變數的位址傳入涵式,所以在涵式中所得到的是pp這個變數的位址,在函式中p所含的內容是pp的位址,pp是一個char*形態的變數,p是一個指標它的內容是char*的形態,現在p已經指向pp了,所以對p的內容作改變,相對的pp的值也會跟著改變。 現在來看另...