C++ 智能指針原理、使用與最佳實(shí)踐指南
前言
在 C++ 編程中,動(dòng)態(tài)內(nèi)存管理是核心難點(diǎn)之一,手動(dòng)new/delete容易因異常處理、邏輯疏忽導(dǎo)致內(nèi)存泄漏。智能指針作為 RAII 思想的典型實(shí)現(xiàn),完美解決了這一問(wèn)題。本文將結(jié)合完整代碼示例,從使用場(chǎng)景、設(shè)計(jì)思路、標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)、核心原理到實(shí)際問(wèn)題解決,全面講解智能指針的相關(guān)知識(shí)。
一、智能指針的核心使用場(chǎng)景
手動(dòng)管理動(dòng)態(tài)內(nèi)存時(shí),異常會(huì)打斷程序執(zhí)行流程,導(dǎo)致后續(xù)delete語(yǔ)句無(wú)法執(zhí)行,最終造成內(nèi)存泄漏。以下是典型教學(xué)代碼:
double Divide(int a, int b) {
if (b == 0)
throw "Divide by zero condition!"; // 除0拋出異常
else
return (double)a / (double)b;
}
void Func() {
int* array1 = new int[10];
int* array2 = new int[10]; // 若此處拋異常,array1已無(wú)法釋放
try {
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...) {
// 捕獲異常后釋放資源,再重新拋出
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw;
}
// 正常流程釋放資源
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
catch (const exception& e) {
cout << e.what() << endl;
}
catch (...) {
cout << "未知異常" << endl;
}
return 0;
}痛點(diǎn)分析:為覆蓋所有異常場(chǎng)景,需要嵌套多層try-catch,代碼冗余且易出錯(cuò)。
二、RAII 思想與智能指針設(shè)計(jì)思路
2.1 RAII 核心思想
RAII(Resource Acquisition Is Initialization)即 “資源獲取即初始化”,本質(zhì)是一種利用對(duì)象生命周期來(lái)管理獲取到的動(dòng)態(tài)資源,避免資源泄漏的資源管理機(jī)制。
- 資源(內(nèi)存、文件句柄、網(wǎng)絡(luò)連接等)獲取時(shí),委托給一個(gè)對(duì)象管理。
- 資源在對(duì)象生命周期內(nèi)始終有效。
- 對(duì)象析構(gòu)時(shí)自動(dòng)釋放資源,確保資源不會(huì)泄漏。
2.2 智能指針的設(shè)計(jì)要點(diǎn)
智能指針需滿(mǎn)足兩個(gè)核心需求:遵循 RAII 思想、模擬原生指針行為。以下是簡(jiǎn)化的智能指針實(shí)現(xiàn):
template<class T>
class SmartPtr {
public:
// RAII:構(gòu)造時(shí)接管資源
SmartPtr(T* ptr) : _ptr(ptr) {}
// 析構(gòu)時(shí)自動(dòng)釋放資源
~SmartPtr() {
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重載運(yùn)算符,模擬指針行為
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T& operator[](size_t i) { return _ptr[i]; }
private:
T* _ptr; // 管理的資源指針
};2.3 優(yōu)化后的 Func 函數(shù)
使用自定義SmartPtr后,無(wú)需手動(dòng)釋放資源,異常場(chǎng)景下也能自動(dòng)析構(gòu):
double Divide(int a, int b) {
if (b == 0)
throw "Divide by zero condition!";
else
return (double)a / (double)b;
}
void Func() {
// 智能指針管理動(dòng)態(tài)數(shù)組,自動(dòng)釋放
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
sp1[i] = sp2[i] = i; // 重載[]運(yùn)算符,直接訪(fǎng)問(wèn)
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
catch (const exception& e) {
cout << e.what() << endl;
}
catch (...) {
cout << "未知異常" << endl;
}
return 0;
}三、C++ 標(biāo)準(zhǔn)庫(kù)智能指針的使用
C++ 標(biāo)準(zhǔn)庫(kù)提供的智能指針均定義在<memory>頭文件中,核心包括auto_ptr(已廢棄)、unique_ptr、shared_ptr、weak_ptr,它們的核心差異在于資源所有權(quán)管理機(jī)制。
3.1 各智能指針核心特性對(duì)比
| 智能指針 | 推出標(biāo)準(zhǔn) | 核心特性 | 適用場(chǎng)景 |
|---|---|---|---|
auto_ptr | C++98 | 拷貝時(shí)轉(zhuǎn)移資源所有權(quán) | 已廢棄,避免使用 |
unique_ptr | C++11 | 獨(dú)占資源,禁止拷貝,支持移動(dòng) | 無(wú)需共享資源的場(chǎng)景 |
shared_ptr | C++11 | 共享資源,引用計(jì)數(shù)實(shí)現(xiàn) | 需要拷貝 / 共享資源的場(chǎng)景 |
weak_ptr | C++11 | 不管理資源,不增加引用計(jì)數(shù) | 解決shared_ptr循環(huán)引用 |
3.2 基礎(chǔ)使用示例
首先定義測(cè)試類(lèi)Date,用于驗(yàn)證析構(gòu)行為:
struct Date {
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
~Date() {
cout << "~Date()" << endl;
}
};3.2.1auto_ptr(廢棄)
auto_ptr的拷貝會(huì)轉(zhuǎn)移資源所有權(quán),導(dǎo)致原對(duì)象懸空,訪(fǎng)問(wèn)時(shí)會(huì)崩潰:
int main() {
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // 拷貝后,ap1的資源所有權(quán)轉(zhuǎn)移給ap2
// ap1->_year++; // 錯(cuò)誤:ap1已懸空,空指針訪(fǎng)問(wèn)
return 0;
}3.2.2unique_ptr(獨(dú)占所有權(quán))
unique_ptr禁止拷貝,支持移動(dòng)語(yǔ)義(移動(dòng)后原對(duì)象懸空,需謹(jǐn)慎使用):
int main() {
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 錯(cuò)誤:禁止拷貝
unique_ptr<Date> up3(move(up1)); // 支持移動(dòng),up1懸空
return 0;
}3.2.3shared_ptr(共享所有權(quán))
shared_ptr通過(guò)引用計(jì)數(shù)跟蹤資源持有者數(shù)量,拷貝時(shí)計(jì)數(shù)遞增,析構(gòu)時(shí)計(jì)數(shù)遞減,計(jì)數(shù)為 0 時(shí)釋放資源:
int main() {
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // 拷貝,引用計(jì)數(shù)變?yōu)?
shared_ptr<Date> sp3(sp2); // 拷貝,引用計(jì)數(shù)變?yōu)?
cout << sp1.use_count() << endl; // 輸出3:查看引用計(jì)數(shù)
sp1->_year++; // 重載->,直接訪(fǎng)問(wèn)成員
cout << sp1->_year << endl; // 輸出2
cout << sp2->_year << endl; // 輸出2(共享同一資源)
cout << sp3->_year << endl; // 輸出2
shared_ptr<Date> sp4(move(sp1)); // 移動(dòng),sp1懸空
return 0;
}3.3 特殊資源管理:刪除器
智能指針默認(rèn)使用delete釋放資源,若管理new[]分配的數(shù)組或文件句柄等特殊資源,需自定義刪除器(可調(diào)用對(duì)象)。
3.3.1 管理new[]數(shù)組
unique_ptr和shared_ptr提供了new[]特化版本,直接支持?jǐn)?shù)組管理:
int main() {
// 特化版本,析構(gòu)時(shí)自動(dòng)調(diào)用delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
return 0;
}重點(diǎn): 自定義刪除器(三種方式)
此處提供仿函數(shù)、函數(shù)指針、lambda 表達(dá)式三種刪除器實(shí)現(xiàn):
// 1. 函數(shù)指針刪除器
template<class T>
void DeleteArrayFunc(T* ptr) {
delete[] ptr;
}
// 2. 仿函數(shù)刪除器
template<class T>
class DeleteArray {
public:
void operator()(T* ptr) {
delete[] ptr;
}
};
// 3. 文件句柄刪除器(仿函數(shù))
class Fclose {
public:
void operator()(FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main() {
// 仿函數(shù)作為刪除器(unique_ptr模板參數(shù)指定,shared_ptr構(gòu)造函數(shù)傳入)
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// 函數(shù)指針作為刪除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// lambda表達(dá)式作為刪除器
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 管理文件句柄
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}3.4 其他實(shí)用特性
make_shared構(gòu)造:直接通過(guò)參數(shù)初始化資源,更高效(減少一次內(nèi)存分配):
int main() {
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11); // 直接初始化日期
auto sp3 = make_shared<Date>(2024, 9, 11); // 自動(dòng)推導(dǎo)類(lèi)型
return 0;
}operator bool類(lèi)型轉(zhuǎn)換:直接判斷智能指針是否管理資源:
int main() {
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp4;
if (sp1) // 等價(jià)于sp1.operator bool(),管理資源返回true
cout << "sp1 is not nullptr" << endl;
if (!sp4) // 未管理資源返回false
cout << "sp4 is nullptr" << endl;
return 0;
}explicit構(gòu)造:禁止普通指針隱式轉(zhuǎn)換為智能指針(文末有介紹),避免意外錯(cuò)誤:
int main() {
// 錯(cuò)誤:explicit構(gòu)造禁止隱式轉(zhuǎn)換
// shared_ptr<Date> sp5 = new Date(2024, 9, 11);
// unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}四、智能指針的核心原理
4.1auto_ptr原理(管理權(quán)轉(zhuǎn)移)
auto_ptr的核心是 “拷貝時(shí)轉(zhuǎn)移資源所有權(quán)”,導(dǎo)致原對(duì)象懸空,這是其被廢棄的根本原因:
namespace zephyr {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr) : _ptr(ptr) {}
// 拷貝構(gòu)造:轉(zhuǎn)移所有權(quán)
auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) {
sp._ptr = nullptr; // 原對(duì)象懸空
}
// 賦值運(yùn)算符:轉(zhuǎn)移所有權(quán)
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
if (this != &ap) { // 避免自賦值
if (_ptr)
delete _ptr; // 釋放當(dāng)前資源
_ptr = ap._ptr; // 轉(zhuǎn)移資源
ap._ptr = nullptr; // 原對(duì)象懸空
}
return *this;
}
// 析構(gòu)釋放資源
~auto_ptr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 模擬指針行為
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
}4.2unique_ptr原理(禁止拷貝)
unique_ptr通過(guò) “刪除拷貝構(gòu)造和賦值運(yùn)算符” 實(shí)現(xiàn)獨(dú)占所有權(quán),僅支持移動(dòng):
namespace zephyr {
template<class T>
class unique_ptr {
public:
explicit unique_ptr(T* ptr) : _ptr(ptr) {}
// 移動(dòng)構(gòu)造:轉(zhuǎn)移資源所有權(quán)
unique_ptr(unique_ptr<T>&& sp) : _ptr(sp._ptr) {
sp._ptr = nullptr;
}
// 移動(dòng)賦值:轉(zhuǎn)移資源所有權(quán)
unique_ptr<T>& operator=(unique_ptr<T>&& sp) {
if (_ptr)
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
return *this;
}
// 析構(gòu)釋放資源
~unique_ptr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 模擬指針行為
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 禁止拷貝:刪除拷貝構(gòu)造和賦值運(yùn)算符
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}4.3shared_ptr原理(引用計(jì)數(shù))
shared_ptr的核心是 “引用計(jì)數(shù)”,通過(guò)堆上的計(jì)數(shù)變量跟蹤資源持有者數(shù)量:
namespace zephyr {
template<class T>
class shared_ptr {
public:
// 構(gòu)造:初始化資源和引用計(jì)數(shù)(計(jì)數(shù)初始為1)
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
, _del([](T* ptr) { delete ptr; }) {} // 默認(rèn)刪除器
// 帶自定義刪除器的構(gòu)造
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del) {}
// 拷貝構(gòu)造:共享資源,引用計(jì)數(shù)+1
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del) {
++(*_pcount);
}
// 賦值運(yùn)算符:釋放當(dāng)前資源,共享新資源
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) { // 避免自賦值
release(); // 釋放當(dāng)前資源(計(jì)數(shù)-1,為0則刪除)
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = sp._del;
++(*_pcount); // 新資源計(jì)數(shù)+1
}
return *this;
}
// 釋放資源邏輯
void release() {
if (--(*_pcount) == 0) { // 計(jì)數(shù)為0,釋放資源和計(jì)數(shù)變量
_del(_ptr); // 調(diào)用刪除器
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
// 析構(gòu):調(diào)用release釋放資源
~shared_ptr() {
release();
}
// 模擬指針行為
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 實(shí)用接口
T* get() const { return _ptr; } // 獲取原生指針
int use_count() const { return *_pcount; } // 獲取引用計(jì)數(shù)
private:
T* _ptr; // 管理的資源指針
int* _pcount; // 引用計(jì)數(shù)(堆上分配,支持共享)
function<void(T*)> _del; // 自定義刪除器
};
}4.4weak_ptr原理(輔助共享)
weak_ptr不管理資源,僅作為shared_ptr的輔助,不增加引用計(jì)數(shù),用于解決循環(huán)引用問(wèn)題:
namespace zephyr {
template<class T>
class weak_ptr {
public:
weak_ptr() : _ptr(nullptr) {}
// 僅支持通過(guò)shared_ptr構(gòu)造/賦值,不增加引用計(jì)數(shù)
weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
_ptr = sp.get();
return *this;
}
private:
T* _ptr; // 僅存儲(chǔ)資源指針,不參與管理
};
}五、shared_ptr的核心問(wèn)題與解決方案
5.1 循環(huán)引用問(wèn)題
shared_ptr的共享特性可能導(dǎo)致循環(huán)引用,使引用計(jì)數(shù)無(wú)法歸零,最終造成內(nèi)存泄漏。
問(wèn)題代碼(循環(huán)引用)
struct ListNode {
int _data;
shared_ptr<ListNode> _next; // 共享下一個(gè)節(jié)點(diǎn)
shared_ptr<ListNode> _prev; // 共享上一個(gè)節(jié)點(diǎn)
~ListNode() {
cout << "~ListNode()" << endl; // 循環(huán)引用時(shí)不會(huì)執(zhí)行
}
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl; // 輸出1
cout << n2.use_count() << endl; // 輸出1
n1->_next = n2; // n2的引用計(jì)數(shù)變?yōu)?
n2->_prev = n1; // n1的引用計(jì)數(shù)變?yōu)?
cout << n1.use_count() << endl; // 輸出2
cout << n2.use_count() << endl; // 輸出2
// 析構(gòu)n1和n2時(shí),引用計(jì)數(shù)均變?yōu)?,無(wú)法歸零,資源泄漏
return 0;
}問(wèn)題分析
n1的析構(gòu)依賴(lài)n2->_prev的釋放。n2的析構(gòu)依賴(lài)n1->_next的釋放。- 兩者形成循環(huán)依賴(lài),引用計(jì)數(shù)無(wú)法歸零,資源永遠(yuǎn)不會(huì)釋放。
循環(huán)引用問(wèn)題總結(jié)分析
1. 循環(huán)引用的 “本質(zhì)原因”:引用計(jì)數(shù)無(wú)法歸 0
循環(huán)引用導(dǎo)致內(nèi)存泄漏的核心是:當(dāng)所有外部
shared_ptr(如main中的n1、n2)的生命周期結(jié)束后,對(duì)象之間的內(nèi)部shared_ptr引用仍然互相維持,使得每個(gè)對(duì)象的引用計(jì)數(shù)都大于 0,從而無(wú)法觸發(fā)析構(gòu)和內(nèi)存釋放。
2. 循環(huán)引用的 “結(jié)構(gòu)表現(xiàn)”:不一定是嚴(yán)格的 “環(huán)形”,但必然存在 “互相引用的環(huán)”
所謂 “循環(huán)”,是這種 “引用計(jì)數(shù)無(wú)法歸 0” 的典型結(jié)構(gòu)表現(xiàn),但不是唯一形式。只要存在對(duì)象之間通過(guò)
shared_ptr互相引用形成的 “環(huán)”,就會(huì)導(dǎo)致該問(wèn)題。
解決方案:weak_ptr
將ListNode的_next和_prev改為weak_ptr,不增加引用計(jì)數(shù),打破循環(huán):
struct ListNode {
int _data;
weak_ptr<ListNode> _next; // 改為weak_ptr
weak_ptr<ListNode> _prev; // 改為weak_ptr
~ListNode() {
cout << "~ListNode()" << endl; // 正常執(zhí)行
}
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // weak_ptr不增加n2的引用計(jì)數(shù)(仍為1)
n2->_prev = n1; // weak_ptr不增加n1的引用計(jì)數(shù)(仍為1)
// 析構(gòu)n1和n2時(shí),引用計(jì)數(shù)均變?yōu)?,資源正常釋放
return 0;
}5.2weak_ptr的實(shí)用接口
weak_ptr不直接訪(fǎng)問(wèn)資源(無(wú)operator*/operator->),需通過(guò)lock()獲取shared_ptr,確保資源有效:
int main() {
shared_ptr<string> sp1(new string("111111"));
shared_ptr<string> sp2(sp1);
weak_ptr<string> wp = sp1;
cout << wp.expired() << endl; // 輸出0:資源未過(guò)期
cout << wp.use_count() << endl; // 輸出2:獲取引用計(jì)數(shù)
// sp1和sp2轉(zhuǎn)移資源,原資源釋放
sp1 = make_shared<string>("222222");
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl; // 輸出1:資源已過(guò)期
cout << wp.use_count() << endl; // 輸出0
// 重新綁定資源
wp = sp1;
auto sp3 = wp.lock(); // 資源有效時(shí),返回非空shared_ptr
if (sp3) {
*sp3 += "###";
cout << *sp1 << endl; // 輸出"222222###"
}
return 0;
}六、shared_ptr的線(xiàn)程安全問(wèn)題
6.1 問(wèn)題本質(zhì)
- 引用計(jì)數(shù)的線(xiàn)程安全:
shared_ptr的引用計(jì)數(shù)存儲(chǔ)在堆上,多線(xiàn)程拷貝 / 析構(gòu)時(shí)會(huì)并發(fā)修改計(jì)數(shù),導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。 - 資源對(duì)象的線(xiàn)程安全:
shared_ptr指向的對(duì)象本身無(wú)線(xiàn)程安全保障,需用戶(hù)手動(dòng)控制(如加鎖)。
問(wèn)題代碼(線(xiàn)程安全隱患)
struct AA {
int _a1 = 0;
int _a2 = 0;
~AA() {
cout << "~AA()" << endl;
}
};
int main() {
zephyr::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
// 多線(xiàn)程拷貝智能指針,修改資源對(duì)象
auto func = [&]() {
for (size_t i = 0; i < n; ++i) {
zephyr::shared_ptr<AA> copy(p); // 拷貝時(shí)修改引用計(jì)數(shù)(線(xiàn)程不安全)
unique_lock<mutex> lk(mtx); // 資源對(duì)象加鎖,保證線(xiàn)程安全
copy->_a1++;
copy->_a2++;
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl; // 可能小于200000(計(jì)數(shù)競(jìng)爭(zhēng)導(dǎo)致部分操作失效)
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}6.2 解決方案
將引用計(jì)數(shù)改為原子類(lèi)型(atomic<int>),保證計(jì)數(shù)修改的原子性:
namespace zephyr {
template<class T>
class shared_ptr {
private:
// 其他成員不變,僅修改引用計(jì)數(shù)類(lèi)型
atomic<int>* _pcount; // 原子類(lèi)型,保證線(xiàn)程安全
};
}七、C++11 與 Boost 智能指針的關(guān)系
Boost 庫(kù)是 C++ 標(biāo)準(zhǔn)庫(kù)的重要參考,智能指針的發(fā)展歷程如下:
- C++98:推出首個(gè)智能指針
auto_ptr,設(shè)計(jì)缺陷明顯。 - Boost 庫(kù):提供
scoped_ptr(獨(dú)占)、shared_ptr(共享)、weak_ptr(輔助)、scoped_array(數(shù)組獨(dú)占)等,實(shí)用性更強(qiáng)。 - C++ TR1:引入
shared_ptr,但非標(biāo)準(zhǔn)正式內(nèi)容。 - C++11:正式引入
unique_ptr(對(duì)應(yīng) Boost 的scoped_ptr)、shared_ptr、weak_ptr,實(shí)現(xiàn)原理參考 Boost 庫(kù)。
八、內(nèi)存泄漏的全面解析
8.1 內(nèi)存泄漏的定義與危害
- 定義:程序分配內(nèi)存后,因設(shè)計(jì)錯(cuò)誤失去對(duì)該內(nèi)存的控制,導(dǎo)致內(nèi)存無(wú)法回收。
- 危害:短期運(yùn)行程序影響較小,長(zhǎng)期運(yùn)行程序(如服務(wù)器、操作系統(tǒng))會(huì)因可用內(nèi)存持續(xù)減少,導(dǎo)致響應(yīng)變慢甚至崩潰。
示例(無(wú)害的內(nèi)存泄漏)
int main() {
// 分配1G內(nèi)存未釋放,但程序立即結(jié)束,進(jìn)程退出時(shí)系統(tǒng)回收資源
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}8.2 內(nèi)存泄漏的檢測(cè)工具
- Linux:Valgrind、AddressSanitizer。
- Windows:VLD(Visual Leak Detector)、BoundsChecker。
8.3 內(nèi)存泄漏的避免方法
- 遵循良好的編碼規(guī)范,手動(dòng)匹配
new/delete(理想狀態(tài))。 - 優(yōu)先使用智能指針管理動(dòng)態(tài)資源,利用 RAII 思想自動(dòng)釋放。
- 自定義 RAII 類(lèi)管理特殊資源(如文件句柄、網(wǎng)絡(luò)連接)。
- 項(xiàng)目上線(xiàn)前使用檢測(cè)工具排查泄漏。
總結(jié)
智能指針是 C++ 動(dòng)態(tài)內(nèi)存管理的核心解決方案,基于 RAII 思想實(shí)現(xiàn)資源的自動(dòng)釋放。unique_ptr適用于獨(dú)占資源場(chǎng)景,shared_ptr適用于共享資源場(chǎng)景,weak_ptr用于解決循環(huán)引用問(wèn)題。掌握智能指針的原理、使用場(chǎng)景及核心問(wèn)題(循環(huán)引用、線(xiàn)程安全),能有效避免內(nèi)存泄漏,提升代碼的健壯性和可維護(hù)性。
文章相關(guān)問(wèn)題答疑及講解:
1.shared_ptr和unique_ptr都得構(gòu)造函數(shù)都使?explicit 修飾,防止普通指針隱式類(lèi)型轉(zhuǎn)換成智能指針對(duì)象。
場(chǎng)景模擬:未用 explicit 修飾的智能指針構(gòu)造函數(shù)
假設(shè)我們自定義一個(gè)簡(jiǎn)化的智能指針(模擬未加 explicit 的情況):
template<class T>
class MySmartPtr {
public:
// 未用 explicit 修飾的構(gòu)造函數(shù)(危險(xiǎn)!)
MySmartPtr(T* ptr) : _ptr(ptr) {}
~MySmartPtr() {
delete _ptr; // 析構(gòu)時(shí)釋放內(nèi)存
cout << "內(nèi)存已釋放" << endl;
}
T& operator*() { return *_ptr; }
private:
T* _ptr;
};普通指針的隱式轉(zhuǎn)換案例
定義一個(gè)接收智能指針作為參數(shù)的函數(shù),然后傳遞普通指針:
// 函數(shù)參數(shù)為智能指針類(lèi)型
void UseResource(MySmartPtr<int> sp) {
cout << "使用資源:" << *sp << endl;
}
int main() {
int* raw_ptr = new int(100); // 普通指針
// 隱式轉(zhuǎn)換:raw_ptr 被自動(dòng)轉(zhuǎn)換為 MySmartPtr<int> 對(duì)象
UseResource(raw_ptr);
// 危險(xiǎn)!此時(shí) raw_ptr 指向的內(nèi)存已被 UseResource 中智能指針的析構(gòu)函數(shù)釋放
cout << *raw_ptr << endl; // 訪(fǎng)問(wèn)已釋放內(nèi)存(未定義行為,可能崩潰)
delete raw_ptr; // 重復(fù)釋放(必然崩潰)
return 0;
}2.自定義刪除器的必要性(何時(shí)需要傳遞析構(gòu)方法)
只有當(dāng)需要管理非new分配的資源(如文件句柄、動(dòng)態(tài)庫(kù)指針、通過(guò) malloc 分配的內(nèi)存等)時(shí),才需要顯式傳遞自定義刪除器。例如:
- 釋放
malloc分配的內(nèi)存需要用free,而非delete; - 關(guān)閉文件句柄需要用
fclose,而非內(nèi)存釋放操作。
我的代碼的模擬實(shí)現(xiàn)中,shared_ptr 的構(gòu)造函數(shù) explicit shared_ptr(T* ptr, X del) 正是為這種場(chǎng)景設(shè)計(jì)的 —— 允許用戶(hù)傳遞自定義的釋放邏輯(del),而默認(rèn)構(gòu)造函數(shù) explicit shared_ptr(T* ptr = nullptr) 則使用默認(rèn)刪除器 [](T* ptr) {delete ptr;},這與標(biāo)準(zhǔn)庫(kù)的設(shè)計(jì)思路完全一致。
標(biāo)準(zhǔn)庫(kù)中傳遞自定義刪除器的示例:
#include <memory>
#include <cstdio>
using namespace std;
int main() {
// 管理 FILE* 資源,自定義刪除器用 fclose 釋放
FILE* fp = fopen("test.txt", "w");
shared_ptr<FILE> sp(fp, [](FILE* p) {
fclose(p);
cout << "文件已關(guān)閉" << endl;
});
return 0;
}3.為什么使用unique_ptr時(shí)函數(shù)指針 /lambda 也需要傳入實(shí)例?
1)unique_ptr的模板參數(shù):僅指定刪除器的 “類(lèi)型”
unique_ptr的模板參數(shù)class Deleter(第二個(gè)參數(shù))的作用是聲明刪除器的類(lèi)型,它告訴編譯器:“這個(gè)unique_ptr將使用某種類(lèi)型的刪除器來(lái)釋放資源”。
例如:
unique_ptr<Date, void(*)(Date*)> up3;
這里的void(*)(Date*)是一個(gè)函數(shù)指針類(lèi)型(指向 “接收Date*參數(shù)、返回void的函數(shù)” 的指針),它僅聲明了 “up3的刪除器必須是這種類(lèi)型的函數(shù)指針”,但并沒(méi)有指定具體用哪個(gè)函數(shù)指針。
2) 構(gòu)造函數(shù)的參數(shù):必須提供刪除器的 “實(shí)例”
unique_ptr需要知道具體調(diào)用哪個(gè)刪除器來(lái)釋放資源。模板參數(shù)只規(guī)定了刪除器的類(lèi)型,而具體的刪除器實(shí)例(即實(shí)際執(zhí)行刪除操作的函數(shù)指針)必須通過(guò)構(gòu)造函數(shù)傳入。
例如,DeleteArrayFunc<Date>是一個(gè)符合void(*)(Date*)類(lèi)型的函數(shù)指針實(shí)例(它指向真正執(zhí)行delete[]的函數(shù))。如果不傳入這個(gè)實(shí)例:
// 錯(cuò)誤示例:只指定類(lèi)型,未傳入具體刪除器實(shí)例 unique_ptr<Date, void(*)(Date*)> up3(new Date[5]);
unique_ptr將無(wú)法知道 “到底用哪個(gè)函數(shù)指針來(lái)釋放new Date[5]分配的資源”,編譯時(shí)會(huì)報(bào)錯(cuò)(缺少刪除器實(shí)例)。
其實(shí)不僅是函數(shù)指針,仿函數(shù)和 lambda 作為刪除器時(shí),都應(yīng)傳入實(shí)例,仿函數(shù)沒(méi)有傳入實(shí)例是因?yàn)樵趗nique_ptr構(gòu)造時(shí)隱式構(gòu)造了一個(gè)仿函數(shù)實(shí)例:
// 仿函數(shù)刪除器: unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>()); // (省略時(shí)其實(shí)是隱式構(gòu)造了一個(gè)實(shí)例,等價(jià)于上面的寫(xiě)法) // lambda刪除器:傳入了delArrOBJ這個(gè)實(shí)例 unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); //lambda表達(dá)式的特殊之處在于無(wú)法用推導(dǎo)出的類(lèi)型來(lái)隱式構(gòu)造一個(gè)實(shí)例 //lambda 閉包類(lèi)型的構(gòu)造函數(shù)是 “受限的”,因?yàn)殚]包類(lèi)型沒(méi)有默認(rèn)構(gòu)造函數(shù),且用戶(hù)無(wú)法手動(dòng)調(diào)用其構(gòu)造函數(shù)

到此這篇關(guān)于C++ 智能指針原理、使用與實(shí)踐的文章就介紹到這了,更多相關(guān)C++ 智能指針使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于OpenGL實(shí)現(xiàn)多段Bezier曲線(xiàn)拼接
這篇文章主要為大家詳細(xì)介紹了基于OpenGL實(shí)現(xiàn)多段Bezier曲線(xiàn)拼接,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04
C語(yǔ)言如何實(shí)現(xiàn)BOOL類(lèi)型
這篇文章主要介紹了C語(yǔ)言如何實(shí)現(xiàn)BOOL類(lèi)型問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
C/C++中g(shù)etline函數(shù)案例總結(jié)
這篇文章主要介紹了C/C++中g(shù)etline函數(shù)案例總結(jié),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09
C語(yǔ)言實(shí)現(xiàn)紙牌計(jì)算24點(diǎn)小游戲
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)紙牌計(jì)算24點(diǎn)小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
Qt6.0開(kāi)發(fā)環(huán)境搭建步驟(圖文)
這篇文章主要介紹了Qt6.0開(kāi)發(fā)環(huán)境搭建步驟(圖文),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03

