第 14 講:運(yùn)算符(六):溢出校驗(yàn)運(yùn)算符
1-1 溢出的概念
C# 里出現(xiàn)了一種新的處理過程,叫做溢出校驗(yàn)(Overflow Checking)。在數(shù)據(jù)類型的內(nèi)容里,我們說到,任何一種數(shù)據(jù)類型都是具有有限的存儲(chǔ)空間的。既然空間有限,那么自然數(shù)據(jù)存儲(chǔ)就是有限的。比如說 byte
,最大只能到 255。如果我們有這樣的一種情況:
我們嘗試對 a
繼續(xù)增大一個(gè)單位,會(huì)如何呢?這個(gè)情況被稱為溢出(Overflow)。但是很遺憾的是,在 C 語言里,數(shù)據(jù)溢出是未定義行為
那么,說這個(gè)溢出是未定義行為,是什么意思呢?就是說,有可能這個(gè)編譯器會(huì)對溢出情況這么做,但別的編譯器可能那么做,總之做法不同。既然沒有統(tǒng)一的處理模式,那么代碼上就是不嚴(yán)謹(jǐn)?shù)?。因此,C# 將這種溢出完善了。
在 C# 里,我們有兩種處理模式:
沒有溢出校驗(yàn):將數(shù)值從這個(gè)數(shù)據(jù)類型的最大值直接改成這個(gè)數(shù)據(jù)的最小值,就認(rèn)為它增大了一個(gè)單位;
啟用溢出校驗(yàn):當(dāng)溢出的時(shí)候,直接產(chǎn)生一個(gè)運(yùn)行錯(cuò)誤。
大多數(shù)時(shí)候,直接產(chǎn)生運(yùn)行錯(cuò)誤是沒必要的,因?yàn)槲覀兌加貌坏竭@種“邊界處理”。但 C# 仍舊提供了這種行為。比如說我寫了一個(gè)計(jì)算器的程序。如果數(shù)據(jù)在計(jì)算的時(shí)候超出了數(shù)據(jù)能表示的范圍,我們肯定希望用戶可以看到“數(shù)據(jù)超出計(jì)算范圍”的信息。如果選擇前者處理模型的話,由于它不會(huì)產(chǎn)生錯(cuò)誤信息,因此我們根本不可能從代碼層面實(shí)現(xiàn)“讓用戶看到運(yùn)算超出范圍”的結(jié)果。在這種情況下,我們可能會(huì)考慮選擇后者。
1-2 沒有溢出校驗(yàn)的處理,為什么會(huì)從最大值改成最小值?
C# 采用了一種容易電腦處理的機(jī)制。我們拿 byte
類型舉例。byte
只需要 8 個(gè)比特位,如果是 255 的話,這個(gè)數(shù)值一定是 1111 1111
。當(dāng)給這個(gè)數(shù)值增大一個(gè)單位的時(shí)候,數(shù)字會(huì)變成 1 0000 0000
。但是很顯然,byte
類型只能使用 8 個(gè)比特位,那么超出來的最高位的 1 就必須得舍棄。因此這個(gè)數(shù)值就變成了 0000 0000
;而這個(gè)數(shù)字就是 0。這不就是從最大改成最小了嘛。
所以,對于一個(gè)整數(shù)類型來說,如果對這個(gè)數(shù)據(jù)類型的最大值再增大一個(gè)單位,就會(huì)將這個(gè)數(shù)字變成這個(gè)數(shù)據(jù)類型的最小值;反之的話,如果這個(gè)數(shù)字是這個(gè)數(shù)據(jù)類型的最小值,那么減小一個(gè)單位,自然就變成最大值了:因?yàn)闇p 1 的話不夠減,就會(huì)瘋狂從高位借位,然后就全部比特位都得借,因此就變成了最大值了。
在說完前面的內(nèi)容之后,下面我們來說一下如何使用這個(gè)溢出校驗(yàn)的功能。
Part 2 啟用溢出校驗(yàn)
啟用溢出校驗(yàn)是通過項(xiàng)目配置文件來啟用的。為了學(xué)習(xí)簡單,我們使用較為簡單的配置方式:使用界面。
首先,我們找到解決方案資源管理器,單擊項(xiàng)目。

