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這個類型成了一張說明書,說明動物具有的功能,它的實例變得沒有意義,而它由