1 什麼是指針
本文所謂的指針(pointer) ,是指C和C++等語言中的內建的語言特性。
在不一樣範疇中指針這個概念有所不一樣。在體系結構規範中,指針指稱特定的整數字節地址或者兩個地址的差(地址偏移量),是整數數值;而在C和C++中,做爲核心語言特性支持的指針是一類類型的統稱。這兩種徹底不一樣的概念常常被混淆,形成一些稀裏糊塗的問題(和數組混在一塊兒的時候尤甚)。除非另行說明,本文老是指後者,並不對此進一步展開論述。
C和C++中,指針(右)值是具備指針類型的(右)值。指針值有時也會被和指針混淆,但在健全的理解下一般能消歧義,所以問題不大(數組也有相似的狀況,但涉及轉換,問題相對嚴重)。爲清晰起見,在這裏不會不加區分地使用。
注意,C++的成員指針(pointer-to-member)明確地不是指針。儘管它的數值表示在一些狀況下可能被實現爲地址的偏移量,但語言中並不存在這種保證,實際也一般不那麼簡單。重複:成員指針不是這裏討論的指針。
此外,C++中,除了做爲語言特性支持的內建(builtin) 指針,也有所謂的智能指針(smart pointer) 。後者在概念上也被 ISO C++11 以來的標準庫正式支持。這裏討論的指針不包括這些智能指針,儘管後者和主題相關,而且會在下文重點討論。
2 什麼是設計
這裏討論的設計,是指語言的設計,也就是語言規則的做者決定語言特性中應該存在什麼和不存在什麼的決策之下的抽象結果。
用戶如何使用指針即語用問題是和本文主題相關的問題,會一併討論,但和這裏的設計是兩個不一樣的話題。
3 什麼是糟糕
糟糕是一個形容詞。
形容設計的糟糕從兩個遞進的視點得出:對照設計要解決的問題,即需求;對照同類解決方案,即語言中的其它特性或應用領域有交集的其它程序設計語言中的特性。
通俗地,糟糕以「很差用」和「並不是不得很差用」來表現。
注意由於語言規則之間的相互做用,是否「好用」或者說要解決的問題,須結合使用場景下的其它問題一併討論:一項特性即使能很好地解決某些問題,但若幾乎老是引發其它難以迴避的問題,那麼至少是「不夠好用」的;而要形成的問題麻煩到必定程度時就顯然「很差用」了。算法
4 指針有什麼用
在說明很差用以前,首先須要瞭解有什麼用。
這是一個發散的語用問題,但大多數用法都很淺顯,清楚語言規則就並不難概括。
4.1 指針和地址
C/C++的指針值和體系結構中的所說的指針(地址或地址偏移量)的基本做用相似,它用來指示數據存儲的位置。
以體系結構的接口實現C/C++,能夠輕易保證相同類型的指針值到地址的映射是單射,即相同指針類型的指針值的不一樣的數值表示能夠老是找到不一樣的地址對應,這樣就能夠在整數算術和關係操做的基礎上毫無額外代價地定義指針算術和關係操做;而指針上的操做符*抽象的正是間接尋址操做。這就是一些用戶口中的所謂「接近底層」。這種簡單直接實現的最大好處就是容易以很是小的代價生成針對特定體系結構的代碼。
一個須要注意的關鍵不一樣點在於,C/C++做爲強類型語言(這裏的用法也比較亂,指的是本來意義——默認具備靜態類型檢查),其中的值(value) 脫離類型討論並無意義,指針值也不例外。對象指針能夠進行算術操做,但和整數地址算術的含義並不相同,這受到具體指針類型的影響——例如,T*和整數的+操做和sizeof(T)相關;而函數指針並無相似的意義。此外,須要不一樣間接操做層次的值如T*和T**也可被明確地靜態地區分,光靠地址並不能作到這點。
然而,由於體系結構(硬件)實現的慣例,這個差別在每每能被利用(典型地,基址變址尋址),生成相對高效的代碼。這是語言中保留指針算術的用途之一。另外一方面,把地址相關的整數數值明確和通常的整數值區分,也明確的目的,使靜態檢查非預期的混用成爲可能,有限地提供了類型安全性(例如,指針和指針不能相加)。
經過兩個地址,或一個地址和表示字節大小的一個非負整數就能夠標識出地址空間的區間範圍。把地址替換爲對象指針、字節大小替換爲長度(指針值指向的連續元素的個數)同時限制取得指針的手段,能保留這種標記連續存儲區域的功效,同時提供必定的可移植性。這種連續的存儲在類型系統上被抽象爲數組。不過應當注意,在可移植的要求下,實際上指針的語義依賴於數組。徹底繞過數組的存在任意地構造一個指針值不能保證指向有效的對象或函數,進行間接操做基本上總會致使未定義行爲。
另外一個關鍵不一樣是空指針值(null pointer value) 並不保證有特定的地址對應,見下文。
4.2 存儲資源管理
由於一個對象指針和長度能夠用於表示連續的內存,而對象(排除VLA)的大小能在翻譯時靜態肯定,因此在已知大小的存儲區域能夠用一個對象指針值直接表示。
ISO C標準庫的malloc和calloc以及ISO C++標準庫的::operator new和::operator new[]等的返回值是典型的實例。
這裏大小是由存儲分配另外保存,這樣釋放時仍然只須要傳遞一個指針值便可。ISO C++14提供了sized deallocation,不過這並不是強制。
4.3 參數傳遞
由於從分配函數中取得的指針表示的存儲並不會如自動變量同樣會被自動回收,同時指針有間接操做,而指針值做爲對象類型的值能夠做爲參數傳遞,所以傳遞指向對象的指針值配合間接操做就能夠模擬傳遞對象的引用。
4.4 基於存儲的迭代
由於對象指針能表示存儲位置,連續存儲的佈局由存儲模型(以及基於數組的語義)規則限定,適當類型的指針值進行算術操做能夠雙向順序迭代乃至隨機訪問連續存儲的對象。
4.5 空指針值
指針類型是可空類型(nullable) 類型,約定特殊的空指針值表示不指向任何對象或函數,但能夠進行有限的比較。
可空類型很容易用來表示可選(optional) 值:約定空指針表示值不存在,非空指針指向的對象或函數即存在的可選值。
空指針值還能夠表示哨位(sentinal) 即迭代終止的標識。相對於具體存儲區間結束的指針相比,空指針值是通用的,並不須要根據特定的區間使用不一樣的值。
注意空指針值的存儲表示不必定是整數零值(這再次體現了人爲預選的數值和地址的無關性),儘管使用零值通常能有更好的初始化性能。數組
5 指針爲何很差用
既然標題已經肯定了指針設計的糟糕,那麼在「很差用」上天然有充分的理由。
總結就是,指針看上去能幹不少事,但沒同樣事是徹底幹好的,還有的事(好比聲明語法)甚至在通常意義上就特別差。
諷刺的是,第一個大規模使用這種指針的C語言做爲UNIX系統的實現卻徹底違反了UNIX程序鼓吹的模塊化設計哲學:只作一件事,而且把事作好。
爲何「程序」應當遵照的原則,分解到實現語言的特性的層次上,就能夠罔顧設計原則乃至表現得相反了呢?難道這裏不更應該體現接口的可組合性嗎?回味無窮。
5.1 使用的意圖
首要的原罪就是指針能幹太多事了,致使若是隻須要其中的某些功能子集(幾乎全部狀況都是這樣,實際上也不可能全用上,見下文)就不容易看清楚代碼在作什麼,也就是任何「正常」的使用與使用其它替代實現手段相比,都很容易明顯損害代碼的可讀性。
要避免這點,要麼放棄使用指針而使用其它替代,要麼就不得不以文檔(包括註釋)等形式來把這些接口規格中大多沒必要出現的瑣碎細節約定清楚。後者很容易顯著增大實現和維護的工做量。
5.2 易錯
由於意圖不明的關係,使用指針的代碼比使用其它更清晰的替代的代碼更有機會錯誤,而指針自己的靜態類型檢查對此心有餘而力不足。
最顯著和嚴重的錯誤多是對於存儲資源管理的錯誤。
注意C/C++語言要求去配函數的指針值參數若非空,則必須和適當的分配函數的返回(指針)值相等且不能以相同的值調用去配函數超過一次,不然程序行爲未定義。
由於指針值並不保證翻譯時肯定,靜態檢查對此類誤用效果頗有限,要想安全使用且不泄露資源,用戶必須清楚使用的指針是否能夠被釋放,而後準確保證從分配函數獲得的非空指針值剛好做爲參數調用正確的對應的去配函數一次——這裏是否能夠釋放的全部權(ownership) 信息並無編碼在指針的類型之中。
注意,單看一個指針值,有或者沒有全部權是肯定的,不存在第三種情況。鑑於這兩種情況互斥,所以一個指針值不可能同時是表示存在全部權的資源指針和表示不存在全部權的資源視圖/觀察者指針。這也就是上文說「不可能全用上」的緣由。
然而事實是明確持有一個有全部權的指針,須要釋放時,光看指針根本無法知道該使用哪一個去配函數……更有甚者,其實光從指針上根本就看不出有沒有全部權。
若是一個返回指針值的函數不幸沒有文檔描述清楚用戶該如何處理資源釋放問題,就面臨了兩難的風險:調用錯誤的去配函數或重複釋放致使未定義行爲,或者放置無論而至少產生泄漏的後果。
可能就是由於這樣,WG14( C 標準委員會)有一條不成文的規矩:返回指針的函數老是不帶全部權——也就是用戶不該該釋放這裏的資源。
然而就連 Austin Group (起草 POSIX 標準的做者)對此都並不買帳(更別提 GNU 等了),形成了接口設計上的衝突(詳見 WG14/N1174 ),可見這條默許的規則在 C 用戶的範疇內總體上行不通。
用戶該何去何從?看臉……(沒有接口文檔的本身踩坑怎麼死?看着辦。)
5.3 沒必要要的負擔
這裏最明顯的例子是明明靜態肯定在不須要空指針的狀況下不得不判斷指針值是否爲空,給程序運行帶來沒必要要的開銷。
所謂的「空指針」濫觴於C.A.R.Hoare在1960年代的ALGOL W語言的發明。2009年,Hoare在一個會議上爲此道歉,緣由是空指針特性引起了不少程序設計中的錯誤和漏洞。
盲目省略空指針值檢查的致使使用指針間接操做的值引發未定行爲的錯誤威力並不比上面資源管理的錯誤來得小。所以一旦接口沾染了指針,事情就複雜了——最容易的修復就是放棄使用指針這樣的可空類型。
5.4 語法噪音
上面說的都是語義直接相關的語用困難。
事實上,即使不考慮語義問題,經驗代表光是 C/C++ 的指針語法(嚴格來講不光是指針本身的問題,還有數組、 C++ 的引用和 C++/CLI 的句柄等,都屬於此類)也至關反直覺了。大部分用戶遇到嵌套的指針聲明符甚至都不能一會兒看明白邊界,更別說表示什麼意思了。
對這個問題的主要變通是使用 typedef 。但未必每一個接口都會老實用——好比 ISO C 的 signal 函數就沒有用。因此遇到了用戶仍是要硬着頭皮看。另外還可能有同時有使用 typedef 和不使用 typedef 名稱並存然而二者等價的局面,此時用戶就得當人肉編譯器自行驗證 typedef 和複雜聲明符的等價性了……
而現代的編譯器也沒能利用這樣的語法帶來簡化。
鑑於這種看起來精巧實則無用的設計帶來的困難,Bjarne Stroustrup 等在 C++ 嘗試引入更直白的語法。可是,雖然 trailing-return-type syntax 是引入到 ISO C++11 裏了,兼容 C 卻不能排除舊的語法,結果就是對用戶來講存在兩套不徹底兼容語法要學,編譯器也得把兩套語法都實現這樣一個混亂局面……
5.5 語義噪音
一樣由於意圖不明的關係,要讓不一樣用法之間存在差別變得困難了。
舉例來講,C++不須要內建指針模擬對象引用傳遞參數,因此看到->和一元操做*(重載另說,但不抽風的重載不該該和這裏的清晰性背道而馳)就能夠大體肯定此處進行的是非平凡(模擬參數傳遞)的操做。
考慮到模擬引用參數也必然不須要空指針值,這樣一來差距更明顯。
5.6 抽象的無能
或許抽象能力的缺失纔是最大的現實問題,由於關乎高級語言的本質目的,而並不是特定的個別需求。
一個例子是,迭代存儲連續的序列用算術操做,爲何一樣是迭代,鏈表就不能相似的語法呢?
不過只是「很差用」的角度並不容易集中體現這一點,此處先略過。
5.7 互操做性
和體系結構的交互或許是指針惟一合適的領域了。不過,這依賴於實現的假設,所以操做起來並不那麼有普適性。
即使平時鼓吹「硬件友好」「接近底層」,事實上 C 就不存在對地址空間的抽象,還得靠廠商或者 WG14/N1169 這類幾乎名不見經傳的擴展。
卻是 C++ 標準庫的分配器機制原本有要支持上面擴展的考慮不一樣的指針,雖而後來都流行平坦地址空間而後這個需求就死得差很少了……
一個根本硬傷是,相同類型指針值到地址的映射是單射而不是滿射——也就是任意一個地址即使在體系結構和實際機器的環境下容許,也有重重限制,根本不保證能用能自由操做的指針表示。
這樣,關鍵時刻到底還得上體系結構相關的擴展乃至彙編和/或機器語言……(什麼硬件友好接近底層,見鬼去吧(╯`□′)╯(┻━┻!)
5.8 理解的混亂
事實證實,指針自身的微妙規則以及和數組之間看似說不清道不明的關係給教材編寫者以及初學者帶來了極大麻煩。
無論是 Bjarne Stroustrup 鼓吹的 teachability 仍是通常用戶期待的「易用性」,指針的語法和語義規則都是重災區。
整體來看,這種的問題的根源來源於指針這項語言特性自身的設計——包括是否是真的適合做爲核心語言特性這點。
5.9 誰來承擔責任
好笑的是,缺陷這樣明顯的語言特性,一邊在被各類集中地濫用和誤用,一邊被井底之蛙吹噓爲「 C 的靈魂」騙更多不知情者上當……
容忍這樣的缺陷和製造混亂代碼的做者一般是同一撥用戶。對於不良語用致使的後果卻每每由合做的理解更透徹的維護者承擔,把本能夠知足更多現實需求的時間花在給腦殘粉的爛代碼擦屁股的破事上。
這是有多不公平呢?安全
6 「指針」必須這樣很差用嗎/不用指針用啥
若是不限於內建指針,答案是否認的。
從指針幾個有用和經常使用的使用慣例來看,搞清楚真實需求以後,很容易設計出更安全好用的機制。固然,得有足夠的其它核心語言特性支持,類型系統羸弱的 C 只能靠邊站。
對這裏的缺陷修正得比較完全而又比較流行的例子主要就是 C++ ,同時 C++ 也保留了指針的操做,反而更有必要澄清何時不適合用指針。因此如下以 C++ 爲例(涉及的主要特性,其它現代語言,即使沒有指針也大多有對應)。
6.1 瞭解意圖、避免常見錯誤和提高可讀性
若須要間接操做表示資源,使用帶全部權的智能指針。同時能夠自動管理資源,避免資源泄漏。不加封裝地使用內建指針意味着更混亂的代碼路徑,一般是糟糕的代碼。
若須要間接操做表示不帶全部權的資源視圖,使用不帶全部權的特定指針類型,如 WG21/N4282 提議的 observer_ptr 來幫助代表意圖。(這裏使用內建指針的問題相對比較小,在沒有其它選擇的偷懶狀況下,使用內建指針相對來講可以被容忍,由於帶有全部權的指針已經被其它智能指針區分出去了。)
若須要傳遞引用,直接使用內建引用。在須要複製引用的場合,使用 std::ref 之類的包裝。內建指針在此本質上毫無必要,而且沒法使用大部分其它設施(只有 std::bind 等一些少數例外)。
若須要可空類型,使用 WG21/N4480 等規範的 optional 類型。(內建指針仍然是個能夠忍耐的替代,但並不推薦。)
若須要迭代操做,使用迭代器(iterator) 。迭代器同時有更好的類型安全性、適應性和可擴展性。指針做爲隨機訪問迭代器的特例是能夠被使用的,但仍然應當當心行事。
經過劃分典型應用場景,就基本解決了上面的最麻煩的一些問題。除了靜態區分存在和不存在全部權相互矛盾以外,以上類型也是能夠組合的,所以同時須要多種意圖也不須要使用內建指針。
6.2 抽象能力和可擴展性
這集中體如今智能指針和迭代器與內建指針的對比之上。
內建指針的語義基本是被核心語言規則寫死的,它並不能實現智能指針這樣用戶自定義資源全部權管理策略,以及迭代器這樣的適配於不一樣實現構造的序列上。由於過於特殊,能夠說是至關地無能。高下立判。
經過迭代器類別(iterator category) 的抽象層次和 tag dispatch 這樣基於重載(說穿了,一種模式匹配)的技巧,還能實現對不一樣性質的序列靜態自動選取最優算法。不知比指針高了哪裏去了。
固然,內建指針和典型體系結構實現之間的能力仍舊沒有被取代。但指針在真正底層(好比說,地址空間)的抽象仍然一直是個坑。並且這明顯不是高級語言的本職工做。若是不是照顧兼容性,讓廠商實現成擴展並用標準庫包裝,說不定還不會像如今那麼混亂。
6.3 約束更強的設計
(現代) C++ 是強烈強調靜態類型存在感的語言。這種設計有利有弊,但從實踐效果來看,正確地使用可以發揮靜態類型檢查的優點,是當代軟件工程實踐的重要趨勢之一。(靜態類型固然有很是雞肋的地方然而現實是大部分用戶根本連邊都碰不到……注意缺少元數據是 C++ 和標準化的鍋,不是靜態類型的鍋。)
可是 C++ 限於歷史包袱(兼容 C 、兼容如今各類代碼),即使比 ISO C 勇於甩手扔包袱,也得考慮一下現實影響。在這個意義上,用戶相對較少的小衆語言以及新設計的語言就沒有那麼多顧慮,能將有目的的設計刻意發揮得更充分。
舉兩個稍微不怎麼小衆的例子。
一個是 Haskell 。應該說重點不純粹是靜態類型的問題,而是在類型系統的設計上使用了對靜態分析友好的較爲系統化的設計。(而並非像 C++ 那樣一小坨一小坨地加特性,而這裏最大坨的 Concept 被否了……)
固然這貨主要用於開眼和拓展想象力,由於默認求值策略過於標新立異實際上不適合通用的需求,在 DSL 以上的實用仍是算了。
另外一個是 Rust 。嗯,設計的主要目標是取代 C++ ,應該還算是比較現實(?)的。在這裏值得一提的是有很多設計把上面的策略整合到核心語言特性上去了而且有系統的理論支撐,好比 linear typing 是對 C++ 的 std::unique_ptr 強化。
姑且不論大雜燴的實用程度,這在科普上比較有意義。
6.4 複雜性誰來買單
有的用戶可能會說,這麼複雜,仍是用內建指針直接偷懶算了。
對此我只能表示呵呵。你真有自覺到徹底寫清楚各個層次的接口文檔代表語用?——注意,各個層次,包括如今當成實現細節而未來可能被接手的其餘維護者當成內部接口使用的任意層次的「接口」。
若是:
(1)由於非自身緣由只能用 C 這等無能玩意兒的並且真作獲得及說服了其它倒騰這坨代碼的(若是有)也一樣作到上面所說的自覺,或者——
(2)保證這坨代碼不流入公衆視野充實反面教材,同時實現者保證必要時時刻懺悔生產垃圾多出來的碳排放
那麼當我沒說。
不然……思想有多遠就給我滾多遠。
又不是叫你發明新語言特性本身實現編譯器,都敢倒騰「底層」語言了,瞭解基本需求和解決方案這麼點簡單的分內之事都作很差還有臉生產垃圾污染環境讓人擦屁股來添亂?
仍是有誰逼你用這坨容易炸的東西了?(不懂適應現實?那麼餓死活該。)
注意,業界歷來不缺豬隊友,少一頭的確照樣轉(蠢代碼照樣蠢)。
7 結語
略。模塊化