1. 程式人生 > >【程式語言】C++繼承和派生類、虛基類

【程式語言】C++繼承和派生類、虛基類

從已有的物件型別出發建立一種新的物件型別,使它部分或全部繼承原物件的特點和功能,這是面向物件設計方法中的基本特性之一。繼承不僅簡化了程式設計方法,顯著提高了軟體的重用性,而且還使得軟體更加容易維護。派生則是繼承的直接產物,它通過繼承已有的一個或多個類來產生一個新的類,通過派生可以建立一種類族。

繼承

基本概念

在定義一個類A時,若它使用了一個已定義類B的部分或全部成員,則稱類A繼承了類B,並稱類B為基類或父類,稱類A為派生類或子類。一個派生類又可以作為另一個類的基類,一個基類可以派生出若干個派生類,這樣就構成類樹。

在C++中有兩種繼承:單一繼承和多重繼承。當一個派生類僅由一個基類派生時,稱為單一繼承;而當一個類由兩個或更多個基類派生時,稱為多重繼承。

C++中派生類從父類繼承特性時,可在派生類中擴充套件它們,或者對其做些限制,也可以改變或刪除某一特性,還可對某些特性不作任何修改。所有這些變化可歸結為兩種基本的面向物件技術:

  • 特性約束,即對父類的特性加以限制或刪除;
  • 特性擴充套件,即增加父類的特性。

單一繼承

從一個基類派生一個類的一般格式為:

class 派生類類名: <Access> 基類類名{
    private:
        ...
    public:
        ...
    protected:
        ...
};

其中:Access可有可無,用於規定基類中的成員在派生類中的訪問許可權,它可以是關鍵字private、public、protected三者之一。當Access省略的時候,對於類,系統預設約定為private;而對於結構體而言,系統預設約定為public。

花括號中的部分是在派生類中新增加的成員資料或成員函式,這部分也可為空。

當Access為public時,稱派生類為公有派生;當Access為private時,稱派生類為私有派生;當Access為protected時,稱派生類為保護派生。派生時指定不同的訪問許可權,直接影響到基類中的成員在派生類中的訪問許可權。

公有派生

公有派生時,基類中所有成員在公有派生類中保持各個成員的訪問許可權。具體地說:

  • 基類中public成員,在派生類中保持public,在派生類中或派生類外部可以直接使用;
  • 基類中private成員,屬於基類私有,在派生類中或派生類外部都不能直接使用;
  • 基類中protected成員,可在公有派生類中直接使用,但在派生類外不可直接訪問。

例如:

#include <iostream>
using namespace std;

class A {
	private:
		int x;
	protected:
		int y;
	public:
		int z;
		A(int a, int b, int c) { x = a; y = b; z = c; }
		void Setx(int a) { x = a; }
		void Sety(int a) { y = a; }
		int Getx() { return x; }
		int Gety() { return y; }
		void ShowA() {
			cout << x << '\t' << y << '\t' << z << endl;
		}
};

class B : public A {
	private:
		int Length, Width;
	public:
		B(int a, int b, int c, int d, int e) :A(a, b, c) {                //A
			Length = d; Width = e;
		}
		void Show() {
			cout << Length << '\t' << Width << endl;
			cout << Getx() << '\t' << y << '\t' << z << endl;         //B
		}
};

int main()
{
	B b1(1, 2, 3, 4, 5);
	b1.ShowA();
	b1.Show();
	cout << b1.Getx() << '\t' << b1.Gety() << '\t' << b1.z << endl;            //C

	system("pause");
	return 0;
}

解釋:A行是在派生類的建構函式中呼叫基類的建構函式,寫法與物件成員比較類似。只不過物件成員是類中有其他類的物件作為成員,而此處為繼承;物件成員使用的是物件名,繼承為類名。具體的講解在下面會介紹到。

主要看一下B行和C行。B行是在公有派生類中檢視基類的方法,除了private不能直接檢視之外,public和protected都能;C行是在公有派生類外檢視基類的方法,僅有public的能夠直接檢視之外,private和protected都不能。

私有派生

對於私有派生類而言,其:

  • 基類中public成員和protected成員在派生類中均變成私有的,在派生類中仍可使用這些成員,而派生類之外均不能;
  • 基類中private成員,在派生類中和派生類外都不可直接使用。

例子:

#include <iostream>
using namespace std;

class A {
	private:
		int x;
	protected:
		int y;
	public:
		int z;
		A(int a, int b, int c) { x = a; y = b; z = c; }
		void Setx(int a) { x = a; }
		void Sety(int a) { y = a; }
		int Getx() { return x; }
		int Gety() { return y; }
		void ShowA() {
			cout << x << '\t' << y << '\t' << z << endl;
		}
};

