C++學習之移動語義與智能指針詳解

移動語義

1.幾個基本概念的理解

(1)可以取地址的是左值,不能取地址的就是右值,右值可能存在寄存器,也可能存在於棧上(短暫存在棧)上

(2)右值包括:臨時對象、匿名對象、字面值常量

(3)const 左值引用可以綁定到左值與右值上面,稱為萬能引用。正因如此,也就無法區分傳進來的參數是左值還是右值。

const int &ref = a;//const左值引用可以綁定到左值
const int &ref1 = 10;//const左值引用可以綁定到右值

(4)右值引用:隻能綁定到右值不能綁定到左值

2.移動構造函數

註意:

移動函數(移動構造函數和移動賦值運算符函數)優先於復制函數(拷貝構造函數和賦值運算符函數)的執行;具有移動語義的函數(移動構造函數和移動賦值運算符函數)優先於具有復制控制語義函數(拷貝構造函數和賦值運算符函數)的執行

String(String &&rhs)
: _pstr(rhs._pstr)
{
cout << "String(String &&)" << endl;
rhs._pstr = nullptr;
}

3.移動賦值函數

String &operator=(String &&rhs)
{
    cout << "String &operator=(String &&)" << endl;
    if(this != &rhs)//1、自移動
    {
        delete [] _pstr;//2、釋放左操作數
        _pstr = nullptr;
        _pstr = rhs._pstr;//3、淺拷貝
        rhs._pstr = nullptr;
    }
    
    return *this;//4、返回*this
}

4.std::move函數

std::move:

原理:將左值轉換為右值,在內部其實上是做瞭一個強制轉換,static_cast<T &&>(lvaule)。將左值轉換為右值後,左值就不能直接使用瞭,如果還想繼續使用,必須重新賦值。std::move()作用於內置類型沒有任何作用,內置類型本身是左值還是右值,經過std::move()後不會改變。

5.面試題,關於實現String

#include <iostream>
#include <string>

using std::string;
using std::cout;
using std::endl;

class String
{
public:
    //(當傳遞右值的時候)具有移動語義的函數優先於具有復制控制語義的函數
    //移動構造函數(隻針對右值)
    String(String &&rhs)
    : _pstr(rhs._pstr)
    {
        cout << "String(String &&)" << endl;
        rhs._pstr = nullptr;
    }

    //移動賦值運算符函數(傳入右值)
    String &operator=(String &&rhs)
    {
        cout << "String &operator=(String &&)" << endl;
        if(this!=&rhs){     //不能自復制
            delete [] _pstr;//釋放左操作數
            _pstr = nullptr;

            _pstr=rhs._pstr;//轉移右操作的資源
            rhs._pstr=nullptr;//釋放右值
        }
        return *this;
    }

private:
    char* _pstr;
};

資源管理和智能指針

一、C語言中的問題

C語言在對資源管理的時候,比如文件指針,由於分支較多,或者由於寫代碼的人與維護的人不一致,導致分支沒有寫的那麼完善,從而導致文件指針沒有釋放,所以可以使用C++的方式管理文件指針。。。

class SafeFile
{
public:
    //在構造的時候托管資源(fp)
    SafeFile(FILE *fp)
    : _fp(fp)
    {
        cout << "SafeFile(FILE *)" << endl;
        if(nullptr==fp)
        {
            cout << "nullptr == _fp " << endl;
        }
    }

    //提供若幹訪問資源的方法
    void write(const string &msg)
    {
        fwrite(msg.c_str(),1,msg.size(),_fp);  //調用c語言的函數往_fp輸入數據
    }

    //在銷毀(析構)時候釋放資源(fp)
    ~SafeFile()
    {
        cout << "~SafeFile()" << endl;
        if(_fp)
        {
            fclose(_fp);
            cout << "fclose(_fp)" << endl;
        }
    }
private:
    FILE *_fp;
};
void test()
{
    string s1 = "hello,world\n";
    SafeFile sf(fopen("text.txt","a+"));
    sf.write(s1);
}

二、C++的解決辦法(RAII技術)

1)概念:資源管理 RAII 技術,利用對象的生命周期管理程序資源(包括內存、文件句柄、鎖等)的技術,因為對象在離開作用域的時候,會自動調用析構函數

2)關鍵:要保證資源的釋放順序與獲取順序嚴格相反。。正好是析構函數與構造函數的作用

3)RAII常見特征

1、在構造時初始化資源,或者托管資源。
2、析構時釋放資源。
3、一般不允許復制或者賦值(值語義-對象語義)
4、提供若幹訪問資源的方法。

4)區分:值語義:可以進行復制與賦值。

5)對象語義:不能進行復制與賦值,一般使用兩種方法達到要求:

