手動記憶體管理是 C++ 最強大也最危險的能力。現代 C++ 透過智慧指標(smart pointers)把資源管理自動化,消除記憶體洩漏、懸空指標、重複刪除等一整類最難除錯的錯誤。本章的核心訊息只有一句話:在現代 C++ 中,你幾乎不該再手寫 newdelete


學習目標

讀完本章後,你應該能夠:

  1. 用記憶體配置圖解釋 Stack 與 Heap 的差異與生命週期
  2. 正確配對 new / deletenew[] / delete[],並說明為什麼不能混用
  3. 辨識並重現原始指標的三大經典錯誤:記憶體洩漏、懸空指標、重複刪除
  4. 說明「原始的擁有型指標」為何危險,以及 RAII 如何根治這些問題
  5. 熟練使用 std::unique_ptr(獨佔擁有權、make_unique、move-only、自訂刪除器、unique_ptr<T[]>
  6. 理解 std::shared_ptr 的參考計數與控制區塊(control block),並說明 make_shared 的好處
  7. std::weak_ptr 打破循環參考,並正確使用 lock() / expired()
  8. 依 C++ Core Guidelines 判斷何時用哪一種指標(含「非擁有」傳遞)

一、為什麼要關心記憶體?

C++ 沒有垃圾回收器(garbage collector)。這既是它高效能、可預測的根源,也是它最容易出錯的地方。當你向作業系統「借」了一塊記憶體,你就有責任在用完後「還」回去;忘了還,就是洩漏;還了又用,就是懸空;還了兩次,就是重複刪除。

WHY:為什麼不直接學垃圾回收的語言就好? 因為 C++ 把「資源何時釋放」交還給程式設計師,換來的是確定性解構(deterministic destruction)—— 物件一離開作用域就立刻釋放,不需要等待 GC。這對即時系統、遊戲、嵌入式、高頻交易等延遲敏感的領域至關重要。智慧指標讓我們同時擁有「自動管理」與「確定性釋放」兩個世界的好處。


二、Stack vs Heap 記憶體

2.1 記憶體配置圖

一個典型程式的虛擬記憶體佈局(由高位址到低位址)大致如下:

   高位址
  ┌─────────────────────────┐
  │   命令列引數 / 環境變數      │
  ├─────────────────────────┤
  │        Stack(堆疊)       │   ← 區域變數、函式呼叫框架
  │           │               │     向下成長 ↓
  │           ▼               │
  │                          │
  │           ▲               │
  │           │               │     向上成長 ↑
  │        Heap(堆積)        │   ← new / malloc 配置
  ├─────────────────────────┤
  │   BSS(未初始化全域/靜態)   │
  ├─────────────────────────┤
  │   Data(已初始化全域/靜態)  │
  ├─────────────────────────┤
  │   Text(程式碼,唯讀)      │
  └─────────────────────────┘
   低位址

Stack 與 Heap 相向成長;兩者一旦相撞,就是 stack overflow 或 out of memory。

2.2 Stack(堆疊)

void foo() {
    int x = 42;          // 配置在 stack 上
    double arr[100];      // 同樣在 stack 上(800 bytes)
}  // 函式結束時自動釋放(只是移動堆疊指標)
  • 自動管理:進入作用域時配置,離開時自動釋放
  • 速度極快:配置/釋放只是調整一個暫存器(堆疊指標)
  • 大小有限:通常 1–8 MB(執行緒堆疊預設值,依系統而定)
  • 生命週期:與宣告的作用域({})綁定

2.3 Heap(堆積)

void bar() {
    int* p = new int(42);             // 配置在 heap 上
    double* arr = new double[1000];     // 8000 bytes
    // ...
    delete p;                           // 必須手動釋放
    delete[] arr;                       // 陣列要用 delete[]
}
  • 手動管理:程式設計師負責 newdelete 的配對
  • 容量大:受系統可用記憶體限制(GB 級)
  • 速度較慢:涉及配置器的搜尋、簿記、可能的系統呼叫
  • 生命週期:從 newdelete,可跨越作用域與函式

2.4 比較表

特性 Stack Heap
配置/釋放 自動 手動(new / delete
速度 極快(移動指標) 較慢(配置器簿記)
大小 有限(~MB 級) 大(~GB 級)
生命週期 作用域結束即釋放 直到 delete
碎片化 可能產生
適合放 小型、生命週期明確的物件 大型、生命週期需跨作用域的物件

經驗法則:能放 Stack 就放 Stack。只有當物件「很大」、「數量在執行期才決定」、或「生命週期必須超出當前作用域」時,才考慮 Heap —— 而且請用智慧指標管理它。


三、new / deletenew[] / delete[]

3.1 配對規則

int*  p1 = new int;            delete p1;       // 單一物件
int*  p2 = new int(42);        delete p2;       // 單一物件 + 初始化
int*  a1 = new int[10];        delete[] a1;     // 陣列
auto* a2 = new int[3]{1,2,3};  delete[] a2;     // 陣列 + 初始化
配置方式 釋放方式
new T delete p
new T(args) delete p
new T[n] delete[] p
new T[n]{...} delete[] p

WHY:為什麼 new[] 一定要配 delete[] new T[n] 通常會在配置的記憶體前面多存一個「元素個數」,這樣 delete[] 才知道要呼叫幾次解構子。若你對陣列用 delete(少了 []),只會呼叫第一個元素的解構子,其餘元素的解構子被跳過 —— 這是未定義行為(UB),可能洩漏、可能崩潰。反過來對單一物件用 delete[] 同樣是 UB。

3.2 new 失敗會怎樣?

預設的 new 在配置失敗時拋出 std::bad_alloc,不會回傳 nullptr(這點和 C 的 malloc 不同)。如果你想要回傳 nullptr 的版本:

int* p = new(std::nothrow) int[1000000000ull];  // 失敗時回傳 nullptr
if (!p) { /* 處理配置失敗 */ }

四、原始指標的三大經典錯誤

以下三個 bug 是 C/C++ 數十年來最大宗的記憶體相關崩潰與安全漏洞來源。

4.1 記憶體洩漏(Memory Leak)

忘了釋放,記憶體永遠回不來。

void leak() {
    int* p = new int(42);
    // 忘記 delete p → 洩漏!
}  // p(指標本身)被銷毀,但它指向的 heap 記憶體沒被釋放

更陰險的版本:提前 return 或例外路徑跳過了 delete

bool process() {
    int* buf = new int[1024];
    if (somethingWrong()) {
        return false;        // ← buf 洩漏!
    }
    delete[] buf;
    return true;
}

4.2 懸空指標(Dangling Pointer)

指標還活著,但它指向的記憶體已經死了。

int* p = new int(42);
delete p;            // 記憶體已釋放
std::cout << *p;     // ← UB!p 是懸空指標

另一種常見來源:回傳區域變數的位址。

int* dangerous() {
    int local = 42;
    return &local;   // ← local 在函式結束就消失,回傳的位址懸空
}

4.3 重複刪除(Double Free)

同一塊記憶體被釋放兩次。

int* p = new int(42);
int* q = p;          // q 與 p 指向同一塊
delete p;            // 第一次:OK
delete q;            // 第二次:UB!堆積管理結構被破壞

4.4 圖解:問題的本質是「擁有權不明」

   p ──┐
       ├──► [ heap 上的 int = 42 ]
   q ──┘

   誰負責 delete?p?q?兩個都 delete?都不 delete?
   → 「擁有權」沒有在型別上表達出來,全靠人腦記憶 → 必然出錯

核心觀念:三大錯誤的根源都是同一件事 —— 擁有權(ownership)沒有被編碼進型別系統。智慧指標的本質,就是把「誰擁有這塊記憶體、何時釋放」寫進型別裡,交給編譯器與解構子強制執行。


五、RAII —— 一切的基礎

RAII(Resource Acquisition Is Initialization,資源取得即初始化)是 C++ 資源管理的核心原則:

  • 建構子中取得資源(記憶體、檔案、鎖、socket……)
  • 解構子中釋放資源
  • 利用「物件離開作用域時保證呼叫解構子」這個語言特性
class IntBuffer {
public:
    explicit IntBuffer(size_t n) : data_(new int[n]), size_(n) {}  // 取得
    ~IntBuffer() { delete[] data_; }                                // 釋放
    int& operator[](size_t i) { return data_[i]; }
private:
    int* data_;
    size_t size_;
};

void use() {
    IntBuffer buf(1000);
    buf[0] = 42;
    // 即使這裡拋出例外,buf 的解構子仍會被呼叫 → 記憶體一定釋放
}

智慧指標就是「為單一指標量身打造的 RAII 包裝」。

C++ Core Guidelines R.1Manage resources automatically using resource handles and RAII.(用資源句柄與 RAII 自動管理資源。)


六、std::unique_ptr — 獨佔擁有權

unique_ptr 表達「我,而且只有我,擁有這個物件」。它是 move-only(只能移動、不能複製),且在大多數實作下與原始指標一樣大、一樣快 —— 零額外執行期開銷。它應該是你的預設選擇

#include <memory>

auto p = std::make_unique<int>(42);   // 建議用 make_unique(C++14)
std::cout << *p << "\n";               // 42

// auto q = p;                         // ✗ 編譯錯誤:不可複製
auto q = std::move(p);                 // ✓ 擁有權轉移,p 變成 nullptr

預期輸出

42

6.1 常用操作

操作 說明
std::make_unique<T>(args...) 建立並回傳 unique_ptr<T>(首選)
*p / p->member 解參考 / 存取成員
p.get() 取得原始指標(轉移擁有權,僅供借用)
p.reset() 釋放並設為 nullptr
p.reset(new T(...)) 釋放舊的,改管理新的
p.release() 放棄擁有權並回傳原始指標(你變成要負責 delete 的人!)
std::move(p) 轉移擁有權給另一個 unique_ptr
if (p) 檢查是否非空

6.2 為什麼是 move-only?

「獨佔」的語意要求同一時間只能有一個擁有者。如果允許複製,就會出現兩個 unique_ptr 指向同一物件,解構時重複刪除。禁止複製、只允許移動,從型別層面消滅了這個錯誤。

std::unique_ptr<int> a = std::make_unique<int>(1);
std::unique_ptr<int> b = std::move(a);   // a 變 nullptr,擁有權交給 b
// 此後只有 b 會 delete 該物件,永遠不會重複刪除

6.3 在函式間傳遞擁有權

std::unique_ptr<Widget> make() {
    return std::make_unique<Widget>();   // 回傳 = 把擁有權交出去
}

void sink(std::unique_ptr<Widget> w) {   // 以「值」接收 = 接管擁有權
    // 用完 w,函式結束時自動釋放
}

void observe(const Widget& w);           // 只是「借用」,不碰擁有權

Core Guidelines F.26 / R.30:當你要在介面中表達擁有權轉移時用 unique_ptr;若函式只是要使用物件而不接管,請傳 T&const T& 或裸指標 T*(表示可為空的非擁有),不要const unique_ptr&

6.4 自訂刪除器(Custom Deleter)

對於非 new 配置的資源(C API、檔案、socket),可指定釋放方式:

auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen("data.txt", "r"), fileDeleter);
// 離開作用域自動 fclose

注意:unique_ptr 的刪除器是型別的一部分(會影響 sizeof,無狀態 lambda 通常可被最佳化掉)。

6.5 unique_ptr<T[]> — 管理動態陣列

auto arr = std::make_unique<int[]>(5);   // 5 個 int,值初始化為 0
for (int i = 0; i < 5; ++i) arr[i] = i * 10;
// arr[i] 用 [] 存取;陣列版本沒有 operator* 與 operator->

實務建議:除非有特殊理由,優先用 std::vector 取代 unique_ptr<T[]>,因為 vector 還提供大小、迭代器、自動擴容等。


七、std::shared_ptr — 共享擁有權

shared_ptr 表達「這個物件由多個擁有者共享,最後一個離開的負責關燈」。它透過參考計數追蹤有多少個 shared_ptr 指向同一物件,計數歸零時自動釋放。

auto sp1 = std::make_shared<int>(42);
std::cout << sp1.use_count() << "\n";   // 1
{
    auto sp2 = sp1;                      // 複製 → 計數 +1
    std::cout << sp1.use_count() << "\n"; // 2
}                                        // sp2 離開作用域 → 計數 -1
std::cout << sp1.use_count() << "\n";   // 1

預期輸出

1
2
1

7.1 控制區塊(Control Block)與參考計數圖解

shared_ptr 其實「胖」於原始指標:它內含兩個指標 —— 一個指向受管物件,一個指向控制區塊。控制區塊存放強參考計數(strong count)、弱參考計數(weak count)與刪除器等。

   sp1 ─┐
        ├─► [控制區塊: strong=2, weak=0] ──► [ 受管物件 int=42 ]
   sp2 ─┘
  • 每次複製 shared_ptr:strong count +1
  • 每次 shared_ptr 被銷毀或 reset():strong count -1
  • strong count 歸零:刪除受管物件
  • weak count 也歸零:釋放控制區塊本身

7.2 make_shared vs shared_ptr(new ...)

// ✓ 建議:一次配置(物件 + 控制區塊合併在同一塊記憶體)
auto sp = std::make_shared<Widget>(args...);

// ✗ 不建議:兩次配置(控制區塊一塊、物件一塊)
std::shared_ptr<Widget> sp2(new Widget(args...));
make_shared:        [ 控制區塊 | 物件 ]      ← 一次配置,快取友善
shared_ptr(new):    [ 控制區塊 ]  [ 物件 ]   ← 兩次配置

make_shared 的好處:(1) 少一次堆積配置,效能與快取局部性更好;(2) 例外安全 —— 避免「new 成功但 shared_ptr 建構前拋例外」造成的洩漏。

make_shared 的小代價:物件與控制區塊同塊,因此只要還有 weak_ptr 存在,物件的記憶體(雖然物件已解構)就無法歸還;此外無法使用自訂刪除器。需要這兩者時才退回 shared_ptr(new ...)

7.3 參考計數的執行緒安全性

參考計數的增減是原子操作(atomic),因此「多個執行緒同時複製/銷毀指向同一物件的不同 shared_ptr 物件」是安全的。

但要注意:這只保證計數本身安全。它保證: - 受管物件的內容是執行緒安全的(你仍需自己加鎖保護資料) - 對同一個 shared_ptr 變數同時讀寫是安全的(那需要 atomic<shared_ptr> 或外部同步)

原子操作有成本。這是 shared_ptrunique_ptr 慢的主因之一,也是「不需要共享就別用 shared_ptr」的理由。

7.4 自訂刪除器

unique_ptr 不同,shared_ptr 的刪除器不是型別的一部分(型別擦除存在控制區塊中),所以不同刪除器的 shared_ptr<T> 仍是同一型別:

std::shared_ptr<FILE> fp(fopen("data.txt", "r"),
                         [](FILE* f) { if (f) fclose(f); });

八、std::weak_ptr — 打破循環參考

8.1 循環參考問題

兩個物件互相用 shared_ptr 持有,彼此的計數永遠到不了 0 —— 即使外部已沒有任何人引用它們,它們也永遠不會被釋放(洩漏)。

struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;   // ← 問題所在
};