class B : private A {
	private:
		int Length, Width;
	public:
		B(int a, int b, int c, int d, int e) :A(a, b, c) {                //A
			Length = d; Width = e;
		}
		void Show() {
			cout << Length << '\t' << Width << endl;
			cout << Getx() << '\t' << y << '\t' << z << endl;         //B
		}
};

int main()
{
	B b1(1, 2, 3, 4, 5);
	b1.ShowA();                                                                //D
	b1.Show();
	cout << b1.Getx() << '\t' << b1.Gety() << '\t' << b1.z << endl;            //C

	system("pause");
	return 0;
}

首先:這段程式執行出錯!

B行沒有問題,基類中的private無法訪問,而public和protected都能直接訪問。而C行和D行在派生類的外部,都不能直接呼叫基類的任何方法或成員,出錯。

綜上所述,在派生類中,繼承基類的訪問許可權可以概括為:

公有或私有派生
派生方式 基類訪問許可權 派生類中訪問許可權 派生類外訪問許可權
public public public 可訪問
public protected protected 不可訪問
public private 不可訪問 不可訪問
private

public

private 不可訪問
private protected private 不可訪問
private private 不可訪問 不可訪問

抽象類與保護的成員函式

若定義了一個類,該類只能用做基類來派生出新的類,而不能用作定義物件,該類稱為抽象類。當對某些特殊的物件要進行很好地封裝時,需要定義抽象類。

當把一個類的建構函式和解構函式的訪問許可權定義為保護的時,這種類為抽象類。

原因:當某個類的建構函式或解構函式的訪問許可權定義為保護時,在類的外面(如:主函式等)由於無法呼叫該類的建構函式和解構函式,因此無法產生該類的物件或者撤銷物件。而當使用該類作為基類產生派生類時,在派生類中是可以呼叫其基類的建構函式和解構函式的,因為基類中的保護成員在派生類中一樣可以使用。

但:如果將一個類的建構函式和解構函式的訪問許可權定義為私有的時候,通常這種類是沒有任何實際意義的,既不能產生物件,也不能用來產生派生類。

多重繼承

用多個基類來派生一個類時,其一般格式為:

class 派生類類名: <Access> 基類類名1, <Access> 基類類名2,..., <Access> 基類類名n {
    private:
        ...
    public:
        ...
    protected:
        ...
};

其中:Access是以限定該基類中的成員在派生類中的訪問許可權,其規則與單一繼承的用法類似。從上述格式可以看出,很容易將單一繼承推廣到多重繼承。

初始化基類成員

在基類中定義了基類的建構函式,並且在派生類中也定義了派生類的建構函式,那麼在產生派生類的物件時,一方面系統要呼叫派生類的建構函式來初始化在派生類中新增的成員資料,另一方面系統也要呼叫基類的建構函式來初始化派生類中的基類成員。

這種基類的建構函式是由派生類的建構函式來確定的。為了初始化基類成員,派生類的建構函式的一般格式為:

派生類類名(派生類引數列表): 基類類名1(基類1引數列表),  基類類名2(基類2引數列表),..., 基類類名n(基類n引數列表) {
    ...
}

需要注意的是:“派生類引數列表”是帶有型別說明的形參表,而“基類引數列表”是不帶有型別說明的實參表。其中,“基類引數列表”可以是表示式,可以是“派生類引數列表”下的形參,也可以是各種常量。

如果派生類中包含物件成員,則在派生類的建構函式的初始化成員列表不僅要列舉要呼叫的基類的建構函式,而且要列舉呼叫的物件成員的建構函式。

這裡的順序問題:

  • 如果某派生類有好幾個基類,那麼各個基類建構函式的呼叫順序:在類繼承中說明的順序,與它們在建構函式的初始化成員列表的先後順序無關;
  • 派生類建構函式、基類建構函式的呼叫順序:當說明派生類物件時,系統首先呼叫個基類的建構函式,對基類成員進行初始化,然後執行派生類的建構函式。若某一個基類仍是派生類,則這種呼叫基類的建構函式的過程遞進下去。當撤銷派生類的物件時,解構函式的呼叫順序正好與建構函式的順序相反;
  • 物件成員的建構函式、基類建構函式的呼叫順序:先呼叫基類的建構函式,再呼叫物件成員的建構函式,最後執行派生類的建構函式。有多個物件成員的條件下,呼叫這些物件成員的建構函式的順序取決於它們在派生類中說明的順序。

