# 宽字符和宽字符串 ## 中文字符串的存储 正确地存储中文字符需要解决两个问题。 #### 1) 足够长的数据类型 char 只能处理 ASCII 编码中的英文字符,是因为 char 类型太短,只有一个字节,容纳不下我大中华几万个汉字,要想处理中文字符,必须得使用更长的数据类型。 一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数,所以我们可以用整数类型来存储一个字符,比如 unsigned short、unsigned int、unsigned long 等。 #### 2) 选择包含中文的字符集 C语言规定,对于汉语、日语、韩语等 ASCII 编码之外的单个字符,也就是专门的字符类型,要使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。 在真正实现时,微软编译器(内嵌于 Visual Studio 或者 Visual C++ 中)采用 UTF-16 编码,使用 2 个字节存储一个字符,用 unsigned short 类型就可以容纳。GCC、LLVM/Clang(内嵌于 Xcode 中)采用 UTF-32 编码,使用 4 个字节存储字符,用 unsigned int 类型就可以容纳。 > 对于编号较小的字符,UTF-16 采用两个字节存储;对于编号较大的字符,UTF-16 使用四个字节存储。但是,全球常用的字符也就几万个,使用两个字节存储足以,只有极其罕见,或者非常古老的字符才会用到四个字节。 > > 微软编译器使用两个字节来存储 UTF-16 编码的字符,虽然不能囊括所有的 Unicode 字符,但是也足以容纳全球的常见字符了,基本满足了软件开发的需求。使用两个字节存储的另外一个好处是可以节省内存,而使用四个字节会浪费 50% 以上的内存。 你看,不同的编译器可以使用不同的整数类型。如果我们的代码使用 unsigned int 来存储宽字符,那么在微软编译器下就是一种浪费;如果我们的代码使用 unsigned short 来存储宽字符,那么在 GCC、LLVM/Clang 下就不够。 为了解决这个问题,C语言推出了一种新的类型,叫做 **wchar_t**。w 是 wide 的首字母,t 是 type 的首字符,wchar_t 的意思就是宽字符类型。wchar_t 的长度由编译器决定: - 在微软编译器下,它的长度是 2,等价于 unsigned short; - 在GCC、LLVM/Clang 下,它的长度是 4,等价于 unsigned int。 > wchar_t 其实是用 typedef 关键字定义的一个别名,我们会在一节中深入讲解,大家暂时只需要记住,wchar_t 在不同的编译器下长度不一样。 wchar_t 类型位于 头文件中,它使得代码在具有良好移植性的同时,也节省了不少内存,以后我们就用它来存储宽字符。 上节我们讲到,单独的字符由单引号`' '`包围,例如`'B'`、`'@'`、`'9'`等;但是,这样的字符只能使用 ASCII 编码,要想使用宽字符的编码方式,就得加上`L`前缀,例如`L'A'`、`L'9'`、`L'中'`、`L'国'`、`L'。'。` 注意,加上`L`前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。 下面的例子演示了如何存储宽字符(注意引入 头文件): ``` wchar_t a = L'A'; //英文字符(基本拉丁字符)wchar_t b = L'9'; //英文数字(阿拉伯数字) wchar_t c = L'中'; //中文汉字wchar_t d = L'国'; //中文汉字wchar_t e = L'。'; //中文标点 wchar_t f = L'ヅ'; //日文片假名wchar_t g = L'♥'; //特殊符号wchar_t h = L'༄'; //藏文 ``` 在以后的编程中,我们将不加`L`前缀的字符称为窄字符,将加上`L`前缀的字符称为宽字符。窄字符使用 ASCII 编码,宽字符使用 UTF-16 或者 UTF-32 编码。 ## 宽字符的输出 putchar、printf 只能输出不加`L`前缀的窄字符,对加了`L`前缀的宽字符无能为力,我们必须使用 头文件中的宽字符输出函数,它们分别是 putwchar 和 wprintf: - putwchar 函数专门用来输出一个宽字符,它和 putchar 的用法类似; - wprintf 是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串。宽字符对应的格式控制符为`%lc`。 另外,在输出宽字符之前还要使用 setlocale 函数进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。由于大家基础还不够,关于本地化设置的内容我们不再展开讲解,请大家先记住这种写法。 如果希望设置为中文简体环境,在 Windows 下请写作: setlocale(LC_ALL, "zh-CN"); 在 Linux 和 Mac OS 下请写作: setlocale(LC_ALL, "zh_CN"); setlocale 函数位于 头文件中,我们必须引入它。 下面的代码完整地演示了宽字符的输出: ``` #include #include int main(){ wchar_t a = L'A'; //英文字符(基本拉丁字符) wchar_t b = L'9'; //英文数字(阿拉伯数字) wchar_t c = L'中'; //中文汉字 wchar_t d = L'国'; //中文汉字 wchar_t e = L'。'; //中文标点 wchar_t f = L'ヅ'; //日文片假名 wchar_t g = L'♥'; //特殊符号 wchar_t h = L'༄'; //藏文 //将本地环境设置为简体中文 setlocale(LC_ALL, "zh_CN"); //使用专门的 putwchar 输出宽字符 putwchar(a); putwchar(b); putwchar(c); putwchar(d); putwchar(e); putwchar(f); putwchar(g); putwchar(h); putwchar(L'\n'); //只能使用宽字符 //使用通用的 wprintf 输出宽字符 wprintf( L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lc\n", //必须使用宽字符串 a, b, c, d, e, f, g, h ); return 0; } ``` 运行结果: A9中国。ヅ♥༄ Wide chars: A 9 中 国 。 ヅ ♥ ༄ ## 宽字符串 给字符串加上`L`前缀就变成了宽字符串,它包含的每个字符都是宽字符,一律采用 UTF-16 或者 UTF-32 编码。输出宽字符串可以使用 头文件中的 wprintf 函数,对应的格式控制符是`%ls`。 下面的代码演示了如何使用宽字符串: ``` #include #include int main(){ wchar_t web_url[] = L"http://c.biancheng.net"; wchar_t *web_name = L"C语言中文网"; //将本地环境设置为简体中文 setlocale(LC_ALL, "zh_CN"); //使用通用的 wprintf 输出宽字符 wprintf(L"web_url: %ls \nweb_name: %ls\n", web_url, web_name); return 0; } ``` 运行结果: web_url: http://c.biancheng.net web_name: C语言中文网 ## 源文件编码 语言是 70 年代的产物,那个时候只有 ASCII,各个国家的字符编码都还未成熟,所以C语言不可能从底层支持 GB2312、GBK、Big5、Shift-JIS 等国家编码,也不可能支持 Unicode 字符集。 稍微有点C语言基本功的读者可能认为C语言使用 ASCII 编码,字符在存储时会转换成对应的 ASCII 码值,这也是错误的,你被大学老师和教材误导了!在C语言中,只有 char 类型的窄字符才使用 ASCII 编码,char 类型的窄字符串、wchar_t 类型的宽字符和宽字符串都不使用 ASCII 编码! wchar_t 类型的宽字符和宽字符串使用 UTF-16 或者 UTF-32 编码,这个在上节已经讲到了,现在只剩下 char 类型的窄字符串(下面称为窄字符串)没有讲了,这就是本节的重点。 对于窄字符串,C语言并没有规定使用哪一种特定的编码,只要选用的编码能够适应当前的环境即可,所以,窄字符串的编码与操作系统和编译器有关。 但是,可以肯定的说,在现代计算机中,窄字符串已经不再使用 ASCII 编码了,因为 ASCII 编码只能显示字母、数字等英文字符,对汉语、日语、韩语等其它地区的字符无能为力。 源文件用来保存我们编写的代码,它最终会被存储到本地硬盘,或者远程服务器,这个时候就要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分的字符都是 ASCII 编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。 UTF-8 兼容 ASCII,代码中的大部分字符可以用一个字节保存;另外 UTF-8 基于 Unicode,支持全世界的字符,我们编写的代码可以给全球的程序员使用,真正做到技术无国界。 常见的 IDE 或者编辑器,例如 Xcode、Sublime Text、Gedit、Vim 等,在创建源文件时一般也默认使用 UTF-8 编码。但是 Visual Studio 是个奇葩,它默认使用本地编码来创建源文件。 > 所谓本地编码,就是像 GBK、Big5、Shift-JIS 等这样的国家编码(地区编码);针对不同国家发行的操作系统,默认的本地编码一般不同。简体中文本的 Windows 默认的本地编码是 GBK。 对于编译器来说,它往往支持多种编码格式的源文件。微软编译器、GCC、LLVM/Clang(内嵌于 Xcode 中)都支持 UTF-8 和本地编码的源文件,不过微软编译器还支持 UTF-16 编码的源文件。如果考虑到源文件的通用性,就只能使用 UTF-8 和本地编码了。 ## 窄字符串使用编码 `"C语言中文网"`和`"http://c.biancheng.net"`就是需要被处理的窄字符串,程序运行后,它们会被载入到内存中。你看,这里面还包含了中文,肯定不能使用 ASCII 编码了。 1) 微软编译器使用本地编码来保存这些字符。不同地区的 Windows 版本默认的本地编码不一样,所以,同样的窄字符串在不同的 Windows 版本下使用的编码也不一样。对于简体中文版的 Windows,使用的是 GBK 编码。 2) GCC、LLVM/Clang 编译器使用和源文件相同的编码来保存这些字符:如果源文件使用的是 UTF-8 编码,那么这些字符也使用 UTF-8 编码;如果源文件使用的是 GBK 编码,那么这些字符也使用 GBK 编码。 你看,对于代码中需要被处理的窄字符串,不同的编译器差别还是挺大的。不过可以肯定的是,这些字符始终都使用窄字符(多字节字符)编码。 正是由于这些字符使用 UTF-8、GBK 等编码,而不是使用 ASCII 编码,所以它们才能包含中文。 那么,为什么很多初学者会误认为C语言使用 ASCII 编码呢? 不管是在课堂跟着老师学习,还是通过互联网自学,初学者都是从处理英文开始的,对于英文来说,使用 GBK、UTF-8、ASCII 都是一样的,GBK、UTF-8 都兼容 ASCII,初学者根本察觉不出用了哪种编码。 另外,很多大学老师和书籍作者也经常会念叨,字符在存储时会被转换成对应的 ASCII 码,在读取时又会从 ASCII 码转换成对应的字符实体,大家需要熟悉 ASCII 编码,它是C语言处理字符的基础,这从很大程度上给初学者造成一种错误印象:C语言和 ASCII 编码是绑定的,C语言使用 ASCII 编码。