# 物件導向程式設計:補充教材
本文件補充一些 OOP 課程可能沒提及,或是不夠深入的教學內容
## Smart Pointer
> https://en.cppreference.com/w/cpp/header/memory
Smart Pointer(智慧指標)是 C++11 引入的 RAII 工具,用來自動管理動態記憶體的生命週期。相較於傳統裸指標,智慧指標會在物件離開作用域時自動釋放資源,避免記憶體洩漏與懸垂指標。常見類型包括 std::unique_ptr(唯一所有權)、std::shared_ptr(共享所有權)與 std::weak_ptr(非擁有式引用),能安全、彈性地處理各種資源擁有關係。
RAII(Resource Acquisition Is Initialization)資源獲取即初始化
> 資源的有效期與持有資源的對象的生命期嚴格繫結,即由對象的建構函式完成資源的分配(取得),同時由解構函式完成資源的釋放。在這種要求下,只要對象能正確地解構,就不會出現資源洩漏問題。
```cpp
#include <cstdio>
class FileGuard {
public:
FileGuard(const char* filename, const char* mode) {
file = std::fopen(filename, mode);
if (!file) {
throw std::runtime_error("Failed to open file");
}
}
~FileGuard() {
if (file) {
std::fclose(file);
}
}
FILE* get() const { return file; }
private:
FILE* file;
};
int main() {
try {
FileGuard fg("example.txt", "w");
std::fprintf(fg.get(), "Hello RAII!\n");
// 自動 fclose(),即使發生例外也保證釋放
} catch (const std::exception& e) {
std::fprintf(stderr, "Error: %s\n", e.what());
}
}
```
那接下來,我們使用 `Example`,用以觀察物件的建立與解構行為:
```cpp
class Example {
public:
int data;
Example(int _data) : data(_data) {}
~Example() { std::cout << "Destructure this->data: " << data << '\n'; }
};
```
透過解構函式的輸出,可以清楚觀察每個物件被銷毀的時機。
```cpp
std::cout << "Scope 1:" << '\n';
{
// scope 1, scalar-type
int *ptr1 = new int(100);
std::shared_ptr<int> ptr2(new int(200));
// if you want declare array
std::shared_ptr<int[]> ptr3(new int[100]);
std::cout << '\t' << "Ptr1 = " << *ptr1 << '\n';
std::cout << '\t' << "Ptr2 = " << *ptr2 << '\n';
}
```
```
Scope 1:
Ptr1 = 100
Ptr2 = 200
```
- `ptr1` 使用裸指標管理記憶體,需手動釋放,存在記憶體洩漏風險。
- `ptr2` 是安全的 `shared_ptr`,會在離開作用域時自動釋放。
- `ptr3` 表示陣列版本的 `shared_ptr`,需使用 `shared_ptr<T[]>` 的語法。
```cpp
std::cout << "Scope 2:" << '\n';
{
// scope 2, Object Type
Example *ptr1 = new Example(300);
auto ptr2 = std::make_shared<Example>(400);
auto ptr3 = ptr2;
std::cout << '\t' << "Ptr1->data = " << ptr1->data << '\n';
std::cout << '\t' << "Ptr2->data = " << ptr2->data << '\n';
std::cout << '\t' << "Ptr3->data = " << ptr3->data << '\n';
// They use same reference.
std::cout << '\t' << "Ptr2 reference conunt = "
<< ptr2.use_count() << '\n';
std::cout << '\t' << "Ptr3 reference conunt = "
<< ptr3.use_count() << '\n';
}
```
```
Scope 2:
Ptr1->data = 300
Ptr2->data = 400
Ptr3->data = 400
Ptr2 reference conunt = 2
Ptr3 reference conunt = 2
```
- `ptr2` 與 `ptr3` 共用資源,其參考計數為 2。
- `shared_ptr` 透過內部控制區塊追蹤引用數,最後一個離開作用域的指標會觸發 `delete`。
```cpp
std::cout << "Scope 3:" << '\n';
{
// scope 3, Unique Pointer
auto uptr = std::make_unique<Example>(500);
// NOT Allowed, because unique pointer only allow one owner
// auto uptr2 = uptr;
std::cout << '\t' << "uptr->data = " << uptr->data << '\n';
}
```
```
Scope 3:
uptr->data = 500
Destructure this->data: 500
```
- `unique_ptr` 僅允許單一所有者,不能複製。
- 進出 scope 時會自動釋放所管理的資源。
```cpp
std::cout << "Scope 4:" << '\n';
{
// scope 4, Move unique Pointer
auto uptr = std::make_unique<Example>(500);
// Allowed, Use std::move to explicitly relinquish
// ownership of the object.
auto uptr2 = std::move(uptr);
/**
* The following behavior is undefined because uptr has already been
* transferred
*/
// std::cout << "uptr->data = " << uptr->data << '\n';
std::cout << '\t' << "uptr->data = " << uptr2->data << '\n';
}
}
```
```
Scope 4:
uptr->data = 500
Destructure this->data: 500
```
- 使用 `std::move()` 可將擁有權從一個 `unique_ptr` 轉移至另一個。
- 移動後原指標為空(null),再訪問會造成未定義行為。
```cpp
auto uptr1 = std::make_unique<Example>(600);
auto f = [m = std::move(uptr1)]() {
std::cout << "At Lambda, m->data = " << m->data << '\n';
};
f();
// NOT Allow, uptr1 move to lambda capture
// std::cout << "uptr->data = " << uptr1->data << '\n';
```
```
At Lambda, m->data = 600
Destructure this->data: 600
```
- 此為 C++14 起的初始化捕捉語法。
- 透過 `std::move()` 可將 `unique_ptr` 移入 lambda 內部,並安全釋放於 lambda 結束時。
C++ 的 Smart Pointer 提供了自動化的資源管理機制,透過不同型別的智慧指標,可以根據所有權與用途選擇最適合的管理策略。
| 智慧指標類型 | 所有權模式 | 可複製? | 釋放時機 |
| ---------------------- | ------- | --------------- | -------------- |
| `std::unique_ptr<T>` | 單一所有者 | 不可複製,可移動 | 離開 scope 時自動刪除 |
| `std::shared_ptr<T>` | 多個共用者 | 可複製 | 參考計數為 0 時自動釋放 |
| `std::weak_ptr<T>` | 非擁有式觀察者 | 可複製| 不控制資源生命週期|
| 建立方式 | 說明 |
| ------------------------------ | -------------------------------------- |
| `std::make_unique<T>(args...)` | 建立並初始化 `unique_ptr`,具最佳效能與安全性(C++14 起) |
| `std::make_shared<T>(args...)` | 建立共享控制區與物件,減少配置次數 |
| `std::shared_ptr<T>(new T)` | 可行但不建議:兩次配置,易混淆所有權 |
>[!Note]
> `make_shared<T[]>` 是不支援的語法
> 這是因為 make_shared 需要知道陣列長度來配置空間,但 `T[]` 是不完整型別
> ```cpp
> std::make_shared<int[]>(100); // Compile Failed
> std::shared_ptr<int[]> ptr(new int[100]); // Allow
> ```
## lambda & Closure (閉包)
> https://en.cppreference.com/w/cpp/language/lambda
Lambda 是 C++11 引入的一種匿名函式語法,用來就地定義一個可呼叫的函式物件,語法簡潔、可捕捉周圍變數。
```cpp
[capture](parameters) -> return_type {
body
}
```
| 語法單元 | 意義 |
| - | - |
| `[capture]` | 指定要捕捉哪些外部變數(by value 或 by reference)|
| `(parameters)` | 和一般函式一樣的參數清單 |
| `->return_type` | 可選,指定回傳型別(若不指定,使用 auto 推導)
| `{ body }` |
```cpp
int a = 10;
auto add = [a](int x) { return a + x; };
std::cout << add(5); // 輸出 15
```
### Capture
透過 capture clause(`[]` 中的語法)將變數「捕捉」進 Lambda 內部。捕捉的方式可以是 值捕捉(by value) 或 參考捕捉(by reference)。
| Capture | 行為 |
| --------- | ----------------------- |
| `[x]` | **值捕捉**變數 `x`(複製一份到內部) |
| `[&x]` | **參考捕捉**變數 `x`(引用原本的變數) |
| `[=]` | 對所有使用到的變數 **值捕捉** |
| `[&]` | 對所有使用到的變數 **參考捕捉** |
| `[=, &x]` | 預設值捕捉,例外對 `x` 使用參考捕捉 |
| `[&, x]` | 預設參考捕捉,例外對 `x` 使用值捕捉 |
```cpp
int a = 10;
auto f = [a]() { std::cout << a << "\n"; };
a = 20;
f(); // 10
```
```cpp
int a = 10;
auto f = [&a]() { std::cout << a << "\n"; };
a = 20;
f(); // 20, type of a is reference
```
```cpp
class MyClass {
public:
int x = 42;
void print() {
// capture `this` pointer
auto f = [this]() { std::cout << x << "\n"; };
f(); // 42
}
};
```
```cpp
int y = 10;
// initial capture value
auto f = [z = y + 1]() {
std::cout << z << "\n";
};
f();
```
> https://en.wikipedia.org/wiki/Closure_(computer_programming)
閉包則是從Lambda 延伸出來的一個概念:
```
閉包是指一個函式與其捕捉到的外部環境綁定所形成的物件。
在 C++ 中,lambda 表達式建立的實體就是閉包物件。
```
以下是一個經典的 closure(閉包)示範:
```cpp
#include <iostream>
#include <functional>
void invoke(std::function<void()> f){
// f 的定義存在於 `invoke` 的參數中
// 此時 a 應該不可見,但仍然可以正確運作
f(); // 30
}
int main() {
int a = 30;
auto f = [&a](){
std::cout << a << '\n';
};
invoke(f);
}
```
另一個經典的 closure(閉包):
```cpp=
#include <iostream>
#include <functional>
std::function<int()> closure(){
static int data = 12345;
return [&data](){
return data;
};
}
int main() {
auto f = closure();
std::cout << f(); //12345
}
```
在本範例中,`data` 在函式內部具有靜態儲存期(static)。我們透過一個 Lambda 捕捉該值,並且回傳。在 `main` 中,調用了 `closure`,因此取得了 `data` 的內容。
對於 `main` 來說, `closure` 內部宣告的變數是不可見的,但是由於 `closure` 提供了一組對外存取的函式,使 `main` 可以訪問到變數 `data`。在本例子中,`data` 猶如一個私有成員,`closure` 回傳的 lambda 就像個公開成員方法(method)。
請注意,本範例中由於 `data` 被 static 修飾,使其在函式結束後不會被回收。在腳本語言(如 Python, PHP, JavaScript 等),變數通常會有參考計數,使GC得以正確回收;但在靜態語言中,若此變數不是 static,當函式結束後就會被回收,導致 UB (**Undefined Behavior**)
## std::bind
> https://en.cppreference.com/w/cpp/utility/functional/bind
> Defined in `<functional>` Header
std::bind 是 C++11 引入的工具,用來產生一個新的函式物件,將一個已有的函式、成員函式或可呼叫物件,**部分綁定(partial binding)** 某些參數,延後剩餘參數的傳遞時機。
```
#include <functional>
auto bound = std::bind(callable, arg1, arg2, ...);
```
範例
```cpp
int add(int a, int b) {
return a + b;
}
auto add5 = std::bind(add, 5, std::placeholders::_1);
std::cout << add5(10); // 15
```
`add5` 是一個接受參數的函式物件,第一個參數綁定5,第二個參數是未定的(`std::placeholders`),分別用 `_1` , `_2` ... `_20` 表示
由於 add 接受兩個參數,在綁定後已經確認一個參數,因此 add5 只需要在一個參數即可。
```cpp
#include <iostream>
#include <functional>
double devide(double a, double b) {
return a / b;
}
int main(){
using namespace std::placeholders;
auto div1 = std::bind(devide, _1, _2);
auto div2 = std::bind(devide, _2, _1);
std::cout << div1(10. , 20.); // 0.5
std::cout << div2(10. , 20.); // 2.0
}
```
該範例則展示了 `placeholders` 的順序,將會影響函數的方法。
由於不支援型別推導,所以不如 lambda 彈性。實際上幾乎可以被 Lambda 取代,但是 `std::bind` 可以協助觀察 class 的行為:
```cpp
#include <iostream>
#include <functional>
class Point {
public:
int x, y;
Point(int x, int y) : x(x), y(y) {}
// Point::print(Point* this) const
void print() const {
std::cout << "(" << x << ", " << y << ")\n";
}
};
int main() {
Point p1(3, 4);
Point p2(40, 50);
auto bound = std::bind(&Point::print, std::placeholders::_1);
bound(p1); // (3, 4)
bound(p2); // (40, 50)
}
```
這案例模擬了如何實作類別,成員函式不像自由函式,它需要知道是誰的成員函式,也就是需要一個 this 指標。
在一些 C 的函式庫中,模擬OOP的作法就是提供 `CLASS` 以及 `METHOD` 巨集,把某個物件的指標綁定到Function上,展示了 OOP 是一種「開發的範式,而非程式語言特別的實作」。
## std::function
> https://en.cppreference.com/w/cpp/utility/functional/function
> Defined in `<functional>` Header
```cpp
template<class R, class... Args>
class std::function<R(Args...)>;
```
std::function 是一個泛型的可呼叫物件包裝器,可以儲存任何能像函式一樣呼叫的東西,包括:
- 一般函式(function pointer)
- lambda 表達式(含閉包)
- 仿函式(functor)
- 經 std::bind 處理後的物件
```cpp
#include <functional>
#include <iostream>
void say_hello() {
std::cout << "Hello!\n";
}
int main() {
std::function<void()> f = say_hello;
f(); // Hello!
int a = 100;
std::function<void()> g = [a]() {
std::cout << a << '\n';
};
g(); // 100
}
```
## Functor 仿函式
前面不斷提到該名詞,其意義是:
```
可以通過 operator() 進行調用的物件
```
有很多不同的說法,比方說 `Functor`, `Callable`, `Function Object`
它是一種物件化的函式,可以封裝狀態、擁有生命周期,並可與 STL、lambda 互補。
```cpp
#include <iostream>
struct Adder {
int base;
Adder(int b) : base(b) {}
int operator()(int x) const {
return base + x;
}
};
int main() {
Adder add5(5); // Functor
std::cout << add5(3) << '\n';
}
```
| 特性 | 函式指標 | Lambda | 仿函式(Functor) |
| ---------- | ---- | ------ | ------------ |
| 可以封裝狀態 | ☓ | ✓ | ✓ |
| 型別可推導 | ☓ | ✓ | ☓(需完整型別) |
| 可重複使用與擴充 | ☓ | 部分 | ✓(可繼承) |
| 編譯期可內聯 | ☓ | ✓ | ✓ |
| 可讀性與簡潔性 | ☓ | ✓ | 中等 |
| 可搭配 STL 使用 | ☓ | ✓ | ✓ |
其實 Lambda 就是仿函式的語法糖
```cpp
auto f = [base = 5](int x) { return base + x; };
```
等價於
```cpp
struct __Lambda {
int base;
int operator()(int x) const {
return base + x;
}
};
__Lambda f{5};
```
在 https://cppinsights.io/ ,可以嘗試輸入一段包含 Lambda 的語法,其會展開成內聯`ReturnType operator(T arg)` 的類別。
## typeid 與 type_info
> https://en.cppreference.com/w/cpp/language/typeid
在 C++ 中, typeid 與 type_info,這是 *RTTI(Run-Time Type Information,執行期型別資訊)* 的一部分。它們能讓我們在程式執行時獲得物件的實際型別,是分析 lambda、仿函式、std::function 等高階 callable 的有力工具。
`typeid` 是 C++ 的運算子,可以用來查詢某個物件或型別的型別資訊。
```cpp
typeid(expression)
typeid(type)
```
```cpp
#include <iostream>
#include <typeinfo>
int main() {
int x = 42;
std::cout << typeid(x).name() << '\n';
// 輸出內容取決於編譯器實作。
// 在 GCC/Clang 下,通常是縮寫(如 i 表示 int),可用 c++filt 解碼。
// MSVC 則是提供相對完整的型別名稱
}
```
`typeid` 將會回傳 `std::type_info` 物件
```cpp
class std::type_info {
public:
bool operator==(const type_info& rhs) const noexcept;
const char* name() const noexcept;
virtual ~type_info();
};
```
| 成員 | 說明 |
| ----------- | ---------------------- |
| `.name()` | 回傳型別名稱(編譯器特定格式) |
| `==` / `!=` | 比較兩個型別是否相同 |
```cpp
#include <iostream>
#include <typeinfo>
void printType(const std::type_info& t) {
std::cout << "type: " << t.name() << '\n';
}
int main() {
int a = 5;
float b = 3.14f;
printType(typeid(a));
printType(typeid(b));
std::cout << std::boolalpha << (typeid(a) == typeid(b)) << '\n'; // false
}
```
之所以提及該運算元,是為了解釋 `std::bind`, `std::function`, Lambda 其實是不同型別的物件:
```cpp
#include <iostream>
#include <functional>
#include <typeinfo>
int add(int a, int b) {
return a + b;
}
int main() {
// lambda:會產生一個匿名 closure 類別
auto lambda = [](int x) { return x + 1; };
// bind:會產生一個複合型別(bind_t)
auto bound = std::bind(add, 10, std::placeholders::_1);
// std::function:型別固定,但內部儲存的是 type-erased callable
std::function<int(int)> f1 = lambda;
std::function<int(int)> f2 = bound;
std::cout << "lambda : " << typeid(lambda).name() << '\n';
std::cout << "bind : " << typeid(bound).name() << '\n';
std::cout << "function1 : " << typeid(f1).name() << '\n';
std::cout << "function2 : " << typeid(f2).name() << '\n';
}
```
輸出(使用 clang++ 編譯)
```
lambda : Z4mainEUliE_
bind : St5_BindIFPFiiiEiSt12_PlaceholderILi1EEEE
function1 : St8functionIFiiEE
function2 : St8functionIFiiEE
```
| 變數 | 實際型別 | 說明 |
| --------------- | ----------------- | --------------------------- |
| `lambda` | 匿名 closure 類別 | 每個 lambda 都會產生獨立型別 |
| `bound` | `std::_Bind` 模板實例 | bind 是模板生成器 |
| `std::function` | 固定模板型別 | 型別統一,但內部會做型別抹除(type-erased) |
## auto 與 decltype
auto 與 decltype 則是 C++ 現代型別推導的核心工具。
> auto 告訴編譯器:請根據右側表達式的值自動推導型別。
```cpp
int x = 42;
auto y = x; // y 的型別是 int
```
請注意,auto 將會把型別的額外資訊消除
```cpp
int x = 42;
int& r = x;
auto a = r; // int(不是 int&)
decltype(r) b = x; // int&(保留參考)
auto& ar = r; // int&(你加了 & 才會保留)
```
> decltype(expr) 會根據「表達式的語法行為」決定它的型別,但不會執行該表達式。
```cpp
int x = 5;
decltype(x) y = 10; // y 是 int
int& rx = x;
decltype(rx) z = x; // z 是 int&
const int cx = 7;
decltype((cx)) d = x; // d 是 const int&,因為 (cx) 是 lvalue
```
兩者結合:
```cpp
int x = 5;
int& rx = x;
auto a = rx; // int(值)
decltype(auto) b = rx; // int&(保留參考)
decltype(rx) c = x; // int&
```
使用 `auto` 推導型別的值,再使用 `decltype` 推導是否為參考。
## Template
由於課程已經學過 template 的使用,本節將會針對一些特殊例子進行說明
```cpp
template<typename T>
class Example {
public:
using valueType = T; // 宣告別名 valueType 指向模板參數 T
valueType data;
};
```
這段程式的含意是:
- 類別 `Example<T>` 宣告了一個名為 valueType 的內部型別
- valueType 是 T 的別名,讓類別使用者或子類別可用更語義化的名稱引用 T
```cpp
Example<int>::valueType x = 42; // int x
Example<double>::valueType y = 3.14; // double y
```
## using
using 與 typedef 都可以用來處理 Type Alias:
```cpp
using valueType = T; // 現代寫法(推薦)
typedef T valueType; // 舊寫法(等價,但較難讀)
```
| 比較項目 | `using` | `typedef` |
| ------ | ------- | --------- |
| 可讀性 | 高 | 低 |
| 支援模板別名 | 是 | 否 |
```cpp
template<typename T>
using Vec = std::vector<T>; // Template Alias
Vec<int> v1; // 相當於 std::vector<int>
Vec<double> v2;
```
建議統一使用 using 就好,因為 using 很單純的就是左邊是型別名稱,右邊是型別:
```cpp
typedef void(funcT*)(int);
using funcT = void(*)(int);
```
## Traits
Traits 是一種技巧,用來在編譯期查詢或定義型別相關資訊。
不同型別做出不同行為的判斷或決策,但不需要在執行期用 if 判斷,而是在編譯期就決定好。
```cpp
template<typename T>
struct Traits {
using valueType = T;
};
```
你可以透過 Traits<T>::valueType 來取得相關型別資訊,這就是所謂的「型別萃取」。
```cpp
template<typename T>
struct is_integer {
static constexpr bool value = false;
};
template<>
struct is_integer<int> {
static constexpr bool value = true;
};
template<>
struct is_integer<long> {
static constexpr bool value = true;
};
```
## SFINAE
> https://en.cppreference.com/w/cpp/language/sfinae
> Substitution Failure Is Not An Error(SFINAE)
其行為:定義多個模板函式,只讓其中某些函式適用於特定型別。編譯器遇到不適用的,就直接忽略它。
在C++ 中,有一個模板 `std::enable_if`,其可能的實作:
```cpp
template<bool Condition, typename T = void>
struct enable_if {};
// 當 Condition = true 時,才會產生 type 成員
template<typename T>
struct enable_if<true, T> {
using type = T;
};
// 當 Condition = false 時,不產生 type 成員
template<typename T>
struct enable_if<false, T> {
};
```
```cpp
#include <iostream>
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_integral<T>::value>::type
print(T value) {
std::cout << "integer:" << value << '\n';
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value>::type
print(T value) {
std::cout << "floating:" << value << '\n';
}
```
- print<int> 成功推導 : is_integral<int>::value == true ,通過
- print<double> : 第一個版本失敗,但不報錯 ,編譯器選擇第二個版本
這就是 SFINAE:替換失敗(不是 integral)但不是錯誤,繼續找其他函式。
| SFINAE 目標 | 範例 |
| -------- | ----------------------------------------------- |
| 函式啟用與禁用 | `enable_if` 控制參數或回傳型別 |
| 類別模板部分特化 | `template<typename T, typename = enable_if...>` |
| 選擇重載版本 | 根據 traits 選擇最合適版本 |