候必須用指針)
「?jìng)鲄⒌降子弥羔樳€是引用」這個(gè)問(wèn)題被問(wèn)了一萬(wàn)遍多數(shù)回答卻走向一個(gè)教條用引用指針是 C 的殘余。這話只對(duì)了一半。引用有它不可替代的場(chǎng)合但指針在好幾類(lèi)需求面前是唯一正確選項(xiàng)。下面務(wù)實(shí)地說(shuō)清邊界什么時(shí)候該用引用什么時(shí)候必須用指針以及為什么兩者在傳參開(kāi)銷(xiāo)上其實(shí)一樣。官方文檔References (cppreference)一個(gè)引用表達(dá)不了的需求假設(shè)你寫(xiě)一個(gè)「在數(shù)組里找第一個(gè)偶數(shù)」的函數(shù)。找到了要返回那個(gè)元素的地址沒(méi)找到呢引用不能為空、不能重綁它根本沒(méi)有「沒(méi)找到」這個(gè)狀態(tài)可以表達(dá)。這時(shí)候硬用引用只會(huì)寫(xiě)出別扭的接口或者偷偷塞一個(gè)哨兵值進(jìn)去。指針天生能取nullptr恰好把「可選」語(yǔ)義說(shuō)清楚。下面從兩者的硬性區(qū)別說(shuō)起。引用與指針的硬性區(qū)別能力引用T指針T*必須初始化是且只能綁一次否可先聲明后賦值能否重新綁定到別的對(duì)象不能終身綁定能p other能否為空不指向任何對(duì)象不能理論上能nullptr指針?biāo)阈g(shù) / 遍歷數(shù)組不能能p、p i表示數(shù)組或連續(xù)內(nèi)存不能單對(duì)象能天然指向首元素底層傳遞開(kāi)銷(xiāo)大對(duì)象傳地址約 8 字節(jié)傳地址約 8 字節(jié)結(jié)論先放這引用勝在契約強(qiáng)它向讀代碼的人承諾這里一定有個(gè)有效對(duì)象指針勝在靈活可空、可重定向、可算術(shù)。選哪個(gè)取決于你要的是強(qiáng)契約還是靈活性。什么時(shí)候必須用指針下面四類(lèi)需求引用做不到或做得很丑必須用指針或它的現(xiàn)代替身std::optional稍后說(shuō)邊界表達(dá)「可能沒(méi)有」函數(shù)可能找不到東西、可能失敗用nullptr表示「無(wú)」比拋異?;蛏诒蹈p量。在容器里存「可重置的指向」比如std::vectorNode*某個(gè)槽位可以隨時(shí)設(shè)成nullptr或換指另一個(gè)節(jié)點(diǎn)引用既不能重綁也不能為空做不到。遍歷數(shù)組 / 指針?biāo)阈g(shù)要p挨個(gè)掃 buffer、用*(base i)隨機(jī)訪問(wèn)只有指針能寫(xiě)。多態(tài)對(duì)象基類(lèi)指針Base*指向派生類(lèi)實(shí)例是實(shí)現(xiàn)運(yùn)行時(shí)多態(tài)的經(jīng)典手段。引用也能多態(tài)但它不能數(shù)組化也不能在「可能沒(méi)有」時(shí)退成空。官方文檔 Polymorphism via pointers (cppreference)傳參開(kāi)銷(xiāo)對(duì)比引用底層也是指針有人覺(jué)得「引用比指針快」這是誤解。在常見(jiàn)平臺(tái)x86-64上引用在絕大多數(shù)實(shí)現(xiàn)里就是底層傳了一個(gè)地址和指針一模一樣沒(méi)有解引用次數(shù)的差異也沒(méi)有拷貝整個(gè)大對(duì)象的開(kāi)銷(xiāo)。用 Compiler Explorer 看void f(const Big)和void f(const Big*)生成的匯編對(duì)參數(shù)的存取指令基本一致。傳參開(kāi)銷(xiāo)以大對(duì)象 Big 為例64 位平臺(tái) 棧/寄存器里傳的內(nèi)容 是否拷貝整個(gè)對(duì)象 引用 T 8 字節(jié)地址 否 指針 T* 8 字節(jié)地址 否 值 T 整個(gè)對(duì)象可能上百字節(jié) 是 - 大對(duì)象才該用引用/指針避免所以引用和指針怎么選是語(yǔ)義問(wèn)題不是性能問(wèn)題。大對(duì)象別按值傳至于引用和指針開(kāi)銷(xiāo)相同。真正影響性能的是按值傳大對(duì)象觸發(fā)的整份拷貝那才是要避的坑。把兩種「指向」在內(nèi)存里畫(huà)出來(lái)會(huì)看得更清楚。編譯器給它們生成的機(jī)器表示幾乎一樣差別全在類(lèi)型系統(tǒng)加的那些約束上同一個(gè)對(duì)象 obj棧地址 0x7ffd10兩種「指向」方式 int obj 42; 0x7ffd10 ┌──────┐ │ 42 │ └──────┘ int r obj; r 不是一塊獨(dú)立存儲(chǔ)它就是 obj 的別名 r 7 → 0x7ffd10 變成 7 r → 永遠(yuǎn)是 0x7ffd10綁不到別處 沒(méi)有「空引用」這種狀態(tài) int* p obj; p 自己占 8 字節(jié)假設(shè)放在 0x7ffd20 0x7ffd20 ┌──────────┐ │ 0x7ffd10 │──? 0x7ffd10: 42 └──────────┘ p nullptr → 指向「無(wú)」 引用沒(méi)有這個(gè)取值 p other → 改指別處 引用做不到 p 1 → 下一個(gè) int 引用做不到 傳給函數(shù)時(shí)f(const int) 與 f(const int*) 傳的都是一個(gè)地址寄存器里的指令一致 結(jié)論選引用還是指針 選「要哪些類(lèi)型層約束」與運(yùn)行時(shí)開(kāi)銷(xiāo)無(wú)關(guān)完整示例 A多態(tài)基類(lèi)指針 可空查找下面這個(gè)程序同時(shí)用到了「指針實(shí)現(xiàn)多態(tài)」和「返回nullptr表達(dá)沒(méi)找到」兩個(gè)必須用語(yǔ)指針的場(chǎng)景// demo.cpp — 編譯: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includevectorstructShape{virtual~Shape()default;// 基類(lèi)析構(gòu)必須 virtualvirtualvoiddraw()const0;};structCircle:Shape{voiddraw()constoverride{std::cout畫(huà)圓\n;}};structSquare:Shape{voiddraw()constoverride{std::cout畫(huà)方\n;}};// 返回指向找到元素的指針越界則返回 nullptr表達(dá)「可能沒(méi)有」constShape*find_by_index(conststd::vectorShape*shapes,intidx){if(idx0||idxstatic_castint(shapes.size()))returnnullptr;returnshapes[idx];}intmain(){Circle c;Square s;std::vectorShape*shapes{c,s};// 多態(tài)基類(lèi)指針容器for(constShape*p:shapes)p-draw();// 指針實(shí)現(xiàn)多態(tài)constShape*foundfind_by_index(shapes,1);if(found)found-draw();elsestd::cout沒(méi)找到\n;constShape*missfind_by_index(shapes,9);if(miss)miss-draw();elsestd::cout沒(méi)找到\n;return0;}畫(huà)圓 畫(huà)方 畫(huà)方 沒(méi)找到shapes是Shape*容器循環(huán)里通過(guò)基類(lèi)指針調(diào)用draw()實(shí)際派發(fā)到Circle和Square。find_by_index越界時(shí)返回nullptr這個(gè)「可能沒(méi)有」的狀態(tài)引用給不了。完整示例 B指針?biāo)阈g(shù)遍歷數(shù)組引用做不到「p掃 buffer」只有指針能寫(xiě)。這個(gè)例子用指針求和并隨機(jī)訪問(wèn)// demo.cpp — 編譯: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostreamintmain(){intbuf[]{10,20,30,40};int*pbuf;// 指向首元素int*endbuf4;// 尾后哨兵intsum0;for(;p!end;p){// 指針遍歷數(shù)組sum*p;}std::cout和: sum\n;std::cout第二個(gè)元素: *(buf1)\n;// 指針?biāo)阈g(shù)隨機(jī)訪問(wèn)return0;}和: 100 第二個(gè)元素: 20p、*(buf 1)這類(lèi)指針?biāo)阈g(shù)是引用的禁區(qū)。標(biāo)準(zhǔn)庫(kù)的很多底層遍歷以及 C API 離不開(kāi)指針原因就在這兒。完整示例 C容器里存「可重置的指向」前面那張區(qū)別表里「引用不能重新綁定」一行看著抽象放到容器里就很具體了std::vectorT*里每個(gè)槽位都可以隨時(shí)換成別的對(duì)象或者重置成nullptr而std::vectorT連寫(xiě)都寫(xiě)不出來(lái)標(biāo)準(zhǔn)容器根本不存引用。下面是一個(gè)觀察者注冊(cè)表廣播時(shí)遍歷槽位注銷(xiāo)時(shí)把某個(gè)槽位置nullptr。// observer_registry.cpp — 編譯: g -stdc17 -Wall -O2 observer_registry.cpp -o ob#includeiostream#includevectorstructObserver{constchar*name;voidnotify()const{std::coutname 收到通知\n;}};intmain(){Observer alpha{alpha};Observer beta{beta};Observer gamma{gamma};// 引用不能重綁、不能為空所以進(jìn)不了容器指針可以std::vectorObserver*registry{alpha,beta,gamma};std::cout--- 第一輪廣播 ---\n;for(constObserver*o:registry)o-notify();registry[1]nullptr;// 注銷(xiāo) beta把槽位重置為空 —— 引用做不到std::cout--- 第二輪廣播 ---\n;for(constObserver*o:registry){if(o)o-notify();elsestd::cout(空槽位已注銷(xiāo))\n;}std::cout注冊(cè)表長(zhǎng)度 registry.size()\n;}--- 第一輪廣播 --- alpha 收到通知 beta 收到通知 gamma 收到通知 --- 第二輪廣播 --- alpha 收到通知 (空槽位已注銷(xiāo)) gamma 收到通知 注冊(cè)表長(zhǎng)度 3這里的registry只是借看alpha/beta/gamma三個(gè)棧對(duì)象并不擁有它們所以對(duì)象必須在registry之前聲明、在它之后銷(xiāo)毀。這正是「不擁有」型指針的固有代價(jià)換來(lái)可重置、可為空的靈活性代價(jià)是生命周期得由你自己擔(dān)保。順序?qū)懛戳说诙啅V播就會(huì)讀到一個(gè)已經(jīng)沒(méi)了的對(duì)象。易錯(cuò)點(diǎn)與常見(jiàn)誤解「引用還是指針」這個(gè)話題里下面幾條誤解傳播得最廣「引用一定不會(huì)懸垂指針才會(huì)」。引用只是排除了「空」這一種失效狀態(tài)。const T f() { T local; return local; }返回的是已經(jīng)銷(xiāo)毀的局部對(duì)象的引用引用綁到一個(gè)delete過(guò)的對(duì)象同樣會(huì)炸。引用不比指針更安全它只是把「可能沒(méi)有」這一種情況從類(lèi)型里刪掉了?!窩17 里可以用std::optionalT表達(dá)『不擁有且可能沒(méi)有』」。不行。std::optional要求T是對(duì)象類(lèi)型std::optionalT不合法標(biāo)準(zhǔn)禁止optional的模板參數(shù)為引用。要表達(dá)「空」只有兩條路T*或者用std::reference_wrapperT包一層但它本身也不能為空。所以「不擁有 可能沒(méi)有」的正確答案仍然是裸指針。「返回const T比返回const T*更現(xiàn)代」。只在「一定能返回一個(gè)對(duì)象」時(shí)成立。查找類(lèi)函數(shù)必須有「沒(méi)找到」這個(gè)取值硬用引用就只能返回一個(gè)靜態(tài)哨兵對(duì)象調(diào)用方無(wú)從分辨「找到的正好是哨兵」和「沒(méi)找到」。Core Guidelines 的建議是可能失敗就用指針nullptr本身就是最清晰的失敗信號(hào)?!赣靡镁痛韰?shù)一定有效」。引用保證的是調(diào)用那一刻綁定到了一個(gè)對(duì)象不保證這個(gè)對(duì)象之后一直活著。真正要防「對(duì)象被銷(xiāo)毀后仍被使用」得靠所有權(quán)設(shè)計(jì)比如把非擁有指針換成std::weak_ptr而不是靠換簽名?!钢羔槄?shù)是 C 時(shí)代的遺留寫(xiě)法」。Core Guidelines F.7 恰恰推薦用T*/T傳遞可能被修改的對(duì)象。被勸退的從來(lái)不是裸指針本身而是用裸指針傳遞所有權(quán)那條是 I.11屬于另一個(gè)話題?!窽* const和const T*差不多」。差得多const T* p是內(nèi)容不能改T* const p是指針自己不能再指向別處。引用天然接近后者綁定后就不能改指向這也是T與T*在可變性上一個(gè)微妙差異。用引用就不必再糾結(jié)頂層 const。選型決策表把前面所有場(chǎng)景收進(jìn)一張表照著選你的需求用參數(shù)一定存在、函數(shù)內(nèi)不改指向、也不要空T強(qiáng)契約首選參數(shù)需要被修改且一定存在T非常量表達(dá)「可能沒(méi)有」——且不擁有它T*配合nullptr判空擁有且「可能沒(méi)有」std::optionalT值語(yǔ)義不裸指針需要重新指向不同對(duì)象T*遍歷數(shù)組 / 指針?biāo)阈g(shù) / 緩沖區(qū)T*或std::span多態(tài)基類(lèi)接口、可數(shù)組化T*引用也能多態(tài)但不能數(shù)組/重綁務(wù)實(shí)提醒表示「擁有且可能沒(méi)有」時(shí)優(yōu)先std::optionalT而不是指針——它把「空」和「析構(gòu)」一起管好了表示「不擁有且可能沒(méi)有」才用裸指針。所有權(quán)的邊界是下一篇的主題。延伸閱讀Reference vs Pointer (cppreference)引用語(yǔ)義的權(quán)威定義。std::optional (cppreference)表達(dá)「擁有且可能沒(méi)有」的現(xiàn)代替代。Compiler Explorer (godbolt.org)貼f(const Big)與f(const Big*)看匯編驗(yàn)證兩者傳參開(kāi)銷(xiāo)一致。收個(gè)尾能用引用就用引用需要「可能沒(méi)有」、需要重新指向、需要算術(shù)再換指針。傳參開(kāi)銷(xiāo)兩者一樣別把它當(dāng)選型理由那是想多了。真正容易卡住的是所有權(quán)誰(shuí)負(fù)責(zé)delete這件事引用和指針的寫(xiě)法都不寫(xiě)在臉上。這個(gè)得靠類(lèi)型名和約定說(shuō)清楚下一篇專(zhuān)門(mén)講。