控制檯程序的中文輸出亂碼問題,printf,wprintf與setlocale

#include <stdio.h>
#include <wchar.h>
int main(void) {
    char str[] = "中文";
    wchar_t wstr[] = L"中文";
    printf("1:%s\n", str);
    wprintf(L"2:%s\n", wstr);
    return 0;
}

Windows平臺下VS2008輸出:

Windows平臺下MinGW輸出:

當加上setlocale函數設定後,

#include <stdio.h><span style="color: rgb(255, 0, 0); ">
</span>#include <locale.h>
#include <wchar.h>
int main(void) {
    setlocale(LC_CTYPE, "");
    char str[] = "中文";
    wchar_t wstr[] = L"中文";
    printf("1:%s\n", str);
    wprintf(L"2:%s\n", wstr);
    return 0;
}

輸出分別爲:

   

 

爲解其中各種紛亂的糾結,又讓我一個美好的下午就此悲劇=  =.

=============================================================分割線

這檔子事還得從字符編碼說起.關於字符集和編碼的基礎知識,請看咱昨天寫的 字符集相關知識的簡單總結.

這裏涉及到一個字符在源代碼(文本)中,編譯好的二進制文件中,以及最後控制檯輸出編碼形式的區別.

首先,要明確一點:C(語言/程序)並不理解ANSI,UTF-8以及任何其他編碼.它只知道處理你給它的字符二進制表示.

在簡體中文Windows下,默認的文本保存編碼是ANSI(即GBK);Linux下根據系統locale設定,一般應該是(zh_CN.UTF-8).(以下基於簡體中文Windows)

1)對於源文件中保存的"中文"這個字符串,VS2008看到的就是"0xd6d0"和"0xcec4"的形式(默認ANSI編碼得到).但編譯器纔不管是不是GBK神馬的,它就管那串數字.

區別,MinGW看到的是"0xe4b8ad"和"0xe69687"(gcc默認UTF-8).注意,用MinGW編譯的源文件中有中文寬字符必須保存爲UTF-8編碼.

2)然後,在二進制文件中的存儲形式,對傳統的字符串(char str[] = "中文";),編譯器什麼都不做,直接把那串數字(如"0xd6d0","0xcec4")搬過去塞進二進制文件.

但對於寬字符串(wchar_t wstr[] = L"中文";),編譯器會將其做轉換,轉換成Unicode編碼格式(在Windows是UTF-16,而Linux下是UTF-32).如"中文"的16位Unicode是"0x4e2d"和"0x6587",然後把這串轉換後的數字("0x4e2d","0x6587")塞進二進制文件中.(這裏VS和MinGW做的沒有區別)

這裏有點需要注意,編譯器必須知道你的源文件保存的編碼!如VS默認是ANSI編碼,如果你用UTF-8保存.c源文件去用VS打開看一定是亂碼.同理如果你用mingw編譯ANSI編碼保存的源文件,也會出錯!(但可以修改編譯選項解決,見文章末尾) 在本文這裏這個原因其實很好理解,因爲編譯器需要知道,如果它要將一個保存在文件中的字符轉成寬字符時,是從什麼編碼轉到Unicode.(可見上述VS是GBK->Unicode,而MinGW是UTF-8->Unicode)

來小結下"中""文"的3種編碼:

ANSI(GBK): 0xd6d0  0xcec4

UTF-8: 0xe4b8ad 0xe69687

Unicode: 0x4e2d 0x6587

到這裏,一切都還正常~ 

3)控制檯的輸出是問題關鍵!在簡體中文Windows下的控制檯顯示環境是ANSI編碼(代碼頁936, GBK),先明確這點.

對於傳統字符串輸出printf("%s\n", str);程序運行時,直接將二進制文件中存儲的那串數字丟進輸出流.到這裏,你該發現了吧:str保存在文件中是GBK,存儲在二進制文件中是GBK,到控制檯的輸出環境也是GBK!三者一致,自然輸出正常.(當然,如果你修改三者中任一的一個編碼,輸出結果都會不一樣)

但對於寬字符串呢,wprintf(L"%s\n", wstr);會怎麼做?wprintf會先二進制文件的Unicode編碼那串東西轉成本地區域編碼,然後丟進輸出流.哦!這本地區域編碼程序是怎麼得到就成關鍵中的關鍵了.這時咱們來看看setlocale這個函數吧.(看這裏看這裏>o<)