(1)、將拷貝構造函數和賦值運算符函數設置為私有的就 ok 。

(2)、將拷貝構造函數和賦值運算符函數使用=delete.

6)RAII技術代碼

template <typename T>
class RAII
{
public:
    //通過構造函數托管資源
    RAII(T *data)
    : _data(data)
    {
        std::cout<< "RAII(T *)" << std::endl;
    }

    //訪問資源的方法
    T *operator->()
    {
        return _data;
    }
    T &operator*()
    {
        return *_data;
    }
    T *get()const
    {
        return _data;
    }
    void reset(T *data)
    {
        if(_data)
        {
            delete _data;
            _data = nullptr;
        }
        _data = data;
    }

    //不能賦值和復制
    RAII(const RAII&rhs) = delete;
    RAII&operator=(const RAII&rhs)=delete;

    //通過析構函數釋放資源
    ~RAII()
    {
        cout << "~RAII()" << endl;
        if(_data)
        {
            delete _data;
            _data = nullptr;
        }
    }


private:
    T *_data;
};
void test3()
{
    //這裡沒給出Point類的實現方式
    RAII<Point> ppt(new Point(1,2));
    cout<<"ppt = ";
    ppt->print();
    cout<<endl;
}

三、四種智能指針

RAII的對象ppt就有智能指針的雛形。

1、auto_ptr.cc

最簡單的智能指針,使用上存在缺陷,所以被棄用。。。(C++17已經將其刪除瞭)

2、unique_ptr

比auto_ptr安全多瞭,明確表明是獨享所有權的智能指針,所以不能進行復制與賦值。

    unique_ptr<int> up(new int(10));
    cout<<"*up="<<*up<<endl;              //打印10
    cout<<"up.get() = "<<up.get()<<endl;  //獲取托管的指針的值,也就是10的地址

    cout << endl << endl;
    /* unique_ptr<int> up2(up);//error,獨享資源的所有權,不能進行復制 */

    unique_ptr<int> up4(std::move(up));  //通過移動語義轉移up的所有權
    cout<<"*up="<<*up4<<endl;
    cout<<"up.get() = "<<up4.get()<<endl;

    unique_ptr<Point> up5(new Point(3,4));//通過移動語義轉移up的所有權
    vector<unique_ptr<Point>> numbers;
    numbers.push_back(unique_ptr<Point>(new Point(1,2)));
    numbers.push_back(std::move(up5));

3、shared_ptr

    shared_ptr<int> sp(new int(10));
    cout << "*sp = " << *sp << endl;        //打印10
    cout << "sp.get() = " << sp.get() << endl;  //地址
    cout << "sp.use_count() = " << sp.use_count() << endl;  //引用次數為1

    cout<<endl<<endl;
    //提前結束棧對象
    {
        shared_ptr<int> sp2(sp);//共享所有權,使用淺拷貝
        cout << "*sp = " << *sp << endl;
        cout << "sp.get() = " << sp.get() << endl;
        cout << "sp.use_count() = " << sp.use_count() << endl;
        cout << "*sp2 = " << *sp2 << endl;
        cout << "sp2.get() = " << sp2.get() << endl;              //地址都一樣
        cout << "sp2.use_count() = " << sp2.use_count() << endl;  //引用次數加1,變為2瞭
    }

    cout << "sp.use_count() = " << sp.use_count() << endl;  //又變為1瞭

    cout << endl << endl;
    shared_ptr<Point> sp4(new Point(3.4));//通過移動語義轉移sp的所有權
    vector<shared_ptr<Point>> numbers;
    numbers.push_back(shared_ptr<Point> (new Point(1,2)));
    numbers.push_back(sp4);
    numbers[0]->print();
    numbers[1]->print();

3.1、循環引用

該智能指針在使用的時候,會使得引用計數增加,從而會出現循環引用的問題,兩個shared_ptr智能指針互指,導致引用計數增加,不能靠對象的銷毀使得引用計數變為0,從而導致內存泄漏。。

class Child;
class Parent
{
public:
  Parent()
 {
    cout << "Parent()" << endl;
 }
  ~Parent()
 {
    cout << "~Parent()" << endl;
 }
  shared_ptr<Child> pParent;
};
class Child
{
public:
  Child()
 {
    cout << "Child()" << endl;
 }
  ~Child()
 {
    cout << "~Child()" << endl;
 }
  shared_ptr<Parent> pChild;
};
void test()
{
  //循環引用可能導致內存泄漏
  shared_ptr<Parent> parentPtr(new Parent());
  shared_ptr<Child> childPtr(new Child());
  cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
  cout << "childPtr.use_count() = " << childPtr.use_count() << endl;
 
  cout << endl << endl;
  parentPtr->pParent = childPtr;//sp = sp
  childPtr->pChild = parentPtr;
  cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
  cout << "childPtr.use_count() = " << childPtr.use_count() << endl;
}

