有些類與類之間存在特殊的關系,有共性也有特性,比如動物類可以細分為貓,狗等。下級別的成員除了擁有上一級的共性,還有自己的特性,這個時候就可以考慮繼承的技術,減少重復代碼。
一、繼承中的對象模型
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 子類與父類構造函數的原則
上面提到,子類可以繼承父類中所有的成員變量和成員方法,但不繼承父類的構造函數。因此在創建子類對象時,為了初始化從父類繼承來的成員變量,系統需要調用父類的構造方法。我們都知道,任何一個類都要有構造函數,那么子類的構造函數和父類的構造函數之間的關系是怎樣的?
-
如果子類沒有定義構造函數,則調用父類的無參構造函數;
-
如果子類定義了構造函數(不管是無參還是有參),在創建子類對象時首先執行父類的無參構造函數,然后執行自己的構造函數;
這兩種情況下,不管子類是否定義了新的構造函數,只要沒有顯示的調用父類中的構造函數,都只會調用父類中的無參構造函數。
-
如果父類中自己定義了有參構造函數,那么編譯器便不會再提供默認無參構造函數,子類便只能在構造函數中顯示的調用父類的構造函數來對父類成員進行初始化。
僅僅通過文字會比較抽象,下面我們通過幾個具體的例子來解釋一下上面的構造原則:
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"); // 如果此時創建對象,那么將會調用父類中的無參構造函數,然后調用子類的構造函數
輸出結果如下:
我是父類的有參構造函數 //顯示調用父類的有參構造函數
我是子類的構造函數 // 調用子類的構造函數
二、繼承中的構造和析構順序
在上面一節中也簡單提到了繼承過程中的構造順序,原則是:
- 父類先執行構造函數,子類然后執行構造函數
- 子類先執行析構函數,父類然后執行析構函數
下面通過一個簡單的例子來簡要說明一下:
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!