C++從零開始(下)

C++從零開始(十一)上篇

——類的相關知識

    前面已經介紹了自定義類型的成員變量和成員函數的概念,並給出它們各自的語義,本文繼續說明自定義類型剩下的內容,並說明各自的語義。


權限

    成員函數的提供,使得自定義類型的語義從資源提升到了具有功能的資源。什麼叫具有功能的資源?比如要把收音機映射爲數字,需要映射的操作有調整收音機的頻率以接收不同的電臺;調整收音機的音量;打開和關閉收音機以防止電力的損耗。爲此,收音機應映射爲結構,類似下面:
    struct Radiogram
    {
        double Frequency;  /* 頻率 */  void TurnFreq( double value );   // 改變頻率
        float  Volume;     /* 音量 */  void TurnVolume( float value );  // 改變音量
        float  Power;      /* 電力 */  void TurnOnOff( bool bOn );      // 開關
        bool   bPowerOn;   // 是否開啓
    };
    上面的Radiogram::Frequency、Radiogram::Volume和Radiogram::Power由於定義爲了結構Radiogram的成員,因此它們的語義分別爲某收音機的頻率、某收音機的音量和某收音機的電力。而其餘的三個成員函數的語義也同樣分別爲改變某收音機的頻率、改變某收音機的音量和打開或關閉某收音機的電源。注意這面的“某”,表示具體是哪個收音機的還不知道,只有通過成員操作符將左邊的一個具體的收音機和它們結合時才知道是哪個收音機的,這也是爲什麼它們被稱作偏移類型。這一點在下一篇將詳細說明。
    注意問題:爲什麼要將剛纔的三個操作映射爲結構Radiogram的成員函數?因爲收音機具有這樣的功能?那麼對於選西瓜、切西瓜和喫西瓜,難道要定義一個結構,然後給它定義三個選、切、喫的成員函數??不是很荒謬嗎?前者的三個操作是對結構的成員變量而言,而後者是對結構本身而言的。那麼改成喫快餐,喫快餐的漢堡包、喫快餐的薯條和喝快餐的可樂。如果這裏的兩個喫和一個喝的操作變成了快餐的成員函數,表示是快餐的功能?!這其實是編程思想的問題,而這裏其實就是所謂的面向對象編程思想,它雖然是很不錯的思想,但並不一定是合適的,下篇將詳細討論。
    上面我們之所以稱收音機的換臺是功能,是因爲實際中我們自己是無法直接改變收音機的頻率,必須通過旋轉選臺的那個旋鈕來改變接收的頻率,同樣,調音量也是通過調節音量旋鈕來實現的,而由於開機而導致的電力下降也不是我們直接導致,而是間接通過收聽電臺而導致的。因此上面的Radiogram::Power、Radiogram::Frequency等成員變量都具有一個特殊特性——外界,這臺收音機以外的東西是無法改變它們的。爲此,C++提供了一個語法來實現這種語義。在類型定義符中,給出這樣的格式:<權限>:。這裏的<權限>爲public、protected和private中的一個,分別稱作公共的、保護的和私有的,如下:
    class Radiogram
    {
    protected: double m_Frequency; float m_Volume; float m_Power;
    private:   bool   m_bPowerOn;
    public:    void TurnFreq( double ); void TurnVolume( float ); void TurnOnOff( bool );
    };
    可以發現,它和之前的標號的定義格式相同,但並不是語句修飾符,即可以struct ABC{ private: };。這裏不用非要在private:後面接語句,因爲它不是語句修飾符。從它開始,直到下一個這樣的語法,之間所有的聲明和定義而產生的成員變量或成員函數都帶有了它所代表的語義。比如上面的類Radiogram,其中的Radiogram::m_Frequency、Radiogram::m_Volume和Radiogram::m_Power是保護的成員變量,Radiogram::m_bPowerOn是私有的成員變量,而剩下的三個成員函數都是公共的成員函數。注意上面的語法是可以重複的,如:struct ABC { public: public: long a; private: float b; public: char d; };。
    什麼意思?很簡單,公共的成員外界可以訪問,保護的成員外界不能訪問,私有的成員外界及子類不能訪問。關於子類後面說明。先看公共的。對於上面,如下將報錯:
    Radiogram a; a.m_Frequency = 23.0; a.m_Power = 1.0f; a.m_bPowerOn = true;
    因爲上面對a的三次操作都使用了a的保護或私有成員,編譯器將報錯,因爲這兩種成員外界是不能訪問的。而a.TurnFreq( 10 );就沒有任何問題,因爲成員函數Radiogram::TurnFreq是公共成員,外界可以訪問。那麼什麼叫外界?對於某個自定義類型,此自定義類型的成員函數的函數體內以外的一切能寫代碼的地方都稱作外界。因此,對於上面的Radiogram,只有它的三個成員函數的函數體內可以訪問它的成員變量。即下面的代碼將沒有問題。
    void Radiogram::TurnFreq( double value ) { m_Frequency += value; }
    因爲m_Frequency被使用的地方是在Radiogram::TurnFreq的函數體內,不屬於外界。
    爲什麼要這樣?表現最開始說的語義。首先,上面將成員定義成public或private對於最終生成的代碼沒有任何影響。然後,我之前說的調節接收頻率是通過調節收音機裏面的共諧電容的容量來實現的,這個電容的容量人必須藉助元件才能做到,而將接收頻率映射成數字後,由於是數字,則CPU就能修改。如果直接a.m_Frequency += 10;進行修改,就代碼上的意義,其就爲:執行這個方法的人將收音機的接收頻率增加10KHz,這有違我們的客觀世界,與前面的語義不合。因此將其作爲語法的一種提供,由編譯器來進行審查,可以讓我們編寫出更加符合我們所生活的世界的語義的代碼。
    應注意可以union ABC { long a; private: short b; };。這裏的ABC::a之前沒有任何修飾,那它是public還是protected?相信從前面舉的那麼多例子也已經看出,應該是public,這也是爲什麼我之前一直使用struct和union來定義自定義類型,否則之前的例子都將報錯。而前篇說過結構和類只有一點很小的區別,那就是當成員沒有進行修飾時,對於類,那個成員將是private而不是public,即如下將錯誤。
    class ABC { long a; private: short b; }; ABC a; a.a = 13;
    ABC::a由於前面的class而被看作private。就從這點,可以看出結構用於映射資源(可被直接使用的資源),而類用於映射具有功能的資源。下篇將詳細討論它們在語義上的差別。


構造和析構

    瞭解了上面所提的東西,很明顯就有下面的疑問:
    struct ABC { private: long a, b; }; ABC a = { 10, 20 };
    上面的初始化賦值變量a還正確嗎?當然錯誤,否則在語法上這就算一個漏洞了(外界可以藉此修改不能修改的成員)。但有些時候的確又需要進行初始化以保證一些邏輯關係,爲此C++提出了構造和析構的概念,分別對應於初始化和掃尾工作。在瞭解這個之前,讓我們先看下什麼叫實例(Instance)。
    實例是個抽象概念,表示一個客觀存在,其和下篇將介紹的“世界”這個概念聯繫緊密。比如:“這是桌子”和“這個桌子”,前者的“桌子”是種類,後者的“桌子”是實例。這裏有10只羊,則稱這裏有10個羊的實例,而羊只是一種類型。可以簡單地將實例認爲是客觀世界的物體,人類出於方便而給各種物體分了類,因此給出電視機的說明並沒有給出電視機的實例,而拿出一臺電視機就是給出了一個電視機的實例。同樣,程序的代碼寫出來了意義不大,只有當它被執行時,我們稱那個程序的一個實例正在運行。如果在它還未執行完時又要求操作系統執行了它,則對於多任務操作系統,就可以稱那個程序的兩個實例正在被執行,如同時點開兩個Word文件查看,則有兩個Word程序的實例在運行。
    在C++中,能被操作的只有數字,一個數字就是一個實例(這在下篇的說明中就可以看出),更一般的,稱標識記錄數字的內存的地址爲一個實例,也就是稱變量爲一個實例,而對應的類型就是上面說的物體的種類。比如:long a, *pA = &a, &ra = a;,這裏就生成了兩個實例,一個是long的實例,一個是long*的實例(注意由於ra是long&所以並未生成實例,但ra仍然是一個實例)。同樣,對於一個自定義類型,如:Radiogram ab, c[3];,則稱生成了四個Radiogram的實例。
    對於自定義類型的實例,當其被生成時,將調用相應的構造函數;當其被銷燬時,將調用相應的析構函數。誰來調用?編譯器負責幫我們編寫必要的代碼以實現相應構造和析構的調用。構造函數的原型(即函數名對應的類型,如float AB( double, char );的原型是float( double, char ))的格式爲:直接將自定義類型的類型名作爲函數名,沒有返回值類型,參數則隨便。對於析構函數,名字爲相應類型名的前面加符號“~”,沒有返回值類型,必須沒有參數。如下:
