條款32:確定public繼承塑模出is-a關(guān)系
is-a和has-a是C++類的兩個重要關(guān)系描述幢炸,如果類D基于public方式繼承于類B摹闽,則D類的實例 is-a B類的實例,適用于Base class身上的每一件事情也一定適用于Derived class身上箭启。
條款33:避免遮掩繼承而來的名稱
C++編譯器在編譯的時候針對類內(nèi)的成員函數(shù)也會按照變量域類似的規(guī)則進行搜索:即先在本類的作用域中搜索乾颁,如果找到對應(yīng)的符號涂乌,則不會繼續(xù)擴大搜索域,如果沒有找到則會擴大搜索域英岭,先擴展至其父類域湾盒,依次往上擴展∽缑茫考察下面的示例代碼:
#include <iostream>
class B {
public:
void mf1() {}
void mf2() {}
};
class D : public B {
public:
void mf3() {}
void mf1(int a) {}
};
int main()
{
D obj;
obj.mf3(); // 編譯OK罚勾,調(diào)用D::mf3()
obj.mf1(2); // 編譯OK,調(diào)用D::mf1(int)
obj.mf1(); // 編譯不通過
obj.mf2(); // 編譯通過吭狡,調(diào)用B::mf2
return 0;
}
- 在編譯obj.mf3()時尖殃,直接就命中D類作用域定義的mf3函數(shù),并且參數(shù)與定義的一致划煮,編譯通過送丰。
- 在編譯obj.mf1(2)時,因為在D類的作用域中有mf1函數(shù)弛秋,并且參數(shù)定義一致蚪战,編譯通過牵现。
- 在編譯obj.mf1()時,因為D類作用域中有mf1函數(shù)邀桑,編譯器不會擴大搜索域了瞎疼,然后比較參數(shù)定義發(fā)現(xiàn)不一致,所以編譯不通過壁畸。
- 在編譯obj.mf2()時贼急,由于D類作用域中沒有這個函數(shù)符號,所以編譯器擴大搜索域至其父類捏萍,即B類作用域太抓,在B類作用域中發(fā)現(xiàn)此函數(shù)符號,并且參數(shù)定義檢查一致令杈,所以編譯通過走敌。
結(jié)論:在子類中需要避免定義與父類中同名的函數(shù)(虛函數(shù)除外),因為那樣的話逗噩,子類對象實例就不能調(diào)用父類定義的該函數(shù)了掉丽,如果一定要調(diào)用,則需要在子類中使用using語句顯式的聲明父類被掩蓋的函數(shù)异雁,如下所示:
#include <iostream>
class B {
public:
void mf1() {}
void mf2() {}
};
class D : public B {
public:
using B::mf1; // 使用using語句聲明父類中被掩蓋的函數(shù)
void mf3() {}
void mf1(int a) {}
};
int main()
{
D obj;
obj.mf3(); // 編譯OK捶障,調(diào)用D::mf3()
obj.mf1(2); // 編譯OK,調(diào)用D::mf1(int)
obj.mf1(); // 現(xiàn)在編譯可以通過了
obj.B::mf1(); // 等同于上面的寫法
obj.mf2(); // 編譯通過纲刀,調(diào)用B::mf2
return 0;
}
條款34:區(qū)分接口繼承和實現(xiàn)繼承
- 純虛函數(shù)意味著接口繼承项炼,子類如果要實例化,則必須要實現(xiàn)父類中定義的純虛函數(shù)示绊。
- 非純虛函數(shù)意味著接口繼承和繼承一份默認的實現(xiàn)锭部,該默認的實現(xiàn)在父類中給出(非純虛函數(shù)必須在父類函數(shù)中給出一份默認實現(xiàn)),子類可以給出自己的實現(xiàn)以覆蓋默認實現(xiàn)面褐。
- 非虛函數(shù)意味著接口繼承以及強制性實現(xiàn)繼承拌禾,一般來說,如果在父類中定義了一個非虛函數(shù)盆耽,則子類中一般無需重新定義。
條款35:考慮virtual函數(shù)以外的其它選擇
- 選擇1:NVI(Non-virtual Interface)手法扼菠,采用非虛函數(shù)調(diào)用虛函數(shù)的方式摄杂,如下:
#include <iostream>
class B {
public:
void FuncWrapper() {std::cout << "Entering B::FooWrapper" << std::endl; Foo();}
private:
virtual void Foo() {std::cout << "Entering B::Foo" << std::endl;};
};
class D : public B {
private:
virtual void Foo() {std::cout << "Entering D::Foo" << std::endl;}
};
int main()
{
D obj;
obj.FuncWrapper();
return 0;
}
在基類B中定義了一個非虛函數(shù)FuncWrapper,然后定義了一個一般虛函數(shù)Foo循榆,在FuncWrapper調(diào)用Foo析恢,這樣我們將Foo放在private域中,增強了類的封裝性秧饮。
- 方法2:virtual函數(shù)替換為函數(shù)指針(或者std::function對象映挂,C++11之后的特性)泽篮,這是Strategy設(shè)計模式的具體形式。
#include <iostream>
#include <functional>
static void Func1()
{
std::cout << "Calling Func1" << std::endl;
}
static void Func2()
{
std::cout << "Calling Func2" << std::endl;
}
using MyFunc = std::function<void()>; // 使用using語句代替?zhèn)鹘y(tǒng)的typedef柑船,定義一個函數(shù)類型
class MyClass {
public:
MyClass(MyFunc func) : f(func) {}
void DoSomething() {f();}
private:
MyFunc f;
};
int main()
{
MyClass obj1(Func1), obj2(Func2);
obj1.DoSomething();
obj2.DoSomething();
return 0;
}
在這個MyClass類中帽撑,定義了一個私有成員變量,它代表處理函數(shù)真正的執(zhí)行動作鞍时,由于每個對象可能的執(zhí)行動作不一樣亏拉,所以可以在構(gòu)造函數(shù)中傳入具體的函數(shù),注意:這里使用了C++11之后的std::function模板逆巍,這是比傳統(tǒng)的函數(shù)指針更好用的一種C++方式及塘。
條款36:絕不重新定義繼承而來的非虛函數(shù)
一句話:子類切勿重新定義繼承自父類中的非虛函數(shù),如果你真的要這么做锐极,請將其定義成虛函數(shù)笙僚。
條款37:絕不重新定義繼承而來的缺省參數(shù)值
這是個C++考試中常考的一個知識點灵再,考察以下示例代碼:
#include <iostream>
class B {
public:
virtual void Foo(int val = 1) {std::cout << "val = " << val << std::endl;}
};
class D : public B {
public:
virtual void Foo(int val = 2) {std::cout << "val = " << val << std::endl;}
};
int main()
{
B* p = new D();
p->Foo();
return 0;
}
可以看到在調(diào)用子類的Foo虛函數(shù)版本中肋层,參數(shù)卻是用了父類中的缺省值。
簡而言之:缺省參數(shù)的設(shè)置是靜態(tài)的檬嘀,而虛函數(shù)的執(zhí)行是動態(tài)的槽驶,所以請不要重新定義繼承而來的缺省值,否則會引起誤解鸳兽。
條款38:通過組合關(guān)系塑模出“has-a”
- 一些C++設(shè)計模式中推薦盡可能使用組合而非繼承掂铐。
- 組合意味著“has-a”關(guān)系。
條款39:明智而謹慎的使用private繼承
條款40:多重繼承
以上兩條不常用揍异,也不推薦使用全陨。