C++深度探索運算符重載和返回值優(yōu)化
今天遇到的是內(nèi)存釋放錯誤的問題。原因是沒寫拷貝構(gòu)造函數(shù),奇怪的是我之前也沒寫確實能正常工作的,今天深究了一下發(fā)現(xiàn)是編譯器做了返回值優(yōu)化。
問題背景
編譯環(huán)境還是針對C6455 DSP,為了做一些簡單的圖像直方圖的處理,并且盡可能不用模板類,我自己寫了一個簡單的類用來存放帶長度信息的數(shù)組,并且可以做一些簡單的運算。重載了減法運算符,從而可以對兩個直方圖求差。
具體問題
當(dāng)類中有那種需要動態(tài)分配空間的成員的時候,要記得提醒自己重載拷貝構(gòu)造函數(shù)和賦值運算符,這兩者缺一不可。為了舉個例子,我定義了下面這個CData類。
class CData { private: int *m_pData; unsigned int m_nLen; public: CData(); CData(unsigned int nLen); /* There is no copy constructor at the very begining */ CData(const CData &data); ~CData(); int &at(unsigned int i); int &at(unsigned int i) const; CData operator-(const CData &data); CData &operator=(const CData &data); CData &operator-=(const CData &data); };
在CData中,有兩個成員,分別是一個int類型的指針和一個unsigned int類型的變量代表數(shù)據(jù)的個數(shù)。
我重載了一個減法運算符,為的是將兩個CData類型的數(shù)據(jù)長度相同的對象中的每個對應(yīng)位置的int類型數(shù)據(jù)直接相減。而在調(diào)用減法運算的時候勢必會用到拷貝構(gòu)造函數(shù)和賦值運算符,因此也需要對它們重載。
順帶一提的是,在上面我還聲明了一個“-=”運算符的重載,為的是突出“-=”在一定場合會更加高效,但因為我在實際應(yīng)用中不能直接覆蓋“被減數(shù)”中的數(shù)據(jù),所以不能用“-=”運算符,只能通過減法運算符來實現(xiàn)。
CData CData::operator-(const CData &data) { cout << "entering subtract" << endl; assert(m_nLen == data.m_nLen); CData ret(data.m_nLen); for (int i = 0; i < m_nLen; i++){ ret.at(i) = at(i) - data.at(i); } return ret; }
減法運算重載,返回值是一個類的對象。函數(shù)返回后,這個類的對象就已經(jīng)被析構(gòu)了,所以在它被析構(gòu)之前一般會先調(diào)用一個拷貝構(gòu)造函數(shù),將這個類的對象拷貝給一個新的臨時的對象。默認拷貝構(gòu)造函數(shù)采用淺拷貝,這在遇到類中包含指針類型的成員時很可能造成對同一塊內(nèi)存區(qū)域的重復(fù)釋放。所以我們需要重載拷貝構(gòu)造函數(shù),為指針成員申請新的內(nèi)存空間。
CData CData::&operator=(const CData &data) { cout << "entering assign" << endl; assert(m_nLen == data.m_nLen); for (int i = 0; i < m_nLen; i++) at(i) = data.at(i); return *this; }
減法運算返回的臨時對象如果不給新的對象接收,它就沒了。所以我們還需要重載賦值運算符,用來接收這個臨時對象中的數(shù)據(jù),這一臨時對象被接收后,也會被析構(gòu)。分析整個過程可以看到,從減法運算返回的類對象,其中的數(shù)據(jù)先要被復(fù)制到一個臨時對象里,再要從臨時對象復(fù)制到真正的賦值目標(biāo)對象里,浪費了很多時間。
有的編譯器就會對這樣的情況進行優(yōu)化,省略對拷貝構(gòu)造函數(shù)的調(diào)用,直接賦值。
我今天遇到的問題是因為,我之前不知道在這個過程中會調(diào)用拷貝構(gòu)造函數(shù),所以沒有重寫拷貝構(gòu)造函數(shù)。意外的是,之前我這么做也是能用的,但我現(xiàn)在覺得不管怎樣還是得把拷貝構(gòu)造函數(shù)寫上,不能偷懶。
測試代碼
為了便于驗證這一過程,我寫了一部分測試代碼,可以用g++編譯。
#include <assert.h> #include <iostream> using namespace std; class CData { private: int *m_pData; unsigned int m_nLen; public: CData(): m_nLen(0), m_pData(nullptr) { cout << "construct without arg" << endl; } CData(unsigned int nLen): m_nLen(nLen) { cout << "construct with arg" << endl; m_pData = new int[nLen]; assert(m_pData != NULL); } CData(const CData &data) { cout << "copy construct" << endl; m_nLen = data.m_nLen; m_pData = new int[data.m_nLen]; assert(m_pData != nullptr); for (int i = 0; i < m_nLen; i++){ at(i) = data.at(i); } } ~CData() { cout << "destruct" << endl; if(m_pData){ delete[] m_pData; m_pData = nullptr; } } int &at(unsigned int i) { assert(i < m_nLen); return *(m_pData + i); } int &at(unsigned int i) const { assert(i < m_nLen); return *(m_pData + i); } CData operator-(const CData &data) { cout << "entering subtract" << endl; assert(m_nLen == data.m_nLen); CData ret(data.m_nLen); for (int i = 0; i < m_nLen; i++){ ret.at(i) = at(i) - data.at(i); } return ret; } CData &operator=(const CData &data) { cout << "entering assign" << endl; assert(m_nLen == data.m_nLen); for (int i = 0; i < m_nLen; i++) at(i) = data.at(i); return *this; } CData &operator-=(const CData &data) { cout << "entering subtract and assign" << endl; assert(m_nLen == data.m_nLen); for (int i = 0; i < m_nLen; i++) at(i) -= data.at(i); return *this; } }; int main() { cout << "---------- start construct ----------" << endl; CData dataA(10); CData dataB(10); CData dataC(10); cout << "---------- start subtract ----------" << endl; dataC = dataA - dataB; cout << "---------- start subtract and assign ----------" << endl; dataA -= dataB; cout << "---------- end of program ----------" << endl; return 0; }
g++編譯的程序,返回類對象的時候,默認是會省略拷貝構(gòu)造函數(shù)的,加上“-fno-elide-constructors”的編譯選項后可以讓它不省略。
上圖是在加上了“-fno-elide-constructors”編譯選項后的運行結(jié)果,可以看到執(zhí)行了兩次析構(gòu),對應(yīng)減法重載函數(shù)內(nèi)的對象和拷貝構(gòu)造函數(shù)生成的臨時對象被析構(gòu)。
而沒有“-fno-elide-constructors”選項的時候,就只會有一次析構(gòu),因為沒有調(diào)用拷貝構(gòu)造函數(shù)。
如果加了這個選項,但是沒有重寫拷貝構(gòu)造函數(shù),就會在第二次析構(gòu)的時候產(chǎn)生重復(fù)釋放內(nèi)存的問題。
到此這篇關(guān)于C++深度探索運算符重載和返回值優(yōu)化的文章就介紹到這了,更多相關(guān)C++運算符重載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用C# 判斷給定大數(shù)是否為質(zhì)數(shù)的詳解
本篇文章是對使用C#判斷給定大數(shù)是否為質(zhì)數(shù)的方法進行了詳細的分析介紹,需要的朋友參考下2013-05-05