struct ABC { ABC(); ABC( long, long ); ~ABC(); bool Do( long ); long a, count; float *pF; };
ABC::ABC() { a = 1; count = 0; pF = 0; }
ABC::ABC( long tem1, long tem2 ) { a = tem1; count = tem2; pF = new float[ count ]; }
ABC::~ABC() { delete[] pF; }
bool ABC::Do( long cou )
{
    float *p = new float[ cou ];
    if( !p )
        return false;
    delete[] pF;
    pF = p;
    count = cou;
    return true;
}
extern ABC g_ABC;
void main(){ ABC a, &r = a; a.Do( 10 ); { ABC b( 10, 30 ); } ABC *p = new ABC[10]; delete[] p; }
ABC g_a( 10, 34 ), g_p = new ABC[5];
    上面的結構ABC就定義了兩個構造函數(注意是兩個重載函數),名字都爲ABC::ABC(實際將由編譯器轉成不同的符號以供連接之用)。也定義了一個析構函數(注意只能定義一個,因爲其必須沒有參數,也就無法進行重載了),名字爲ABC::~ABC。
    再看main函數,先通過ABC a;定義了一個變量,因爲要在棧上分配一塊內存,即創建了一個數字(創建裝數字的內存也就導致創建了數字,因爲內存不能不裝數字),進而創建了一個ABC的實例,進而調用ABC的構造函數。由於這裏沒有給出參數(後面說明),因此調用了ABC::ABC(),進而a.a爲1,a.pF和a.count都爲0。接着定義了變量r,但由於它是ABC&,所以並沒有在棧上分配內存,進而沒有創建實例而沒有調用ABC::ABC。接着調用a.Do,分配了一塊內存並把首地址放在a.pF中。
    注意上面變量b的定義,其使用了之前提到的函數式初始化方式。它通過函數調用的格式調用了ABC的構造函數ABC::ABC( long, long )以初始化ABC的實例b。因此b.a爲10,b.count爲30,b.pF爲一內存塊的首地址。但要注意這種初始化方式和之前提到的“{}”方式的不同,前者是進行了一次函數調用來初始化,而後者是編譯器來初始化(通過生成必要的代碼)。由於不調用函數,所以速度要稍快些(關於函數的開銷在《C++從零開始(十五)》中說明)。還應注意不能ABC b = { 1, 0, 0 };,因爲結構ABC已經定義了兩個構造函數,則它只能使用函數式初始化方式初始化了,不能再通過“{}”方式初始化了。
    上面的b在一對大括號內,回想前面提過的變量的作用域,因此當程序運行到ABC *p = new ABC[10];時,變量b已經消失了(超出了其作用域),即其所分配的內存語法上已經釋放了(實際由於是在棧上,其並沒有被釋放),進而調用ABC的析構函數,將b在ABC::ABC( long, long )中分配的內存釋放掉以實現掃尾功能。
    對於通過new在堆上分配的內存,由於是new ABC[10],因此將創建10個ABC的實例,進而爲每一個實例調用一次ABC::ABC(),注意這裏無法調用ABC::ABC( long, long ),因爲new操作符一次性就分配了10個實例所需要的內存空間,C++並沒有提供語法(比如使用“{}”)來實現對一次性分配的10個實例進行初始化。接着調用了delete[] p;,這釋放剛分配的內存,即銷燬了10個實例,因此將調用ABC的析構函數10次以進行10次掃尾工作。
    注意上面聲明瞭全局變量g_ABC,由於是聲明,並不是定義,沒有分配內存,因此未產生實例,故不調用ABC的構造函數,而g_a由於是全局變量,C++保證全局變量的構造函數在開始執行main函數之前就調用,所有全局變量的析構函數在執行完main函數之後才調用(這一點是編譯器來實現的,在《C++從零開始(十九)》中將進一步討論)。因此g_a.ABC( 10, 34 )的調用是在a.ABC()之前,即使它的位置在a的定義語句的後面。而全局變量g_p的初始化的數字是通過new操作符的計算得來,結果將在堆上分配內存,進而生成5個ABC實例而調用了ABC::ABC()5次,由於是在初始化g_p的時候進行分配的,因此這5次調用也在a.ABC()之前。由於g_p僅僅只是記錄首地址,而要釋放這5個實例就必須調用delete(不一定,也可不調用delete依舊釋放new返回的內存,在《C++從零開始(十九)》中說明),但上面並沒有調用,因此直到程序結束都將不會調用那5個實例的析構函數,那將怎樣?後面說明異常時再討論所謂的內存泄露問題。
    因此構造的意思就是剛分配了一塊內存,還未初始化,則這塊內存被稱作原始數據(Raw Data),前面說過數字都必須映射成算法中的資源,則就存在數字的有效性。比如映射人的年齡,則這個數字就不能是負數,因爲沒有意義。所以當得到原始數據後,就應該先通過構造函數的調用以保證相應實例具有正確的意義。而析構函數就表示進行掃尾工作,就像上面,在某實例運作的期間(即操作此實例的代碼被執行的時期)動態分配了一些內存,則應確保其被正確釋放。再或者這個實例和其他實例有關係,因確保解除關係(因爲這個實例即將被銷燬),如鏈表的某個結點用類映射,則這個結點被刪除時應在其析構函數中解除它與其它結點的關係。


派生和繼承

    上面我們定義了類Radiogram來映射收音機,如果又需要映射數字式收音機,它和收音機一樣,即收音機具有的東西它都具有,不過多了自動搜臺、存儲臺、選臺和刪除臺的功能。這裏提出了一個類型體系,即一個實例如果是數字式收音機,那它一定也是收音機,即是收音機的一個實例。比如蘋果和梨都是水果,則蘋果和梨的實例一定也是水果的實例。這裏提出三個類型:水果、蘋果和梨。其中稱水果是蘋果的父類(父類型),蘋果是水果的子類(子類型)。同樣,水果也是梨的父類,梨是水果的子類。這種類型體系是很有意義的,因爲人類就是用這種方式來認知世界的,它非常符合人類的思考習慣,因此C++又提出了一種特殊語法來對這種語義提供支持。
    在定義自定義類型時,在類型名的後面接一“:”,然後接public或protected或private,接着再寫父類的類型名,最後就是類型定義符“{}”及相關書寫,如下:
    class DigitalRadiogram : public Radiogram
    {
    protected:  double m_Stations[10];
    public:     void SearchStation();                void SaveStation( unsigned long );
                void SelectStation( unsigned long ); void EraseStation( unsigned long );
    };
    上面就將Radiogram定義爲了DigitalRadiogram的父類,DigitalRadiogram定義成了Radiogram的子類,被稱作類Radiogram派生了類DigitalRadiogram,類DigitalRadiogram繼承了類Radiogram。
    上面生成了5個映射元素,就是上面的4個成員函數和1個成員變量,但實際不止。由於是從Radiogram派生,因此還將生成7個映射,就是類Radiogram的7個成員,但名字變化了,全變成DigitalRadiogram::修飾,而不是原來的Radiogram::修飾,但是類型卻不變化。比如其中一個映射元素的名字就爲DigitalRadiogram::m_bPowerOn,類型爲bool Radiogram::,映射的偏移值沒變,依舊爲16。同樣也有映射元素DigitalRadiogram::TurnFreq,類型爲void ( Radiogram:: )( double ),映射的地址依舊沒變,爲Radiogram::TurnFreq所對應的地址。因此就可以如下:
    void DigitalRadiogram::SaveStation( unsigned long index )
    {
        if( index >= 10 ) return;
        m_Station[ index ] = m_Frequency; m_bPowerOn = true;
    }
    DigitalRadiogram a; a.TurnFreq( 10 ); a.SaveStation( 3 );
    上面雖然沒有聲明DigitalRadiogram::TurnFreq,但依舊可以調用它,因爲它是從Radiogram派生來的。注意由於a.TurnFreq( 10 );沒有書寫全名,因此實際是a.DigitalRadiogram::TurnFreq( 10 );,因爲成員操作符左邊的數字類型是DigitalRadiogram。如果DigitalRadiogram不從Radiogram派生,則不會生成上面說的7個映射,結果a.TurnFreq( 10 );將錯誤。
    注意上面的SaveStation中,直接書寫了m_Frequency,其等同於this->m_Frequency,由於this是DigitalRadiogram*(因爲在DigitalRadiogram::SaveStation的函數體內),所以實際爲this->DigitalRadiogram::m_Frequency,也因此,如果不是派生自Radiogram,則上面將報錯。並且由類型匹配,很容易知道:void ( Radiogram::*p )( double ) = DigitalRadiogram::TurnFreq;。雖然這裏是DigitalRadiogram::TurnFreq,但它的類型是void ( Radiogram:: )( double )。
    應注意在SaveStation中使用了m_bPowerOn,這個在Radiogram中被定義成私有成員,也就是說子類也沒權訪問,而SaveStation是其子類的成員函數,因此上面將報錯,權限不夠。
    上面通過派生而生成的7個映射元素各自的權限是什麼?先看上面的派生代碼:
    class DigitalRadiogram : public Radiogram {…};
    這裏由於使用public,被稱作DigitalRadiogram從Radiogram公共繼承,如果改成protected則稱作保護繼承,如果是private就是私有繼承。有什麼區別?通過公共繼承而生成的映射元素(指從Radiogram派生而生成的7個映射元素),各自的權限屬性不變化,即上面的DigitalRadiogram::m_Frequency對類DigitalRadiogram來說依舊是protected,而DigitalRadiogram::m_bPowerOn也依舊是private。保護繼承則所有的公共成員均變成保護成員,其它不變。即如果保護繼承,DigitalRadiogram::TurnFreq對於DigitalRadiogram來說將爲protected。私有繼承則將所有的父類成員均變成對於子類來說是private。因此上面如果私有繼承,則DigitalRadiogram::TurnFreq對於DigitalRadiogram來說是private的。
    上面可以看得很簡單,即不管是什麼繼承,其指定了一個權限,父類中凡是高於這個權限的映射元素,都要將各自的權限降低到這個權限(注意是對子類來說),然後再繼承給子類。上面一直強調“對於子類來說”,什麼意思?如下:
    struct A { long a; protected: long b; private: long c; };
    struct B : protected A { void AB(); };
    struct C : private B { void ABC(); };
    void B::AB() { b = 10; c = 10; }
    void C::ABC() { a = 10; b = 10; c = 10; AB(); }
    A a; B b; C c; a.a = 10; b.a = 10; b.AB(); c.AB();
    上面的B的定義等同於struct B { protected: long a, b; private: long c; public: void AB(); };。
    上面的C的定義等同於struct C { private: long a, b, c; void AB(); public: void ABC(); };
    因此,B::AB中的b = 10;沒有問題,但c = 10;有問題, 因爲編譯器看出B::c是從父類繼承生成的,而它對於父類來說是私有成員,因此子類無權訪問,錯誤。接着看C::ABC,a = 10;和b = 10;都沒問題,因爲它們對於B來說都是保護成員,但c = 10;將錯誤,因爲C::c對於父類B來說是私有成員,沒有權限,失敗。接着AB();,因爲C::AB對於父類B來說是公共成員,沒有問題。
    接着是a.a = 10;,沒問題;b.a = 10;,錯誤,因爲B::a是B的保護成員;b.AB();,沒有問題;c.AB();,錯誤,因爲C::AB是C的私有成員。應注意一點:public、protected和private並不是類型修飾符,只是在語法上提供了一些信息,而繼承所得的成員的類型都不會變化,不管它保護繼承還是公共繼承,權限起作用的地方是需要運用成員的地方,與類型沒有關係。什麼叫運用成員的地方?如下:
    long ( A::*p ) = &A::a; p = &A::b;
    void ( B::*pB )() = B::AB; void ( C::*pC )() = C::ABC; pC = C::AB;
    上面對變量p的初始化操作沒有問題,這裏就運用了A::a。但是在p = &A::b;時,由於運用了A::b,則編譯器就要檢查代碼所處的地方,發現對於A來說屬於外界,因此報錯,權限不夠。同樣下面對pB的賦值沒有問題,但pC = C::AB;就錯誤。而對於b.a = 10;,這裏由於成員操作符而運用了類B的成員B::a,所以在這裏進行權限檢查,並進而發現權限不夠而報錯。
    好,那爲什麼要搞得這麼複雜?弄什麼保護、私有和公共繼承?首先回想前面說的爲什麼要提供繼承,因爲想從代碼上體現類型體系,說明一個實例如果是一個子類的實例,則它也一定是一個父類的實例,即可以按照父類的定義來操作它。雖然這也可以通過之前說的轉換指針類型來實現,但前者能直接從代碼上表現出類型繼承的語義(即子類從父類派生而來),而後者只能說明用不同的類型來看待同一個實例。
    那爲什麼要給繼承加上權限?表示這個類不想外界或它的子類以它的父類的姿態來看待它。比如雞可以被食用,但做成標本的雞就不能被食用。因此子類“雞的標本”在繼承時就應該保護繼承父類“雞”,以表示不準外界(但准許其派生類)將它看作是雞。它已經不再是雞,但它實際是由雞轉變過來的。因此私有和保護繼承實際很適合表現動物的進化關係。比如人是猴子進化來的,但人不是猴子。這裏人就應該使用私有繼承,因爲並不希望外界和人的子類——黑種人、黃種人、白種人等——能夠把父類“人”看作是猴子。而公共繼承就表示外界和子類可以將子類的實例看成父類的實例。如下:
