日前公司同事請我幫忙解決一個數字顯示的問題。客戶有一個讀卡設備,接著 COM 埠上。
他按照客戶提供的規格手冊,從該設備中讀出卡號。但顯示出來的卡號不是他預期的樣子。
我看了他的程式與執行結果後,我第一時間覺得程式和結果都沒錯啊,哪裡有問題。
又聽了一次他的解釋後,才注意到他忘了一項計算機概論的基本觀念:儲存在記憶體中的數值若要顯示成文字,要經過數值轉文字內碼的程序。
他忘了這件事,所以才一直以為是程式有問題。
當我意識到他的錯誤時,我還稍微向他解釋了一下數值與內碼的差異。
不過看他的表情似乎還是有點迷糊,也不知他是否真的理解了。
總之,我最後還是很快地寫好BCD碼轉ASCII碼的函數給他用。
他的情況可簡化為下列敘述:
$n = 12;
// in memory: [0x12]
echo $n, "\n";
// 他預期顯示 12 ,而且他也確實看到 12
$s = "\x00\x12\x34\x56\x78\x90";
// in memory: [00, 0x12, 0x34, 0x56, 0x78, 0x90]
echo $s, "\n";
// 他預期顯示001234567890,但他看到的是 ??4Vx?
// 他認為有問題。
他從客戶的讀卡設備中讀取卡號的方式,就是用 IO 設備的 read 方法,將資料存入資料緩衝區。
而讀入的內容形式,就是像上述程式碼的形式。例如卡號為 12345678,則讀入的資料位元組就是 4 個 bytes,內容為 [0x12, 0x34, 0x56, 0x78]
。
這種儲存形式即 Packed BCD 。
通常用於處理超長數字,因為此形式可以忽視計算機本身的硬體限制,諸如CPU 位元數、記憶體排列順序等限制。
以 12345678 這個數字為例,在 x86 架構的機器中,以整數形式儲存時為 [0x4E, 0x61, 0xBC, 0x00]
。
以 ASCII 編碼儲存時則為 [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]
。
而本案例所使用的 BCD 編碼卻為 [0x12, 0x34, 0x56, 0x78]
。
現在常用的程式語言中不提供 BCD 編碼的處理函數,故若要輸出其內容,就需要撰寫額外的轉換函數。
我分別用 PHP 和 Python 寫了 BCD碼與ASCII碼互轉的函數。
第一個是 PHP 的轉碼程式。
第二個是 Python 的轉碼程式。
這種轉碼程式的定則是「查表比推算快」。我實際測了一下這兩份程式碼,在 PHP 與 Python 版的表現也符合定則,查表快許多。
針對 Python (2.6版),我還另外測過 list 與 tuple 這兩種表的查表速度。令我意外的是,兩者無分軒輊,list 偶爾還比 tuple 快。
我本以為 tuple 應該快些的。
想當年我在學組合語言時,這道程序還是第一個必寫的課題。由於當年學的很辛苦,所以現在腦子已經習慣這麼思考了。
故而我看了他的程式後,只覺得這樣的輸入內容,本來就是這樣的輸出結果,哪有問題。
當我意識到他的錯誤時,我還稍微向他解釋了一下數值與內碼的差異。不過看他的表情還是有點迷糊,也不知他是否真的理解了。
我後來仔細一想,似乎不能怪他不懂這件事。因為現代的程式語言教程中,數值輸出成文字根本不需要理解數值與內碼的差異。
舉個例子來說,若有一整數 n 其值為 123 ,要輸出為文字。學 C 語言,就是記住 printf() 中要填 %d 才能輸出。
學 C++ 的就是用 cout 。學 Java 語言,也只知 println(n) 就可以了。哪需要什麼數值轉內碼的程序。
或許有些用心的書籍作者,還會提示 Java 的 println() 實際上實作了一個不可被覆寫的內置型態轉換行為,會先調用整數轉字串的方法,才把結果傳給 println() 顯示。
既然現代程式語言都把這些細節隱匿起來了,單憑背誦計算機概論中的隻字片語,大概還是無法體會這之中到底做了什麼。
我想只剩下老派的程式設計人員,還會注意到這個細節吧。
樂多舊網址: http://blog.roodo.com/rocksaying/archives/16150047.html
樂多舊回應