? ? ? ? 在筆試,面試中智能指針經常出現,如果你對智能指針的作用,原理,用法不了解,那么可以看看這篇博客講解,此外本博客還簡單模擬實現了各種指針,在本篇的最后還應對面試題對智能指針的知識點進行了拓展。希望能加深你對智能指針的理解。那么開始學習吧!
一.智能指針作用
? ? ? ? C++的智能指針主要作用是為了防止內存泄漏。在代碼中我們new出來的對象都需要delete,但是當我們我們忘記或者代碼出現異常導致沒有delete對象,就會產生內存泄漏。長期運行的程序出現內存泄漏,影響很大,如操作系統、后臺服務等等,出現內存泄漏會導致響應越來越慢,最終卡死。
? ? ? ? 下面我們看看一個常見的因為異常導致內存泄漏的例子:
#include<iostream>
using namespace std;static int sa = 1;
int div_func(int a, int b)
{if (b == 0){throw invalid_argument("除0錯誤");}return a / b;
}
int Func1()
{int* a1 = new int{1};int* a2 = new int{sa};int n=div_func(*a1,*a2);sa--;delete a1;delete a2;return n;
}int main()
{try{while (1){int n = Func1();cout << n;}}catch (exception& e){cout << e.what() << endl;}return 0;
}
運行結果:
? ? ? ? main函數調用func1函數,func中new出了a1,和a2,然后調用div_func,當b=0,此時就會拋出異常,異常被mian函數捕獲直接跳轉,此時new出來的a1和a2就不會被delete,導致內存泄漏。這種代碼的內存泄漏,還是比較難防備,此時就需要使用智能指針。
二.智能指針原理?
? ? ? ? 我們首先介紹一下什么是RAII。
????????RAII(Resource Acquisition Is Initialization)(資源獲取即初始化)是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象。
????????這種做法有兩大好處:
- 1.不需要顯式地釋放資源。
- 2.對象所需的資源在其生命期內始終保持有效。
????????智能指針也就是利用RAII的原理實現的,把管理一份資源的責任托管給了一個對象,通過構造函數獲取資源,通過析構函數釋放資源,看代碼:
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}
private:T* _ptr;
};
? ? ? ? 這樣我們就能通過類的生命周期來對資源進行管理和釋放。對于最開始的代碼我們只需要把?int* a1 = new int{1};int* a2 = new int{sa}代碼寫成SmartPtr<int > sp1=new{1};SmartPtr<int >sp2=new{sa},這樣即使因為拋異常跳轉到mian()函數也會因為生命周期的結束自動釋放資源。上面的代碼就是智能指針的基本原理。
三.智能指針介紹和使用?
C++常見的智能指針有三種,這里我們只做基本介紹和使用,詳細特點我們后面實現再介紹。
1.std::unique_ptr
特點:獨占資源所有權,不可復制(不能進行拷貝構造和賦值運算符重載)但支持移動語義,生命周期結束時自動釋放資源,保證只有一個對象只有一個unique_ptr指針,避免重復析構。
class A
{
public:int a;A(int n){a = n;}~A(){std::cout << "調用析構" << std::endl;}};
int main()
{std::unique_ptr<A> ptr = std::make_unique<A>(1);//c++高版本寫法。std::unique_ptr<A> ptr( new A(1));//第二種寫法//std::unique_ptr<A> ptr1=ptr;//禁止了拷貝構造會報錯std::unique_ptr<A> ptr2 = std::move(ptr); // 所有權轉移
}
2.std::shared_ptr
特點:共享資源所有權,通過引用計數管理生命周期,線程安全的引用計數更新。允許復制。
每復制一個shared_ptr,計數+1,析構一個計數-1,計數為零才調用析構。
class A
{
public:int a;A(int n){a = n;}~A(){std::cout << "調用析構" << std::endl;}};
int main()
{std::shared_ptr<A> ptr = std::make_shared<A>(1);std::shared_ptr<A> ptr1 = ptr;}
3.std::weak_ptr
- ?特點:弱引用,不影響?
shared_ptr
?的引用計數,需通過?lock()
?提升為?shared_ptr
?訪問資源(后面詳細講解)。
四.簡單模擬實現std::auto_ptr
? ? ? ? auto_ptr主要是在早期版本的C++使用,現在基本不會使用,特點是轉移管理權,即當指針復制時,讓新指針指向舊指針,再將舊指針指向空,我們主要做個了解。
namespace bit
{template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}//移交管理權auto_ptr(auto_ptr<T>& ap){_ptr = ap.get();ap._ptr = nullptr;}//釋放原來的,接收管理權auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){// 釋放當前對象中資源if (_ptr)delete _ptr;}_ptr=ap.get();ap.ptr=null;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~auto_ptr(){delete _ptr;}T* get(){return _ptr;}private:T* _ptr;};}class Date
{
public:Date(int year,int month,int day){_year = year;_month = month;_day = day;cout << "調用構造函數" << endl;}~Date(){cout << "調用析構" << endl;}int _year;int _month;int _day;
};
int main()
{bit::auto_ptr<int> ap1 = new int{ 1 };bit::auto_ptr<Date> ap2 = new Date{ 1,1,1 };cout << *ap1 << endl;cout << ap2->_year << endl;bit::auto_ptr<Date> ap3 = ap2;}
運行結果:
最后一行時的監視窗口:
? ? ? ? auto_ptr作為智能指針,當調用拷貝構造或賦值運算符重載,不允許多個智能指針指向同一個對象,而是將一個智能指針的資源管理權移交給另外一個智能指針,這種做法是不太好的,這意味著,賦值后的原auto_ptr對象將不再擁有指針的所有權,其內部指針會被置為NULL。這種行為可能導致一些潛在的錯誤,因為程序員可能期望原對象仍然擁有指針的所有權。很多公司明確要求不能使用auto_ptr。
五.簡單模擬實現std::unique_ptr
????????unique_ptr的實現原理:簡單粗暴的防拷貝,下面簡化模擬實現了一份UniquePtr來了解它的原理。
template<class T>class unique_ptr{public:unique_ptr(T* ptr) :_ptr(ptr){}unique_ptr (unique_ptr& up) = delete;unique_ptr<T>& operator=(unique_ptr& up) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}private:T* _ptr;};
? ? ? ? 通過刪除拷貝構造函數和賦值運算符重載來確保指向該資源的只有該智能指針。也就是說是一個資源只能有一個智能指針。
六.簡單模擬實現std::shared_ptr
? ? ? ?上面的倆種指針之所以只能做到一個資源只能有一個智能指針,是因為沒有解決多個智能指針指向一份資源從而導致重復析構的問題而shared_ptr可以解決這個問題。
????????shared_ptr的原理:是通過引用計數的方式來實現多個shared_ptr對象之間共享資源。所有智能指針都指向同一個內存和引用計數。
? ? ? ? 當有智能指針指向內存資源時,同時讓共享的引用計數++,當智能指針析構時,只是讓引用計數--,只有當引用計數為0時再調用指向資源的析構函數。此外為了多線程訪問,對計數需要加鎖保護。
? ? ? ? 具體看代碼:
namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pRefCount(new int(1)), _pmtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pRefCount(sp._pRefCount), _pmtx(sp._pmtx){AddRef();}void Release(){_pmtx->lock();bool flag = false;if (--(*_pRefCount) == 0 && _ptr){cout << "delete:" << _ptr << endl;delete _ptr;delete _pRefCount;flag = true;}_pmtx->unlock();if (flag == true){delete _pmtx;}}void AddRef(){_pmtx->lock();++(*_pRefCount);_pmtx->unlock();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pmtx = sp._pmtx;AddRef();}return *this;}int use_count(){return *_pRefCount;}~shared_ptr(){Release();}// 像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;int* _pRefCount;mutex* _pmtx;};
? ? ? ? ?必須注意的是多線程的話,因為所有指針共享一個引用計數,對引用計數必須加鎖訪問,這里我們只做簡單模擬。
七.weak_ptr的模擬實現
? ? ? ? weak_ptr有2個作用:
- ?打破循環引用:通過將類成員聲明為?
weak_ptr
,避免?shared_ptr
?的循環引用導致內存泄漏- ?安全訪問資源:通過?
lock()
?方法原子性地獲取?shared_ptr
,若對象已釋放則返回空指針,避免懸垂指針
? ? ? ? 下面我們引入第一個作用:?打破循環引用
? ? ? ? 在shard_ptr中看似很安全,但是可能會出現循環引用的問題,下面讓我們看看
class B;
class A {
public:std::shared_ptr<B> b_ptr; // 強引用int a;~A() { std::cout << "A destroyed\n"; }
};
class B {
public:std::shared_ptr<A> a_ptr; // 強引用int b;~B() { std::cout << "B destroyed\n"; }
};int main(){auto ptr_A = std::make_shared<A>();//c++新版本創建智能指針的新方法auto ptr_B = std::make_shared<B>();ptr_A->b_ptr = ptr_B; //ptr_A的引用計數++;ptr_B->a_ptr = ptr_A; //ptr_B的引用計數++ 循環引用,引用計數均為2Bstd::cout << "運行完畢" << std::endl;
} // mian結束,智能指針ptr_A,ptr_B引用計數只能減為1,對象未銷毀
運行結果:
?????????可以看到我們并未成功調用對象A和B的析構函數,造成了內存泄漏。我們來分析一下原因。
????????首先ptr_A指向A對象(假設為a),ptr_A的計數為1,ptr_B指向B對象(假設為b),ptr_B的計數也為1,然后?ptr_A->b_ptr = ptr_B; ptr_B->a_ptr = ptr_A; 此時ptr_A和ptr_B的計數增加為2.
????????我們來畫圖理解。
? ? ? ? 這里我們用控制塊A和控制B代表指向A 和B的計數 ,當程序運行結束ptr_A,ptr_B調用析構時,計數都減少1,如下:
? ? ? ? ?此時指向A和B對象的計數都為1,無法自動調用析構,造成內存泄漏(new 出來的對象也是不會自動調用析構的)。
? ? ? ? 我們要知道shared_ptr智能指針計數為0時才能調用指向對象的析構函數。
????????為了解決上面的問題,我們創鍵了weak_ptr.
? ? ? ? weak_ptr一般和shard_ptr搭配使用,weak_ptr可以接受shard_prt類型的指針,但是不會影響計數。也就是說weak_ptr的構造和析構都不會增加和減少計數,同時weak_ptr也不會計數為0也不會調用指向對象的析構函數,只是充當指向作用。
? ? ? ? 我們使用weak_ptr對上面的代碼進行修改。
class B;
class A {
public:std::weak_ptr<B> b_ptr; // 強引用int a;~A() { std::cout << "A destroyed\n"; }
};
class B {
public:std::shared_ptr<A> a_ptr; // 強引用int b;~B() { std::cout << "B destroyed\n"; }
};int main()
{auto ptr_A = std::make_shared<A>();auto ptr_B = std::make_shared<B>();ptr_A->b_ptr = ptr_B;ptr_B->a_ptr = ptr_A;std::cout << "運行完畢";
}
運行結果如下:
? ? ? ? ?我們將對象A的智能指針替換為weak_ptr,其他不變。再來分析析構過程
?????????首先ptr_A指向A對象,ptr_B指向B對象,計數都為1。ptr_A->b_ptr = ptr_B,此時A中的是weak_ptr<B>指針,不會增加ptr_B的計數,而?ptr_B->a_ptr = ptr_A,會增加ptr_A的計數(對哪個指針進行拷貝就是增加哪個指針的計數)。ptr_A計數為2,ptr_B計數為1。此時情況如圖:
????????當main結束時ptr_A調用自己的析構函數計數減少為1, 同時ptr_B自動調用自己的析構函數,計數減少為0。
????????由于ptr_B計數為0需要調用b的析構函數,調用b的析構函數釋放資源后要調用成員變量a_ptr的析構函數(析構函數的順序是先調用自己的,再調用成員變量的),此時控制塊A計數為1(a_ptr是拷貝ptr_A的,倆者計數相同),減一后為0,計數為零需要調用a的析構函數,a的成員變量b_ptr計數為0,A可以直接析構,到此對象全部成功析構。圖示如下:
????????
? ? ? ? 因此析構函數的調用順序是先B后A,但是B對象是后于A對象被銷毀的。?
? ? ? ? 上面這么多就是為了論證weak_ptr的一個作用:?打破循環引用,避免內存泄漏。
????????對于weak_ptr的第二個作用就好理解多了。?
- 安全訪問資源:通過?
lock()
?方法原子性地獲取?shared_ptr
,若對象已釋放則返回空指針,避免懸垂指針。
std::weak_ptr<int> wp;
if (auto sp = wp.lock()) { // 檢查對象是否存在// 安全使用 sp
}
? ? ????wp.lock()是看計數是否為0,為0返回空,不為零返回一個shared_ptr(計數也會++)。
????????以上這些講講的都是weak_ptr的作用和原理,下面我們給出weak_ptr的模擬實現。
template <typename T>
class WeakPtr {
public:// 默認構造函數(空指針)WeakPtr() : ptr_(nullptr), ref_count_(nullptr) {}// 從 SharedPtr 構造template <typename U>WeakPtr(const SharedPtr<U>& shared) : ptr_(shared.ptr), ref_count_(shared.ref_count) {}// 拷貝構造函數WeakPtr(const WeakPtr& other) : ptr_(other.ptr_), ref_count_(other.ref_count) {}shared_ptr<T> lock() const {if (*ptr<=0) return shared_ptr<T>();return shared_ptr<T>(ptr, ref_count_);}// 析構函數~WeakPtr() {}T& operator*(){return *ptr;}T* operator->(){return ptr;}T* get() const{return ptr;}private:T* ptr; // 指向對象的指針int* ref_count_; // 指向控制塊的指針// 允許 SharedPtr 訪問私有成員template <typename U>friend class SharedPtr;
};
? ? ? ? ? ? 要注意的這里只是簡單實現,并不詳細。
八.面試題拓展
????????上面的講解基本就能解決絕大多數的面試題了,但是面試的知識點也越來越細了,因此我們再根據常見的面試題進行拓展。
weak_ptr真的不計數?是否有計數方式?在哪分配的空間?
? ? ? ??
對于1,2小問,這里我們需要介紹一下share_ptr和weak_ptr中控制塊的概念。
????????控制塊是智能指針實現引用計數機制的核心數據結構,包含以下信息
- ?強引用計數(
use_count
)?:記錄當前有多少個shared_ptr
持有對象。 - ?弱引用計數(
weak_count
)?:記錄當前有多少個weak_ptr
觀察對象。 - ?對象指針:指向實際管理的對象(可能為空,若對象已被銷毀)。
- ?自定義刪除器(可選)。
?????也就是說shared_ptr和weak_ptr中有2個強弱倆個計數,其中強引用計數作用就是當計數為0時調用指向對象的析構函數,但控制塊仍存在,弱引用作用是當?弱引用歸零時控制塊本身被釋放。
那么控制塊的作用是什么:
?1. 支持
weak_ptr
的安全操作
- ?感知對象狀態:即使對象已被銷毀(強引用歸零),
weak_ptr
仍需通過控制塊判斷對象是否有效(如lock()要通過控制塊判斷
);- ?避免懸空控制塊:若控制塊隨對象一起釋放,
weak_ptr
將無法判斷對象是否存在,導致未定義行為?2. 避免控制塊內存泄漏
- ?生命周期分離:控制塊的存活由弱引用計數決定。即使對象已銷毀,只要存在
weak_ptr
觀察,控制塊就必須保留以記錄弱引用信息- ?最終釋放機制:當所有
weak_ptr
銷毀(弱引用歸零),控制塊才會被釋放,避免內存殘留
這里我們在總結一下智能指針的釋放流程。
釋放流程
- ?對象銷毀:當最后一個
shared_ptr
析構時,強引用計數歸零,對象被釋放。- ?控制塊保留:若仍有
weak_ptr
觀察(弱引用計數>0),控制塊繼續存在。- ?控制塊釋放:當所有
weak_ptr
析構(弱引用歸零),控制塊被銷毀
?對于最后1個小問,我們還需要了解控制塊的分配方式
- ?
new
分配:直接使用new
時,對象和控制塊分兩次分配,控制塊獨立存在- ?
make_shared
優化:通過make_shared
創建shared_ptr
時,對象和控制塊分配在同一塊連續內存中,減少內存碎片和分配次數。因此第二種方法更好一點。
因此上面面試題的答案是:
weak_ptr真的不計數?是否有計數方式,在哪分配的空間。
計數,控制塊中有強弱引用計數,如果是使用make_shared初始化的函數則它所在的控制塊空間是在所引用的shared_ptr中同一塊的空間,若是new則控制器所分配的內存與shared_ptr本身所在的空間不在同一塊內存。
??好了,智能指針就講解到這了,感覺有幫助的話,請點點贊吧,這真的很重要。
????????????????????????????????