struct A { long a, b; };
struct AB : private A { long c; void ABCD(); };
struct ABB : public AB { void AAA(); };
struct AC : public A { long c; void ABCD(); };
void ABC( A *a ) { a->a = 10; a->b = 20; }
void main() { AB b; ABC( &b ); AC c; ABC( &c ); }
void AB::ABCD() { AB b; ABC( &b ); }
void AC::ABCD() { AB b; ABC( &b ); }
void ABB::AAA() { AB b; ABC( &b ); }
    上面的類AC是公共繼承,因此其實例c在執行ABC( &c );時將由編譯器進行隱式類型轉換,這是一個很奇特的特性,本文的下篇將說明。但類AB是私有繼承,因此在ABC( &b );時編譯器不會進行隱式類型轉換,將報錯,類型不匹配。對於此只需ABC( ( A* )&b );以顯示進行類型轉換就沒問題了。
    注意前面的紅字,私有繼承表示外界和它的子類都不可以用父類的姿態來看待它,因此在ABB::AAA中,這是AB的子類,因此這裏的ABC( &b );將報錯。在AC::ABCD中,這裏對於AB來說是外界,報錯。在AB::ABCD中,這裏是自身,即不是子類也不是外界,所以ABC( &b );將沒有問題。如果將AB換成保護繼承,則在ABB::AAA中的ABC( &b );將不再錯誤。
    關於本文及本文下篇所討論的語義,在《C++從零開始(十二)》中會專門提出一個概念以給出一種方案來指導如何設計類及各類的關係。由於篇幅限制,本文分成了上中下三篇,剩下的內容在本文的後兩篇說明。

C++從零開始(十一)中篇

——類的相關知識

    由於篇幅限制,本篇爲《C++從零開始(十一)》的中篇,說明多重繼承、虛繼承和虛函數的實現方式。


多重繼承

    這裏有個有趣的問題,如下:
    struct A { long a, b, c; char d; }; struct B : public A { long e, f; };
    上面的B::e和B::f映射的偏移是多少?不同的編譯器有不同的映射結果,對於派生的實現,C++並沒有強行規定。大多數編譯器都是讓B::e映射的偏移值爲16(即A的長度,關於自定義類型的長度可參考《C++從零開始(九)》),B::f映射20。這相當於先把空間留出來排列父類的成員變量,再排列自己的成員變量。但是存在這樣的語義——西紅柿即是蔬菜又是水果,鯨魚即是海洋生物又是脯乳動物。即一個實例既是這種類型又是那種類型,對於此,C++提供了多重派生或稱多重繼承,用“,”間隔各父類,如下:
    struct A { long A_a, A_b, c; void ABC(); }; struct B { long c, B_b, B_a; void ABC(); };
    struct AB : public A, public B { long ab, c; void ABCD(); };
    void A::ABC() { A_a = A_b = 10; c = 20; }
    void B::ABC() { B_a = B_b = 20; c = 10; }
    void AB::ABCD() { A_a = B_a = 1; A_b = B_b = 2; c = A::c = B::c = 3; }
    void main() { AB ab; ab.A_a = 3; ab.B_b = 4; ab.ABC(); }
    上面的結構AB從結構A和結構B派生而來,即我們可以說ab既是A的實例也是B的實例,並且還是AB的實例。那麼在派生AB時,將生成幾個映射元素?照前篇的說法,除了AB的類型定義符“{}”中定義的AB::ab和AB::c以外(類型均爲long AB::),還要生成繼承來的映射元素,各映射元素名字的修飾換成AB::,類型不變,映射的值也不變。因此對於兩個父類,則生成8個映射元素(每個類都有4個映射元素),比如其中一個的名字爲AB::A_b,類型爲long A::,映射的值爲4;也有一個名字爲AB::B_b,類型爲long B::,映射的值依舊爲4。注意A::ABC和B::ABC的名字一樣,因此其中兩個映射元素的名字都爲AB::ABC,但類型則一個爲void( A:: )()一個爲void( B:: )(),映射的地址分別爲A::ABC和B::ABC。同樣,就有三個映射元素的名字都爲AB::c,類型則分別爲long A::、long B::和long AB::,映射的偏移值依次爲8、0和28。照前面說的先排列父類的成員變量再排列子類的成員變量,因此類型爲long AB::的AB::c映射的值爲兩個父類的長度之和再加上AB::ab所帶來的偏移。
    注意問題,上面繼承生成的8個映射元素中有兩對同名,但不存在任何問題,因爲它們的類型不同,而最後編譯器將根據它們各自的類型而修改它們的名字以形成符號,這樣連接時將不會發生重定義問題,但帶來其他問題。ab.ABC();一定是ab.AB::ABC();的簡寫,因爲ab是AB類型的,但現在由於有兩個AB::ABC,因此上面直接書寫ab.ABC將報錯,因爲無法知道是要哪個AB::ABC,這時怎麼辦?
    回想本文上篇提到的公共、保護、私有繼承,其中說過,公共就表示外界可以將子類的實例當作父類的實例來看待。即所有需要用到父類實例的地方,如果是子類實例,且它們之間是公共繼承的關係,則編譯器將會進行隱式類型轉換將子類實例轉換成父類實例。因此上面的ab.A_a = 3;實際是ab.AB::A_a = 3;,而AB::A_a的類型是long A::,而成員操作符要求兩邊所屬的類型相同,左邊類型爲AB,且AB爲A的子類,因此編譯器將自動進行隱式類型轉換,將AB的實例變成A的實例,然後再計算成員操作符。
    注意前面說AB::A_b和AB::B_b的偏移值都爲4,則ab.A_b = 3;豈不是等效於ab.B_b = 3;?即使按照上面的說法,由於AB::A_b和AB::B_b的類型分別是long A::和long B::,也最多隻是前者轉換成A的實例後者轉換成B的實例,AB::A_b和AB::B_b映射的偏移依舊沒變啊。因此變的是成員操作符左邊的數字。對於結構AB,假設先排列父類A的成員變量再排列父類B的成員變量,則AB::B_b映射的偏移就應該爲16(結構A的長度加上B::c引入的偏移),但它實際映射爲4,因此就將成員操作符左側的地址類型的數字加上12(結構A的長度)。而對於AB::A_b,由於結構A的成員變量先被排列,故只偏移0。假設上面ab對應的地址爲3000,對於ab.B_b = 4;,AB類型的地址類型的數字3000在“.”的左側,轉成B類型的地址類型的數字3012(因爲偏移12),然後再將“.”右側的偏移類型的數字4加上3012,最後返回類型爲long的地址類型的數字3016,再繼續計算“=”。同樣也可知道ab.A_a = 3;中的成員操作符最後返回long類型的地址類型的數字3000,而ab.A_b將返回3004,ab.ab將返回3024。
    同樣,這樣也將進行隱式類型轉換long AB::*p = &AB::B_b;。注意AB::B_b的類型爲long B::,則將進行隱式類型轉換。如何轉換?原來AB::B_b映射的偏移爲4,則現在將變成12+4=16,這樣才能正確執行ab.*p = 10;。
    這時再回過來想剛纔提的問題,AB::ABC無法區別,怎麼辦?注意還有映射元素A::ABC和B::ABC(兩個AB::ABC就是由於它們兩個而導致的),因此可以書寫ab.A::ABC();來表示調用的是映射到A::ABC的函數。這裏的A::ABC的類型是void( A:: )(),而ab是AB,因此將隱式類型轉換,則上面沒有任何語法問題(雖然說A::ABC不是結構AB的成員,但它是AB的父類的成員,C++允許這種情況,也就是說A::ABC的名字也作爲類型匹配的一部分而被使用。如假設結構C也從A派生,則有C::a,但就不能書寫ab.C::a,因爲從C::a的名字可以知道它並不屬於結構AB)。同樣ab.B::ABC();將調用B::ABC。注意上面結構A、B和AB都有一個成員變量名字爲c且類型爲long,那麼ab.c = 10;是否會如前面ab.ABC();一樣報錯?不會,因爲有三個AB::c,其中有一個類型和ab的類型匹配,其映射的偏移爲28,因此ab.c將會返回3028。而如果期望運用其它兩個AB::c的映射,則如上通過書寫ab.A::c和ab.B::c來偏移ab的地址以實現。
    注意由於上面的說法,也就可以這樣:void( AB::*pABC )() = B::ABC; ( ab.*pABC )();。這裏的B::ABC的類型爲void( B:: )(),和pABC不匹配,但正好B是AB的父類,因此將進行隱式類型轉換。如何轉換?因爲B::ABC映射的是地址,而隱式類型轉換要保證在調用B::ABC之前,先將this的類型變成B*,因此要將其加12以從AB*轉變成B*。由於需要加這個12,但B::ABC又不是映射的偏移值,因此pABC實際將映射兩個數字,一個是B::ABC對應的地址,一個是偏移值12,結果pABC這個指針的長度就不再如之前所說的爲4個字節,而變成了8個字節(多出來的4個字節用於記錄偏移值)。
    還應注意前面在AB::ABCD中直接書寫的A_b、c、A::c等,它們實際都應該在前面加上this->,即A_b = B_b = 2;實際爲this->A_b = this->B_b = 2;,則同樣如上,this被偏移了兩次以獲得正確的地址。注意上面提到的隱式類型轉換之所以會進行,是因爲繼承時的權限滿足要求,否則將失敗。即如果上面AB保護繼承A而私有繼承B,則只有在AB的成員函數中可以如上進行轉換,在AB的子類的成員函數中將只能使用A的成員而不能使用B的成員,因爲權限受到限制。如下將失敗。
    struct AB : protected A, private B {…};
    struct C : public AB { void ABCD(); };
    void C::ABCD() { A_b = 10; B_b = 2; c = A::c = B::c = 24; }
    這裏在C::ABCD中的B_b = 2;和B::c = 24;將報錯,因爲這裏是AB的子類,而AB私有繼承自B,其子類無權將它看作B。但只是不會進行隱式類型轉換罷了,依舊可以通過顯示類型轉換來實現。而main函數中的ab.A_a = 3; ab.B_b = 4; ab.A::ABC();都將報錯,因爲這是在外界發起的調用,沒有權限,不會自動進行隱式類型轉換。
    注意這裏C::ABCD和AB::ABCD同名,按照上面所說,子類的成員變量都可以和父類的成員變量同名(上面AB::c和A::c及B::c同名),成員函數就更沒有問題。只用和前面一樣,按照上面所說進行類型匹配檢驗即可。應注意由於是函數,則可以參數變化而函數名依舊相同,這就成了重載函數。


