造函數(shù)為什么必須用const引用?一文拆透值傳遞與指針的致命缺陷)
1. 一個(gè)最容易被背錯(cuò)答案的問題這道題到底在考什么我在社區(qū)里見過不少 C 的初學(xué)者提問十有八九繞不開這個(gè)經(jīng)典考點(diǎn)拷貝構(gòu)造函數(shù)的形參為什么不能用值傳遞很多人的回答是“會(huì)用死循環(huán)導(dǎo)致棧溢出”也確實(shí)有人背過“必須用引用”這個(gè)結(jié)論但追問一句“為什么用指針也不行”往往就卡殼了。這其實(shí)是一個(gè)非常典型的 C 原理題背后牽扯的不只是語法規(guī)則而是編譯器如何理解對象構(gòu)造、如何匹配構(gòu)造函數(shù)以及一個(gè)類作為“值類型”在語言層面最基本的語義約定。搞懂這個(gè)問題一類“為什么標(biāo)準(zhǔn)庫這么設(shè)計(jì)”“為什么我的類不能放進(jìn)某些容器”的疑問會(huì)順帶打開。先把概念釘死拷貝構(gòu)造函數(shù)是一個(gè)類的一個(gè)重載構(gòu)造函數(shù)參數(shù)是const T作用是用一個(gè)同類型的既有對象去初始化另一個(gè)新對象。標(biāo)準(zhǔn)形式長這樣class MyClass { public: MyClass(); // 默認(rèn)構(gòu)造 MyClass(const MyClass rhs); // 拷貝構(gòu)造 };為什么規(guī)定必須是const T這個(gè)const和兩個(gè)修飾符哪個(gè)都不能缺。下面我們從值傳遞、指針傳遞和引用傳遞三條路徑逐一拆解看看編譯器面對它們時(shí)分別經(jīng)歷了什么你就徹底明白這條規(guī)則不是為了考試通關(guān)而是語言設(shè)計(jì)的必然結(jié)論。2. 當(dāng)形參寫成值傳遞一次無限遞歸的現(xiàn)場演示把人直接按進(jìn)水里之前先看它掙扎的過程。假如我們把拷貝構(gòu)造函數(shù)的形參改成值傳遞會(huì)是什么情形代碼如下class BadClass { public: BadClass() {} BadClass(const BadClass other) { // 形參是值傳遞 // 業(yè)務(wù)邏輯... } };你猜大多數(shù)編譯器面對這段代碼會(huì)怎么做直接拒絕編譯。GCC、Clang 和 MSVC 都會(huì)提示大概這樣一句話拷貝構(gòu)造函數(shù)的形參必須是const BadClass而不能是BadClass或BadClass。為什么編譯器直接抬出這個(gè)限制因?yàn)檎Z言的語義約束其實(shí)就是在根源上杜絕炸彈。但從理解的角度我們必須親眼看看如果編譯器允許這個(gè)代碼存在運(yùn)行時(shí)會(huì)發(fā)生什么。這是整個(gè)機(jī)制中最核心的部分——拷貝構(gòu)造函數(shù)的值傳參會(huì)引發(fā)無窮無盡的“套娃構(gòu)造”。2.1 從“用對象初始化新對象”到“給函數(shù)傳參”每一次值傳遞都在呼叫拷貝構(gòu)造先明確一個(gè)底層事實(shí)在 C 里函數(shù)形參如果聲明為“按值傳遞”那么在傳入實(shí)參的那一刻系統(tǒng)必須創(chuàng)建一個(gè)形參副本。這個(gè)副本的創(chuàng)建方式就是調(diào)用類的拷貝構(gòu)造函數(shù)。打個(gè)比方值傳參相當(dāng)于把一本書的內(nèi)容手抄一份送給函數(shù)而“手抄”這一行為本身必須靠“抄寫員”來完成這里的“抄寫員”就是拷貝構(gòu)造。現(xiàn)在麻煩來了如果拷貝構(gòu)造函數(shù)本身的形參就是按值傳遞那么函數(shù)體內(nèi)的邏輯是“用other來初始化this所指的新對象”。這個(gè)初始化根本還沒開始函數(shù)入口處就已經(jīng)發(fā)生了一次新的值傳遞把外面?zhèn)魅氲膶?shí)參復(fù)制成形參other。而這一復(fù)制動(dòng)作需要再次調(diào)用同一個(gè)類的拷貝構(gòu)造函數(shù)。每次調(diào)用函數(shù)入口都要再做一次復(fù)制于是形成了一個(gè)沒有終點(diǎn)的調(diào)用鏈。用偽代碼描述的話整個(gè)遞歸過程是這樣的拷貝構(gòu)造(other): // 進(jìn)入之前用外部對象初始化形參 other需要調(diào)用拷貝構(gòu)造 拷貝構(gòu)造(other): // 進(jìn)入之前用參數(shù)初始化形參 other需要調(diào)用拷貝構(gòu)造 拷貝構(gòu)造(other): // 無限遞歸下去...對真實(shí)編譯器而言這種遞歸在語義分析階段就已經(jīng)不是“能不能運(yùn)行”的問題而是“在類型層面這個(gè)函數(shù)在反復(fù)套自己”標(biāo)準(zhǔn)委員會(huì)干脆直接規(guī)定拷貝構(gòu)造函數(shù)必須接受引用。這不是堵死一條實(shí)現(xiàn)繞路的通道而是從語義上承認(rèn)“生成一個(gè)拷貝”這個(gè)動(dòng)作不應(yīng)該以“再生成一個(gè)拷貝”為前提。2.2 遞歸的代價(jià)棧上每層都要留一套完整幀如果硬要繞過編譯器的檢查去模擬這種遞歸例如用一個(gè)返回類型為T的函數(shù)強(qiáng)行觸達(dá)我們要面對的災(zāi)難是每一層調(diào)用都要在調(diào)用棧上分配一份包含形參對象和函數(shù)局部狀態(tài)的空間。假設(shè)這個(gè)對象包含 1KB 的數(shù)據(jù)值傳參一次就需要在棧上復(fù)制 1KB函數(shù)還沒等跑完業(yè)務(wù)邏輯第一層“復(fù)制實(shí)參到形參”的動(dòng)作就吃完了棧。這個(gè)過程會(huì)一直膨脹到棧爆掉。所以即便沒有編譯器的攔截就算某天標(biāo)準(zhǔn)放寬允許這種寫法程序也幾乎沒有可能正常返回。它不是一個(gè)“低效但能用”的設(shè)計(jì)而是一個(gè)“根本不可能完成第一步有效工作”的設(shè)計(jì)。值傳遞的拷貝構(gòu)造函數(shù)等于讓每個(gè)拷貝都要先拷貝出一個(gè)中間人來執(zhí)行拷貝而這個(gè)中間人又需要另一個(gè)中間人來產(chǎn)生。與“先有雞還是先有蛋”的哲學(xué)困境不同這里連蛋殼都落不了地。2.3 一個(gè)圖景類比理發(fā)師悖論的工程版你一定聽過理發(fā)師悖論或者“這個(gè)列表包含所有不包含自己的列表”。值傳遞的拷貝構(gòu)造函數(shù)與此類同拷貝的目標(biāo)是生成一份新對象但為了生成新對象卻又必須先完成一次新的拷貝。用生活化的方式理解可以想象有兩個(gè)寫字的人需要互相幫忙抄寫文件。如果規(guī)定“幫我抄文件的這個(gè)人必須由一個(gè)和我長得一樣的人來指定”但那個(gè)指定者也需要有人來指定最終你永遠(yuǎn)等不到一個(gè)能開始抄寫的人。搞懂這一層你在面試?yán)锶绻粏柕健爸祩鬟f會(huì)發(fā)生什么”就不會(huì)只背一句“無限遞歸”了。你能描述出遞歸鏈條每一環(huán)的本質(zhì)函數(shù)入口的“實(shí)參到形參的復(fù)制動(dòng)作”本身就是一個(gè)合法拷貝構(gòu)造的調(diào)用點(diǎn)。編譯器不允許這種寫法是在入口處直接拆除了炸彈的引信。3. 指針形參語法上能編譯語義上徹底走偏接著是另一個(gè)高頻誤區(qū)有人說“既然值傳遞不行那我就用指針傳反正指針不會(huì)觸發(fā)構(gòu)造函數(shù)”。這話一半對。語法層面類確實(shí)可以寫一個(gè)參數(shù)為Foo*的構(gòu)造函數(shù)這種構(gòu)造函數(shù)也能夠被正常調(diào)用、編譯運(yùn)行。但它有一個(gè)致命問題它根本不被語言判定為拷貝構(gòu)造函數(shù)。3.1 語言定義里的“拷貝構(gòu)造函數(shù)”只認(rèn)T和const T打開 C 標(biāo)準(zhǔn)對類構(gòu)造函數(shù)的限定你會(huì)發(fā)現(xiàn)拷貝構(gòu)造函數(shù)有一個(gè)專門的定義要么參數(shù)是T要么參數(shù)是const T其他形如T* p的形式都不在拷貝構(gòu)造函數(shù)的范疇內(nèi)。這意味著什么意味著編譯器在以下三個(gè)場景中不會(huì)調(diào)用這個(gè)“指針拷貝構(gòu)造”:Foo b(a)這樣直接用已有對象初始化新對象時(shí)編譯器找的是真正的拷貝構(gòu)造函數(shù)按值返回局部對象時(shí)編譯器嘗試用拷貝構(gòu)造來構(gòu)造返回值如果類里只寫了Foo(Foo*)這個(gè)過程就會(huì)失敗把對象放入標(biāo)準(zhǔn)庫容器比如std::vectorFoo需要拷貝對象時(shí)容器模板內(nèi)部要求類型滿足“可拷貝構(gòu)造”找不到合適的拷貝構(gòu)造函數(shù)會(huì)導(dǎo)致編譯錯(cuò)誤或行為異常。一句話指針版“拷貝構(gòu)造”只是類里一個(gè)名字碰巧很像的普通構(gòu)造函數(shù)它不能滿足 C 對值語義對象的基本要求——能夠被按值復(fù)制。3.2 從使用場景細(xì)看指針傳參的麻煩有多大就算我們放下“標(biāo)準(zhǔn)定義”這個(gè)框架只看實(shí)用層面指針傳參也充滿了各種麻煩事。第一是無名臨時(shí)對象。C 里大量對象生成于表達(dá)式中間比如func(Bar())這個(gè)臨時(shí)對象沒有名字不能取地址。如果你類的指針拷貝構(gòu)造函數(shù)長得像Foo(Foo*)那Foo f(Bar())這種寫法就無法工作因?yàn)锽ar()是一個(gè)非法表達(dá)式。而 C 程序員時(shí)刻都在和臨時(shí)對象打交道一個(gè)無法接收臨時(shí)對象的構(gòu)造接口等于自動(dòng)屏蔽掉一半的使用場景。第二是可讀性的混亂。Foo f(source)讀起來像什么像在明確表示“我要把source這塊內(nèi)存的地址傳給構(gòu)造器”至于這個(gè)構(gòu)造器是打算拷貝內(nèi)容、還是只是觀察一下、甚至是侵入性地接管所有權(quán)讀者無法從調(diào)用點(diǎn)判斷??截愂且粋€(gè)很基礎(chǔ)的動(dòng)作它不應(yīng)該要求調(diào)用者去記憶“傳指針會(huì)被當(dāng)成拷貝傳引用也會(huì)被當(dāng)成拷貝二者行為卻有差異”這類心照不宣的潛規(guī)則。第三是空指針和有效性檢查。如果拷貝構(gòu)造函數(shù)接受指針那函數(shù)體內(nèi)必須時(shí)刻準(zhǔn)備處理nullptr。但拷貝操作要求的語義是“源對象一定存在且處于有效狀態(tài)”一個(gè)合法的拷貝動(dòng)作不應(yīng)當(dāng)可能出現(xiàn)“源對象根本不存在”的情況。把非空作為前提程序里到處塞空指針防御邏輯既降低效率又增加犯錯(cuò)面。3.3 指針版的地獄除非你接受一個(gè)不可能達(dá)成的語義有人可能會(huì)辯解說“我可以讓指針版構(gòu)造函數(shù)內(nèi)部做深拷貝行為不就一樣了嗎”問題在于語言層面對拷貝行為的調(diào)度是基于類型系統(tǒng)的。想象一個(gè)極短的場景class Foo { public: Foo(int v) : data(v) {} Foo(const Foo* src) { data src-data; } // 指針“拷貝” private: int data; }; Foo getFoo() { Foo local(42); return local; // 這里會(huì)發(fā)生什么 }函數(shù)getFoo()按值返回local編譯器試圖用local來構(gòu)造返回對象。它需要的是一個(gè)接受Foo作為右值或有 const 引用形參的構(gòu)造入口但類里沒有。于是編譯器報(bào)錯(cuò)“無法將Foo轉(zhuǎn)換為const Foo*”。即使你在返回處手寫return local也不行因?yàn)榉祷仡愋褪荈oo而不是Foo*類型根本不匹配。這就是指針方案最尷尬的地方它自己覺得能做拷貝類型系統(tǒng)卻不認(rèn)賬。每到需要標(biāo)準(zhǔn)拷貝語義的場合編譯器都會(huì)禮貌地告訴你缺一個(gè)真正的拷貝構(gòu)造函數(shù)。所以從工程結(jié)論上講指針傳遞是一個(gè)“寫得出來但用不上的擺設(shè)”誰用誰在真正的拷貝場合翻車。4. 為什么答案偏偏落在 const 引用接口設(shè)計(jì)的三個(gè)層次聊完兩個(gè)錯(cuò)誤答案真相已經(jīng)露出來了拷貝構(gòu)造函數(shù)必須用const T。這個(gè)結(jié)論并非來自一個(gè)人的偏好而是源于三點(diǎn)環(huán)環(huán)相扣的語言設(shè)計(jì)邏輯。4.1 引用保障了“零拷貝地看源對象”引用本質(zhì)上是目標(biāo)對象的一個(gè)別名它不產(chǎn)生新對象、不復(fù)制任何內(nèi)存。因此在拷貝構(gòu)造的入口使用引用傳參可以保證調(diào)用拷貝構(gòu)造不會(huì)因?yàn)椤皞鲄ⅰ边@個(gè)動(dòng)作本身再觸發(fā)一次構(gòu)造。整個(gè)調(diào)用鏈?zhǔn)瞧秸耐獠總魅胍粋€(gè)對象函數(shù)內(nèi)部以引用方式觀看它然后只需要復(fù)制一次其內(nèi)部數(shù)據(jù)就完成了新對象的初始化。這一點(diǎn)和值傳遞形成了決定性反差。值傳參本身就是一次拷貝拷貝構(gòu)造函數(shù)最不能容忍的恰恰是“為了拷貝而先拷貝”。引用把這條捷徑堵住讓拷貝只在一個(gè)地方發(fā)生——即構(gòu)造函數(shù)的函數(shù)體內(nèi)。4.2 const 保護(hù)了源對象不被意外改寫又有人問那為什么不用普通的T理由有很多其中最重要的一條是保護(hù)傳入對象??截悩?gòu)造的目的是“創(chuàng)建一個(gè)獨(dú)立的副本”而不是“順手修改原對象”。把形參限定為const T等于由編譯器替我們確保在拷貝構(gòu)造過程中源對象的任何成員都不可被改動(dòng)。如果有人試圖在拷貝構(gòu)造里寫rhs.data 5編譯器會(huì)直接報(bào)錯(cuò)。這個(gè)設(shè)計(jì)非常重要。因?yàn)榭截悇?dòng)作最常見的場景是形形色色的值語義容器。你把對象塞進(jìn)vectorvector擴(kuò)容時(shí)會(huì)拷貝元素如果容器內(nèi)部對元素調(diào)用拷貝構(gòu)造時(shí)不加 const 保護(hù)可能出現(xiàn)擴(kuò)容過程中源元素被莫名其妙篡改的數(shù)據(jù)競態(tài)。對不可變性的堅(jiān)持消除了大量潛在的邏輯漏洞。同時(shí)const T還能綁定非 const 左值、const 左值以及臨時(shí)右值。類型系統(tǒng)的這種靈活性讓拷貝構(gòu)造可以被廣泛調(diào)用無論是Foo f1(src);、拷貝一個(gè) const 對象還是Foo f2(Foo(1));這種臨時(shí)對象場景都能正常工作。4.3 接口設(shè)計(jì)的三重邏輯行為完整、調(diào)用自由、編譯期兜底如果把const T的三個(gè)性質(zhì)拆開看它們恰好構(gòu)成一個(gè)完整的設(shè)計(jì)矩陣引用消除無限遞歸保證編譯和運(yùn)行兩個(gè)層面都不會(huì)出現(xiàn)自指循環(huán)const 保證源對象只讀不改把拷貝構(gòu)造成本控制在“只讀”的低風(fēng)險(xiǎn)姿態(tài)里引用配合 const 提供了對所有表達(dá)式類別的兼容左值、右值、const 對象一網(wǎng)打盡。工程上我們其實(shí)很難找出另一個(gè)形參類型能同時(shí)滿足這三個(gè)條件。值傳遞從根上破壞第一點(diǎn)T破壞了第二點(diǎn)的安全性指針傳遞雖然繞過遞歸但不符合語言對拷貝語義的識(shí)別規(guī)則。于是設(shè)計(jì)者們選擇了唯一一個(gè)在類型系統(tǒng)內(nèi)自洽的方案。這里順帶提一個(gè)很多人的困惑為什么不能用T右值引用因?yàn)橛抑狄弥荒芙壎ㄅR時(shí)對象而拷貝的應(yīng)用場景大多是持久化的左值對象。如果拷貝構(gòu)造函數(shù)能被右值引用調(diào)用那Foo f(src)這種常規(guī)拷貝反而會(huì)報(bào)錯(cuò)因?yàn)閟rc是左值。右值引用是給移動(dòng)構(gòu)造函數(shù)準(zhǔn)備的與拷貝的目的天然相反。5. 真正會(huì)混淆人的反例為什么拷貝賦值運(yùn)算符的形參可以是值傳遞很多人在這個(gè)知識(shí)點(diǎn)上栽過一次跟頭因?yàn)樗麄儼l(fā)現(xiàn)拷貝賦值運(yùn)算符operator的形參可以寫值傳遞而且這在工程上非常常用。這跟拷貝構(gòu)造函數(shù)完全是兩碼事卻總被放在一起比較。我們把這條線也理清楚否則你對“為什么拷貝構(gòu)造不能值傳”的理解始終缺一塊拼圖。5.1 executor和文書的區(qū)別構(gòu)造函數(shù)與賦值函數(shù)的本質(zhì)差異拷貝構(gòu)造負(fù)責(zé)的是“從無到有生成一個(gè)新對象”它發(fā)生在對象生命周期的最開頭。而拷貝賦值負(fù)責(zé)的是“把已有對象的內(nèi)容覆蓋到另一個(gè)已經(jīng)存在的對象”它發(fā)生在對象生命周期中期?;氐?2.3 節(jié)的比喻拷貝構(gòu)造就像“新招一個(gè)文書讓他模仿老文書的字跡寫一份文檔”問題是這個(gè)新文書自身需要先由另一個(gè)文書仿制出來——于是自指。賦值則不同它的操作對象是“文書已經(jīng)坐在工位上了讓他照著隔壁工位的文檔重寫一遍自己的文檔”。他不會(huì)因?yàn)閰⒖假Y料而需要再被復(fù)制一遍因?yàn)槲臋n抄寫是發(fā)生在人自己頭腦里的。從語言層面看operator(Foo other)中形參是值傳遞但這次值傳遞是調(diào)用賦值函數(shù)之前一次“合法的拷貝構(gòu)造”。外部傳入實(shí)參時(shí)會(huì)用拷貝構(gòu)造復(fù)制一份作為形參other然后函數(shù)體在對已有對象this做賦值操作。這里沒有循環(huán)因?yàn)槊總€(gè)角色都在正確的位置上文書的復(fù)制動(dòng)作發(fā)生在賦值前而不是賦值又要觸發(fā)賦值。5.2 copy-and-swap 慣用法一次值傳參實(shí)現(xiàn)強(qiáng)異常安全具體工程里值傳遞的operator通常跟 copy-and-swap 慣用法綁定出現(xiàn)。核心思路是構(gòu)造一個(gè)臨時(shí)副本再把副本與當(dāng)前對象交換讓所有資源管理的臟活累活都集中在拷貝構(gòu)造和析構(gòu)函數(shù)里class Resource { public: Resource() default; Resource(const Resource rhs) { /* 真正的深拷貝 */ } void swap(Resource rhs) noexcept { using std::swap; swap(ptr_, rhs.ptr_); } Resource operator(Resource rhs) { swap(rhs); return *this; } private: char* ptr_ nullptr; };此時(shí)operator的形參是值傳遞外界傳入實(shí)參時(shí)先通過拷貝構(gòu)造生成rhs隨后用swap交換當(dāng)前對象與rhs的內(nèi)容函數(shù)結(jié)束后rhs作為局部對象自動(dòng)析構(gòu)將舊資源釋放掉。整個(gè)過程中若拷貝構(gòu)造拋異常則賦值操作直接失敗且不會(huì)破壞原對象強(qiáng)異常安全就是這么來的。5.3triple 對比一張表說清楚“能值傳”和“不能值傳”的邊界操作形參類型是否合法為什么拷貝構(gòu)造函數(shù)const T合法拷貝動(dòng)作本身不再觸發(fā)新拷貝遞歸被斬?cái)嗫截悩?gòu)造函數(shù)T非法入口傳參觸發(fā)自調(diào)用無限遞歸拷貝構(gòu)造函數(shù)T*合法但不被承認(rèn)為拷貝構(gòu)造無法綁定臨時(shí)對象語義失真容器無法使用拷貝賦值運(yùn)算符T值傳遞合法正因先走了合法拷貝構(gòu)造才有臨時(shí)副本可交換放到面試場景如果你能把這個(gè)對照表講清楚同時(shí)順帶說明“拷貝賦值值傳遞之所以不遞歸是因?yàn)樗蕾嚨氖呛戏截悩?gòu)造”基本就證明你理解了對象生命周期和函數(shù)調(diào)用的真實(shí)鏈路。6. 實(shí)戰(zhàn)中的旁證與踩坑從“拒絕編譯”到“默認(rèn)構(gòu)造被吞掉”理論知識(shí)說完了最后分享幾個(gè)實(shí)戰(zhàn)層面的直觀印證。這些東西回頭看都是在理解了“拷貝構(gòu)造必須 const 引用”之后才真正想通的。6.1 誤寫值傳遞時(shí)編譯器究竟在吼什么假設(shè)你真的在代碼里寫了Foo(Foo other)不同編譯器給出的診斷信息各有風(fēng)味。GCC 大概會(huì)說“invalid constructor; you probably meant Foo (const Foo)”。MSVC 則說“copying variable of type Foo invokes deleted copy constructor”。不管措辭怎么變本質(zhì)都是編譯器在明確地把這個(gè)構(gòu)造聲明排除在拷貝構(gòu)造之外。更重要的是一旦這個(gè)構(gòu)造函數(shù)被排除原本依賴拷貝構(gòu)造的很多能力也會(huì)瞬間消失Foo b a;會(huì)編譯失敗函數(shù)按值返回Foo會(huì)失敗放進(jìn)std::vector或std::pair會(huì)失敗。你能從錯(cuò)誤列表里感受到一種“連鎖崩塌”——因?yàn)檎麄€(gè)值語義體系都建立在拷貝構(gòu)造這個(gè)地基上地基一歪全部上層建筑跟著歪。6.2 使用std::move時(shí)的教科書級(jí)誤判另一個(gè)有趣的旁證來自移動(dòng)語義時(shí)代。有人會(huì)覺得有了移動(dòng)構(gòu)造函數(shù)之后我把一個(gè)對象std::move進(jìn)新對象走的是移動(dòng)構(gòu)造而非拷貝構(gòu)造哪還需要糾結(jié)拷貝構(gòu)造但注意如果沒有顯式聲明移動(dòng)構(gòu)造編譯器在遇到右值實(shí)參時(shí)仍然會(huì)去匹配const T作為替代方案——只是這個(gè)替代方按拷貝處理。如果你的類設(shè)計(jì)里拷貝構(gòu)造被誤刪或者誤寫成非法形式那些看似與拷貝無關(guān)的右值初始化也會(huì)一起失敗。更進(jìn)一步C17 的拷貝省略copy elision并不能拯救所有拷貝場景。返回值優(yōu)化可以把“返回臨時(shí)對象”省略掉但當(dāng)對象真正需要被復(fù)制時(shí)例如從容器里取出元素并復(fù)制一份拷貝構(gòu)造仍然是必經(jīng)之路。把希望寄托在編譯器的省略優(yōu)化上不如把一個(gè)合規(guī)拷貝構(gòu)造函數(shù)擺正。6.3 我個(gè)人的一條工程箴言多年來我形成了一條習(xí)慣定義任何資源管理類時(shí)先寫T(const T)和T(T)兩個(gè)構(gòu)造聲明再回頭寫其他重載。這條習(xí)慣幫我避開了大量因?yàn)椤皹?gòu)造函數(shù)重載列表亂糟糟”而產(chǎn)生的編譯謎題。當(dāng)你核對過一個(gè)類的特構(gòu)造函數(shù)族之后你很快會(huì)發(fā)現(xiàn)const T幾乎是所有正確設(shè)計(jì)的軸心它能被左值綁定、能被右值綁定、能接收 const 對象、能接收非 const 對象并且絕不參與任何復(fù)制流程堪稱一切初始化操作的兼容底座。如果還有人問起“為什么不能是指針”我的回答永遠(yuǎn)是傳指針意味著傳遞的是一個(gè)地址而地址所指向的對象可能不存在、可能臨時(shí)、可能被 const 限定??截惖恼Z義要求一個(gè)穩(wěn)定且完整的源對象引用恰好把“對象本身”呈現(xiàn)在函數(shù)內(nèi)部而不必為對象再支付一次“產(chǎn)生新對象”的代價(jià)。這條原理從 C 誕生至今沒有變過未來也不太可能變。把這個(gè)問題想透了你對整個(gè)值語義、對象生命周期和編譯器重載決議的把握都會(huì)上一個(gè)臺(tái)階。