淺談C中的wprintf和寬字符顯示

From:http://blog.csdn.net/lovekatherine/archive/2007/11/06/1868724.aspx
今天在CSDN的Blog首頁看到一篇文章“也談計算機字符編碼 ”,由於前一陣業餘翻譯了“UTF-8 and Unicode FAQ for Unix/Linux ”一文,自己對字符集、編碼和Unicode等內容一直保着者很強的興趣,自然不會放過這樣的文章。

作者的文章寫得很明白易懂,雖然有一些概念上的細節問題我覺得有商榷之處;作者還給出一個簡單的在windows下使用wprintf正確輸出字符串“中文”的小例子,我linux下模仿作者給出的示例代碼寫了如下的示例代碼:

#include < cstdio >
#include
< cstdlib >
#include
< clocale >
#include
< cwchar >

int main( int argc, char * argv[])
... {
     wchar_t wstr[]
= L " 中文 " ;    
     setlocale(LC_ALL,
" zh_CN.UTF-8 " );       
     wprintf(L " %s/n " ,wstr);
    
    
return 0 ;
}


這裏需要說明的是我的機器的locale爲"zh_CN-UTF-8"

然而程序的運行結果卻讓我很詫異

whodare@whodare: $ . / a. out
- N


我的第一反應就是作者的示例代碼是不是有問題,畢竟這裏面調用的全都是C的標準庫函數,不應該存在移植性問題;然而,我找了臺windows機器測試作者的代碼,結果讓我很鬱悶,一切正常......

爲 什麼我在Linux下的程序就不對呢?我很不服氣,於是開始以各種關鍵字進行搜索,想看看別人是否遇到過類似的問題。一個搜索結果引起了我的主意,有人說 問題出在wprintf中的格式轉換符上,將%s替換成%ls就沒有這樣的問題。帶着幾分懷疑,我修改了上面的程序,編譯運行後,居然真的就沒問題了

#include < cstdio >
#include
< cstdlib >
#include
< clocale >
#include
< cwchar >

int main( int argc, char * argv[])
...
... {
     wchar_t wstr[]
= L " 中文 " ;    
     setlocale(LC_ALL,
" zh_CN.UTF-8 " );       
     wprintf(L
" %s " ,wstr);
     wprintf(L
" %ls " ,wstr);
    
    
return 0 ;
}


上述代碼的運行結果

whodare@whodare:$ . / a. out
- N
中文


問題解決了,可我還是感到迷茫:格式轉換符"ls"和“s"的區別是什麼?爲什麼原來的程序會出問題?“-N"這個字符串是怎麼冒出來的?爲什麼作者在windows下的程序就不存在該問題?

這麼多的疑惑堵在心口,我哪能心安呢。知其然還要知其所以然嘛!花了一個下午的時間仔細讀了下wprintf的manual,並在gdb的幫助下做了各種試驗,終於算是把我的疑惑基本都解決了。

一、以下的所有試驗都是以“中文”爲例,因此有必要先把它的Unicdoe碼值、UTF-8編碼都列出來,以便於更好的理解下文

‘中’ Unicode碼值:U+4E2D UTF-8 編碼 e4 b8 ad
‘文’ Unicode碼值:U+6587 UTF-8 編碼 e6 96 87

二、我們需要理解用char[ ]和wchar_t [ ]來存放“中文”時有什麼不同

     char      str[] = " 中文 " ;
     wchar_t wstr[]
= L " 中文 " ;    


我們使用gdb這個強大的工具來查看str[]和wst[]中究竟都存放了哪些值(請注意顏色之間的對應關係)

(gdb) x / 8xb & str
0xbf83decd :      0xe4     0xb8     0xad    0xe6     0x96     0x87     0x00      0xf0
(gdb) x
/ 12xb & wstr
   0xbf83dec0:     0x2d    0x4e    0x00    0x00     0x87    0x65    0x00    0x00
   0xbf83dec8:     0x00    0x00    0x00    0x00


不 難看出,char str[ ]中存儲的是“中文"的UTF-8編碼,這是因爲我的機器的locale是zh_CN.UTF-8,程序源文件的自然採用的是UTF-8編碼,因此編譯器 在處理 char str[ ]="中文"; 時,t它對str[]所做得初始化實際上可以理解成    char str[ ]={ 0xe4,0xb8,0xad,0xe6,0x96,0x87,0x00}

而wchar_t wstr[ ]中存放的是“中文"的Unicode碼值,這符合C標準對寬字符的定義。這裏需要解釋的是C標準中規定寬字符是16 bit的字符,而從GNU glibc 2.2開始,類型wchar_t只用於存放32-bit的ISO 10646碼值(你可以粗略的把ISO 10646理解成Unicode,儘管它們並不是一回事),而獨立於當前使用的locale;因此在上面的輸出中,我們看到每個Unicode碼值用 32bit表示,而不是16bit。

