按值傳遞實參到函數和函數返回臨時變量的副本,函數的效率對執行性能來說至關重要
如果避免這樣的復制操作,則執行時間可能會大大縮短。
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引用形參的標准版本。
編譯器會提供它們的默認版本,逐一成員的進行復制。