前言

作為 C++ 開發者,你是否曾因以下場景頭疼不已?

  • 函數中new了數組,卻因異常拋出導致後續delete沒執行,排查半天定位到內存泄漏;
  • 多模塊共享一塊內存,不知道該由誰負責釋放,最後要麼重複釋放崩潰,要麼漏釋放泄漏;
  • 用了auto_ptr後,拷貝對象導致原對象 “懸空”,訪問時直接崩潰卻找不到原因。

如果你有過這些經歷,那智能指針一定是你必須掌握的現代 C++ 工具。它基於 RAII 思想,自動管理動態資源,讓你無需手動delete,從根源上減少內存泄漏風險。今天,我們就從 “為什麼需要智能指針” 到 “不同智能指針的實戰場景”,帶你係統掌握這一核心特性。

請君瀏覽

  • 前言
  • 一、智能指針的誕生:解決手動管理內存的 “千古難題”
  • 1.1 一個典型的內存泄露場景
  • 1.2 智能指針的核心:RAII 思想
  • 二、C++ 標準庫智能指針:4 種指針的特性與適用場景
  • 2.1 auto_ptr:被淘汰的 “過渡品”(C++98)
  • 2.2 unique_ptr:不可共享的 “獨佔指針”(C++11)
  • 2.3 shared_ptr:可共享的 “計數指針”(C++11)
  • 2.4 weak_ptr:解決循環引用的 “輔助指針”(C++11)
  • 2.5 刪除器
  • 3. shared_ptr的模擬實現
  • 3.1 原理
  • 3.2 代碼
  • 4. 總結:智能指針的最佳實踐
  • 尾聲

一、智能指針的誕生:解決手動管理內存的 “千古難題”

在 C++ 中,內存泄漏的核心原因往往是 “資源申請與釋放不匹配”—— 尤其是當程序流程被異常、分支跳轉打斷時,手動編寫的delete可能永遠不會執行。

**內存泄漏:**內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使⽤的內存,⼀般是忘記釋放或者發⽣異常釋放程序未能執⾏導致的。內存泄漏並不是指內存在物理上的消失,⽽是應⽤程序分配某段內存後,因為設計錯誤,失去了對該段內存的控制,因⽽造成了內存的浪費。

**危害:**普通程序運⾏⼀會就結束了,出現內存泄漏問題也不大,進程正常結束,⻚表的映射關係解除,物理內存也可以釋放。但⻓期運⾏的程序出現內存泄漏影響就很⼤了,如操作系統、後台服務、⻓時間運⾏的客⼾端等等,不斷出現內存泄漏會導致可⽤內存不斷變少,各種功能響應越來越慢,最終卡死。

1.1 一個典型的內存泄露場景

若函數中存在異常拋出,裸指針會因delete未執行導致泄漏,例如:我們在Func函數中new了兩個數組,但如果Divide拋異常,後續的delete會被跳過,導致內存泄漏,如下面代碼所示:

double Divide(int a, int b)
{
    // 當b == 0時拋出異常
    if (b == 0)
    {
        throw "Divide by zero condition!";
    } 
    else
    {
        return (double)a / (double)b;
    }
} 
void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10]; 
    //...
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
    // ...
    cout << "delete []" << endl;
    delete[] array1;
    delete[] array2;
}
int main()
{
    try
    {
        Func();
    } 
    catch (...)
    {
        cout << "abnormal" << endl;
    }
    return 0;
}

可以看到當Divide拋出異常時,我們new的兩個數組就無法正常釋放,導致內存泄漏:

C++的智能指針 - 牆頭草的個人空間 -_智能指針

即使我們加了try-catch,若new array2時本身拋異常,array1也無法釋放,代碼會變得臃腫且脆弱:

void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10]; 
    //...
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    } 
    catch (...)
    {
        cout << "delete []" << endl;
        delete[] array1;
        delete[] array2;
        throw; // 異常重新拋出,捕獲到什麼拋出什麼
    }
    // ...
    cout << "delete []" << endl;
    delete[] array1;
    delete[] array2;
}

即便如此,因為new本⾝也可能拋異常,連續的兩個new和下⾯的Divide都可能會拋異常,讓我們處理起來很麻煩,這種場景下,我們需要一種 “能自動釋放資源” 的機制 —— 這就是智能指針的設計初衷。

1.2 智能指針的核心:RAII 思想