圖解:

   建立 a、b 並互相連結後:

   外部 a ─┐
           ├─► [Node A: strong=2] ◄── b->prev
   外部 b ─┐         │ next
           │         ▼
           └─► [Node B: strong=2] ◄── a->next(經由 A->next)

   離開作用域後外部指標消失,但 A、B 仍互相持有:
   A.strong = 1(被 B.prev 持有)
   B.strong = 1(被 A.next 持有)
   → 兩者都無法歸零 → 永久洩漏

8.2 解法:把其中一個方向改成 weak_ptr

weak_ptr 是「觀察但不擁有」的指標:它指向 shared_ptr 管理的物件,但不增加 strong count,因此不會阻止物件被釋放。

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node>   prev;   // ✓ 不增加計數,打破循環
};
   A.strong = 1,B.strong = 1(外部)
   A->next(shared)使 B.strong = 2
   B->prev(weak)  不影響 A.strong
   → 外部釋放後 A.strong = 0 → A 釋放 → B.strong = 1 → ... → 全部釋放 ✓

8.3 使用 weak_ptrlock()expired()

weak_ptr 不能直接解參考(因為它指向的物件隨時可能已被釋放)。你必須先 lock() 取得一個臨時 shared_ptr

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int>   wp = sp;

if (auto locked = wp.lock()) {   // 嘗試升級成 shared_ptr
    std::cout << *locked << "\n";  // 物件還在 → 42
} else {
    std::cout << "物件已釋放\n";
}

