本文的中篇已經介紹了虛的意思,就是要間接獲得,並且舉例說明電視機的頻道就是讓人間接獲得電視台頻率的,因此其從這個意義上說是虛的,因為它可能操作失敗--某個頻道還未調好而導致一片雪花。並且說明了間接的好處,就是只用編好一段代碼(按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的執行個體的依賴,進而實現上面說的靜態。
由於上面對執行個體依賴的消除,即成員函數去掉this參數,成員變數映射的是一確切的內為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語句一樣,只是一種演算法的封裝而已。下篇介紹物件導向編程思想,並給出“世界”的概念以從語義出發來說明如何設計類及類的繼承體系