在 C++ 中,智能指針是一種封裝了裸指針(raw pointer)的模板類,其核心作用是自動管理動態內存,避免因手動調用delete疏忽導致的內存泄漏、重複釋放或懸空指針等問題。它基於RAII(資源獲取即初始化) 機制:在智能指針構造時獲取資源(如動態內存),在析構時自動釋放資源,無需手動干預。

智能指針的本質是RAII(Resource Acquisition Is Initialization,資源獲取即初始化) 的實踐:

  • 資源(如動態內存、文件句柄)在智能指針對象構造時獲取,並委託給該對象管理;
  • 智能指針對象析構時自動釋放資源,無論程序是正常結束還是異常退出(對象生命週期由作用域管理,析構總會執行);
  • 為了方便使用,智能指針會重載*->[]等運算符,模擬原生指針的行為。

基於此,我們可以先來自己簡單粗略的實現一下智能指針,如下面代碼所示:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete[] " << _ptr << endl;
		delete[] _ptr;
	}

	// 重載運算符,模擬指針的⾏為,⽅便訪問資源
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};

有了智能指針,我們的func函數就可以改成:

void Func()
{
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];
    
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
}

通過運行結果我們可以看到即便Divide函數拋出異常也不會影響我們new出來兩個數組的釋放:

C++的智能指針 - 牆頭草的個人空間 -_引用計數_02

雖然我們上面設計的智能指針是十分粗略的,但是可以看到即便如此也可以幫助我們解決內存泄漏的問題。那麼接下來讓我們看一看標準庫中是如何設計智能指針的。

二、C++ 標準庫智能指針:4 種指針的特性與適用場景

C++ 標準庫(<memory>頭文件)提供了 4 種智能指針,它們分別是auto_ptr、unique_ptr、shared_ptr、weak_ptr。其中除了auto_ptr外的三個都是在C++11中提出的。除weak_ptr外均遵循 RAII,原理上而⾔主要是解決智能指針拷⻉時的思路不同。下面來讓我們看一看它們之間的區別,以及在不同場景下該如何選擇。

2.1 auto_ptr:被淘汰的 “過渡品”(C++98)

auto_ptr是C++98時設計出來的智能指針,設計思路是 “拷貝時轉移資源管理權”—— 但這是一個致命缺陷:拷貝後原對象會 “懸空”(資源指針被置空),後續訪問原對象會觸發空指針錯誤。如下面代碼所示:

int main() 
{
    auto_ptr<Date> ap1(new Date); // ap1管理Date對象
    auto_ptr<Date> ap2(ap1);      // 拷貝:ap2獲取管理權,ap1->_ptr被置空

    // ap1->_year++; // 崩潰!ap1已懸空,訪問空指針
    return 0;
}

auto_ptr拷貝的原理是將自己的指針賦值給新的auto_ptr,並且使自己的指針置為空,這樣我們再去訪問這個對象時,就會因為訪問空指針而導致報錯。可以看到,當我們將ap1拷貝給ap2後,我們就訪問ap1時就會報錯,因為ap1已經懸空,我們不能去訪問空指針。

正因如此,C++11 推出後,auto_ptr被明確標記為 “不推薦使用”,多數公司的編碼規範也會直接禁止它。

2.2 unique_ptr:不可共享的 “獨佔指針”(C++11)

unique_ptr(唯⼀指針)的設計思路是禁止拷貝、僅支持移動—— 確保同一時間只有一個unique_ptr管理資源,從根源上避免 “多個指針競爭釋放” 的問題。它是 C++11 中最常用的智能指針之一,適用於 “資源無需共享” 的場景。

核心特性:

  1. 不可複製,只能移動:由於是獨佔所有權,unique_ptr 不支持複製構造或賦值(會編譯報錯),但可以通過 std::move 轉移所有權(轉移後原 unique_ptr 會失效,變為空指針)。
  2. 高效輕量:無額外引用計數開銷,性能接近裸指針。
int main() 
{
    // 創建unique_ptr(推薦用make_unique,C++14起支持)
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10); 
    std::cout << *ptr1 << std::endl; // 輸出:10

    // 轉移所有權(ptr1失效,ptr2擁有對象)
    std::unique_ptr<int> ptr2 = std::move(ptr1); 
    if (ptr1 == nullptr) {
        std::cout << "ptr1已失效" << std::endl; // 輸出:ptr1已失效
    }

    // 超出作用域時,ptr2析構,自動釋放內存
    return 0;
}

適用場景:管理獨佔資源(如局部動態對象、類的成員變量),作為函數返回值(無需手動釋放,避免返回裸指針的風險)。

