原来一篇总结了下 C 标准库的 setlocale() 用法,这篇讲解的是 C++ 标准库中 locale 类的用法。
参考:
cplusplus.com 上关于标准 C++ 中国际化支持的参考:
Localization library
Locale class
The C++ Standard Library(Nicolai M. Josuttis,侯捷译)第 14 章 Internationalization(国际化)
locale 类在头文件 <locale> 中声明,另外可能会用到 <stdexcept> 中的标准异常类,和<iostream> 中的流对象类。
参考:
GNU libstdc++ 在线文档:The GNU C++ Library Documentation
上面的在线文档可以在 libstdc++ 的源码包中 doc/html 目录下找到。
Doxygen 生成的 libstdc++ API 参考:libstdc++ Source Documentation
locale 类参考:std::locale Class Reference
libstdc++ 在线手册国际化章节:Part VI. Localization
locale 对象的构造
构造函数有以下几个常用的重载形式:
locale()
构造一个 locale 对象,它和程序当前的全局 locale 属性相同。
explicit locale(const char* _Locname)
根据 locale 名构造一个 locale 对象,影响该对象的所有的 locale 类别(category),locale 名就是 C 标准库 setlocale() 中使用的名字,所以 libstdc++ 用的是 POSIX 标准的 locale 名,而 Windows CRT 实现的 locale 类,用的是 Windows 的 setlocale() 中特有的 locale 名字。
和 setlocale() 类似,如果要使用环境设置的 locale 时,可以将 locale 名字写为空字符串""。
注意这个构造函数被声明为显式的(explicit),说明隐式的参数转换会被认为是语法错误,例如:locale loc = "zh_CN.UTF-8" 会在编译时报错。
当指定的 locale 名为 NULL,或是一个无效的名字,此构造函数会抛出 runtime_error 异常。
locale(const locale& _Loc, const locale& _Other, category _Cat)
locale(const locale& _Loc, const char* _Locname, category _Cat)
首先用 _Loc 拷贝构造一个 locale 对象作为基础 locale,然后从 _Other 或 _Locname 指定的 locale 中,抽取里面由 _Cat 指定的 category,替换掉基础 locale 中相应的 category。
category 是 int 的 typedef,在 locale 类内有一些预定义的 category 常量(类静态常量成员),分别对应于 C 标准库中预定义的 locale 类别常量:
locale::category 常量 | setlocale() 中的 category |
---|---|
all | LC_ALL |
collate | LC_COLLATE |
ctype | LC_CTYPE |
messages | LC_MESSAGES |
monetary | LC_MONETARY |
none | 表示所有 category 的空集(LC_ALL 的补集) |
numeric | LC_NUMERIC |
time | LC_TIME |
category 类型被用作掩码类型,所以上面 locale 类别可以用 | 进行组合,比如:monetary | time。
locale 类静态方法
const locale& classic()
得到一个表示 C locale 的 locale 对象。
locale global(const locale& _Loc)
设置程序全局 locale 为 _Loc,返回以前的全局 locale。
所谓全局 locale,就是流对象不用 imbue() 方法采用指定 locale 时的缺省 locale,相当于 C 标准库中 setlocale() 设定的 locale。程序初始化时,全局 locale 为 C locale。
例如,设定全局 locale 为环境设置的 locale:
1 |
// 相当于 setlocale(LC_ALL, ""); |
2 |
locale::global(locale( "" )); |
locale 对象方法
string name() const
返回 locale 的名字,例如,打印当前全局 locale 名字可以为:
1 |
locale lc; |
2 |
cout << "Current global locale is: " << lc.name() << endl; |
注意:name() 返回的是 string 对象,而 wostream 的 << 操作在 string 上是无定义的,所以这个会出错:wcout << lc.name(),除非自己重载 wostream 的 << 操作。
locale 使用示例与问题
locale 类的作用和 setlocale() 相同:一是输出 wchar_t 字符时,根据活动 locale 将字符从 UCS 编码转换为 Native ANSI 编码,最后传递给终端、控制台设备;另一个作用是使用特定 locale 的某些 category 做操作(相应的函数、方法称为 locale 相关方法),比如:时间、货币文本的格式化,排序等等。
使用 wcout 流对象和 locale 对象,输出 wchar_t 型字符限制很大,以下是正确向终端输出中文的例子:
01 |
const wchar_t * strzh = L "中文abc" ; |
02 |
03 |
try |
04 |
{ |
05 |
locale lc( "zh_CN.UTF-8" ); |
06 |
locale::global(lc); |
07 |
// wcout.imbue(lc); // libstdc++ 的实现 imbue() 不起作用 |
08 |
wcout << L "Zhong text is: " << strzh << endl; |
09 |
} |
10 |
catch (runtime_error& e) |
11 |
{ |
12 |
cerr << "Error: " << e.what() << endl; |
13 |
cerr << "Type:" << typeid (e).name() << endl; |
14 |
} |
上面程序的运行环境为:终端和 shell 都使用 zh_CN.UTF-8 编码,最后用重定向输出到文件的方法测试出:输出的文本为 UTF-8 编码。
限制来自于:无法混用 char 和 wchar_t 版的流对象,而很多对象只有返回 string 的方法(比如:locale::name()),导致必须使用 char 版的 cout、cerr 等,或者可以用 string::c_str() 方法。
GNU libstdc++ 中实现的 locale 类,在使用时注意以下问题(VC8 中没有下面前两个问题):
流对象的 imbue() 方法不起作用
使用 locale::global() 设定全局 locale 后,wostream 可以输出正确转换的 Native ANSI 编码字符。而使用 wostream::imbue() 后,流对象无法向终端传递正确转换的编码字符。在测试 wcout 时,使用重定向输出到文件的方法发现:wcout 在输出含有 ASCII 外的 wchar_t 时,无法正确转换到 Native ANSI 编码字符,而转换成字节 0x3F(对应 ASCII 字符 ?)。
char 和 wchar_t 版的流对象不能混用
和 glibc 实现的 C 标准库类似,在 libstdc++ 中混用 char 和 wchar_t 版的流对象,也会出现意外的错误,比如下面代码:
01 |
const wchar_t * strzh = L "中文abc" ; |
02 |
locale lc( "zh_CN.UTF-8" ); |
03 |
locale::global(lc); |
04 |
05 |
// 这句会引发后面的 wcout 输出错误 |
06 |
cout << "abc" << endl; |
07 |
08 |
// 即使刷缓冲区和清空流对象状态也无法解决问题 |
09 |
cout.flush(); |
10 |
cout.clear(); |
11 |
wcout.flush(); |
12 |
wcout.clear(); |
13 |
14 |
wcout << L "Zhong text is: " << strzh << endl; |
程序的输出如下:
# ./test_03.exe | hd 00000000 61 62 63 0a 5a 68 6f 6e 67 20 74 65 78 74 20 69 |abc.Zhong text i| 00000010 73 3a 20 2d 87 61 62 63 0a |s: -.abc.| 00000019
上面程序错误之处为:没有将 "中文" 的 UCS-4 LE 编码 2D 4E 00 00 87 65 00 00 转换为正确的 UTF-8 编码 E4 B8 AD E6 96 87,而是将每个 wchar_t 字符的最低字节(L'中' 为 2D,L'文' 为 87)传给了终端。
如果把 cout << "abc" << endl; 这句放到 wcout 之后,那么 wcout 是可以正常输出中文的,但 cout 就工作不正常了,根本就不向终端传递字符。
Cygwin 的 libstdc++ 中的 locale 实现
测试版本:g++ 4.3.4,bash 3.2.49,cygwin1.dll 1.7.1,libstdc++6 4.3.4-3
构造 locale 对象时,C.UTF-8,zh_CN.UTF-8,en_US.UTF-8 这些 locale 名字都无效,都会抛出runtime_error 异常,就连使用环境 locale 的空字符串参数 "" 都无效。只有一个 locale 名是有效的,就是最小的 C locale,即程序初始化时的默认locale。另外,Cygwin bash 启动后默认的 locale 为 C.UTF-8,可用查看 LANG 环境变量得知。
cout、wcout 与 printf()、wprintf() 的区别
我的感觉是 cout、wcout 没有 printf()、wprintf() 好用,至少 printf()、wprintf() 都可以输出 wchar_t 型字符,而 cout 默认无法输出 wchar_t 型字符(当然可以自己重载 <<),而把 wchar_t 当整数输出,把 wchar_t[] 和 wchar_t* 输出个首地址。
wostream 是可以以 char 型字符、字符串为 << 的参数的,但如果传入的 char 型字符在 ASCII 范围之外(在 0x00 ~ 0x7F 之外),wostream 是不会将正确的 Native ANSI 编码序列传递给终端、控制台设备的,导致终端中不会显示超出 ASCII 之外的字符。
string 没有在 wostream 的 << 上定义,但可以用 string::c_str() 的方法让其输出 char 型字符,前提是事先知道 string 对象中不含有超出 ASCII 之外的字符。
参考:
MSDN VC8 参考:
<locale>
locale Class
我在 VC8 下测试,和 GNU libstdc++ 的差异挺大,表现结果是:wcout.imbue(lc) 起作用,可以在控制台中输出含中文的 wchar_t 型字符,并且控制台得到的是根据 locale 转换的 Native ANSI 编码。Windows 的 locale 类实现不支持 UTF-8(代码页 65001),所以要用 GBK 的 locale,对应的 locale 名为 chs,长名字:Chinese_People's Republic of China.936。
用 VC8 的 locale::global(lc) 时有个奇怪的现象,它导致后面的 wcout 流输出到控制台完全没有显示,我将输出重定向到文件,发现结果正常,和用 imbue() 输出的相同,为 GBK 编码的中文。所以我认为,无法显示中文字符是 Windows 的控制台设备实现,以及 CRT 和控制台设备交互的问题,CRT 根据 locale 转换编码的工作是正常的。
另外,VC8 的 cout 和 wcout 的混用,以及 printf() 和 wprintf() 混用,都没有 GNU 实现的问题。