【1】為什么需要繼承構造函數?
首先,看看如下代碼片段:
1 struct A 2 { 3 A(int i) 4 {} 5 }; 6 7 struct B : A 8 { 9 B(int i) : A(i) 10 {} 11 };
在C++中非常常見:B派生於A,B在構造函數中調用A的構造函數,從而完成構造函數的“傳遞”。
有時候,基類可能擁有數量眾多的不同版本的構造函數(這樣的情況並不少見)。
那么,倘若基類中有大量的構造函數,而派生類卻只有一些成員函數時,那么對於派生類而言,其構造就等同於構造基類。
為了遵從語法規則,就需要寫很多的“透傳”的構造函數。如下面這個例子:
struct A { A(int i) {} A(double d, int i) {} A(float f, int i, const char* c) {} // ... }; struct B : A { B(int i) : A(i) {} B(double d, int i) : A(d, i) {} B(float f, int i, const char* c) : A(f, i, c) {} // ... virtual void ExtraInterface() {} };
在構造B的時候想要擁有A這樣多的構造方法的話,就必須一—“透傳”各個接口。這無疑是相當不方便的。
而引入繼承構造函數的機制就是為了解決這種麻煩的。
【2】using聲明
我們知道C++中已經有一個好用的規則:
如果派生類要使用基類的成員函數(被隱藏)的話,可以通過using聲明(using-declaration)來完成。請看如下示例:
(1)不聲明基類成員函數:
基類成員函數被隱藏(關於隱藏可以參考《重載、覆蓋、隱藏》),在派生類中不做聲明:
1 #include <iostream> 2 using namespace std; 3 4 struct Base 5 { 6 void f(double i) 7 { 8 cout << "Base:" << i << endl; 9 } 10 }; 11 12 struct Derived : Base 13 { 14 // using Base::f; // 聲明基類Base的f函數 15 void f(int i) 16 { 17 cout << "Derived:" << i << endl; 18 } 19 }; 20 21 int main() 22 { 23 Base b; 24 b. f(4.5); 25 Derived d; 26 d. f(4.5); 27 } 28 29 /* 30 Base:4.5 31 Derived:4 32 */
編譯警告:warning C4244 : “參數”: 從“double”轉換到“int”,可能丟失數據
再結合運行結果分析,說明執行的是派生類Derived中參數為int類型的函數f。
(2)聲明基類成員函數:
1 #include <iostream> 2 using namespace std; 3 4 struct Base 5 { 6 void f(double i) 7 { 8 cout << "Base:" << i << endl; 9 } 10 }; 11 12 struct Derived : Base 13 { 14 using Base::f; // 聲明繼承基類Base的f函數 15 void f(int i) 16 { 17 cout << "Derived:" << i << endl; 18 } 19 }; 20 21 int main() 22 { 23 Base b; 24 b. f(4.5); 25 Derived d; 26 d. f(4.5); 27 } 28 29 /* 30 Base:4.5 31 Base:4.5 32 */
編譯無警告,再結合運行結果分析:
說明執行的是基類Base中參數為double類型的函數f。
同時,可以看到派生類Derived中其實有兩個f函數的版本。
經過以上的分析,在C++11中,這個特性被擴展到了構造函數上。
即派生類可以通過使用using聲明來繼承基類的構造函數。
【3】應用繼承構造函數
如上示例1,應用繼承構造函數,改造如下:
1 #include <iostream> 2 using namespace std; 3 4 struct A 5 { 6 A(int i = 10) : m_a(i) 7 {} 8 9 int m_a; 10 }; 11 12 struct B : A 13 { 14 using A::A; // 繼承構造函數 15 16 int m_b{ 100 }; 17 }; 18 19 int main() 20 { 21 B b; 22 cout << b.m_a << endl; // 10 23 cout << b.m_b << endl; // 100 24 25 B bb(200); 26 cout << bb.m_a << endl; // 200 27 cout << bb.m_b << endl; // 100 28 }
通過using A::A的聲明,把基類中的構造函數全部繼承到派生類B中。
應用注意項:
(1)C++11標准更精巧的是,繼承構造函數被設計為跟派生類中的各種類默認函數(默認構造、析構、拷貝構造等)一樣,是隱式聲明的。
這意味着如果一個繼承構造函數不被相關代碼使用,編譯器不會為其產生真正的函數代碼。
這無疑比“透傳”方案總是生成派生類的各種構造函數更加節省目標代碼空間。
(2)繼承構造函數只會初始化基類中成員變量,對於派生類中的成員變量,則無能為力。
不過配合C++11中類成員的初始化表達式,為派生類成員變量設定一個默認值還是沒有問題的。
比如此例中的m_b成員變量,設定默認值為100。
(3)基類構造函數的參數可能會有默認值。
對於繼承構造函數來講,基類構造函數參數的默認值是不會被繼承的。
比如此例中的bb對象,利用實參200進行構造對象,結果m_a的值為200,而不是默認的10。
(4)基類構造函數有默認值會導致基類產生多個構造函數的版本,這些版本都會被派生類繼承。
比如此例中,事實上,構建對象b,使用的均是默認構造函數;構建對象bb,使用的均是帶一個參數的構造函數版本。
(5)有的時候,還會遇到繼承構造函數“沖突”的情況。這通常發生在派生類擁有多個基類的時候。
多個基類中的部分構造函數可能導致派生類中的繼承構造函數的函數名、參數(有的時候,我們也稱其為函數簽名)都相同,
那么繼承類中的沖突的繼承構造函數將導致不合法的派生類代碼,如下示例:
1 struct A { A(int) {} }; 2 3 struct B { B(int) {} }; 4 5 struct C : A, B 6 { 7 using A::A; 8 using B::B; 9 };
這種情況下,可以通過顯式定義繼承類的沖突的構造函數,阻止隱式生成相應的繼承構造函數來解決沖突:
1 struct A { A(int) {} }; 2 3 struct B { B(int) {} }; 4 5 struct C : A, B 6 { 7 C(int c) : A(c), B(c) 8 {} 9 };
(6)如果基類的構造函數被聲明為私有成員函數,或者派生類是從基類中虛繼承的,那么就不能夠在派生類中聲明繼承構造函數。
(7)如果基類的構造函數沒有默認構造函數,那么一旦使用了繼承構造函數,編譯器也不會再為派生類生成默認構造函數。
如下示例不能夠通過編譯:
1 struct A 2 { 3 A(int) {} 4 }; 5 6 struct B : A 7 { 8 using A::A; 9 }; 10 11 int main() 12 { 13 B objB; // C2280 “B::B(void)”: 嘗試引用已刪除的函數 14 }
個人認為,解決這種問題有兩種方式,代碼如下:
1 #if 0 // 方式一:基類定義默認構造函數,派生類自然也就有了 2 struct A { A(int a = 10) {} }; 3 struct B : A 4 { 5 using A::A; 6 }; 7 #else // 方式二:派生類自定義默認構造函數 8 struct A { A(int) {} }; 9 struct B : A 10 { 11 using A::A; 12 B(int c = 20) : A(c) 13 {} 14 }; 15 #endif 16 17 int main() 18 { 19 B objB; 20 }
如上所述。
good good study, day day up.
順序 選擇 循環 總結