【C++ 筆記】繼承(Inheritance) - part 22

很感謝你點進來這篇文章。

你好,我並不是什麼 C++、程式語言的專家,所以本文若有些錯誤麻煩請各位鞭大力一點,我極需各位的指正及指導!!本系列文章的性質主要以詼諧的口吻,一派輕鬆的態度自學程式語言,如果你喜歡,麻煩留言說聲文章讚讚吧!

OOP 四大特性

首先讓我們複習一下 OOP(物件導向程式設計) 四大特性:

  1. 封裝(Encapsulation)
  2. 繼承(Inheritance)
  3. 多型(Polymorphism)
  4. 抽象(Abstraction)

封裝在前面我們已經有學過了,其實就是使用存取修飾子 private 或 protected 來限制資料成員的權限,不被其他程式碼控制。

封裝就跟他的名字一樣,一個被封裝的紙箱,箱子外的人看不見裡面的東西是啥,只有透過打開這個行為(方法)才能進一步看到箱子裡的東西,而這個行為就是限制了外人存取資料成員的權限。

:::success
封裝具體定義:將資料和運算這些資料的方法組合在一個類別(Class)中,並透過存取控制來限制對資料的直接存取。
:::

封裝主要有以下四種用途跟目的:

  1. 資料隱藏:防止外部「直接」存取物件的內部資料,保護資料的完整性和一致性。
  2. 介面與實現分離:外部只需透過公開的介面(方法)與物件互動,不必看到內部實現細節。
  3. 模組化(modularity):將功能封裝在類別中,使程式結構更清晰,易於維護和擴展。
  4. 提高安全性:透過控制存取權限,防止未授權的資料修改。

總之,封裝他主要做的事情就是「隱藏」,將資料隱藏,做安全性措施。實際上可以透過 const、static member、friend 去做控制。

繼承(Inheritance)

本篇要說明的 OOP 四大特性之二,就是繼承(Inheritance)。

:::info
定義:繼承是一種機制,允許一個類別(衍生類)基於另一個類別(基類)來定義,並繼承其非私有的成員(包括資料和方法)。
:::

衍生類(derived-class):從基類衍生出來的。

衍生類我們也可以稱之為「子類別」,而基類也可稱為「父類別」。

簡單來說,繼承的概念可從下例去做理解:

假設動物(Animal)是基類(最根本的類別),而動物可以往下分類,比如說人類也是一種動物,狗、貓都也是動物。

所以可以再細分如下:

Animal() -> Human()Dog()Cat()

Human()Dog()Cat() 本身都「繼承」了 Animal() 的特性。(因為都是動物嘛,既然都是動物,他們一定都會有共同的特徵。)

語法(Syntax)


1
2
3
4
class  derived_class_name : access-specifier  base_class_name
{
// body ....
};

derived_class_name:衍生類名稱。

access-specifier:存取修飾子。

base_class_name:基類名稱。

Note: A derived class doesn’t inherit access to private data members. However, it does inherit a full parent object, which contains any private members which that class declares.

by:https://www.geeksforgeeks.org/inheritance-in-c/

注意:衍生類別不會繼承對私有資料成員的存取權限。但它確實繼承了一個完整的父物件,其中包含該類別宣告的任何私有成員。

範例 1:繼承的使用方法


以下是個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

// 基類
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};

// 衍生類
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};

int main(void)
{
Rectangle Rect;

Rect.setWidth(5);
Rect.setHeight(7);

// 輸出物件的面積
cout << "Total area: " << Rect.getArea() << endl;

return 0;
}

來源:https://www.runoob.com/cplusplus/cpp-inheritance.html

輸出結果:

1
Total area: 35

以上範例中,基類被定義成 Shape,並設定資料成員 width、height,及兩個 methods:setWidth、setHeight。

而衍生類被定義為 Rectangle,繼承 Shape 的資料成員跟方法。

所以可以在主函數中看到,衍生類能夠使用 Shape 的方法去設定長寬。

繼承的存取控制

以下列表,表示各個型態的類別是否能夠從 public、protected、private 中進行存取。

存取類型publicprotectedprivate
同一個類別裡面yesyesyes
衍生類別yesyesno
類別的外面(外部類別)yesnono

表格來源:https://www.runoob.com/cplusplus/cpp-inheritance.html

一個衍生類別繼承了所有的基類別方法,但下列情況除外:

  • 基類別的建構子、解構子和複製建構子。
  • 基類別的重載運算子。
  • 基類別的 Friend Function。

