字串與數值互相轉換是非常常用的功能,大家都對它習以為常了。我想除了程式庫的編寫者之外,沒有人會像我這樣為了這個問題糾結一兩天。
C提供了一套函數用於字串與數值互轉換,包括itoa,atoi,strtol等。為了方便敘述,我將這套函數抽象成下面兩個偽C函數:
string C_IntToStr(int value, int radix);int C_StrToInt(string str, int radix);
根據函數名稱就可以知道它們的用途。注意本文只討論整型值,浮點數不在該範圍內。
對於十進位的轉換來說,這兩個函數的行為很正常;對於非負數的非十進位轉換來說,也很正常。這都沒什麼好說的。可是對於負數的非十進位轉換來說就不一樣了。先看看下面的調用:
string str = C_IntToStr(-1, 16);
執行之後str的值是“ffffffff”。嗯,一切都沒有什麼問題。
在我的理想世界中,C_IntToStr和C_StrToInt應該是相互可逆的,也就是說,經過下面的調用之後:
int a = -1;string str = C_IntToStr(a, 16);int b = C_StrToInt(str, 16);
a和b應該都是-1。然而實際上,執行了這段代碼之後,b的值是2147483647,恰好是int類型的最大值。如果檢查一下errno,會發現它的值是ERANGE,意味著發生了溢出。
在這裡會產生很多疑惑:ffffffff不就是-1嗎?為什麼會溢出?為什麼傳回值是2147483647而不是-2147483648?……
回答這些問題之前,要先明確一個事實:我們之所以認為ffffffff等於-1,是從電腦科學家的角度來看的。我們都知道在電腦內負數是用補碼來表示的,-1的補碼形式是所有位都是1,對於32位的int類型來說,轉換成十六進位就是ffffffff。不知道大家有沒有注意到,在討論電腦內部儲存的時候,是根本沒有正負數概念的——所有數值都是無符號的,負數只是通過一種特殊的方式來表示。所以,我們很自然地把ffffffff看作是電腦內部的儲存方式。
如果從數學家的角度來看(假設他不瞭解電腦),十進位的-1轉換成十六進位也是-1,ffffffff是十進位的4294967295,而-ffffffff是十進位的-4294967295。從純數學的角度看來,數值都是有正負之分的,無論它用何種進位來表示。
因此,我們得到了字串與數值互相轉換的兩種語義:電腦語義和數學語義。電腦語義認為,除了十進位之外,其它進位的字串都表示數值在電腦內的儲存方式;數學語義則認為所有進位的字串都表示這個數值本身。在電腦語義中,之所以要把十進位與非十進位區分開來,是因為用其它進位來描述的儲存方式最終目的都是為了表示十進位數,而且正負數本身就是針對十進位數的——這意味著其它進位的字串不能帶有負號。
如果C_IntToStr和C_StrToInt使用了相同的語義,那麼它們就是相互可逆的。遺憾的是,它們恰好使用了不同的語義:C_IntToStr使用的是電腦語義;C_StrToInt使用的是數學語義。所以,使用C_IntToStr永遠不會得到帶有負號的非十進位字串;使用C_StrToInt時,如果不在字串前面加個負號,永遠不會得到負數。
現在可以解答上面提出的三個問題了。從電腦語義來看,C_IntToStr(-1, 16)得到“ffffffff”是顯然的。從數學語義來看,ffffffff是4294967295,大於int類型的最大值2147483647,所以C_StrToInt("ffffffff", 16)會判斷溢出。最後,如果發生溢出的話,C_StrToInt會根據字串是否有符號來返回int類型的最小值或最大值:“ffffffff”是正數,所以返回最大值2147483647;如果是“-ffffffff”,則會返回最小值-2147483648。
上面的分析基於MSC,我不知道其它C運行庫的情況如何。我正在寫一個C++函數庫,其中有ToString和FromString函數,封裝了C的相應函數。一開始C_IntToStr和C_StrToInt的這種不一致性給我帶來了非常大的困惑,我為這個問題糾結了好久,現在總算把它們理清了。
最後嘮叨一下,如果自己要寫一套字串與數值互相轉換的函數,一定要明確好所使用的語義,切勿混用。要是必須支援兩種語義(不太可能會有這種需求吧!),最好的做法是提供兩套這樣的函數,一套對應電腦語義,一套對應數學語義。使用電腦語義的時候,還要注意類型位元組大小的問題,因為數實值型別不只int一種,還有char,short甚至long long。