浅谈C中的wprintf和宽字符显示【其实很深】

 今天在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,单独用于表示对应参数是宽字符串

       谁能告诉我该问题的答案,不盛感激.......



转自:http://zyxhome.org/wp/cc-prog-lang/c-stdlib-setlocale-usage-note/ 

[在此向原文作者说声谢谢!若有读者看到文章转载时请写该转载地址,不要写我的BLOG地址。尊重他人的劳动成果 ^_^ ]

C 和 C++ 的标准库分别有自己的 locale 操作方法,C 标准库的 locale 设定函数是 setlocale(),而 C++ 标准库有 locale 类和流对象的 imbue() 方法。这篇是我自己的 setlocale() 使用总结。

Linux的glibc中的setlocale()

具体参考:man 3 setlocale

头文件与声明如下:

1 #include <locale.h>
2 charsetlocale(int category, const char* locale);

说明

category:为locale分类,表达一种locale的领域方面,通常有下面这些预定义常量:LC_ALL、LC_COLLATE、LC_CTYPE、LC_MESSAGES、LC_MONETARY、LC_NUMERIC、LC_TIME,其中 LC_ALL 表示所有其它locale分类的并集。

locale:为期望设定的locale名称字符串,在Linux/Unix环境下,通常以下面格式表示locale名称:language[_territory][.codeset][@modifier],language 为 ISO 639 中规定的语言代码,territory 为 ISO 3166 中规定的国家/地区代码,codeset 为字符集名称。

在Linux下,可以使用 locale -a 命令查看系统中所有已配置的 locale。用不带选项的 locale 命令查看当前 Shell 中活动的 locale。用 locale -m 命令查看locale系统支持的所有可用的字符集编码。

和locale相关的包叫做:locales,locale系统支持的所有可用locale在文件:/usr/share/i18n/SUPPORTED 中列出。

在Debian下,可用 dpkg-reconfigure locales 命令重新配置 locale,也可以手工修改 /etc/locale.gen 文件,然后运行 locale-gen 命令。

在Ubuntu下,修改 /var/lib/locales/supported.d/local 文件,配置新的 locale,然后运行 locale-gen 命令。

当 locale 为 NULL 时,函数只做取回当前 locale 操作,通过返回值传出,并不改变当前 locale。

当 locale 为 "" 时,根据环境的设置来设定 locale,检测顺序是:环境变量 LC_ALL,每个单独的locale分类LC_*,最后是 LANG 变量。为了使程序可以根据环境来改变活动 locale,一般都在程序的初始化阶段加入下面代码:setlocale(LC_ALL, "")。

当C语言程序初始化时(刚进入到 main() 时),locale 被初始化为默认的 C locale,其采用的字符编码是所有本地 ANSI 字符集编码的公共部分,是用来书写C语言源程序的最小字符集(所以才起locale名叫:C)。

当用 setlocale() 设置活动 locale 时,如果成功,会返回当前活动 locale 的全名称;如果失败,会返回 NULL。

Windows的CRT中的setlocale()

具体参考:setlocale - MSDN Run-Time Library Reference

在 Windows CRT 的实现中还有一个使用 wchar_t 作为 locale 名的宽字符版本:_wsetlocale()。因此,也有了使用 _TCHAR 宏版本的 setlocale():_tsetlocale()

Windows CRT 实现的 setlocale() 和 glibc 版本的头文件与声明相同,使用方法类似,如下:

支持的 locale 分类常量:LC_ALL、LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC、LC_TIME。

请求设定的 locale 名可以为以下格式(参考MSDN:Language and Country/Region Strings):

  1. lang[_country_region[.code_page]]:虽然形式与 glibc 的相同,当 Windows 的 locale 名并不符合 POSIX 的规范,比如采用 GBK 字符集的大陆中文,POSIX 的名字为:zh_CN.GBK,而在 Windows CRT 中要用:Chinese_People's Republic of China.936,(-_-^)。

  2. .code_page:可以直接使用代码页来设定 locale,而且可以使用 .OCP、.ACP 两个伪代码页,.OCP 表示从系统获得的当前活动的 OEM 代码页,.ACP 表示从系统获得的活动 ANSI 代码页。

  3. "":根据 Windows 系统环境的活动 ANSI 代码页来设定 locale。.OCP、.ACP、和环境代码页都受控制面板中“区域与语言选项”的设置影响。默认装完简体中文版 Windows 后,活动的 ANSI 代码页为:936(即 GBK),可用 chcp 控制台程序查看活动代码页。

  4. NULL:取回当前 locale,不改变当前 locale。

setlocale()的作用和使用例子

