📌 1. 野指針 (Wild Pointer)
什么是野指針?
野指針指的是未初始化的指針變量。它指向的內存地址是隨機的、未知的。
產生原因
cpp
int* ptr; // 野指針!未初始化,指向隨機地址 *ptr = 10; // 危險!可能破壞系統內存char* str; // 同樣是野指針 std::cout << *str; // 未定義行為
危險后果
** segmentation fault**:訪問受保護的內存區域
數據損壞:意外修改了其他程序或系統的數據
難以調試:錯誤表現隨機,難以重現
解決方法
總是初始化指針:
cpp
int* ptr = nullptr; // 初始化為空指針 int* ptr2 = new int(10); // 初始化為有效內存 int* ptr3 = &someVariable; // 指向已有變量
📌 2. 懸空指針 (Dangling Pointer)
什么是懸空指針?
懸空指針指的是指針指向的內存已被釋放,但指針本身仍然保存著原來的地址。
產生原因
情況1:釋放后未置空
cpp
int* ptr = new int(10); delete ptr; // 內存已釋放 // 現在ptr是懸空指針! *ptr = 20; // 危險!訪問已釋放內存
情況2:局部變量返回
cpp
int* createArray() {int arr[5] = {1, 2, 3, 4, 5};return arr; // 返回局部數組的指針 } // arr的內存被釋放int* danglingPtr = createArray(); // 懸空指針! std::cout << danglingPtr[0]; // 未定義行為
情況3:多個指針指向同一內存
cpp
int* ptr1 = new int(100); int* ptr2 = ptr1; // 兩個指針指向同一內存delete ptr1; // 釋放內存 // 現在ptr1和ptr2都是懸空指針! std::cout << *ptr2; // 危險!
危險后果
** use-after-free**:使用已釋放的內存
數據損壞:可能覆蓋新分配的內存數據
安全漏洞:可能被利用進行攻擊
解決方法
方法1:釋放后立即置空
cpp
int* ptr = new int(10); delete ptr; ptr = nullptr; // 重要!釋放后立即置空if (ptr != nullptr) { // 安全檢查*ptr = 20; }
方法2:使用智能指針(推薦)
cpp
#include <memory> std::shared_ptr<int> ptr1 = std::make_shared<int>(10); std::shared_ptr<int> ptr2 = ptr1; // 共享所有權// 不需要手動delete,自動管理生命周期 // 當所有shared_ptr都超出作用域時,內存自動釋放
方法3:避免返回局部變量地址
cpp
// 錯誤 int* createArray() {int arr[5] = {1, 2, 3, 4, 5};return arr; // 不要這樣做! }// 正確:動態分配 int* createArray() {int* arr = new int[5]{1, 2, 3, 4, 5};return arr; // 調用者需要負責delete[] }// 更正確:使用vector std::vector<int> createArray() {return {1, 2, 3, 4, 5}; // 安全! }
📌 一、什么是內存對齊?
內存對齊指的是數據在內存中的存儲地址必須是某個值(通常是2、4、8、16等2的冪次方)的整數倍。
簡單例子
cpp
struct Example {char a; // 1字節int b; // 4字節 short c; // 2字節 };
沒有對齊時(假設從地址0開始):
text
地址: 0 1 2 3 4 5 6 7 8 9 數據: [a][b][b][b][b][c][c][ ][ ][ ] 總大小: 7字節?但實際上...
實際對齊后(在64位系統 typical alignment):
text
地址: 0 1 2 3 4 5 6 7 8 9 10 11 數據: [a][ ][ ][ ][b][b][b][b][c][c][ ][ ] 總大小: 12字節!(因為有填充字節)
📌 二、為什么需要內存對齊?
1.?硬件要求(最主要的原因)
現代CPU不是以字節為單位訪問內存,而是以字長(word size)為單位(通常為4字節或8字節)。
不對齊訪問的代價:
cpp
// 假設int需要4字節對齊,但存儲在地址0x3 int* ptr = (int*)0x3; int value = *ptr; // CPU需要2次內存訪問!
CPU需要:
讀取地址0x0-0x3的4字節
讀取地址0x4-0x7的4字節
拼接出需要的4字節數據
2.?性能優化
對齊的內存訪問只需要1次內存操作,而不是2次或更多。
性能對比:
訪問類型 | CPU操作次數 | 性能影響 |
---|---|---|
對齊訪問 | 1次 | ? 最快 |
不對齊訪問 | 2次 | 🐢 慢2倍 |
嚴重不對齊 | 多次 | 🚫 極慢 |
3.?平臺兼容性
某些架構(如ARM、SPARC)根本不允許未對齊的內存訪問,會導致硬件異常。
cpp
// 在ARM架構上可能直接崩潰! int* misaligned_ptr = (int*)(char_buffer + 1); int value = *misaligned_ptr; // 硬件異常!
📌 三、對齊規則和示例
基本對齊規則
每個數據類型都有自然的對齊要求:
char
:1字節對齊short
:2字節對齊int
:4字節對齊float
:4字節對齊double
:8字節對齊指針:4字節(32位)或8字節(64位)對齊
結構體對齊示例
cpp
struct MyStruct {char a; // 1字節,偏移0// 3字節填充(因為int需要4字節對齊)int b; // 4字節,偏移4 short c; // 2字節,偏移8// 2字節填充(使整體大小為最大成員的整數倍) }; // 總大小:12字節
內存布局:
text
偏移: 0 1 2 3 4 5 6 7 8 9 10 11 數據: [a][pad][pad][pad][b][b][b][b][c][c][pad][pad]
📌 四、如何控制內存對齊?
1. 編譯器指令(通用)
cpp
// 強制4字節對齊 struct alignas(4) MyStruct {char a;int b;short c; }; // 大小:8字節(而不是12字節)// 或者使用pragma(編譯器特定) #pragma pack(push, 1) // 強制1字節對齊(無填充) struct TightPacked {char a;int b; short c; }; // 大小:7字節 #pragma pack(pop) // 恢復默認對齊
2. C++11 alignas 關鍵字
cpp
#include <iostream>struct alignas(16) AlignedStruct {int a;double b;char c; };int main() {std::cout << "Alignment: " << alignof(AlignedStruct) << std::endl;std::cout << "Size: " << sizeof(AlignedStruct) << std::endl;return 0; }
3. 動態內存對齊
cpp
#include <cstdlib> #include <iostream>// C11/C++17 的動態對齊分配 void* aligned_memory = std::aligned_alloc(64, 1024); // 64字節對齊,分配1KB // 使用... std::free(aligned_memory);
在 C++ 中,智能指針是一種封裝了原始指針的類模板,用于自動管理動態內存,避免內存泄漏。它們通過 RAII(資源獲取即初始化)機制,在離開作用域時自動釋放所指向的內存。C++ 標準庫提供了三種主要的智能指針:unique_ptr
、shared_ptr
?和?weak_ptr
,它們各自有不同的特性和用途。
1.?unique_ptr
:獨占所有權的智能指針
- 特性:同一時間內,只能有一個?
unique_ptr
?指向某塊內存,即所有權是獨占的。 - 行為:
- 不允許拷貝(
copy
),但允許移動(move
),即所有權可以轉移。 - 當?
unique_ptr
?離開作用域或被銷毀時,會自動釋放所指向的內存。
- 不允許拷貝(
- 適用場景:
- 管理單個對象的動態內存,且不需要共享所有權。
- 作為函數的返回值或參數(通過移動語義傳遞)。
- 示例:
cpp
運行
#include <memory> int main() {std::unique_ptr<int> ptr1(new int(10)); // 獨占指向10的內存// std::unique_ptr<int> ptr2 = ptr1; // 錯誤:不能拷貝std::unique_ptr<int> ptr2 = std::move(ptr1); // 正確:轉移所有權(ptr1變為空)return 0; } // ptr2離開作用域,自動釋放內存
2.?shared_ptr
:共享所有權的智能指針
- 特性:允許多個?
shared_ptr
?指向同一塊內存,通過引用計數跟蹤所有者數量。 - 行為:
- 當一個?
shared_ptr
?被拷貝時,引用計數加 1;當被銷毀時,引用計數減 1。 - 當引用計數減為 0 時,自動釋放所指向的內存。
- 當一個?
- 適用場景:
- 需要多個對象共享同一資源的所有權(例如:樹結構中父節點和子節點互相引用)。
- 注意:
- 避免循環引用(如兩個?
shared_ptr
?互相指向對方),會導致引用計數無法歸零,內存泄漏。此時需配合?weak_ptr
?解決。
- 避免循環引用(如兩個?
- 示例:
cpp
運行
#include <memory> int main() {std::shared_ptr<int> ptr1(new int(20));std::shared_ptr<int> ptr2 = ptr1; // 引用計數變為2{std::shared_ptr<int> ptr3 = ptr1; // 引用計數變為3} // ptr3銷毀,引用計數變為2return 0; } // ptr1和ptr2銷毀,引用計數變為0,內存釋放
3.?weak_ptr
:弱引用的智能指針
- 特性:一種 “弱引用”,不擁有所指向內存的所有權,也不影響引用計數。
- 行為:
- 必須從?
shared_ptr
?轉換而來,無法直接管理內存。 - 可以通過?
lock()
?方法獲取一個?shared_ptr
(若內存未釋放),否則返回空。
- 必須從?
- 適用場景:
- 解決?
shared_ptr
?的循環引用問題。 - 需訪問某資源,但不希望延長其生命周期(例如:緩存、觀察者模式)。
- 解決?
- 示例:
cpp
運行
#include <memory> struct Node {std::weak_ptr<Node> parent; // 用weak_ptr避免循環引用// std::shared_ptr<Node> parent; // 若用shared_ptr,會導致循環引用 };int main() {std::shared_ptr<Node> child(new Node());std::shared_ptr<Node> parent(new Node());child->parent = parent; // weak_ptr不增加引用計數return 0; } // 引用計數正常歸零,內存釋放
三者的核心區別總結
智能指針 | 所有權 | 拷貝 / 移動 | 引用計數 | 主要用途 |
---|---|---|---|---|
unique_ptr | 獨占 | 禁止拷貝,允許移動 | 無 | 管理單個對象,不共享所有權 |
shared_ptr | 共享 | 允許拷貝和移動 | 有 | 多對象共享同一資源 |
weak_ptr | 無所有權 | 允許拷貝和移動 | 不影響 | 解決循環引用,弱引用資源 |
通過合理使用這三種智能指針,可以大幅減少手動管理內存的錯誤,使 C++ 代碼更安全、更易維護。
在 C++ 中,循環引用(Circular Reference)?是使用shared_ptr
時容易出現的問題,它會導致內存泄漏。這一問題的根源在于shared_ptr
的引用計數機制與循環依賴的結合,使得引用計數無法歸零,最終導致動態內存無法釋放。
什么是循環引用?
當兩個或多個shared_ptr
互相持有對方的引用,形成一個 “閉環” 時,就會發生循環引用。此時,每個shared_ptr
的引用計數都無法減到 0,導致它們指向的內存永遠不會被釋放,造成內存泄漏。
舉個具體例子
假設我們有兩個類A
和B
,它們互相用shared_ptr
引用對方:
cpp
運行
#include <memory>class B; // 前向聲明class A {
public:std::shared_ptr<B> b_ptr; // A持有B的shared_ptr~A() { std::cout << "A被銷毀" << std::endl; }
};class B {
public:std::shared_ptr<A> a_ptr; // B持有A的shared_ptr~B() { std::cout << "B被銷毀" << std::endl; }
};int main() {{std::shared_ptr<A> a(new A());std::shared_ptr<B> b(new B());// 形成循環引用:a持有b,b持有aa->b_ptr = b;b->a_ptr = a;} // 離開作用域,預期A和B被銷毀return 0;
}
運行結果:程序不會輸出 “A 被銷毀” 和 “B 被銷毀”,說明A
和B
的內存沒有被釋放,發生了內存泄漏。
為什么會發生內存泄漏?
我們一步步分析引用計數的變化:
- 創建
a
(指向 A 對象)時,A 的引用計數為 1。 - 創建
b
(指向 B 對象)時,B 的引用計數為 1。 a->b_ptr = b
:B 的引用計數變為 2(b
和a->b_ptr
共同引用)。b->a_ptr = a
:A 的引用計數變為 2(a
和b->a_ptr
共同引用)。- 離開作用域時,
a
和b
被銷毀:a
銷毀:A 的引用計數從 2 減為 1(剩余b->a_ptr
的引用)。b
銷毀:B 的引用計數從 2 減為 1(剩余a->b_ptr
的引用)。
- 此時,A 和 B 的引用計數都為 1,永遠不會再減為 0,它們的內存永遠不會被釋放。
如何解決循環引用?
使用weak_ptr
可以打破循環引用。weak_ptr
是一種 “弱引用”,它持有對對象的引用,但不增加引用計數,因此不會影響對象的生命周期。
修改上面的例子,將其中一個shared_ptr
改為weak_ptr
:
cpp
運行
#include <memory>
#include <iostream>class B;class A {
public:std::shared_ptr<B> b_ptr; // 仍用shared_ptr~A() { std::cout << "A被銷毀" << std::endl; }
};class B {
public:std::weak_ptr<A> a_ptr; // 改為weak_ptr,不增加引用計數~B() { std::cout << "B被銷毀" << std::endl; }
};int main() {{std::shared_ptr<A> a(new A());std::shared_ptr<B> b(new B());a->b_ptr = b; // B的引用計數變為2b->a_ptr = a; // A的引用計數仍為1(weak_ptr不增加計數)} // 離開作用域return 0;
}
運行結果:輸出 “A 被銷毀” 和 “B 被銷毀”,內存正常釋放。
原因分析:
b->a_ptr
是weak_ptr
,賦值時 A 的引用計數仍為 1(不增加)。- 離開作用域時,
a
銷毀:A 的引用計數從 1 減為 0 → A 被釋放。 - A 釋放后,其成員
b_ptr
(指向 B)被銷毀:B 的引用計數從 2 減為 1。 - 隨后
b
銷毀:B 的引用計數從 1 減為 0 → B 被釋放。
總結
- 循環引用是
shared_ptr
的常見陷阱,由互相引用的shared_ptr
形成閉環導致。 - 核心問題:引用計數無法歸零,內存無法釋放。
- 解決方案:在循環引用的一方使用
weak_ptr
,打破計數閉環。 weak_ptr
的適用場景:需要引用對象,但不希望影響其生命周期(如解決循環引用、緩存等)。
通過合理搭配shared_ptr
和weak_ptr
,可以既享受共享所有權的便利,又避免循環引用帶來的內存泄漏。