2.3 shared_ptr:可共享的 “計數指針”(C++11)

shared_ptr(共享指針)允許多個 shared_ptr 共同擁有同一個動態對象。。也就是説支持資源共享,其核心是通過 “引用計數” 跟蹤管理資源的指針數量:

  • 當新的shared_ptr拷貝或賦值時,引用計數+1
  • shared_ptr析構時,引用計數-1
  • 當引用計數減至0時,代表當前是最後一個管理資源的指針,自動釋放資源。

核心特性:

  1. 引用計數透明管理:用户無需手動維護計數,use_count()方法可查看當前計數;
  2. 支持拷貝與移動:拷貝時計數+1,移動時計數不變(原對象懸空);
int main() {
    // 創建shared_ptr(推薦用make_shared,更高效)
    std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
    std::cout << "引用計數:" << ptr1.use_count() << std::endl; // 輸出:1

    // 複製ptr1,引用計數+1
    std::shared_ptr<int> ptr2 = ptr1; 
    std::cout << "引用計數:" << ptr1.use_count() << std::endl; // 輸出:2

    // ptr2超出作用域,引用計數-1
    {
        std::shared_ptr<int> ptr3 = ptr1;
        std::cout << "引用計數:" << ptr1.use_count() << std::endl; // 輸出:3
    }
    std::cout << "引用計數:" << ptr1.use_count() << std::endl; // 輸出:2

    // 最後ptr1和ptr2析構,引用計數變為0,內存釋放
    return 0;
}

shared_ptr的構造有兩種方式,除了⽀持⽤指向資源的指針構造,還⽀持 make_shared ⽤初始化資源對象的值直接構造:

  1. shared_ptr<Date> sp(new Date(2024, 10, 1));:兩次內存分配(一次給Date對象,一次給引用計數);
  2. auto sp = make_shared<Date>(2024, 10, 1);:一次內存分配(同時存儲Date對象和引用計數),效率更高,且避免內存泄漏風險(若new成功但計數分配失敗,new的對象無法釋放)。
template <class T, class... Args> 
shared_ptr<T> make_shared(Args&&... args);

對於shared_ptrunique_ptr,我們還需要注意下面幾點:

  • shared_ptrunique_ptr 都⽀持了operator bool的類型轉換:如果智能指針對象是⼀個空對象沒有管理資源,則返回false,否則返回true,意味着我們可以直接把智能指針對象給if判斷是否為空。
  • shared_ptrunique_ptr 的構造函數都使⽤explicit 修飾,防⽌普通指針隱式類型轉換成智能指針對象。
// 報錯:無法進行隱式類型轉換
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);

使用shared_ptr還要注意線程安全問題,shared_ptr的引⽤計數對象在堆上,如果多個shared_ptr對象在多個線程中,進⾏shared_ptr的拷貝和析構時會訪問修改引⽤計數,就會存在線程安全問題,所以shared_ptr引⽤計數是需要加鎖或者原⼦操作保證線程安全的。

2.4 weak_ptr:解決循環引用的 “輔助指針”(C++11)

weak_ptr(弱指針)完全不同於上⾯的智能指針,是一個特殊的智能指針。它不⽀持RAII,也就意味着不能⽤它直接管理資源,weak_ptr的產⽣本質是要解決shared_ptr的⼀個循環引⽤導致內存泄漏的問題。

什麼是循環引用呢?shared_ptr 的引用計數機制可能導致循環引用問題:兩個對象互相持有對方的 shared_ptr,此時它們的引用計數永遠不會變為 0,導致內存泄漏。

例如:我們有兩個鏈表結點,把它們分別交給智能指針shared_ptr管理,然後將它們連接起來,如下面代碼所示:

struct ListNode 
{
    int _data;
    shared_ptr<ListNode> _next; // 指向後一個節點
    shared_ptr<ListNode> _prev; // 指向前一個節點
    
    ~ListNode() 
    { 
        cout << "~ListNode()" << endl; 
    }
};

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的計數+1 → 2
    n2->_prev = n1; // n1的計數+1 → 2

    // 析構n1和n2:計數各減1 → 1(而非0)
    // 節點資源永遠無法釋放,內存泄漏!
    return 0;
}

循環引用的邏輯鏈:n1->_next依賴n2釋放,n2->_prev依賴n1釋放,最終誰都無法釋放。

C++的智能指針 - 牆頭草的個人空間 -_智能指針_03

