程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 更多關於編程 >> C++學習從零開始(六)

C++學習從零開始(六)

編輯:更多關於編程

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

    1. 上一頁:
    2. 下一頁:
    Copyright © 程式師世界 All Rights Reserved