Windows 编程中的字符编码

经常在写代码的时候需要处理宽字符,ASCII 字符,在代码中看到 wchar、char 等等。一般都是处理一个方法的时候发现需要的是某字符串,然后这边有什么字符串,之后查一个转换方法。还有对于 Unicode 、ANSI 这些不太分得清,所以花了一点时间看了一看。做个小结。

编码介绍

ANSI

ANSI(American National Standards Institute) 其实并不算是一种固定编码,可以理解为在不同国家,有着不同的解释。例如在中国大陆,ANSI 编码指的就是 GBK 编码,在台湾地区指的是 BIG5 编码。所以一个场景下这种编码是有问题的,比方说一个保存为 ANSI 编码的文件,在不同区域的系统下,用记事本打开就会有问题,因为对文本的解释是不同的。例如在中国的系统上保存,其实就是 GBK 编码,然后在美国的系统上打开,会被当做 ASCII 编码来解释,就会出现问题。看不到想要的内容。(注:所以《 Windows 核心编程(第五版)》(下称《核心编程》)2.1节作者说到:调用 strlen 会返回“以 0 结尾的一个 ANSI 单字节字符数组”中的字符数,这个表述是不准确的,之所以这么说是因为作者所在的国家显然是 ASCII 编码,但是拿到中文这里说就不恰当,可以说是作者的锅也可以说是译者的锅。所以此书第二章所有讲到 ANSI,都可以理解为 ASCII 编码)

Unicode

Unicode 标准(使用多字符编码)解决了 ASCII 编码这种单字符编码无法表示一些包含特别多字符的问题。官方的一段解释The Unicode Standard provides a unique number for every character, no matter what platform, device, application or language. ,其实就是把每个字符作为一个具体数字 。对于 Unicode 标准,存在多种编码,例如:UTF-8 编码,UTF-16 编码等等。UTF(Unicode Transformation Format),指的是 Unicode 转换格式。

UTF-8

以下引用《核心编程》原文:

UTF-8 将一些字符编码为 1 个字节(可以说就是那些 ASCII 字符),一些字符编码为 2 个字节,一些字符编码为 3 个字节,一些字符编码为 4 个字节。根据 Unicode 的数字不同来区分应该编码为几个字节,属于变长字节编码。这样的好处是显而易见的,就是节省空间,坏处也是显而易见的,处理一些字符编码比较复杂的文本,显然效率会差,至少要不断判断是几个字节,计算长度就比较麻烦。

UTF-16

UTF-16 就比较鸡贼了,如果细说就要扯到辅助平面和基本文字平面了,感觉意义都不是很大。简单理解就是一般字符(文字基本都是这个范畴)编码为 2 个字节,不一般的编码为 4 个字节(也就是 2 个 2 字节)。关于 UTF-16 连《核心编程》都没说,可见作者也是非常鸡贼了。

UTF-32

UTF-32 这个算是最省事了,把 Unicode 值用 32 位无符号整数表示就得到了 UTF-32 的编码了。缺点也是显而易见的,贼占地方。

BOM头

经常在 Code Page 中看到带 BOM 头和不带 BOM 头。这个跟编码的大小端有关。对于这种多个字节的编码存在一个大小端的问题。如何来区分编码的大小端。Unicode 标准推荐使用一个 BOM(Byte Order Mark)来做区分。BOM 的字符编码是0xFEFF,这个叫做零宽无中断字符,这也解释了为什么你在文件里边去掉和添加 BOM 头都不会影响排版。所以 BOM 头的存在可以帮助判断文本的编码的大小端,如果没有 BOM 头的文本,在跨系统使用的时候,编辑器的实现可以做出两种做法:1. 会根据系统是大小端强行解释,这样的问题是一旦两个系统不一致,看到的内容也就完全不对了;2. 根据里边的数据,做一个判断,因为当大端被解释成小端有可能会出现 Unicode 中不存在的字符(如 BOM 头这个字符,0xFEFF存在,0xFFEF不存在)。在我看来显然应该是第一种做法。具体理由按下不表了。

