C++11中移動構造函數案例代碼
1. 拷貝構造函數中的深拷貝問題
在 C++ 98/03 標準中,如果想用其它對象初始化一個同類的新對象,只能借助類中的拷貝構造函數??截悩嬙旌瘮档膶崿F原理很簡單,就是為新對象復制一份和其它對象一模一樣的數據。需要注意的是,當類中擁有指針類型的成員變量時,拷貝構造函數中需要以深拷貝(而非淺拷貝)的方式復制該指針成員。
舉個例子:
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
//拷貝構造函數
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}如上所示,我們?yōu)?demo 類自定義了一個拷貝構造函數。該函數在拷貝 d.num 指針成員時,必須采用深拷貝的方式,即拷貝該指針成員本身的同時,還要拷貝指針指向的內存資源。否則一旦多個對象中的指針成員指向同一塊堆空間,這些對象析構時就會對該空間釋放多次,這是不允許的。
可以看到,程序中定義了一個可返回 demo 對象的 get_demo() 函數,用于在 main() 主函數中初始化 a 對象,其整個初始化的流程包含以下幾個階段:
- 執(zhí)行 get_demo() 函數內部的 demo() 語句,即調用 demo 類的默認構造函數生成一個匿名對象;
- 執(zhí)行 return demo() 語句,會調用拷貝構造函數復制一份之前生成的匿名對象,并將其作為 get_demo() 函數的返回值(函數體執(zhí)行完畢之前,匿名對象會被析構銷毀);
- 執(zhí)行 a = get_demo() 語句,再調用一次拷貝構造函數,將之前拷貝得到的臨時對象復制給 a(此行代碼執(zhí)行完畢,get_demo() 函數返回的對象會被析構);
- 程序執(zhí)行結束前,會自行調用 demo 類的析構函數銷毀 a。
注意,目前多數編譯器都會對程序中發(fā)生的拷貝操作進行優(yōu)化,因此如果我們使用 VS 2017、codeblocks 等這些編譯器運行此程序時,看到的往往是優(yōu)化后的輸出結果:
construct!
class destruct!
而同樣的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors命令運行(其中 demo.cpp 是程序文件的名稱),就可以看到完整的輸出結果:
construct! <-- 執(zhí)行 demo()
copy construct! <-- 執(zhí)行 return demo()
class destruct! <-- 銷毀 demo() 產生的匿名對象
copy construct! <-- 執(zhí)行 a = get_demo()
class destruct! <-- 銷毀 get_demo() 返回的臨時對象
class destruct! <-- 銷毀 a
如上所示,利用拷貝構造函數實現對 a 對象的初始化,底層實際上進行了 2 次拷貝(而且是深拷貝)操作。當然,對于僅申請少量堆空間的臨時對象來說,深拷貝的執(zhí)行效率依舊可以接受,但如果臨時對象中的指針成員申請了大量的堆空間,那么 2 次深拷貝操作勢必會影響 a 對象初始化的執(zhí)行效率。
事實上,此問題一直存留在以 C++ 98/03 標準編寫的 C++ 程序中。由于臨時變量的產生、銷毀以及發(fā)生的拷貝操作本身就是很隱晦的(編譯器對這些過程做了專門的優(yōu)化),且并不會影響程序的正確性,因此很少進入程序員的視野。
那么當類中包含指針類型的成員變量,使用其它對象來初始化同類對象時,怎樣才能避免深拷貝導致的效率問題呢?C++11 標準引入了解決方案,該標準中引入了右值引用的語法,借助它可以實現移動語義。
2. C++移動構造函數(移動語義的具體實現)
所謂移動語義,指的就是以移動而非深拷貝的方式初始化含有指針成員的類對象。簡單的理解,移動語義指的就是將其他對象(通常是臨時對象)擁有的內存資源“移為已用”。
以前面程序中的 demo 類為例,該類的成員都包含一個整形的指針成員,其默認指向的是容納一個整形變量的堆空間。當使用 get_demo() 函數返回的臨時對象初始化 a 時,我們只需要將臨時對象的 num 指針直接淺拷貝給 a.num,然后修改該臨時對象中 num 指針的指向(通常另其指向 NULL),這樣就完成了 a.num 的初始化。
事實上,對于程序執(zhí)行過程中產生的臨時對象,往往只用于傳遞數據(沒有其它的用處),并且會很快會被銷毀。因此在使用臨時對象初始化新對象時,我們可以將其包含的指針成員指向的內存資源直接移給新對象所有,無需再新拷貝一份,這大大提高了初始化的執(zhí)行效率。
例如,下面程序對 demo 類進行了修改:
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
//添加移動構造函數
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
可以看到,在之前 demo 類的基礎上,我們又手動為其添加了一個構造函數。和其它構造函數不同,此構造函數使用右值引用形式的參數,又稱為移動構造函數。并且在此構造函數中,num 指針變量采用的是淺拷貝的復制方式,同時在函數內部重置了 d.num,有效避免了“同一塊對空間被釋放多次”情況的發(fā)生。
在 Linux 系統(tǒng)中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors命令執(zhí)行此程序,輸出結果為:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
通過執(zhí)行結果我們不難得知,當為 demo 類添加移動構造函數之后,使用臨時對象初始化 a 對象過程中產生的 2 次拷貝操作,都轉由移動構造函數完成。
我們知道,非 const 右值引用只能操作右值,程序執(zhí)行結果中產生的臨時對象(例如函數返回值、lambda 表達式等)既無名稱也無法獲取其存儲地址,所以屬于右值。當類中同時包含拷貝構造函數和移動構造函數時,如果使用臨時對象初始化當前類的對象,編譯器會優(yōu)先調用移動構造函數來完成此操作。只有當類中沒有合適的移動構造函數時,編譯器才會退而求其次,調用拷貝構造函數。
在實際開發(fā)中,通常在類中自定義移動構造函數的同時,會再為其自定義一個適當的拷貝構造函數,由此當用戶利用右值初始化類對象時,會調用移動構造函數;使用左值(非右值)初始化類對象時,會調用拷貝構造函數。
那么,如果使用左值初始化同類對象,但也想調用移動構造函數完成,有沒有辦法可以實現呢?
默認情況下,左值初始化同類對象只能通過拷貝構造函數完成,如果想調用移動構造函數,則必須使用右值進行初始化。C++11 標準中為了滿足用戶使用左值初始化同類對象時也通過移動構造函數完成的需求,新引入了 std::move() 函數,它可以將左值強制轉換成對應的右值,由此便可以使用移動構造函數。
到此這篇關于C++11中移動構造函數的文章就介紹到這了,更多相關C++11移動構造函數內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
利用C++開發(fā)一個protobuf動態(tài)解析工具
數據庫中存儲的protobuf序列化的內容,有時候查問題想直接解析查看內容。很多編碼在網上很容易找到編解碼工具,但protobuf沒有找到編解碼工具,可能這樣的需求比較少吧,那就自己用C++實現一個,感興趣的可以了解一下2023-01-01

