C++從零開始(十一)上篇——類的相關知識

來源:互聯網
上載者:User

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++從零開始(十二)》中會專門提出一個概念以給出一種方案來指導如何設計類及各類的關係。由於篇幅限制,本文分成了上中下三篇,剩下的內容在本文的後兩篇說明。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.