C++拷貝構造、移動構造與返回值優化
拷貝建構函式
拷貝建構函式(又稱複製建構函式),是用來建立已存在物件的副本。對應的還有一個概念是拷貝賦值運算子,當需要顯示地宣告拷貝建構函式時,一般建議同時宣告拷貝賦值運算子,以使得程式碼的含義明確。
如果不宣告拷貝建構函式(或拷貝賦值運算子),編譯器將會生一個預設的拷貝建構函式(或拷貝賦值運算子)(當被呼叫時生成)。參閱 宣告建構函式的規則 。
看一段典型的程式碼:
class TextFile { public: TextFile() {}; TextFile(const TextFile& tf) { cout << "copy" << endl; }; TextFile& operator=(TextFile& tf) { cout << "operator=" << endl; return *this; }; }; int main() { vector<TextFile> text_files; text_files.push_back(TextFile()); TextFile text_file_a; TextFile text_file_b; text_file_b = text_file_a; }
輸出如下:
copy
operator=
operator=
TextFile
類顯示地定義了拷貝建構函式與拷貝賦值運算子,分別在 text_files.push_back(TextFile());
和 text_file_b = text_file_a;
兩處呼叫。
需要注意的是,如果寫成 TextFile text_file_b = text_file_a;
,那麼被呼叫的將會是拷貝建構函式(也就是輸出 copy
)。
移動建構函式
上文的程式中, text_files.push_back(TextFile());
該行構造了兩個一模一樣的 TextFile
物件,一次是在 TextFile
push_back
時將臨時物件又拷貝一份,而第二次構造明顯是可以避免的。
因此,C++11中引入了 移動構造
的概念,與臨時物件發生拷貝時不需要重新分配記憶體而是使用被拷貝物件的資源,即臨時物件的資源。移動建構函式配合右值引用完成從臨時物件資料轉移到新的物件中,避免了資料拷貝。
class TextFile { public: TextFile() {}; // 移動建構函式 TextFile(TextFile && tf) { cout << "move" << endl; } }; int main() { vector<TextFile> text_files; text_files.push_back(TextFile()); }
輸出:
<strong>move</strong>
可以看到,當再需要 push_back
時,呼叫的就是移動構造而不是拷貝構造函數了,其他情況同理,以此可以節省記憶體分配上的效能開支。
返回值優化
這一點是我在編寫樣例程式碼時遇到的問題,講到拷貝建構函式時,常舉的一個例子如下:
class TextFile {
public:
TextFile() {};
TextFile(const TextFile& tf) {
cout << "copy" << endl;
};
TextFile& operator=(TextFile& tf) {
cout << "operator=" << endl;
return *this;
};
};
TextFile get_tmp_text_file() {
TextFile tf;
return tf;
}
int main() {
TextFile tf = get_tmp_text_file();
}
TextFile {
public:
TextFile() {};
TextFile(const TextFile& tf) {
cout << "copy" << endl;
};
TextFile& operator=(TextFile& tf) {
cout << "operator=" << endl;
return *this;
};
};
TextFile get_tmp_text_file() {
TextFile tf;
return tf;
}
int main() {
TextFile tf = get_tmp_text_file();
}
這是很經典的例子,很多文章都會講 TextFile tf = get_tmp_text_file();
會呼叫兩次拷貝建構函式,一次是 get_tmp_text_file
函式返回時,將函式返回拷貝到臨時變數裡,第二次是在 main
函式內構造 TextFile
物件時,將臨時變數拷貝到 tf
物件上。所以,程式會輸出兩遍 copy
。
然而,如果你現在寫了這麼一個程式,真的跑一遍,它什麼都不會輸出。
不要懷疑自己,程式碼沒錯,教程也沒錯,這是因為GCC的 返回值優化 。
當一個函式返回一個物件例項,一個臨時物件將被建立並通過複製建構函式把目標物件複製給這個臨時物件。C++標準允許省略這些複製建構函式,即使這導致程式的不同行為,即使編譯器把兩個物件視作同一個具有副作用。
這是C++標準允許編譯器獨立實現的優化。被稱為返回值優化(RVO/NRVO)。
G++編譯時,會預設對程式碼進行返回值優化,優化後的等價程式碼如下:
TextFile get_tmp_text_file(TextFile * tf) {
// 直接在tf上構造
}
int main() {
TextFile tf;
get_tmp_text_file(&tf);
}
get_tmp_text_file(TextFile * tf) {
// 直接在tf上構造
}
int main() {
TextFile tf;
get_tmp_text_file(&tf);
}
因此就不需要再呼叫 TextFile
物件的拷貝建構函式。
要關閉這種優化,只要在編譯時加上 -fno-elide-constructors
強制G++總是使用拷貝建構函式。g++ 2DimensionArray.cpp -o run -fno-elide-constructors && ./run
2DimensionArray.cpp -o run -fno-elide-constructors && ./run
輸出:
copy
copy
copy