是的,沒有打錯,标題中是<code>/0</code>而不是<code>0</code>。
那麼問題就來了:除以0會發生什麼?
限定條件是必須的:在cs領域,*nix | win作業系統下任意程式設計語言中,整數除法運算中除數為零的情況。
答案并不是固定的,在不同的作業系統,不同的程式設計語言,甚至不同的編譯器下,答案都可能是不同的。
譬如, 在os x下,使用c語言,clang編譯,引發除零并不會報錯,會傳回一個垃圾值。
同樣的代碼在linux下,使用c語言,gcc編譯,就會引發<code>float point exception</code>。
c++在兩種環境中與c表現是一緻的。至于windows,手頭沒有windows機器且vs隻支援c++,但沒記錯的話/od下windows是會通過seh抛exception的,而/o2則會傳回垃圾值。but who cares windows here….
相比之下python與java在不同的系統中表現是一緻的:
js這種隻有浮點數的奇葩‘巧妙’地用<code>inf</code>繞開了這個問題,就不讨論了。備注:浮點數除數為0是合法的。
如果做過王爽《彙編語言》裡面的小實驗:編寫零号中斷處理程式,就能知道在硬體機器碼與彙編程式設計的洪荒年代裡,異常是怎樣處理的:程式員需要自己寫一段代碼,作為硬體中斷的處理程式。
當然,在沒有作業系統的環境裡,所謂“異常”,其實就是硬體級異常,翻來覆去也就哪幾種:除零、溢出、越界、非法指令等等。異常的種類雖然不多,但想找出異常的原因,或者編寫合适的處理函數确實是相當讓人抓狂的工作。
許多我們耳熟能詳的概念,例如程序與檔案,都是伴随作業系統的發明而引入的。
在擁有檔案概念的現代作業系統中,資料被存放在檔案裡,有獨立的從零開始的尋址空間,程式員隻需要通過檔案路徑就能拿到這坨資料;如果檔案不存在,可以通過open的傳回值-1和全局的errno來判斷究竟是什麼原因導緻了錯誤。想一想這是多麼幸福的事啊!在洪荒年代,整個計算機就那麼一兩個尋址空間,對應着記憶體或者硬碟,資料就放在固定的偏移量,沒什麼所謂的檔案(其實在固定偏移量維護一點中繼資料,這就是所謂的檔案系統了)。如果讀取不出有意義的資料那就隻能報錯挂掉呗,根本沒有所謂的“filenotexistexception”。
除了檔案,程序也是一樣。在沒有作業系統的世界裡,連棧的概念都不存在。控制流的玩弄可以稱得上随心所欲,隻要不越界,不跳到非代碼段,整個世界真是天高任鳥飛,随你怎麼跳。
在洪荒年代裡,異常處理就是處理硬體異常。硬體異常的種類隻手可數,不除零,不越界,不幹蠢事,幾乎可以說是百無禁忌。當然這并不一定是好事,人們往往聲稱向往自由;但在真正的自由面前,很少的人才能把握方向,其他人隻能在無窮的選擇面前感到焦慮迷茫。
程式員們呼喚着新秩序的到來,于是就有了作業系統。
時代在發展,c語言和作業系統出現了,程式員們從洪荒年代進入了遠古時代。終于告别了直接和硬體異常打交道的苦日子。但從c語言的錯誤處理方式中,我們還是能看到那個時代的縮影。
作業系統引入諸多新穎抽象,随之而來的則是各種新穎的異常:檔案打開失敗,程序fork失敗。這些異常,不同于硬體級異常,屬于作業系統的異常。posix标準中很多系統調用使用傳回-1的方式告知調用者出現異常,通過設定全局<code>errno</code>的方式傳遞異常的具體原因。于是我們經常能看見這樣的代碼:
但還有一個問題:原來的硬體異常怎麼辦?
譬如喜聞樂見的野指針越界:<code>segmentation fault</code>:
雖然<code>printf</code>并不是系統調用,隻是一個庫函數。即便如此,發生硬體異常時,庫函數并沒有如同發生普通的作業系統異常一樣傳回-1 ,而是直接coredump給程式員一個surprise~,tada~。
因為這種異常并不是作業系統産生的,作業系統面對硬體異常也要撓頭。怎麼辦?顯然,讓程式員自己編寫0号中斷處理程式是不現實的,作業系統能做的就是把接受這個硬體中斷包裝成一個作業系統的中斷,即“信号”的概念,然後發送給程序。這幾個異常信号程序要是不處理,預設的行為就是挂掉。
但是到了作業系統的時代,編寫除零、越界信号的處理程式往往是沒有太大意義的……,因為程式員在此類異常發生後往往無能為力。不然怎麼着,越界讀寫是準備重試?還是不讀了跳過?除零錯誤是準備加個小的抖動偏移量除出一個天文數字?還是準備拿着垃圾值湊數?如果有這個閑工夫寫這種handler,為啥不在錯誤語句事前加上條件判斷呢……。程式能做到的最好程度,無非是handle sig之後打好日志,保留現場然後老老實實的挂掉……。
是以,在作業系統級(c,c++),我們還是可以清晰地看到硬體異常與作業系統異常處理方式的差異,前者通過信号(linux),後者通過傳回值和錯誤碼。
在linux下c語言處理硬體異常的方式:
c和c++是所謂的“中級”語言,由于标準庫的功能非常有限,在不同作業系統中,程式員還是需要與不少ad-hoc的細節打交道。java的出現可以說解決了(well, at least part of)這一問題。我們可以看到java中整數除0發生的是<code>java.lang.arithmeticexception</code>,看上去和其他異常并沒有什麼不同。隻是所屬的<code>uncheked runtimeexception</code>好像又隐隐地告訴着我們這個異常和其他異常有點不太一樣。
雖然說jvm提供了中間位元組碼的解釋器,但最終jvm還是使用c或彙編将位元組碼映射為系統調用與機器指令。那麼作業系統異常與硬體異常仍然是不可避免的。但是jvm會幫程式員打理好這一切:當發生硬體級異常,比如除零錯誤時,java捕獲sigfpe,sigsegv等異常信号(linux下),并将其轉化為語言内部的異常抛出;相比之下,諸如檔案沒找着這種系統調用失效,也都會被java包裝相應的異常;在java的語言概念中,至少在處理方式上并沒有對這些異常(硬體異常,作業系統異常,應用邏輯異常)進行區分,程式員想捕獲都能用同一種方式來捕捉處理。
世界大同了嗎?java這一類進階語言雖然在形式上消弭了硬體異常、作業系統異常、應用異常的區分,但從語義設計、程式設計規範、工程實踐的方式,卻制定了另一種分類方式:
先來看一下java異常與錯誤的繼承關系。這個繼承樹中有三大類葉子節點:
<code>error</code>,<code>runtimeexception</code>,<code>blahblah...exception</code>。
<code>blahblahexception</code>就是程式或者庫定義的普通異常,需要顯式在代碼中處理。
<code>error</code>是jvm運作時産生的緻命錯誤,不允許去處理。不過實際上去catch throwable也是可以的……。
<code>runtimeexception</code>,又稱為<code>unchecked exception</code>。是不推薦程式員去捕獲的異常。
事實上我們可以還原出這樣對異常分類的設計初衷,如下表所示:
原因能否處理
程式員能處理的(checked)
程式員處理不了的(unchecked)
設計缺陷
假命題
runtimeexception
操作失效
普通exception,需要顯式處理
error
老朋友除零異常換了身馬甲:<code>java.lang.arithmeticexception</code>藏在了<code>runtimeexception</code>中。
程式員能處理的設計缺陷,本身就是一個沖突的陳述。
程式員能處理的操作失效,就是java中普通的異常。這類異常設計的初衷就是提供一種fancy的控制流,程式員在調用鍊條中玩起抛繡球遊戲,讓錯誤處理變得友善一些。
程式員處理不了的設計缺陷,屬于所謂的<code>runtimeexception</code>。這一點需要解釋一下:大家都知道防止npe是程式員的基本修養。除非文檔顯式指明,拿到參數或者傳回值,首先要做的就是檢查是否為空。同理,程式員也有義務在邏輯上保證除法的除數不為0。如果程式員沒有這麼做,那麼這就是一個設計缺陷。任何硬體異常,或者可能導緻硬體異常的條件(譬如:除0,數組越界、野指針、棧溢出),都應當在運作時抛出<code>runtimeexception</code>。
程式員處理不了的操作失效:另一方面,<code>jvm</code>本身也是一個程式。人固有一死,程式固有一挂。無論是因為jvm自己的bug也好,還是環境條件不符合預期,當jvm陷入嚴重錯誤時,程式員對此是毫無辦法的(自己去改jvm不算!),這類異常是所謂的程式員處理不了的操作失效,即<code>error</code>。
對于程式員處理不了的異常,java處理為<code>unchecked exception</code>,也就是無需在函數簽名後顯式列出此類異常。這很好了解,如果這類異常需要指明,那每個使用到指針和除法的地方都可能會抛異常,也就是說幾乎每個函數都要在簽名後面加上<code>throws runtimeexception</code>,蛋疼無比。是以<code>uncheck</code>是<code>runtimeexception</code>所必須的性質。
從程式員的視角,異常分為兩種:能處理的應用異常,處理不了的運作時異常
應用異常是程式員或庫作者所使用的錯誤處理方式。這種異常設計就是為了被捕獲處理。
運作時異常屬于系統異常,産生原因應當包括兩個:應用設計缺陷導緻的硬體異常。環境條件導緻的jvm或者crt嚴重操作失效。不管怎樣,這種異常設計就是為了讓程式趕緊當掉避免造成更大損失的。
從異常的原因來說,異常分為:設計缺陷與操作失效
設計缺陷是因為程式員或者庫作者的考慮不周導緻的,應當立即挂掉暴露出錯誤來。
操作失效是因為環境條件不滿足導緻的異常,不太嚴重的操作失效是可以搶救一下的,例如io timeout可以等一段時間重試幾次,不行再挂掉,或者可選步驟出錯可以直接跳過。嚴重的操作失效,比如jvm自己尿了,那就沒辦法了,早死早超生吧。
除零會發生什麼呢?
在intel x86_64 linux下:
cpu執行div指令,遇到操作數為0,産生0号中斷(#de)
linux核心捕獲0号中斷,給相應程序産生一個sigfpe (8)
程序接受到信号
不處理:産生coredump
程式自行處理:例如c中注冊sigfpe信号的handler,實作異常捕獲。
運作時壓制:比如一些c運作時就偷偷忽略或者壓制了這個異常,提着垃圾高高興興回家了。(c++标準中,整數除以0是未定義行為,讀者可以自行實驗。)
運作時包裝并抛出:java和python的運作時接受到信号後,轉換為相應的語言内異常抛出。runtimeexception一般不捕獲,是以一般來說程式就挂了。