那麼該如何解決循環引用呢?這時候weak_ptr就派上用場了。weak_ptr 是一種弱引用智能指針,它不擁有對象的所有權,也不會增加引用計數,因此我們修改鏈表節點為weak_ptr後,循環引用被打破:

struct ListNode 
{
    int _data;
    weak_ptr<ListNode> _next; // 改為weak_ptr,不增加計數
    weak_ptr<ListNode> _prev;
    ~ListNode() { cout << "~ListNode()" << endl; }
};

int main() 
{
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);

    n1->_next = n2; // 不增加n2的計數(仍為1)
    n2->_prev = n1; // 不增加n1的計數(仍為1)

    // 析構n1和n2:計數各減1 → 0,節點資源正常釋放
    return 0;
}

weak_ptr不⽀持RAII,也不⽀持訪問資源,所以weak_ptr構造時不⽀持綁定到資源,只⽀持綁定到shared_ptr,綁定到shared_ptr時,不增加shared_ptr的引⽤計數,那麼就可以解決上述的循環引⽤問題。

C++的智能指針 - 牆頭草的個人空間 -_引用計數_04

weak_ptr的核心特性是:綁定到shared_ptr時不增加引用計數,僅作為 “觀察者” 跟蹤資源是否有效。weak_ptr也沒有重載operator*operator->等,因為他不參與資源管理 。那麼如果它綁定的shared_ptr已經釋放了資源,那麼它去訪問資源就是很危險的,為此它提供了兩個關鍵方法:

  • expired():判斷綁定的shared_ptr資源是否已釋放(計數為 0);
  • lock():若資源有效,返回一個shared_ptr(計數 + 1,安全訪問資源);若無效,返回空shared_ptr
int main()
{
    std::shared_ptr<string> sp1(new string("111111"));
    std::shared_ptr<string> sp2(sp1);
    
    std::weak_ptr<string> wp = sp1;
    cout << wp.expired() << endl;
    cout << wp.use_count() << endl;
    
    // sp1和sp2都指向了其他資源,則weak_ptr就過期了
    sp1 = make_shared<string>("222222");
    cout << wp.expired() << endl;
    cout << wp.use_count() << endl;
    
    sp2 = make_shared<string>("333333");
    cout << wp.expired() << endl;
    cout << wp.use_count() << endl;
    
    wp = sp1;
    //std::shared_ptr<string> sp3 = wp.lock();
    auto sp3 = wp.lock();
    cout << wp.expired() << endl;
    cout << wp.use_count() << endl;
    
    *sp3 += "###";
    
    cout << *sp1 << endl;
    return 0;
}

weak_ptr僅作為shared_ptr的輔助工具,解決循環引用(如鏈表、樹、圖等數據結構的節點引用)。

相關文檔weak_ptr

2.5 刪除器

智能指針析構時默認是進⾏delete釋放資源,這也就意味着如果不是new出來的資源,交給智能指針管理,析構時就會崩潰。**因此當管理非new資源(如new[]、文件指針)時,需自定義刪除器。**智能指針⽀持在構造時給⼀個刪除器,所謂刪除器本質就是⼀個可調⽤對象,在這個可調⽤對象中實現你想要的釋放資源的⽅式,當構造智能指針時,給了定製的刪除器,在智能指針析構時就會調⽤刪除器去釋放資源。因為new[]經常使⽤,所以為了簡潔,unique_ptrshared_ptr都特化了⼀份[]的版本:

C++的智能指針 - 牆頭草的個人空間 -_#c++_05

int main()
{
    // 這樣實現程序會崩潰
    // unique_ptr<Date> up1(new Date[10]);
    // shared_ptr<Date> sp1(new Date[10]);
    
    // 因為new[]經常使⽤,所以unique_ptr和shared_ptr
    // 實現了⼀個特化版本,這個特化版本析構時⽤的delete[]
    unique_ptr<Date[]> up1(new Date[5]);
    shared_ptr<Date[]> sp1(new Date[5]);
    return 0;
}

除此之外,我們還可以自定義刪除器,這裏我們有三種方式:

  • 仿函數
template<class T>
class DeleteArray
{ 
public:
	void operator()(T* ptr)
    {
    	delete[] ptr;
    }
};

unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
  • 函數指針
template<class T>
class DeleteArray
{ 
public:
    void operator()(T* ptr)
    {
    	delete[] ptr;
    }
}

unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
  • lambda表達式
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };

unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);