虛繼承

    前面已經說了,當生成了AB的實例,它的長度實際應該爲A的長度加B的長度再加上AB自己定義的成員所佔有的長度。即AB的實例之所以又是A的實例又是B的實例,是因爲一個AB的實例,它既記錄了一個A的實例又記錄了一個B的實例。則有這麼一種情況——蔬菜和水果都是植物,海洋生物和脯乳動物都是動物。即繼承的兩個父類又都從同一個類派生而來。假設如下:
    struct A { long a; };
    struct B : public A { long b; }; struct C : public A { long c; };
    struct D : public A, public C { long d; };
    void main() { D d; d.a = 10; }
    上面的B的實例就包含了一個A的實例,而C的實例也包含了一個A的實例。那麼D的實例就包含了一個B的實例和一個C的實例,則D就包含了兩個A的實例。即D定義時,將兩個父類的映射元素繼承,生成兩個映射元素,名字都爲D::a,類型都爲long A::,映射的偏移值也正好都爲0。結果main函數中的d.a = 10;將報錯,無法確認使用哪個a。這不是很奇怪嗎?兩個映射元素的名字、類型和映射的數字都一樣!編譯器爲什麼就不知道將它們定成一個,因爲它們實際在D的實例中表示的偏移是不同的,一個是0一個是8。同樣,爲了消除上面的問題,就書寫d.B::a = 1; d.C::a = 2;以表示不同實例中的成員a。可是B::a和C::a的類型不都是爲long A::嗎?但上面說過,成員變量或成員函數它們自身的名字也將在類型匹配中起作用,因此對於d.B::a,因爲左側的類型是D,則看右側,其名字表示爲B,正好是D的父類,先隱式類型轉換,然後再看類型,是A,再次進行隱式類型轉換,然後返回數字。假設上面d對應的地址爲3000,則d.C::a先將d這個實例轉換成C的實例,因此將3000偏移8個字節而返回long類型的地址類型的數字3008。然後再轉換成A的實例,偏移0,最後返回3008。
    上面說明了一個問題,即希望從A繼承來的成員a只有一個實例,而不是像上面那樣有兩個實例。假設動物都有個飢餓度的成員變量,很明顯地鯨魚應該只需填充一個飢餓度就夠了,結果有兩個飢餓度就顯得很奇怪。對此,C++提出了虛繼承的概念。其格式就是在繼承父類時在權限語法的前面加上關鍵字virtual即可,如下:
    struct A { long a, aa, aaa; void ABC(); }; struct B : virtual public A { long b; };
    這裏的B就虛繼承自A,B::b映射的偏移爲多少?將不再是A的長度12,而是4。而繼承生成的3個映射元素還是和原來一樣,只是名字修飾變成B::而已,映射依舊不變。那麼爲什麼B::b是4?之前的4個字節用來放什麼?上面等同於下面:
    struct B { long *p; long b; long a, aa, aaa; void ABC(); };
    long BDiff[] = { 0, 8 }; B::B(){ p = BDiff; }
    上面的B::p指向一全局數組BDiff。什麼意思?B的實例的開頭4個字節用來記錄一個地址,也就相當於是一個指針變量,它記錄的地址所標識的內存中記錄着由於虛繼承而導致的偏移值。上面的BDiff[1]就表示要將B實例轉成A實例,就需要偏移BDiff[1]的值8,而BDiff[0]就表示要將B實例轉成B實例需要的偏移值0。爲什麼還要來個B實例轉B實例?後面說明。但爲什麼是數組?因爲一個類可以通過多重派生而虛繼承多個類,每個類需要的偏移值都會在BDiff的數組中佔一個元素,它被稱作虛類表(Virtual Class Table)。
    因此當書寫B b; b.aaa = 20; long a = sizeof( b );時,a的值爲20,因爲多了一個4字節來記錄上面說的指針。假設b對應的地址爲3000。先將B的實例轉換成A的實例,本來應該偏移12而返回3012,但編譯器發現B是虛繼承自A,則通過B::p[1]得到應該的偏移值8,然後返回3008,接着再加上B::aaa映射的8而返回3016。同樣,當b.b = 10;時,由於B::b並不是被虛繼承而來,直接將3000加上B::b映射的偏移值4得3004。而對於b.ABC();將先通過B::p[1]將b轉成A的實例然後調用A::ABC。
    爲什麼要像上面那樣弄得那麼麻煩?首先讓我們來了解什麼叫做虛(Virtual)。虛就是假象,並不是真的。比如一臺老式電視機有10個頻道,即它最多能記住10個電視臺的頻率。因此可以說1頻道是中央1臺、5頻道是中央5臺、7頻道是四川臺。這裏就稱頻道對我們來說代表着電臺頻率是虛假的,因爲頻道並不是電臺頻率,只是記錄了電臺頻率。當我們按5頻道以換到中央5臺時,有可能有人已經調過電視使得5頻道不再是中央5臺,而是另一個電視臺或者根本就是一片雪花沒有信號。因此虛就表示不保證,其可能正確可能錯誤,因爲它一定是間接得到的,其實就相當於之前說的引用。有什麼好處?只用記着按5頻道就是中央5臺,當以後不想再看中央5臺而換成中央2臺,則同樣的“按5頻道”卻能得到不同的結果,但是程序卻不用再編寫了,只用記着“按5頻道”就又能實現換到中央2臺看。所以虛就是間接得到結果,由於間接,結果將不確定而顯得更加靈活,這在後面說明虛函數時就能看出來。但虛的壞處就是多了一道程序(要間接獲得),效率更低。
    由於上面的虛繼承,導致繼承的元素都是虛的,即所有對繼承而來的映射元素的操作都應該間接獲得相應映射元素對應的偏移值或地址,但繼承的映射元素對應的偏移值或地址是不變的,爲此紅字的要求就只有通過隱式類型轉換改變this的值來實現。所以上面說的B轉A需要的偏移值通過一個指針B::p來間接獲得以表現其是虛的。
    因此,開始所說的鯨魚將會有兩個飢餓度就可以讓海洋生物和脯乳動物都從動物虛繼承,因此將間接使用脯乳動物和海洋生物的飢餓度這個成員,然後在派生鯨魚這個類時,讓脯乳動物和海洋生物都指向同一個動物實例(因爲都是間接獲得動物的實例的,通過虛繼承來間接使用動物的成員),這樣當鯨魚填充飢餓度時,不管填充哪個飢餓度,實際都填充同一個。而C++也正好這樣做了。如下:
    struct A { long a; };
    struct B : virtual public A { long b; }; struct C : virtual public A { long c; };
    struct D : public B, virtual public C { long d; };
    void main() { D d; d.a = 10; }
    當從一個類虛繼承時,在排列派生類時(就是決定在派生類的類型定義符“{}”中定義的各成員變量的偏移值),先排列前面提到的虛類表的指針以實現間接獲取偏移值,再排列各父類,但如果父類中又有被虛繼承的父類,則先將這些部分剔除。然後排列派生類自己的映射元素。最後排列剛剛被剔除的被虛繼承的類,此時如果發現某個被虛繼承的類已經被排列過,則不用再重複排列一遍那個類,並且也不再爲它生成相應的映射元素。
    對於上面的B,發現虛繼承A,則先排列前面說過的B::p,然後排列A,但發現A需要被虛繼承,因此剔除,排列自己定義的映射元素B::b,映射的偏移值爲4(由於B::p的佔用)。最後排列A而生成繼承來的映射元素B::a,所以B的長度爲12。
    對於上面的D,發現要從C虛繼承,因此:
    排列D::p,佔4個字節。
    排列父類B,發現其中的A是被虛繼承的,剔除,所以將繼承映射元素B::b(還有前面編譯器自動生成的B::p),生成D::b,佔4個字節(編譯器將B::p和D::p合併爲一個,後面說明虛函數時就瞭解了)。
    排列父類C,發現C需要被虛繼承,剔除。
    排列D自己定義的成員D::d,其映射的偏移值就爲4+4=8,佔4個字節。
    排列A和C,先排列A,佔4個字節,生成D::a。
    排列C,先排列C中的A,結果發現它是虛繼承的,並發現已經排列過A,進而不再爲C::a生成映射元素。接着排列C::p和C::c,佔8個字節,生成D::c。
    所以最後結構D的長度爲4+4+4+4+8=24個字節,並且只有一個D::a,類型爲long A::,偏移值爲0。
    如果上面很昏,不要緊,上面只是給出一種算法以實現虛繼承,不同的編譯器廠商會給出不同的實現方法,因此上面推得的結果對某些編譯器可能並不正確。不過應記住虛繼承的含義——被虛繼承的類的所有成員都必須被間接獲得,至於如何間接獲得,則不同的編譯器有不同的處理方式。
    由於需要保證間接獲得,所以對於long D::*pa = &D::a;,由於是long D::*,編譯器發現D的繼承體系中存在虛繼承,必須要保證其某些成員的間接獲得,因此pa中放的將不再是偏移值,否則d.*pa = 10;將導致直接獲得偏移值(將pa的內容取出來即可),違反了虛繼承的含義。爲了要間接訪問pa所記錄的偏移值,則必須保證代碼執行時,當pa裏面放的是D::a時會間接,而D::d時則不間接。很明顯,這要更多和更復雜的代碼,大多數編譯器對此的處理就是全部都使用間接獲得。因此pa的長度將爲8字節,其中一個4字節記錄偏移,還有一個4字節記錄一個序號。這個序號則用於前面說的虛類表以獲得正確的因虛繼承而導致的偏移量。因此前面的B::p所指的第一個元素的值表示B實例轉換成B實例,是爲了在這裏實現全部間接獲得而提供的。
    注意上面的D::p對於不同的D的實例將不同,只不過它們的內容都相同(都是結構D的虛類表的地址)。當D的實例剛剛生成時,那個實例的D::p的值將是一隨機數。爲了保證D::p被正確初始化,上面的結構D雖然沒有生成構造函數,但編譯器將自動爲D生成一缺省構造函數(沒有參數的構造函數)以保證D::p和上面從C繼承來的C::p的正確初始化,結果將導致D d = { 23, 4 };錯誤,因爲D已經定義了一個構造函數,即使沒有在代碼上表現出來。
    那麼虛繼承有什麼意義呢?它從功能上說是間接獲得虛繼承來的實例,從類型上說與普通的繼承沒有任何區別,即虛繼承和前面的public等一樣,只是一個語法上的提供,對於數字的類型沒有任何影響。在瞭解它的意義之前先看下虛函數的含義。