範例 2:建構子、解構子在繼承的行為


衍生類的建構子會先自動呼叫基類的建構子,然後執行自己的建構子。

以下範例程式碼的輸出結果,就可看出這東西的先後順序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

using namespace std;

class Base {
public:
Base() { std::cout << "Base constructor\n"; }
virtual ~Base() { std::cout << "Base destructor\n"; }
};

class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor\n"; }
~Derived() { std::cout << "Derived destructor\n"; }
};

int main() {
Base* ptr = new Derived();
delete ptr;
return 0;
}

輸出結果:

1
2
3
4
Base constructor
Derived constructor
Derived destructor
Base destructor

繼承類型(存取修飾子)

我們可以特別指定要繼承 public 還是 private,又或是 protected。

通常在程式設計上普遍會使用 public。

:::info
如果繼承時沒特別指定存取修飾子,預設使用 private。
:::

以下資訊來自:https://www.runoob.com/cplusplus/cpp-inheritance.html

當使用不同類型的繼承時,遵循以下幾個規則:

  • 公有繼承(public):當一個類別衍生自「公有」基類別時,基類的「公有」成員也是衍生類別的「公有」成員,基類的「保護」成員(protected)也是衍生類的「保護」成員(protected),基類的「私有」成員(private)不能直接被衍生類存取,但是可以透過呼叫基類的「公有」和「保護」成員來存取。
  • 保護繼承(protected): 當一個類別衍生自保護基類別時,基底類別的公有保護成員將成為衍生類別的保護成員。
  • 私有繼承(private):當一個類別衍生自私有基底類別時,基底類別的公有保護成員將成為衍生類別的私有成員。

其實以上這些資訊可從下圖直接明瞭看出:

image

Image Source:https://www.geeksforgeeks.org/inheritance-in-c/

左欄藍色的為基類成員的存取修飾子,而黑色的為繼承模式。

這個表就是在說當繼承基類的時候,不同存取修飾子會有怎樣的行為。

當衍生類指定為 public 時,基類的 public、protected 都不會受繼承影響而改變,反倒是最後一個基類的 private 會被隱藏起來,不能直接存取。

而後衍生類的 protected,會將基類的 public 變成 protected;private 將首兩項全變成 private。

:::success
我自己是用權限大小來理解,依照這個表格來看就是(權限由小到大):public -> protected -> private。

public 因為權限最小,所以繼承時基類的 public 還是 public,基類的 protected 還是 protected。因為 private 權限最大嘛,所以不管你是外部還是繼承都不能偷看他的資料。

接下來就以此類推。
:::

範例 3:繼承模式


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
using namespace std;

// 基類
class Base {
public:
int publicVar = 1;
protected:
int protectedVar = 2;
private:
int privateVar = 3;
};

// 公有繼承
class PublicDerived : public Base {
public:
void show() {
cout << "Public Inheritance:" << endl;
cout << "publicVar = " << publicVar << endl; // 還是 public
cout << "protectedVar = " << protectedVar << endl; // 還是 protected
// cout << "privateVar = " << privateVar; // 無法存取
cout << endl;
}
};

// 保護繼承
class ProtectedDerived : protected Base {
public:
void show() {
cout << "Protected Inheritance:" << endl;
cout << "publicVar = " << publicVar << endl; // 變成 protected
cout << "protectedVar = " << protectedVar << endl; // 還是 protected
// cout << "privateVar = " << privateVar; // 無法存取
cout << endl;
}
};

// 私有繼承
class PrivateDerived : private Base {
public:
void show() {
cout << "Private Inheritance:" << endl;
cout << "publicVar = " << publicVar << endl; // 變成 private
cout << "protectedVar = " << protectedVar << endl; // 變成 private
// cout << "privateVar = " << privateVar; // 無法存取
cout << endl;
}
};

int main() {
PublicDerived pub;
ProtectedDerived prot;
PrivateDerived priv;

pub.show();
// cout << pub.publicVar; // 可直接存取 public 成員
// cout << pub.protectedVar; // 無法存取 (因為 protected)

prot.show();
// cout << prot.publicVar; // 無法存取 (因為變 protected)

priv.show();
// cout << priv.publicVar; // 無法存取 (因為變 private)

return 0;
}

輸出結果:

1
2
3
4
5
6
7
8
9
10
11
Public Inheritance:
publicVar = 1
protectedVar = 2

Protected Inheritance:
publicVar = 1
protectedVar = 2