我們可以看到,使用不同的可調用對象,unique_ptrshared_ptr需要傳入的參數也是不同的,這是因為unique_ptrshared_ptr⽀持刪除器的⽅式有所不同:

  • unique_ptr是在類模板參數⽀持的;
  • shared_ptr是構造函數參數⽀持的。

這⾥沒有使⽤相同的⽅式還是挺不方便,也是標準庫中的一點小弊端。

使⽤仿函數unique_ptr可以不在構造函數傳遞,因為仿函數類型構造的對象直接就可以調用,但是函數指針和lambda表達式的類型是不可以的,所以在傳參時不僅要在模板傳類型,還要在構造傳入相應的對象。

3. shared_ptr的模擬實現

想要加深對智能指針的印象,我們可以自己來模擬實現一下智能指針,在標準庫中的四種智能指針中shared_ptr涉獵最廣,所以我們來模擬實現一下shared_ptr,當然我們這裏只是簡單的模擬,標準庫中的shared_ptr的實現是極為複雜的。

3.1 原理

實現shared_ptr我們需要搞定兩個比較重要的東西,其中之一是引用計數的設計,主要這⾥⼀份資源就需要⼀個引⽤計數,所以引⽤計數採⽤靜態成員的⽅式是⽆法實現的,要使⽤堆上動態開闢的⽅式,構造智能指針對象時來⼀份資源,就要new⼀個引⽤計數出來。多個shared_ptr指向資源時就++引⽤計數,shared_ptr對象析構時就--引⽤計數,引⽤計數減到0時代表當前析構的shared_ptr是最後⼀個管理資源的對象,則析構資源。

C++的智能指針 - 牆頭草的個人空間 -_引用計數_06

其次就是刪除器,我們要在構造時傳入刪除器,但是在析構時才會使用刪除器,也就是説我們需要將刪除器保存為成員函數,這樣才能在析構時去調用,那麼我們該如何保存刪除器呢?我們知道函數指針、仿函數、lambda表達式這些都可以做刪除器,這時候我們就需要用到function包裝器(詳情點擊)了,通過包裝器來存儲刪除器,這樣就可以存儲不同的刪除器了:function<void(T*)> _del = [](T* ptr) {delete ptr; };

3.2 代碼

下面讓我們來看具體的代碼實現:

template<class T>
class shared_ptr
{
public:
    explicit shared_ptr(T* ptr = nullptr)
        : _ptr(ptr)
        , _pcount(new int(1))
    {}
    
    template<class D>
    shared_ptr(T* ptr, D del)
        : _ptr(ptr)
        , _pcount(new int(1))
        , _del(del)
    {}
    
    shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        , _pcount(sp._pcount)
        , _del(sp._del)
    {
        ++(*_pcount);
    }
    
    void release()
    {
        if (--(*_pcount) == 0)
        {
            // 最後⼀個管理的對象,釋放資源
            _del(_ptr);
            delete _pcount;
            _ptr = nullptr;
            _pcount = nullptr;
        }
    }
    
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            release();
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            ++(*_pcount);
            _del = sp._del;
        }
        return *this;
    }
    
    ~shared_ptr()
    {
        release();
    }
    
    //獲得原生指針
    T* get() const
    {
        return _ptr;
    }
    
    int use_count() const
    {
        return *_pcount;
    }
    
    T& operator*()
    {
        return *_ptr;
    }
    
    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
    int* _pcount;
    function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

需要注意的是我們這⾥實現的shared_ptr是以最簡潔的⽅式實現的,只能滿⾜基本的功能。感興趣的可以去查看源代碼。

4. 總結:智能指針的最佳實踐

掌握智能指針後,我們可以從 “手動管理內存” 的焦慮中解放出來。以下是核心實踐原則:

  1. 優先用 unique_ptr:若資源無需共享,unique_ptr是最高效的選擇(無引用計數開銷);
  2. 共享用 shared_ptr:需多模塊共享資源時用shared_ptr,優先用make_shared優化;
  3. 循環引用用 weak_ptr:鏈表、樹等結構中,節點間引用用weak_ptr避免泄漏;
  4. 自定義刪除器:管理new[]、文件句柄等非new資源時,務必指定刪除器;
  5. 避免裸指針混用:儘量不要用智能指針管理 “已被裸指針管理的資源”,避免重複釋放。

最後記住:智能指針不是 “銀彈”,但它是現代 C++ 中避免內存泄漏的最有效工具。用好智能指針,讓你的代碼更安全、更優雅!