程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 對象復制問題 && lvalue-rvalue && 引用,對象復制

對象復制問題 && lvalue-rvalue && 引用,對象復制

編輯:C++入門知識

對象復制問題 && lvalue-rvalue && 引用,對象復制


按值傳遞實參到函數和函數返回臨時變量的副本,函數的效率對執行性能來說至關重要

如果避免這樣的復制操作,則執行時間可能會大大縮短。

class CMessage
{
private:
    char * m_pMessage;

public:
    void showIt()const
    {
        cout << m_pMessage << endl;
    }
    //構造函數
    CMessage(const char* text = "Default message")
    {
        cout << " 構造函數" << endl;

        size_t length{ strlen(text) + 1 };
        m_pMessage = new char[length + 1];
        strcpy_s(m_pMessage, length + 1, text);
    }
    //復制構造函數
    CMessage(const CMessage & aMess)
    {
        cout << "復制構造函數" << endl;
        size_t len{ strlen(aMess.m_pMessage) + 1 };
        this->m_pMessage = new char[len];
        strcpy_s(m_pMessage, len, aMess.m_pMessage);
    }
    //重載賦值運算符
    CMessage & operator=(const CMessage & aMess)
    {
        cout << "重載賦值運算符函數" << endl;
        if (this != &aMess)
        {
            delete[]m_pMessage;
            size_t length{ strlen(aMess.m_pMessage) + 1 };
            m_pMessage = new char[length];
            strcpy_s(this->m_pMessage, length, aMess.m_pMessage);
        }
        return *this;
    }

    CMessage operator+(const CMessage & aMess)
    {
        cout <<"重載加法運算符函數" << endl;
        size_t len{strlen(m_pMessage)+strlen(aMess.m_pMessage)+1};
        CMessage message;
        message.m_pMessage = new char[len];

        strcpy_s(message.m_pMessage,len,m_pMessage);
        strcat_s(message.m_pMessage,len,aMess.m_pMessage);

        return message;
    }

    //析構函數
    ~CMessage()
    {
        cout << " 析構函數" << endl;
        delete[]m_pMessage;
    }
};
int main()
{
    CMessage motto1{ "Amiss is " };
    CMessage motto2{"as good as a mile"};
    CMessage motto3;

    motto3 = motto1 + motto2;

    motto3.showIt();
}

運行結果如下:

 構造函數            //motto1調用
 構造函數            //motto2調用
 構造函數            //motto3調用
重載加法運算符函數        
 構造函數            //operator+()中message對象調用
復制構造函數           //返回時對message對象的復制,生成message的臨時副本
 析構函數            //message調用,銷毀臨時對象
重載賦值運算符函數        //motto3調用operator=()
 析構函數              //message的臨時副本調用
Amiss is as good as a mile
 析構函數
 析構函數
 析構函數

----------------------------------------------------------------------------------------------------------------------------------------------------------------

改進方法:應用rvalue引用形參

當源對象是一個臨時對象,在復制操之後立即就被銷毀時,復制的替代方案是偷用由 m_pMessage 成員指向的臨時對象的內存,並傳送到目標對象。

如果這麼做,那麼不需要為目標對象分配更多的內存,不需要復制對象,也不需要釋放源對象擁有的內存。

在操作完成以後將立即銷毀源對象,因此這麼做沒有風險,只是加快了執行速度。

實現此技術的關鍵是檢測復制操作中何時是一個 rvalue。

  CMessage(const CMessage & aMess)
  {
      cout << "復制構造函數" << endl;
      size_t len{ strlen(aMess.m_pMessage) + 1 };
      this->m_pMessage = new char[len];
      strcpy_s(m_pMessage, len, aMess.m_pMessage);
   }

  CMessage(CMessage && aMess)
   {
      cout << "" << endl;
      m_pMessage = aMess.m_pMessage;
      aMess.m_pMessage = nullptr;      //必須要這麼做,防止刪除原指向的內存
    }

我們知道用對象初始化當前對象、返回臨時對象都會調用復制構造函數。

motto3 = motto1 + motto2;調用重載加法運算符函數後會產生臨時變量 message。

而對臨時變量的復制產生需要的臨時副本會增加運行時間。

所以,臨時變量的副本可以直接“偷用”源臨時變量對象成員指向的內存。通過以上兩個函數比較可知,lvalue引用形參多了復制的操作。

難道要 lvalue引用形參的形式沒有用了嗎?

若: motto3=motto1 時,可知必須用 lvalue 引用形參的形式。如果 rvalue 可用,將會使兩個對象同時指向一塊內存。

所以, rvalue 針對 臨時變量。

 

可以像下面這樣額外創建 operator=()函數的重載

  CMessage & operator=(CMessage && aMess)
   {
      cout <<"Move assignment operator function called." << endl;
      delete[]m_pMessage;
      m_pMessage = aMess.m_pMessage;
      aMess.m_pMessage = nullptr;       //必須要這麼做,防止刪除原指向的內存
      return *this;
   }

  CMessage & operator=(const CMessage & aMess)
  {
     cout << "重載賦值運算符函數" << endl;
     if (this != &aMess)
     {   delete[]m_pMessage;
        size_t length{ strlen(aMess.m_pMessage) + 1 };
          m_pMessage = new char[length];
        strcpy_s(this->m_pMessage, length, aMess.m_pMessage);
     }
     return *this;
  }

觀察這兩個 lvalue、rvalue引用形參重載賦值運算符函數的區別:

motto3 = motto1 + motto2;調用重載加法運算符函數後會產生臨時變量 message。在函數返回時又調用 rvalue引用形參類型的復制構造函數,

產生臨時對象 message 的臨時副本。

之後調用重載賦值運算符函數,由於此時仍是臨時對象的副本,

所以,仍可以采用“ 偷換 ”源臨時變量對象成員指向的內存。而避免賦值函數對對象成員的復制。

臨時對象由編譯器生成,使用之後會自動調用析構函數釋放。

所以此處需要我們通過觀察代碼運行,自己來理解。

 

  CMessage operator+(const CMessage & aMess)
   {
    cout <<"重載加法運算符函數" << endl;
    size_t len{strlen(m_pMessage)+strlen(aMess.m_pMessage)+1};
    CMessage message;
    message.m_pMessage = new char[len];

    strcpy_s(message.m_pMessage,len,m_pMessage);
    strcat_s(message.m_pMessage,len,aMess.m_pMessage);

    return message;
   }

不知道你有沒想過喲,為什麼上面函數沒有返回引用,引用可以避免不必要的復制,不是很方便嗎?

添加引用 CMessage operator+(const CMessage & aMess)

運行結果:

   構造函數
   構造函數
   構造函數
  重載加法運算符函數
   構造函數
   析構函數
  重載賦值運算符函數
請按任意鍵繼續. . .

發現程序崩潰,運行到重載賦值運算符函數就不能繼續運行了。

Why?

如果被返回的對象是被調用函數中的局部變量,則不應按引用方式返回它。

因為,在被調用函數執行完畢時,局部對象將調用其 析構函數。

 

如果函數返回一個沒有公有復制構造函數的類(如 ostream 類)的對象,它必須返回指向對象的引用。

如果在類中定義了 operator=()成員函數和復制構造函數時,將形參定義為非常量 rvalue 引用,則需要確保也定義了具有 const lvalue引用形參的標准版本。

編譯器會提供它們的默認版本,逐一成員的進行復制。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved