首頁 > 軟體

C++之智慧指標初步及棄用auto_ptr的原因分析

2023-03-23 22:03:59

RAII

使用區域性物件來管理資源的技術

RAII的原理

RAII的四個步驟

裸指標存在的問題

delete後的指標變數就變成了一個失效指標(也叫作懸空指標)。

對於下面的程式碼:

void Destroy(Object *op)
{
	delete op;
	delete[] op;
}

Object *op = new Object(10);
Object *arop = new Object[10];

Destroy(op);
Destroy(arop);

因此:

智慧指標

智慧指標的引入

智慧指標是比原始指標更加智慧的類,解決懸空指標多次刪除被指向物件,以及資源洩漏問題,通常用來確保指標的壽命和其指向物件的壽命一致。

智慧指標雖然很智慧,很容易被誤用,智慧也是有代價的。

四種智慧指標

  • auto_ptr
  • unqiue_ptr(唯一性智慧指標)
  • shared_ptr(共用性智慧指標)
  • weak_ptr(管理弱參照)

其中後三個是C11支援,並且第一個已經被C11棄用。

C98中的auto_ptr所做的事情,就是動態分配物件以及當物件不再需要時自動執行清理。

下面我們首先來了解一下為什麼要將auto_ptr移除的原因:

因為該型別的智慧指標意義不明確,使用淺拷貝方式時,兩個物件擁有同一塊資源:我們模仿原始碼的邏輯

瞭解一下:比如下面的程式碼:

class Object
{
    int value;
public:
    Object(int x = 0):value(x){cout<<"Create Object:"<<this<<endl;}
    ~Object(){cout<<"Destroy Object:"<<this<<endl;}

    int  & Value(){return value;}
    const int& Value() const{return value;}
};

template<class _Ty>
class my_auto_ptr
{
private:
    bool _Owns;//所有權
    _Ty* _Ptr;
public:
    my_auto_ptr(_Ty* p = NULL):_Owns(p != NULL),_Ptr(p){}
    ~my_auto_ptr()
    {
        if(_Owns)
        {
            delete _Ptr;
        }
        _Owns = false;
        _Ptr = NULL;
    }
    _Ty* get() const 
    {
        return _Ptr;
    }
    _Ty* operator->()const
    {
        return get();
    }
    _Ty & operator*()
    {
        return *get();
    }
    void reset(_Ty* p = NULL) 
    {
       if(_Owns)
       {
           delete _Ptr;
       }
       _Ptr = p;
    }
    _Ty * release()const//編譯要通過,要麼異變,要麼強轉成普通指標
    {
        _Ty* tmp = NULL;
        if(_Owns)
        {
            ((my_auto_ptr*)this)->_Owns = false;
            tmp = _Ptr;
            ((my_auto_ptr*)this)->_Ptr = NULL;
        }
        return tmp;
    }
    my_auto_ptr(const my_auto_ptr & op):_Owns(op._Owns)
    {
        if(_Owns)
        {
            _Ptr = op._Ptr;
        }
    }
};

void fun()
{
    my_auto_ptr<Object> pobj(new Object(10));//pobj是my_auto_ptr型別
    cout<<pobj->Value()<<endl;
    cout<<(*pobj).Value()<<endl;//(*pobj)是Object的堆區物件。*(pobj._Ptr).Value()
}
int main()
{
    my_auto_ptr<Object> pobja(new Object(10));
    my_auto_ptr<Object> pobjb(pobja);
}

相關函數解釋:

此時程式必然會導致程式崩潰引發異常,主函數結束時對同一部分資源釋放了兩次,堆記憶體被釋放兩次

那麼我們可能會考慮,將資源轉移,即修改拷貝構造如下:利用是釋放函數

my_auto_ptr(const my_auto_ptr & op):_Owns(op._Owns),_Ptr(op.release())
    {}

看似好像解決了上面的問題,實則存在隱患

繼續來看:下面的程式碼存在什麼問題呢?

void fun(my_auto_ptr<Object> apx)
{
    int x = apx->Value();
    cout<<x<<endl;
}

int main()
{
    my_auto_ptr<Object> pobja(new Object(10));
    
    fun(pobja);

    int a = pobja->Value();
    cout<<a<<endl;
}

上述程式碼的執行邏輯如下:

  • pobja有兩個域擁有權域和指標域,拿pobja初始化形參apx時,會調動拷貝建構函式
  • apx將自己的擁有權域設為1,調動release函數,銷燬了pobja物件的資源後,返回堆區物件的地址,apx接收後將自身的指標域指向原先pobja所指向的堆區物件
  • fun函數結束,apx區域性物件就會被解構,此時再列印a,物件其實已經不存在了並且自身早已失去了pobja的擁有權。

綜上,此時智慧指標的拷貝建構函式的兩種寫法:

 my_auto_ptr(const my_auto_ptr & op):_Owns(op._Owns)
    {
        if(_Owns)
        {
            _Ptr = op._Ptr;
        }
    }
   
 my_auto_ptr(const my_auto_ptr & op):_Owns(op._Owns),_Ptr(op.release())
    {}
  • 第一種存在的問題:Object的資源會被兩個釋放兩次
  • 第二種存在的問題:解決了第一種問題,但是不能解決類似於實參物件初始化形參時,實參之前自身的資源丟失的問題,找不著了,因為這種情況太過於隱蔽,容易出錯,所以auto_ptr作為函數引數傳遞時一定要避免的。或許你想到加上參照解決上面的問題,但是仔細思考後發現,我們並不知道函數對傳入的傳入的auto_ptr做了什麼,如果當中的某些操作使其失去了對物件的所有權,那麼這還可能會導致致命的執行期錯誤。獲取再加上const 才是個不錯的選擇。

因此,C11標準之前的auto_ptr這個智慧指標不被廣泛使用的原因就是:在某些應用場景下,拷貝建構函式的意義不明確,同理賦值語句也是這個道理,意義同樣不明確,因為C11標準之前並不存在移動賦值和移動構造的概念,還有就是之前談到的一個物件和一組物件的問題,對於自定義型別而言,auto_ptr的解構函式僅能夠解構一個物件,不能夠處理一組物件的情況,這些都是尚未解決的問題。

於是在C11中棄用,C17標準中直接移除。 

歷史淵源:

在STL庫之前,有一個功能更加強大的boost庫,STL為了與其抗衡,應急製造了STL,但製作的不夠完善,由此因為STL未解決auto_ptr的問題,因此STl內的容器vector和list都不想和auto_ptr建立聯絡。

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com