Private Inheritance:
publicVar = 1
protectedVar = 2

多重繼承(Multiple inheritance)

語法:

1
2
3
4
5
6
7
8
9
10
11
12
class A
{
... .. ...
};
class B
{
... .. ...
};
class C: public A, public B
{
... ... ...
};

就是一個衍生類繼承自多個類別,如 class C,同時繼承了 A、B 類別。

範例 4:多重繼承


from:GeeksForGeeks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
using namespace std;

class A
{
public:
A() { cout << "A's constructor called" << endl; }
};

class B
{
public:
B() { cout << "B's constructor called" << endl; }
};

class C: public B, public A // Note the order
{
public:
C() { cout << "C's constructor called" << endl; }
};

int main()
{
C c;
return 0;
}

輸出結果:

1
2
3
B's constructor called
A's constructor called
C's constructor called

菱形問題(Diamond Problem)


範例 4 的多重繼承,很有可能會造成所謂的菱形問題。

以下是引用自 GeeksForGeeks 的一段話跟圖片:

The diamond problem occurs when two superclasses of a class have a common base class. For example, in the following diagram, the TA class gets two copies of all attributes of Person class, this causes ambiguities.

當一個類別的兩個超類別(層次較高的類別)有一個共同的基類時,就會出現菱形問題。例如,在下圖中,TA 類別獲得了 Person 類別的所有屬性的兩個副本,這會導致歧義。

image

翻譯一下:

:::info
Student、Faculty 都是 Person 的衍生類,而 TA 多重繼承自 Student、Faculty 兩個類別,但是這樣做會在 C++ 裡面發生一個問題叫菱形問題,也就是 TA 會擁有兩套 Person 實例資料的問題。因此實例化 TA 的時候,Person 的建構子會被呼叫兩次,然後解構子也會。
:::

為了要解決這個問題,所以可以透過一個關鍵字叫做 virtual 加在 Student、Faculty 類旁邊(因為被 TA 多重繼承),像是:

1
2
3
4
5
6
7
8
9
10
11
class Faculty : virtual public Person {
// ...
};

class Student : virtual public Person {
// ...
};

class TA : public Faculty, public Student {
// ...
};

加了關鍵字 virtual 的繼承也稱為虛繼承(or 虛擬繼承)。

範例 5:虛繼承


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;

class A {
public:
void display() {
cout << "Class A display" << endl;
}
};

class B : virtual public A { // 使用 virtual 繼承 A
public:
void showB() {
cout << "Class B show" << endl;
}
};

class C : virtual public A { // 使用 virtual 繼承 A
public:
void showC() {
cout << "Class C show" << endl;
}
};

class D : public B, public C { // D 同時繼承 B 和 C
public:
void showD() {
cout << "Class D show" << endl;
}
};

int main() {
D obj;
obj.display(); // 直接呼叫 A 的 display, 無歧義
obj.showB(); // 呼叫 B 的方法
obj.showC(); // 呼叫 C 的方法
obj.showD(); // 呼叫 D 的方法
return 0;
}

輸出結果:

1
2
3
4
Class A display
Class B show
Class C show
Class D show

範例 6:帶有資料成員的虛繼承


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;

class A {
protected:
int value;
public:
A(int v = 0) : value(v) {
cout << "A constructor, value = " << value << endl;
}
void setValue(int v) { value = v; }
int getValue() { return value; }
};

class B : virtual public A {
public:
B(int v = 0) : A(v) {
cout << "B constructor" << endl;
}
};

class C : virtual public A {
public:
C(int v = 0) : A(v) {
cout << "C constructor" << endl;
}
};

class D : public B, public C {
public:
D(int v = 0) : A(v), B(v), C(v) { // 明確呼叫 A 的建構子
cout << "D constructor" << endl;
}
};

int main() {
D obj(42);
cout << "Value from D: " << obj.getValue() << endl;
obj.setValue(100); // 修改共享的 value
cout << "Updated value from D: " << obj.getValue() << endl;
return 0;
}

輸出結果:

1
2
3
4
5
6
A constructor, value = 42
B constructor
C constructor
D constructor
Value from D: 42
Updated value from D: 100

多級繼承(Multilevel Inheritance)

多級繼承很容易和多重繼承搞混,所以作者設計下表讓觀念稍微清晰一些:

特性多級繼承(Multilevel Inheritance)多重繼承(Multiple Inheritance)
基類數量每個類別只有一個直接繼承的基類衍生類可有多個直接繼承的基類
結構垂直層次結構(如 A -> B -> C)平行結構(如 D 繼承 B 和 C)
關係像是「單一血統」的繼承關係像是「多重來源」的繼承關係
複雜性較易,無歧義問題較複雜,有菱形問題
應用動物 -> 哺乳動物 -> 狗學生同時繼承「人」和「學員身分」特性
是否虛繼承

GeeksForGeeks 給多級繼承舉了一個例子,如下:

1
Base class-> Wood, Intermediate class-> furniture, subclass-> table

基類 -> Wood(木頭), 間接類 -> furniture(家具), 子類 -> table(桌子)

image

Image Source:https://www.geeksforgeeks.org/cpp-multilevel-inheritance/

語法如下:

1
2
3
4
5
6
7
8
9
10
11
12
class A // base class
{
...........
};
class B : access_specifier A // derived class
{
...........
} ;
class C : access_specifier B // derived from derived class B
{
...........
} ;

範例 7:多級繼承


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class Father {
public:
void fatherMethod() {
cout << "Father method" << endl;
}
};

class Mother {
public:
void motherMethod() {
cout << "Mother method" << endl;
}
};

class Child : public Father, public Mother {
public:
void childMethod() {
cout << "Child method" << endl;
}
};

int main() {
Child obj;
obj.fatherMethod(); // 來自 Father
obj.motherMethod(); // 來自 Mother
obj.childMethod(); // 來自 Child
return 0;
}

輸出結果:

1
2
3
Father method
Mother method
Child method

總結

仍有其他的繼承模式,如:Hierarchical Inheritance(層次繼承)、Hybrid Inheritance(混合繼承),但這些都是基於前面的多重、多級繼承,而且概念類似,但礙於篇幅原因所以到此為止。

複習 OOP 四大特性:

  1. 封裝(Encapsulation)
  2. 繼承(Inheritance)
  3. 多型(Polymorphism)
  4. 抽象(Abstraction)

繼承定義:繼承是一種機制,允許一個類別(衍生類,Derived Class,或稱子類別)基於另一個類別(基類,Base Class,或稱父類別)定義,並繼承其非私有成員(包括資料和方法)。

如:Animal 是基類,Human、Dog、Cat 是衍生類,這些子類別繼承了 Animal 的共同特徵。

語法:

1
2
3
class DerivedClassName : access-specifier BaseClassName {
// contents
};
  • access-specifier:存取修飾子(public、protected、private)。
  • 注意:衍生類無法直接存取基類的 private 成員,但繼承了包含這些成員的完整物件。

存取權限表(來自菜鳥教程):

存取類型publicprotectedprivate
同一個類別裡面yesyesyes
衍生類別yesyesno
類別的外面(外部類別)yesnono

例外:基類的建構子、解構子、重載運算子和 friend 函數不會被繼承。

繼承模式表(from GeeksForGeeks):

image

多重繼承定義:一個衍生類同時繼承多個基類。

語法:

1
2
3
4
5
6
7
8
9
10
11
12
class A
{
... .. ...
};
class B
{
... .. ...
};
class C: public A, public B
{
... ... ...
};

多重繼承會遇到的菱形問題:當多個基類共享同一祖先類時,可能導致衍生類擁有該祖先的多份副本,造成歧義。

解決方案:虛繼承(virtual inheritance),使用 virtual 關鍵字讓祖先類只有一份實例。

多級繼承(把他想成是線性的就對了)定義:類別層次結構逐級繼承(如 A -> B -> C)。

多重繼承與多級繼承比較表:

特性多級繼承(Multilevel Inheritance)多重繼承(Multiple Inheritance)
基類數量每個類別只有一個直接繼承的基類衍生類可有多個直接繼承的基類
結構垂直層次結構(如 A -> B -> C)平行結構(如 D 繼承 B 和 C)
關係像是「單一血統」的繼承關係像是「多重來源」的繼承關係
複雜性較易,無歧義問題較複雜,有菱形問題
應用動物 -> 哺乳動物 -> 狗學生同時繼承「人」和「學員身分」特性
是否虛繼承

參考資料

C++ Multilevel Inheritance | GeeksforGeeks

Virtual inheritance - Wikipedia

superclass 並不超級 - Huan-Lin 學習筆記

Multiple Inheritance in C++ | GeeksforGeeks

Inheritance in C++ | GeeksforGeeks

C++ 继承 | 菜鸟教程