重點:在派生類的建構函式的初始化成員列表中,對物件成員的初始化必須使用物件名;而對基類成員的初始化,使用的是對應基類的建構函式名。比如:

Der(int a, int b): Base1(a), Base2(20), b1(200), b2(a+b){
    ...
}

來一條練習題:

#include <iostream>
using namespace std;

class Base1 {
	private:
		int x;
	public:
		Base1(int a) {
			x = a;
			cout << x <<" 呼叫基類1的建構函式!" << endl;
		}
		~Base1() {
			cout << "呼叫基類1的解構函式!" << endl;
		}
};

class Base2 {
private:
	int y;
public:
	Base2(int a) {
		y = a;
		cout << y <<" 呼叫基類2的建構函式!" << endl;
	}
	~Base2() {
		cout << "呼叫基類2的解構函式!" << endl;
	}
};

class Der : public Base2, public Base1 {                                             //A
	private:
		int z;
		Base1 b1;                                                            //B
		Base2 b2;                                                            //C
	public:
		Der(int a, int b) :Base1(a), Base2(20), b2(200), b1(a + b) {         //D
			z = b;
			cout << "呼叫派生類的建構函式!" << endl;
		}
		~Der() {
			cout << "呼叫派生類的解構函式!" << endl;
		}
};

int main()
{
	Der d(100, 200);

	system("pause");
	return 0;
}

這段程式的執行結果為:

20 呼叫基類2的建構函式!
100 呼叫基類1的建構函式!
300 呼叫基類1的建構函式!
200 呼叫基類2的建構函式!
呼叫派生類的建構函式!
呼叫派生類的解構函式!
呼叫基類2的解構函式!
呼叫基類1的解構函式!
呼叫基類1的解構函式!
呼叫基類2的解構函式!
請按任意鍵繼續. . .

解析:派生類建構函式的順序,首先基類建構函式,再物件成員建構函式。基類建構函式順序看A行,物件成員建構函式順序看B、C行,而不是看初始化成員列表D行。同時注意一下,在D行,基類建構函式用類名,物件成員建構函式用物件名。

衝突、支配規則和賦值相容性

衝突

若一個公有的派生類是由兩個或多個基類派生,當基類中成員的訪問許可權為public,且不同基類中的成員具有相同的名字時,出現了重名的情況。這是在派生類使用到基類中的同名成員時,出現了不唯一性,這種情況稱為衝突。

例如:

class A {
	public:
		int x;
		void Show() {
			cout << x << endl;
		}
};

class B {
	public:
		int x;
		void Show() {
			cout << x << endl;
		}
};

class C :public A, public B {
	public:
		void Setx(int a) {
			x = a;                    //A出錯
		}
};

此時,在A行編譯器無法確定要訪問的時基類A中的x,還是基類B中的x,即出現編譯出錯。

解決這種衝突的辦法有三種:

  • 使得各個基類中的成員名各不相同,但並不是最優解決方法;
  • 在各個基類中,把成員資料的訪問許可權說明為私有的,並在相應的基類中提供公有的成員函式來對這些成員資料進行操作,但這隻適合於成員資料,不適合成員函式;
  • 使用作用域運算子“::”來限定所訪問的是屬於哪一個基類的。

作用域運算子的一般格式為:

類名:: 成員名

其中:類名可以是任一基類或派生類的類名,成員名只能是成員資料名或成員函式名。

當把派生類作為基類,又派生出新的派生類時,這種限定作用域的運算子不能巢狀使用,也就是說,如下形式的使用方式是不允許的:

類名1:: 類名2:: ...:: 成員名

例如:

class A {
	public:
		int x;
		void Show() {
			cout << x << endl;
		}
};

class B {
	public:
		int x;
		void Show() {
			cout << x << endl;
		}
};

class C :public A, public B {
	public:
		void Setx(int a) {
			A::x = a;                    //作用域運算子
		}
};

支配規則

在C++中,允許派生類中新增加的成員名與其基類的成員名相同,這種同名並不產生衝突。當沒有使用作用域運算子時,則派生類中定義的成員名優先於基類中的成員名,這種優先關係稱為支配規則。

有點類似於“區域性優先”的意思。

例如:

#include <iostream>
using namespace std;

class A {
	public:
		int x;
		void Showx() {
			cout << x << endl;
		}
};

class B {
	public:
		int x;
		void Showx() {
			cout << x << endl;
		}
		int y;
		void Showy() {
			cout << y << endl;
		}
};

class C :public A, public B {
	public:
		int y;
		void Showy() {
			cout << y << endl;
		}
};