三、關於%s和%ls的區別

我搜到了一篇帖子(很傷感,我再此發現在CS領域,最靠的住的資料總是英文的),裏面對各種格式轉換符有詳細的解釋,願意看原文的同學直接忽略本段文字.......

http://www-ccs.ucsd.edu/c/lib_prin.html

首先,%ls和%s的區別很簡單,%ls意味着將對應的參數會被當作基於寬字符的字符串(wide chraracter string )看待,而%s則意味着對應的參數會被當作普通字符串(multi-byte string)看待。

其次,不要因爲上面一句話而錯誤的認爲%s只用於printf,而%ls只用於wprintf 。實際上,(printf, wprintf) 和(%s,%ls)這兩個元組之間是相互獨立的,也就是說它們之間的四種組合都是可以的。

再次,printf用於byte stream,即輸出流中的每個字符顫1 byte;而wprintf則用於wide stream,輸出流中的每個字符不止 1 byte。

說了一堆廢話,還是結合實例來看看%ls和%s的區別吧

例子1 printf + %s + wstr

printf( " %s " ,wstr);

whodare@whodare:$ .
/ a. out
- N


哈,這個鬱悶的"-N"又一次出現!爲什麼會出現呢?讓我來分析一下printf在執行時所完成的操作吧。

這 裏用了%s, printf 就會將對應的參數wstr視爲普通字符串(儘管我們清楚他是個wcs而不是mbs);另一方面,我們已經看到了wstr[ ]的內存佈局,其前3 byte爲 0x2d ,0x4e,0x00。我們都知道C中的字符串以'/0'爲結束標誌,因此printf只會處理wstr[ ]中的前三個byte,而查一查ASCII表,0x2d對應字符'-',0x4e對應字符'N',所以我們會看到”-N"這個詭異的輸出。

例子2 printf + %ls + wstr

printf( " %ls " ,wstr);

whodare@whodare:$ .
/ a. out
中文



使 用了%ls,printf會將對應的參數視爲寬字符串(wcs),而printf又對應byte stream,因此這裏要對寬字符(wcs)進行轉換,變成普通的字符串(mbs)。這裏的轉換是printf通過對每個寬字符隱式的調用wcrtomb ()這個標準庫函數完成的。按麼,wcrtomb()這個函數進行是按照什麼規則進行轉換的?這就是setlocale()的作用所在了,wcrtomb 會依據程序員設定的locale,將wcha_t中存放的碼值,轉換爲相應的的多字節編碼。

回到例子中,我的機器的locale爲zh_CN.UTF-8,對應的編碼爲UTF-8,因此wstr[ ]中存放的Unicode碼值會轉換爲UTF-8編碼的形式輸出到標準輸出流中,這樣採用UTF-8編碼的console就能正確識別受到的字節流並顯示出"中文"

例子3 wprintf + %s +wstr (最初的代碼!)

wprintf(L " %s " ,wstr);

whodare@whodare:$ .
/ a. out
- N



使 用了%s,wprintf會將對應的參數視爲普通字符串mbs,儘管我們還是很清楚它其實是個wcs。wprintf 使用的是wide stream,因此需要將所給的mbs參數轉換爲wcs再由wprintf完成輸出;這個轉換是由wprintf隱式的對mbs不斷調用mbrtowc來 完成,轉換規則依然是和locale相關的。


我們知道wstr的內存佈局爲:
    0x2d    0x4e    0x00    0x00
    0x87    0x65    0x00    0x00
    0x00    0x00    0x00    0x00

該"mbs"的轉換結果爲 L‘0x2d' + L '0x4e' + L '0x00' ,最終輸出結果又是討厭的"-N"

例子4 wprintf + %ls+ wstr

wprintf(L " %ls " ,wstr);

whodare@whodare:$ .
/ a. out
中文




使用了%ls,wprintf會將對應參數視爲寬字符串wcs,這次終於沒有搞錯。因此wprintf會順利的將給定的寬字符串寫入標準輸出流,最終正確顯示"中文"


看完這4個例子,你對wprintf、printf和%ls 、%s的使用還有疑惑麼?

四、小結

    1。要清楚%ls和%s的意義在於指明所期待的參數是何種字符串,而printf和wprintf的區別在於所使用的是不同類型的stream

    2。貌似在linux下輸出“中文"的正確方法是 wprintf( "%ls/n",L"中文") ,而引文中作者在Windows成功操作的wprintf("%s/n", L"中文")在linux無法正確工作,至於爲何wprintf這個標準庫函數在兩個系統下有不同表現,我是無心再向下深挖了,難道這又是VC一處不符合 標準的地方?.......

    3 。貌似還有一個%S,單獨用於表示對應參數是寬字符串

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