当向终端、控制台输出 wchar_t 类型的字符时,需要设置 setlocale(),因为通常终端、控制台环境自身是不支持 UCS 系列的字符集编码的,使用流操作函数时(如:printf()),在标准/RT库实现的内部会将 UCS 字符转换成合适的本地 ANSI 编码字符,转换的依据就是 setlocale() 设定的活动 locale,最后将结果字符序列传递给终端,对于来自终端的输入流这个过程刚好相反。

可以用重定向输出流到文件的方法验证上面的机制:无论是 Windows CRT、Linux glibc、Cygwin glibc,使用 wprintf() 打印 wchar_t 字符文本时,重定向到文件的内容总是 GBK、UTF-8 等本地 ANSI 编码,而不会是 UCS 编码。

下面是我写的一个使用 setlocale() 的示例:

01 #ifdef __GNUC__
02   
03 #define CSET_GBK    "GBK"
04 #define CSET_UTF8   "UTF-8"
05   
06 #define LC_NAME_zh_CN   "zh_CN"
07   
08 // ifdef __GNUC__
09 #elif defined(_MSC_VER)
10   
11 #define CSET_GBK    "936"
12 #define CSET_UTF8   "65001"
13   
14 #define LC_NAME_zh_CN   "Chinese_People's Republic of China"
15   
16 // ifdef _MSC_VER
17 #endif
18   
19 #define LC_NAME_zh_CN_GBK       LC_NAME_zh_CN "." CSET_GBK
20 #define LC_NAME_zh_CN_UTF8      LC_NAME_zh_CN "." CSET_UTF8
21 #define LC_NAME_zh_CN_DEFAULT   LC_NAME_zh_CN_GBK
22   
23 void print_current_loc();
24   
25 int main(int argc, char* argv[])
26 {
27     char* locname = NULL;
28     const wchar_t* strzh = L"中文字符串";
29   
30     print_current_loc();
31   
32     // 使用指定的 locale
33     locname = setlocale(LC_ALL, LC_NAME_zh_CN_DEFAULT);
34     if ( NULL == locname )
35     {
36         printf("setlocale() with %s failed.\n", LC_NAME_zh_CN_DEFAULT);
37     }
38     else
39     {
40         printf("setlocale() with %s succeed.\n", LC_NAME_zh_CN_DEFAULT);
41     }
42   
43     print_current_loc();
44   
45     wprintf(L"Zhong text is: %ls\n", strzh);
46   
47     // 使用运行环境中的 locale 设置
48     locname = setlocale(LC_ALL, "");
49     if ( NULL == locname )
50     {
51         printf("setlocale() from environment failed.\n");
52     }
53     else
54     {
55         printf("setlocale() from environment succeed.\n");
56     }
57   
58     print_current_loc();
59   
60     wprintf(L"Zhong text is: %ls\n", strzh);
61   
62     puts("End of program.");
63     return 0;
64 }
65   
66 // 打印当前 locale
67 void print_current_loc()
68 {
69     char* locname = setlocale(LC_ALL, NULL);
70     printf("Current locale is: %s\n", locname);
71 }

要使上面程序成功编译并执行,需要注意一下几点:

Windows CRT 是不支持 UTF-8 编码作为 locale 的,运行时使用 setlocale(LC_ALL, ".65001") 会失败。

使用 Linux 和 Cygwin 的 glibc 时,要在终端显示正确的中文,需满足以下条件:

  1. 不要混用 char 和 wchar_t 版本的流操作函数,否则会导致这些函数运行异常,我用Cygwin GCC 4测试混用 printf() 和 wprintf() 时,程序甚至崩掉,所以要将上面程序中 printf() 语句全注释掉才行。Window CRT 的实现则没有这个问题。

  2. 运行环境的 locale 设置要和程序中 setlocale() 设定的 locale 一致,比如:终端的活动字符集、环境变量(一般用 LANG),要设置为 *.UTF-8,才能显示 setlocale(LC_ALL, "zh_CN.UTF-8") 设定的 wchar_t 的中文字符。

  3. 用 GCC 编译时,要使用 UTF-8 编码保存源文件,这是 GCC 在编译时,将 wchar_t 文字量(以 L 打头)正确转换为 UCS 编码保存在对象文件中的必需条件,用 Native ANSI 编码(比如:GBK)有 wchar_t 文字量的源文件时,GCC 会编译出错,Linux 和 Cygwin 的 GCC 都有这个约束。另外在 Linux GCC 使用 UCS-4 编码保存 wchar_t,而 Windows 和 Cygwin GCC 使用 UCS-2。

  4. 用 wprintf() 时,要用 %ls 表示 wchar_t 的字符串,用 %s 表示 char 的字符串,具体参考:man 3 wprintf,而 Windows 的实现用 %ls、%s 都可以正确输出 wchar_t 字符串。


发布了339 篇原创文章 · 获赞 200 · 访问量 304万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览