1.解決循環引用的辦法是使得其中一個改為weak_ptr,不會增加引用計數,這樣可以使用對象的銷毀而打破引用計數減為0的問題。。

2.修改: shared_ptr pChild;改為 weak_ptr pChild;即可解決循環引用的問題。。

parentPtr->pParent = childPtr;//sp = sp
childPtr->pChild = parentPtr;//wp = sp,weak_ptr不會導致引用計數加1

4、weak_ptr

與shared_ptr相比,稱為弱引用的智能指針,shared_ptr是強引用的智能指針。weak_ptr不會導致引用計數增加,但是它不能直接獲取資源,必須通過lock函數從wp提升為sp,從而判斷共享的資源是否已經銷毀

    weak_ptr<Point> wp
    {
        shared_ptr<Point> sp(new Point(1,2));
        wp = sp;
        cout << "wp.use_count = " << wp.use_count() << endl;
        cout << "sp.use_count = " << sp.use_count() << endl;

        cout<<"wp.expired = "<<wp.expired()<<endl;//此方法等同於use_count()==0?
        //等於0表示false,空間還存在
        //不等0表示true,空間已經不存在瞭
        //expired = use_count
        shared_ptr<Point> sp2 = wp.lock();//判斷共享的資源是否已經銷毀的方式就是從wp提升為sp
        if(sp2)
        {
            cout << "提升成功" << endl;
        }
        else
        {
            cout << "提升失敗" << endl;
        }
     }

四、為智能指針定制刪除器

1)很多時候我們都用new來申請空間,用delete來釋放。庫中實現的各種智能指針,默認也都是用delete來釋放空間,但是若我們采用malloc申請的空間或是用fopen打開的文件,這時我們的智能指針就無法來處理,因此我們需要為智能指針定制刪除器,提供一個可以自由選擇析構的接口,這樣,我們的智能指針就可以處理不同形式開辟的空間以及可以管理文件指針。

2)自定義智能指針的方式有兩種:

(1)函數指針

(2)仿函數(函數對象)

函數指針的形式:

template<class T>
void Free(T* p)
{
  if (p)
    free(p);
}
template<class T>
void Del(T* p)
{
  if (p)
    delete p;
}
void FClose(FILE* pf)
{
  if (pf)
    fclose(pf);
}
//定義函數指針的類型
typedef void(*DP)(void*);
template<class T>
class SharedPtr
{
public:
  SharedPtr(T* ptr = NULL ,DP dp=Del)
 :_ptr(ptr)
 , _pCount(NULL)
 , _dp(dp)
 {
    if (_ptr != NULL)
   {
      _pCount = new int(1);
   }
 }
private:
  void Release()
 {
    if (_ptr&&0==--GetRef())
   {
      //delete _ptr;
      _dp(_ptr); 
      delete _pCount;
   }
 }
  int& GetRef()
  {
    return *_pCount;
 }
private:
  T* _ptr;
  int* _pCount;
  DP _dp;
};

仿函數(函數對象)

關於刪除器的使用

五、智能指針的誤用

1、同一個裸指針被不同的智能指針托管,導致被析構兩次。

1.1、直接使用

1.2、間接使用

2、還是裸指針被智能指針托管形式,但是比較隱蔽。。。

class Point
: public std::enable_shared_from_this<Point>
{
public:
  Point(int ix = 0, int iy = 0)
 : _ix(ix)
 , _iy(iy)
 {
    cout << "Point(int = 0, int = 0)" << endl;
 }
  void print() const
 {
    cout << "(" <<_ix
       << ","  << _iy
       << ")" << endl;
 }
  /* Point *addPoint(Point *pt) */
  shared_ptr<Point> addPoint(Point *pt)
 {
    _ix += pt->_ix;
    _iy += pt->_iy;
    //this指針是一個裸指針
    /* return shared_ptr<Point>(this); */
    return shared_from_this();
 }
  ~Point()
 {
    cout << "~Point()" << endl;
 }
private:
  int _ix;
  int _iy;
};
void test3()
{
  shared_ptr<Point> sp1(new Point(1, 2));
  cout << "sp1 = ";
  sp1->print();
  cout << endl;
  shared_ptr<Point> sp2(new Point(3, 4));
  cout << "sp2 = ";
  sp2->print();
  cout << endl;
  shared_ptr<Point> sp3(sp1->addPoint(sp2.get()));
  cout << "sp3 = ";
  sp3->print();
}

總結

到此這篇關於C++學習之移動語義與智能指針的文章就介紹到這瞭,更多相關C++移動語義與智能指針內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: