程式碼用什麼格式儲存?還用問,當然是「純文字」囉。其實這個問題並不簡單,所謂的「純文字」可能是眾多編碼當中的一種:

  • 「本地」使用的編碼:例如 Big5、GBK、Shift-JIS、ISO-8859-1,2,... 等等。Windows 下的編輯器常以此為預設。
  • UTF-8:一些 Linux 的編輯器可能以此為預設。
  • UTF-16、UTF32:這類編碼方式非常不適合用來儲存程式碼,這裡不討論。

對於只用 ASCII 字集寫程式的人來說,用什麼編碼儲存程式碼應該都沒有差別。但有很多人會在註解和 string literal 裡面放其他語言的字集,這就是個問題。別誤會,我並不是說原始碼當中不能出現非 ASCII 字符,而是說很多人是在欠缺考慮的情形下寫出這種程式碼。

一般來說,編譯式程式語言的 string literal 必須通過兩個關卡:

  1. 編譯階段:首先編譯器必須分析 string literal 真正的內容是什麼,以及代換 escape sequence;然後內容會被重新編碼,留待編譯後期分配儲存空間。最後產生的字串資料有可能保持和原始碼相同的編碼方式,但也有可能轉成另一種編碼,要看編譯器而定。舉例來說,當 Visual Studio 使用者寫出 _T("中文") 這樣的片段時,大概都沒意識到自己是用 Big5 存檔,編譯器生成的字串資料卻是 UTF-16。
  2. 執行階段:不論編譯器如何處理原始碼中的 string literal,在目的檔裡面就只是一串 bytes。當應用程式把字串丟給底層程式庫、系統 API 時,如果這串 byte 剛好符合底層程式庫、系統 API 的編碼方式,一切都沒有問題;但若是編碼方式不合,輕則被解釋成亂碼,重則造成操作失敗。

再仔細看編譯階段,其中牽涉到兩方面的編碼方式:

  • 其一是「原始碼的編碼方式」,這會影響到編譯器如何讀取所有的語彙元素,例如識別名稱、關鍵字、註解、string literal 等等;
  • 其二是「文字經過處理後的編碼方式」,受影響的大概只有 string literal 和 character constant。

前者在 C/C++ 的術語裡稱為「Source character set」,後者則稱為「Execution character set」,標準規格書裡面有規定兩者最底限的字集。其他程式語言應該也有的機制,不過我沒有深入研究。

易爆物

接下來我想實際操作比空談更容易理解,首先感謝杜子美先生提供的測試案例:

// in ex1.c -- encoded in Big5
...
printf("功蓋三分國,名成八陣圖。\n");

先用Big5儲存程式碼,然後在 Windows 上使用 MinGW gcc 編譯:

gcc ex1.c

在預設的情況下 gcc 會假設原始碼採用 UTF-8 編碼,但就算假設錯誤,gcc 仍然會繼續以 byte 為單位讀入原始碼。因「功」和「蓋」含有「\」標記,卻沒有緊跟著合法的 escape sequence,所以編譯過程中會發出 warning。雖然最後還是可以產生執行檔,但是輸出結果顯然和預期的不太一樣。

這純粹是編譯方式搞的鬼,如果在執行階段才從外部讀入「功蓋三分國...」,編譯器沒有插手的餘地,這個現象就不會發生。

前面提過 Source character set 還會影響其他語彙元素,這裡舉個邪惡的例子:

// in ex2.c -- encoded in Big5
...
if (done) { // 執行成功
    printf("ok.\n");
    ...

若上面這段程式採用 Big5 編碼,並且和前面一樣用 gcc 預設值編譯,「功」這個字的第二 byte 就是「\」,在 C/C++ 裡面代表延續註解到下一行,所以接下來的 printf("ok.n"); 也會被視為註解的一部分。

gcc 的字集選項

其實會有上面這些問題,都是因為使用者不熟悉 gcc 字集設定的緣故。gcc 各階段的字集由這幾個選項控制:

-finput-charset=charset
輸入檔的字集,即前述 Source character set,預設為 UTF-8。
-fexec-charset=charset
經處理過的 string literal 和 character constant 最終會被轉換成的編碼方式,即前述的 Execution character set,預設也是 UTF-8。
-fwide-exec-charset=charset
類似前者,但影響的是寬字元的 string literal 和 character constant,像是 L"wide string"。預設值可能是 UTF-16 或 UTF-32,視 sizeof(wchar_t) 而定。

所以只要編譯時加上「-finput-charset=Big5」,gcc 就會以 Big5 字碼做為讀取單位,前述 ex2.c 邪惡註解的問題果然就不再發生了。

但在同樣的編譯設定下,「功蓋三分國...」似乎被輸出成一堆怪東西,因為這次 gcc 終於讀懂string literal 的內容了,所以有辦法正確轉換成預設的 Execution character set,即UTF-8。反過來說,如果我們一開始使用 UTF-8 儲存程式碼,並且採用 gcc 預設值編譯,也會發生同樣的情形。

我們寫的程式很忠實地把 UTF-8 字串丟給 console,但 console 卻不認得,所以顯示成亂碼,這個情形就像以前 DOS 時代必須先執行倚天系統才能顯示中文字型一樣。只要先用指令「chcp 65001」切換 console 內碼表至 UTF-8,然後再執行前面的程式,那麼「功蓋三分國...」就能被正常顯示。

若不想做「chcp 65001」這個動作,也可以在編譯時另外指定「-fexec-charset=Big5」,那麼 gcc 就會改用 Big5 做為 Execution character set。再做個實驗以凸顯 Execution character set 的影響,同樣使用 Big5 做為原始碼的編碼方式:

// in ex3.c -- encoded in Big5

char msg[] = "白日依山盡";
printf("%u\n", sizeof(msg));

如果用以下選項編譯,結果會顯示 sizeof(msg) 的值為 16,這表示每個中文字碼被編成 UTF-8 的 3 byte,而非 Big5 的 2 byte。:

gcc -finput-charset=Big5 ex3.c

改用下面方式編譯的話,結果會顯示 sizeof(msg) 的值為 11:

gcc -finput-charset=Big5 -fexec-charset=Big5 ex3.c

這告訴我們,如果有人喜歡在 string literal 裡面使用中文,配置記憶體時又老是以每個中文字佔 2 byte 為前提,那麼在運氣很背的情境下有可能撞鬼。

以上僅以 gcc 為例,其他編譯器概念上是相同的,移植程式碼時應多留意相關設定。C++11 可直接在程式碼中以 u8 前綴指定 string literal 使用 UTF-8 編碼,如:

char s[] = u8"This is a UTF-8 string.";

就我的理解,其他的前綴如 L、U、u 等,指定的只是儲存單位寬度,至於具體編碼方式在標準中沒有明定(頂多算是暗示)。

結論?

其實這裡沒有什麼堪稱結論的見解,畢竟現實中的程式語言、相關工具實在太多樣化了,應該不會有什麼一體通用的方案。

需要解讀原始碼的工具,我能想到的大概就有:編譯器/直譯器、文件生成、程式重構、靜態分析(這是非常大的一類)、程式碼生成等等。另外還有一群比較通用型的工具,如:文字編輯器(通常含一堆非官方支援的 plugin,或可視為獨立程式?)、模板、搜尋、比較、補丁等等,低階工具的多數功能可能不受編碼影響,但難免有踩到地雷的時候。

如果想要在原始碼裡面使用英文以外的語言,確認一下相關工具的支援程度會幫自己省下很多事。如果不確定因素很多,或許比較保守的策略還是乖乖用英文。工具的限制固然是必要考量,同樣重要的是會讀這個程式,畢竟程式碼是寫給人看的。若是連作者自己都不太讀的隨用即丟程式碼,只要跑起來沒問題就好了,註解也沒什麼好寫的,管他的。

至於原始碼的編碼方式,我個人認為第一順位是無 BOM 的 UTF-8,因為 UTF-8 具有某些良好的特性,很多以 byte 為單位的軟體即使沒有特別處理 UTF-8,往往還是能夠得到正確的結果。其中一個惱人的例外是那些不支援非 ASCII 的 Regex 引擎,但同樣的情形發生在 Big5 這類的 MBCS 身上也沒好到哪裡去(GNU Flex 就是這樣的例子,使用者必須額外花心思)。而且支援 UTF-8 的軟體資源會越來越多,其他的「本地」編碼很難說。

這裡提供一個不要 BOM 的理由:Unix-like 環境會根據純文字文件開頭的 #! 標記來選擇該文件的解釋器,這是個很方便的功能,我手邊的 bash、python、gnuplot 等等腳本常會這樣寫,BOM 會破壞這個機制。不幸的是許多 Windows 平台的編輯器並沒考慮到這一層,即使在記事本或 Visual Studio 選擇了 UTF-8 存檔,它們不讓使用者選擇就擅自加 BOM。(不過應該沒有正常人用記事本寫程式吧,世界上有太多專業的純文字編輯器可以選)

arrow
arrow
    全站熱搜

    novus 發表在 痞客邦 留言(2) 人氣()