目錄
一、引入
二、智能指針的兩大特性:
1、RAII
特點:
好處:
2、行為像指針
三、智能指針起初的缺陷:拷貝問題
四、幾種智能指針的介紹。
1、C++98出現的智能指針——auto_ptr
auto_ptr解決上述拷貝構造的問題:
2、boost庫
3、unique_ptr
引用計數的實現:
賦值運算符的問題:(循環引用)
5、weak_ptr
特點:
解決循環引用問題:
五、C++智能指針的基本框架:
六、定制刪除器,以及包裝器的使用場景之一
七、內存泄漏:
1、什么是內存泄漏,內存泄漏的危害:
2、內存泄漏的分類
八、關于C++智能指針的相關代碼:
std::unique_ptr
std::weak_ptr
前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家
點擊跳轉到網站
一、引入
首先通過一個使用場景來引入智能指針,如下:
class HF { public:HF(int a, int b):_a(a),_b(b){cout << "HF(int a, int b)" << endl;};~HF(){cout << "~HF()" << endl;} private:int _a = 0;int _b = 1; };void fun() {HF* h1 = new HF(1, 1);HF* h2 = new HF(1, 1);HF* h3 = new HF(1, 1);delete h1;delete h2;delete h3; }int main() {try{fun();}catch (exception& e){cout << e.what() << endl;}return 0; }
這里有一個類HF,一個子函數fun,在fun里面new了三個HF對象,然后delete,正常情況下delete會先調用析構函數,然后再釋放相應的資源:
二、智能指針的兩大特性:
智能指針的兩大特性:
1、RAII
2、行為像指針
1、RAII
是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。(通俗來講,就是將資源交給一個類對象來管理,通過該類的構造函數交給對象。)
特點:
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源。
好處:
(1)、不需要顯式地釋放資源,而是通過智能指針間接幫忙釋放(2)、采用這種方式,對象所需的資源在其生命期內始終保持有效
2、行為像指針
智能指針實際也是一個類,要是行為像一個指針,即要重載解引用(*),箭頭(->),甚至有時還要重載方括號([ ])。
三、智能指針起初的缺陷:拷貝問題
首先我們實現一個簡易版智能指針:
new了一個日期類對象交給智能指針管理,智能指針對象存在期間,資源都是存在的,最后智能指針對象生命周期結束,調用析構函數釋放,同時釋放掉資源(delete);
template<class T> class SmartPtr { public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; } private:T* _ptr; }; struct Date {int _year;int _month;int _day; }; int main() {SmartPtr<int> sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPtr<Date> sparray(new Date);// 需要注意的是這里應該是sparray.operator->()->_year = 2018;// 本來應該是sparray->->_year這里語法上為了可讀性,省略了一個->sparray->_year = 2018;sparray->_month = 1;sparray->_day = 1;return 0; }
但是當我們用對象sp1去拷貝構造sp2時:
此時就會報錯:
原因在于我們沒有實現拷貝構造,此時默認拷貝構造就是淺拷貝,這樣兩個對象的成員變量都會指向這份資源,最后調用析構函數時,就會對這份資源delete兩次,從而造成野指針的問題。如何解決這個問題,在第四點進行介紹。
四、幾種智能指針的介紹。
1、C++98出現的智能指針——auto_ptr
頭文件:memory
具體信息可以查看官網文檔。
auto_ptr解決上述拷貝構造的問題:
auto_ptr是直接將資源的管理權轉移,用對象sp1去拷貝構造sp2,那么就會將sp1的資源的管理權交給sp2管理,而sp1被置空。
大致處理方法如下:拷貝后將sp1置空就行了:
注意,C++11的移動語義也是資源的轉移,但和這里是不一樣的,移動語義是針對將亡值去轉移資源,而這里sp1不是將亡值。
這樣做是有些問題的,這里資源轉移后,sp1就懸空了,此時拷貝后就不能去訪問sp1,否則就會出現空指針的問題,所以很多公司都禁止使用auto_ptr;
2、boost庫
提到智能指針,就得提一下boost庫,boost庫是C++第三方庫,里面就有智能指針,而C++的智能指針就是從這個庫里面引入的,然后進行了略微修改。
3、unique_ptr
該智能指針解決拷貝構造的問題的方法就是:簡單粗暴,禁止拷貝,適用于不需要拷貝的場景。
底層實際就是將拷貝構造給delete了:
同時,賦值運算符重載也要禁掉,默認生成的賦值運算符重載也是淺拷貝。
4、shared_ptr
當遇到需要拷貝構造的場景時,就需要使用shared_ptr,shared_ptr解決拷貝構造的問題的方法是:引用計數,去解決多次析構的問題。
引用計數的實現:
引用計數:記錄當前有幾個對象參與管理這個資源,在某個對象析構時,就將引用計數減1,當最后一個對象析構時才去釋放資源。
要實現引用計數,就需要一份資源對應一個計數,有人會想到定義一個靜態成員count,但實則不行,因為靜態成員是屬于整個類的,屬于所有對象。管理一個資源的時候是可以解決的,但當第二個資源出現時,就不能適用了,因為不同資源之間的引用計數都是同一個靜態成員變量,所以會相互影響。
實際上的實現如下:
增加一個成員變量*pcount,即指向引用計數的指針,在構造的時候(即資源來了),就new一個計數給該指針,在拷貝構造發生的時候,除了使兩個對象指向同一個資源外,兩個對象的引用計數也要指向同一個,并且要記得把引用計數++一下,在某個對象析構時,就將引用計數減1,然后判斷是否為最后一個對象的析構,如果是的話就釋放資源。
賦值運算符的問題:(循環引用)
shared_ptr雖然解決了拷貝構造的問題,但正因為引用計數的這樣實現,又會造成賦值運算符重載后出現問題。
賦值運算符簡單重載:
為了分析這里的缺陷,我們引入一個場景:雙向鏈表的賦值:
這是雙向鏈表的簡單實現
因為會將鏈表資源交給智能指針管理,如下:
所以鏈表的定義中,成員next和prev的類型也應該是智能指針,不然在賦值的時候會出現類型不同的問題,正因為需要這樣設計,問題就來了。
一般情況上述實現是沒有問題的,但當執行下面兩句代碼后,問題就來了:
這是在鏈接兩個節點,鏈接完后就會這樣:
首先出現兩個節點分別由n1和n2指向,此時兩個節點的引用計數分別都是1,當執行n1->next = n2時,n2指向的節點的引用計數就會變成2;當執行n2->prve = n1時,n1指向的節點的引用計數就會變成2。
最后當析構鏈表時:
這樣就形成了一個閉環,導致這兩個節點的內存泄漏,這個問題也叫循環引用。當兩個shared_ptr互相引用就會出現循環引用的問題。
5、weak_ptr
為了解決shared_ptr的循環引用問題,所以引入了weak_ptr。
特點:
weak_ptr的特點:沒有引用計數,支持默認構造,構造函數的形參沒有指針,因為該智能指針不參與資源管理,但自身成員變量會有一個指針,但會被置空,weak_ptr的重點在于拷貝構造和賦值。
解決循環引用問題:
這里的不同是將鏈表的成員變量_next和_prev的類型變為weak_ptr,正因為weak_ptr沒有增加引用計數,所以在節點鏈接的時候,引用計數不會增加,所以節點會正常釋放。
五、C++智能指針的基本框架:
六、定制刪除器,以及包裝器的使用場景之一
七、內存泄漏:
1、什么是內存泄漏,內存泄漏的危害:
什么是內存泄漏:內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏并不是指內存在物理上的消失,而是應用程序分配某段內存后,因為設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費。內存泄漏的危害:長期運行的程序出現內存泄漏,影響很大,如操作系統、后臺服務等等,出現內存泄漏會導致響應越來越慢,最終卡死。
2、內存泄漏的分類
C++中我們一般關心兩種分類:(1)、堆內存泄漏(Heap leak)堆內存指的是程序執行中依據須要分配通過malloc / calloc / realloc / new等從堆中分配的一塊內存,用完后必須通過調用相應的 free或者delete 刪掉。假設程序的設計錯誤導致這部分內存沒有被釋放,那么以后這部分空間將無法再被使用,就會產生Heap Leak。(2)、系統資源泄漏指程序使用系統分配的資源,比方套接字、文件描述符、管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定。
八、關于C++智能指針的相關代碼:
此小點內容來源于:豆包AI
#include <iostream> #include <memory>// 自定義類 class MyClass { public:MyClass() { std::cout << "MyClass constructor" << std::endl; }~MyClass() { std::cout << "MyClass destructor" << std::endl; }void doSomething() { std::cout << "Doing something..." << std::endl; } };class B;class A { public:std::shared_ptr<B> bPtr;~A() { std::cout << "A destructor" << std::endl; } };class B { public:std::weak_ptr<A> aPtr; // 使用 weak_ptr 避免循環引用~B() { std::cout << "B destructor" << std::endl; } };void uniquePtrExample() {// 創建一個 unique_ptr 指向 MyClass 對象std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>();// 調用對象的成員函數uniquePtr->doSomething();// unique_ptr 不能被復制,但可以轉移所有權// std::unique_ptr<MyClass> anotherPtr = uniquePtr; // 錯誤,不能復制std::unique_ptr<MyClass> anotherPtr = std::move(uniquePtr);if (!uniquePtr) {std::cout << "uniquePtr is empty after move" << std::endl;}if (anotherPtr) {anotherPtr->doSomething();} }void sharedPtrExample() {// 創建一個 shared_ptr 指向 MyClass 對象std::shared_ptr<MyClass> sharedPtr1 = std::make_shared<MyClass>();std::cout << "Shared pointer 1 use count: " << sharedPtr1.use_count() << std::endl;// 復制 shared_ptr,引用計數增加std::shared_ptr<MyClass> sharedPtr2 = sharedPtr1;std::cout << "Shared pointer 1 use count after copy: " << sharedPtr1.use_count() << std::endl;std::cout << "Shared pointer 2 use count: " << sharedPtr2.use_count() << std::endl;// 調用對象的成員函數sharedPtr2->doSomething();// 釋放一個 shared_ptr,引用計數減少sharedPtr2.reset();std::cout << "Shared pointer 1 use count after reset sharedPtr2: " << sharedPtr1.use_count() << std::endl; }void weakPtrExample() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->bPtr = b;b->aPtr = a;std::cout << "A use count: " << a.use_count() << std::endl;std::cout << "B use count: " << b.use_count() << std::endl;// 當 a 和 b 離開作用域時,對象會被正確銷毀 }int main() {std::cout << "=== Unique Ptr Example ===" << std::endl;uniquePtrExample();std::cout << std::endl;std::cout << "=== Shared Ptr Example ===" << std::endl;sharedPtrExample();std::cout << std::endl;std::cout << "=== Weak Ptr Example ===" << std::endl;weakPtrExample();std::cout << std::endl;return 0; }
在 C++ 里,手動管理動態分配的內存容易引發內存泄漏、懸空指針等問題。智能指針作為一種類模板,能有效管理動態分配的內存,避免這些問題的出現。C++ 標準庫提供了三種主要的智能指針:
std::unique_ptr
、std::shared_ptr
?和?std::weak_ptr
。
std::unique_ptr
?
std::unique_ptr
?屬于獨占式智能指針,它對所指向的對象擁有唯一的所有權。一旦?std::unique_ptr
?被銷毀,其指向的對象也會隨之被自動銷毀。在?
?uniquePtrExample
?函數中:
- 借助?
std::make_unique
?創建了一個?std::unique_ptr
,它指向?MyClass
?的一個對象。- 調用?
doSomething
?方法來使用這個對象。- 嘗試復制?
std::unique_ptr
?會引發編譯錯誤,因為它不允許復制,不過可以使用?std::move
?轉移所有權。- 轉移所有權之后,原?
std::unique_ptr
?變為空。
std::shared_ptr
?
std::shared_ptr
?是共享式智能指針,多個?std::shared_ptr
?能夠指向同一個對象。它采用引用計數來管理對象的生命周期,當引用計數變為 0 時,對象就會被銷毀。在?
?sharedPtrExample
?函數中:
- 利用?
std::make_shared
?創建了一個?std::shared_ptr
,它指向?MyClass
?的一個對象。- 通過?
use_count
?方法可以查看當前的引用計數。- 復制?
std::shared_ptr
?會使引用計數增加。- 調用?
reset
?方法可以釋放?std::shared_ptr
,從而使引用計數減少。
std::weak_ptr
?
std::weak_ptr
?是弱引用智能指針,它不擁有對象的所有權,只是對?std::shared_ptr
?所管理的對象進行弱引用。std::weak_ptr
?主要用于解決?std::shared_ptr
?的循環引用問題。在?
?weakPtrExample
?函數中:
- 定義了?
A
?和?B
?兩個類,其中?A
?類包含一個?std::shared_ptr<B>
?成員,B
?類包含一個?std::weak_ptr<A>
?成員。- 創建了?
A
?和?B
?的?std::shared_ptr
?對象,并相互引用。- 由于?
B
?類使用了?std::weak_ptr
,所以不會出現循環引用,當?a
?和?b
?離開作用域時,對象能夠被正確銷毀。