【C++ 筆記】多型(Polymorphism) - part 23

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

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

OOP 四大特性

再次強調 OOP(物件導向程式設計)的四大特性:

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

多型(Polymorphism)

多型也稱為多態。

多型簡單來說,就是「同一個動作,因物件不同而產生不同結果」。想像我們現在有一個遙控器(介面),上面有一個「開啟」按鈕。當按下這個按鈕去控制不同設備時,會發生不同的事:

  • 對電視按「開啟」,電視會顯示畫面。
  • 對電風扇按「開啟」,風扇會開始轉動。
  • 對電燈按「開啟」,燈會亮起來。

同一個「開啟」指令(介面),因為控制的物件(電視、風扇、電燈)不同,產生不同的行為,這就是多型。

在程式設計中,多型可讓我們用統一的方式(像是遙控器的按鈕)去控制不同類型的物件,卻能得到各自其專屬的結果,就像「網站上的連結被點擊」後,YouTube 會播放影片、Netflix 會顯示影集頁面,而教育網站可能顯示課程資料。

:::info
多型的具體定義:

相同的操作介面,對於不同的資料型別或物件,能夠產生不同的行為表現。
:::

當類別之間存在層次結構,並且類別之間是透過繼承關聯時,就會使用到多態。
From 菜鳥教程

順便說說他的英文涵義:

The word polymorphism means having many forms.
polymorphism 這個單字的意思是有著多種形式。
From GeeksForGeeks

至於 C++ 的多型可以分為兩種:

  • Compile-time Polymorphism(編譯期多型)
  • Run-time Polymorphism(執行期多型)

image

Image Source:https://www.geeksforgeeks.org/cpp-polymorphism/

Compile-time Polymorphism


也被稱為早期繫結(early binding)、靜態多型(static polymorphism)。

In compile-time polymorphism, the compiler determines how the function or operator will work depending on the context. This type of polymorphism is achieved by function overloading or operator overloading.
From GeeksForGeeks

在編譯期多型中,編譯器會根據上下文來決定函數或運算子的行為方式。這種類型的多型是透過函數多載function overloading)或運算子多載operator overloading)來實現的。

Run-time Polymorphism


也被稱為晚期繫結(late binding)、動態多型(dynamic polymorphism)。

The function call in runtime polymorphism is resolved at runtime in contrast with compile time polymorphism, where the compiler determines which function call to bind at compilation. Runtime polymorphism is implemented using function overriding with virtual functions.
From GeeksForGeeks

與編譯期多型不同,執行期多型中的函數呼叫是在執行期間才被解析,而非在編譯期間由編譯器決定綁定哪一個函數呼叫。

執行期多型是透過使用虛擬函數virtual functions)來進行函數覆寫function overriding)實現的。

Compare with both


類型名稱決定時機技術效率
Compile-time Polymorphism編譯時多型編譯時函式多載(Overloading)、運算子多載(Operator Overloading)、模板(Templates)高(編譯時確定)
Run-time Polymorphism執行時多型執行時虛擬函式(Virtual Functions)與繼承低(需虛擬表 vtable 支援)

Compile-time Polymorphism

因為是「編譯期」多型,所以也稱靜態多型。

函數多載(Function Overloading)


這是 OOP 的特性,他可以有兩個或是更多的同名函數,但對於參數資料型態與數量而表現得有所不同。

啥意思?假設 add(1, 2)add(1.0, 4.4),裡面的兩參數要做相加,但是他們資料型態不同,所輸出的結果也當然不同,前者是 3,後者為 5.4

筆者寫了一個小例題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bits/stdc++.h>

using namespace std;

class display{
public:

void print(int x) {
cout << "整數:" << x << endl;
}

void print(string s) {
cout << "字串:" << s << endl;
}

};

int main(){
display a;
a.print(123);
a.print("this is 123");
return 0;
}

輸出結果:

1
2
整數:123
字串:this is 123

上面這個例子展示了同名函數,但是參數的資料型態不同,而有不同結果,如:輸入 123 就輸出一個整數型態的 123,輸入 “this is 123” 就輸出一個字串型態的 “this is 123”。

以下再舉一個當參數數量不同的例子:

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 Display{
public:
int area(int length, int width) {
return length * width;
}

int area(int length, int width, int height) {
return length * width * height;
}
};

int main() {
Display shape;
cout << "矩形面積: " << shape.area(10, 5) << endl;
cout << "長方體體積: " << shape.area(10, 5, 3) << endl;
return 0;
}

輸出結果:

1
2
矩形面積: 50
長方體體積: 150

可以看到 area 這個同名函數,有不同的參數數量,平面的矩形只要 長*寬,因此需要兩個參數,長方體需要 長*寬*高,所以需要三個參數,就這樣。

運算子多載(Operator Overloading)


可以自定義物件去使用 C++ 中的運算子。

C++ has the ability to provide the operators with a special meaning for particular data type, this ability is known as operator overloading.
From GeeksForGeeks

C++ 能夠為運算子提供針對特定資料型態的特殊意義,這叫做運算子多載。

而要多載一個運算子,具體格式如下:

1
類別名 operator"放要多載的運算子"(const 類別名&);

舉個例子:

1
myclass operator+(const myclass&);

以下是個完整的運算子多載範例:

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
#include <iostream>

using namespace std;

class Point {

public:

int x, y;
Point(int x, int y) : x(x), y(y) {}

Point operator+(const Point& other) {
return Point(x + other.x, y + other.y);
}
};

ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}

int main(){
Point p1(1, 2), p2(3, 4);
Point p3 = p1 + p2;
cout << p3 << endl;
return 0;
}

輸出結果:

1
(4, 6)

這支程式是在對兩點的 x y 進行相加。

而至於為什麼要加上:

1
2
3
4
ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}

這行程式碼,是因為 Point 類別沒有提供對 ostream 的支援,也就是說 C++ 標準輸出串流沒有定義 operator<< 對應 Point 類別,會讓 C++ 不知道怎麼輸出這個物件。因此需要我們自己撰寫一個全域函數來重載 operator<<

由於此 Point 類別是 public 的存取修飾詞,不然要在裡面加上 friend ostream& operator<<(ostream& os, const Point& pt); 讓函數去存取這些成員。

而要做 return os; 的動作是為了支援鏈式呼叫,讓每次 operator<< 都回傳 ostream&,讓下一個 << 能接續呼叫。如:cout << a << b << c;

更完善範例:加入其他可多載的運算子

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
#include <iostream>
using namespace std;

class Point {
private:
int x, y;

public:
Point(int x = 0, int y = 0) : x(x), y(y) {}

// 運算子 + 多載:加總兩點座標
Point operator+(const Point& other) const {
return Point(x + other.x, y + other.y);
}

// 運算子 == 多載:比較兩點是否相同
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}

int getX() const { return x; }
int getY() const { return y; }

friend ostream& operator<<(ostream& os, const Point& p);
};

ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}

int main() {
Point p1(2, 3);
Point p2(4, 5);

Point p3 = p1 + p2;
cout << "p1 + p2 = " << p3 << endl;

if (p1 == p2)
cout << "p1 與 p2 相等" << endl;
else
cout << "p1 與 p2 不相等" << endl;

return 0;
}

另外提供一個範例,較為直覺一點(兩個體積相加):

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
67
68
69
70
71
72
73
74
#include <iostream>
using namespace std;

class Box
{
public:

double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}

void setBreadth( double bre )
{
breadth = bre;
}

void setHeight( double hei )
{
height = hei;
}
// 多載 + 運算子,用於把兩個 Box 物件相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 長度
double breadth; // 寬度
double height; // 高度
};
// 程式的主函數
int main( )
{
Box Box1; // 宣告 Box1,型態为 Box
Box Box2; // 宣告 Box2,型態为 Box
Box Box3; // 宣告 Box3,型態为 Box
double volume = 0.0; // 把體積儲存在該變數中

// Box1 詳述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);

// Box2 詳述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);

// Box1 的體積
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;

// Box2 的體積
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;

// 把兩個物件相加,得到 Box3
Box3 = Box1 + Box2;

// Box3 的體積
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;

return 0;
}

來源:菜鳥教程

可多載與不可多載的運算子

可多載的運算子:

運算子之係屬運算子
二元算術運算子+ - * /
關係運算子== != < > <= >=
邏輯運算符&& !
一元運算子+(正) -(負) *(指標) &(取位址)
遞增、遞減運算子++ --
位元運算子& ~ ^ << >>
指定運算子= += -= *= /= %= &= ^= <<= >>=
記憶體空間新增與釋放new delete new[] delete[]
其他運算子()(函數呼叫) ->(箭頭運算子) ,(逗號) [](索引運算子)

