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