C++語義copy and swap示例詳解
class對象的初始化
我們有一個class Data
, 里面有一個int m_d
變量,存儲一個整數(shù)。
class Data { int m_i; public: void print() { std::cout << m_i << std::endl; } };
我們?nèi)绻枰粋€Data類的對象的話,可以這樣寫:
void test() { Data d; d.print(); // 打印內(nèi)部的變量 m_i }
看到這里,應(yīng)該能發(fā)現(xiàn)問題,雖然 d 變量已經(jīng)實例化了,但是,我們好像沒有在初始化的時候指定內(nèi)部m_i
到底是什么值。
有沒有一種可能性,我們并沒有將 d 所引用的內(nèi)存變成一個可以使用的狀態(tài)。
比如說,這里提一個業(yè)務(wù)需求,內(nèi)部的m_i
只能是奇數(shù)。
而上述代碼中的變量d所引用的內(nèi)存中的m_i
到底是什么數(shù),是未知的,有可能你的編譯器將m_i的初始值設(shè)置成了0,但這是于事無補的,因為我們的業(yè)務(wù)需求是:
- m_i 必須是奇數(shù)
所有用到d的地方,都會有這個假設(shè),所以如果在初始化d的時候,沒有保證這個m_i是奇數(shù)的話,那么后續(xù)的所有業(yè)務(wù)邏輯全部都會崩潰。
說了這么多,實際上就是想道明一句話:
- 想要使用一個類對象,先進行初始化,這個對象的內(nèi)存變成一個
合法的狀態(tài)
。
合法的狀態(tài)
大部分跟業(yè)務(wù)邏輯相關(guān),比如上面的m_i必須是奇數(shù)
。
constructor 構(gòu)造器
對象在實例化的時候,大抵有這么兩步:
- 分配內(nèi)存:這里分棧和堆,又叫自動分配內(nèi)存(函數(shù)棧自動張開)和手動(使用new操作符在堆上申請)
- 填充內(nèi)存
分配好的內(nèi)存,幾乎都是混沌的,完全不知道里面存的數(shù)據(jù)是什么,所以需要第二步填充內(nèi)存
,使得這塊內(nèi)存變成合法的
。
而 constructor 的最大職責(zé)就是這個。(打開文件,打開數(shù)據(jù)庫,或者網(wǎng)絡(luò)連接也能在這里面干)
這意思就是,constructor 執(zhí)行的時機一定是在內(nèi)存已經(jīng)準備好了的時候。
拿上面的例子,我們這樣來確保一個合法的m_i:
class Data { int m_i; public: Data(int i): m_i{i} // 變量m_i初始化 {} }; void test() { Data d{3};// 這里確保了變量 m_i 為 3 }
也許不想在初始化的非要想一個合法值傳給m_i,我們可以搞一個默認constructor:
class Data { int m_i; public: Data():m_i{1} {} }; void test() { Data d{}; // 這里不用填參數(shù) }
constructor overload 構(gòu)造器重載
constructor的形式有很多,但是它本質(zhì)上就是一個函數(shù),在初始化的時候會調(diào)用而已。
只要是函數(shù),那么就可以按照一般的函數(shù)的重載規(guī)則進行重載。
上面的例子已經(jīng)說明了這個用法
Data() : m_i{1} // 不帶參數(shù) Data(int i) : m_i{i} // 帶了一個int參數(shù) i
所以一個類該有什么樣的constructor,由業(yè)務(wù)邏輯自己決定。
copy constructor 拷貝構(gòu)造器
還是上面的Data的例子:
void test { Data d1{5}; 調(diào)用 Data(int i) 進行初始化 Data d2{d1}; // 這個是啥????? }
從寫法上來看,我們可以猜測到,d2.m_i 應(yīng)該拷貝自 d1.m_i, 所以最后的結(jié)果是 5。
這沒問題的,但是我們前面說了,初始化一定是調(diào)用了某個constructor,那么這里是調(diào)用的哪個constructor呢?
答案是:
Data(const Data& other);
形如這樣的參數(shù)是這樣的constructor,還特意起了個名字:copy constructor, 也就是拷貝構(gòu)造器
。
這個函數(shù)接受一個參數(shù),我們起了個名叫other
,所以一看就明白了,這個other
就是我們想要拷貝的對象。
這個constructor,我們并沒有手動提供,所以這是編譯器自動給我們加上去的。
你可能會問,編譯器怎么知道這個函數(shù)內(nèi)部應(yīng)該怎樣實現(xiàn)?
對啊,編譯器不知道,他對我們的業(yè)務(wù)邏輯以及合法性
一無所知,所以,編譯器只能提供一個比較基礎(chǔ)的功能:
- 逐個成員變量拷貝
Data類里只有一個m_i, 所以這里編譯器提供的這個constructor,就是做了大概這樣的事情:
class Data { int m_i; public: Data(const Data& other):m_i{other.m_i} {} };
像m_i這種基礎(chǔ)類型,就是直接拷貝了。那如果Data類內(nèi)部有class類型的變量呢:
class Foo { int m_i; }; class Data { Foo m_f; };
從形式上看,編譯器給我們提供的默認的拷貝構(gòu)造器,應(yīng)該是這樣的:
class Data { Foo m_f; public: Data(const Data& other):m_f{other.m_f} {} };
雖然m_f不是基本類型的變量,但是形式上來看,和基本變量是一致的。
有必要提一下:
m_f{other.m_f}
這句,實際上繼續(xù)調(diào)用了Foo類的拷貝構(gòu)造
,所以到這里,那就是Foo類
的事情了,與Data類
無關(guān)了。
總之:
- 拷貝構(gòu)造器,就是一個普通的構(gòu)造器,接收一個參數(shù)
const T &
- 拷貝構(gòu)造器,可以讓我們新產(chǎn)生的對象去拷貝一個已有的老對象,進行初始化
- 如果我們不提供一個拷貝構(gòu)造器,那么編譯器會給我們搞一個默認的,逐個成員拷貝的,拷貝構(gòu)造器
拷貝構(gòu)造器的調(diào)用時機
上面已經(jīng)說過一種:
Data d1{}; Data d2{d1} // 這里會調(diào)用拷貝構(gòu)造器
事實上,還有別的時候,拷貝構(gòu)造器會被調(diào)用,那就是函數(shù)的傳參,和返回值。
class Data{}; // 內(nèi)部省略 void foo(Data d) { // 一些邏輯 } void test() { Data d1{}; foo(d1); // 這一句調(diào)用了拷貝構(gòu)造器 }
函數(shù)傳參的時候,如果是值類型參數(shù),那么會調(diào)用拷貝構(gòu)造器。
再來看看函數(shù)返回值:
class Data{}; // 內(nèi)部省略 Data getData() { Data d1{}; return d1; // 這里也是調(diào)用拷貝構(gòu)造器 } void test() { Data d{getData()}; // 這里依然調(diào)用了拷貝構(gòu)造器 }
從理論上來看,上面的 Data d{getData()}
這一句應(yīng)該調(diào)用兩次拷貝構(gòu)造
- 第一次是函數(shù)getData內(nèi)部的一個局部d1,拷貝給了一個臨時匿名變量
- 第二次是這個臨時匿名變量拷貝給了變量d
但是如果你在拷貝構(gòu)造器里加上打印,你會發(fā)現(xiàn),沒有任何東西會打印出來,也就是說,壓根就沒有調(diào)用到拷貝構(gòu)造器。
這不代表上面關(guān)于函數(shù)的說法是錯的,這只是編譯器的優(yōu)化而已,因為來來回回的拷貝,實在是沒有必要,所以在某些編譯器認為可以的情況下,編譯器就直接省了。這個不重要,就不具體往里面細說規(guī)則了。
自定義拷貝構(gòu)造器
大部分時候,編譯器生成的這個拷貝構(gòu)造器就滿足需求了。
但是,如果我們的class包含了動態(tài)資源,比如說一個堆上動態(tài)的int數(shù)組, 默認的拷貝構(gòu)造器就沒那么好用了:
class Data { int m_size; // 數(shù)組的元素個數(shù) int* m_ptr; // 指向數(shù)組首元素的指針 public: Data(int size):m_size{size} { if (size > 0) { m_ptr = new int[size]{}; } } ~Data() { delete[] m_ptr; } };
由于這個Data類,擁有一個動態(tài)的數(shù)組,所以我們提供了一個析構(gòu)函數(shù),省的這塊內(nèi)存不會被回收。
然后,我們沒有提供一個拷貝構(gòu)造器,所以編譯器就給我們添加了一個:
class Data { // 忽略別的代碼,現(xiàn)在只關(guān)注拷貝構(gòu)造器 Data(const Data& other):m_size{other.m_size}, m_ptr{other.m_ptr} {} }; void test() { Data d1{10}; // 第一句 Data d2{d1}; // 第二句 }
沒什么懸念,就是按照成員,逐個拷貝,注意,連指針也是直接拷貝。
所以上述test函數(shù)中,第二句執(zhí)行了之后,整個內(nèi)存應(yīng)該是這樣的:
這有問題嗎?
有很大的問題,考慮一下test函數(shù)執(zhí)行完畢前,是不是需要對這兩個變量 d1,d2d1, d2d1,d2 進行析構(gòu)。
你會發(fā)現(xiàn),兩次析構(gòu),delete 的資源是一份?。?!
一份資源,被delete兩次,這就是所謂double free
問題。
還有別的問題嗎?
有??紤]下面的代碼:
void foo(Data d) { // 一些邏輯 } void test() { Data d1{10}; foo(d1); // }
上面代碼里,foo執(zhí)行完之前,會析構(gòu)這個局部變量d!導(dǎo)致資源已經(jīng)被delete!
而外面d1和里面的d,指向的是同一份資源,也就是說,foo執(zhí)行完之后,d1.m_ptr 成為了一個懸掛指針
!
沒辦法了,只能靠自己定義拷貝構(gòu)造器,來解決上面的問題了:
class Data { int m_size; // 動態(tài)數(shù)組的元素個數(shù) int* m_ptr; // 指向數(shù)據(jù)的指針 public: Data(const Data& other){ if(other.m_ptr) { auto temp_ptr { new int[other.m_size]}; std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr); m_ptr = temp_ptr; m_size = other.m_size; } else { m_ptr = nullptr; } } };
上面的拷貝構(gòu)造器,才是真正的拷貝,這種拷貝一般稱之為深拷貝
。
進行深拷貝之后,新對象和老對象,各自都有一份資源,不會再有任何粘連了。
拷貝賦值,copy assignment
想要完成深拷貝
,到現(xiàn)在只進行了一半。
剩下的一般就是重載一個操作符,operator=
,這是用來解決如下形式的拷貝:
Data d1{10}; Data d2{2}; /// d2 = d1;
這里,兩個變量 d1,d2d1, d2d1,d2 都自己進行了初始化,在經(jīng)過一堆代碼邏輯之后,此時我們的需求是:
- 清除 d2 的數(shù)據(jù)
- 將 d1 完整的拷貝給 d2
兩個類對象之間用賦值操作符,其實是調(diào)用了一個成員函數(shù):operator=
。
對,這玩意雖然是操作符,但是操作符本質(zhì)上也還是函數(shù),這個函數(shù)的名字就是operator=
。
還是一樣的,如果我們不提供一個自定義的operator=
, 那么編譯器會給我們添加一個如下的:
class Data { int m_size; int* m_ptr; public: Data(int size):m_size{size} // 普通構(gòu)造器 { if (size > 0) { m_ptr = new int[size]{}; } } Data(const Data& other) // 拷貝構(gòu)造器 { if(other.m_ptr) { auto temp_ptr { new int[other.m_size]}; std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr); m_ptr = temp_ptr; m_size = other.m_size; } else { m_ptr = nullptr; } } ~Data() // 析構(gòu) { delete[] m_ptr; } ///////// 編譯器自動添加的 operator= Data& operator=(const Data& other) { m_size = other.m_size; m_ptr = other.m_ptr; return *this; } };
看這個編譯器自動添加的operator=
, 是顯而易見能發(fā)現(xiàn)問題的:
- 自身的m_ptr指向的內(nèi)存永遠無法回收了
自定義 operator=
還是得靠自己來編寫 operator=
。
前方警告,終于要點題了,copy and swap
即將出現(xiàn)。
先按照我們的思路來寫一個:
Data& operator=(const Data& other) { // 1. 首先清除本身的資源 delete[] m_ptr; // 2. 拷貝other的資源 m_size = other.m_size; if (other.m_ptr) { m_ptr = new int[m_size]; std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr); } return *this; }
如果按照上面的代碼,來看下面的test函數(shù),會發(fā)生什么問題:
void test() { Data d1{10}; d1 = d1; // 自己賦值給自己 }
我們在operator=
里面看見,上來直接把整個資源刪除了,GG!
我們要加一個判斷:
Data& operator=(const Data& other) { if (this == &other) // 加了一個判斷 { return *this; } // 1. 首先清除本身的資源 delete[] m_ptr; // 2. 拷貝other的資源 m_size = other.m_size; if (other.m_ptr) { m_ptr = new int[m_size]; // 這句有可能異常 std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr); } return *this; }
關(guān)于這里加不加判斷,很多大師級人物也認為不該加:
- 誰會寫出這種
d1 = d1;
這種代碼???加了判斷,徒增煩惱而已。
再來看上面注釋那個, new 在申請新的內(nèi)存的時候,可能會發(fā)生異常,此時出現(xiàn)了一個問題,在文章開頭提及的:
- 內(nèi)存合法性
m_size 已經(jīng)拷貝過來了
而真正的數(shù)據(jù)沒有拷貝過來,導(dǎo)致這兩個變量,不滿足我們的業(yè)務(wù)合法性。
所以再改改:
Data& operator=(const Data& other) { // 1. 首先清除本身的資源 delete[] m_ptr; m_ptr = nullptr; // 2. 拷貝other的資源 auto temp_size {other.m_size}; if (other.m_ptr) { m_ptr = new int[temp_size]; std::copy(other.m_ptr, other.m_ptr+temp_size, m_ptr); m_size = temp_size; } return *this; }
此時此刻,這個代碼已經(jīng)沒啥大問題了,除了一樣:
- 代碼重復(fù)了,我們發(fā)現(xiàn)在拷貝other的數(shù)據(jù)的時候,邏輯是和拷貝構(gòu)造器是一模一樣的
c++里有一個原則:DRY
: Do not Repeat Yourself。
別寫重復(fù)的代碼!
所以接著往下,copy-and-swap正式出場:
copy-and-swap 語義
- 首先copy就是指拷貝構(gòu)造器
我們先來講講swap是個啥。
就是說,我們需要寫一個函數(shù)swap,如下:
class Data { // 其余部分省略,將重點放在swap函數(shù) friend void swap(Data &left, Data& right) { std::swap(left.m_size, right.m_size); std::swap(left.m_ptr, right.m_ptr); } };
這個swap函數(shù)很簡單,就是交換兩個已有的Data對象的內(nèi)部數(shù)據(jù),僅此而已。
現(xiàn)在,
- copy有了
- swap有了
讓我們寫出最終極的operator=
:
Data& operator=(Data other) { swap(*this, other); return *this; }
是不是驚呆了,就這么兩句,就行了!
仔細領(lǐng)略一下這個寫法的高深之處:
- 函數(shù)傳參,用的值傳參,而非引用,所以此時會調(diào)用拷貝構(gòu)造器(copy)
- 函數(shù)內(nèi)部,交換了當(dāng)前對象,和局部臨時變量other的數(shù)據(jù)(swap)
你可能會問,沒有清除自身的資源啊???
注意,other 是一個局部臨時變量,這個函數(shù)結(jié)束之前,會進行析構(gòu),而析構(gòu)的時候,other身上已經(jīng)是被交換過的了,所以other被析構(gòu)的時候,就是自身資源清除的時候。
妙,妙,妙?。?/p>
用如此短的代碼實現(xiàn)了operator=
, 實在是妙~
以上就是C++語義copy and swap示例詳解的詳細內(nèi)容,更多關(guān)于C++語義copy and swap的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言之?dāng)?shù)組名與數(shù)組起始地址的關(guān)系解析
這篇文章主要介紹了C語言之?dāng)?shù)組名與數(shù)組起始地址的關(guān)系,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07