註:可多載的運算子還有邏輯運算子的 ||(OR)、位元運算子的 | (OR 位元運算子),跟指定運算子 |=

以下是不可多載的運算子:

  • . (成員存取運算子)
  • .* ->*(成員指標存取運算子)
  • :: (範圍解析運算子)
  • sizeof
  • ? :(三元運算子)
  • #(預處理符號)

來源:菜鳥教程

Run-time Polymorphism

因為是「執行期」多型,所以也稱為動態多型。

Virtual Functions


達成執行期多型的必備條件:

  1. 使用繼承(Inheritance)
  2. 基類別(Base Class)中的函數必須使用 virtual 關鍵字
  3. 透過指標或參考呼叫函數

在這邊要先解釋一下虛函數和純虛函數:

:::info
虛函數(Virtual Functions):

  • 在基類別中宣告一個函數為虛擬函數,使用關鍵字 virtual。
  • 衍生類別可以覆寫(override)這個虛擬函數。
  • 呼叫虛函數時,會根據物件的實際型態來決定呼叫哪個版本的函數。
    :::

至於上面的第三項,他的意思是當我們用一個指標或參考來呼叫函數時,系統會根據「這個指標實際指向的物件是哪一種型態」來決定執行哪個版本的函數,而不是根據變數表面上的型態。

做一個比喻:

假設你有一張「動物」身分證(Animal 為基類),上面寫著這是一隻動物。但其實這張身分證是借給一隻狗(Dog 是衍生類別)用的。

讓這隻「動物」叫一聲時(呼叫 speak() 函數),如果這個函數是虛函數(virtual),那牠會發出狗的叫聲(Dog 的 speak()),因為牠實際上是狗,不是真的只有「動物」那麼簡單。

:::info
純虛函數(Pure Virtual Functions):

  • 一個包含純虛函數的類別被稱為抽象類別(Abstract Class),它不能被直接實例化。
  • 純虛函數沒有函數體,宣告時使用 = 0。
  • 它強制衍生類別提供具體的實作。
    :::

來源:菜鳥教程

純虛函數宣告例子:

1
2
3
4
class Shape {
public:
virtual void draw() = 0; // 純虛擬函式
};

Function Overriding


至於虛函數的部分講到了 override 的部分,這也是在執行期多型時很重要的概念:

:::info
C++ 的函數覆寫(function overriding)僅能在衍生類別(derived class)中使用,是為了重新定義基類別中的虛函數,以實現多型(polymorphism)。
:::

在 function overriding 的過程中,需要具備以下三項基本條件:

  1. 基類別中的函數必須是 virtual 函數。
  2. 衍生類別中定義的函數必須具有相同的函數簽章(signature)。
  3. (於 C++11 之後)加上 override 關鍵字來明確表示此為覆寫行為。

至於函數簽章是啥?

:::info
函數簽章(function signature),又名為函數原型(function prototype),在 C++ 中,是對函數的一種宣告,用來告訴編譯器該函數的名稱、回傳型態、參數型態(與順序),但不包含函數體內部的內容。
:::

語法形式:data_type function_name(parameters, ...)

其實就是以下這樣(宣告一個函數而已,並未進行明確定義):

1
2
3
4
5
#include <iostream>

using namespace std;

int add(a, b); // 函數原型

Run-time Polymorphism 的例題 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
#include <bits/stdc++.h>
using namespace std;

class Base {
public:
// 虛擬函數
// Virtual function
virtual void display() {
cout << "Base class function";
}
};

class Derived : public Base {
public:
// 覆寫基類別的函數
// Overriding the base class function
void display() override {
cout << "Derived class function";
}
};

int main() {
// 這邊 basePtr 是一個指向 Base 類別的指標變數
// 建立一個 Base 型態的指標
// Creating a pointer of type Base
Base* basePtr;

// 建立一個衍生類別的物件
// Creating an object of Derived class
Derived derivedObj;

// 將 Base 類別指標指向衍生類別物件
// Pointing base class pointer to
// derived class object
basePtr = &derivedObj;

// 用 Base 類別指標去呼叫 display 函數
// Calling the display function
// using base class pointer
basePtr->display(); // 但這邊會呼叫到 Derived 裡面的 display 函數
return 0;
}

