很多人都說:C++比Java難,難就難在指針,指針很難學,但是不可否認,通過指針,我們確實將很多問題解決了,不過爲什麼要引入智能指針呢?
首先這裏先聲明一點:智能指針並不是指針,它的本質是個類,不過,它的功能是自動管理指針所指向的動態內存和釋放工作,所以叫他智能指針。
那麼爲什麼要引入智能指針呢?先來看一個例子:
int FunTest()
{
int *p = new int;
if (1)
{
throw 1;
}
delete p; //由於異常處理,導致delete未執行
}
int main()
{
try
{
FunTest();
}
catch (...)
{
cout << "未知異常" << endl;
}
return 0;
}
異常拋出我們都知道,上面的例子中因爲異常拋出,導致delete p;未執行,從而導致內存泄漏問題,異常拋出只是改變程序執行順序的一種情況,而類似這樣的有很多,那麼如何避免這樣的問題呢?又回到了問題的起點,當然是智能指針的引入了。
那麼接下來我們就來看幾種智能指針吧!
auto_ptr
auto_ptr是C++標準庫中的一個智能指針,其中包括幾個成員函數,但是標準庫又建議最好不要再使用auto_ptr,這是爲什麼?
既然有這個智能指針,而又強調不讓使用,那麼肯定是這個智能指針出問題了,或者是其中存在很大的缺陷,如果存在缺陷,那麼是什麼呢?
只看這些沒什麼用,我們還是來模擬一下auto_ptr吧,這樣,有什麼缺陷就一目瞭然了:
template<typename T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
//拷貝構造函數
AutoPtr(AutoPtr<T>&ap)
{
_ptr = new T; //先分配空間
_ptr = ap._ptr; //再資源轉移
ap._ptr = NULL; //將原來的指針置空
}
~AutoPtr()
{
if (NULL != _ptr)
{
delete _ptr;
}
}
//賦值運算符重載
AutoPtr<T> operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr
}
private:
T* _ptr;
};
auto_ptr說白了就是權限的轉移,在賦值運算符重載和拷貝構造函數中將資源轉移,防止出現多個指針同時指向一塊內存空間的現象從而防止內存被釋放多次導致內存泄漏。
但是同時,這個智能指針存在很大的缺陷,我們知道資源轉移之後,原來的指針被賦空,要是再進行一些使用的語句那麼程序會崩的:
比如:
int main()
{
AutoPtr<int> a = new int(1);
AutoPtr<int> b(a);
*a = 2;//出現錯誤 1
AutoPtr<int> b(AutoPtr<int> (new int));// 2
return 0;
}
總結一下,使用auto_ptr的話,一般會出現兩個問題:
一是,已經轉移權限的指針要是再次被使用的時候會出現錯誤,因爲那個指針在轉移資源之後就被賦成空指針了,要是再使用,通過解引用的方法來賦值 ,顯然是不行的,所以就報錯了。
二是,拷貝運算符接受了常量。我們知道臨時對象具有常性,(也就是不能改變),如果使用拷貝運算符的時候,將一個臨時對象附進去,也就是像例子中的那樣,那麼就會出錯。
然而,auto_ptr已經是標準庫中的智能指針了,也就是已經有人使用了,或許在某些重要的領域,已經佔有重要的位置了,所以,標準庫肯定不會輕易將auto_ptr移除,但是確實存在問題,所以只能建議:最好不要使用了
scoped_ptr
首先聲明一點:那就是, scoped_ptr 並不是C++標準庫中的智能指針,而是boost庫中的智能指針,不過在標準庫中也有一個分身,它叫:unique_ptr。
scoped_ptr是一個很有特點的智能指針,之所以說他很有特點,從名字上就會看出來,當然不是boost庫中的那個名字了,而是標準庫中的那個名字,unique(唯一的),這就是它的 特點:獨佔一份,不允許賦值和拷貝。
那麼問題來了,如何防拷貝呢?
下面我們給出三種方法,看看哪一種適合:
1、在類中給出賦值運算符的重載和拷貝構造函數的聲明,但是,不給出定義;
2、在類中給出賦值運算符的重載和拷貝構造函數的定義,不過給成私有的;
3、在類中 只 給出賦值運算符的重載和拷貝構造函數的聲明,並且給成私有的。
在討論這個問題之前,我們還是將scoped_ptr 模擬實現出來,這樣,我們再來討論並驗證我們的答案:
template<typename T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr)
:_ptr(ptr)
{}
~ScopedPtr()
{
if (_ptr)
{
delete _ptr;
}
}
T &operator *() const
{
if (_ptr)
{
return *_ptr;
}
}
T *operator ->() const
{
return _ptr;
}
private:
ScopedPtr(const ScopedPtr<T> & sp);
ScopedPtr<T>& operator=(const ScopedPtr<T>& sp);
private:
T *_ptr;
};
先說一下第一種吧,當我們的函數原型給出,並且是public的形式,那麼我們可不可以認爲,這個函數已經被給出了,因爲,不管在類的內部還是外部,我們都可以將這個函數實現了,那麼,防拷貝還有效嗎?
顯而易見第一種是不行的,那麼剩下的第二種和第三種,這兩種都是私有的形式,但是區別是,一個定義了,一個只是聲明瞭,不過,定義了就有風險,因爲在C++中有一個特殊的存在可以在類外訪問類的私有成員,那就是友元函數,如果說懶得話,那麼最好還是不寫吧。
綜合所述,還是隻給出聲明,並且給成私有的,這樣可以防普通類型的同時也可以防友元。