目標: 掌握C++核心特性,為嵌入式開發打基礎
好的,我來為你詳細梳理一下 繼承與多態、虛函數 相關的知識點,包括單繼承、多繼承、虛函數表機制、純虛函數與抽象類、動態綁定。以下內容適合中等難度層次的理解,便于考試復習或面試準備。
🌟 繼承與多態,虛函數
1?? 單繼承和多繼承
單繼承
-
一個派生類只有一個基類。
-
結構簡單,層次清晰。
-
示例:
class Base { public:void show() { std::cout << "Base" << std::endl; } };class Derived : public Base { public:void display() { std::cout << "Derived" << std::endl; } };
public 公開繼承(最常用,基類 public 和 protected 成員在派生類中保持原有權限)
protected 保護繼承(基類 public 和 protected 成員在派生類中都變成 protected)
private 私有繼承(基類 public 和 protected 成員在派生類中都變成 private)
多繼承
-
一個派生類可以同時繼承多個基類。
-
可帶來靈活性,但也可能引發二義性問題(如菱形繼承問題)。
-
解決辦法:虛繼承(
virtual
關鍵字) -
示例:
class Base1 { public:void func1() { std::cout << "Base1" << std::endl; } };class Base2 { public:void func2() { std::cout << "Base2" << std::endl; } };class Derived : public Base1, public Base2 { };
菱形繼承問題
class A { public: int x; };
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { };
👉 如果 B
和 C
都虛繼承自 A
,D
中只有一份 A
的成員。
🚀 構造函數后 :
的用途 —— 初始化列表
在 C++ 中,構造函數定義時可以用 :
引出一個 初始化列表,用于初始化成員變量和基類。
語法
class 類名 {
public:類型1 成員1;類型2 成員2;類名(參數列表) : 成員1(值1), 成員2(值2) {// 構造函數體}
};
🌟 初始化列表主要作用
? 初始化 const 成員(必須用初始化列表)
? 初始化 引用成員(必須用初始化列表)
? 調用基類構造函數(在繼承中必用)
? 效率更高(成員在進入構造函數體之前就已初始化)
🌰 示例 1:普通成員初始化
class Point {int x;int y;
public:Point(int a, int b) : x(a), y(b) {// 構造函數體可以為空}
};
等價于:
Point p(1, 2);
這里 x
初始化為 1,y
初始化為 2。
🌰 示例 2:const 和引用成員
class Example {const int ci;int& ref;
public:Example(int i, int& r) : ci(i), ref(r) { }
};
👉 注意:const
和 &
成員必須在初始化列表里賦值,不能在構造函數體內賦值。
🌰 示例 3:繼承情況下,調用基類構造函數
class Base {
public:Base(int a) { std::cout << "Base: " << a << std::endl; }
};class Derived : public Base {
public:Derived(int a, int b) : Base(a) {std::cout << "Derived: " << b << std::endl;}
};
當你寫:
Derived d(10, 20);
輸出:
Base: 10
Derived: 20
? 先調用基類構造函數。
用法 | 符號 : 位置 | 例子 |
---|---|---|
類繼承聲明 | class A : public B | class B : public A { }; |
初始化列表 | 構造函數頭部后 | A(int x) : a(x) { } |
🌟 初始化列表主要作用
? 初始化 const 成員(必須用初始化列表)
? 初始化 引用成員(必須用初始化列表)
? 調用基類構造函數(在繼承中必用)
? 效率更高(成員在進入構造函數體之前就已初始化)
普通 const 成員是屬于對象的,每個對象的 const 成員值可能不同。
必須在構造對象時確定 const 成員值,所以需要初始化列表。
👉 引用必須在定義時綁定到對象(或變量)上,不能晚綁定。
一旦引用被初始化(綁定),它就永遠指向這個對象或變量,不可改變。
特性 | const 成員變量 | static const 成員變量 |
---|---|---|
初始化方式 | 必須用初始化列表初始化 | 可以在類內初始化 |
屬于 | 對象的每個實例 | 類的所有實例共享一份 |
例子 | A(int v) : x(v) {} | static const int x = 10; |
2?? 虛函數表機制
- 虛函數表 (vtable):編譯器為含有虛函數的類生成的一張函數地址表。
- 虛指針 (vptr):每個含虛函數的對象實例中包含一個指向虛函數表的指針。
- 派生類覆蓋虛函數時,vtable 的相應入口會被派生類的函數地址替換。
- 調用虛函數時,根據 vptr 定位 vtable,再調用正確的函數地址,實現多態。
class Base {
public:virtual void func() { std::cout << "Base::func" << std::endl; }
};class Derived : public Base {
public:void func() override { std::cout << "Derived::func" << std::endl; }
};
📌 當你寫 Base* p = new Derived(); p->func();
時,會通過 vptr 查找 vtable 中的 Derived::func
地址。
3?? 純虛函數和抽象類
純虛函數
- 語法:
virtual void func() = 0;
- 沒有實現,需要派生類重寫。
抽象類
- 包含至少一個純虛函數的類。
- 無法實例化對象,只能作為基類。
示例:
class Shape {
public:virtual void draw() = 0; // 純虛函數
};class Circle : public Shape {
public:void draw() override { std::cout << "Draw Circle" << std::endl; }
};
👉 Shape s;
?不允許
👉 Shape* ps = new Circle();
?允許,用基類指針指向子類對象。
4?? 動態綁定
-
動態綁定(又稱 運行時多態):在運行時確定調用哪個函數。
-
前提:
- 函數是虛函數。
- 通過基類指針或引用調用。
-
如果不滿足上面條件,編譯時靜態綁定。
示例:
Base* p = new Derived();
p->func(); // 動態綁定,調用 Derived::func
如果是 p->Base::func();
則會靜態綁定,強制調用基類版本。
💡 總結
特性 | 單繼承 | 多繼承 | 虛函數 | 純虛函數 | 動態綁定 |
---|---|---|---|---|---|
關系 | 一個基類 | 多個基類 | 實現多態 | 實現接口 | 運行時確定函數 |
優點 | 簡單易維護 | 靈活 | 多態行為 | 強制派生類實現 | 多態支持 |
缺點 | 功能受限 | 易引發二義性 | 增加內存開銷 | 不能實例化基類 | 性能略低于靜態綁定 |
繼承與 vptr
重新賦值的背景
- 每個對象的
vptr
用來指向當前對象所屬類的虛函數表(vtable
)。 - 當你創建一個派生類對象時,這個對象其實包含了基類子對象部分。
- 在構造過程中,隨著構造函數的調用,
vptr
會被設置為對應類的vtable
。
兩個對象:
各自有一個 vptr
vptr 都指向同一張 vtable(Derived 的 vtable)vtable 是編譯器為類生成的唯一一張表(每個帶虛函數的類一張)
🌟 C++ 模板基礎
1?? 函數模板語法
👉 語法:
template <typename T>
T add(T a, T b) {return a + b;
}
或:
template <class T> // typename 和 class 都可以
T add(T a, T b) {return a + b;
}
👉 使用:
add(3, 4); // T 推導為 int
add(3.5, 4.2); // T 推導為 double
add<int>(3, 4); // 顯式指定 T = int
2?? 類模板實現
👉 語法:
template <typename T>
class MyClass {
public:T data;MyClass(T val) : data(val) {}void show() { std::cout << data << "\n"; }
};
👉 使用:
MyClass<int> obj1(10);
MyClass<std::string> obj2("hello");
👉 注意:
- 必須在使用類模板時提供模板參數(除非有默認參數)。
- 類模板成員函數定義可以在類外,但必須帶模板頭:
template <typename T>
void MyClass<T>::show() {std::cout << data << "\n";
}
3?? 模板特化
👉 全特化:
template <typename T>
class Printer {
public:void print(T val) { std::cout << val << "\n"; }
};// 對 char* 的特化
template <>
class Printer<char*> {
public:void print(char* val) { std::cout << "char* : " << val << "\n"; }
};
👉 偏特化:
template <typename T, typename U>
class Pair { /* ... */ };template <typename T>
class Pair<T, int> { /* 針對第二個參數是 int 的特化 */ };
4?? 模板參數推導
👉 函數模板支持參數推導:
template <typename T>
void func(T val) { /* ... */ }func(10); // 推導 T=int
func(3.14); // 推導 T=double
func("hello"); // 推導 T=const char*
👉 類模板 不支持自動推導(C++17 前),但 C++17 起支持 類模板參數推導引擎:
template <typename T>
class Wrapper {
public:T value;Wrapper(T v) : value(v) {}
};Wrapper w(10); // C++17 起推導出 Wrapper<int>
🌟 小結表
模板特性 | 作用 | 特點 |
---|---|---|
函數模板 | 讓函數支持不同類型 | 支持參數推導,可顯式指定 |
類模板 | 讓類支持不同類型 | 使用時需指定參數(除非 C++17 推導) |
模板特化 | 針對特定類型提供不同實現 | 全特化或偏特化 |
參數推導 | 自動根據實參確定模板參數 | 類模板一般不推導,函數模板支持 |
🌟 C++ 異常處理基礎
1?? 異常處理機制
基本語法:
try {// 可能拋出異常的代碼throw std::runtime_error("Error occurred");
}
catch (const std::runtime_error& e) {std::cout << "Caught: " << e.what() << "\n";
}
catch (...) {std::cout << "Caught unknown exception\n";
}
👉 流程
try
塊中代碼運行時遇到throw
,立即停止執行,跳轉到對應catch
。- 匹配的
catch
語句被調用。 - 如果沒有匹配的
catch
,程序調用std::terminate()
。
👉 注意
throw;
可以重新拋出當前捕獲的異常。- 異常匹配是按照
catch
的順序自上而下。
2?? 自定義異常類
你可以自定義異常類型,通常繼承自 std::exception
或其子類。
class MyException : public std::exception {
public:const char* what() const noexcept override {return "My custom exception";}
};
使用:
try {throw MyException();
}
catch (const MyException& e) {std::cout << e.what() << "\n";
}
3?? RAII 與異常安全
RAII(資源獲取即初始化)是 C++ 保證異常安全的重要手段。
例子:
class FileWrapper {FILE* fp;
public:FileWrapper(const char* filename) {fp = fopen(filename, "r");if (!fp) throw std::runtime_error("File open failed");}~FileWrapper() {if (fp) fclose(fp);}
};
? 如果在構造中拋出異常,已構造對象的析構函數會被自動調用,資源得到釋放。
? 這就是 異常安全 的 RAII 精神。
4?? 嵌入式系統中異常使用注意事項
👉 為什么嵌入式常禁用異常?
- 異常會增加代碼體積(嵌入式對ROM/Flash大小敏感)
- 異常處理可能需要棧展開,增加運行時開銷
- 嵌入式通常要求可控、確定性的錯誤處理
👉 替代方案
- 返回錯誤碼
- 使用斷言
assert
- 用狀態機或專用錯誤處理函數
👉 嵌入式項目編譯器一般禁用異常
g++ -fno-exceptions ...
🌟 小結表
特性 | 描述 | 注意事項 |
---|---|---|
try-catch-throw | 異常處理結構,用于捕獲和處理異常 | 匹配順序重要,throw 可重新拋出 |
自定義異常類 | 提供更清晰的異常類型 | 繼承自 std::exception ,重寫 what() |
RAII | 自動管理資源,防止泄漏 | 析構函數釋放資源,保證異常安全 |
嵌入式異常 | 通常禁用,因開銷大 | 推薦用錯誤碼或斷言代替 |
🌟 C++ 異常為什么會增大代碼空間?
因為 編譯器為了支持異常處理,需要生成額外的元數據、表和隱藏代碼。主要包括以下幾個方面:
1?? 棧展開(stack unwinding)信息
👉 當你 throw
異常時,程序必須從拋出點開始,依次調用每個對象的析構函數,正確釋放資源。
💡 為了做到這點,編譯器會:
- 在二進制中生成一個“異常處理表”(也叫 棧展開表 或 unwind table)
- 記錄每個函數的棧幀布局、哪些地方有局部對象、析構函數地址等
? 這些表存在于可執行文件中(通常是 .eh_frame
段),直接占用代碼空間。
2?? 異常處理控制邏輯
👉 編譯器生成隱藏代碼:
- 檢測拋出異常的地方
- 跳轉到異常處理器(
catch
代碼) - 調用析構函數做清理
這些代碼雖然不顯式寫在源代碼中,但會體現在最終的機器碼中。
3?? 多余的輔助代碼 / 運行庫支持
👉 異常處理需要運行庫提供輔助函數,比如:
- 異常對象的創建、復制、銷毀邏輯
- 拋出異常時調用的全局函數(例如
__cxa_throw
,__cxa_begin_catch
等,GCC/Clang 下)
👉 這部分庫代碼也會被鏈接進你的程序中,增加體積。
🌈 對比:C vs C++ 錯誤處理
特性 | C語言 | C++異常 |
---|---|---|
機制 | 返回值、errno 、setjmp/longjmp | try-catch-throw |
自動清理 | ? 無,必須手工清理 | ? 自動調用析構,RAII |
錯誤傳播 | 必須層層傳遞或顯式 longjmp | 自動沿調用棧尋找 catch |
可讀性 | 易出錯、代碼繁瑣 | 更簡潔、可讀性好 |
性能 | 開銷小,簡單高效 | 異常表增加代碼體積,棧展開有開銷 |
🌟 你問得很好!咱們一起把“C語言需要根據錯誤手動操作、層層處理”這個概念徹底搞清楚。
🔑 為什么說 C 需要“層層”處理錯誤?
在 C 語言里,函數出錯后,它本身不會“跳回去”或者自動通知調用者發生了什么(不像 C++ 異常可以自動向上傳遞)。
👉 你得 顯式返回錯誤碼,并在每一層調用代碼里手工檢查。
🌰 例子:層層返回錯誤碼
假設你寫一個程序,調用很多函數,某一層出錯了,需要上層知道:
#include <stdio.h>int lowLevel(int a) {if (a == 0) {return -1; // 出錯:除 0}return 100 / a;
}int midLevel(int a) {int res = lowLevel(a);if (res == -1) {return -1; // 出錯,繼續向上報告}return res + 10;
}int highLevel(int a) {int res = midLevel(a);if (res == -1) {return -1; // 出錯,繼續向上報告}return res * 2;
}int main() {int result = highLevel(0);if (result == -1) {printf("Error happened!\n");} else {printf("Result = %d\n", result);}
}
? 這就是所謂 層層檢查、層層返回:
每一層都必須寫:
if (返回值 == 錯誤碼) return 錯誤碼;
否則,錯誤就會“漏掉”。
🔥 和 C++ 異常對比:自動向上傳遞
同樣邏輯如果是 C++:
int lowLevel(int a) {if (a == 0) throw std::runtime_error("div by zero");return 100 / a;
}int midLevel(int a) {return lowLevel(a) + 10;
}int highLevel(int a) {return midLevel(a) * 2;
}int main() {try {int result = highLevel(0);std::cout << "Result = " << result << "\n";} catch (const std::exception& e) {std::cout << "Error: " << e.what() << "\n";}
}
? 你不用每層寫“檢查返回值、return -1”了,異常會自動穿過每一層,直到 catch
。
🌈 層層檢查的含義總結
C 語言層層檢查 | C++ 異常 |
---|---|
每層手動檢查返回值或 errno | 異常自動向上傳遞到最近的 catch |
錯誤處理代碼和主邏輯混在一起,容易混亂 | 錯誤處理代碼集中在 catch ,主邏輯更清晰 |
易寫漏檢查,容易埋 bug | 不易漏掉錯誤(除非沒有 catch ) |
? C 的手工層層處理的風險
- 容易漏掉某層的檢查,錯誤被默默吞掉
- 錯誤碼設計混亂時,調試困難
- 錯誤處理代碼重復多,維護麻煩
🚀 STL 容器基礎 (vector, array, list)
1?? 容器的選擇原則
? vector
- 連續內存塊(像動態數組)
- 隨機訪問快(支持
[]
、at()
) - 尾部插入/刪除快(
push_back
/pop_back
O(1)) - 中間/開頭插入刪除慢(需移動元素)
? array
- 固定大小、棧分配(本質是封裝了 C 風格數組)
- 編譯期大小確定
- 非常輕量、效率高(零開銷封裝)
? list
- 雙向鏈表
- 任意位置插入/刪除 O(1)
- 不支持隨機訪問(無
[]
)
💡 選擇思路:
需求 | 容器 |
---|---|
需要頻繁隨機訪問 | vector 、array |
需要頻繁插入刪除(中間或兩端) | list |
大小固定、性能極高 | array |
動態大小、插尾快 | vector |
2?? 迭代器使用
所有 STL 容器都支持迭代器,用于統一遍歷:
#include <vector>
#include <iostream>int main() {std::vector<int> v = {1, 2, 3, 4};// 普通迭代器for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {std::cout << *it << " ";}std::cout << "\n";// 范圍 for(C++11)for (auto x : v) {std::cout << x << " ";}std::cout << "\n";// 反向迭代器for (auto it = v.rbegin(); it != v.rend(); ++it) {std::cout << *it << " ";}std::cout << "\n";
}
👉 注意:
vector
:隨機訪問迭代器list
:雙向迭代器(不能做隨機訪問操作)
3?? 算法庫基礎
STL 提供強大算法,與容器無關:
#include <algorithm>
#include <vector>
#include <iostream>int main() {std::vector<int> v = {4, 1, 3, 2};std::sort(v.begin(), v.end()); // 排序std::for_each(v.begin(), v.end(), [](int x) {std::cout << x << " ";});std::cout << "\n";auto it = std::find(v.begin(), v.end(), 3); // 查找if (it != v.end()) {std::cout << "Found: " << *it << "\n";}
}
💡 STL 算法特點:
- 和容器解耦(基于迭代器工作)
- 提供排序、查找、修改、統計等功能
- 支持自定義謂詞(lambda、函數對象)
4?? 嵌入式環境下 STL 使用注意事項
嵌入式開發中 STL 使用會遇到一些問題:
? 動態分配
vector
、list
都依賴堆內存,嵌入式堆內存可能緊張或管理嚴格。
? 代碼膨脹
- 模板、泛型帶來代碼體積增大。
? 實時性
- 有些操作(如
vector
擴容)可能會帶來不可預測的耗時。
? 對策
- 優先選用
array
(棧上固定大小,零開銷封裝) - 或自定義 allocator 控制內存管理
- 或使用輕量級替代庫(如:Embedded STL、ETL)
🌈 小結
容器 | 優勢 | 適用場景 |
---|---|---|
vector | 動態大小、隨機訪問快 | 數組替代、需要動態增長 |
array | 固定大小、棧分配、零開銷 | 大小固定、嵌入式友好 |
list | 插入刪除快、穩定迭代器 | 頻繁中間操作、元素數量不大 |
嵌入式建議 |
---|
盡量避免堆分配容器(vector、list) |
用 array 或靜態分配的容器 |
控制代碼體積(注意模板實例化膨脹) |
迭代器類自己重載了運算符
這些迭代器類型都會重載必要的運算符,比如:
*it
—— 重載了operator*()
,返回元素引用++it
—— 重載了operator++()
,讓迭代器移動到下一個元素--it
—— 重載了operator--()
(如果支持)it + n
、it - n
—— 只有隨機訪問迭代器(如vector
)會重載it == it2
、it != it2
—— 重載了比較運算符
? 不同容器的迭代器能力不同
容器 | 迭代器類型 | 支持運算符 |
---|---|---|
vector / array | 隨機訪問迭代器 | ++ , -- , + , - , [] |
list | 雙向迭代器 | ++ , -- |
forward_list | 單向迭代器 | 只能 ++ |
🚀 智能指針 (unique_ptr, shared_ptr)
智能指針(std::unique_ptr, std::shared_ptr, std::weak_ptr)
→ 屬于 C++ 標準庫的一部分
→ 定義在 頭文件中
→ 實現基于模板、RAII、引用計數等技術,但不歸類在 STL 容器/算法里
1?? RAII 原理
RAII 全稱:
👉 Resource Acquisition Is Initialization
👉 資源獲取即初始化
🚀 RAII 的核心思想
把資源(內存、文件句柄、鎖、網絡連接等)的管理交給對象的生命周期。
也就是說:
- 對象創建時(構造函數):獲取資源
- 對象銷毀時(析構函數):釋放資源
💡 用對象的生命周期保證資源安全,不需要手工釋放。
🌈 RAII 的應用場景
- 智能指針(unique_ptr, shared_ptr)管理內存
- fstream 管理文件句柄
- lock_guard / unique_lock 管理鎖
- 各種容器管理內部數據
- 自定義資源類(比如管理數據庫連接、網絡 socket)
🌰 RAII 例子
裸指針(非 RAII)
void foo() {int* p = new int(10);// ... 使用 pdelete p; // 別忘了!否則內存泄漏
}
? 如果程序中途拋異常或 return,可能忘了 delete。
💡 智能指針就是 RAII 的經典應用:
- 構造時接管裸指針(
new
的結果) - 析構時自動
delete
,防止內存泄漏
{std::unique_ptr<int> p(new int(10)); // 自動管理內存,無需手動 delete
} // 作用域結束,自動 delete
? RAII 的實現邏輯
RAII 的實現 = 用類封裝資源
- 構造函數:負責獲取資源
- 析構函數:負責釋放資源
資源對象的生命周期 = 資源的生命周期
對象被創建時 → 自動獲得資源
對象被銷毀時 → 自動釋放資源
🌰 手寫一個簡單 RAII 類(以文件操作為例)
我們不用 std::ofstream
,自己實現一個簡單 RAII 文件類:
#include <iostream>
#include <cstdio>class FileRAII {
private:FILE* file;public:// 構造函數:打開文件FileRAII(const char* filename, const char* mode) {file = std::fopen(filename, mode);if (!file) {throw std::runtime_error("Failed to open file");}std::cout << "File opened\n";}// 提供操作文件的方法void write(const char* text) {if (file) {std::fputs(text, file);}}// 析構函數:關閉文件~FileRAII() {if (file) {std::fclose(file);std::cout << "File closed\n";}}// 禁止拷貝,防止重復關閉FileRAII(const FileRAII&) = delete;FileRAII& operator=(const FileRAII&) = delete;
};
🌟 使用例子
int main() {try {FileRAII file("test.txt", "w");file.write("Hello RAII!");// 不需要手工 fclose,離開作用域自動關閉} catch (const std::exception& e) {std::cerr << e.what() << "\n";}return 0;
}
🌈 RAII 實現的關鍵點
部分 | 作用 |
---|---|
構造函數 | 獲取資源(例如分配內存、打開文件、加鎖) |
析構函數 | 釋放資源(例如釋放內存、關閉文件、解鎖) |
禁止拷貝 | 防止多個對象共享同一資源導致重復釋放 |
可選:支持移動語義 | 允許資源轉移所有權(C++11 以后推薦) |
RAII 本質就是編寫一個資源封裝類,通過構造 + 析構管理資源,讓對象生命周期決定資源管理,無需手工操作。
🚀 RAII + 異常處理:優雅管理資源
在 C++ 中,經常這樣用:
try {SomeRAIIObject obj;// ... 可能拋異常的代碼 ...
} catch (...) {// 處理異常
}
// 無論如何 obj 析構、資源釋放
💡 這就是 現代 C++ 推薦風格:RAII 負責資源安全,異常處理負責邏輯控制。
2?? unique_ptr 使用場景
? 特點
- 獨占所有權(禁止拷貝,只允許移動)
- 不允許多個 unique_ptr 管同一塊內存
? 使用場景
- 確保資源唯一所有權
- 避免手寫
delete
,防止泄漏 - 用在工廠函數、返回局部對象時:
std::unique_ptr<Foo> createFoo() {return std::unique_ptr<Foo>(new Foo);
}
- 用于指向大對象、避免復制
? 移動所有權
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // p1 放棄所有權
3?? shared_ptr 引用計數
? 特點
- 多個
shared_ptr
可以共享一塊內存的所有權 - 內部維護一個 引用計數
- 最后一個
shared_ptr
被銷毀時,資源才被釋放
? 引用計數機制
auto p1 = std::make_shared<int>(10);
auto p2 = p1; // 引用計數 +1
auto p3 = p2; // 引用計數 +1
// p1, p2, p3 全部銷毀后,delete 內存
你可以通過 use_count()
查看計數:
std::cout << p1.use_count(); // 輸出當前計數
4?? 避免循環引用
? 循環引用問題
如果兩個對象都用 shared_ptr
指向對方,會導致引用計數永遠不為 0,內存無法釋放。
💡 示例
struct B;
struct A {std::shared_ptr<B> bptr;
};
struct B {std::shared_ptr<A> aptr;
};
💥 這里 A 和 B 相互持有 shared_ptr
,會產生循環引用。
? 解決方案
用
weak_ptr
打破循環
struct B;
struct A {std::shared_ptr<B> bptr;
};
struct B {std::weak_ptr<A> aptr; // 不增加引用計數
};
weak_ptr
不會增加引用計數,只是觀測對象是否還活著。- 用
lock()
可以臨時獲得一個shared_ptr
:
if (auto sp = aptr.lock()) {// 安全訪問
}
🌈 小結對比
智能指針 | 特點 | 適用場景 |
---|---|---|
unique_ptr | 獨占、禁止拷貝 | 獨占資源,防止泄漏 |
shared_ptr | 引用計數、共享資源 | 多方共享、動態生命周期管理 |
weak_ptr | 弱引用、不增加計數 | 避免循環引用、觀察 shared_ptr |
🌟 C++ 智能指針主要就是 unique_ptr, shared_ptr, weak_ptr 三種,每種針對不同的所有權管理需求,配合 RAII 自動管理內存,防止泄漏。
? RAII 類本身不需要配合智能指針
👉 RAII 類 = 自己封裝了資源管理(構造獲取資源 + 析構釋放資源)
👉 它的資源管理已經是安全的,不依賴智能指針。
🌰 比如:
{FileRAII file("test.txt", "w");file.write("Hello RAII");// 離開作用域時自動關閉文件,不需要智能指針
}
? 這里 RAII 類的對象本身就在棧上,出作用域自動銷毀,資源釋放。根本不需要智能指針參與。
🌈 什么時候會配合智能指針使用?
你可能會 動態創建 RAII 對象,這時:
- 如果你用
new
創建 RAII 對象,為避免手工 delete,就用智能指針管理它。 - 特別是當 RAII 對象需要跨作用域、多處共享時,智能指針(如
unique_ptr
/shared_ptr
)就很方便。
🌰 示例:
#include <memory>
auto filePtr = std::make_unique<FileRAII>("test.txt", "w");
filePtr->write("Hello RAII");
// unique_ptr 離開作用域自動釋放 FileRAII 對象,FileRAII 析構釋放資源
👉 這里智能指針管理 RAII 對象本身的生命周期,RAII 對象內部管理資源的生命周期。
? 智能指針和 RAII 類的關系總結
情況 | 是否需要智能指針 |
---|---|
RAII 類對象在棧上聲明(局部變量) | ? 不需要智能指針,作用域退出時自動釋放 |
RAII 類對象動態創建(new) | ? 推薦用智能指針管理(避免手工 delete) |
RAII 類對象需要跨多個作用域共享 | ? 用 shared_ptr |
RAII 類對象轉移所有權 | ? 用 unique_ptr |
📝 核心思路
RAII 解決資源管理,智能指針解決對象管理。它們可以單獨用,也可以組合用,取決于對象的使用方式。