輸出結果:

1
Derived class function

From:GeeksForGeeks

在 Base 類別中定義了一個虛擬函數 display(),然後他會在 Derived 類別(繼承自 Base)中被 override。(符合執行期多型的前兩個必備條件)

接下來在主函數這邊,最後用到指標去呼叫函數,也符合條件,所以這是一個執行期多型(呼叫的函數會根據實際物件的型態,而不是指標的型態來決定最終執行哪個版本的函數)。

而在這邊的 basePtr 就是上面講虛函數時比喻的動物,但隨著他指向的物件不同,在呼叫一個虛函數(假設 virtual speak())時,C++ 會在執行期去查這隻「動物」實際是誰,然後執行「狗的版本」(衍生類)的 speak()

Run-time Polymorphism 的例題 2


簡單的小小範例:

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 Animal {
public:
// 虛擬函數:允許衍生類別 override
virtual void makeSound() {
cout << "某種動物的叫聲" << endl;
}
};

class Dog : public Animal {
public:
void makeSound() override { // 覆寫 makeSound() 函數
cout << "汪!汪!" << endl;
}
};

class Cat : public Animal {
public:
void makeSound() override {
cout << "喵~" << endl;
}
};

int main() {
Animal* animal; // 宣告一個基類別指標

Dog myDog;
Cat myCat;

animal = &myDog; // animal 指標指向 Dog 物件
animal->makeSound(); // 汪!汪!

animal = &myCat; // animal 指標指向 Cat 物件
animal->makeSound(); // 喵~

return 0;
}

輸出結果:

1
2
汪!汪!
喵~

總結

多型(Polymorphism)概述


  • 定義:同一操作介面,對不同物件或資料型態產生不同行為。「多型」意為「多種形式」。
  • 比喻:如同有一個遙控器「開啟」按鈕,對不同設備(電視、風扇、電燈)執行不同動作。
  • 應用:透過統一介面控制不同物件,實現其專屬行為(如網站連結點擊後的不同反應)。
  • 前提:通常需有繼承關係的類別層次結構。

多型分類


在 C++ 中,多型分為兩種:

  1. 編譯期多型(Compile-time Polymorphism):靜態多型,編譯時決定行為。
  2. 執行期多型(Run-time Polymorphism):動態多型,執行時決定行為。

編譯期多型(Compile-time Polymorphism)


  • 別名:早期繫結(early binding)、靜態多型。
  • 實現方式
    • 函數多載(Function Overloading):同名函數根據參數型態或數量不同,執行不同行為。
    • 運算子多載(Operator Overloading):自定義運算子行為。
  • 特點:編譯時確定,效率高。

執行期多型(Run-time Polymorphism)


  • 別名:晚期繫結(late binding)、動態多型。
  • 實現方式
    • 虛擬函數(Virtual Functions):基類中使用 virtual 關鍵字,衍生類別覆寫(override)。
      • 條件
        1. 使用繼承。
        2. 基類函數為虛擬函數。
        3. 透過指標或參考呼叫。
      • 純虛函數:宣告為 virtual void func() = 0,函數體內無內容,衍生類別必須實作內容,包含純虛函數的類別為抽象類別。
    • 函數覆寫(Function Overriding):衍生類重新定義基類虛函數,需相同函數簽章(名稱、回傳型態、參數)。
      • C++11 後可用 override 關鍵字明確標示。
  • 特點:執行時需透過虛擬表(vtable),效率較低但靈活。

編譯期與執行期多型比較


類型名稱決定時機技術效率
Compile-time編譯期多型編譯時函數多載、運算子多載、模板高(靜態確定)
Run-time執行期多型執行時虛擬函數、函數覆寫低(需 vtable)

參考資料

Operator Overloading in C++ | GeeksforGeeks

Function Overloading in C++ | GeeksforGeeks

Functions in C++ | GeeksforGeeks

C++ Polymorphism | GeeksforGeeks

Function Overriding in C++ | GeeksforGeeks

TypeScript 初學者也能看的學習指南 09 - Function Overloads 函式重載 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

C++ 多态 | 菜鸟教程

C++ 重载运算符和重载函数 | 菜鸟教程