C++——智能指針和RAII


該文章代碼均在gitee中開源

C++智能指針hppicon-default.png?t=N7T8https://gitee.com/Ehundred/cpp-knowledge-points/tree/master/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88???????


智能指針

傳統指針的問題

在C++自定義類型中,我們為了避免內存泄漏,會采用析構函數的方法釋放空間。但是對于一些情況,系統往往并沒有那么聰明,比如C語言里,我們malloc一塊空間;C++里,我們new一塊空間,系統不會對這些空間進行特別檢查, 最后便造成了內存泄漏

void func()
{int* a = new int(1);//...一通操作if (true){return;}//如果程序在中途就終止了,那這段delete便不會執行,內存泄漏了delete a;
}

有的時候并不是我們不想釋放或者忘了釋放,而是經常會發生函數異常終止或者中途結束,導致某一塊空間的釋放被跳過了

并且,在一些較大的程序中,某一個類似的函數會調用成千上萬次, 每一次去泄漏一點點內存,極少成多,漸漸內存便開始以肉眼無法看見的速度漸漸泄漏。

此時,C++便想出了一個C++獨有的解決方案:智能指針

為什么是C++獨有?因為只有C++才把這種史甩給程序員去自己解決

智能指針的原理

我們在文章剛開始便解釋到,對于自定義類型,C++會通過析構函數的方式將其釋放,但是new出來的空間并沒有析構函數。那為什么我們就不能強行給他一個析構函數呢??

而這個想法的實現方法其實也很簡單:只需要給一個類,讓這個類來裝這一個指針便可以了

template<class T>
class smart_ptr
{
public:smart_ptr(T* ptr=nullptr):_ptr(ptr){}~smart_ptr(){delete _ptr;}
private:T* _ptr;
};

我們在new一塊空間之后,把這個指針裝在smart_ptr這塊盒子里,當函數結束時,smart_ptr會自動調用析構函數銷毀,從而讓這個野指針實現自動銷毀的行為,這便是智能指針

void func()
{int* a = new int(1);smart_ptr<int> spa(a);//無論函數從哪里終止,只要函數被銷毀,spa就會被銷毀,從而釋放a
}

同時,為了方便,我們完全可以改造一下只能指針,將智能指針改造成智能指針來使用

//改造后的智能指針,與普通指針的使用方法便一致了
template<class T>
class smart_ptr
{
public:smart_ptr(T* ptr=nullptr):_ptr(ptr){}~smart_ptr(){delete _ptr;}T& operator*(){return *ptr;}T* operator->(){return &_ptr;}
private:T* _ptr;
};

改造之后,不僅可以實現指針的所有功能,而且被指針指向的空間也可以自動釋放,相當于指針plus

同時,我們在初始化時,不需要引入新變量了

void func()
{/*int* a = new int(1);smart_ptr<int> spa(a);*///直接簡化成smart_ptr<int> spa(new int(1));}

智能指針的問題

智能指針雖然看著好用,但是還是有著很多大問題。其中最大的便是賦值問題,如果我們想用一個智能指針去賦值另一個智能指針,那我們會發現一個嚴重的問題:?

那咋整?

而為了解決這一問題,C++標準庫給出了三種解決方案,這也便是C++智能指針的發展歷史。


std中的智能指針

其實智能指針的發展史很早。早在C++98中,std庫中便有了一個智能指針名為auto_ptr,但是一個字便可以概括:

不僅被開發者們詬病,而且很多公司還明確要求:不許使用庫中的智能指針。而這也導致了一個結果:不同的庫智能指針千奇百怪,程序和程序間的兼容依托稀爛。

而后C++11,對備受詬病的智能指針進行了改造,產生了兩種應用場景的智能指針:unique_ptr和shared_ptr,至此,智能指針的發展便已經完美畫上了句號,而我們如今最常用也最需要去學習的便是在C++11新加入的兩種智能指針

auto_ptr

C++98在剛開始接觸智能指針這一問題的時候,可能是項目經理開始催命了,便展現出了及其離譜的操作:權限轉移。這個操作雖然理論上確實解決了兩次delete的問題,但是就相當于餓到沒辦法才去赤石,沒有任何實際使用的價值

什么是權限轉移?就是在a賦值b的時候,將a裝著的指針清空,而原本的指針到了b身上,就相當于把a變成了b,然后a這一變量銷毀掉。

下面只展示賦值的情況代碼

template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr=nullptr):_ptr(ptr){}auto_ptr(const auto_ptr& ptr){if (ptr != *this)swap(*this, ptr);}auto_ptr& operator=(const auto_ptr& ptr){if(ptr!=*this)swap(*this, ptr);return *this;}~auto_ptr(){delete _ptr;}
private:T* _ptr;
};

