可信賴(lài)的武漢網(wǎng)站建設(shè)信息如何優(yōu)化上百度首頁(yè)公司
文章目錄
- 用于大型程序的工具
- 18.1異常處理
- 18.1.1拋出異常
- 棧展開(kāi)
- 棧展開(kāi)過(guò)程中對(duì)象被自動(dòng)銷(xiāo)毀
- 析構(gòu)函數(shù)與異常
- 異常對(duì)象
- 18.1.2捕獲異常
- 查找匹配的處理代碼
- 重新拋出
- 捕獲所有異常的處理代碼
- 18.1.3函數(shù)try語(yǔ)句塊與構(gòu)造函數(shù)
- 18.1.4noexcept異常說(shuō)明
- 違反異常說(shuō)明
- 異常說(shuō)明的實(shí)參
- noexcept運(yùn)算符
- 異常說(shuō)明與指針、虛函數(shù)和拷貝控制
- 18.1.5異常類(lèi)層次
- 18.2命名空間
- 18.2.1命名空間定義
- 每個(gè)命名空間都是一個(gè)作用域
- 命名空間可以是不連續(xù)的
- 定義命名空間成員
- 模板特例化
- 內(nèi)聯(lián)命名空間
- 未命名的命名空間
- 18.2.2使用命名空間成員
- 命名空間的別名
- using聲明:扼要概述
- using指示
- using指示與作用域
- 頭文件與using聲明或指示
- 18.2.3類(lèi)、命名空間與作用域
- 實(shí)參相關(guān)的查找與類(lèi)類(lèi)型形參
- 查找與std;:move和std::forward
- 友元聲明與實(shí)參相關(guān)的查找
- 18.2.4重載與命名空間
- 與實(shí)參相關(guān)的查找與重載
- 重載與using聲明
- 重載與using指示
- 跨越多個(gè)using指示的重載
- 18.3多重繼承與虛繼承
- 18.3.1多重繼承
- 多重繼承的派生類(lèi)從每個(gè)基類(lèi)中繼承狀態(tài)
- 派生類(lèi)構(gòu)造函數(shù)初始化所有基類(lèi)
- 繼承的構(gòu)造函數(shù)與多重繼承
- 多重繼承的派生類(lèi)的拷貝與移動(dòng)操作
- 18.3.2類(lèi)型轉(zhuǎn)換與多個(gè)基類(lèi)
- 基于指針類(lèi)型或引用類(lèi)型的查找
- 18.3.3多重繼承下的類(lèi)作用域
- 18.3.4虛繼承
- 使用虛基類(lèi)
- 支持向基類(lèi)的常規(guī)類(lèi)型轉(zhuǎn)換
- 虛基類(lèi)成員的可見(jiàn)性
- 18.3.5構(gòu)造函數(shù)與虛繼承
- 虛繼承的對(duì)象的構(gòu)造方式
- 構(gòu)造函數(shù)與析構(gòu)函數(shù)的次序
用于大型程序的工具
18.1異常處理
異常處理機(jī)制允許程序中獨(dú)立開(kāi)發(fā)的部分能夠在運(yùn)行時(shí)就出現(xiàn)的問(wèn)題進(jìn)行通信并做出相應(yīng)的處理。
18.1.1拋出異常
當(dāng)執(zhí)行一個(gè)
throw
時(shí),跟在throw
后面的語(yǔ)句將不再被執(zhí)行。相反,程序的控制權(quán)從throw
轉(zhuǎn)移到與之匹配的catch
模塊。該catch
可能是同一個(gè)函數(shù)中的局部catch
,也可能位于直接或間接調(diào)用了發(fā)生異常的函數(shù)的另一個(gè)函數(shù)中。此時(shí):
- 沿著調(diào)用鏈的函數(shù)可能會(huì)提早退出。
- 一旦程序開(kāi)始執(zhí)行異常處理代碼,則沿著調(diào)用鏈創(chuàng)建的對(duì)象將被銷(xiāo)毀。
throw
的用法有點(diǎn)類(lèi)似于return
語(yǔ)句:它通常作為條件語(yǔ)句的一部分或者作為某個(gè)函數(shù)的最后(或者唯一)一條語(yǔ)句。
棧展開(kāi)
棧展開(kāi)過(guò)程沿著嵌套函數(shù)的調(diào)用鏈不斷查找,直到找到了與異常匹配的
catch
子句為止;或者也可能一直沒(méi)找到匹配的catch
,則退出主函數(shù)后查找過(guò)程終止(terminate
)。
棧展開(kāi)過(guò)程中對(duì)象被自動(dòng)銷(xiāo)毀
如果在棧展開(kāi)過(guò)程中退出了某個(gè)塊,編譯器將負(fù)責(zé)確保在這個(gè)塊中創(chuàng)建的對(duì)象能被正確地銷(xiāo)毀。如果某個(gè)局部對(duì)象的類(lèi)型是類(lèi)類(lèi)型,則該對(duì)象的析構(gòu)函數(shù)將被自動(dòng)調(diào)用。編譯器在銷(xiāo)毀內(nèi)置類(lèi)型的對(duì)象時(shí)不需要做任何事情。
如果異常發(fā)生在構(gòu)造函數(shù)中,即使某個(gè)對(duì)象只構(gòu)造了一部分,也要確保已構(gòu)造的成員能被正確地銷(xiāo)毀。
類(lèi)似的,異常也可能發(fā)生在數(shù)組或標(biāo)準(zhǔn)庫(kù)容器的元素初始化過(guò)程中,此時(shí)應(yīng)該確保已經(jīng)構(gòu)造的這部分元素被正確地銷(xiāo)毀。
析構(gòu)函數(shù)與異常
在棧展開(kāi)的過(guò)程中,運(yùn)行類(lèi)類(lèi)型的局部對(duì)象的析構(gòu)函數(shù)。因?yàn)檫@些析構(gòu)函數(shù)是自動(dòng)執(zhí)行的,所以它們不應(yīng)該拋出異常。一旦在棧展開(kāi)的過(guò)程中析構(gòu)函數(shù)拋出了異常,并且析構(gòu)函數(shù)自身沒(méi)能捕獲到該異常,則程序?qū)⒈唤K止(
terminate
)。因此,如果析構(gòu)函數(shù)需要執(zhí)行某個(gè)可能拋出異常的操作,則該操作應(yīng)該被放置在一個(gè)try
語(yǔ)句塊中,并且在析構(gòu)函數(shù)內(nèi)部得到處理。
在實(shí)際的編程過(guò)程中,因?yàn)槲鰳?gòu)函數(shù)僅僅是釋放資源,所以它不太可能拋出異常。所有標(biāo)準(zhǔn)庫(kù)類(lèi)型都能確保它們的析構(gòu)函數(shù)不會(huì)引發(fā)異常。
異常對(duì)象
編譯器使用異常拋出表達(dá)式來(lái)對(duì)異常對(duì)象進(jìn)行拷貝初始化。如果該表達(dá)式是類(lèi)類(lèi)型的話,則相應(yīng)的類(lèi)必須含有一個(gè)可訪問(wèn)的析構(gòu)函數(shù)和一個(gè)可訪問(wèn)的拷貝或移動(dòng)構(gòu)造函數(shù)。如果該表達(dá)式是數(shù)組類(lèi)型或函數(shù)類(lèi)型,則表達(dá)式將被轉(zhuǎn)換成與之對(duì)應(yīng)的指針類(lèi)型。
當(dāng)拋出一條表達(dá)式時(shí),該表達(dá)式的靜態(tài)編譯時(shí)類(lèi)型決定了異常對(duì)象的類(lèi)型。如果一條throw
表達(dá)式解引用一個(gè)基類(lèi)指針,而該指針實(shí)際指向的是派生類(lèi)對(duì)象,則拋出的對(duì)象將被切掉一部分,只有基類(lèi)部分被拋出。
18.1.2捕獲異常
如果
catch
無(wú)須訪問(wèn)拋出的表達(dá)式的話,則可以忽略捕獲形參的名字。
聲明的類(lèi)型必須是完全類(lèi)型,它可以是左值引用,但不能是右值引用。如果catch
的參數(shù)類(lèi)型是非引用類(lèi)型,則該參數(shù)是異常對(duì)象的一個(gè)副本;相反,如果參數(shù)是引用類(lèi)型,則該參數(shù)是異常對(duì)象的一個(gè)別名。
如果catch
的參數(shù)是基類(lèi)類(lèi)型,則可以使用其派生類(lèi)類(lèi)型的異常對(duì)象對(duì)其進(jìn)行初始化。此時(shí),如果catch
的參數(shù)是非引用類(lèi)型,則異常對(duì)象將被切掉一部分。另一方面,如果catch
的參數(shù)是基類(lèi)的引用,則該參數(shù)將以常規(guī)方式綁定到異常對(duì)象上。
需要注意的是,異常聲明的靜態(tài)類(lèi)型將決定catch
語(yǔ)句所能執(zhí)行的操作。如果catch
的參數(shù)是基類(lèi)類(lèi)型,則catch
無(wú)法使用派生類(lèi)特有的任何成員。
通常情況下,如果catch
接受的異常與某個(gè)繼承體系有關(guān),則最好將該catch
的參數(shù)定義成引用類(lèi)型。
查找匹配的處理代碼
越是專(zhuān)門(mén)的
catch
越應(yīng)該置于整個(gè)catch
列表的前端,所以當(dāng)程序使用具有繼承關(guān)系的多個(gè)異常時(shí),派生類(lèi)異常的處理代碼應(yīng)該出現(xiàn)在基類(lèi)異常的處理代碼之前。
與實(shí)參和形參的匹配規(guī)則相比,異常和catch
異常聲明的匹配規(guī)則受到更多限制:
- 允許從非常量向常量的類(lèi)型轉(zhuǎn)換,一條非常量對(duì)象的
throw
語(yǔ)句可以匹配一個(gè)接受常量引用的catch
語(yǔ)句。- 允許從派生類(lèi)向基類(lèi)的類(lèi)型轉(zhuǎn)換。
- 數(shù)組被轉(zhuǎn)換成指向數(shù)組(元素)類(lèi)型的指針,函數(shù)被轉(zhuǎn)換成指向該函數(shù)類(lèi)型的指針。
重新拋出
有時(shí),一個(gè)單獨(dú)的
catch
不能完整地處理某個(gè)異常。在執(zhí)行了某些校正操作之后,當(dāng)前的catch
可能會(huì)決定由調(diào)用鏈更上一層的函數(shù)接著處理異常:
// 只能出現(xiàn)在catch語(yǔ)句或catch語(yǔ)句直接或間接調(diào)用的函數(shù)之內(nèi),如果在處理代碼
// 之外的區(qū)域遇到了空throw語(yǔ)句,編譯器將調(diào)用terminate。
throw;
很多時(shí)候,
catch
語(yǔ)句會(huì)改變其參數(shù)的內(nèi)容。如果在改變了參數(shù)的內(nèi)容后catch
語(yǔ)句重新拋出異常,則只有當(dāng)catch
異常聲明是引用類(lèi)型時(shí)對(duì)參數(shù)所做的改變才會(huì)被保留并繼續(xù)傳播:
catch (my_error &eObj) { // 引用類(lèi)型eObj.status = errCodes::severeErr; // 修改了異常對(duì)象throw; // 異常對(duì)象的status成員是severeErr
} catch (other_error eObj) { // 非引用類(lèi)型eObj.status = errCodes::badErr; // 只修改了異常對(duì)象的局部副本throw; // 異常對(duì)象的status成員沒(méi)有改變
}
捕獲所有異常的處理代碼
有時(shí)希望不論拋出的異常是什么類(lèi)型,程序都能統(tǒng)一捕獲它們。
void manip() {try {// 這里的操作將引發(fā)并拋出一個(gè)異常} catch(...) {// 處理異常的某些特殊操作throw;}
}
catch(...)
通常與重新拋出語(yǔ)句一起使用,其中catch
執(zhí)行當(dāng)前局部能完成的工作,隨后重新拋出異常。catch(...)
既能單獨(dú)出現(xiàn),也能與其他幾個(gè)catch
語(yǔ)句一起出現(xiàn),此時(shí)catch(...)
必須在最后的位置。
18.1.3函數(shù)try語(yǔ)句塊與構(gòu)造函數(shù)
構(gòu)造函數(shù)在進(jìn)入其函數(shù)體之前首先執(zhí)行初始值列表,因此,此時(shí)構(gòu)造函數(shù)體內(nèi)的
catch
語(yǔ)句無(wú)法處理相關(guān)的異常。所以,必須將構(gòu)造函數(shù)寫(xiě)成函數(shù)try
語(yǔ)句塊的形式:
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) try : data(std::make_shared<std::vector<T>>(il)) {/* 空函數(shù)體 */
} catch(const std::bad_alloc &e) {handle_out_of_memory(e);
}
值得注意的是,在初始化構(gòu)造函數(shù)的參數(shù)時(shí)也可能發(fā)生異常,這樣的異常不屬于函數(shù)
try
語(yǔ)句塊的一部分。該異常屬于調(diào)用表達(dá)式的一部分,并將在調(diào)用者所在的上下文中處理。
18.1.4noexcept異常說(shuō)明
在c++11新標(biāo)準(zhǔn)中,可以通過(guò)提供
noexcept
說(shuō)明指定某個(gè)函數(shù)不會(huì)拋出異常:
void recoup(int) noexcept; // 不會(huì)拋出異常
void alloc(int); // 可能拋出異常
noexcept
說(shuō)明要么出現(xiàn)在函數(shù)的所有聲明語(yǔ)句和定義語(yǔ)句中,要么一次也不出現(xiàn)。- 該說(shuō)明應(yīng)該在函數(shù)的尾置返回類(lèi)型之前。
- 可以在函數(shù)指針的聲明和定義中指定
noexcept
。- 在
typedef
或類(lèi)型別名中則不能出現(xiàn)noexcept
。- 在成員函數(shù)中,
noexcept
說(shuō)明符需要跟在const
及引用限定符之后,而在final
、override
或虛函數(shù)的=0
之前。
違反異常說(shuō)明
編譯器并不會(huì)在編譯時(shí)檢查
noexcept
說(shuō)明。實(shí)際上,如果一個(gè)函數(shù)在說(shuō)明了noexcept
的同時(shí)又含有throw
語(yǔ)句或者調(diào)用了可能拋出異常的其他函數(shù),編譯器將順利通過(guò),并不會(huì)因?yàn)檫@種違反異常說(shuō)明的情況而報(bào)錯(cuò):
void f() noexcept {throw exception(); // 違反了異常說(shuō)明
}
此時(shí),程序會(huì)調(diào)用
terminate
以確保遵守不在運(yùn)行時(shí)拋出異常的承諾。因此,noexcept
可以用在兩種情況下:一是確認(rèn)函數(shù)不會(huì)拋出異常,二是根本不知道該如何處理異常。
指明某個(gè)函數(shù)不會(huì)拋出異??梢粤钤摵瘮?shù)的調(diào)用者不必再考慮如何處理異常。無(wú)論是函數(shù)確實(shí)不拋出異常,還是程序被終止,調(diào)用者都無(wú)須為此負(fù)責(zé)。
異常說(shuō)明的實(shí)參
noexcept
說(shuō)明符接受一個(gè)可選的實(shí)參,該實(shí)參必須能轉(zhuǎn)換為bool
類(lèi)型:如果實(shí)參是true
,則函數(shù)不會(huì)拋出異常;如果實(shí)參是false
,則函數(shù)可能拋出異常:
void recoup(int) noexcept(true); // recoup不會(huì)拋出異常
void alloc(int) noexcept(false); // alloc可能拋出異常
noexcept運(yùn)算符
noexcept
說(shuō)明符的實(shí)參常常與noexcept
運(yùn)算符混合使用。
noexcept
運(yùn)算符是一個(gè)一元運(yùn)算符,返回值是一個(gè)bool
類(lèi)型的右值常量表達(dá)式,用于表示給定的表達(dá)式是否會(huì)拋出異常,不會(huì)求其運(yùn)算對(duì)象的值:
noexcept(recoup(i)) // 如果recoup不拋出異常則結(jié)果為true;否則結(jié)果為false。// 更普通的形式是:
noexcept(e)
// 當(dāng)e調(diào)用的所有函數(shù)都做了不拋出說(shuō)明且e本身不含有throw語(yǔ)句時(shí),上述表達(dá)式為true;
// 否則noexcept(e)返回false。// 可以使用noexcept運(yùn)算符得到如下的異常說(shuō)明:
void f() noexcept(noexcept(g())); // f和g的異常說(shuō)明一致
異常說(shuō)明與指針、虛函數(shù)和拷貝控制
函數(shù)指針及該指針?biāo)傅暮瘮?shù)必須具有一致的異常說(shuō)明。即,如果為某個(gè)指針做了不拋出異常的聲明,則該指針只能指向不拋出異常的函數(shù)。相反,如果顯式或隱式地說(shuō)明了指針可能拋出異常,則該指針可以指向任何函數(shù),即使是承諾了不拋出異常的函數(shù)也可以:
// recoup和pf1都承諾不會(huì)拋出異常
void (*pf1)(int) noexcept = recoup;
// 正確:recoup不會(huì)拋出異常,pf2可能拋出異常,二者之間互不干擾。
void (*pf2)(int) = recoup;pf1 = alloc; // 錯(cuò)誤:alloc可能拋出異常,但是pf1已經(jīng)說(shuō)明了它不會(huì)拋出異常。
pf2 = alloc; // 正確:pf2和alloc都可能拋出異常。
如果一個(gè)虛函數(shù)承諾了它不會(huì)拋出異常,則后續(xù)派生出來(lái)的虛函數(shù)也必須做出同樣的承諾;與之相反,如果基類(lèi)的虛函數(shù)允許拋出異常,則派生類(lèi)的對(duì)應(yīng)函數(shù)既可以允許拋出異常,也可以不允許拋出異常:
class Base {
public:virtual double f1(double) noexcept; // 不會(huì)拋出異常virtual int f2() noexcept(false); // 可能拋出異常virtual void f3(); // 可能拋出異常
};class Derived : public Base {
public:double f1(double); // 錯(cuò)誤:Base::f1承諾不會(huì)拋出異常。int f2() noexcept(false); // 正確:與Base::f2的異常說(shuō)明一致。void f3() noexcept; // 正確:Derived的f3做了更嚴(yán)格的限定,這是允許的。
};
當(dāng)編譯器合成拷貝控制成員時(shí),同時(shí)也生成一個(gè)異常說(shuō)明:
- 如果對(duì)所有成員和基類(lèi)的所有操作都承諾了不會(huì)拋出異常,則合成的成員是
noexcept
的。- 如果合成成員調(diào)用的任意一個(gè)函數(shù)可能拋出異常,則合成的成員是
noexcept(false)
。- 如果定義了一個(gè)析構(gòu)函數(shù)但是沒(méi)有為它提供異常說(shuō)明,則編譯器將合成一個(gè)。合成的異常說(shuō)明將與假設(shè)由編譯器為類(lèi)合成析構(gòu)函數(shù)時(shí)所得的異常說(shuō)明一致。
18.1.5異常類(lèi)層次
在這些類(lèi)中,
what
負(fù)責(zé)返回用于初始化異常對(duì)象的信息。因?yàn)?code>what是虛函數(shù),所以當(dāng)捕獲基類(lèi)的引用時(shí),對(duì)what
函數(shù)的調(diào)用將執(zhí)行與異常對(duì)象動(dòng)態(tài)類(lèi)型對(duì)應(yīng)的版本。
// 為某個(gè)書(shū)店應(yīng)用程序設(shè)定的異常類(lèi)
class out_of_stock : public std::runtime_error {
public:explicit out_of_stock(const std::string &s) : std::runtime_error(s) {}
};class isbn_mismatch : public std::logic_error {
public:explicit isbn_mismatch(const std::string &s) : std::logic_error(s) {}isbn_mismatch(const std::string &s, const std::string &lhs, const std::string &rhs) : std::logic_error(s), left(lhs), right(rhs) {}const std::string left, right;
};// 如果參與加法的兩個(gè)對(duì)象并非同一本書(shū)籍,則拋出一個(gè)異常。
Sales_data &Sales_data::operator+=(const Sales_data &rhs) {if (isbn() != rhs.isbn()) {throw isbn_mismatch("wrong isbns", isbn(), rhs.isbn());}units_sold += rhs.units_sold;revenue += rhs.revenue;return *this;
}// 使用之前設(shè)定的書(shū)店程序異常類(lèi)
Sales_data item1, item2, sum;
while (cin >> item1 >> item2) { // 讀取兩條交易信息try {sum = item1 + item2; // 計(jì)算它們的和// 此處使用sum} catch(const isbn_mismatch &e) {cerr << e.what() << ": left isbn(" << e.left<< ") right isbn(" << e.right << ")" << endl;}
}
18.2命名空間
18.2.1命名空間定義
namespace cplusplus_primer {// ...
} // 無(wú)須分號(hào),與塊類(lèi)似。
命名空間的名字必須在定義它的作用域內(nèi)保持唯一。
每個(gè)命名空間都是一個(gè)作用域
定義在某個(gè)命名空間中的名字可以被該命名空間內(nèi)的其他成員直接訪問(wèn),也可以被這些成員內(nèi)嵌作用域中的任何單位訪問(wèn)。位于該命名空間之外的代碼則必須明確指出所用的名字屬于哪個(gè)命名空間:
cplusplus_primer::Query q = cplusplus_primer::Query("hello");
命名空間可以是不連續(xù)的
// 可能是定義了一個(gè)名為nsp的新命名空間,也可能是
// 為已經(jīng)存在的命名空間添加一些新成員。
namespace nsp {// ...
}
這種特性使得可以將幾個(gè)獨(dú)立的接口和實(shí)現(xiàn)文件組成一個(gè)命名空間。此時(shí),命名空間的組織方式類(lèi)似于管理自定義類(lèi)及函數(shù)的方法:
- 命名空間的一部分成員的作用是定義類(lèi),以及聲明作為類(lèi)接口的函數(shù)及對(duì)象,則這些成員應(yīng)該置于頭文件中,這些頭文件將被包含在使用了這些成員的文件中。
- 命名空間成員的定義部分則置于另外的源文件中。
在程序中某些實(shí)體只能定義一次:如非內(nèi)聯(lián)函數(shù)、靜態(tài)數(shù)據(jù)成員、變量等,命名空間中定義的名字也需要滿足這一要求,可以通過(guò)上面的方式組織命名空間并達(dá)到目的。這種接口和實(shí)現(xiàn)分離的機(jī)制確保所需的函數(shù)和其他名字只定義一次,而只要是用到這些實(shí)體的地方都能看到對(duì)于實(shí)體名字的聲明。
// Sales_data.h
// #include應(yīng)該出現(xiàn)在打開(kāi)命名空間的操作之前,否則隱含的意思是
// 把頭文件中所有的名字定義成該命名空間的成員。
#include <string>
namespace cplusplus_primer {class Sales_data { /* ... */ };Sales_data operator+(const Sales_data &, const Sales_data &);// Sales_data的其他接口函數(shù)的聲明
}
// Sales_data.cpp
// 確保#include出現(xiàn)在打開(kāi)命名空間的操作之前
#include "Sales_data.h"
namespace cplusplus_primer {// Sales_data成員及重載運(yùn)算符的定義,可以直接使用名字,此時(shí)無(wú)須前綴。
}
// user.cpp
#include "Sales_data.h"
int main() {using cplusplus_primer::Sales_data;Sales_data trans1, trans2;// ...return 0;
}
這種程序的組織方式提供了開(kāi)發(fā)者和庫(kù)用戶所需的模塊性。每個(gè)類(lèi)仍組織在自己的接口和實(shí)現(xiàn)文件中,一個(gè)類(lèi)的用戶不必編譯與其他類(lèi)相關(guān)的名字。庫(kù)的開(kāi)發(fā)者可以分別實(shí)現(xiàn)每一個(gè)類(lèi),相互之間沒(méi)有干擾。
定義命名空間成員
可以在命名空間定義的外部定義該命名空間的成員,但是這樣的定義必須出現(xiàn)在所屬命名空間的外層空間中,而不能在一個(gè)不相關(guān)的作用域中。
// 命名空間之外定義的成員必須使用含有前綴的名字,一旦看到含有完整前綴的名字,
// 就可以確定該名字位于命名空間的作用域內(nèi),因此可以直接使用該命名空間的其他成員。
cplusplus_primer::Sales_data
cplusplus_primer::operator+(const Sales_data &lhs, const Sales_data &rhs) {// ...
}
模板特例化
模板特例化必須定義在原始模板所屬的命名空間中:
// 必須將模板特例化聲明成std的成員
namespace std {template<> struct hash<Sales_data>;
}// 在std中添加了模板特例化的聲明后,就可以在命名空間std的外部定義它了。
template<> struct std::hash<Sales_data> {size_t operator()(const Sales_data &) const {return hash<string>()(s.bookNo) ^hash<unsigned>()(s.units_sold) ^hash<double>()(s.revenue);}// 其他成員保持一致
}
內(nèi)聯(lián)命名空間
C++11新標(biāo)準(zhǔn)引入,內(nèi)聯(lián)命名空間中的名字可以被外層命名空間直接使用。當(dāng)應(yīng)用程序的代碼在一次發(fā)布和另一次發(fā)布之間發(fā)生了改變時(shí),常常會(huì)用到內(nèi)聯(lián)命名空間。
// 必須出現(xiàn)在第一次定義的地方,后續(xù)可寫(xiě)可不寫(xiě)。
inline namespace FifthEd {// 該命名空間表示第5版的代碼
}namespace FifthEd { // 隱式內(nèi)聯(lián)class Query_base { /* ... */ };// 其他與Query有關(guān)的聲明
}namespace FourthEd {class Item_base { /* ... */ };class Query_base { /* ... */ };// 第4版用到的其他代碼
}// 因?yàn)镕ifthEd是內(nèi)聯(lián)的,所以形如cplusplus_primer::的代碼
// 可以直接獲得FifthEd的成員。如果想使用早起版本的代碼,則
// 必須像其他嵌套的命名空間一樣加上完整的外層命名空間名字。
namespace cplusplus_primer {#include "FifthEd.h"#include "FourthEd.h"
}
未命名的命名空間
未命名的命名空間是指關(guān)鍵字
namespace
后緊跟花括號(hào)括起來(lái)的一系列聲明語(yǔ)句。其中定義的變量擁有靜態(tài)生命周期:它們?cè)诘谝淮问褂们皠?chuàng)建,并且直到程序結(jié)束才銷(xiāo)毀。
一個(gè)未命名的命名空間可以在某個(gè)給定的文件內(nèi)不連續(xù),但是不能跨越多個(gè)文件。每個(gè)文件定義自己的未命名的命名空間,如果兩個(gè)文件都含有未命名的命名空間,則這兩個(gè)空間互相無(wú)關(guān)。因此,如果一個(gè)頭文件定義了未命名的命名空間,則該命名空間中定義的名字將在每個(gè)包含了該頭文件的文件中對(duì)應(yīng)不同實(shí)體。
定義在未命名的命名空間中的名字可以直接使用,畢竟找不到什么名字來(lái)限定它們;同樣的,也不能對(duì)未命名的命名空間的成員使用作用域運(yùn)算符。
未命名的命名空間中定義的名字的作用域與該命名空間所在的作用域相同。如果未命名的命名空間定義在文件的最外層作用域中,則該命名空間中的名字一定要與全局作用域中的名字有所區(qū)別:
int i; // i的全局聲明
namespace {int i;
}
// 二義性:i的定義既出現(xiàn)在全局作用域中,又出現(xiàn)在未嵌套的未命名的命名空間中。
i = 10;namespace local {namespace {int i;}
}
// 正確:定義在嵌套的未命名的命名空間中的i與全局作用域中的i不同。
local::i = 42;
未命名的命名空間取代文件中的靜態(tài)聲明:
在標(biāo)準(zhǔn)c++引入命名空間的概念之前,程序需要將名字聲明成static
的以使得其對(duì)于整個(gè)文件有效。這樣的做法是從c語(yǔ)言繼承而來(lái)的。在c語(yǔ)言中,聲明為static
的全局實(shí)體在其所在的文件外不可見(jiàn)。
在文件中進(jìn)行靜態(tài)聲明的做法已經(jīng)被c++標(biāo)準(zhǔn)取消了,現(xiàn)在的做法是使用未命名的命名空間。
18.2.2使用命名空間成員
命名空間的別名
// 不能在命名空間還沒(méi)有定義前就聲明別名。
// 別名也可以指向一個(gè)嵌套的命名空間。
namespace primer = cplusplus_primer;
using聲明:扼要概述
一條
using
聲明語(yǔ)句一次只引入命名空間的一個(gè)成員,可以清楚地知道程序中所用的到底是哪個(gè)名字。
using
聲明的有效范圍從聲明的地方開(kāi)始,一直到其所在的作用域結(jié)束為止。在此過(guò)程中,外層作用域的同名實(shí)體將被隱藏。
using指示
using
指示無(wú)法控制哪些名字是可見(jiàn)的,因?yàn)樗忻侄际强梢?jiàn)的。簡(jiǎn)寫(xiě)的名字從using
指示開(kāi)始,一直到其所在的作用域結(jié)束都能使用。
using指示與作用域
using
聲明的名字的作用域與語(yǔ)句本身的作用域一致,從效果上看就好像為命名空間的成員在當(dāng)前作用域內(nèi)創(chuàng)建了一個(gè)別名一樣。
而using
指示具有將命名空間成員提升到包含命名空間本身和using
指示的最近作用域的能力。通常情況下,命名空間中會(huì)含有一些不能出現(xiàn)在局部作用域中的定義,因此,using
指示一般被看作是出現(xiàn)在最近的外層作用域中。
// 命名空間A和函數(shù)f定義在全局作用域中
namespace A [int i, j;
}void f() {using namespace A; // 把A中的名字注入到全局作用域中cout << i * j << endl; // 使用命名空間A中的i和j// ...
}
namespace blip {int i = 16, j = 15, k = 23;// 其他聲明
}int j = 0; // 正確:blip的j隱藏在命名空間中。void manip() {// using指示,blip中的名字被添加到全局作用域中。using namespace blip; // 如果使用了j,則將在::j和blip::j之間產(chǎn)生沖突。++i; // 將blip::i設(shè)定為17++j; // 二義性錯(cuò)誤:是全局的j還是blip::j?++::j; // 正確:將全局的j設(shè)定為1。++blip::j; // 正確:將blip::j設(shè)定為16。int k = 97; // 當(dāng)前局部的k隱藏了blip::k++k; // 將當(dāng)前局部的k設(shè)定為98
}
頭文件與using聲明或指示
頭文件如果在其頂層作用域中含有
using
指示或聲明,則會(huì)將名字注入到所有包含了該頭文件的文件中。
通常情況下,頭文件應(yīng)該只負(fù)責(zé)定義接口部分的名字,而不定義實(shí)現(xiàn)部分的名字。因此,頭文件最多只能在它的函數(shù)或命名空間內(nèi)使用using
指示或using
聲明。
避免
using
指示:
using
指示引發(fā)的二義性錯(cuò)誤只有在使用了沖突名字的地方才能被發(fā)現(xiàn);而**using
聲明引起的二義性問(wèn)題在聲明處就能發(fā)現(xiàn)**。
using
指示也并非一無(wú)是處,例如在命名空間本身的實(shí)現(xiàn)文件中就可以使用using
指示。
18.2.3類(lèi)、命名空間與作用域
對(duì)命名空間內(nèi)部名字的查找遵循常規(guī)的查找規(guī)則:即由內(nèi)向外依次查找每個(gè)外層作用域。只有位于開(kāi)放的塊中且在使用點(diǎn)之前聲明的名字才被考慮:
namespace A {int i;namespace B {int i; // 在B中隱藏了A::iint j;int f1() {int j; // j是f1的局部變量,隱藏了A::B::jreturn i; // 返回B::i}} // 命名空間B結(jié)束,此后B中定義的名字不再可見(jiàn)。int f2() {return j; // 錯(cuò)誤:j沒(méi)有被定義。}int j = i; // 用A::i進(jìn)行初始化
}
對(duì)于命名空間中的類(lèi)來(lái)說(shuō),常規(guī)的查找規(guī)則仍然適用:當(dāng)成員函數(shù)使用某個(gè)名字時(shí),首先在該成員中查找,然后在類(lèi)中查找(包括基類(lèi)),接著在外層作用域中查找。可以從函數(shù)的限定名推斷出查找名字時(shí)檢查作用域的次序,限定名以相反次序指出被查找的作用域。
namespace A {int i;int k;class C1 {public:C1() : i(0), j(0) {} // 正確:初始化C1::i和C1::j。int f1() { return k; } // 返回A::kint f2() { return h; } // 錯(cuò)誤:h未定義。int f3();private:int i; // 在C1中隱藏了A::iint j;};int h = i; // 用A::i進(jìn)行初始化
}
// 成員f3定義在C1和命名空間A的外部
int A::C1::f3() { return h; } // 正確:返回A::h。
實(shí)參相關(guān)的查找與類(lèi)類(lèi)型形參
std::string s;
// 不用std::限定符和using聲明就可以調(diào)用operator>>
std::cin >> s;
// 等價(jià)于:
operator>>(std::cin, s);
// 首先在當(dāng)前作用域中尋找合適的函數(shù),接著查找輸出語(yǔ)句的外層作用域。
// 隨后,因?yàn)?gt;>表達(dá)式的形參是類(lèi)類(lèi)型的,所以編譯器還會(huì)查找cin和s的類(lèi)
// 所屬的命名空間,即std。
當(dāng)給函數(shù)傳遞一個(gè)類(lèi)類(lèi)型的對(duì)象時(shí),除了在常規(guī)的作用域查找外還會(huì)查找實(shí)參類(lèi)所屬的命名空間,對(duì)于傳遞類(lèi)的引用或指針的調(diào)用同樣有效。
因此,允許概念上作為類(lèi)接口一部分的非成員函數(shù)無(wú)須單獨(dú)的using
聲明就能被程序使用。
查找與std;:move和std::forward
通常情況下,如果在應(yīng)用程序中定義了一個(gè)標(biāo)準(zhǔn)庫(kù)中已有的名字,則:要么根據(jù)一般的重載規(guī)則確定某次調(diào)用應(yīng)該執(zhí)行函數(shù)的哪個(gè)版本;要么應(yīng)用程序根本就不會(huì)執(zhí)行函數(shù)的標(biāo)準(zhǔn)庫(kù)版本。
由于標(biāo)準(zhǔn)庫(kù)中move
和forward
都是模板函數(shù),且都接受一個(gè)右值引用的函數(shù)形參(可以匹配任何類(lèi)型),因此,如果應(yīng)用程序也定義了一個(gè)接受單一形參的move
/forward
函數(shù),則不管該形參是什么類(lèi)型,都將與標(biāo)準(zhǔn)庫(kù)的版本沖突。
因此,move
/forward
的名字沖突要比其他標(biāo)準(zhǔn)庫(kù)函數(shù)的沖突頻繁得多,所以建議最好使用帶限定語(yǔ)的完整版本。
友元聲明與實(shí)參相關(guān)的查找
當(dāng)類(lèi)聲明了一個(gè)友元時(shí),該友元聲明并沒(méi)有使得友元本身可見(jiàn)。然而,一個(gè)另外的未聲明的類(lèi)或函數(shù)如果第一次出現(xiàn)在友元聲明中,則認(rèn)為它是最近的外層命名空間的成員:
namespace A {class C {// 兩個(gè)友元,在友元聲明之外沒(méi)有其他的聲明,// 這些函數(shù)隱式地成為命名空間A的成員。friend void f2(); // 除非另有聲明,否則不會(huì)被找到。friend void f(const C &); // 根據(jù)實(shí)參相關(guān)的查找規(guī)則可以被找到};
}int main() {A::C cobj;f(cobj); // 正確:通過(guò)在A::C中的友元聲明找到A::f。f2(); // 錯(cuò)誤:A::f2沒(méi)有被聲明。
}
18.2.4重載與命名空間
與實(shí)參相關(guān)的查找與重載
對(duì)于接受類(lèi)類(lèi)型實(shí)參的函數(shù)來(lái)說(shuō),將在每個(gè)實(shí)參類(lèi)(以及實(shí)參類(lèi)的基類(lèi))所屬的命名空間中搜尋候選函數(shù)。在這些命名空間中所有與被調(diào)函數(shù)同名的函數(shù)都將被添加到候選集當(dāng)中,即使其中某些函數(shù)在調(diào)用語(yǔ)句處不可見(jiàn)也是如此:
namespace NS {class Quote { /* ... */ };void display(const Quote &) { /* ... */ }
}
// Bulk_item的基類(lèi)聲明在命名空間NS中
class Bulk_item : public NS::Quote { /* ... */ };
int main() {Bulk_item book1;display(book1);return 0;
}
重載與using聲明
using
聲明語(yǔ)句聲明的是一個(gè)名字,而非一個(gè)特定的函數(shù):
using NS::print(int); // 錯(cuò)誤:不能指定形參列表。
using NS::print; // 正確:using聲明只聲明一個(gè)名字。
當(dāng)為函數(shù)書(shū)寫(xiě)
using
聲明時(shí),該函數(shù)的所有版本都被引入到當(dāng)前作用域中。
- 一個(gè)
using
聲明引入的函數(shù)將重載該聲明語(yǔ)句所屬作用域中已有的其他同名函數(shù)。- 如果
using
聲明出現(xiàn)在局部作用域中,則引入的名字將隱藏外層作用域的相關(guān)聲明。- 如果
using
聲明所在的作用域中已經(jīng)有一個(gè)函數(shù)與新引入的函數(shù)同名且形參列表相同,則該using
聲明將引發(fā)錯(cuò)誤。- 除此之外,
using
聲明將為引入的名字添加額外的重載實(shí)例,并最終擴(kuò)充候選函數(shù)集的規(guī)模。
重載與using指示
using
指示將命名空間的成員提升到外層作用域中,如果命名空間的某個(gè)函數(shù)與該命名空間所屬作用域的函數(shù)同名,則命名空間的函數(shù)將被添加到重載集合中:
namespace libs_R_us {extern void print(int);extern void print(double);
}
// 普通的聲明
void print(const std::string &);
// 這個(gè)using指示把名字添加到print調(diào)用的候選函數(shù)集
using namespace libs_R_us;
// print調(diào)用此時(shí)的候選函數(shù)集包括:
// libs_R_us的print(int)
// libs_R_us的print(double)
// 顯式聲明的print(const std::string &)
void fooBar(int ival) {print("Value: "); // 調(diào)用全局函數(shù)print(const std::string &)print(ival); // 調(diào)用libs_R_us::print(int)
}
對(duì)于
using
指示來(lái)說(shuō),引入一個(gè)與已有函數(shù)形參列表完全相同的函數(shù)并不會(huì)產(chǎn)生錯(cuò)誤。此時(shí),只要指明調(diào)用的是命名空間中的函數(shù)版本還是當(dāng)前作用域的版本即可。
跨越多個(gè)using指示的重載
如果存在多個(gè)
using
指示,則來(lái)自每個(gè)命名空間的名字都會(huì)成為候選函數(shù)集的一部分:
namespace AW {int print(int);
}
namespace Primer {double print(double);
}
// using指示從不同的命名空間中創(chuàng)建了一個(gè)重載函數(shù)集合
using namespace AW;
using namespace Primer;long double print(long double);int main() {print(1); // 調(diào)用AW::print(int)print(3.1); // 調(diào)用Primer::print(double)return 0;
}
18.3多重繼承與虛繼承
多重繼承是指從多個(gè)直接基類(lèi)產(chǎn)生派生類(lèi)的能力。多重繼承的派生類(lèi)繼承了所有父類(lèi)的屬性。
18.3.1多重繼承
class Bear : public ZooAnimal { /* ... */ };
class Panda : public Bear, public Endangered { /* ... */ };
需要注意的是,在某個(gè)給定的派生列表中,同一個(gè)基類(lèi)只能出現(xiàn)一次。
多重繼承的派生類(lèi)從每個(gè)基類(lèi)中繼承狀態(tài)
在多重繼承關(guān)系中,派生類(lèi)的對(duì)象包含有每個(gè)基類(lèi)的子對(duì)象。
派生類(lèi)構(gòu)造函數(shù)初始化所有基類(lèi)
構(gòu)造一個(gè)派生類(lèi)的對(duì)象將同時(shí)構(gòu)造并初始化它的所有基類(lèi)子對(duì)象。多重繼承的派生類(lèi)的構(gòu)造函數(shù)初始值也只能初始化它的直接基類(lèi):
// 顯式地初始化所有基類(lèi)
// 首先初始化ZooAnimal,其次是Bear,然后是Endangered,最后是Panda。
Panda::Panda(std::string name, bool onExhibit): Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) {}
// 隱式地使用Bear的默認(rèn)構(gòu)造函數(shù)初始化Bear子對(duì)象
Panda::Panda(): Endangered(Endangered::critical) {}
基類(lèi)的構(gòu)造順序與派生列表中基類(lèi)的出現(xiàn)順序保持一致,而與派生類(lèi)構(gòu)造函數(shù)初始值列表中基類(lèi)的順序無(wú)關(guān)。析構(gòu)函數(shù)的調(diào)用順序則與構(gòu)造函數(shù)相反。
繼承的構(gòu)造函數(shù)與多重繼承
在c++11新標(biāo)準(zhǔn)中,允許派生類(lèi)從它的一個(gè)或幾個(gè)基類(lèi)中繼承構(gòu)造函數(shù)。但是如果從多個(gè)基類(lèi)中繼承了相同的構(gòu)造函數(shù)(即形參列表完全相同),則程序?qū)a(chǎn)生錯(cuò)誤。此時(shí),必須定義自己的版本:
struct Base1 {Base1() = default;Base1(const std::string &);Base1(std::shared_ptr<int>);
};
struct Base2 {Base2() = default;Base2(const std::string &);Base2(int);
};
// D1試圖從兩個(gè)基類(lèi)中都繼承D1::D1(const string &),
// 如果不定義自己的版本將引發(fā)錯(cuò)誤。
struct D1 : public Base1, public Base2 {using Base1::Base1; // 從Base1繼承構(gòu)造函數(shù)using Base2::Base2; // 從Base2繼承構(gòu)造函數(shù)// D2必須自定義一個(gè)接受string的構(gòu)造函數(shù)D2(const string &s) : Base1(s), Base2(s) {}D2 = default; // 一旦D2定義了它自己的構(gòu)造函數(shù),則必須出現(xiàn)。
};
多重繼承的派生類(lèi)的拷貝與移動(dòng)操作
與單繼承一樣,多重繼承的派生類(lèi)如果定義了自己的拷貝/賦值構(gòu)造函數(shù)和賦值運(yùn)算符,則必須在完整的對(duì)象上執(zhí)行拷貝、移動(dòng)或賦值操作。
只有當(dāng)派生類(lèi)使用的是合成版本的拷貝、移動(dòng)或賦值成員時(shí),才會(huì)自動(dòng)對(duì)其基類(lèi)部分執(zhí)行這些操作。在合成的拷貝控制成員中,每個(gè)基類(lèi)分別使用自己的對(duì)應(yīng)成員隱式地完成構(gòu)造、賦值或銷(xiāo)毀等工作。
Panda ying_yang("ying_yang");
// 將調(diào)用Bear的拷貝構(gòu)造函數(shù),后者又在執(zhí)行自己的拷貝任務(wù)之前先調(diào)用
// ZooAnimal的拷貝構(gòu)造函數(shù)。一旦ling_ling的Bear部分構(gòu)造完成,接著
// 就會(huì)調(diào)用Endangered的拷貝構(gòu)造函數(shù)來(lái)創(chuàng)建對(duì)象相應(yīng)的部分。最后,執(zhí)行
// Panda的拷貝構(gòu)造函數(shù)。合成的移動(dòng)構(gòu)造函數(shù)/拷貝賦值運(yùn)算符類(lèi)似。
Panda ling_ling = ying_yang; // 使用拷貝構(gòu)造函數(shù)
18.3.2類(lèi)型轉(zhuǎn)換與多個(gè)基類(lèi)
可以令某個(gè)可訪問(wèn)基類(lèi)的指針或引用直接指向一個(gè)派生類(lèi)對(duì)象:
// 接受Panda的基類(lèi)引用的一系列操作
void print(const Bear &);
void highlight(const Endangered &);
ostream &operator<<(ostream &, const ZooAnimal &);
Panda ying_yang("ying_yang");
print(ying_yang); // 把一個(gè)Panda對(duì)象傳遞給一個(gè)Bear的引用
highlight(ying_yang); // 把一個(gè)Panda對(duì)象傳遞給一個(gè)Endangered的引用
cout << ying_yang << endl; // 把一個(gè)Panda對(duì)象傳遞給一個(gè)ZooAnimal的引用
編譯器不會(huì)在派生類(lèi)向基類(lèi)的幾種轉(zhuǎn)換中進(jìn)行比較和選擇,因?yàn)樵谒磥?lái)轉(zhuǎn)換到任意一種基類(lèi)都一樣好。
基于指針類(lèi)型或引用類(lèi)型的查找
對(duì)象、指針和引用的靜態(tài)類(lèi)型決定了能夠使用哪些成員。
Bear *pb = new Panda("ying_yang");
pb->print(); // 正確:Panda::print()。
pb->cuddle(); // 錯(cuò)誤:不屬于Bear的接口。
pb->highlight(); // 錯(cuò)誤:不屬于Bear的接口。
delete pb; // 正確:Panda::~Panda()。Endangered *pe = new Panda("ying_yang");
pe->print(); // 正確:Panda::print()。
pe->toes(); // 錯(cuò)誤:不屬于Endangered的接口。
pe->cuddle(); // 錯(cuò)誤:不屬于Endangered的接口。
pe->highlight(); // 正確:Panda::highlight()。
delete pe; // 正確:Panda::~Panda()。
18.3.3多重繼承下的類(lèi)作用域
在多重繼承的情況下,查找過(guò)程在所有直接基類(lèi)中同時(shí)進(jìn)行。如果名字在多個(gè)基類(lèi)中都被找到,則對(duì)該名字的使用將具有二義性。
對(duì)于一個(gè)派生類(lèi)來(lái)說(shuō),從它的幾個(gè)基類(lèi)中分別繼承名字相同的成員是完全合法的,只不過(guò)在使用這個(gè)名字時(shí)必須明確指出它的版本。
// 如果ZooAnimal和Endangered都定義了名為max_weight的成員,并且
// Panda沒(méi)有定義該成員,則下面的調(diào)用是錯(cuò)誤的。此時(shí),需要指出所調(diào)用
// 的版本:ZooAnimal::max_weight或者Endangered::max_weight。
double d = ying_yang.max_weight();
一種更復(fù)雜的情況是,有時(shí)即使派生類(lèi)繼承的兩個(gè)函數(shù)形參列表不同也可能引發(fā)錯(cuò)誤。此時(shí),即使函數(shù)在一個(gè)類(lèi)中是私有的,而在另一個(gè)類(lèi)中是公有的或受保護(hù)的同樣也可能發(fā)生錯(cuò)誤。
和往常一樣,先查找名字后進(jìn)行類(lèi)型檢查。當(dāng)編譯器在兩個(gè)作用域中同時(shí)發(fā)現(xiàn)了相同的成員時(shí),將直接報(bào)告一個(gè)調(diào)用二義性的錯(cuò)誤。
要想避免潛在的二義性,最好的辦法是在派生類(lèi)中為該函數(shù)定義一個(gè)新版本。
double Panda::max_weight() const {return std::max(ZooAnimal::max_weight(), Endangered::max_weight());
}
18.3.4虛繼承
盡管在派生列表中同一個(gè)基類(lèi)只能出現(xiàn)一次,但實(shí)際上派生類(lèi)可以多次繼承同一個(gè)類(lèi)。
在默認(rèn)情況下,派生類(lèi)中含有繼承鏈上每個(gè)類(lèi)對(duì)應(yīng)的子部分。如果某個(gè)類(lèi)在派生過(guò)程中出現(xiàn)了多次,則派生類(lèi)中將包含該類(lèi)的多個(gè)子對(duì)象。
因此,對(duì)于形如iostream
這樣的類(lèi)顯然是行不通的。一個(gè)iostream
對(duì)象肯定希望在同一個(gè)緩沖區(qū)中進(jìn)行讀寫(xiě)操作,也會(huì)要求條件狀態(tài)能同時(shí)反映輸入和輸出操作的情況。假如在iostream
對(duì)象中真的包含了base_ios
的兩份拷貝,則共享行為就無(wú)法實(shí)現(xiàn)了。
在c++中通過(guò)虛繼承的機(jī)制解決這樣的問(wèn)題。虛繼承的目的是令某個(gè)類(lèi)做出聲明,承諾愿意共享它的基類(lèi)。其中,共享的基類(lèi)子對(duì)象稱(chēng)為虛基類(lèi)。在這種機(jī)制下,不論虛基類(lèi)在繼承體系中出現(xiàn)了多少次,在派生類(lèi)中都只包含唯一一個(gè)共享的虛基類(lèi)子對(duì)象。
虛繼承的一個(gè)不太直觀的特征:必須在虛派生的真實(shí)需求出現(xiàn)前就已經(jīng)完成虛派生的操作。
在實(shí)際的編程過(guò)程中,位于中間層次的基類(lèi)將其繼承聲明為虛繼承一般不會(huì)帶來(lái)什么問(wèn)題。通常情況下,使用虛繼承的類(lèi)層次是由一個(gè)人或一個(gè)項(xiàng)目組一次性設(shè)計(jì)完成的。對(duì)于一個(gè)獨(dú)立開(kāi)發(fā)的類(lèi)來(lái)說(shuō),很少需要基類(lèi)中的某一個(gè)是虛基類(lèi),況且新基類(lèi)的開(kāi)發(fā)者也無(wú)法改變已存在的類(lèi)體系。
使用虛基類(lèi)
在派生列表中添加關(guān)鍵字
virtual
,后續(xù)的派生類(lèi)當(dāng)中共享虛基類(lèi)的同一份實(shí)例:
// 關(guān)鍵字public和virtual的順序隨意
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };
// 如果某個(gè)類(lèi)指定了虛基類(lèi),則該類(lèi)的派生仍按常規(guī)方式進(jìn)行:
class Panda : public Bear,public Raccoon, public Endangered { /* ... */ };
支持向基類(lèi)的常規(guī)類(lèi)型轉(zhuǎn)換
// 不論基類(lèi)是不是虛基類(lèi),派生類(lèi)對(duì)象都能被可訪問(wèn)基類(lèi)的指針或引用操作。
void dance(const Bear &);
void rummage(const Raccoon &);
ostream &operator<<(ostream &, const ZooAnimal &);
Panda ying_yang;
dangce(ying_yang); // 正確:把一個(gè)Panda對(duì)象當(dāng)成Bear傳遞。
rummage(ying_yang); // 正確:把一個(gè)Panda對(duì)象當(dāng)成Raccoon傳遞。
cout << ying_yang; // 正確:把一個(gè)Panda對(duì)象當(dāng)成ZooAnimal傳遞。
虛基類(lèi)成員的可見(jiàn)性
因?yàn)樵诿總€(gè)共享的虛基類(lèi)中只有唯一一個(gè)共享的子對(duì)象,所以該基類(lèi)的成員可以被直接訪問(wèn),并且不會(huì)產(chǎn)生二義性。此外,如果虛基類(lèi)的成員只被一條派生路徑覆蓋,則仍然可以直接訪問(wèn)這個(gè)被覆蓋的成員。但是如果成員被多余一個(gè)基類(lèi)覆蓋,則一般情況下派生類(lèi)必須為該成員自定義一個(gè)新的版本。
18.3.5構(gòu)造函數(shù)與虛繼承
在虛派生中,虛基類(lèi)是由最低層的派生類(lèi)初始化的。之所以這樣設(shè)計(jì),不妨假設(shè)當(dāng)以普通規(guī)則處理初始化任務(wù)時(shí)會(huì)發(fā)生什么情況。在此例中,虛基類(lèi)將會(huì)在多條繼承路徑上被重復(fù)初始化。
當(dāng)然,繼承體系中的每個(gè)類(lèi)都可能在某個(gè)時(shí)刻成為最低層的派生類(lèi)。只要能創(chuàng)建虛基類(lèi)的派生類(lèi)對(duì)象,該派生類(lèi)的構(gòu)造函數(shù)就必須初始化它的虛基類(lèi)。
// 當(dāng)創(chuàng)建一個(gè)Bear(或Raccoon)的對(duì)象時(shí),它已經(jīng)位于派生的最低層,
// 因此Bear(或Raccoon)的構(gòu)造函數(shù)將直接初始化其ZooAnimal基類(lèi)部分:
Bear:Bear(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Bear") {}
Raccoon::Raccoon(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Raccoon") {}
// 當(dāng)創(chuàng)建一個(gè)Panda對(duì)象時(shí),其位于派生的最低層并由它負(fù)責(zé)初始化共享的ZooAnimal基類(lèi)部分。
// 即使ZooAnimal不是Panda的直接基類(lèi),Panda的構(gòu)造函數(shù)也可以初始化ZooAnimal。
Panda::Panda(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Panda"),Bear(name, onExhibit),Raccoon(name, onExhibit),Endangered(Endangered::critical),sleeping_flag(false) {}
虛繼承的對(duì)象的構(gòu)造方式
首先使用提供給最低層派生類(lèi)構(gòu)造函數(shù)的初始值初始化該對(duì)象的虛基類(lèi)子部分,接下來(lái)按照直接基類(lèi)在派生列表中出現(xiàn)的次序依次對(duì)其進(jìn)行初始化。
虛基類(lèi)總是先于非虛基類(lèi)構(gòu)造,與它們?cè)诶^承體系中的次序和位置無(wú)關(guān)。
構(gòu)造函數(shù)與析構(gòu)函數(shù)的次序
一個(gè)類(lèi)可以有多個(gè)虛基類(lèi)。此時(shí),這些虛的子對(duì)象按照它們?cè)谂缮斜碇谐霈F(xiàn)的順序從左向右依次構(gòu)造:
class Character { /* ... */ };
class BookCharacter : public Character { /* ... */ };
class ToyAnimal { /* ... */ };
class TeddyBear : public BookCharacter,public Bear, public virtual ToyAnimal { /* ... */ };
編譯器按照直接基類(lèi)的聲明順序?qū)ζ湟来芜M(jìn)行檢查,以確定其中是否含有虛基類(lèi)。如果有,則先構(gòu)造虛基類(lèi),然后按照聲明的順序逐一構(gòu)造其他非虛基類(lèi)。
// 創(chuàng)建一個(gè)TeddyBear對(duì)象需要按照如下次序調(diào)用這些構(gòu)造函數(shù):
ZooAnimal(); // Bear的虛基類(lèi)
ToyAnimal(); // 直接虛基類(lèi)
Character(); // 第一個(gè)非虛基類(lèi)的間接基類(lèi)
BookCharacter(); // 第二個(gè)直接非虛基類(lèi)
Bear(); // 第二個(gè)直接非虛基類(lèi)
TeddyBear(); // 最低層的派生類(lèi)
合成的拷貝和移動(dòng)構(gòu)造函數(shù)按照完全相同的順序執(zhí)行,合成的賦值運(yùn)算符中的成員也按照該順序賦值。和往常一樣,對(duì)象的銷(xiāo)毀順序與構(gòu)造順序正好相反。