int main()
{
	C c1;
	c1.A::x = 100;                        //衝突,基類A的x
	c1.B::x = 100;                        //衝突,基類B的x
	c1.y = 200;                           //支配,派生類中的y
	c1.B::y = 100;                        //支配,基類B的y

	system("pause");
	return 0;
}

基類和物件成員

任一基類在派生類中只能繼承一次,否則就會造成成員名的衝突。比如:

class A {
    public:
        float x;
};

class B :public A, public A{                //發生衝突
    ...
};

此時在派生類B中包含兩個繼承來的成員x,會在使用的時候產生衝突。為了避免這樣的衝突,C++規定同一基類只能繼承一次。

若在類B中,確實要有兩個類A的成員,則可用類A的兩個物件作為類B的成員。例如:

class B {
    A a1, a2;
};

把一個類作為派生類的基類或把一個類的物件作為一個類的成員,從程式的執行效果上看是相同的,但在使用上是有區別的:在派生類中可直接使用基類的成員(訪問許可權允許的話),但要使用物件成員的成員時,必須在物件名後面加上成員運算子“.”和成員名。

賦值相容規則

在同類型的物件之間可以相互賦值,那麼派生類的物件與其基類的物件之間能否相互賦值呢?這裡就要討論賦值相容規則。

簡答的說:對於公有派生類來說,可以將派生類的物件賦給其基類的物件,反之是不允許的。

例如:

#include <iostream>
using namespace std;

class A {
	public:
		int x;
};

class B {
	public:
		int y;
};

class C :public A, public B {
	public:
		int y;
};

int main()
{
	C c1, c2, c3;
	A a1, *pa1;
	B b1, *pb1;

	system("pause");
	return 0;
}

賦值相容與限制可歸結為以下四點:

  • 派生類的物件可以賦值給基類的物件,系統是將派生類物件中從對應基類中繼承來的成員賦給基類物件。例如:
a1 = c1;                //將c1中從a1繼承下來的對應成員(x)賦值給a1的對應成員
  • 不能將基類的物件賦值給派生類物件。例如:
c2 = a1;                //錯誤
  • 可以將一個派生類物件的地址賦給基類的指標變數。例如:
pa1 = &c2;
pb1 = &c3;
  • 派生類物件可以初始化基類的引用。例如:
B &rb = c1;

注意:在後兩者的情況下,使用基類的指標或引用時,只能訪問從相應基類中繼承來的成員,而不允許訪問其他基類的成員或在派生類中增加的成員。

虛基類

在C++中,假定已定義了一個公共的基類A,類B由類A公有派生,類C也由類A公有派生,而類D是由類B和類C共同公有派生。顯然在類D中包含類A的兩個拷貝(例項)。這種同一個公共的基類在派生類中產生多個拷貝不僅多佔用了儲存空間,而且可能會造成多個拷貝中資料不一致。

例如:

#include <iostream>
using namespace std;

class A {
	public:
		int x;
		A(int a = 0){
			x = a;
		}
};

class B : public A {
	public:
		int y;
		B(int a = 0, int b=0):A(b) {
			y = a;
		}
		void PB() {
			cout << x << ' ' << y << endl;
		}
};

class C : public A {
	public:
		int z;
		C(int a = 0, int b = 0) :A(b) {
			z = a;
		}
		void PC() {
			cout << x << ' ' << z << endl;
		}
};

class D :public B, public C {
	public:
		int m;
		D(int a, int b, int d, int e, int f) :B(a, b), C(d, e) {
			m = f;
		}
		void Print() {
			PB();
			PC();
			cout << m << endl;
		}
};

int main()
{
	D d1(100, 200, 300, 400, 500);
	d1.Print();

	system("pause");
	return 0;
}

這段程式的執行結果為:

200 100
400 300
500
請按任意鍵繼續. . .

從結果可以看出,在類D中包含公共基類A的兩個不同的拷貝(例項)。

但如果將類D改成如下:

class D :public B, public C {
	public:
		int m;
		D(int a, int b, int d, int e, int f) :B(a, b), C(d, e) {
			m = f;
		}
		void Print() {
			cout << x << ' ' << y << endl;                //出錯
			cout << x << ' ' << z << endl;                //出錯
			cout << m << endl;
		}
};

此時編譯器無法確定成員x時從B類繼承過來的,還是C類繼承過來的,產生了衝突。可使用作用域運算子“::”來限定成員是屬於類B或類C。也就是將程式改為:

class D :public B, public C {
	public:
		int m;
		D(int a, int b, int d, int e, int f) :B(a, b), C(d, e) {
			m = f;
		}
		void Print() {
			cout << B::x << ' ' << y << endl;
			cout << C::x << ' ' << z << endl;
			cout << m << endl;
		}
};