虛函數

    虛繼承了一個函數類型的映射元素,按照虛繼承的說法,應該是間接獲得此函數的地址,但結果卻是間接獲得this參數的值。爲了間接獲得函數的地址,C++又提出了一種語法——虛函數。在類型定義符“{}”中書寫函數聲明或定義時,在聲明或定義語句前加上關鍵字virtual即可,如下:
    struct A { long a; virtual void ABC(), BCD(); };
    void A::ABC() { a = 10; } void A::BCD() { a = 5; }
    上面等同於下面:
    struct A { void ( A::*pF )(); long a; void ABC(), BCD(); A(); };
    void A::ABC() { a = 10; } void A::BCD() { a = 5; }
    void ( A::*AVF[] )() = { A::ABC, A::BCD }; void A::A() { pF = AVF; }
    這裏A的成員A::pF和之前的虛類表一樣,是一個指針,指向一個數組,這個數組被稱作虛函數表(Virtual Function Table),是一個函數指針的數組。這樣使用A::ABC時,將通過給出A::ABC在A::pF中的序號,由A::pF間接獲得,因此A a; a.ABC();將等同於( a.*( a.pF[0] ) )();。因此結構A的長度是8字節,再看下面的代碼:
    struct B : public A { long b; void ABC(); }; struct C : public A { long c; virtual void ABC(); };
    struct BB : public B { long bb; void ABC(); }; struct CC : public C { long cc; void ABC(); };
    void main() { BB bb; bb.ABC(); CC cc; cc.cc = 10; }
    首先,上面執行bb.ABC()但沒有給出BB::ABC或B::ABC的定義,因此上面雖然編譯通過,但連接時將失敗。其次,上面沒有執行cc.ABC();但連接時卻會說CC::ABC未定義以表示這裏需要CC::ABC的地址,爲什麼?因爲生成了CC的實例,而CC::pF就需要在編譯器自動爲CC生成的缺省構造函數中被正確初始化,其需要CC::ABC的地址來填充。接着,給出如下的各函數定義。
    void B::ABC() { b = 13; } void C::ABC() { c = 13; }
    void BB::ABC() { bb = 13; b = 10; } void CC::ABC() { cc = 13; c = 10; }
    如上後,對於bb.ABC();,等同於bb.BB::ABC();,雖然有三個BB::ABC的映射元素,但只有一個映射元素的類型爲void( BB:: )(),其映射BB::ABC的地址。由於BB::ABC並沒有用virtual修飾,因此上面將等同於bb.BB::ABC();而不是( bb.*( pF[0] ) )();,bb將爲13。對於cc.ABC();也是同樣的,cc將爲13。
    對於( ( B* )&bb )->ABC();,因爲左側類型爲B*,因此將爲( ( B* )&bb )->B::ABC();,由於B::ABC並沒被定義成虛函數,因此這裏等同於( ( B* )&bb )->B::ABC();,b將爲13。對於( ( C* )&cc )->ABC();,同樣將爲( ( C* )&cc )->C::ABC();,但C::ABC被修飾成虛函數,則前面等同於C *pC = &cc; ( pC->*( pC->pF[0] ) )();。這裏先將cc轉換成C的實例,偏移0。然後根據pC->pF[0]來間接獲得函數的地址,爲CC::ABC,c將爲10。因爲cc是CC的實例,在其被構造時將填充cc.pF,那麼如下:
    void ( CC::*CCVF[] )() = { CC::ABC, CC::BCD }; CC::CC() { cc.pF = &CCVF; }
    因此導致pC->ABC();結果調用的竟是CC::ABC而不是C::ABC,這正是由於虛的緣故而間接獲得函數地址導致的。同樣道理,對於( ( A* )&cc )->ABC();和( ( A* )&bb )->ABC();都將分別調用CC::ABC和BB::ABC。但請注意,( pC->*( pC->pF[0] ) )();中,pC是C*類型的,而pC->pF[0]返回的CC::ABC是void( CC:: )()類型的,而上面那樣做將如何進行實例的隱式類型轉換?如果不進行將導致操作錯誤的成員。可以像前面所說,讓CCVF的每個成員的長度爲8個字節,另外4個字節記錄需要進行的偏移。但大多數類其實並不需要偏移(如上面的CC實例轉成A實例就偏移0),此法有些浪費資源。VC對此給出的方法如下,假設CC::ABC對應的地址爲6000,並假設下面標號P處的地址就爲6000,而CC::A_thunk對應的地址爲5990。
    void CC::A_thunk( void *this )
    {
        this = ( ( char* )this ) + diff;
    P:
        // CC::ABC的正常代碼
    }
    因此pC->pF[0]的值爲5990,而並不是CC::ABC對應的6000。上面的diff就是相應的偏移,對於上面的例子,diff應該爲0,所以實際中pC->pF[0]的值還是6000(因爲偏移爲0,沒必要是5990)。此法被稱作thunk,表示完成簡單功能的短小代碼。對於多重繼承,如下:
    struct D : public A { long d; };
    struct E : public B, public C, public D { long e; void ABC() { e = 10; } };
    上面將有三個虛函數表,因爲B、C和D都各自帶了一個虛函數表(因爲從A派生)。結果上面等同於:
    struct E
    {
        void ( E::*B_pF )(); long B_a, b;
        void ( E::*C_pF )(); long C_a, c;
        void ( E::*D_pF )(); long D_a, d; long e; void ABC() { e = 10; } E();
        void E_C_thunk_ABC() { this = ( E* )( ( ( char* )this ) – 12 ); ABC(); }
        void E_D_thunk_ABC() { this = ( E* )( ( ( char* )this ) – 24 ); ABC(); }
    };
    void ( E::*E_BVF[] )() = { E::ABC, E::BCD };
    void ( E::*E_CVF[] )() = { E::E_C_thunk_ABC, E::BCD };
    void ( E::*E_DVF[] )() = { E::E_D_thunk_ABC, E::BCD };
    E::E() { B_pF = E_BVF; C_pF = E_CVF; D_pF = E_DVF; }
    結果E e; C *pC = &e; pC->ABC(); D *pD = &e; pD->ABC();,假設e的地址爲3000,則pC的值爲3012,pD的值爲3024。結果pC->pF的值就是E_CVF,pD->pF的值就是E_DVF,如此就解決了偏移問題。同樣,對於前面的虛繼承,當類裏有多個虛類表時,如:
    struct A {};
    struct B : virtual public A{}; struct C : virtual public A{}; struct D : virtual public A{};
    struct E : public B, public C, public D {};
    這是E將有三個虛類表,並且每個虛類表都將在E的缺省構造函數中被正確初始化以保證虛繼承的含義——間接獲得。而上面的虛函數表的初始化之所以那麼複雜也都只是爲了保證間接獲得的正確性。
    應注意上面將E_BVF的類型定義爲void( E::*[] )()只是由於演示,希望在代碼上儘量符合語法而那樣寫,並不表示虛函數的類型只能是void( E:: )()。實際中的虛函數表只不過是一個數組,每個元素的大小都爲4字節以記錄一個地址而已。因此也可如下:
    struct A { virtual void ABC(); virtual float ABC( double ); };
    struct B : public A { void ABC(); float ABC( double ); };
    則B b; A *pA = &b; pA->ABC();將調用類型爲void( B:: )()的B::ABC,而pA->ABC( 34 );將調用類型爲float( B:: )( double )的B::ABC。它們屬於重載函數,即使名字相同也都是兩個不同的虛函數。還應注意virtual和之前的public等,都只是從語法上提供給編譯器一些信息,它們給出的信息都是針對某些特殊情況的,而不是所有在使用數字的地方都適用,因此不能作爲數字的類型。所以virtual不是類型修飾符,它修飾一個成員函數只是告訴編譯器在運用那個成員函數的地方都應該間接獲得其地址。
    爲什麼要提供虛這個概念?即虛函數和虛繼承的意義是什麼?出於篇幅限制,將在本文的下篇給出它們意義的討論,即時說明多態性和實例複製等問題。

