遇到的問題:
在編寫C++程序時,我們最常遇到的問題也就是內存方面的問題了。申請內存後未釋放,打開文件後未關閉,這些都屬於內存泄漏的問題。
舉個栗子:
如果我們在申請內存之後,程序拋了一個異常,並且我們的catch代碼段也沒有去做對應的處理,那麼這個時候就會發生內存泄漏。如下程序:
int Division(int a, int b)
{
if (b == 0) {
throw "Division by zero condition";
}
return a / b;
}
int main()
{
try
{
int *str = new int[1000]; //申請內存
int a = 10;
int b = 0;
cout << Division(a, b) << endl; //拋異常
delete[] str; //釋放內存語句未執行
}
catch (const char* e)
{
cout << e << endl; //沒有對內存進行釋放,導致內存泄漏
}
return 0;
}
上面這段代碼可以看到在 try 代碼段中我們申請了一塊空間,但是當我們運行到Division 函數中時,它判斷除數是 0 ,拋出異常,那麼程序接下來會跳過後面的 delete[ ] str;而且我們也沒有在 catch 代碼塊中進行釋放內存的操作,因此會造成內存泄漏。
智能指針:
RAII:
RAII 是一種利用對象生命週期來控制內存資源的一種技術,因爲在面向對象的編程語言中,對象的創建和銷燬分別是通過構造函數和析構函數來完成的,而且這兩個函數都不用程序猿人爲的控制,都是由系統自動調用的。
我們可以將資源的申請放在構造函數中,將資源的釋放放在析構函數中,這樣即便程序異常退出,在對象生命結束的時候,系統也會自動調用析構函數去執行釋放資源的語句。這樣大大減少了內存泄漏一系列問題的產生。
RAII帶來的好處:
1、省去了我們人爲釋放資源的過程。
2、資源在對象生命週期內始終有效。
智能指針的實現:
原理:RAII 的思想 + 實現指針的特徵(重載 *、->的操作)
std::auto_ptr:
auto_ptr 是 C++98版本就提供的一款智能指針。
auto_ptr 的問題:管理權轉移,當對象發生拷貝或者賦值之後,前面對象就會懸空,導致前面的智能指針不能再用。
模擬實現:
下面的所有代碼中的 A 類如下:
class A
{
public:
A()
{ cout << "A()" << endl; }
~A()
{ cout << "~A()" << endl; }
int _num;
};
auto_ptr 模擬實現:
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr) {
delete _ptr;
}
}
//拷貝
AutoPtr(AutoPtr& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr; //資源轉移,管理權移交,ap的管理權失效
}
//賦值
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap) {
//釋放當前對象的資源
if (_ptr) {
delete _ptr;
}
//將新來的資源移交給自己
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
演示拷貝/賦值管理權轉移:
std::unique_ptr:
爲了解決這個管理權轉移的問題,在 C++11 中引入了一個新的智能指針 unique_ptr,這個智能指針的做法非常粗暴,那麼就是直接不讓拷貝和賦值。
所以它的問題就是不能拷貝和賦值。
模擬實現:
template <class T>
class UniquePtr
{
public:
UniquePtr()
:_ptr(nullptr)
{}
~UniquePtr()
{
if(_ptr)
delete _ptr;
}
T* operator->()
{ return _ptr; }
T& operator*()
{ return *_ptr; }
private:
UniquePtr(const UniquePtr<T>&) = delete;
UniquePtr<T>& operator=(const UniquePtr<T>&) = delete;
private:
T* _ptr;
};
不能賦值和拷貝肯定是不合理的,所以C++11又有了shared_ptr來解決這些問題。
std::shared_ptr:
原理:通過引用計數來解決不能拷貝和賦值的問題。
1、shared_ptr在其內部給每一份資源都維護了一個引用計數,通過引用計數來統計當前資源被幾個對象共享。
2、在對象被銷燬時,這份資源並不是直接釋放,而是該資源的引用計數 -1 。直到引用計數減爲 0,證明現在資源沒有對象在使用,資源纔會被釋放。
問題:循環引用
模擬實現:(在模擬實現的過程中要注意,因爲引用計數是被多個對象所共享的,所以在對引用計數進行 ++操作或者 --操作時要注意線程安全問題,要通過加鎖來實現線程安全)
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
:_ptr(ptr)
,_mutex(new mutex)
,_Count(new int(1))
{}
~SharedPtr()
{
Release();
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _mutex(sp._mutex)
, _Count(sp._Count)
{
AddRefCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
if (_ptr != sp._ptr) {
Release();
_ptr = sp._ptr;
_mutex = sp._mutex;
_Count = sp._Count;
AddRefCount();
}
return *this;
}
T* operator->()
{ return _ptr; }
T& operator*()
{ return *_ptr; }
int UseCount()
{
return *_Count;
}
void AddRefCount()
{
_mutex->lock();
++(*_Count);
_mutex->unlock();
}
private:
void Release()
{
bool flag = false; //計數爲0標誌位
_mutex->lock();
if (--(*_Count) == 0) {
delete _ptr;
delete _Count;
flag = true;
}
_mutex->unlock();
if (flag == true)
delete _mutex;
}
private:
T* _ptr; //指向管理資源的指針
mutex* _mutex; //互斥鎖
int* _Count; //引用計數
};
上面提到shared_ptr有一個問題:循環引用。
我們看看下面這種情況:
struct ListNode
{
int data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
運行結果如圖:
看起來好像沒什麼問題,在沒有指向新的資源時,node1 和 node2 的引用計數都是 1. 指向新的資源後,引用計數變爲 2。但是按照邏輯,程序執行完應該調用析構函數釋放對象資源纔對,這裏並沒有調用析構函數,這就是循環引用問題,這個問題導致了令人談虎色變的內存泄漏問題。
我們將相互指向的兩行代碼註釋掉,看看運行結果:
這樣程序是走的正常的邏輯,當有對象釋放資源時,引用計數 -1,引用計數減爲 0 時,最終調用析構函數,資源徹底釋放。
所以應該如何解決循環引用的問題呢?
主要有以下兩種辦法:
① 當要發生循環引用的時候,手動打破循環引用釋放對象資源
② 使用弱引用指針 weak_ptr 來打破循環引用
第一種方法較爲麻煩,所以第二種方法是最常用的方法。
將我們剛纔的代碼進行修改,將ListNode中的強引用智能指針改爲弱引用智能指針。也就是說在有可能發生循環引用的地方都是用弱引用智能指針。
struct ListNode
{
int data;
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
修改之後循環引用的問題也就迎刃而解了。
boost::scoped_ptr:
scoped_ptr 爲了解決管理權轉移問題也是非常簡單粗暴直接防拷貝防賦值。
在boost庫中有很多優秀的東西,比如 C++11 標準庫中的uniqued_ptr、weak_ptr、shared_ptr都是參照boost庫中的實驗原理實現的。
在 boost 庫中有一個 scoped_ptr ,在C++11標準庫中的 uniqued_ptr 就是對應 boost 庫中的scoped_ptr。