std::cout << std::boolalpha << wp.expired() << "\n";  // false(物件還在)
sp.reset();                                            // 釋放物件
std::cout << wp.expired() << "\n";                     // true(已釋放)

預期輸出

42
false
true

WHY 用 lock() 而非 expired() + 解參考? 在多執行緒下,expired() 回傳 false 後、你解參考前,物件可能剛好被別的執行緒釋放。lock()原子地「檢查是否還在 + 取得一個延長生命週期的 shared_ptr」,沒有這個競態空窗,是唯一安全的存取方式。


九、enable_shared_from_this(簡介)

有時物件的成員函式需要交出一個指向「自己」的 shared_ptr(例如把自己註冊到某個回呼清單)。直接寫 shared_ptr<T>(this)嚴重錯誤 —— 會建立一個獨立的控制區塊,導致同一物件被兩套計數管理、最終重複刪除。

正解是繼承 std::enable_shared_from_this<T>,並呼叫 shared_from_this()

struct Session : std::enable_shared_from_this<Session> {
    std::shared_ptr<Session> self() {
        return shared_from_this();   // 取得與既有控制區塊一致的 shared_ptr
    }
};

auto s = std::make_shared<Session>();
auto s2 = s->self();   // ✓ 共用同一控制區塊,use_count == 2