需要注意的是,這裡使用的是B::x和C::x,而不是B::A::x和C::A::x。也就是和上文衝突的區別:

衝突是派生類繼承的兩個基類中含有相同名稱的成員資料,而此處是派生類繼承的兩個基類同時又繼承同一個基類。

上面都是派生類中包含同一基類的兩個拷貝,這樣極有可能造成使用上的衝突。那麼,在多重派生的過程中,若欲使公共的基類在派生類中只有一個拷貝,怎麼來實現呢?

可以將這種基類說明為虛基類。在派生類的定義中,只要在其基類的類名前加上關鍵字virtual,就可以將基類說明為虛基類。其一般格式為:

class 派生類類名:virtual <Access> 基類類名{
    ...
}

class 派生類類名:<Access> virtual 基類類名{
    ...
}

其中:關鍵字virtual可放在訪問許可權之前,也可以放在訪問許可權之後,而且該關鍵字只對緊隨其後的基類名起作用。

例如:

#include <iostream>
using namespace std;

class A {
	public:
		int x;
		A(int a = 0){                                        //A
			x = a;
		}
};

class B : virtual public A {                                        //B
	public:
		int y;
		B(int a = 0, int b=0):A(b) {
			y = a;
		}
		void PB() {
			cout << x << ' ' << y << endl;
		}
};

class C : public virtual A {                                        //C
	public:
		int z;
		C(int a = 0, int b = 0) :A(b) {
			z = a;
		}
		void PC() {
			cout << x << ' ' << z << endl;
		}
};

class D :public B, public C {
	public:
		int m;
		D(int a, int b, int d, int e, int f) :B(a, b), C(d, e) {        //D
			m = f;
		}
		void Print() {
			PB();
			PC();
			cout << m << endl;
		}
};

int main()
{
	D d1(100, 200, 300, 400, 500);                                            //E
	d1.Print();
	d1.x = 600;                                                               //F
	d1.Print();

	system("pause");
	return 0;
}

這段程式的執行結果為:

0 100
0 300
500
600 100
600 300
500
請按任意鍵繼續. . .

首先,從這段程式的輸出可以看出:在派生類D中的物件d1中僅僅只有基類A的一個拷貝(原因:當F行改變了基類中成員x的值之後,接下來基類B和C的輸出結果同時都改變了)。這是因為在B行和C行都定義為了虛基類。

其次,還可以看出一個好玩的現象:x的初值為0。

當沒有定義成虛基類的時候,在類D中包含公共基類A的兩個不同的拷貝(例項),所以在E行初始化的時候,兩份拷貝一份為200,一份為400。一旦變成虛基類僅有一個拷貝的時候,保留哪一個?還是怎麼處理這個問題呢?

解釋:呼叫虛基類的建構函式的方法與呼叫一般基類的建構函式的方法是不同的。由虛基類經過一次或多次派生出來的派生類,在其每一個派生類的建構函式的成員初始化列表中,都必須給出對虛基類的建構函式的呼叫;如果未列出,則呼叫虛基類的預設的建構函式。在這種情況下,在虛基類的定義中必須要有預設的建構函式。

可能還會有點理解不了,下面就結合例項分析一下:

在程式的D行,類D的建構函式儘管呼叫了其基類B和基類C的建構函式,但由於虛基類A在D中只有一個拷貝,所以編譯器也無法確定應該由B類的建構函式還是C類的建構函式來呼叫類A的建構函式。在這種情況下,編譯器約定,在執行類B和類C的建構函式時都不呼叫虛基類A的建構函式,而是在類D的建構函式中直接呼叫虛基類A的建構函式。但由於類D的建構函式中並沒有呼叫,所以就呼叫虛基類A的預設建構函式(也就是A行)。所以x的初值為0。

若將A類改成:

class A {
	public:
		int x;
		A(int a){
			x = a;
		}
};

重新編譯時,會出現錯誤。原因是:D行未給出虛基類A的建構函式,基類A有沒有預設的建構函式。

若將D類改成:

class D :public B, public C {
	public:
		int m;
		D(int a, int b, int d, int e, int f) :B(a, b), C(d, e), A(1000) {
			m = f;
		}
		void Print() {
			PB();
			PC();
			cout << m << endl;
		}
};

那麼,此時就不會呼叫基類A的預設建構函式,而是直接呼叫基類A的建構函式來進行1000的賦值。

最後再次強調:用虛基類進行多重派生時,若虛基類沒有預設的建構函式,則在派生的每一個派生類的建構函式的初始化成員列表中,必須有對虛基類建構函式的呼叫!