手動記憶體管理是 C++ 最強大也最危險的能力。現代 C++ 透過智慧指標(smart pointers)把資源管理自動化,消除記憶體洩漏、懸空指標、重複刪除等一整類最難除錯的錯誤。本章的核心訊息只有一句話:在現代 C++ 中,你幾乎不該再手寫
new與delete。
學習目標
讀完本章後,你應該能夠:
- 用記憶體配置圖解釋 Stack 與 Heap 的差異與生命週期
- 正確配對
new/delete與new[]/delete[],並說明為什麼不能混用 - 辨識並重現原始指標的三大經典錯誤:記憶體洩漏、懸空指標、重複刪除
- 說明「原始的擁有型指標」為何危險,以及 RAII 如何根治這些問題
- 熟練使用
std::unique_ptr(獨佔擁有權、make_unique、move-only、自訂刪除器、unique_ptr<T[]>) - 理解
std::shared_ptr的參考計數與控制區塊(control block),並說明make_shared的好處 - 用
std::weak_ptr打破循環參考,並正確使用lock()/expired() - 依 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[]
}
- 手動管理:程式設計師負責
new與delete的配對 - 容量大:受系統可用記憶體限制(GB 級)
- 速度較慢:涉及配置器的搜尋、簿記、可能的系統呼叫
- 生命週期:從
new到delete,可跨越作用域與函式
2.4 比較表
| 特性 | Stack | Heap |
|---|---|---|
| 配置/釋放 | 自動 | 手動(new / delete) |
| 速度 | 極快(移動指標) | 較慢(配置器簿記) |
| 大小 | 有限(~MB 級) | 大(~GB 級) |
| 生命週期 | 作用域結束即釋放 | 直到 delete |
| 碎片化 | 無 | 可能產生 |
| 適合放 | 小型、生命週期明確的物件 | 大型、生命週期需跨作用域的物件 |
經驗法則:能放 Stack 就放 Stack。只有當物件「很大」、「數量在執行期才決定」、或「生命週期必須超出當前作用域」時,才考慮 Heap —— 而且請用智慧指標管理它。
三、new / delete 與 new[] / 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.1:Manage 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_ptr比unique_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_ptr:lock() 與 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)
- R.1:用 RAII 自動管理資源,避免裸
new/delete。 - R.20 / R.21:預設用
unique_ptr;只有真的需要共享擁有權時才用shared_ptr。 - R.23 / R.22:用
make_unique/make_shared建立智慧指標。 - R.3:裸指標(
T*)代表「非擁有」。函式參數若不接管擁有權,就用裸指標或參考,不要傳智慧指標。 - F.7:泛用函式應接受
T*或T&,而非unique_ptr<T>&/shared_ptr<T>,這樣呼叫端不被迫使用特定智慧指標。 - R.24:用
weak_ptr打破shared_ptr的循環。
十一、最佳實踐(Best Practices)
- 預設
unique_ptr,需要共享才shared_ptr。shared_ptr有原子計數與控制區塊的開銷,不是「比較安全的萬用解」。 - 永遠用
make_unique/make_shared,除非需要自訂刪除器或unique_ptr<T[]>帶刪除器等特例。 - 裸指標只用於「借用」:函式參數、回傳「我不擁有」的指標。絕不用裸指標表達擁有權。
- 不要
delete從get()拿到的指標,那會與智慧指標重複刪除。 - 每個 heap 物件只用一條
shared_ptr鏈管理:不要拿同一個原始指標建立兩個獨立的shared_ptr。 - 循環就用
weak_ptr切斷:通常「子→父」或「back pointer」方向改成 weak。 - 避免回傳
unique_ptr&/shared_ptr&作為參數型別:傳值表示轉移、傳參考/裸指標表示借用。
十二、常見錯誤與陷阱
new[]配delete(或反之):用new[]配置的陣列必須用delete[],混用是 UB。shared_ptr循環參考:兩物件互持shared_ptr→ 永久洩漏;用weak_ptr打破。- 從同一原始指標建立多個獨立
shared_ptr:cpp 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; - 在建構子裡呼叫
shared_from_this():此時控制區塊尚未建立,UB。 unique_ptr嘗試複製:unique_ptr不可複製,需std::move。delete p.get();:對get()的結果delete,稍後智慧指標會再次釋放 → 重複刪除。- 把
shared_ptr當作「比較安全的指標」濫用:多了原子開銷、隱藏的共享擁有權語意,反而讓擁有權關係更難理解。 - 以為複製
shared_ptr就能保護受管物件的資料:原子的只有計數,物件內容仍需自行同步。
重點整理
| 概念 | 要點 |
|---|---|
| Stack vs Heap | Stack 自動、快、有限;Heap 手動、慢、容量大 |
new/delete 配對 |
new↔delete、new[]↔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),依參數回傳 Circle、Rectangle 或 nullptr,並用一個 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 管理)與 Observer,Observer 內部以 weak_ptr<Subject> 持有目標。當 Subject 被釋放後,Observer::report() 應能透過 lock() 安全偵測到「目標已不存在」。
- 提示:
report()中寫if (auto sp = target_.lock()) { 使用 sp } else { 報告已釋放 }。
練習 6(挑戰):雙向鏈結串列免洩漏
用 shared_ptr 作 next、weak_ptr 作 prev 實作一個可正向與反向走訪的雙向鏈結串列。在節點建構/解構子印訊息,證明離開作用域後所有節點都被正確解構(沒有循環洩漏)。
- 提示:反向走訪時,每一步都要
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、循環參考問題與解法、觀察者模式 |