原文地址:https://qunxinghu.github.io/2016/09/12/C++ 三大特性之繼承/
基本概念
- 繼承
- 類的繼承,就是新的類從已有類那里得到已有的特性。原有的類稱為基類或父類,產生的新類稱為派生類或子類。
基本語法
派生類的聲明:
class 派生類名:繼承方式 基類名1, 繼承方式 基類名2,...,繼承方式 基類名n
{
派生類成員聲明;
};
在 c++ 中,一個派生類可以同時有多個基類,這種情況稱為多重繼承。如果派生類只有一個基類,稱為單繼承。派生類繼承基類中除構造和析構函數以外的所有成員。
類的繼承方式
繼承方式規定了如何訪問基類繼承的成員。繼承方式有public, private, protected。如果不顯示給出繼承方式,默認為private繼承。繼承方式指定了派生類成員以及類外對象對於從基類繼承來的成員的訪問權限。
- 公有繼承
當類的繼承方式為公有繼承時,基類的公有和保護成員的訪問屬性在派生類中不變,而基類的私有成員不可訪問。即基類的公有成員和保護成員被繼承到派生類中仍作為派生類的公有成員和保護成員。派生類的其他成員可以直接訪問它們。無論派生類的成員還是派生類的對象都無法訪問基類的私有成員。 - 私有繼承
當類的繼承方式為私有繼承時,基類中的公有成員和保護成員都以私有成員身份出現在派生類中,而基類的私有成員在派生類中不可訪問。基類的公有成員和保護成員被繼承后作為派生類的私有成員,派生類的其他成員可以直接訪問它們,但是在類外部通過派生類的對象無法訪問。無論是派生類的成員還是通過派生類的對象,都無法訪問從基類繼承的私有成員。通過多次私有繼承后,對於基類的成員都會成為不可訪問。因此私有繼承比較少用。 - 保護繼承
保護繼承中,基類的公有成員和私有成員都以保護成員的身份出現在派生類中,而基類的私有成員不可訪問。派生類的其他成員可以直接訪問從基類繼承來的公有和保護成員,但是類外部通過派生類的對象無法訪問它們,無論派生類的成員還是派生類的對象,都無法訪問基類的私有成員。
派生類的構造函數
- 派生類中由基類繼承而來的成員的初始化工作還是由基類的構造函數完成,派生類中新增的成員在派生類的構造函數中初始化。
派生類構造函數的語法:
派生類名::派生類名(參數總表):基類名1(參數表1),基類名(參數名2)....基類名n(參數名n),內嵌子對象1(參數表1),內嵌子對象2(參數表2)....內嵌子對象n(參數表n)
{
派生類新增成員的初始化語句;
}
注:構造函數的初始化順序並不以上面的順序進行,而是根據聲明的順序初始化。
2. 如果基類中沒有不帶參數的構造函數,那么在派生類的構造函數中必須調用基類構造函數,以初始化基類成員。
3. 派生類構造函數執行的次序:
1. 調用基類構造函數,調用順序按照它們 被繼承時聲明的順序 (從左到右);
2. 調用內嵌成員對象的構造函數,調用順序按照它們在類中聲明的順序;
3. 派生類的構造函數體中的內容。
派生類的析構函數
派生類的析構函數的功能是在該對象消亡之前進行一些必要的清理工作,析構函數沒有類型,也沒有參數。析構函數的執行順序與構造函數相反。
實例:
#include <iostream>
#include <time.h>
using namespace std;
// 基類 B1
class B1
{
public:
B1(int i)
{
cout<<"constructing B1 "<<i<<endl;
}
~B1()
{
cout<<"destructing B1"<<endl;
}
};
//基類 B2
class B2
{
public:
B2(int j)
{
cout<<"constructing B2 "<<j<<endl;
}
~B2()
{
cout<<"destructing B2"<<endl;
}
};
//基類 B3
class B3
{
public:
B3()
{
cout<<"constructing B3"<<endl;
}
~B3()
{
cout<<"destructing B3"<<endl;
}
};
//派生類 C, 繼承B2, B1,B3(聲明順序從左至右。 B2->B1->B3)
class C: public B2, public B1, public B3
{
public:
C(int a, int b, int c, int d):B1(a), memberB2(d), memberB1(c),B2(b)
{
//B1,B2的構造函數有參數,B3的構造函數無參數
//memberB2(d), memberB1(c)是派生類對自己的數據成員進行初始化的過程、
//構造函數執行順序, 基類(聲明順序)-> 內嵌成員對象的構造函數(聲明順序) -> 派生類構造函數中的內容
}
private:
B1 memberB1;
B2 memberB2;
B3 memberB3;
};
int main()
{
C obj(1,2,3,4);
return 0;
}
/* 輸出結果 */
/*
constructing B2 2
constructing B1 1
constructing B3
constructing B1 3
constructing B2 4
constructing B3
destructing B3
destructing B2
destructing B1
destructing B3
destructing B1
destructing B2
*/
二義性問題
在單繼承下,基類的public 和protected 成員可以直接被訪問,就像它們是派生類的成員一樣,對多繼承這也是正確的。但是在多繼承下,派生類可以從兩個或者更多個基類中繼承同名的成員。然而在這種情況下,直接訪問是二義的,將導致編譯時刻錯誤。
示例:
#include <iostream>
using namespace std;
class A
{
public:
void f();
};
class B
{
public:
void f();
void g();
};
class C : public A, public B
{
public:
void g();
void h();
};
int main(){
C c1;
// c1.f(); 產生二義性問題,訪問A中的 f()? or B的 f() ?
//通過指定成員名,限定消除二義性
c1.A::f();
c1.B::f();
}
使用成員名限定法可以消除二義性,但是更好的解決辦法是在類C中定義一個同名函數 f(), 類C中的 f() 再根據需要來決定調用 A::f()
or B::f()
, 這樣 c1.f()
將調用 C::f()
.
當一個派生類從多個基類派生類,而這些基類又有一個共同的基類,則對該基類中說明的成員進行訪問時,也可能會出現二義性。
示例:
// 派生類 B1,B2 繼承相同的基類 A, 派生類 C 繼承 B1, B2
class A
{
public:
int a;
};
class B1 : public A
{
private:
int b1;
};
class B2 : public A
{
private:
int b2;
};
class C : public B1, public B2
{
public:
int f();
private:
int c;
};
int main(){
C c1;
c1.a();
c1.A::a();
c1.B1::a();
c1.B2::a();
return 0;
}
c1.a;
c1.A::a;
這兩個訪問都有二義性,c1.B1::a;
c1.B2::a;
是正確的:
類C的成員函數 f()
用如下定義可以消除二義性:
int C::f()
{
retrun B1::a + B2::a;
}
由於二義性的原因,一個類不可以從同一個類中直接繼承一次以上。
虛基類
多繼承時很容易產生命名沖突,即使我們很小心地將所有類中的成員變量和成員函數都命名為不同的名字,命名沖突依然有可能發生,比如非常經典的菱形繼承層次。如下圖所示:
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
類A派生出類B和類C,類D繼承自類B和類C,這個時候類A中的成員變量和成員函數繼承到類D中變成了兩份,一份來自 A-->B-->D 這一路,另一份來自 A-->C-->D 這一條路。當D訪問從A中繼承的數據時,變一起將無法決定采用哪一條路傳過來的數據,於是便出現了虛基類。
在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數據,但大多數情況下這是多余的:因為保留多份成員變量不僅占用較多的存儲空間,還容易產生命名沖突,而且很少有這樣的需求。使用虛基類,可以使得在派生類中只保留間接基類的一份成員。
聲明虛基類只需要在繼承方式前面加上 virtual 關鍵字,如下面示例:
#include <iostream>
using namespace std;
class A{
protected:
int a;
public:
A(int a):a(a){}
};
class B: virtual public A{ //聲明虛基類
protected:
int b;
public:
B(int a, int b):A(a),b(b){}
};
class C: virtual public A{ //聲明虛基類
protected:
int c;
public:
C(int a, int c):A(a),c(c){}
};
class D: virtual public B, virtual public C{ //聲明虛基類
private:
int d;
public:
D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
void display();
};
void D::display(){
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
cout<<"c="<<c<<endl;
cout<<"d="<<d<<endl;
}
int main(){
(new D(1, 2, 3, 4)) -> display();
return 0;
}
/*
運行結果:
a=1
b=2
c=3
d=4
*/
本例中我們使用了虛基類,在派生類D中只有一份成員變量 a 的拷貝,所以在 display() 函數中可以直接訪問 a,而不用加類名和域解析符。
- 虛基類的初始化
- 請注意派生類D的構造函數,與以往的用法有所不同。 以往,在派生類的構造函數中只需負責對其直接基類初始化,再由其直接基類負責對間接基類初始化。現在,由於虛基類在派生類中只有一份成員變量,所以對這份成員變量的初始化必須由派生類直接給出。如果不由最后的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有可能由於在類B和類C的構造函數中對虛基類給出不同的初始化參數而產生矛盾。所以規定: 在最后的派生類中不僅要負責對其直接基類進行初始化,還要負責對虛基類初始化。
在上述代碼中,類D的構造函數通過初始化表調了虛基類的構造函數A,而類B和類C的構造函數也通過初始化表調用了虛基類的構造函數A,這樣虛基類的構造函數豈非被調用了3次?大家不必過慮,C++編譯系統只執行最后的派生類對虛基類的構造函數的調用,而忽略虛基類的其他派生類(如類B和類C)對虛基類的構造函數的調用,這就保證了虛基類的數據成員不會被多次初始化。
最后請注意: 為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中聲明為虛基類,否則仍然會出現對基類的多次繼承。
賦值兼容原則
- 賦值兼容
- 賦值兼容規則是指在 需要基類對象的任何地方都可以使用公有派生類的對象來替代。
賦值兼容規則中所指的替代包括:
- 派生類的對象可以賦值給基類對象;
- 派生類的對象可以初始化基類的引用;
- 派生類對象的地址可以賦給指向基類的指針。
在替代之后,派生類對象就可以作為基類的對象使用,但只能使用從基類繼承的成員。