談到物件導向會提到三大基本概念:繼承(Inheritance)、封裝(Encapsulation)、多型(Polymorphism)。
本篇就稍微紀錄一下簡單的解釋,未來會依理解程度慢慢增加……
繼承(Inheritance)
繼承的概念就如同字面意思,把共同的東西繼承下來繼續用。如果拿很經典的動物例子舉例的話,每個動物幾乎都需要吃和睡覺,因此我們可以宣告一個類別叫做 Animal,狗這個動物是除了吃和睡覺以外還會吠叫,因此繼承 Animal 之外,還多了 bark 方法。Dog 既繼承了 Animal 的 eat() 和 sleep(),也有自己特有的 bark():
class Animal {
public:
void eat() {
cout << "Eating" << endl;
}
void sleep() {
cout << "Sleeping" << endl;
}
};
class Dog : public Animal {
public:
void bark() {
cout << "Barking" << endl;
}
};
這樣我在主程式就能夠這樣呼叫:
int main() {
Dog dog;
dog.eat();
dog.sleep();
dog.bark();
return 0;
}
封裝(Encapsulation)
將資料和操作這些資料的函數組合在同一個物件中,並隱藏不需要讓外界知道的實作細節與狀態,就是封裝的基本概念。白話一點就是整包拿去用啦,不用讓外界知道太多裡面是什麼。
若要說的精準一點,其中核心是隱藏物件內部的實作細節與狀態,限制外部直接操作,並透過明確的介面控制物件如何被使用。
在 C++ 中,我們可以透過存取控制限制類別成員的存取範圍,常見的存取控制修飾子包含:public、private 以及 protected。
目的不是一般意義上的安全性,而是:
- 保護物件狀態、隱藏實作細節
- 降低耦合,防止外部程式依賴內部實作
簡單來說就是要防止或保護資料被我們無意中破壞。
public
被宣告為 public 的成員變數或是函式,可以讓類別內部、衍生類別與外部程式碼皆可存取。
private
被宣告為 private 的成員變數或是函式,原則上只有類別本身的成員函式與 friend 可以存取。
不過了解後可能會產生一個疑問:如果宣告為 private 的成員無法從外部直接存取,那麼其他物件該如何使用它呢?不會失去物件導向的精神嗎?
物件導向的做法並不是開放外部直接操作內部資料,而是透過 public 方法提供明確的操作介面。如此一來,物件可以自行管理內部狀態,同時限制外部程式任意修改資料。
舉例來說,現在宣告一個名為「咖啡」的物件,將「咖啡豆種類」宣告為 private,再提供一個 public 的 make() 方法執行製作咖啡的動作。
make() 可以在物件內部使用咖啡豆種類以及其他資料完成咖啡的製作。外部程式只需要呼叫 make(),不需要知道內部使用了哪些資料或經過哪些處理,也不能任意修改物件的內部狀態。
這樣既能讓外部使用物件提供的功能,也能避免內部資料被任意修改。
protected
權限有點介於上方兩者之間。被宣告為 protected 的成員變數或是函式,類別本身與衍生類別可以存取,外部程式碼不可直接存取。
多型(Polymorphism)
讓不同型別的物件可以透過相同的介面使用,但實際執行的行為可以不同。
在 C++ 中,多型通常可以分為兩類:
- 編譯期多型(Compile-time Polymorphism)
- 執行期多型(Runtime Polymorphism)
要知道 polymorphism 如何實現,可以在進一步認識前先把問題放心裡:
- 編譯期多型是什麼
- 多載如何實現編譯期多型
- 執行期多型是什麼
- virtual function 和 overriding 如何實現執行期多型
多載(Overloading)
多載通常被歸類為編譯期多型。
簡單來說,就是可以使用相同的函式名稱,根據參數的型別、數量或順序,定義不同版本的函式。編譯器會在編譯期間決定要呼叫哪一個版本。
例如計算圖形面積時,圓形只需要知道半徑,一個參數;長方形需要知道長和寬,兩個參數。我們期望呼叫同一個方法執行不同定義:
- 圓形:半徑 * 半徑 * \(\pi\)
- 長方形:長 * 寬
例如,假設有一個名為 area() 的函式,可以用來計算不同圖形的面積:
double area(double radius) {
return radius * radius * 3.14159;
}
double area(double width, double height) {
return width * height;
}
呼叫時:
area(4.0);
area(2.0, 5.0);
area(4.0):會呼叫只有一個參數的版本,用來計算圓形面積。area(2.0, 5.0):會呼叫有兩個參數的版本,用來計算長方形面積。
這裡不是函式在執行時自行判斷參數數量,而是編譯器根據函式呼叫的參數,選擇符合的多載版本。
覆寫(Overriding)
覆寫是指衍生類別重新定義基底類別提供的虛擬函式。
例如:
class Animal {
public:
virtual void speak() const {
cout << "Animal sound" << endl;
}
};
class Dog : public Animal {
public:
void speak() const override {
cout << "Woof" << endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "Meow" << endl;
}
};
Dog 重新定義了 Animal 的 speak(),因此當實際物件是 Dog 時,可以執行 Dog 自己的版本。
覆寫本身是衍生類別重新定義函式的行為,而搭配虛擬函式、基底類別參考或指標使用時,就能形成執行期多型。在《[基礎概念] 虛擬函式 Virtual Function 是什麼東西?》有稍微提到,可以搭配服用。
執行期多型
執行期多型的重點,是可以透過相同的基底類別介面操作不同的衍生類別物件,並在執行時根據物件的實際型別決定要執行哪一個函式。
例如:
void makeAnimalSpeak(const Animal& animal) {
animal.speak();
}
接著可以傳入不同的動物:
Dog dog; Cat cat; makeAnimalSpeak(dog); makeAnimalSpeak(cat);
makeAnimalSpeak() 只知道自己接收到的是 Animal,不需要知道實際上是 Dog 還是 Cat。當它呼叫 speak() 時,程式會根據物件的實際型別執行對應的版本。
還記得這個章節開頭問的問題嗎?
多型不是多載和覆寫的簡單結合,而是一種透過統一介面處理不同型別物件的能力。多載是編譯期多型的一種形式;覆寫則常與虛擬函式搭配,用來實現執行期多型。







