來龍去脈
在我項目里出牧,經(jīng)常會出現(xiàn)這樣一段代碼:
#define _C_S(x) String(x)
String str = _C_S("hello world");
這個代碼的運行機制實際上是這樣的:
String tmp("hello world");
String str = tmp;
tmp.~String();
構造函數(shù)生成臨時的tmp對象(申請內(nèi)存塊A存放”hello world”)穴肘,然后通過復制構造函數(shù),將tmp內(nèi)存里的內(nèi)容復制到str對象(str申請內(nèi)存塊B舔痕,接受從內(nèi)存塊A復制過來的字符串)评抚,然后tmp對象脫離作用域調用析構函數(shù)(第二行代碼結束,釋放內(nèi)存塊A)伯复。仔細分析下慨代,發(fā)現(xiàn)有冗余的內(nèi)存申請和釋放,這里實際上存在兩次內(nèi)存申請啸如,和一次內(nèi)存釋放鱼响,那是不是有辦法做到,只申請一次內(nèi)存就完成上述代碼组底。
答案是有的丈积,我們只需要把tmp對象的內(nèi)存“移動”到str中即可,這就是C++11的右值引用债鸡。(由于我們項目C++代碼的基礎容器都是自己維護的江滨,并沒有使用stl,因此會缺失很多新特性厌均,如C++11的右值引用)
左值右值的定義
首先說明右值引用之前唬滑,先解釋下C++里對于右值和左值的定義
當一個對象被用作右值的時候,用的是對象的值(內(nèi)容)棺弊;當對象被用作左值的時候晶密,用的是對象的身份(在內(nèi)存中的位置)模她。
概念有點抽象稻艰,舉幾個例子來看看
int a = 52; //a是左值
int b = a + c; //b是左值,a+c是左值
string c = string("hello") //c是左值侈净,string("hello")是右值
從上面代碼可以看出尊勿,其實左值和右值的根本區(qū)別在于能否獲取內(nèi)存地址僧凤,左值有自己的變量名和地址,而右值是函數(shù)返回的或者運算符計算得出的臨時對象元扔,出了作用域就會被析構躯保。
右值引用的應用
那么引入右值引用的目的是什么呢?很簡單澎语,合理規(guī)劃臨時對象的內(nèi)存使用途事。
如果沒有右值引用,像使用string這種有指針成員變量的臨時對象擅羞,去給左值做構造或者賦值時盯孙,就會存在多余的內(nèi)存申請和釋放,如果該指針指向的內(nèi)存塊很大祟滴,那么這種頻繁的臨時對象內(nèi)存的申請和釋放很容易導致內(nèi)存碎片和內(nèi)存尖峰振惰,進而影響性能。
下面代碼是以一個簡單的字符串String類為例垄懂,實現(xiàn)了String的復制構造函數(shù)和賦值運算符的右值引用版本骑晶,來說明右值引用的作用。
class String
{
public:
//構造函數(shù)
String();
String(const char* str);
//復制構造函數(shù)
String(const String& str);
//復制構造函數(shù)-右值引用
String(String&& str);
//賦值運算符函數(shù)
String& operator=(const String& str);
//賦值運算符函數(shù)-右值引用
String& operator=(String&& str);
//析構函數(shù)
virtual ~String();
//字符串反轉
String reverse();
void show(){cout << _pdata <<endl;};
private:
size_t _len;
char* _pdata;
};
String::String()
{
_len = 0;
_pdata = nullptr;
}
String::String(const char* str)
{
_len = strlen(str);
_pdata = new char[_len + 1];
if (_pdata != nullptr)
{
memcpy(_pdata, str, _len);
_pdata[_len] = '\0';
}
}
String::String(const String& str)
{
_len = str._len;
_pdata = new char[_len + 1];
if (_pdata != nullptr)
{
memcpy(_pdata, str._pdata, _len);
_pdata[_len] = '\0';
}
}
String::String(String&& str)
{
_len = str._len;
_pdata = str._pdata;
str._len = 0;
str._pdata = nullptr;
}
String& String::operator=(String&& str)
{
if (_pdata)
{
delete [] _pdata;
_pdata = nullptr;
_len = 0;
}
_pdata = str._pdata;
_len = str._len;
str._len = 0;
str._pdata = nullptr;
return *this;
}
String& String::operator=(const String& str)
{
if (&str != this)
{
if (_len > str._len)
{
memset(_pdata, 0, _len);
_len = 0;
memcpy(_pdata, str._pdata, str._len);
_len = str._len;
_pdata[_len] = '\0';
}
else if (_len == str._len)
{
memcpy(_pdata, str._pdata, str._len);
}
else
{
delete [] _pdata;
_pdata = nullptr;
_len = str._len;
_pdata = new char[_len + 1];
if (_pdata != nullptr)
{
memcpy(_pdata, str._pdata, _len);
_pdata[_len] = '\0';
}
}
}
return *this;
}
String::~String()
{
if (_pdata != nullptr)
{
delete [] _pdata;
_pdata = nullptr;
_len = 0;
}
}
String String::reverse()
{
String ret = _pdata;
int sidx = 0;
int eidx = (int)(ret._len - 1);
while (sidx <= eidx) {
char tmp = ret._pdata[sidx];
ret._pdata[sidx] = ret._pdata[eidx];
ret._pdata[eidx] = tmp;
sidx++;
eidx--;
}
return ret; //調用移動函數(shù)-右值引用
}
int main()
{
String str = String("098"); //調用移動函數(shù)-右值引用
String str2("110");
str2 = String("098"); //調用移動函數(shù)-右值引用
//str2Reverse的地址和reverse函數(shù)中ret變量的地址是一致的
String str2Reverse = str2.reverse(); //調用移動函數(shù)-右值引用
return 0;
}
注意到草慧,右值引用版本的復制構造和賦值運算符函數(shù)桶蛔,將臨時對象的內(nèi)存“移動”到了左值,從而避免了臨時對象的內(nèi)存浪費漫谷,提高了運行效率仔雷。
總結
將該特性移植到我們項目工程代碼后,內(nèi)存申請和調用頻次大幅減少舔示,虛存和cpu都有小幅下降碟婆,其實不僅僅是拷貝構造和賦值運算符存在臨時對象,所有其他用到這兩個函數(shù)的String成員函數(shù)都會涉及到該類問題惕稻,我們比如字符串截取函數(shù)Mid竖共,Left,Right等俺祠,都會返回臨時的String對象公给,使用右值引用后,臨時對象內(nèi)存申請釋放存在浪費的問題也就得到解決蜘渣。C++11中有很多好的特性淌铐,但是使用起來也會有點門檻,還是推薦在項目實踐的過程中蔫缸,慢慢學習和理解這些特性腿准。