C++從零開始(十一)下篇

——類的相關知識

    由於篇幅限制,本篇爲《C++從零開始(十一)》的下篇,討論多態性及一些剩下的問題。


虛的含義

    本文的中篇已經介紹了虛的意思,就是要間接獲得,並且舉例說明電視機的頻道就是讓人間接獲得電視臺頻率的,因此其從這個意義上說是虛的,因爲它可能操作失敗——某個頻道還未調好而導致一片雪花。並且說明了間接的好處,就是隻用編好一段代碼(按5頻道),則每次執行它時可能有不同結果(今天5頻道被設置成中央5臺,明天可以被定成中央2臺),進而使得前面編的程序(按5頻道)顯得很靈活。注意虛之所以能夠很靈活是因爲它一定通過“一種手段”來間接達到目的,如每個頻道記錄着一個頻率。但這是不夠的,一定還有“另一段代碼”能改變那種手段的結果(頻道記錄的頻率),如調臺。
    先看虛繼承。它間接從子類的實例中獲得父類實例的所在位置,通過虛類表實現(這是“一種手段”),接着就必須能夠有“另一段代碼”來改變虛類表的值以表現其靈活性。首先可以自己來編寫這段代碼,但就要求清楚編譯器將虛類表放在什麼地方,而不同的編譯器有不同的實現方法,則這樣編寫的代碼兼容性很差。C++當然給出了“另一段代碼”,就是當某個類在同一個類繼承體系中被多次虛繼承時,就改變虛類表的值以使各子類間接獲得的父類實例是同一個。此操作的功能很差,僅僅只是節約內存而已。如:
    struct A { long a; };
    struct B : virtual public A { long b; }; struct C : virtual public A { long c; };
    struct D : public B, public C { long d; };
    這裏的D中有兩個虛類表,分別從B和C繼承而來,在D的構造函數中,編譯器會編寫必要的代碼以正確初始化D的兩個虛類表以使得通過B繼承的虛類表和通過C繼承的虛類表而獲得的A的實例是同一個。
    再看虛函數。它的地址被間接獲得,通過虛函數表實現(這是“一種手段”),接着就必須還能改變虛函數表的內容。同上,如果自己改寫,代碼的兼容性很差,而C++也給出了“另一段代碼”,和上面一樣,通過在派生類的構造函數中填寫虛函數表,根據當前派生類的情況來書寫虛函數表。它一定將某虛函數表填充爲當前派生類下,類型、名字和原來被定義爲虛函數的那個函數儘量匹配的函數的地址。如:
    struct A { virtual void ABC(), BCD( float ), ABC( float ); };
    struct B : public A { virtual void ABC(); };
    struct C : public B { void ABC( float ), BCD( float ); virtual float CCC( double ); };
    struct D : public C { void ABC(), ABC( float ), BCD( float ); };
    在A::A中,將兩個A::ABC和一個A::BCD的地址填寫到A的虛函數表中。
    在B::B中,將B::ABC和繼承來的B::BCD和B::ABC填充到B的虛函數表中。
    在C::C中,將C::ABC、C::BCD和繼承來的C::ABC填充到C的虛函數表中,並添加一個元素:C::CCC。
    在D::D中,將兩個D::ABC和一個D::BCD以及繼承來的D::CCC填充到D的虛函數表中。
    這裏的D是依次繼承自A、B、C,並沒有因爲多重繼承而產生兩個虛函數表,其只有一個虛函數表。雖然D中的成員函數沒有用virtual修飾,但它們的地址依舊被填到D的虛函數表中,因爲virtual只是表示使用那個成員函數時需要間接獲得其地址,與是否填寫到虛函數表中沒有關係。
    電視機爲什麼要用頻道來間接獲得電視臺的頻率?因爲電視臺的頻率人不容易記,並且如果知道一個頻率,慢慢地調整共諧電容的電容值以使電路達到那個頻率效率很低下。而做10組共諧電路,每組電路的電容值調好後就不再動,通過切換不同的共諧電路來實現快速轉換頻率。因此間接還可以提高效率。還有,5頻道本來是中央5臺,後來看膩了把它換成中央2臺,則同樣的動作(按5頻道)將產生不同的結果,“按5頻道”這個程序編得很靈活。
    由上面,至少可以知道:間接用於簡化操作、提高效率和增加靈活性。這裏提到的間接的三個用處都基於這麼一個想法——用“一種手段”來達到目的,用“另一段代碼”來實現上面提的用處。而C++提供的虛繼承和虛函數,只要使用虛繼承來的成員或虛函數就完成了“一種手段”。而要實現“另一段代碼”,從上面的說明中可以看出,需要通過派生的手段來達到。在派生類中定義和父類中聲明的虛函數原型相同的函數就可以改變虛函數表,而派生類的繼承體系中只有重複出現了被虛繼承的類才能改變虛類表,而且也只是都指向同一個被虛繼承的類的實例,遠沒有虛函數表的修改方便和靈活,因此虛繼承並不常用,而虛函數則被經常的使用。


虛的使用

    由於C++中實現“虛”的方式需要藉助派生的手段,而派生是生成類型,因此“虛”一般映射爲類型上的間接,而不是上面頻道那種通過實例(一組共諧電路)來實現的間接。注意“簡化操作”實際就是指用函數映射覆雜的操作進而簡化代碼的編寫,利用函數名映射的地址來間接執行相應的代碼,對於虛函數就是一種調用形式表現多種執行結果。而“提高效率”是一種算法上的改進,即頻道是通過重複十組共諧電路來實現的,正宗的空間換時間,不是類型上的間接可以實現的。因此C++中的“虛”就只能增加代碼的靈活性和簡化操作(對於上面提出的三個間接的好處)。
    比如動物會叫,不同的動物叫的方式不同,發出的聲音也不同,這就是在類型上需要通過“一種手段”(叫)來表現不同的效果(貓和狗的叫法不同),而這需要“另一段代碼”來實現,也就是通過派生來實現。即從類Animal派生類Cat和類Dog,通過將“叫(Gnar)”聲明爲Animal中的虛函數,然後在Cat和Dog中各自再實現相應的Gnar成員函數。如上就實現了用Animal::Gnar的調用表現不同的效果,如下:
    Cat cat1, cat2; Dog dog; Animal *pA[] = { &cat1, &dog, &cat2 };
    for( unsigned long i = 0; i < sizeof( pA ); i++ ) pA[ i ]->Gnar();
    上面的容器pA記錄了一系列的Animal的實例的引用(關於引用,可參考《C++從零開始(八)》),其語義就是這是3個動物,至於是什麼不用管也不知道(就好象這臺電視機有10個頻道,至於每個是什麼臺則不知道),然後要求這3個動物每個都叫一次(調用Animal::Gnar),結果依次發出貓叫、狗叫和貓叫聲。這就是之前說的增加靈活性,也被稱作多態性,指同樣的Animal::Gnar調用,卻表現出不同的形態。上面的for循環不用再寫了,它就是“一種手段”,而欲改變它的表現效果,就再使用“另一段代碼”,也就是再派生不同的派生類,並把派生類的實例的引用放到數組pA中即可。
    因此一個類的成員函數被聲明爲虛函數,表示這個類所映射的那種資源的相應功能應該是一個使用方法,而不是一個實現方式。如上面的“叫”,表示要動物“叫”不用給出參數,也沒有返回值,直接調用即可。因此再考慮之前的收音機和數字式收音機,其中有個功能爲調臺,則相應的函數應該聲明爲虛函數,以表示要調臺,就給出頻率增量或減量,而數字式的調臺和普通的調臺的實現方式很明顯的不同,但不管。意思就是說使用收音機的人不關心調臺是如何實現的,只關心怎樣調臺。因此,虛函數表示函數的定義不重要,重要的是函數的聲明,虛函數只有在派生類中實現有意義,父類給出虛函數的定義顯得多餘。因此C++給出了一種特殊語法以允許不給出虛函數的定義,格式很簡單,在虛函數的聲明語句的後面加上“= 0”即可,被稱作純虛函數。如下:
    class Food; class Animal { public: virtual void Gnar() = 0, Eat( Food& ) = 0; };
    class Cat : public Animal { public: void Gnar(), Eat( Food& ); };
    class Dog : public Animal { void Gnar(), Eat( Food& ); };
    void Cat::Gnar(){} void Cat::Eat( Food& ){} void Dog::Gnar(){} void Dog::Eat( Food& ){}
    void main() { Cat cat; Dog dog; Animal ani; }
    上面在聲明Animal::Gnar時在語句後面書寫“= 0”以表示它所映射的元素沒有定義。這和不書寫“= 0”有什麼區別?直接只聲明Animal::Gnar也可以不給出定義啊。注意上面的Animal ani;將報錯,因爲在Animal::Animal中需要填充Animal的虛函數表,而它需要Animal::Gnar的地址。如果是普通的聲明,則這裏將不會報錯,因爲編譯器會認爲Animal::Gnar的定義在其他的文件中,後面的連接器會處理。但這裏由於使用了“= 0”,以告知編譯器它沒有定義,因此上面代碼編譯時就會失敗,編譯器已經認定沒有Animal::Gnar的定義。
    但如果在上面加上Animal::Gnar的定義會怎樣?Animal ani;依舊報錯,因爲編譯器已經認定沒有Animal::Gnar的定義,連函數表都不會查看就否定Animal實例的生成,因此給出Animal::Gnar的定義也沒用。但映射元素Animal::Gnar現在的地址欄填寫了數字,因此當cat.Animal::Gnar();時沒有任何問題。如果不給出Animal::Gnar的定義,則cat.Animal::Gnar();依舊沒有問題,但連接時將報錯。
    注意上面的Dog::Gnar是private的,而Animal::Gnar是public的,結果dog.Gnar();將報錯,而dog.Animal::Gnar();卻沒有錯誤(由於它是虛函數結果還是調用Dog::Gnar),也就是前面所謂的public等與類型無關,只是一種語法罷了。還有class Food;,不用管它是聲明還是定義,只用看它提供了什麼信息,只有一個——有個類型名的名字爲Food,是類型的自定義類型。而聲明Animal::Eat時,編譯器也只用知道Food是一個類型名而不是程序員不小心打錯字了就行了,因爲這裏並沒有運用Food。
    上面的Animal被稱作純虛基類。基類就是類繼承體系中最上層的那個類;虛基類就是基類帶有純虛成員函數;純虛基類就是沒有成員變量和非純虛成員函數,只有純虛成員函數的基類。上面的Animal就定義了一種規則,也稱作一種協議或一個接口。即動物能夠Gnar,而且也能夠Eat,且Eat時必須給出一個Food的實例,表示動物能夠喫食物。即Animal這個類型成了一張說明書,說明動物具有的功能,它的實例變得沒有意義,而它由於使用純虛函數也正好不能生成實例。
    如果上面的Gner和Eat不是純虛函數呢?那麼它們都必須有定義,進而動物就不再是一個抽象概念,而可以有實例,則就可以有這麼一種動物,它是動物,但它又不是任何一種特定的動物(既不是貓也不是狗)。很明顯,這樣的語義和純虛基類表現出來的差很遠。
    那麼虛繼承呢?被虛繼承的類的成員將被間接操作,這就是它的“一種手段”,也就是說操作這個被虛繼承的類的成員,可能由於得到的偏移值不同而操作不同的內存。但對虛類表的修改又只限於如果重複出現,則修改成間接操作同一實例,因此從根本上虛繼承就是爲了解決上篇所說的鯨魚有兩個飢餓度的問題,本身的意義就只是一種算法的實現。這導致在設計海洋生物和脯乳動物時,無法確定是否要虛繼承父類動物,而要看派生的類中是否會出現類似鯨魚那樣的情況,如果有,則倒過來再將海洋生物和脯乳動物設計成虛繼承自動物,這不是好現象。


static(靜態)

    在《C++從零開始(五)》中說過,靜態就是每次運行都沒有變化,而動態就是每次運行都有可能變化。C++給出了static關鍵字,和上面的public、virtual一樣,只是個語法標識而已,不是類型修飾符。它可作用於成員前面以表示這個成員對於每個實例來說都是不變的,如下:
    struct A { static long a; long b; static void ABC(); }; long A::a;
    void A::ABC() { a = 10; b = 0; }; void main() { A a; a.a = 10; a.b = 32; }
    上面的A::a就是結構A的靜態成員變量,A::ABC就是A的靜態成員函數。有什麼變化?上面的映射元素A::a的類型將不再是long A::而是long。同樣A::ABC的類型也變成void()而不是void( A:: )()。
    首先,成員要對它的類的實例來說都是靜態的,即成員變量對於每個實例所標識的內存的地址都相同,成員函數對於每個this參數進行修改的內存的地址都是不變的。上面把A::a和A::ABC變成普通類型,而非偏移類型,就消除了它們對A的實例的依賴,進而實現上面說的靜態。
    由於上面對實例依賴的消除,即成員函數去掉this參數,成員變量映射的是一確切的內存地址而不再是偏移,所以struct A { static long a; };只是對變量A::a進行了聲明,其名字爲A::a,類型爲long,映射的地址並沒有給出,即還未定義,所以必須在全局空間中(即不在任何一個函數體內)再定義一遍,進而有long A::a;。同樣A::ABC的類型爲void(),被去除了this參數,進而在A::ABC中的b = 10;等同於A::b = 10;,發現A::b是偏移類型,需要this參數,則等同於this->A::b = 10;。結果A::ABC沒有this參數,錯誤。而對於a = 10;,等同於A::a = 10;,而已經有這個變量,故沒任何問題。
    注意上面的a.a = 10;等同於a.A::a = 10;,而A::a不是偏移類型,那這裏不是應該報錯嗎?對此C++特別允許這種類型不匹配的現象,其中的“a.”等於沒有,因爲這正是前面我們要表現的靜態成員。即A a, b; a.a = 10; b.a = 20;執行後,a.a爲20,因爲不管哪個實例,對成員A::a的操作都修改的同一個地址所標識的內存。
    什麼意義?它們和普通的變量的區別就是名字被A::限定,進而能表現出它們的是專用於類A的。比如房子,房子的門的高度和寬度都定好了,有兩個房子都是某個公司造的,它們的門的高度和寬度相同,因此門的高度和寬度就應該作爲那個公司造的房子的靜態成員以記錄實際的高度和寬度,但它們並不需要因實例的不同而變化。
    除了成員,C++還提供了靜態局部變量。局部變量就是在函數體內的變量,被一對“{}”括起來,被限制了作用域的變量。對於函數,每次調用函數,由於函數體內的局部變量都是分配在棧上,按照之前說的,這些變量其實是一些相對值,則每次調用函數,可能由於棧的原因而導致實際對應的地址不同。如下:
    void ABC() { long a = 0; a++; } void BCD() { long d = 0; ABC(); }
    void main() { ABC(); BCD(); }
    上面main中調用ABC而產生的局部變量a所對應的地址和由於調用BCD,而在BCD中調用ABC而產生的a所對應的地址就不一樣,原理在《C++從零開始(十五)》中說明。因此靜態局部變量就表示那個變量的地址不管是通過什麼途徑調用它所在的函數,都不變化。如下:
    void ABC() { static long a = 0; a++; } void BCD() { long d = 0; d++; ABC(); }
    void main() { ABC(); BCD(); }
    上面的變量a的地址是固定值,而不再是原來那種相對值了。這樣從main中調用ABC和從BCD中調用ABC得到的變量a的地址是相同的。上面等同於下面:
    long g_ABC_a = 0; void ABC() { g_ABC_a++; } void BCD() { long d = 0; d++; ABC(); }
    void main() { ABC(); BCD(); }
    因此上面ABC中的靜態局部變量a的初始化實際在執行main之前就已經做了,而不是想象的在第一次調用ABC時才初始化,進而上面代碼執行完後,ABC中的a的值爲2,因爲ABC的兩次調用。
    它的意義?表示這個變量只在這個函數中才被使用,而它的生命期又需要超過函數的執行期。它並不能提供什麼語義(因爲能提供的“在這個函數才被使用”使用局部變量就可以做到),只是當某些算法需要使用全局變量,而此時這個算法又被映射成了一個函數,則使用靜態變量具有很好的命名效果——既需要全局變量的生存期又應該有局部變量的語義。


inline(嵌入)

    函數調用的效率較低,調用前需要將參數按照調用規則存放起來,然後傳遞存放參數的內存,還要記錄調用時的地址以保證函數執行完後能回到調用處(關於細節在《C++從零開始(十五)》中討論),但它能降低代碼的長度,尤其是函數體比較大而代碼中調用它的地方又比較多,可以大幅度減小代碼的長度(就好像循環10次,如果不寫循環語句,則需要將循環體內的代碼複製10遍)。但也可能倒過來,調用次數少而函數體較小,這時之所以還映射成函數是爲了語義更明確。此時可能更注重的是執行效率而不是代碼長度,爲此C++提供了inline關鍵字。
    在函數定義時,在定義語句的前面書寫inline即可,表示當調用這個函數時,在調用處不像原來那樣書寫存放、傳遞參數的代碼,而將此函數的函數體在調用處展開,就好像前面說的將循環體裏的代碼複製10遍一樣。這樣將不用做傳遞參數等工作,代碼的執行效率將提高,但最終生成的代碼的長度可能由於過多的展開而變長。如下:
    void ABCD(); void main() { ABCD(); } inline void ABCD() { long a = 0; a++; }
    上面的ABCD就是inline函數。注意ABCD的聲明並沒有書寫inline,因爲inline並不是類型修飾符,它只是告訴編譯器在生成這個函數時,要多記錄一些信息,然後由連接器根據這些信息在連接前視情況展開它。注意是“視情況”,即編譯器可能足夠智能以至於在連接時發現對相應函數的調用太多而不適合展開進而不展開。對此,不同的編譯器給出了不同的處理方式,對於VC,其就提供了一個關鍵字__forceinline以表示相應函數必須展開,不用去管它被調用的情況。
    前面說過,對於在類型定義符中書寫的函數定義,編譯器將把它們看成inline函數。變成了inline函數後,就不用再由於多箇中間文件都給出了函數的定義而不知應該選用哪個定義所產生的地址,因爲所有調用這些函數的地方都不再需要函數的地址,函數將直接在那裏展開。


