標題實在下得很爛,但不知道怎麼下比較好。
這篇主要是回答 PTT 上 C_CPP 板,有 Windows programming 的初學者在問 C 程式的命令列為什麼不吃 Unicode 的問題。
這個問題其實是很大的。我的回答可能也不是很清楚。但好歹是我花時間寫的東西,就留在這邊記錄一下吧!
首先,我複議 lwecloud(註:另一位鄉民)的說法:都已經 2023 年,Windows 都是 NT-based 的了,不要再用 MBSC/ANSI 了。
你要做的事,不是把 compiler option 改成 MBSC,而是要想為什麼你在 Unicode 下編譯/執行有問題?
※ 引述《xavier13540 (柊 四千)》之銘言:
我最近在用Johnson M. Hart的書學windows的系統程式設計
書上給出了這份使用CreateFile()的程式碼 簡單實作linux上的cp指令
https://ideone.com/P9q9SD
C++ 標準的 main()
prototype 為:
int main(int argc, char* argv[])
依照這個 prototype,argv
是一個 char*
的陣列。其中的每一個 char*
,都是一個指向「ANSI 字串」的指標。
這個情況下,argv[1]
代表你的第一個參數 ("a.txt"
)。
記憶體示意圖如下:
右邊那些文字字元,一格代表一個 byte (8-bit)。後面 "???" 的意思是:你不能保證後面是什麼。這可能會引發一些安全性的問題(後面會提到)。
可是你的 main()
宣告變成:
int main(int argc, LPTSTR argv[])
當編譯器的設定為 Unicode 時,LPTSTR
最終會被展開為 wchar_t*
。(註:wchar_t
在 Windows 系統中長度為 16-bit;在 Linux 中則是 32-bit)所以此時,(編譯器會認為)argv
是一個 wchar_t*
的陣列。
但是,你的記憶體分佈還是跟前面那張圖一樣!
這個時候,編譯器會認為你的 argv[1]
指向一個由「Unicode 字元」組成的字串。所以,程式在解讀你原本的 char
字串中的每「兩個 8-bit 字元」,組成 16-bit 資料,當成一個 UTF-16 字元來解譯!
如果用 VC++ 的 debugger 來觀察:
這就是你傳入 CreateFile()
中的字串。想當然爾 "File Not Found"。
而且,除了內容錯誤外,這個字串還有另一個安全性問題:wchar_t
字串的結尾也是 '\0'
,但長度是 16-bit(也就是 0x0000
)。所以 ANSI 的 8-bit '\0'
(也就是 0x00
)無法結束字串。可以參考 debugger 那張圖的範例,argv[0]
其實還沒有結束,會一直延續到記憶體有 0x0000
的地方為止!
如果處理不當的話,這個字串可能會存取到不該存取到的記憶體,造成安全問題。
=======================================
你也許會想:為什麼我的 argv
用錯型別,但編譯器還是給我過?
老實說,我也沒有很好的答案。我只知道 C/C++ 對於 main()
參數的型別檢查,一向非常寬鬆。
那如果我們把 LPTSTR
換回 char*
呢?
我想你也試過了,這樣是不行的。原因在於你想把 argv[0]
傳入 CreateFile()
。CreateFile()
其實是個巨集,在 Unicode build 下,它會展開為 CreateFileW()
。從這個 API 的說明文件中可以看到,它的第一個參數為 LPCWSTR
,展開後是 const wchar_t*
。你想要把 char*
傳進去,編譯器不會給過的,因為這並不符合隱式轉換的條件。
=======================================
那為何選 MBSC 就會過呢?
當你選 MBSC(嚴格說是「非 Unicode」)的時候,LPTSTR
就會展開為 char*
,而 CreateFile()
也會被展開為 CreateFileA()
。此時的第一個參數就是 LPCSTR
,展開為 const char*
。這樣就可以過了。執行起來也沒有問題。
但是選 MBSC 有什麼問題?
第一,你沒有辦法處理 Unicode 的檔名。我其實不太清楚 Windows 內部的原則,但依據我的實驗,如果你用 char* argv[]
去接參數,中文的參數會以 Big-5 的編碼傳到程式中,日文則會是 Shift-JIS。這樣的資料如果不經處理直接傳進 CreateFileA()
會不會正確執行?我不敢保證。
第二,現在的 Windows 核心都是用 Unicode 來處理的。雖然 Windows 提供你 A
結尾版本的 Win32 API,但其實內部也只是幫你轉成 Unicode,再去呼叫 W
結尾的版本。效率一定比較差。Windows 保留 A
結尾版本只是為了向前相容,非必要建議不要再使用了。
=======================================
所以,如果你想處理命令列參數,最好的方法,應該是改用 wmain
。或至少,改用 _tmain
,然後編譯選項選 Unicode。
(其實如此一來,_tmain
就會被展開成 wmain
了)
如果你堅持使用 main
(而且 argv
型別為 char* []
),那其實,你是還可以用 CreateFileA()
....... 大不了跟使用者說:「路徑或檔名有 Unicode 的話我不支援喔!」XDDDD