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::ABC變成普通型別,而非偏移型別,就消除了它們對A的例項的依賴,進而實現上面說的靜態。

相關問題答案