const(常量)

    前面提到某公司造的房子的門的高度和寬度應該爲靜態成員變量,但很明顯,在房子的實例存在的整個期間,門的高度和寬度都不會變化。C++對此專門提出了一種類型修飾符——const。它所修飾的類型表示那個類型所修飾的地址類型的數字不能被用於寫操作,即地址類型的數字如果是const類型將只能被讀,不能被修改。如:const long a = 10, b = 20; a++; a = 4;(注意不能cosnt long a;,因爲後續代碼都不能修改a,而a的值又不能被改變,則a就沒有意義了)。這裏a++;和a = 4;都將報錯,因爲a的類型爲cosnt long,表示a的地址所對應的內存的值不能被改變,而a++;和a = 4;都欲改變這個值。
    由於const long是一個類型,因此也就很正常地有const long*,表示類型爲const long的指針,因此按照類型匹配,有:const long *p = &b; p = &a; *p = 10;。這裏p = &a;按照類型匹配很正常,而p是常量的long類型的指針,沒有任何問題。但是*p = 10;將報錯,因爲*p將p的數字直接轉換成地址類型,也就成了常量的long類型的地址類型,因此對它進行寫入操作錯誤。
    注意有:const long* const p = &a; p = &a; *p = 10;,按照從左到右修飾的順序,上面的p的類型爲const long* const,是常量的long類型的指針的常量,表示p的地址所對應的內存的值不能被修改,因此後邊的p = &a;將錯誤,違反const的意義。同樣*p = 10;也錯誤。不過可以:
    long a = 3, *const p = &a; p = &a; *p = 10;
    上面的p的類型爲long* const,爲long類型的常量,因此其必須被初始化。後續的p = &a;將報錯,因爲p是long* const,但*p = 10;卻沒有任何問題,因爲將long*轉成long後沒有任何問題。所以也有:
    const long a = 0; const long* const p = &a; const long* const *pp = &p;
    只要按照從左到右的修飾順序,而所有的const修飾均由於取內容操作符“*”的轉換而變成相應類型中指針類型修飾符“*”左邊的類型,因此*pp的類型是const long* const,*p的類型是const long。
    應注意C++還允許如下使用:
    struct A { long a, b; void ABC() const; };
    void A::ABC() const { a = 10; b = 10; }
    上面的A::ABC的類型爲void( A:: )() const,其等同於:
    void A_ABC( const A *this ) { this->a = 10; this->b = 10; }
    因此上面的a = 10;和b = 10;將報錯,因爲this的類型是const A*。上面的意思就是函數A::ABC中不能修改成員變量的值,因爲各this的參數變成了const A*,但可以修改類的靜態成員變量的值,如:
    struct A { static long c; long a, b; void ABC() const; } long A::c;
    void A::ABC() const { a = b = 10; c = 20; }
    等同於:void A_ABC( const A *this ) { this->a = this->b = 10; A::c = 20; }。故依舊可以修改A::c的值。
    有什麼意義?出於篇幅,有關const的語義還請參考我寫的另一篇文章《語義的需要》。


friend(友員)

    發信機具有發送電波的功能,收信機具有接收電波的功能,而發信機、收信機和電波這三個類,首先發信機由於將信息傳遞給電波而必定可以修改電波的一些成員變量,但電波的這些成員應該是protected,否則隨便一個石頭都能接收或修改電波所攜帶的信息。同樣,收信機要接收電波就需要能訪問電波的一些用protected修飾的成員,這樣就麻煩了。如果在電波中定義兩個公共成員函數,讓發信機和收信機可以通過它們來訪問被protected的成員,不就行了?這也正是許多人犯的毛病,既然發信機可以通過那個公共成員函數修改電波的成員,那石頭就不能用那個成員函數修改電波嗎?這等於是原來沒有門,後來有個門卻不上鎖。爲了消除這個問題,C++提出了友員的概念。
    在定義某個自定義類型時,在類型定義符“{}”中聲明一個自定義類型或一個函數,在聲明或定義語句的前面加上關鍵字friend即可,如:
    class Receiver; class Sender;
    class Wave { private: long b, c; friend class Receiver; friend class Sender; };
    上面就聲明瞭Wave的兩個友員類,以表示Receiver和Sender具備了Wave的資格,即如下:
    class A { private: long a; }; class Wave : public A { … };
    void Receiver::ABC() { Wave wav; wav.a = 10; wav.b = 10; wav.A::a = 10; }
    上面由於Receiver是Wave的友員類,所以在Receiver::ABC中可以直接訪問Wave::a、Wave::b,但wav.A::a = 10;就將報錯,因爲A::a是A的私有成員,Wave不具備反問它的權限,而Receiver的權限等同於Wave,故權限不夠。
    同樣,也可有友員函數,即給出函數的聲明或定義,在語句前加上friend,如下:
    class Receiver { public: void ABC(); };
    class A { private: long a; friend void Receiver::ABC(); };
    這樣,就將Receiver::ABC作爲了A的友員函數,則在Receiver::ABC中,具有類A具有的所有權限。
    應注意按照給出信息的思想,上面還可以如下:
    class A { private: long a; friend void Receiver::ABC() { long a = 0; } };
    這裏就定義了函數Receiver::ABC,由於是在類型定義符中定義的,前面已經說過,Receiver::ABC將被修飾爲inline函數。
    那麼友員函數的意義呢?一個操作需要同時操作兩個資源中被保護了的成員,則這個操作應該被映射爲友員函數。如蓋章需要用到文件和章兩個資源,則蓋章映射成的函數應該爲文件和章的友員函數。


名字空間

    前面說明了靜態成員變量,它的語義是專用於某個類而又獨立於類的實例,它與全局變量的關鍵不同就是名字多了個限定符(即“::”,表示從屬關係),如A::a是A的靜態成員變量,則A::a這個名字就可以表現出a從屬於A。因此爲了表現這種從屬關係,就需要將變量定義爲靜態成員變量。
    考慮一種情況,映射採礦。但是在陸地上採礦和在海底採礦很明顯地不同,那麼應該怎麼辦?映射兩個函數,名字分別爲MiningOnLand和MiningOnSeabed。好,然後又需要映射在陸地勘探和在海底勘探,怎麼辦?映射爲ProspectOnLand和ProspectOnSeabed。如果又需要映射在陸地鑽井和在海底鑽井,在陸地爆破和在海底爆破,怎麼辦?很明顯,這裏通過名字來表現語義已經顯得牽強了,而使用靜態成員函數則顯得更加不合理,爲此C++提供了名字空間,格式爲namespace <名字> { <各聲明或定義語句> }。其中的<名字>爲定義的名字空間的名字,而<各聲明或定義語句>就是多條聲明或定義語句。如下:
    namespace OnLand { void Mining(); void Prospect(); void ArtesianWell(){} }
    namespace OnSeabed { void Mining(); void Prospect(); void ArtesianWell(){} }
    void OnLand::Mining() { long a = 0; a++; } void OnLand::Prospect() { long a = 0; a++; }
    void OnSeabed::Mining() { long a = 0; a++; } void OnSeabed::Prospect() { long a = 0; a++; }
    上面就定義了6個元素,每個的類型都爲void()。注意上面OnLand::ArtesianWell和OnSeabed::ArtesianWell的定義直接寫在“{}”中,將是inline函數。這樣定義的六個變量它們的名字就帶有限定符,能夠從名字上體現從屬關係,語義表現得比原來更好,OnSeabed::Prospect就表示在海底勘探。注意也可以如下:
    namespace A { long b = 0; long a = 0; namespace B { long B = 0; float a = 0.0f } }
    namespace C { struct ABC { long a, b, c, d; void ABCD() { a = b = c = d = 12; } } ab; }
    namespace D { void ABC(); void ABC() { long a = 0; a++; } extern float bd; }
    即名字空間裏面可以放任何聲明或定義語句,也可以用於修飾自定義結構,因此就可以C::ABC a; a.ABCD();。應注意C++還允許給名字空間別名,比如:namespace AB = C; AB::ABC a; a.ABCD();。這裏就給名字空間C另起了個名字AB,就好像之前提過的typedef一樣。
    還應注意自定義類型的定義的效果和名字空間很像,如struct A { long a; };將生成A::a,和名字空間一樣爲映射元素的名字加上了限定符,但應該瞭解到結構A並不是名字空間,即namespace ABC = A;將失敗。名字空間就好像所有成員都是靜態成員的自定義結構。
    爲了方便名字空間的使用,C++提供了using關鍵字,其後面接namespace和名字空間的名字,將把相應名字空間中的所有映射元素複製一份,但是去掉了名字前的所有限定符,並且這些元素的有效區域就在using所在的位置,如:
    void main() { { using namespace C; ABC a; a.ABCD(); } ABC b; b.ABCD(); }
    上面的ABC b;將失敗,因爲using namespace C;的有效區域只在前面的“{}”內,出了就無效了,因此應該C::ABC b; b.ABCD();。有什麼用?方便書寫。因爲每次調用OnLand::Prospect時都要寫OnLand::,顯得有點煩瑣,如果知道在某個區域內並不會用到OnSeabed的成員,則可以using namespace OnLand;以減小代碼的繁雜度。
    注意C++還提供了using更好的使用方式,即只希望去掉名字空間中的某一個映射元素的限定符而不用全部去掉,比如只去掉OnLand::Prospect而其它的保持,則可以:using OnLand::Prospect; Prospect(); Mining();。這裏的Mining();將失敗,而Prospect();將成功,因爲using OnLand::Prospect;只去掉了OnLand::Prospect的限定符。
    至此基本上已經說明了C++的大部分內容,只是還剩下模板和異常沒有說明(還有自定義類型的操作符重載,出於篇幅,在《C++從零開始(十七)》中說明),它們帶的語義都很少,很大程度上就和switch語句一樣,只是一種算法的包裝而已。下篇介紹面向對象編程思想,並給出“世界”的概念以從語義出發來說明如何設計類及類的繼承體系。

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