詳解C++中繼承的基本內容

ZhiboZhao 發表於 2021-07-06
C++

有些類與類之間存在特殊的關係,有共性也有特性,比如動物類可以細分為貓,狗等。下級別的成員除了擁有上一級的共性,還有自己的特性,這個時候就可以考慮繼承的技術,減少重複程式碼。

一、繼承中的物件模型

1.1 子類繼承父類中的成員變數

子類從父類繼承的成員變數,是屬於子類呢還是屬於父類呢?我們定義如下示例:

class father{
public:
	int f_a;
protected:
	int f_b;
private:
	int f_c;
};
class son : public father{	// 從son是father的子類
public:
	int s_a;
};
son S1;
cout << "son所佔的記憶體為:" << sizeof(S1) << endl;

輸出結果為:

son所佔的記憶體為:16

因此可以看出,父類中的所有變數都被子類給繼承了下來,都屬於子類的一部分。雖然父類中 private 訪問許可權的成員不能被子類訪問,但是仍然屬於子類的一部分。同理,在子類繼承父類時,除了繼承父類中所有的成員變數,也同時繼承了除了父類建構函式外的所有成員函式,這樣便可以有效節省程式碼量,提高程式碼複用效率。至於子類與父類建構函式之間的關係,將在後文進行解釋。

1.2 子類與父類建構函式的原則

上面提到,子類可以繼承父類中所有的成員變數和成員方法,但不繼承父類的建構函式。因此在建立子類物件時,為了初始化從父類繼承來的成員變數,系統需要呼叫父類的構造方法。我們都知道,任何一個類都要有建構函式,那麼子類的建構函式和父類的建構函式之間的關係是怎樣的?

  1. 如果子類沒有定義建構函式,則呼叫父類的無參建構函式;

  2. 如果子類定義了建構函式(不管是無參還是有參),在建立子類物件時首先執行父類的無參建構函式,然後執行自己的建構函式;

    這兩種情況下,不管子類是否定義了新的建構函式,只要沒有顯示的呼叫父類中的建構函式,都只會呼叫父類中的無參建構函式。

  3. 如果父類中自己定義了有參建構函式,那麼編譯器便不會再提供預設無參建構函式,子類便只能在建構函式中顯示的呼叫父類的建構函式來對父類成員進行初始化。

僅僅通過文字會比較抽象,下面我們通過幾個具體的例子來解釋一下上面的構造原則:

class father{
public:
	string f_a;
    // 與預設建構函式一樣,都是無參建構函式
	father(){
		cout << "我是父類的無參建構函式" << endl;
	}
    // 自定義的有參建構函式
    father(string f_a){
        this->f_a = f_a;
        cout << "我是父類的有參建構函式" << endl;
    }
};
class son : public father{
public:
	string f_a;
    // 子類中的建構函式,在沒有顯示呼叫父類建構函式的情況下,預設呼叫父類中的無參建構函式
	son(string f_a){
		cout << "我是子類的建構函式" << endl;
	}
};
son S1("abc"); // 如果此時建立物件,那麼將會呼叫父類中的無參建構函式,然後呼叫子類的建構函式

輸出結果如下:

我是父類的無參建構函式	//呼叫父類的無參建構函式
我是子類的建構函式	// 呼叫子類的建構函式

假如父類中只定義了有參建構函式,在建立子類物件時編譯器便找不到父類中的無參建構函式,於是便會報錯。解決這類問題的辦法就是在子類建構函式中顯示地呼叫父類的有參建構函式來對父類的成員初始化。

class father{
public:
	string f_a;
    // 自定義的有參建構函式
    father(string f_a){
        this->f_a = f_a;
        cout << "我是父類的有參建構函式" << endl;
    }
};
class son : public father{
public:
	string f_a;
    // 子類中的建構函式,由於父類中沒有提供無參建構函式,導致出錯
	//son(string f_a){
	//	cout << "我是子類的建構函式" << endl;
	//}
    son(string f_a):father("father"){	// 顯示呼叫父類中的有參建構函式
		cout << "我是子類的建構函式" << endl;
	}
};
son S1("abc"); // 如果此時建立物件,那麼將會呼叫父類中的無參建構函式,然後呼叫子類的建構函式

輸出結果如下:

我是父類的有參建構函式	//顯示呼叫父類的有參建構函式
我是子類的建構函式	// 呼叫子類的建構函式

二、繼承中的構造和析構順序

在上面一節中也簡單提到了繼承過程中的構造順序,原則是:

  1. 父類先執行建構函式,子類然後執行建構函式
  2. 子類先執行解構函式,父類然後執行解構函式

下面通過一個簡單的例子來簡要說明一下:

class father{
public:
	string f_a;
    father(){
        cout << "我是父類的建構函式" << endl;
    }
	~father(){
        cout << "我是父類的解構函式" << endl;
    }
};
class son : public father{
public:
	string f_a;
	son(){
        cout << "我是子類的建構函式" << endl;
    }
	~son(){
        cout << "我是子類的解構函式" << endl;
    }
};

輸出結果如下:

我是父類的建構函式
我是子類的建構函式
我是子類的解構函式
我是父類的解構函式

二、同名成員的處理

當子類與父類中出現同名的成員,如何通過子類物件訪問到子類或者父類中的同名資料呢?首先我們先理解一下為什麼子類和父類中會出現同名的成員變數和成員函式。這是因為變數和函式都有它的作用域,在同一個作用域中不能出現兩個重名的變數,但是在不同的作用域中可以出現重名。這就相當於每個學校都有一個校長,但是在同一所學校內只能有一個校長。由於父類和子類是兩個不同的作用域,所以可以出現重名的變數或者函式。

class father{
public:
	string f_a;
	int f_b;
	father(){	// 父類中的無參建構函式,初始化父類成員
		f_a = "father";
		f_b = 10;
	}
    void show(){
        cout << " 我是father!" << endl;
    }
};
class son : public father{
public:
	string f_a;
	son(){	// 子類中的無參建構函式,初始化子類成員
		f_a = "son";
	}
    void show(){
        cout << " 我是son!" << endl;
    }
};
son S1;
cout << "f_b = " << S1.f_b << endl;	// 子類物件可以像訪問自己的成員一樣訪問從父類繼承下來的成員
cout << "f_a = " << S1.f_a << endl; // 但是,從父類繼承了一個變數 f_a , 子類自己也有一個 f_a , 那麼訪問的是哪個呢?

輸出結果為:

f_b = 10
f_a = son	// 訪問的是子類中的成員

通過上述例項可以看出,當子類和父類中有同名的成員時,子類物件優先訪問子類中的成員,若想訪問父類中的成員,應該指定父類成員的作用域:

cout << "f_a = " << S1.father::f_a << endl; // 給 f_a 加上一個父類的作用域

輸出結果為:

f_a = father	// 訪問到了父類中的同名成員

對成員函式的訪問也是一樣,比如:

S1.show();	// 訪問子類中的show
S1.father::show();	// 訪問父類中的show

輸出結果為:

我是son!
我是father!