setlocale是用來程序運行時,設置當前的區域信息. 函數參數格式這裏就不介紹了,請看上面鏈接或Google.

值得注意是: 在所有C程序啓動前,locale的默認設置setlocale(LC_ALL,"C");會被執行.

那"C"是什麼環境呢?

The "C" locale is the minimal locale. It is a rather neutral locale which has the same settings across all systems and compilers, and therefore the exact results of a program using this locale are predictable. This is the locale used by default on all C programs.

其實這麼看咱也沒弄懂"C"具體是個啥區域環境,暫且鑑定爲是指那個只認128字符的編碼環境吧.(反正它不認中文=  =)

所以,輸出時Unicode編碼默認轉成這個C環境編碼,然後丟進輸出流.而控制檯的顯示環境默認是GBK啊,這不就亂了嗎!所以亂碼啦~

解決辦法就是在程序中加上setlocale(LC_CTYPE, "");

LC_CTYPE表示C字符串相關的處理.而雙引號中是對應的locale字符串,如果什麼都不寫就從當前系統獲得默認的環境編碼.當然你也可以手動寫成setlocale(LC_CTYPE, "chs"); 一樣的.

這時候,程序輸出時將Unicode編碼的字串轉成系統的默認編碼(Windows下是ANSI),而Windows系統默認編碼一般都與控制檯環境編碼一致,OK~正常輸出了.

 

等等!在加了setlocale函數後的VS2008兩個"中文"都輸出正確了,而MinGW怎麼第一個卻還是亂碼"涓枃"?! 這是當然啦,忘了嗎?MinGW的源文件保存的編碼格式是UTF-8啊.並且程序將文本保存的UTF-8編碼(0xe4b8ad和0xe69687)塞進二進制文件中,輸出時也沒做轉換,又直接將那串UTF-8編碼丟進輸出流,在GBK環境的控制檯輸出,實際過程就相當於UTF-8==>GBK,要知道UTF-8與GBK可不兼容啊,這樣輸出顯示的結果註定是亂碼啊!

一切都清晰了是不是~

 

=============================================================又見分割線 

 在<淺談C中的wprintf和寬字符顯示>一文中,指出在Linux平臺下

    wchar_t wstr[] = L"中文";    
    setlocale(LC_ALL, "zh_CN.UTF-8");        
    wprintf(L"%s/n",wstr);

這樣依然存在輸出亂碼問題.

這個問題的原因在於wprintf的格式化參數%s.應該呢,在標準C中,格式化參數%s表示普通字符串(char*),%ls表示寬字符串(wchar_t*)(貌似%S也可以表示寬字符串,但在C標準中已被拋棄了,迴避之)

所以Linux下應該用wprintf(L"%ls/n",wstr);纔可以正常輸出.

而wprintf(L"%s\n", wstr)的%s是將wstr當作多字節字符串,通過調用mbrtowc()函數轉換成Unicode編碼,再交給wprintf輸出. 可wstr本來就是Unicode編碼字符串,被當成MBCS編碼再轉換成Unicode,這多的一步處理使字符串的內容全亂了.

文章中也說明了Linux下輸出寬字符串,未必非要是wprintf,用printf("%ls\n", wstr)也可以. %ls將wstr當作寬字符,通過調用wcrtomb()函數轉換成多字節編碼(這裏是UTF-8),然後交給printf輸出.所以最後依然顯示正確.

而Windows下用%s和%ls都可以正確輸出.其區別我猜想應該是Windows下C運行庫(CRT)與Linux下的The GUN C Library(glibc)實現不同所致.

CRT太特立獨行,Linux下glibc的實現我覺得應該更符合C標準吧.

 

=============================================================再見分割線 

最後附加:

之前提到過Windows下MinGW編譯的源文件有中文寬字符時,必須是UTF-8保存的(沒有中文的話就隨意啦).否則若源碼中有中文的寬字符變量時編譯會出錯: "converting to execution character set: Illegal byte sequence".(原因之前也說過了,是因爲要讓編譯器知道是從什麼編碼轉到Unicode的)

當然如果你執意要保存ANSI編碼,那麼可以指定gcc的輸入文件的編碼參數-finput-charset. (如-finput-charset=GBK)

同樣也能指定gcc的輸出編碼參數-fexec-charset. (如-fexec-charset=GBK  這樣之前那個"中文"顯示""涓枃"就也能得到正常輸出啦)


原文鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章