C++基礎-繼承
本文為 C++ 學習筆記,參考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代碼大全》第 2 版。
繼承是一種復用,不同抽象層次的對象可以復用相同的特性。繼承通常用于說明一個類(派生類)是另一個類(基類)的特例。繼承的目的在于,通過“定義能為兩個或更多個派生類提供共有元素的基類”的方式寫出更精簡的代碼。
1. 繼承基礎
本節以公有繼承為例,說明繼承中的基礎知識。
日常生活中的繼承示例:
| 基類 | 派生類 |
|---|---|
| Fish(魚) | Goldfish(金魚)、 Carp(鯉魚)、 Tuna(金槍魚,金槍魚是一種魚) |
| Mammal(哺乳動物) | Human(人)、 Elephant(大象)、 Lion(獅子)、 Platypus(鴨嘴獸,鴨嘴獸是一種哺乳動物) |
| Bird(鳥) | Crow(烏鴉)、 Parrot(鸚鵡)、 Ostrich(鴕鳥)、 Platypus(鴨嘴獸,鴨嘴獸也是一種鳥) |
| Shape(形狀) | Circle(圓)、 Polygon(多邊形,多邊形是一種形狀) |
| Polygon(多邊形) | Triangle(三角形)、 Octagon(八角形,八角形是一種多邊形,而多邊形是一種形狀) |
1.1 繼承與派生
基類(比如魚類)派生出派生類(比如金槍魚類),派生類繼承基類。公有繼承中,派生類是基類的一種,比如,我們可以說,金槍魚是魚的一種。
閱讀介紹繼承的文獻時,“從…繼承而來(inherits from)”和“從…派生而來(derives from)”術語的含義相同。同樣,基類(base class)也被稱為超類(super class);從基類派生而來的類稱為派生類(derived class),也叫子類(sub class)。
1.2 構造函數的“繼承”與覆蓋
在 C++11 新標準中,派生類能夠重用其直接基類定義的構造函數,這些構造函數并不是通常意義上的繼承,它只是一種代碼復用,為方便起見,我們暫稱為“繼承”。一個類只初始化其直接基類,出于同樣的原因,一個類也只繼承其直接基類的構造函數。
派生類繼承直接基類的構造函數的方法是在派生類中使用 using 聲明語句,如下:
class Base
{
public:
Base() {}; // 1. 默認、拷貝、移動構造函數不能被繼承和覆蓋
Base(int a) {}; // 2. 被派生類中的構造函數覆蓋
Base(int a, int b) {}; // 3. 被派生類中的構造函數繼承
Base(int a, string b) {}; // 3. 被派生類中的構造函數繼承
};
class Derived: public Base
{
public:
using Base::Base; // 聲明繼承基類中的構造函數,若無此聲明是不繼承構造函數的
Derived(int a) {}; // 覆蓋基類中的構造函數
};
通常情況下,using 聲明只是令某個名字在當前作用域可見。而用于構造函數時,using 聲明語句將令編譯器生成代碼。
對于基類的每個可被繼承的構造函數,編譯器都在派生類中生成一個形參列表完全相當的構造函數。不過有兩種例外情況,第一種:如果派生類構造函數與基類構造函數參數表一樣,則相當于派生類構造函數覆蓋了基類構造函數,這種情況被覆蓋的基類構造函數無法被繼承;第二種:默認、拷貝、移動構造函數不會被繼承。根據這些規則,上例代碼由編譯器生成的派生類構造函數形式如下:
class Derived: public Base
{
public:
Derived(int a, int b) : Base(a, b) {};
Derived(int a, string b) : Base(a, b) {};
};
1.3 在派生類中調用基類構造函數
派生類調用基類構造函數有三種形式:
- 如果基類有默認構造函數,派生類構造函數會隱式調用基類默認構造函數,這由編譯器實現,不需編寫調用代碼;
- 如果基類沒有默認構造函數,即基類提供了重載的構造函數,則派生類構造函數通過初始化列表來調用基類構造函數,這屬于顯式調用。這種方式是必需的,否則編譯器會試圖調用基類默認構造函數,而基類并無默認構造函數,編譯會出錯;
- 在派生類構造函數中,使用
::Base()形式顯示調用基類構造函數。和基類普通函數的調用方式不同,派生類中調用基類普通函數的形式為Base::Function()(需要指定類名)。雖然這種方式和第 2 種方式實現功能基本一樣,但如果只使用這種方式而缺少第 2 種方式,編譯會出錯。這種方式似乎沒有什么意義。??此處出自哪里,怎么找不到??
如果基類包含重載的構造函數,需要在實例化時給它提供實參,創建派生類對象時如何實例化這樣的基類呢?這是上述三種形式中的第二種形式工,方法是使用初始化列表,并通過派生類的構造函數調用合適的基類構造函數。
class Base
{
public:
Base(int a) { m = a };
private:
int m;
};
class Derived: public Base
{
public:
Derived(): Base(25) {}; // 基類構造函數被調用一次,最終 Base::m 值為 25
Derived(): Base(25) { ::Base(36) }; // 基類構造函數被調用兩次,最終 Base::m 值為 25
Derived() { ::Base(36) }; // 編譯器試圖調用基類默認構造函數 Base::Base(),編譯出錯
};
1.4 構造順序與析構順序
Tuna 繼承自 Fish,則創建 Tuna 對象時的構造順序為:1. 先構造 Tuna 中的 Fish 部分;2. 再構造 Tuna 中的 Tuna 部分。實例化 Fish 部分和 Tuna 部分時,先實例化成員屬性,再調用構造函數。析構順序與構造順序相反。示例程序如下:
#include <iostream>
using namespace std;
class FishDummy1Member
{
public:
FishDummy1Member() { cout << "Fish.m_dummy1 constructor" << endl; }
~FishDummy1Member() { cout << "Fish.m_dummy1 destructor" << endl; }
};
class FishDummy2Member
{
public:
FishDummy2Member() { cout << "Fish.m_dummy2 constructor" << endl; }
~FishDummy2Member() { cout << "Fish.m_dummy2 destructor" << endl; }
};
class Fish
{
private:
FishDummy1Member m_dummy1;
FishDummy2Member m_dummy2;
public:
Fish() { cout << "Fish constructor" << endl; }
~Fish() { cout << "Fish destructor" << endl; }
};
class TunaDummyMember
{
public:
TunaDummyMember() { cout << "Tuna.m_dummy constructor" << endl; }
~TunaDummyMember() { cout << "Tuna.m_dummy destructor" << endl; }
};
class Tuna: public Fish
{
private:
TunaDummyMember m_dummy;
public:
Tuna() { cout << "Tuna constructor" << endl; }
~Tuna() { cout << "Tuna destructor" << endl; }
};
int main()
{
Tuna tuna;
}
為了幫助理解成員變量是如何被實例化和銷毀的,定義了兩個毫無用途的空類:FishDummyMember 和 TunaDummyMember。程序輸出如下:(//后不是打印內容,是說明語句)
Fish.m_dummy1 constructor // 基類數據成員1實例化
Fish.m_dummy2 constructor // 基類數據成員2實例化
Fish constructor // 基類構造函數
Tuna.m_dummy constructor // 派生類數據成員實例化
Tuna constructor // 派生類構造函數
Tuna destructor // 派生類析構函數
Tuna.m_dummy destructor // 派生類數據成員銷毀
Fish destructor // 基類析構函數
Fish.m_dummy2 destructor // 基類數據成員2銷毀
Fish.m_dummy1 destructor // 基類數據成員1銷毀
1.5 基類方法的覆蓋與隱藏
這里的覆蓋與隱藏,指的是基類中的方法在派生類對象中的可見性問題。
實現中方法的覆蓋與隱藏,參考注釋 1.1 1.2 1.3。
調用派生類中的覆蓋方法,參考注釋 2.1 2.2。
調用基類中被覆蓋的方法,參考注釋 3.1 3.2。
調用基類中被隱藏的方法,參考注釋 4.1 4.2 4.3。
調用基類中的其他方法,參考注釋 5.1 5.2。
#include <iostream>
using namespace std;
class Fish
{
private:
bool isFreshWaterFish;
public:
Fish(bool IsFreshWater) : isFreshWaterFish(IsFreshWater){}
void Swim() // 1.1 此方法被派生類中的方法覆蓋
{
if (isFreshWaterFish)
cout << "Fish::Swim() Fish swims in lake" << endl;
else
cout << "Fish::Swim() Fish swims in sea" << endl;
}
void Swim(bool freshWater) // 1.3 此方法被派生類中的方法隱藏,因為派生類中的覆蓋函數隱藏了基類中 Swim 的所有重載版本
{
if (freshWater)
cout << "Fish::Swim(bool) Fish swims in lake" << endl;
else
cout << "Fish::Swim(bool) Fish swims in sea" << endl;
}
void Fly()
{
cout << "Fish::Fly() Joke? A fish can fly?" << endl;
}
};
class Tuna: public Fish
{
public:
Tuna(): Fish(false) {}
// using Fish::Swim; // 4.2 解除對基類中所有 Swim() 重載版本的隱藏
void Swim() // 1.2 覆蓋基類中的方法
{
cout << "Tuna::Swim() Tuna swims real fast" << endl;
}
};
class Carp: public Fish
{
public:
Carp(): Fish(true) {}
void Swim() // 1.2 覆蓋基類中的方法
{
cout << "Carp::Swim() Carp swims real slow" << endl;
Fish::Swim(); // 3.2 在派生類中調用基類方法(繼承得到)
Fish::Fly(); // 5.2 在派生類中調用基類方法(繼承得到)
}
/*
void Swim(bool freshWater) // 4.3 覆蓋基類中 Swim(bool) 方法
{
Fish::Swim(freshWater); // 4.3 調用基類中被覆蓋的 Swim(bool) 方法
}
*/
};
int main()
{
Carp carp;
Tuna tuna;
carp.Swim(); // 2.1 調用派生類中的覆蓋方法
tuna.Swim(); // 2.2 調用派生類中的覆蓋方法
tuna.Fish::Swim(); // 3.1 調用基類中被覆蓋的方法
tuna.Fish::Swim(false); // 4.1 調用基類中被隱藏的方法
tuna.Fly(); // 5.1 調用基類中的其他方法(繼承得到)
return 0;
}
運行結果如下:
Carp::Swim() Carp swims real slow
Fish::Swim() Fish swims in lake
Fish::Fly() Joke? A fish can fly?
Tuna::Swim() Tuna swims real fast
Fish::Swim() Fish swims in sea
Fish::Swim(bool) Fish swims in sea
Fish::Fly() Joke? A fish can fly?
2. 訪問權限與類的繼承方式
訪問權限有三種:公有 (public)、保護 (protected) 和私有 (private),這三個關鍵字也稱訪問限定符。訪問限定符出現在兩種場合:一個是類的成員的訪問權限,類有公有成員、保護成員和私有成員;一個是類的繼承方式,繼承方式有公有繼承、保護繼承和私有繼承三種。
這兩種場合的訪問權限組合時,編譯器采用最嚴格的策略,確保派生類中繼承得到的基類成員具有最低的訪問權限。例如,基類的公有成員遇到私有繼承時,就變成派生類中的私有成員;基類的保護成員遇到公有繼承時,就變成派生類中的保護成員;基類的私有成員派生類不可見。
注意一點,基類的私有成員派生類不可見,但派生類對象里實際包含有基類的私有成員信息,只是它沒有權限訪問而已。參 3.1 節。
2.1 類成員訪問權限
類的成員有三種類型的訪問權限:
public: public 成員允許在類外部訪問。類外部訪問方式包括通過類的對象訪問,通過派生類的對象訪問以及在派生類內部訪問。
protected: protected 成員允許在類內部、派生類內部和友元類內部訪問,禁止在繼承層次結構外部訪問。
private: private 成員只能在類內部訪問。
類的內部包括類的聲明以及實現部分,類的外部包括對當前類的調用代碼以及其它類的聲明及實現代碼。
2.2 公有繼承
公有繼承的特點是基類的公有成員和保護成員作為派生類的成員時,它們都保持原來的狀態。基類的公有成員在派生類中也是公有成員,基類的保護成員在派生類中也是保護成員,基類的私有成員派生類不可見。
公有繼承用于“是一種(is-a)”的關系。is-a 表示派生類是基類的一種,比如金槍魚(派生類)是魚(基類)的一種。
2.3 私有繼承
私有繼承的特點是基類的公有成員和保護成員都變成派生類的私有成員?;惖乃接谐蓡T仍然為基類所私有,派生類不可見。
私有繼承使得只有派生類才能使用基類的屬性和方法,因此表示“有一部分”(has-a)關系。has-a 表示基類是派生類的一部分,派生類包含基類,比如發動機(基類)是汽車(派生類)的一部分。
2.4 保護繼承
保護繼承的特點是基類的公有成員和保護成員都變成派生類的保護成員。基類的私有成員仍然為基類所私有,派生類不可見。
與私有繼承類似,保護繼承也表示 has-a 關系。不同的是,基類的公有和保護成員變為派生類中的保護成員,能夠被派生類及派生類的子類訪問。
2.5 總結
下表中,表頭部分表示基類的三種成員,表格正文部分表示不同繼承方式下,對應的基類成員在派生類中的訪問權限。以表格第四行第二列為例,表示在私有繼承方式下,基類的 public 成員將成為派生類中的 private 成員。
| 基類成員 | public 成員 | protected 成員 | private 成員 |
|---|---|---|---|
| 共有繼承 | public 成員 | protected 成員 | 不可見 |
| 保護繼承 | protected 成員 | protected 成員 | 不可見 |
| 私有繼承 | private 成員 | private 成員 | 不可見 |
注意:慎用私有或保護繼承。對于大多數使用私有繼承的情形(如汽車和發動機之間的私有繼承),更好的選擇是,將基類對象作為派生類的一個成員屬性,使用組合,而不是繼承。
3. 基類對象與派生類對象的賦值關系
3.1 派生類對象與基類的關系
#include <iostream>
using namespace std;
class Base
{
private:
int x = 1;
int y = 2;
const static int z = 3;
};
class Derived : public Base
{
private:
int u = 11;
int v = 22;
const static int w = 33;
};
int main()
{
Base base;
Derived derived;
cout << "sizeof(Base) = " << sizeof(Base) << endl;
cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
return 0;
}
程序輸出為:
sizeof(Base) = 8
sizeof(Derived) = 16
類里的 static 成員屬于整個類,而不屬于某一個對象,不計入類的 sizeof。sizeof(類名) 等于 sizeof(對象名),因此 sizeof(Base) 值是 8。對于派生類 Derived,其 sizeof 運算結果為基類數據成員占用空間大小加上派生類數據成員占用空間大小,因此值為 16。
注意一點,派生類對象所在的內存空間里含有基類數據成員信息,包括基類私有數據成員,但派生類沒有權限訪問基類私有數據成員,編譯器在語法上不支持。
使用 gdb 調試程序,打印出基類對象和派生類對象的值,可得到如下信息:
(gdb) p base
$1 = {x = 1, y = 2, static z = 3}
(gdb) p derived
$2 = {<Base> = {x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}
3.2 切除問題
將派生類對象復制給基類對象有如下兩種情況:
第一種:通過賦值操作將派生類對象復制給基類對象
Derived objDerived;
Base objectBase = objDerived;
第二種:通過傳參方式將派生類對象復制給基類對象
void UseBase(Base input);
...
Derived objDerived;
UseBase(objDerived); // copy of objDerived will be sliced and sent
這兩種情況下,編譯器都是只復制派生類對象的基類部分,而不是復制整個對象。這種無意間裁減數據,導致 Derived 變成 Base 的行為稱為切除(slicing)。
要避免切除問題,不要按值傳遞參數,而應以指向基類的指針或 const 引用的方式傳遞。參《C++ 多態》筆記第 1 節。
3.3 賦值關系
如下三條關系的根本原因在 3.1 節中已講述。
派生類對象可以賦值給基類對象,反之則不行。
因為派生類對象數據成員比基類對象數據成員多。將派生類對象賦值給基類對象,基類對象能夠得到所有數據成員的值。反過來,將基類對象賦值給派生類對象,派生類對象中部分數據成員無法取得合適的值,因此賦值失敗。
派生類指針可以賦值給基類指針,反之則不行。
因為派生類指針所指向內存塊比基類指針所指向內存塊大?;愔羔樋梢灾赶蚺缮悓ο螅』惔笮〉膬却婕纯伞7催^來,派生類指針若指向基類對象,勢必會造成內存越界。
派生類對象可以賦值給基類引用,反之則不行。
因為派生類對象比基類對象空間大。將派生類對象賦值給基類引用,基類引用表示派生類對象中的基類部分,多余部分舍棄即可。反過來,顯然不行。
如下:
Base base;
Derived derived;
base = derived; // 正確
derived = base; // 錯誤
Base *pbase = &derived; // 正確
Derived *pderived = &base; // 錯誤
Base &rbase = derived; // 正確
Derived &rderived = base; // 錯誤
4. 多繼承
派生類繼承多個基類的特征稱作多繼承。如對鴨嘴獸來說。鴨嘴獸具備哺乳動物、鳥類和爬行動物的特征,那么鴨嘴獸可以繼承哺乳動物、鳥類和爬行動物這三個基類。代碼形如:
class Platypus: public Mammal, public Reptile, public Bird
{
// ... platypus members
};
5. 禁止繼承
從 C++11 起,編譯器支持限定符 final。被聲明為 final 的類不能用作基類,因此禁止繼承。
修改記錄
2019-05-20 V1.0 初稿
2020-02-29 V1.1 修改例程錯誤

浙公網安備 33010602011771號