# 物件導向程式設計:補充教材 本文件補充一些 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 選擇最合適版本 |