你說他賦值了嗎?好像賦值了,但是又好像沒有賦值

我們想要對智能指針進行賦值,為的就是產生兩份智能指針,但是你這一通轉移,最后還是只給了我一份智能指針,而且還到了最后連我自己都不知道轉移到哪去了。解決問題了嗎?好像解決了,但是實際上讓問題變得更麻煩了,這也是auto_ptr一直被詬病的原因——為了修一個小bug,引入了一個更大的bug

unique_ptr

?C++11里,為了解決掉auto_ptr亂賦值這一毛病,干脆采用了一個簡單粗暴的方法——既然賦值會有bug,那就都別賦值了

unique_ptr在最初的智能指針上加了一個新特性:私有化operator=和賦值構造函數,讓unique_ptr無法被賦值

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

這樣,和他的名字一樣,unique_ptr就是獨一無二的智能指針,只能產生一次,無法多次使用。

雖然這種方法聽起來也是拖史,但是我們不可否認,unique_ptr解決了賦值的問題,而且也沒有產生新的bug

shared_ptr

而從shared_ptr開始,才算是直視多次delete這一問題。既然不斷去賦值會導致delete很多次,那我就記錄一下指向某塊空間的智能指針的個數,當最后一個智能指針也被銷毀,我再去delete,這樣就不會產生delete多次的問題了。而這實際也是引用計數的思想。?

不過這種想法雖然看起來簡單,真正實現起來卻還是有著一些障礙:

  1. 引用計數怎么實現?
  2. 如果某一個智能指針已經指向了一塊空間,之后再對其進行賦值,那原來被指向的空間怎么辦?
  3. 自己賦值自己又是什么情況?

我們來一個一個看

引用計數怎么實現?

最直觀直接的方法便是,在類中加入一個新變量count來記錄指向這塊空間的數量,如果有一個新的智能指針指向了這塊空間,就將count++,然后將++后的count賦值給新的智能指針。雖然想法很好,但是也有著一個巨大的問題——count無法同步

比如count==3,表示有三個智能指針a,b,c指向了這塊空間,我們再將c賦值給d,然后count++變成4,?c和d中的count也變成了4,那a和b怎么辦?a和b里的count還是3

此刻便可以想出一個很簡單的解決方案——在類中存放一個count的地址,這樣一個count改變,所有的count也便隨之改變了。

如何賦值給已存放地址的智能指針

在之前,我們都只考慮了用智能指針進行初始化。但是其實賦值還有一種情況——改變智能指針的值。這種情況,如果我們直接修改,顯然會導致原先的內存泄漏,所以我們在賦值的時候,還需要將原先的count--,不然會導致多出一次count 的問題。

如何自己賦值給自己

這是在所有類型的賦值中,我們都要考慮的情況。一般,如果自己賦值給自己,我們直接跳過就可以了,否則最好的情況是效率的損耗,而最壞的情況則會導致野指針。

舉個例子,如果有一個智能指針sp,其中的count只有1,我們自己賦值給自己,上述情況是count--,最終count==0,sp指向的空間被銷毀。然后再去賦值,指針指向了一塊被銷毀的空間,count++,就導致了指向野指針的問題。

所以,自己賦值給自己必須要進行判斷并跳過,否則或大或小都會產生一些意料之外的問題。

而解決了上述的問題,shared_ptr也算是被暴力解決了

