C++11中移動(dòng)構(gòu)造函數(shù)案例代碼
1. 拷貝構(gòu)造函數(shù)中的深拷貝問題
在 C++ 98/03 標(biāo)準(zhǔn)中,如果想用其它對象初始化一個(gè)同類的新對象,只能借助類中的拷貝構(gòu)造函數(shù)。拷貝構(gòu)造函數(shù)的實(shí)現(xiàn)原理很簡單,就是為新對象復(fù)制一份和其它對象一模一樣的數(shù)據(jù)。需要注意的是,當(dāng)類中擁有指針類型的成員變量時(shí),拷貝構(gòu)造函數(shù)中需要以深拷貝(而非淺拷貝)的方式復(fù)制該指針成員。
舉個(gè)例子:
#include <iostream> using namespace std; class demo{ public: demo():num(new int(0)){ cout<<"construct!"<<endl; } //拷貝構(gòu)造函數(shù) 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 類自定義了一個(gè)拷貝構(gòu)造函數(shù)。該函數(shù)在拷貝 d.num 指針成員時(shí),必須采用深拷貝的方式,即拷貝該指針成員本身的同時(shí),還要拷貝指針指向的內(nèi)存資源。否則一旦多個(gè)對象中的指針成員指向同一塊堆空間,這些對象析構(gòu)時(shí)就會(huì)對該空間釋放多次,這是不允許的。
可以看到,程序中定義了一個(gè)可返回 demo 對象的 get_demo() 函數(shù),用于在 main() 主函數(shù)中初始化 a 對象,其整個(gè)初始化的流程包含以下幾個(gè)階段:
- 執(zhí)行 get_demo() 函數(shù)內(nèi)部的 demo() 語句,即調(diào)用 demo 類的默認(rèn)構(gòu)造函數(shù)生成一個(gè)匿名對象;
- 執(zhí)行 return demo() 語句,會(huì)調(diào)用拷貝構(gòu)造函數(shù)復(fù)制一份之前生成的匿名對象,并將其作為 get_demo() 函數(shù)的返回值(函數(shù)體執(zhí)行完畢之前,匿名對象會(huì)被析構(gòu)銷毀);
- 執(zhí)行 a = get_demo() 語句,再調(diào)用一次拷貝構(gòu)造函數(shù),將之前拷貝得到的臨時(shí)對象復(fù)制給 a(此行代碼執(zhí)行完畢,get_demo() 函數(shù)返回的對象會(huì)被析構(gòu));
- 程序執(zhí)行結(jié)束前,會(huì)自行調(diào)用 demo 類的析構(gòu)函數(shù)銷毀 a。
注意,目前多數(shù)編譯器都會(huì)對程序中發(fā)生的拷貝操作進(jìn)行優(yōu)化,因此如果我們使用 VS 2017、codeblocks 等這些編譯器運(yùn)行此程序時(shí),看到的往往是優(yōu)化后的輸出結(jié)果:
construct!
class destruct!
而同樣的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors
命令運(yùn)行(其中 demo.cpp 是程序文件的名稱),就可以看到完整的輸出結(jié)果:
construct! <-- 執(zhí)行 demo()
copy construct! <-- 執(zhí)行 return demo()
class destruct! <-- 銷毀 demo() 產(chǎn)生的匿名對象
copy construct! <-- 執(zhí)行 a = get_demo()
class destruct! <-- 銷毀 get_demo() 返回的臨時(shí)對象
class destruct! <-- 銷毀 a
如上所示,利用拷貝構(gòu)造函數(shù)實(shí)現(xiàn)對 a 對象的初始化,底層實(shí)際上進(jìn)行了 2 次拷貝(而且是深拷貝)操作。當(dāng)然,對于僅申請少量堆空間的臨時(shí)對象來說,深拷貝的執(zhí)行效率依舊可以接受,但如果臨時(shí)對象中的指針成員申請了大量的堆空間,那么 2 次深拷貝操作勢必會(huì)影響 a 對象初始化的執(zhí)行效率。
事實(shí)上,此問題一直存留在以 C++ 98/03 標(biāo)準(zhǔn)編寫的 C++ 程序中。由于臨時(shí)變量的產(chǎn)生、銷毀以及發(fā)生的拷貝操作本身就是很隱晦的(編譯器對這些過程做了專門的優(yōu)化),且并不會(huì)影響程序的正確性,因此很少進(jìn)入程序員的視野。
那么當(dāng)類中包含指針類型的成員變量,使用其它對象來初始化同類對象時(shí),怎樣才能避免深拷貝導(dǎo)致的效率問題呢?C++11 標(biāo)準(zhǔn)引入了解決方案,該標(biāo)準(zhǔn)中引入了右值引用的語法,借助它可以實(shí)現(xiàn)移動(dòng)語義。
2. C++移動(dòng)構(gòu)造函數(shù)(移動(dòng)語義的具體實(shí)現(xiàn))
所謂移動(dòng)語義,指的就是以移動(dòng)而非深拷貝的方式初始化含有指針成員的類對象。簡單的理解,移動(dòng)語義指的就是將其他對象(通常是臨時(shí)對象)擁有的內(nèi)存資源“移為已用”。
以前面程序中的 demo 類為例,該類的成員都包含一個(gè)整形的指針成員,其默認(rèn)指向的是容納一個(gè)整形變量的堆空間。當(dāng)使用 get_demo() 函數(shù)返回的臨時(shí)對象初始化 a 時(shí),我們只需要將臨時(shí)對象的 num 指針直接淺拷貝給 a.num,然后修改該臨時(shí)對象中 num 指針的指向(通常另其指向 NULL),這樣就完成了 a.num 的初始化。
事實(shí)上,對于程序執(zhí)行過程中產(chǎn)生的臨時(shí)對象,往往只用于傳遞數(shù)據(jù)(沒有其它的用處),并且會(huì)很快會(huì)被銷毀。因此在使用臨時(shí)對象初始化新對象時(shí),我們可以將其包含的指針成員指向的內(nèi)存資源直接移給新對象所有,無需再新拷貝一份,這大大提高了初始化的執(zhí)行效率。
例如,下面程序?qū)?demo 類進(jìn)行了修改:
#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; } //添加移動(dòng)構(gòu)造函數(shù) 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 類的基礎(chǔ)上,我們又手動(dòng)為其添加了一個(gè)構(gòu)造函數(shù)。和其它構(gòu)造函數(shù)不同,此構(gòu)造函數(shù)使用右值引用形式的參數(shù),又稱為移動(dòng)構(gòu)造函數(shù)。并且在此構(gòu)造函數(shù)中,num 指針變量采用的是淺拷貝的復(fù)制方式,同時(shí)在函數(shù)內(nèi)部重置了 d.num,有效避免了“同一塊對空間被釋放多次”情況的發(fā)生。
在 Linux 系統(tǒng)中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors
命令執(zhí)行此程序,輸出結(jié)果為:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
通過執(zhí)行結(jié)果我們不難得知,當(dāng)為 demo 類添加移動(dòng)構(gòu)造函數(shù)之后,使用臨時(shí)對象初始化 a 對象過程中產(chǎn)生的 2 次拷貝操作,都轉(zhuǎn)由移動(dòng)構(gòu)造函數(shù)完成。
我們知道,非 const 右值引用只能操作右值,程序執(zhí)行結(jié)果中產(chǎn)生的臨時(shí)對象(例如函數(shù)返回值、lambda 表達(dá)式等)既無名稱也無法獲取其存儲(chǔ)地址,所以屬于右值。當(dāng)類中同時(shí)包含拷貝構(gòu)造函數(shù)和移動(dòng)構(gòu)造函數(shù)時(shí),如果使用臨時(shí)對象初始化當(dāng)前類的對象,編譯器會(huì)優(yōu)先調(diào)用移動(dòng)構(gòu)造函數(shù)來完成此操作。只有當(dāng)類中沒有合適的移動(dòng)構(gòu)造函數(shù)時(shí),編譯器才會(huì)退而求其次,調(diào)用拷貝構(gòu)造函數(shù)。
在實(shí)際開發(fā)中,通常在類中自定義移動(dòng)構(gòu)造函數(shù)的同時(shí),會(huì)再為其自定義一個(gè)適當(dāng)?shù)目截悩?gòu)造函數(shù),由此當(dāng)用戶利用右值初始化類對象時(shí),會(huì)調(diào)用移動(dòng)構(gòu)造函數(shù);使用左值(非右值)初始化類對象時(shí),會(huì)調(diào)用拷貝構(gòu)造函數(shù)。
那么,如果使用左值初始化同類對象,但也想調(diào)用移動(dòng)構(gòu)造函數(shù)完成,有沒有辦法可以實(shí)現(xiàn)呢?
默認(rèn)情況下,左值初始化同類對象只能通過拷貝構(gòu)造函數(shù)完成,如果想調(diào)用移動(dòng)構(gòu)造函數(shù),則必須使用右值進(jìn)行初始化。C++11 標(biāo)準(zhǔn)中為了滿足用戶使用左值初始化同類對象時(shí)也通過移動(dòng)構(gòu)造函數(shù)完成的需求,新引入了 std::move() 函數(shù),它可以將左值強(qiáng)制轉(zhuǎn)換成對應(yīng)的右值,由此便可以使用移動(dòng)構(gòu)造函數(shù)。
到此這篇關(guān)于C++11中移動(dòng)構(gòu)造函數(shù)的文章就介紹到這了,更多相關(guān)C++11移動(dòng)構(gòu)造函數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++編程中break語句和continue語句的學(xué)習(xí)教程
這篇文章主要介紹了C++編程中break語句和continue語句的學(xué)習(xí)教程,break和continue是C++循環(huán)控制中的基礎(chǔ)語句,需要的朋友可以參考下2016-01-01visual studio code 配置C++開發(fā)環(huán)境的教程詳解 (windows 開發(fā)環(huán)境)
這篇文章主要介紹了 windows 開發(fā)環(huán)境下visual studio code 配置C++開發(fā)環(huán)境的圖文教程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03C語言結(jié)構(gòu)體(struct)的詳細(xì)講解
C語言中,結(jié)構(gòu)體類型屬于一種構(gòu)造類型(其他的構(gòu)造類型還有:數(shù)組類型,聯(lián)合類型),下面這篇文章主要給大家介紹了關(guān)于C語言結(jié)構(gòu)體(struct)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03C++?LeetCode0547題解省份數(shù)量圖的連通分量
這篇文章主要為大家介紹了C++?LeetCode0547題解省份數(shù)量圖的連通分量示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12C++歸并法+快速排序?qū)崿F(xiàn)鏈表排序的方法
這篇文章主要介紹了C++歸并法+快速排序?qū)崿F(xiàn)鏈表排序的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04