数据类型

char

1 个字节(8 bit)。用来表示 ASCII 编码。

wchar_t

2 个字节(16 bit)。用来表示 Unicode 字符(UTF-16)。当写出wchar_t c = L'A';这行代码的时候,编译器会把L后边的东西用 UTF-16 来编码。值得一提的是wchar_t早期的 Microsoft 编译器并不支持。在那个上古时期有这样一个定义typedef unsigned short wchar_t。后来支持以后,编译器搞了一个编译开关/Zc:wchar_t,有这个的才在编译器定义这个数据类型,现在新建项目的时候会默认开启了。

CHAR、WCHAR

按照《核心编程》的说法:

为了与 C 语言稍微有一些区分,Windows 开发团队希望定义自己的数据类型。

  • CHAR:typedef char CHAR
  • WCHAR: typedef wchar_t WCHAR
  • 指针:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Pointer to 8-bit character(s)
    typedef CHAR *PCHAR;
    typedef CHAR *PSTR;
    typedef CONST CHAR *PCSTR

    // Pointer to 16-bit character(s)
    typedef WCHAR *PWCHAR;
    typedef WCHAR *PWSTR;
    typedef CONST WCHAR *PCWSTR

TCHAR

TCHAR c = TEXT('A')。这个可以理解为万能类型,之所以这么说,可以看一下它的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef UNICODE

typedef WCHAR TCHAR, *PTCHAR, PTSTR;
typedef CONST WCHAR *PCTSTR;
#define __TEXT(quote) L##quote

#else

typedef CHAR TCHAR, *PTCHAR, PTSTR;
typedef CONST CHAR *PCTSTR;
#define __TEXT(quote) quote

#endif

#define TEXT(quote) __TEXT(quote)

所以看你的项目是否定义了 UNICODE 宏来决定 TCHAR 的类型,当然这个 UNICODE 宏还会影响 Windows API 调用函数版本的选择,后边细说。所以会看到大批文章告诉你解决什么编不过的问题都直接让你用 TCHAR 和 TEXT()。但我觉得并没有太大意义,至少我暂时想不到需要这两个版本都支持的场景。项目使用哪种数据类型明确一点会比较好,会影响到效率,后边细说。

函数

对于 Windows API 微软都会提供两个版本的例如 CreateWindowExWCreateWindowExA,一个是宽字符版本,一个是单字符版本。当然如果你用CreateWindowEx,你会发现再配合 TCHAR 这套,显然也可以正常使用。

1
2
3
4
5
#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#eles
#define CreateWindowEx CreateWindowExA
#endif

就是因为这个缘故。所以上边我说会影响到函数版本的选择。而效率问题,在 Windows Vista 上(当然可以理解为之后的版本也都如此) A 版本的函数其实只是一个转换层,将传入的 ASCII 字符转换成 Unicode 字符,然后调用 W 版本。所以这中间会有一个分配内存的过程,显然会有一个效率上的问题。所以其实现在写代码,非常推荐统一使用宽字符版本。

另外除了 Windows API 之外,C 运行库,也有类似的操作。

1
2
3
4
5
#ifdef _UNICODE
#define _tcslen wcslen
#eles
#define _tcslen strlen
#endif

只不过使用的是 _UNICODE 宏。所以不想让工程出现编码的混乱,显然 UNICODE、_UNICODE 是要成对出现的。事实上,现在用 Visual Studio 新建工程的时候,默认这两个都会定义上的。

跨平台的坑

对于 wchar_t 在 Windows 平台是 UTF-16 编码,是 2 个字节的长度。而在 Linux 上是 4 个字节的长度,GCC 编译的时候会用 UTF-32 编码。这里边就会有一个不一致。要考虑编码转换问题。

最后

至此编程中需要的编码,大致了解清楚了。Windows 编程中,除非有特殊需要,否则一律使用宽字符是最好的选择。编码则选择 UTF-16 编码。