文章目錄
- 前言:
- 1. 智能指針的使用及原理
- 2. C++ 98 標準庫中的 auto_ptr:
- 3. C++ 11 中的智能指針
- 循環引用:
- shared_ptr 定制刪除器
- 4. 內存泄漏
- 總結:
前言:
隨著C++語言的發展,智能指針作為現代C++編程中管理動態分配內存的一種重要工具,越來越受到開發者的青睞。智能指針不僅簡化了內存管理,還有助于避免內存泄漏等常見問題。本文將深入探討智能指針的使用及其原理,從C++98標準庫中的auto_ptr
開始,逐步過渡到C++11中更為強大和靈活的智能指針類型,如unique_ptr
和shared_ptr
。此外,文章還將討論循環引用問題、內存泄漏的原因及其危害,并提供相應的解決方案。通過本文的學習,讀者將能夠更好地理解和運用智能指針,編寫出更安全、更高效的C++代碼。
1. 智能指針的使用及原理
RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控制程序資源(如內
存、文件句柄、網絡連接、互斥量等等)的簡單技術。
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在
對象析構的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象。這種做
法有兩大好處:
- 不需要顯式地釋放資源。
- 采用這種方式,對象所需的資源在其生命期內始終保持有效。
// SmartPtr.h
// 使用RAII思想設計的smartPtr類template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0錯誤");return a / b;
}
void Func()
{ShardPtr<int> sp1(new int);ShardPtr<int> sp2(new int);cout << div() << endl;
}int main()
{try {Func();}catch(const exception& e){cout<<e.what()<<endl;}return 0;
}
//test.cpp
#include <iostream>
#include "SmartPtr.h"
using namespace std;int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0錯誤");return a / b;
}void Func()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl;
}int main()
{try {Func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}
- 需要像指針一樣的去使用:
// 像指針一樣使用
T& operator*()
{return *_ptr;
}T* operator->()
{return _ptr;
}
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(new int(0));
*sp1 += 10;SmartPtr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";
sp3->second = 1; // 等價于 sp3.opertor->()->second = 1;cout << sp3->first << " " << sp3->second << endl;
- 智能指針的拷貝問題
// 智能指針的拷貝問題
int main()
{SmartPtr<int> sp1(new int(1));SmartPtr<int> sp2(sp1);return 0;
}
vector
/ list.
… 需要深拷貝,它們都是利用資源存儲數據,資源是自己的。拷貝時,每個對象各自一份資源,各管各的,所以深拷貝。
智能指針 / 迭代器… 期望的是淺拷貝
資源不是自己的,代為持有,方便訪問修改數據。他們拷貝的時候期望的指向同一資源,所以淺拷貝。而且智能指針還要負責釋放資源。
itertor it = begin();
2. C++ 98 標準庫中的 auto_ptr:
auto_ptr
管理權轉移,被拷貝的對象把資源管理權轉移給拷貝對象,導致被拷貝對象懸空
注意:在使用auto_ptr
過后不能訪問對象,否則就出現空指針了。很多公司禁止使用它,因為他很坑!
// 智能指針的拷貝問題
// 1. auto_ptr 管理權轉移,被拷貝的對象把資源管理權轉移給拷貝對象,導致被拷貝對象懸空
// 注意:在使用auto_ptr 過后不能訪問對象,否則就出現空指針了。很多公司禁止使用它,因為他很坑!
int main()
{std::auto_ptr<int> sp1(new int(1));std::auto_ptr<int> sp2(sp1);*sp2 += 10;// 懸空*sp1 += 10;return 0;
}
auto_ptr
的實現:
namespace hd
{template<class T>class auto_ptr {public:// RAIIauto_ptr(T* ptr = nullptr):_ptr(ptr){}// ap2(ap1)auto_ptr(auto_ptr<T>& ap){_ptr = ap._ptr;ap._ptr = nullptr;}~auto_ptr(){if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr; }}// 像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
3. C++ 11 中的智能指針
boost 智能指針
scoped_ptr
/ scoped_array
shared_ptr
/ shared_array
C++ 11
unique_ptr
跟scoped_ptr
類似的
shared_ptr
跟shared_ptr
類似的
unique_ptr
:
禁止拷貝,簡單粗暴,適合于不需要拷貝的場景
賦值也禁掉了:
unique_ptr
:實現
namespace hd
{template<class T>class unique_ptr {public:// RAIIunique_ptr(T* ptr = nullptr):_ptr(ptr){}// ap2(ap1)unique_ptr(const unique_ptr<T>& ap) = delete; // 禁掉拷貝構造// 賦值也要禁掉,賦值會生成默認成員函數,淺拷貝,也會出現問題unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;~unique_ptr(){if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}// 像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
如果必須要拷貝用shared_ptr
:
shared_ptr
允許自由拷貝,使用引用計數解決多次釋放的問題
引用計數: 記錄有幾個對象參與管理這個資源
shared_ptr
實現:
使用靜態成員變量實現。
namespace hd
{template<class T>class shared_ptr {public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr){_count = 1;}// sp(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;++_count;}~shared_ptr(){if (--_count == 0){std::cout << "delete:" << _ptr << std::endl;delete _ptr;}}int use_count(){return _count;}// 像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;static int _count;};template<class T>int shared_ptr<T>::_count = 0;
}
中釋放了一個資源!
如果使用靜態成員屬于這個類,屬于這個類的所有對象
需求:每個資源配一個引用計數,而不是全部都是一個引用計數!
所以,一個資源配一個引用計數無論多少個對象管理這個資源,只有這一個計數對象!
怎么找到這個引用呢?每個對象存一個指向計數的指針!
namespace hd
{template<class T>class shared_ptr {public:// RAIIshared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;// 拷貝時++計數++(*_pcount);}void release(){// 說明最后一個管理對象析構了,可以釋放資源了if (--(*_pcount) == 0){std::cout << "delete:" << _ptr << std::endl;delete _ptr;delete _pcount;}}// 賦值 sp1 = sp3;shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr) // 避免自己給自己賦值{release();_ptr = sp._ptr;_pcount = sp._pcount;// 拷貝時++計數++(*_pcount);}return *this;}~shared_ptr(){release();}int use_count(){return *_pcount;}// 像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};}
shared_ptr
的缺陷:
// shared_ptr 的缺陷
struct ListNode
{int _val;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;ListNode(int val = 0):_val(val),_next(nullptr),_prev(nullptr){}};int main()
{std::shared_ptr<ListNode> n1(new ListNode(10));std::shared_ptr<ListNode> n2(new ListNode(20));n1->_next = n2;n2->_prev = n1;//delete n1;//delete n2;return 0;
}
循環引用:
- 左邊的節點,是由右邊的節點
_prev
管著的,_prev
析構,引用計數減到 0, 左邊的節點就是釋放 - 右邊節點中
_prev
什么時候析構呢?右邊的節點被delete
時,_prev
析構。 - 右邊節點什么時候
delete
呢?右邊的節點被左邊的節點的_next
管著的,_next
析構,右邊的節點就釋放了。 _next
什么時候析構呢?_next
是左邊節點的成員,左邊節點delete
,_next
就析構了- 左邊節點什么時候釋放呢?回調 1 點 又循環上去了
右邊節點釋放 -> _prev
析構 -> 左邊節點的釋放 -> _next
析構 -> 右邊節點釋放
所以這是 shared_ptr
特定場景下的缺陷, 只要有兩個shared_ptr
互相管理就會出現這樣的情況,所以即使用了智能指針,同樣可能導致內存的泄漏。
struct ListNode
{int _val;std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;ListNode(int val = 0):_val(val){}};int main()
{std::shared_ptr<ListNode> n1(new ListNode(10));std::shared_ptr<ListNode> n2(new ListNode(20));cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;//delete n1;//delete n2;return 0;
}
用weak_ptr
可以通過不增加引用計數的方式,避免這個問題。(存在單獨自己的 引用計數)
weak_ptr
不支持RAII, 不參與資源管理,不支持指針初始化,但是還是能起到指向你的作用
weak_ptr
的實現:
namespace hd
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;}weak_ptr<T>& operator=(const shared_ptr<T>& sp){ _ptr = sp.get(); // 用 get方法調原生指針}// 像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
shared_ptr 定制刪除器
template<class T>
struct DeleteArry
{void operator()(T* ptr){delete[] ptr;}
};// 定制刪除器
int main()
{std::shared_ptr<ListNode> p1(new ListNode(10));std::shared_ptr<ListNode[]> p2(new ListNode[10]); // 可以用數組的std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArry<ListNode>()); // 用仿函數的對象去釋放!std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr); }); // 用lamada表達式也是可以的return 0;
}
定制刪除器實現:
namespace hd
{template<class T>class shared_ptr{public:// function<void(T*)> _del = [](T* ptr) {delete ptr; };template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _del(del){}// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;// 拷貝時++計數++(*_pcount);}// sp1 = sp4// sp4 = sp4;// sp1 = sp2;shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;// 拷貝時++計數++(*_pcount);}return *this;}void release(){// 說明最后一個管理對象析構了,可以釋放資源了if (--(*_pcount) == 0){std::cout << "delete:" << _ptr << std::endl;//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){// 析構時,--計數,計數減到0,release();}int use_count(){return *_pcount;}// 像指針一樣T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;std::function<void(T*)> _del = [](T* ptr) {delete ptr; };};}
4. 內存泄漏
什么是內存泄漏:內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內
存泄漏并不是指內存在物理上的消失,而是應用程序分配某段內存后,因為設計錯誤,失去了對
該段內存的控制,因而造成了內存的浪費。
內存泄漏的危害:長期運行的程序出現內存泄漏,影響很大,如操作系統、后臺服務等等,出現
內存泄漏會導致響應越來越慢,最終卡死。
void MemoryLeaks()
{// 1.內存申請了忘記釋放int* p1 = (int*)malloc(sizeof(int));int* p2 = new int;// 2.異常安全問題int* p3 = new int[10];Func(); // 這里Func函數拋異常導致 delete[] p3未執行,p3沒被釋放.delete[] p3;
}
總結:
本文詳細介紹了智能指針的概念、使用和原理,從C++98的auto_ptr
到C++11的unique_ptr
和shared_ptr
,展示了智能指針在現代C++編程中的應用和發展。我們了解到RAII(資源獲取即初始化)的設計模式,它通過將資源管理封裝在對象的生命周期中,簡化了資源的獲取和釋放過程。文章還討論了智能指針的拷貝問題,特別是auto_ptr
的缺陷和shared_ptr
的循環引用問題,以及如何使用weak_ptr
和定制刪除器來解決這些問題。
此外,文章還探討了內存泄漏的概念、原因和危害,以及如何在實際編程中避免這些問題。通過具體的例子和代碼,我們學習了如何使用智能指針來管理資源,確保資源在使用完畢后能夠被正確釋放,從而避免內存泄漏和其他潛在的資源管理問題。
總的來說,智能指針是C++中一個強大的特性,它不僅提高了代碼的安全性和效率,還使得資源管理變得更加簡單和直觀。通過本文的學習,讀者應該能夠更加自信地在C++項目中使用智能指針,編寫出更加健壯和可靠的軟件。