融會貫通C++智能指針教程
一、基礎(chǔ)知識介紹
裸指針常出現(xiàn)以下幾個問題:
- 忘記釋放資源,導(dǎo)致資源泄露(常發(fā)生內(nèi)存泄漏問題)
- 同一資源釋放多次,導(dǎo)致釋放野指針,程序崩潰
- 寫了釋放資源的代碼,但是由于程序邏輯滿足條件,執(zhí)行中間某句代碼時程序就退出了,導(dǎo)致釋放資源的代碼未被執(zhí)行到
- 代碼運行過程中發(fā)生異常,隨著異常棧展開,導(dǎo)致釋放資源的代碼未被執(zhí)行到
template<typename T> class SmartPtr { public: SmartPtr(T* ptr = nullptr):_ptr(ptr) {} ~SmartPtr() {delete _ptr;} private: T* _ptr; }; int main(){ SmartPtr<int> ptr(new int); return 0; }
上面這段代碼就是一個非常簡單的智能指針,主要用到了這兩點:
手動實現(xiàn)智能指針體現(xiàn)在把裸指針進行了面向?qū)ο蟮姆庋b,在構(gòu)造函數(shù)中初始化資源地址,在析構(gòu)函數(shù)中負責(zé)釋放資源
利用棧上的對象出作用域自動析構(gòu)這個特點,在智能指針的析構(gòu)函數(shù)中保證釋放資源。
所以,智能指針一般都是定義在棧上的
面試官:能不能在堆上定義智能指針?
答:不能。就好比SmartPtr<int>* ptr = new SmartPtr<int>();
這段代碼中,在堆空間定義一個智能指針,
這依然需要我們手動進行delete
,否則無法堆空間的對象無法釋放,因為堆空間的對象無法自動調(diào)用析構(gòu)函數(shù)。
一般而言智能指針還需要提供裸指針常見的*
和 ->
兩種運算符的重載函數(shù):
const T& operator*() const{return *_ptr;} T& operator*(){return *_ptr;} const T operator->() const{return _ptr;} T operator->(){return _ptr;}
二、不帶引用計數(shù)的智能指針
int main(){ SmartPtr<int> ptr1(new int); SmartPtr<int> ptr2(ptr1); return 0; }
以上代碼運行時,由于ptr2
拷貝構(gòu)造時默認是淺拷貝,會出現(xiàn)同一資源釋放兩次的錯誤(釋放野指針),這里需要解決兩個問題:
智能指針的淺拷貝
多個智能指針指向同一個資源的時候,怎么保證資源只釋放一次,而不是每個智能指針都釋放一次
不帶引用計數(shù)的智能指針主要包括
auto_ptr
,scoped_ptr
,unique_ptr
(1)auto_ptr源碼
auto_ptr(auto_ptr& _Right) noexcept : _Myptr(_Right.release()) {} _Ty* release() noexcept { _Ty* _Tmp = _Myptr; _Myptr = nullptr; return _Tmp; }
使用auto_ptr
auto_ptr<int> ptr1(new int); auto_ptr<int> ptr2(ptr1);
從源碼可以看到,auto_ptr
底層先是將ptr1
置空,然后將指向的資源再給ptr2
, auto_ptr
所做的就是使最后一個構(gòu)造的指針指向資源,以前的指針全都置空,如果再去訪問以前的指針就是訪問空指針了,這很危險。所以一般不使用auto_ptr
(2)scoped_ptr
該智能指針底層私有化了拷貝構(gòu)造函數(shù)和operator
=賦值函數(shù),
從根本上杜絕了智能指針淺拷貝的發(fā)生,所以scoped_ptr
也是不能用在容器當(dāng)中的。
- 如果容器互相進行拷貝或者賦值,就會引起
scoped_ptr
對象的拷貝構(gòu)造和賦值,這是不允許的,代碼會提示編譯錯誤。 auto_ptr
和scoped_ptr
這一點上的區(qū)別,有些資料上用所有權(quán)的概念來描述,道理是相同的。auto_pt
r可以任意轉(zhuǎn)移資源的所有權(quán),而scoped_ptr
不會轉(zhuǎn)移所有權(quán)(因為拷貝構(gòu)造和賦值被禁止了)
一般也不推薦使用scoped_ptr
(3)unique_ptr源碼
template <class _Dx2 = _Dx, enable_if_t<is_move_constructible_v<_Dx2>, int> = 0> unique_ptr(unique_ptr&& _Right) noexcept : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {} unique_ptr(unique_ptr<_Ty2, _Dx2>&& _Right) noexcept : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx2>(_Right.get_deleter()), _Right.release()) {} // 拷貝構(gòu)造或者賦值運算符的時候,用于將以前的智能指針置空 pointer release() noexcept { return _STD exchange(_Mypair._Myval2, nullptr); } unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete;
從源碼可以看到,unique_ptr
直接delete
了拷貝構(gòu)造函數(shù)和operator
=賦值重載函數(shù)
禁止用戶對unique_ptr
進行顯示的拷貝構(gòu)造和賦值,防止智能指針淺拷貝問題的發(fā)生
但是unique_ptr提供了帶右值引用參數(shù)的拷貝構(gòu)造和賦值
即unique_ptr智能指針可以通過右值引用進行拷貝構(gòu)造和賦值操作
unique_ptr<int> ptr1(new int); unique_ptr<int> ptr2(std::move(ptr1));// 使用右值引用的拷貝構(gòu)造,由于執(zhí)行了release,ptr1已經(jīng)被置空 cout << (ptr1 == nullptr) << endl; // true ptr2 = std::move(ptr1); // 使用右值引用的operator=賦值重載函數(shù) cout << (ptr2 == nullptr) << endl; // true
用臨時對象構(gòu)造新的對象時,也會調(diào)用帶右值引用參數(shù)的函數(shù)
unique_ptr<int> get_unique_ptr() { unique_ptr<int> tmp(new int); return tmp; } int main(){ unique_ptr<int> ptr = get_unique_ptr(); // 調(diào)用帶右值引用參數(shù)的拷貝構(gòu)造函數(shù),由tmp直接構(gòu)造ptr return 0; }
unique_ptr從名字就可以看出來,最終也是只能有一個智能指針引用資源,其他智能指針全部置空
三、帶引用計數(shù)的智能指針
#include <iostream> #include <memory> using namespace std; template<typename T> class RefCnt { public: RefCnt(T* ptr = nullptr){ mcount = (mptr == nullptr) ? 0 : 1; } void addRef() { mcount++; } int subRef() { return --mcount; } private: int mcount; // mptr指向某個資源的引用計數(shù),線程不安全。使用atomic_int線程安全 T* mptr; //指向智能指針內(nèi)部指向資源的指針,間接指向資源 }; template<typename T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) :_ptr(ptr) { cout << "SmartPtr()" << endl; mpRefCnt = new RefCnt<int>(_ptr); // 用指向資源的指針初始化引用計數(shù)對象 } ~SmartPtr() { if (0 == mpRefCnt->subRef()) { cout << "釋放資源析構(gòu)~SmartPtr()" << endl; delete _ptr; } else { cout << "空析構(gòu)~SmartPtr()" << endl; } } const T& operator*() const { return *_ptr; } T& operator*() { return *_ptr; } const T operator->() const { return _ptr; } T operator->() { return _ptr; } SmartPtr(const SmartPtr<T>& src) :_ptr(src._ptr), mpRefCnt(src.mpRefCnt) { cout << "SmartPtr(const SmartPtr<T>& src)" << endl; if (_ptr != nullptr) { // 用于拷貝的對象已經(jīng)引用了資源 mpRefCnt->addRef(); } } SmartPtr<T>& operator=(const SmartPtr<T>& src) { cout << "SmartPtr<T>& operator=" << endl; if (this == &src) { return *this; } // 當(dāng)前智能指針指向和src相同的資源,考慮是否釋放之前的資源 if (0 == mpRefCnt->subRef()) { // 若之前指向的資源引用計數(shù)為1,釋放之前的資源 delete _ptr; } _ptr = src._ptr; mpRefCnt = src.mpRefCnt; mpRefCnt->addRef(); return *this; } private: T* _ptr; // 指向資源的指針 RefCnt<T>* mpRefCnt; // 指向該資源引用計數(shù)的指針 }; int main(){ SmartPtr<int> ptr1(new int); SmartPtr<int> ptr2(ptr1); SmartPtr<int> ptr3; ptr3 = ptr2; *ptr2 = 100; cout << *ptr2 << " " << *ptr3 << endl; return 0; }
四、shared_ptr 和 weak_ptr
- shared_ptr:強智能指針,可以改變資源的引用計數(shù)
- weak_ptr:弱智能指針,不可改變資源的引用計數(shù)
weak_ptr
-> shared_ptr
-> 資源
智能指針的交叉引用問題
#include <iostream> #include <memory> using namespace std; class B; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } shared_ptr<B> _ptrb; }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } shared_ptr<A> _ptra; }; int main() { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa->_ptrb = pb; pb->_ptra = pa; cout << pa.use_count() << endl; cout << pb.use_count() << endl; return 0; } /* A() B() 2 2 */
解決辦法: 定義對象時用shared_ptr
,引用對象時用weak_ptr
class B; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } weak_ptr<B> _ptrb; }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } weak_ptr<A> _ptra; }; /* A() B() 1 1 ~B() ~A() */
weak_ptr
只是用于觀察資源,不能夠使用資源,并沒有實現(xiàn)operator*和operator->??梢允褂脀eak_ptr對象的lock()方法返回shared_ptr
對象,這個操作會增加資源的引用計數(shù)。
五、多線程訪問共享對象的線程安全問題
線程A和線程B訪問一個共享對象,如果線程A已經(jīng)析構(gòu)這個對象
線程B又要調(diào)用該共享對象的成員方法,線程B再去訪問該對象,就會發(fā)生不可預(yù)期的錯誤
#include <iostream> #include <memory> #include <thread> using namespace std; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } void test() { cout << "test()" << endl; } }; void handler01(A* q) { // 睡眠2s,使得主線程進行delete std::this_thread::sleep_for(std::chrono::seconds(2)); q->test(); } int main() { A* p = new A(); thread t1(handler01, p); delete p; t1.join(); return 0; }
在多線程訪問共享對象的時候,往往需要lock檢測一下對象是否存在。
開啟一個新線程,并傳入共享對象的弱智能指針。
#include <iostream> #include <memory> #include <thread> using namespace std; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } void test() { cout << "test()" << endl; } }; void handler01(weak_ptr<A> pw) { shared_ptr<A> ps = pw.lock(); // lock方法判定資源對象是否析構(gòu) if (ps != nullptr) { ps->test(); }else { cout << "A對象已經(jīng)析構(gòu),無法訪問" << endl; } } int main() { { shared_ptr<A> p(new A()); //開啟一個新線程,并傳入共享對象的弱智能指針 thread t1(handler01, weak_ptr<A>(p)); // 將子線程和主線程的關(guān)聯(lián)分離,也就是說detach()后子線程在后臺獨立繼續(xù)運行, // 主線程無法再取得子線程的控制權(quán),即使主線程結(jié)束,子線程未執(zhí)行也不會結(jié)束。 t1.detach(); } // 讓主線程等待,給時間子線程執(zhí)行,否則main函數(shù)最后會調(diào)用exit方法結(jié)束進程 std::this_thread::sleep_for(std::chrono::seconds(2)); return 0; }
六、自定義刪除器
通常我們使用智能指針管理的資源是堆內(nèi)存,當(dāng)智能指針出作用域的時候,
在其析構(gòu)函數(shù)中會delete釋放堆內(nèi)存資源,但是除了堆內(nèi)存資源,智能指針還可以管理其它資源,
比如打開的文件,此時對于文件指針的關(guān)閉,就不能用delete了,
這時我們需要自定義智能指針釋放資源的方法。
template<typename T> class ArrDeletor { public: // 對象刪除的時候需要調(diào)用對應(yīng)刪除器的()重載函數(shù) void operator()(T* ptr) const { cout << "ArrDeletor::operator()" << endl; delete[] ptr; } }; template<typename T> class FileDeletor { public: // 對象刪除的時候需要調(diào)用對應(yīng)刪除器的()重載函數(shù) void operator()(T* fp) const { cout << "FileDeletor::operator()" << endl; fclose(fp); } }; int main() { unique_ptr<int, ArrDeletor<int>> ptr1(new int[100]); unique_ptr<FILE, FileDeletor<FILE>> ptr2(fopen("1.cpp", "w")); // 使用lambda表達式 // function<返回值(參數(shù))> // []叫做捕獲說明符,表示一個lambda表達式的開始。接下來是參數(shù)列表,即這個匿名的lambda函數(shù)的參數(shù) unique_ptr<int, function<void(int*)>> ptr1( new int[100], [](int* p)->void { cout << "call lambda release new int[]" << endl; delete[] p; } ); unique_ptr<FILE, function<void(FILE*)>> ptr2( fopen("1.cpp", "w"), [](FILE* p)->void { cout << "call lambda release fopen(\"1.cpp\", \"w\")" << endl; fclose(p); } ); return 0; }
到此這篇關(guān)于C++智能指針教程的文章就介紹到這了,更多相關(guān)C++智能指針內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實現(xiàn)LeetCode(74.搜索一個二維矩陣)
這篇文章主要介紹了C++實現(xiàn)LeetCode(74.搜索一個二維矩陣),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-07-07Qt學(xué)習(xí)之QListWidget控件的使用教程詳解
這篇文章主要為大家詳細介紹了Qt中QListWidget控件的使用教程,文中的示例代碼講解詳細,對我們學(xué)習(xí)Qt有一定的幫助,需要的可以參考一下2022-12-12C++?RBTree紅黑樹的性質(zhì)與實現(xiàn)
紅黑樹是一種二叉搜索樹,但在每個結(jié)點上增加一個存儲位表示結(jié)點的顏色,可以是Red或Black;通過對任何一條從根到葉子的路徑上各個結(jié)點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出倆倍,因而是平衡的2023-03-03C語言中find_package()的搜索路徑的實現(xiàn)
本文主要介紹了C語言中find_package()的搜索路徑的實現(xiàn),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12