前提:該物件必須已經由某個 shared_ptr 管理,才能呼叫 shared_from_this();在建構子中呼叫是 UB(此時控制區塊尚未建立)。


十、智慧指標選擇指南(C++ Core Guidelines)

場景 建議使用
獨佔擁有權(最常見、預設 unique_ptr
共享擁有權(確實有多個所有者) shared_ptr
觀察但不擁有、且需偵測物件是否還活著 weak_ptr(搭配 shared_ptr
只是借用、保證在使用期間物件還活著 T& / const T& / T*(裸指標,非擁有)
效能敏感(零額外開銷) unique_ptr
需要打破循環參考 weak_ptr
與 C API 互動 unique_ptr + 自訂刪除器

核心經驗法則(對應 Core Guidelines)

  1. R.1:用 RAII 自動管理資源,避免裸 new / delete
  2. R.20 / R.21預設用 unique_ptr;只有真的需要共享擁有權時才用 shared_ptr
  3. R.23 / R.22:用 make_unique / make_shared 建立智慧指標。
  4. R.3:裸指標(T*)代表「非擁有」。函式參數若不接管擁有權,就用裸指標或參考,不要傳智慧指標。
  5. F.7:泛用函式應接受 T*T&,而非 unique_ptr<T>& / shared_ptr<T>,這樣呼叫端不被迫使用特定智慧指標。
  6. R.24:用 weak_ptr 打破 shared_ptr 的循環。

十一、最佳實踐(Best Practices)

  1. 預設 unique_ptr,需要共享才 shared_ptrshared_ptr 有原子計數與控制區塊的開銷,不是「比較安全的萬用解」。
  2. 永遠用 make_unique / make_shared,除非需要自訂刪除器或 unique_ptr<T[]> 帶刪除器等特例。
  3. 裸指標只用於「借用」:函式參數、回傳「我不擁有」的指標。絕不用裸指標表達擁有權。
  4. 不要 deleteget() 拿到的指標,那會與智慧指標重複刪除。
  5. 每個 heap 物件只用一條 shared_ptr 鏈管理:不要拿同一個原始指標建立兩個獨立的 shared_ptr
  6. 循環就用 weak_ptr 切斷:通常「子→父」或「back pointer」方向改成 weak。
  7. 避免回傳 unique_ptr& / shared_ptr& 作為參數型別:傳值表示轉移、傳參考/裸指標表示借用。

十二、常見錯誤與陷阱

  1. new[]delete(或反之):用 new[] 配置的陣列必須用 delete[],混用是 UB。
  2. shared_ptr 循環參考:兩物件互持 shared_ptr → 永久洩漏;用 weak_ptr 打破。
  3. 從同一原始指標建立多個獨立 shared_ptrcpp Widget* raw = new Widget; std::shared_ptr<Widget> a(raw); std::shared_ptr<Widget> b(raw); // ✗ 兩個獨立控制區塊 → 重複刪除 正解:auto a = std::make_shared<Widget>(); auto b = a;
  4. 在建構子裡呼叫 shared_from_this():此時控制區塊尚未建立,UB。
  5. unique_ptr 嘗試複製unique_ptr 不可複製,需 std::move
  6. delete p.get();:對 get() 的結果 delete,稍後智慧指標會再次釋放 → 重複刪除。
  7. shared_ptr 當作「比較安全的指標」濫用:多了原子開銷、隱藏的共享擁有權語意,反而讓擁有權關係更難理解。
  8. 以為複製 shared_ptr 就能保護受管物件的資料:原子的只有計數,物件內容仍需自行同步。

重點整理

概念 要點
Stack vs Heap Stack 自動、快、有限;Heap 手動、慢、容量大
new/delete 配對 newdeletenew[]delete[],不可混用
三大錯誤 記憶體洩漏、懸空指標、重複刪除 —— 根源是擁有權不明
RAII 建構取得資源、解構釋放資源;例外安全的基礎
unique_ptr 獨佔、move-only、零開銷、預設選擇
shared_ptr 共享、原子參考計數、有控制區塊開銷
make_shared 一次配置、例外安全、快取友善
weak_ptr 不擁有、打破循環、用 lock() 安全存取
enable_shared_from_this 在成員函式中安全取得自身的 shared_ptr
擁有權準則 預設 unique_ptr、需要時 shared_ptr、借用用裸指標/參考

練習題

以下每題都可用 g++ -std=c++17 -Wall 編譯驗證。建議先自己寫,再對照範例程式。

練習 1(基礎):抓出洩漏

給定下列函式,指出它有哪三條路徑會洩漏,並用 unique_ptr 改寫成完全沒有 delete 的版本。

bool handle(int mode) {
    int* buf = new int[256];
    if (mode == 0) return false;        // ?
    if (mode == 1) { /* ... */ return true; }  // ?
    process(buf);                        // 可能拋例外 ?
    delete[] buf;
    return true;
}
  • 提示:把 int* buf = new int[256]; 換成 auto buf = std::make_unique<int[]>(256);,之後所有 return 都不必再煩惱釋放。

練習 2(基礎):FileWrapper

寫一個 FileWrapper 類別,用 std::unique_ptr<FILE, decltype(...)> 搭配自訂刪除器管理 FILE*,提供 writeLine(const std::string&)bool isOpen()

  • 提示:刪除器是一個 lambda [](FILE* f){ if (f) fclose(f); };用 decltype(deleter) 當作 unique_ptr 的第二個模板參數。

練習 3(中級):物件工廠

寫一個工廠函式 std::unique_ptr<Shape> createShape(const std::string& type, double a, double b = 0),依參數回傳 CircleRectanglenullptr,並用一個 std::vector<std::unique_ptr<Shape>> 收集多個形狀後印出各自面積。

  • 提示push_back(std::make_unique<Circle>(r)) 可直接放入;遍歷時用 for (const auto& s : shapes)

練習 4(中級):共享快取與 use_count

設計一個 loadConfig() 回傳 std::shared_ptr<Config>,讓多個模組共享同一份設定。在不同作用域取得/釋放 shared_ptr,用 use_count() 印出計數變化,驗證最後一個釋放時 Config 的解構子才被呼叫。

  • 提示:在 Config 的建構子/解構子裡印訊息,搭配巢狀 {} 作用域觀察計數的升降。

練習 5(中級):觀察者模式用 weak_ptr

實作 Subject(被觀察者,用 shared_ptr 管理)與 ObserverObserver 內部以 weak_ptr<Subject> 持有目標。當 Subject 被釋放後,Observer::report() 應能透過 lock() 安全偵測到「目標已不存在」。

  • 提示report() 中寫 if (auto sp = target_.lock()) { 使用 sp } else { 報告已釋放 }

練習 6(挑戰):雙向鏈結串列免洩漏

shared_ptrnextweak_ptrprev 實作一個可正向與反向走訪的雙向鏈結串列。在節點建構/解構子印訊息,證明離開作用域後所有節點都被正確解構(沒有循環洩漏)。

  • 提示:反向走訪時,每一步都要 current->prev.lock();若 lock() 失敗代表已到頭。

練習 7(挑戰):自製迷你 SharedPtr

實作一個最小化的 SharedPtr<T>,內含「指向物件的指標」與「指向 int 計數的指標」,支援複製(計數 +1)、解構(計數 -1,歸零則 delete)、use_count()operator* / operator->

  • 提示:複製建構/賦值都要先處理舊計數再接手新計數;賦值時小心自我賦值與先 +1 再 -1 的順序。先不要求執行緒安全與 weak_ptr

對應程式碼檔案

檔案 說明
dynamic_memory.cpp 動態記憶體配置、new/delete、三大原始指標錯誤示範
unique_ptr.cpp unique_ptr 完整操作:建立、移動、reset/release、自訂刪除器、容器、工廠
shared_weak_ptr.cpp shared_ptr 參考計數、weak_ptr、循環參考問題與解法、觀察者模式