template<class T>
class share_ptr
{
public:share_ptr(T* ptr = nullptr):_ptr(ptr){_count = new int(1);}share_ptr(const share_ptr& ptr){_ptr = ptr._ptr;_count = ptr._count;++(*_count);}share_ptr& operator=(const share_ptr& ptr){if (_ptr != ptr._ptr){delete_ptr();_ptr = ptr._ptr;_count=ptr._count;++(*_count);}return *this;}~share_ptr(){delete_ptr();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:void delete_ptr(){--(*_count);if (*_count == 0){delete _ptr;delete _count;}}T* _ptr;int* _count;
};

循環引用

shared_ptr雖然強大,但是shared_ptr也會有著內存泄漏的問題

我們來看雙向鏈表

struct ListNode
{ListNode():_pre(nullptr),_next(nullptr){}share_ptr<ListNode> _pre;share_ptr<ListNode> _next;
};void func()
{share_ptr<ListNode> head(new ListNode);share_ptr<ListNode> tail(new ListNode);head->_next = tail;tail->_pre = head;
}int main()
{func();
}

一個很經典的雙向鏈表問題,但是最終卻暗藏玄機。我們來看func函數內部

void func()
{share_ptr<ListNode> head(new ListNode);share_ptr<ListNode> tail(new ListNode);head->_next = tail;tail->_pre = head;//賦值之后,很正常的head和tail指向的空間count都為2//但是到了最后,調用析構函數,head的count--,tail的count--,兩個count都為1//最后head和tail都沒有被清理掉,內存泄漏了
}

而導致這個問題的本質原因是什么?是智能指針指向的對象,其內部還有一個無法被自動釋放的指針。?

而為了避免這個問題,C++采用了一個新的指針——weak_ptr。

weak_ptr顧名思義,是弱指針,其特性和shared_ptr基本相同,只不過在賦值的時候,count并不會增加

?也就是說,在類內部的智能指針,我們定義成weak_ptr,這樣就可以避免count異常的問題

unique_ptr和shared_ptr

光看解說量,我們都會發現,unique_ptr已經被shared_ptr完爆了。雖然如此,我們仍還是讓兩個不同的智能指針都進入了std標準庫,因為shared_ptr雖然在功能上遠遠戰勝了unique_ptr,但是產生的性能代價仍是非常大的。unique_ptr簡單粗暴,空間開銷少,性能極高,所以在不同的場合還是會在兩種智能指針之間取舍。

而auto_ptr


RAII

?看看得了,經常看我文章的都知道,我最不喜歡甩概念。

簡單說,RAII就是將空間的釋放自動化,我們不需要特意去delete,也不需要檢查內存是否泄漏,我們只需要把地址拋給一個對象,讓這個對象幫我們干這些事情就可以了

其實在很多語言中,都有一個垃圾回收機制,定期去回收掉被泄露的內存,而C++將這個責任甩給了程序員。但是,這并不是C++沒能力弄或者懶得弄,而是為了極致的性能,不得不去舍棄掉這個垃圾回收機制。往后無論C++如何發展,一些其他語言便捷的地方如果會導致性能的損耗,C++都不會去嘗試利用他們,而是讓我們程序員去想更好的解決方案,沒辦法,誰叫我們是站在語言歧視鏈頂端的程序員呢。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/541566.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/541566.shtml
英文地址,請注明出處:http://en.pswp.cn/news/541566.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

移除元素所有事件監聽_DOM 事件模型或 DOM 事件機制

DOM 事件模型DOM 的事件操作&#xff08;監聽和觸發&#xff09;&#xff0c;都定義在EventTarget接口。所有節點對象都部署了這個接口&#xff0c;其他一些需要事件通信的瀏覽器內置對象&#xff08;比如&#xff0c;XMLHttpRequest、AudioNode、AudioContext&#xff09;也部…

gettimezone_Java日歷getTimeZone()方法與示例

gettimezone日歷類的getTimeZone()方法 (Calendar Class getTimeZone() method) getTimeZone() method is available in java.util package. getTimeZone()方法在java.util包中可用。 getTimeZone() method is used to return this Calendar time zone. getTimeZone()方法用于返…

cass展點不在原位置_cass展點之步驟及方法

cass展點之步驟及方法cass展點是根據手工或坐標正反算軟件自動計算的結果&#xff0c;利用cass軟件將點號、坐標及其高程自動展示到圖紙上的一種方法。其基本步驟和方法如下&#xff1a;一、將井下測點的點號、以及計算好的Y坐標、X坐標、及高程由sheet1復制并粘貼到sheet2上面…

Java BufferedWriter close()方法與示例

BufferedWriter類close()方法 (BufferedWriter Class close() method) close() method is available in java.io package. close()方法在java.io包中可用。 close() method is used to flushes the characters from the stream and later will close it by using close() metho…

ISCC2014-reverse

這是我做reverse的題解。在咱逆向之路上的mark一下&#xff0c;&#xff0c;水平有限&#xff0c;大牛見笑。題目及題解鏈接&#xff1a;http://pan.baidu.com/s/1gd3k2RL 宗女齊姜 果然是僅僅有50分的難度&#xff0c;OD直接找到了flag. 找到殺手 這題用OD做非常麻煩。我改用I…

python 獲取當前時間再往前幾個月_Python 中的時間和日期操作

Python中,對日期和時間的操作,主要使用這3個內置模塊: datetime 、 time 和 calendar 獲取當前時間對應的數字 開發程序時,經常需要獲取兩個代碼位置在執行時的時間差,比如,我們想知道某個函數執行大概耗費了多少時間,就可以使用time.time()來做。 import time before =…

Java BigDecimal restder()方法與示例

BigDecimal類的restder()方法 (BigDecimal Class remainder() method) Syntax: 句法&#xff1a; public BigDecimal remainder(BigDecimal divsr);public BigDecimal remainder(BigDecimal divsr, MathContext ma_co);remainder() method is available in java.math package.…

python程序需要編譯么_python需要編譯么

一個經常聽見的問題&#xff0c;那就是&#xff1a;Python是解釋型的語言嗎&#xff1f;它會被編譯嗎&#xff1f;這個問題沒有想象中那么好回答。和很多人認識世界一樣&#xff0c;習慣以一個簡單的模型去評判一些事物。而事實上&#xff0c;里面包含了很多很多的細節。通常的…

DevOps平臺中的自動化部署框架設計

本文目錄&#xff1a; 一、背景 二、我們的需求是什么&#xff1f; 三、概念澄清 四、概念模型 五、總體設計 六、關鍵點設計 七、總結 一、背景 說到自動化部署&#xff0c;大家肯定都會想到一些配置管理工具&#xff0c;像ansible,chef,puppet, saltstack等等。雖然這些工具給…

插入排序算法 ,遞歸實現_C程序實現遞歸插入排序

插入排序算法 ,遞歸實現The only difference between Insertion sort and Recursive Insertion Sort is that in the Recursive method, we start from placing the last element in its correct position in the sorted array instead of starting from the first. 插入排序和…

python虛擬機直接加載字節碼運行程序_第二章 python如何運行程序

一.python解釋器介紹Python解釋器是一種讓程序運行起來的程序。實際上&#xff0c;解釋器是代碼與機器的計算機硬件之間的軟件邏輯層。當Python包安裝在機器上后&#xff0c;它包含了一些最小化的組件&#xff1a;一個解釋器和支持的庫。二.python的視角當Python運行腳本時&…

Java LocalDate類| 帶示例的format()方法

LocalDate類format()方法 (LocalDate Class format() method) format() method is available in java.time package. format()方法在java.time包中可用。 format() method is used to format this LocalDate object by using the given DateTimeFormatter object. format()方法…

胃癌2019csco指南_2019 CSCO胃癌診療指南精華來了!

一文輕松get 2019 CSCO胃癌診療指南更新要點&#xff01;文丨青青子衿 中山大學腫瘤防治中心來源丨醫學界腫瘤頻道近日&#xff0c;2019年CSCO指南發布會于南京召開。今天為大家推送的是2019 CSCO胃癌診療指南的最新更新&#xff0c;在發布專場中&#xff0c;來自華中科技大學同…

001_docker-compose構建elk環境

由于打算給同事分享elk相關的東西,搭建配置elk環境太麻煩了,于是想到了docker。docker官方提供了docker-compose編排工具,elk集群一鍵就可以搞定,真是興奮。好了下面咱們開始吧。 一、 https://github.com/deviantony/docker-elk $ cd /006_xxxallproject/005_docker/001_e…

Java即時類| toString()方法與示例

即時類toString()方法 (Instant Class toString() method) toString() method is available in java.time package. toString()方法在java.time包中可用。 toString() method is used to represent this Instant as a String by using the standards ISO-8601 format. toString…

learn opengl 中文_LearnOpenGL CN

歡迎來到OpenGL的世界歡迎來到OpenGL的世界。這個工程只是我(Joey de Vries)的一次小小的嘗試&#xff0c;希望能夠建立起一個完善的OpenGL教學平臺。無論你學習OpenGL是為了學業&#xff0c;找工作&#xff0c;或僅僅是因為興趣&#xff0c;這個網站都將能夠教會你現代(Core-p…

MYSQL5.7 日志管理

2019獨角獸企業重金招聘Python工程師標準>>> 慢查詢日志slow-query-log1 slow-query-log-filefile_name long_query_time1 #SQL執行多長時間以上會記錄到慢查詢日志&#xff0c;0~10s log_slow_admin_statementsOFF #在寫入慢查詢日志的語句中包含緩慢的管理語句。 …

duration java_Java Duration類| ofHours()方法與示例

duration javaDuration Class of Hours()方法 (Duration Class ofHours() method) ofHours() method is available in java.time package. ofHours()方法在java.time包中可用。 ofHours() method is used to represent the given hours in this Duration. ofHours()方法用于表示…

sumo的簡單應用_sumo快速運行簡單仿真實例詳細教程

本文旨在讓大家快速的了解sumo&#xff0c;并給出運行一個簡單的sumo的例子的教程&#xff0c;進而了解基本sumo工程的架構&#xff0c;使大家對該軟件產生興趣并持續學習下去&#xff0c;剛開始學習仿真的確枯燥&#xff0c;項目“跑起來”才是大家學習下去的動力&#xff0c;…

stl vector 函數_vector :: crbegin()函數,以及C ++ STL中的示例

stl vector 函數C vector :: crbegin()函數 (C vector::crbegin() function) vector::crbegin() is a library function of "vector" header, it is used to get the last element of a vector using const_reverse_iterator, it returns a const reverse iterator …