最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++的智能指針的原理解析

 更新時(shí)間:2026年05月19日 09:17:13   作者:青瓦夢(mèng)滋  
本文主要介紹了C++中智能指針的使用及其解決內(nèi)存泄漏問題的方法,傳統(tǒng)的指針需要手動(dòng)釋放內(nèi)存,容易導(dǎo)致內(nèi)存泄漏,而智能指針可以在生命周期結(jié)束時(shí)自動(dòng)釋放資源,解決內(nèi)存泄漏問題,感興趣的朋友跟隨小編一起看看吧

由于C++沒有GC(垃圾回收機(jī)制),所有動(dòng)態(tài)開辟的空間都需要我們手動(dòng)釋放,這就會(huì)導(dǎo)致

  1. 忘記釋放
  2. 異常安全問題

不管哪種情況,都會(huì)導(dǎo)致內(nèi)存泄漏

 下面先來簡(jiǎn)單復(fù)習(xí)一下異常安全問題

int Div() throw(invalid_argument)
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0錯(cuò)誤");
	return a / b;
}
void fun()
{
	int* p = new int;
	cout << Div() << endl;
	delete p;
}
int main()
{
	try
	{
		fun();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

此時(shí),如果在Div中拋出異常,就會(huì)跳過fun()函數(shù)最后的 delete p; 語句,從而導(dǎo)致內(nèi)存泄漏,這就是異常安全問題

 那么異常是怎么解決這個(gè)問題的呢?

int Div() throw(invalid_argument)
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0錯(cuò)誤");
	return a / b;
}
void fun()
{
	int* p = new int;
	try
	{
		cout << Div() << endl;
	}
	catch (...)//若拋出異常,這里會(huì)先捕獲異常,釋放空間后再重新拋出
	{
		delete p;
		throw;
	}
	delete p;
}
int main()
{
	try
	{
		fun();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

使用了異常的重新拋出,先在內(nèi)層的catch語句中完成空間的釋放,再將該異常重新拋出交給外面的catch處理

但這樣的解決方法會(huì)涌現(xiàn)很多問題

例如,當(dāng)我們開辟了多個(gè)空間,如果DIv拋異常,就需要先將這3個(gè)空間都釋放掉 

void fun()
{
	int* p1 = new int;
	int* p2 = new int;
	int* p3 = new int;
	try
	{
		cout << Div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		delete p3;
		throw;
	}
	delete p1;
	delete p2;
	delete p3;
}

但是開辟空間如果失敗了也是會(huì)拋異常的,如果這里的new拋異常怎么辦?并且還需要清楚是哪個(gè)變量的new拋出的異常

有讀者可能會(huì)想,給每個(gè)new都放在一個(gè)單獨(dú)的try塊中,再給每個(gè)try寫一個(gè)單獨(dú)的catch,不就可以解決了嗎?

首先,如果new語句在try塊內(nèi),那么在try塊外的delete語句就會(huì)找不到p1,p2,p3

再者,就算可以找到,那怎么判斷哪個(gè)new拋的異常,從而不去delete對(duì)應(yīng)對(duì)象,又是個(gè)問題

這時(shí)候,就要請(qǐng)出本文的主角——智能指針

智能指針的原理

智能指針其實(shí)是一個(gè)類 

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if(_ptr)
        {
			delete _ptr;
            _ptr = nullptr;
        }    
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

在該類中,定義了構(gòu)造、析構(gòu)、*重載、->重載,而類中存的是一個(gè)指針

 現(xiàn)在我如果把一個(gè)原生指針存進(jìn)去,就不需要我手動(dòng)釋放了

int* p = new int;
SmartPtr<int> sp(p);

因?yàn)轭愒?strong>生命周期結(jié)束后會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù),而現(xiàn)在原生指針交給智能指針管理后,當(dāng)原生指針的生命周期結(jié)束時(shí),智能指針的生命周期也會(huì)結(jié)束,此時(shí)調(diào)用該類的析構(gòu)函數(shù),就可以釋放該原生指針的空間

*重載 和->重載是為了讓智能指針可以更好的模擬原生指針的行為

 既然智能指針可以完全模擬原生指針的行為了,那其實(shí)我們可以這么定義

SmartPtr<int> sp(new int);//或SmartPtr<int> sp = new int;//這里調(diào)用的也是構(gòu)造

有了智能指針,再發(fā)生異常安全問題時(shí),就無需重新拋出異常了

int Div() throw(invalid_argument)
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0錯(cuò)誤");
	return a / b;
}
void fun()
{
	SmartPtr<int> sp = new int;
	cout << Div() << endl;
}
int main()
{
	try
	{
		fun();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

此時(shí)如果Div函數(shù)內(nèi)部拋出異常,那么在異常拋出后會(huì)先執(zhí)行SmartPtr的析構(gòu)函數(shù),再執(zhí)行catch塊中的代碼

即使沒有拋異常,在fun函數(shù)執(zhí)行完后,也會(huì)調(diào)用SmartPtr的析構(gòu)函數(shù),相當(dāng)于將指針托管給了智能指針

RAII

 智能指針的這種做法,就被叫做RAII(Resource Acquisition Is Initialization),它是一種利用對(duì)象生命周期來控制程序資源(如內(nèi)存、文件句柄、網(wǎng)絡(luò)連接、互斥量等等)的簡(jiǎn)單技術(shù)。

詳細(xì)點(diǎn)解釋,就是在對(duì)象構(gòu)造時(shí)獲取資源,接著控制對(duì)資源的訪問使之在對(duì)象的生命周期內(nèi)始終保持有效,最后在對(duì)象析構(gòu)的時(shí)候釋放資源。借此,我們實(shí)際上把管理一份資源的責(zé)任托管給了一個(gè)對(duì)象。這種做法有兩大好處:

  • 不需要顯式地釋放資源。
  • 采用這種方式,對(duì)象所需的資源在其生命期內(nèi)始終保持有效。

需要注意的是,RAII是一種托管資源的思想,智能指針是依靠RAII實(shí)現(xiàn)的,除了智能指針之外,還有例如unique_lock/lock_gurad等等也是依托RAII實(shí)現(xiàn)的

智能指針的問題

當(dāng)我們想將一個(gè)智能指針的值賦給另一個(gè)智能指針時(shí),問題就出現(xiàn)了

SmartPtr<int> sp1 = new int;
SmartPtr<int> sp2 = sp1;//調(diào)用默認(rèn)拷貝構(gòu)造

 此時(shí)會(huì)報(bào)錯(cuò)

這是因?yàn)閟p1和sp2中指針的地址是同一個(gè),那么就會(huì)導(dǎo)致兩次釋放,和淺拷貝問題很像

針對(duì)這個(gè)問題,C++一共給出了三種解決方案

  • C++98:auto_ptr     管理權(quán)轉(zhuǎn)移  (了解)
  • C++11:unique_ptr  防拷貝  
  • C++11:shared_ptr  引用計(jì)數(shù)的共享拷貝  循環(huán)拷貝問題需要用weak_ptr解決

auto_ptr

auto_ptr是C++98中自帶的類,因此如果要實(shí)現(xiàn)自己的auto_ptr,需要將它放在一個(gè)命名空間中

namespace valkyrie
{
	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& ap)//管理權(quán)轉(zhuǎn)移,將ap存的地址轉(zhuǎn)移給*this
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		auto_ptr& operator=(auto_ptr<T>& ap)//依舊管理權(quán)轉(zhuǎn)移,不過需要先將原先this中的空間釋放
		{
			if (this != &ap)
			{
				if (_ptr)
					delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

 大部分代碼都和之前我們實(shí)現(xiàn)的SmartPtr一樣,唯一區(qū)別在于拷貝構(gòu)造和賦值重載顯式定義了

 該拷貝構(gòu)造的核心在于原先ap1管理的空間,交給ap2管理,而ap1再置空

賦值重載也一樣,但需要先將原先存的空間釋放掉

那此時(shí)如果再想 *ap2 = 1;  就會(huì)引發(fā)空指針解引用問題,而崩潰

這也是C++早期的設(shè)計(jì)缺陷,一般都是公司明令禁止使用的

unique_ptr

 unique_ptr 是C++11新引入的智能指針,它對(duì)于拷貝構(gòu)造和賦值重載的做法是直接禁止,已達(dá)到防拷貝的目的

namespace valkyrie
{
    template <class T>
	class unique_ptr
	{
	public:
		unique_ptr(unique_ptr<T>&) = delete;//刪除該類的拷貝構(gòu)造
		unique_ptr<T>& operator=(unique_ptr<T>&) = delete;//刪除該類的賦值重載
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{
		}
		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

這樣在我們嘗試調(diào)用拷貝構(gòu)造或賦值重載時(shí),就會(huì)報(bào)錯(cuò)

unique_ptr的思路簡(jiǎn)單粗暴,是比較推薦實(shí)用的,但缺陷也很明顯:如果有需要拷貝的場(chǎng)景,它就沒法使用

shared_ptr

 shared_ptr 也是C++11引入的智能指針,它和 unique_ptr 的區(qū)別在于它可以拷貝構(gòu)造和賦值

 shared_ptr 的思路是通過引用計(jì)數(shù)的方式來實(shí)現(xiàn)多個(gè) shared_ptr 對(duì)象之間共享資源。

每個(gè)資源都對(duì)應(yīng)一個(gè)計(jì)數(shù)指針,用來記錄該份資源被幾個(gè)對(duì)象共享。

拷貝構(gòu)造: 

為什么計(jì)數(shù)的變量是指針?因?yàn)?strong>只有指針才可以實(shí)現(xiàn)在多個(gè)對(duì)象中同步變化

 賦值重載時(shí),需要先判斷原先存儲(chǔ)的空間需不需要釋放,再執(zhí)行如同拷貝構(gòu)造一樣的操作

此時(shí)就是直接釋放的情況,下面再演示一下不需要釋放的情況

namespace valkyrie
{	
    template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		shared_ptr(const shared_ptr<T>& sp)//拷貝構(gòu)造,將指針和計(jì)數(shù)指針都拷貝給它,并將引用計(jì)數(shù)+1,表示又多一個(gè)托管這塊內(nèi)存的智能指針
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}
		shared_ptr<T>& operator=(shared_ptr<T>& sp)//賦值重載,先判斷原先該智能指針中的空間需不需要釋放(等于先調(diào)用了一次析構(gòu)函數(shù)),再執(zhí)行和拷貝構(gòu)造一樣的步驟
		{
			if (this != &sp)
			{
				if (--(*_pcount) == 0 && _ptr)
				{
					delete _ptr;
					_ptr = nullptr;
					delete _pcount;
					_pcount = nullptr;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}
		~shared_ptr()
		{
				if (--(*_pcount) == 0 && _ptr)//先將引用計(jì)數(shù)-1,看看是否為0,如果為0,就代表沒有智能指針在托管這塊空間了,直接釋放;但如果不為0,就代表還有*pcount個(gè)智能指針在托管這塊空間,不能釋放
				{
					delete _ptr;
					_ptr = nullptr;
					delete _pcount;
					_pcount = nullptr;
				}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;//引用計(jì)數(shù),用來統(tǒng)計(jì)目前有幾個(gè)智能指針在同時(shí)托管這塊空間
	};
}

shared_ptr的線程安全問題

當(dāng)有多個(gè)線程用 shared_ptr 管理著同一份共享資源時(shí),由于引用計(jì)數(shù)需要++(析構(gòu)時(shí)也需要--),因此就會(huì)有多個(gè)線程同時(shí)對(duì)一個(gè)引用計(jì)數(shù)++(--)而導(dǎo)致的數(shù)據(jù)不一致問題。

C++11的shared_ptr是線程安全的,下面簡(jiǎn)單實(shí)現(xiàn)一下通過將引用計(jì)數(shù)包裝成 atomic 原子類型而實(shí)現(xiàn)的線程安全版本:

namespace val
{
    template<class T>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr)
        :_ptr(ptr)
        ,_count(new atomic<int>(1))
        {}
        shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        ,_count(sp._count)
        {
            if(_count)
                (*_count)++;
        }
        shared_ptr<T>& operator=(shared_ptr sp)
        {
            //現(xiàn)代寫法,并且保證了線程安全
            if(this != &sp)
            {
                swap(_ptr,sp._ptr);
                swap(_count,sp._count);
            }
            return *this;
        }
        ~shared_ptr()
        {
            if(_count && --(*_count) == 0)
            {
                delete _ptr;
                delete _count;
            }
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
        atomic<int>* _count;//將引用計(jì)數(shù)包裝成原子類型
    };
}

shared_ptr的循環(huán)引用問題

當(dāng)shared_ptr托管的對(duì)象需要雙向依賴關(guān)系,例如雙向鏈表(next節(jié)點(diǎn)和prev節(jié)點(diǎn)),或樹的父子節(jié)點(diǎn),就會(huì)因?yàn)?strong>引用計(jì)數(shù)無法歸零而導(dǎo)致內(nèi)存泄漏

例如下面這種情況:

template <class T>
struct ListNode
{
    T val;
    val::shared_ptr<ListNode> prev;
    val::shared_ptr<ListNode> next;
};
int main()
{
    val::shared_ptr<ListNode<int>> ln1(new ListNode<int>);
    val::shared_ptr<ListNode<int>> ln2(new ListNode<int>);
    cout << ln1.getcnt()/*獲取當(dāng)前智能指針的引用計(jì)數(shù)*/ << endl << ln2.getcnt() << endl;
    ln1->next = ln2;
    ln2->prev = ln1;
    cout << ln1.getcnt() << endl << ln2.getcnt() << endl;
    return 0;
}

在這段代碼里,ln1內(nèi)托管的對(duì)象由ln1和ln2->prev同時(shí)托管,ln2內(nèi)托管的對(duì)象由ln2和ln1->next同時(shí)托管,那么ln1和ln2的引用計(jì)數(shù)都為2

當(dāng)程序結(jié)束,ln1和ln2調(diào)用析構(gòu)函數(shù)后,各自都把引用計(jì)數(shù)減到1各自都以為還有別的shared_ptr在托管該資源(next和prev),所以都沒釋放,這就造成了內(nèi)存泄漏

要想解決,就需要把雙向依賴關(guān)系里的指針不再引用計(jì)數(shù)++,因此C++11還有一個(gè) weak_ptr ,它不是智能指針,而是專門用于解決shared_ptr循環(huán)引用問題的指針,將shread_ptr賦給weak_ptr時(shí),引用計(jì)數(shù)不會(huì)++,而只是像個(gè)普通指針一樣

template <class T>
class weak_ptr
{
public:
    weak_ptr() = default; // 默認(rèn)構(gòu)造函數(shù)
    weak_ptr(const shared_ptr<T> &sp)
        : _ptr(sp.operator->())
    {
    }
    weak_ptr<T> &operator=(shared_ptr<T> sp)//參數(shù)是shared_ptr
    {
        _ptr = sp.operator->(); // 獲取sp._ptr
        return *this;
    }
    ~weak_ptr()
    {
        _ptr = nullptr;//不釋放,只置空
    }
    T &operator*()
    {
        return *_ptr;
    }
    T *operator->()
    {
        return _ptr;
    }
private:
    T *_ptr;
};

只要將雙向依賴關(guān)系中的資源交給weak_ptr管理,就解決了循環(huán)引用問題

template <class T>
struct ListNode
{
    T val;
    val::weak_ptr<ListNode> prev;
    val::weak_ptr<ListNode> next;
};
int main()
{
    val::shared_ptr<ListNode<int>> ln1(new ListNode<int>);
    val::shared_ptr<ListNode<int>> ln2(new ListNode<int>);
    cout << ln1.getcnt()/*獲取當(dāng)前智能指針的引用計(jì)數(shù)*/ << endl << ln2.getcnt() << endl;
    ln1->next = ln2;
    ln2->prev = ln1;
    cout << ln1.getcnt() << endl << ln2.getcnt() << endl;
    return 0;
}

定制刪除器

在C++11之前,也就是C++98后,智能指針只有auto_ptr,這之間的十幾年由于官方一直沒有作為,一個(gè)名為 boost 的第三方庫就出現(xiàn)了,它給出了 scoped_ptr 、 shared_ptr weak_ptr ,并且除了智能指針也有非常多的實(shí)現(xiàn),后續(xù)C++11的 unique_ptr 、 shared_ptr 、 weak_ptr 就是參考 boost 庫實(shí)現(xiàn)的

boost庫中除了有scoped_ptr和shared_ptr,還有 scoped_array shared_array 。由于智能指針默認(rèn)的釋放資源的方式都是delete,如果是以new []創(chuàng)建的數(shù)組,就不能用delete了,因此scoped_array/shared_array是boost中專門給new []準(zhǔn)備的智能指針,它們的釋放資源的方式是delete[]

C++11沒有引入xxx_array的智能指針,它靠定制刪除器來實(shí)現(xiàn)(也是借鑒的boost庫)

如果直接將new []的數(shù)據(jù)交給智能指針管理,在析構(gòu)時(shí)會(huì)報(bào)錯(cuò)

class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int a;
    int b;
};
int main()
{
    shared_ptr<A>(new A[10]);
    return 0;
}

此時(shí)可以定義一個(gè)仿函數(shù)。讓智能指針在釋放資源時(shí)自動(dòng)調(diào)用仿函數(shù)內(nèi)的 operator() 

template<class T>
struct deleteArr
{
    void operator()(T* v)
    {
        delete[] v;
    }
};
int main()
{
    shared_ptr<A>(new A[10],deleteArr<A>());
    return 0;
}

這就叫做定制刪除器

除了new,new[]之外,malloc/fopen等等需要回收的類型都可以使用定制刪除器交給智能指針管理

class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int a;
    int b;
};
template<class T>
struct deleteArr
{
    void operator()(T* v)
    {
        delete[] v;
    }
};
template<class T>
struct deleteFree
{
    void operator()(T* v)
    {
        free(v);
    }
};
struct deleteFclose
{
    void operator()(FILE* v)
    {
        fclose(v);
    }
};
int main()
{
    shared_ptr<A>(new A[10],deleteArr<A>());
    shared_ptr<A>((A*)malloc(sizeof(A)),deleteFree<A>());
    shared_ptr<FILE>(fopen("tmp.txt","w"),deleteFclose());
    return 0;
}

需要注意的是,上面只是為了演示定制刪除器的用法,若真需要用智能指針管理數(shù)組,應(yīng)該用unique_ptr,它有operator[]重載

lock_guard/unique_lock(RAII機(jī)制托管鎖)

平時(shí)有加鎖解鎖的操作時(shí),如果中間的某個(gè)函數(shù)拋出異常,就會(huì)出現(xiàn)死鎖問題,需要通過捕獲重新拋出的方式對(duì)其解鎖

mtx.lock();
try
{
    f();
}
catch(...)
{
    mtx.unlock();//捕獲重新拋出
    throw;
}
mtx.unlock();

C++11中,專門為鎖的RAII寫了兩個(gè)對(duì)象, lock_guard unique_lock ,它們可以在出生命周期時(shí)自動(dòng)釋放鎖

lcok_guard就是純粹的RAII的管理鎖,構(gòu)造時(shí)加鎖,析構(gòu)時(shí)解鎖,該對(duì)象只有構(gòu)造和析構(gòu)兩個(gè)接口

template<class lock>
class Lock_Guard
{
public:
    Lock_Guard(lock& lk)//引用傳參,這樣傳過去的鎖也是同一個(gè)
    :_lk(lk)
    {
        //_lk = lk;
        _lk.lock();
    }
    ~Lock_Guard()
    {
        _lk.unlock();
    }
    Lock_Guard(const Lock_Guard<lock>&) = delete;//lock_guard不允許拷貝
    Lock_Guard<lock>& operator=(Lock_Guard<lock>) = delete;
private:
    lock& _lk;//引用類型
};
mutex mtx;
void fun()
{
    Lock_Guard<mutex> lg(mtx);
    throw(string("拋出異常"));
}

需要注意的是lock_guard是不支持拷貝

 unique_lock 除了可以像lock_guard一樣RAII,還可以自己加鎖或解鎖

void fun()
{
    unique_lock<mutex> lg(mtx);
    throw(string("拋出異常"));
    lg.unlock();
    //...其他工作
    lg.lock();
    //...
}

到此這篇關(guān)于【語法】C++的智能指針的文章就介紹到這了,更多相關(guān)C++的智能指針內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

祁门县| 曲沃县| 黄石市| 扎赉特旗| 弋阳县| 息烽县| 南靖县| 聂拉木县| 安达市| 安溪县| 青田县| 吴川市| 桑日县| 台北市| 讷河市| 林甸县| 苍梧县| 包头市| 博客| 怀柔区| 天镇县| 唐山市| 临夏县| 宜州市| 揭西县| 乳山市| 万年县| 达日县| 建始县| 福州市| 忻城县| 库尔勒市| 黔南| 大新县| 晋城| 孟连| 唐河县| 新乡县| 阿尔山市| 阿图什市| 元江|