8. 默認函數控制
在C++
中對於空類編譯器會生成一些默認的成員函數,比如:構造函數、拷貝構造函數、運算符重載、析構函數、&和const&的重載、移動構造、移動拷貝構造等函數。
如果在類中顯式定義了,編譯器將不會重新生成默認版本。有時候這樣的規則可能被忘記,最常見的是聲明瞭帶參數的構造函數,必要時則需要定義不帶參數的版本以實例化無參的對象。而且有時編譯器會生成,有時又不生成,容易造成混亂,於是C++11
讓程序員可以控制是否需要編譯器生成。
顯式缺省函數
在C++11
中,可以在默認函數定義或者聲明時加上=default
,從而顯式的指示編譯器生成該函數的默認版本,用=default
修飾的函數稱爲顯式缺省函數。
class A{
public:
A(int a)
: _a(a)
{}
// 顯式缺省構造函數,由編譯器生成
A() = default;
// 可以選擇在類中聲明,在類外定義時讓編譯器生成默認賦值運算符重載
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default; //類外定義
int main(){
A a1(10);
A a2;
a2 = a1;
return 0;
}
刪除默認函數
如果能想要限制某些默認函數的生成:
- 在
C++98
中,是該函數設置成private
,並且不完成實現,這樣只要其他人想要調用就會報錯。 - 在
C++11
中更簡單,只需在該函數聲明加上=delete
即可,該語法指示編譯器不生成對應函數的默認版本,稱=delete
修飾的函數爲刪除函數。
class A{
public:
A(int a)
: _a(a)
{}
// 禁止編譯器生成默認的拷貝構造函數以及賦值運算符重載
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main(){
A a1(10);
A a2(a1);
// 編譯失敗,因爲該類沒有拷貝構造函數
A a3(10);
a3 = a2;
// 編譯失敗,因爲該類沒有賦值運算符重載
return 0;
}
9. 右值引用【★】
移動語義
如果一個類中涉及到資源管理,用戶必須顯式提供拷貝構造、賦值運算符重載以及析構函數,否則編譯器將會自動生成一個默認的,如果遇到拷貝對象或者對象之間相互賦值,就會出錯,比如:
class String{
public:
String(char* str = ""){
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s){
if (this != &s){
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
~String(){
if (_str) delete[] _str;
}
private:
char* _str;
};
假設現在有一個函數,返回值爲一個String
類型的對象:
String GetString(char* pStr){
String strTemp(pStr);
return strTemp; //此時不是返回棧上對象strTemp,而是拷貝構造一個臨時對象返回
}
int main(){
String s2(GetString("world"));
/* 用GetString返回的臨時對象構造s2
s2構造完成後臨時對象將被銷燬,因爲臨時對象臨時對象不能直接返回
因此編譯器需要拷貝構造一份臨時對象,然後將strTemp銷燬
*/
return 0;
}
上述代碼看起來沒有什麼問題,但是有一個不太盡人意的地方:GetString
函數返回的臨時對象,將s2
拷貝構造成功之後,立馬被銷燬了(臨時對象的空間被釋放),再沒有其他作用;
而s2
在拷貝構造時,又需要分配空間,一個剛釋放一個又申請,有點多此一舉。
那能否將GetString
返回的臨時對象的空間直接交給s2
呢?這樣s2
也不需要重新開闢空間了,代碼的效率會明顯提高。
- 將一個對象中資源移動到另一個對象中的方式,稱之爲移動語義。
- 在
C++11
中如果需要實現移動語義,必須使用右值引用。
String(String&& s) //兩個 &
: _str(s._str)
{
s._str = nullptr;
}
C++11中的右值
右值引用,顧名思義就是對右值的引用。C++11
中,右值由兩個概念組成:純右值和將亡值。
- 純右值
純右值是C++98
中右值的概念,用於識別臨時變量和一些不跟對象關聯的值。
比如:常量、一些運算表達式(1+3)等。 - 將亡值
聲明週期將要結束的對象。比如:在值返回時的臨時對象。
右值引用
右值引用書寫格式:
類型&& 引用變量名字 = 實體;
右值引用最長常見的一個使用地方就是:與移動語義結合,減少無必要資源的開闢來提高代碼的運行效率。
改造一下剛纔的例子代碼演示:
String&& GetString(char* pStr){
String strTemp(pStr);
return strTemp;
}
int main(){
String s1("hello");
String s2(GetString("world"));
return 0;
}
右值引用另一個比較常見的地方是:給一個匿名對象取別名,延長匿名對象的聲明週期。
String GetString(char* pStr){
return String(pStr);
}
int main(){
String&& s = GetString("hello");
return 0;
}
【注】:
- 與引用一樣,右值引用在定義時必須初始化。
- 通常情況下,右值引用不能引用左值。
int main(){
int a = 10;
int&& ra; // 編譯失敗,沒有進行初始化
int&& ra = a; // 編譯失敗,a是一個左值
const int&& ra = 10; // ra是匿名常量10的別名
return 0;
}
std::move()
C++11
中,std::move()
函數位於<utility>
頭文件中,這個函數名字具有迷惑性,它並不搬移任何東西,唯一的功能就是將一個左值強制轉化爲右值引用,通過右值引用使用該值,實現移動語義。
注意:被轉化的左值,其生命週期並沒有隨着左右值的轉化而改變,即std::move
轉化的左值變量left_value
不會被銷燬。
- 下面舉一個
move()
誤用的例子:
// 移動構造函數
class String{
String(String&& s)
: _str(s._str){
s._str = nullptr;
}
};
int main(){
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}
move()
更多的是用在生命週期即將結束的對象上。
【注】:爲了保證移動語義的傳遞,程序員在編寫移動構造函數時,最好使std::move
轉移擁有資源的成員爲右值。
注意點
- 如果將移動構造函數聲明爲常右值引用或者返回右值的函數聲明爲常量,都會導致移動語義無法實現。
String(const String&&);
const Person GetTempPerson();
- 在
C++11
中,無參構造函數 / 拷貝構造函數 / 移動構造函數實際上有3
個版本:
Object();
Object(const T&);
Object(T &&);
C++11
中默認成員函數
默認情況下,編譯器會爲程序員隱式生成一個(如果沒有用到則不會生成)移動構造函數。如果程序員聲明瞭自定義的構造函數、移動構造、拷貝構造函數、賦值運算符重載、移動賦值、析構函數,編譯器都不會再爲程序員生成默認版本。編譯器生成的默認移動構造函數實際和默認的拷貝構造函數類似,都是按照位拷貝(即淺拷貝)來進行的。因此,在類中涉及到資源管理時,程序員最好自己定義移動構造函數。其他類有無移動構造都無關緊要。但在C++11
中,拷貝構造/移動構造/賦值/移動賦值函數必須同時提供,或者同時不提供,程序才能保證類同時具有拷貝和移動語義。
完美轉發
完美轉發是指:在函數模板中,完全依照模板的參數的類型,將參數傳遞給函數模板中調用的另外一個函數。
void Func(int x){
// ......
}
template<typename T>
void PerfectForward(T t){
Fun(t);
}
PerfectForward
爲轉發的模板函數,Func
爲實際目標函數,但是上述轉發還不算完美:
-
完美轉發是:目標函數總希望將參數按照<傳遞給轉發函數的實際類型>轉給目標函數,而不產生額外的開銷,就好像轉發者不存在一樣。
-
所謂完美:函數模板在向其他函數傳遞自身形參時,如果相應實參是左值,它就應該被轉發爲左值;如果相應實參是右值,它就應該被轉發爲右值。這樣做是爲了保留在其他函數針對轉發而來的參數的左右值屬性進行不同處理(比如參數爲左值時實施拷貝語義;參數爲右值時實施移動語義)。
C++11
通過forward
函數來實現完美轉發, 比如:
void Fun(int &x) { cout << "lvalue ref" << endl; }
void Fun(int &&x) { cout << "rvalue ref" << endl; }
void Fun(const int &x) { cout << "const lvalue ref" << endl; }
void Fun(const int &&x) { cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T &&t) { Fun(std::forward<T>(t)); }
int main(){
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
感謝您閱讀至此,感興趣的看官們可以移步上篇與下篇,繼續瞭解C++11
剩餘新特性~
【從零學C++11(上)】
列表初始化
、decltype
關鍵字、委派構造
等新特性
【https://blog.csdn.net/qq_42351880/article/details/100140163】
【從零學C++11(下)】
lambda
表達式、線程庫
、原子操作庫
等新特性
【https://blog.csdn.net/qq_42351880/article/details/100144882】