在點(diǎn)擊之后,會(huì)彈出設(shè)置頁面。當(dāng)然,你依然可以在選中項(xiàng)目后,點(diǎn)擊右鍵,選擇“Properties”(屬性),進(jìn)入設(shè)置頁面。

在進(jìn)入配置頁面后,找到“Build”(生成)選項(xiàng)卡下,右下角的“Advanced...”。

最后選擇啟用溢出檢查就可以了。

這樣就完成了啟用溢出校驗(yàn)。
順帶一提。如果沒有這里的這些個(gè)步驟的話,整個(gè)項(xiàng)目里的代碼是不會(huì)檢查溢出的。換句話說,即使產(chǎn)生了溢出,也不會(huì)產(chǎn)生錯(cuò)誤,而是使用默認(rèn)的計(jì)算邏輯(就前面那個(gè),最大變最小,最小變最大的那個(gè));如果啟用了的話,整個(gè)項(xiàng)目的所有加減乘除模這些會(huì)改動(dòng)變動(dòng)數(shù)據(jù)數(shù)值的過程都會(huì)在運(yùn)算結(jié)果的過程期間校驗(yàn)是不是會(huì)溢出。如果溢出就自動(dòng)產(chǎn)生錯(cuò)誤,讓程序崩潰。
Part 3 checked
和 unchecked
關(guān)鍵字
下面我們的重頭戲就來了。我們使用 checked
關(guān)鍵字或 unchecked
關(guān)鍵字專門用來控制代碼是否需要校驗(yàn)溢出。前文我們介紹了如何配置項(xiàng)目啟用校驗(yàn)溢出的過程,這里我們來實(shí)際使用一下。
先來說一下功能:
checked
關(guān)鍵字:用于一個(gè)表達(dá)式或一個(gè)代碼塊,表示這個(gè)表達(dá)式或者代碼段落都啟用溢出檢查。如果前文我們配置溢出的過程已經(jīng)完成,那么項(xiàng)目里面就不必使用這個(gè)關(guān)鍵字;unchecked
關(guān)鍵字:用于一個(gè)表達(dá)式或一個(gè)代碼塊,表示這個(gè)表達(dá)式或代碼段落不啟用溢出檢查。如果前文我們配置溢出的過程沒有做的話,那么項(xiàng)目默認(rèn)就是不檢查的,因此我們不用使用這個(gè)關(guān)鍵字。
我們先來試試寫這么一段代碼:
或者,如果覺得這么寫很丑,你可以這么寫:
這兩種寫法都是沒有問題的。前者這樣寫的話,大括號(hào)里面的部分,只要參與運(yùn)算的地方,都會(huì)檢查溢出;而后面這種,就只對 ++a
檢查是否溢出了。當(dāng)然,實(shí)際上我們可以看到,就算寫成前面那樣,我們也知道里面就只有 ++a
需要校驗(yàn)是否溢出,因此這兩個(gè)寫法是等價(jià)的。
當(dāng)然,如果你配置了項(xiàng)目溢出檢查的話,
checked
是可以不用的。這一點(diǎn)前面已經(jīng)說過了。
如果我們嘗試運(yùn)行程序。我們確實(shí)看到了程序崩潰,并產(chǎn)生了一個(gè)錯(cuò)誤:

如果不檢查溢出,我們可以使用 unchecked
關(guān)鍵字來避免檢查。
這樣的話,你就可以得到 0 這個(gè)輸出結(jié)果,并且沒有錯(cuò)誤信息。

Part 4 總結(jié)
checked
和 unchecked
關(guān)鍵字的用法。用來標(biāo)記一個(gè)表達(dá)式(或一個(gè)代碼段),來表示這個(gè)表達(dá)式(或代碼段)是否啟用溢出檢查。checked
是啟用,而 unchecked
則是不啟用。
說句實(shí)話,初學(xué)沒有必要搞很清楚,能懂個(gè)大概就可以了。