字符编码详解
在前一章节的例子中,我们曾使用过TEXT()宏,也接触了Win32API中类似LPTSTR、LPCTSTR等自定义的字符串类型,乍一看这些操作都十分迷惑,这里我们再对Win32API中这些宏和数据类型出现的历史背景和意义进行介绍,以此理清Win32API中这些奇怪设计的意义和初衷。
ASCII到Unicode历史脉络
在计算机发展的早期,字符是通过ASCII(American Standard Code for Information Interchange)码存储的,ASCII码最初由美国国家标准协会(ANSI)在1963年发布,最初由7位二进制位表示,仅支持英文字母、数字和一些常用符号。后来,随着计算机的国际化,尤其是欧洲对非英语字母的需求也在增加,7位二进制位不够用了,因此ASCII又扩展到8位二进制位,也就是我们现在所熟知的ASCII码表和扩展ASCII码表,总共可以包含2^8共256个字符。ASCII码表的前128个字符是国际通用的,但对于扩展ASCII码表,各个国家或地区的实现则可能不同。
然而,当计算机推广到东亚地区时,尤其是面对中文汉字,8位二进制位是完全不够用的,汉字总共有数万个,即使常用汉字也有数千个,因此在1980年中国推出了GB标准,即“国标”码GB2312,后来又扩展出了支持更多生僻字的GBK、GB18030编码,港澳台等使用繁体字的地区则通常使用Big5码。然而,各个地区使用不同的编码标准,这又带来了额外的困难。
注:随着时代的发展,目前仅有少数软件会在保存文件或是网络传输时默认使用类似GB码这类编码,大部分都转而采用Unicode了。
再后来,在1991年,Unicode(统一码)标准发布了。Unicode把各种文字都放在同一字符集中,这样一套编码就能支持全世界所有的字母(文字)和符号,使用更加方便了,因此Unicode也被俗称为“万国码”,它彻底解决了之前的各个地区编码不统一问题。Unicode是一套标准,它为全世界的所有字符分配了唯一码点,目前Unicode的编码范围是0x0000 - 0x10FFFF,一个Unicode码点可以写作类似U+0041的形式。UTF-8、UTF-16、UTF-32则都是Unicode的常用具体实现方式,它们定义了如何将Unicode码点序列编码成二进制序列。UTF-8、UTF-16、UTF-32都可以表示相同的Unicode字符,但它们实际编码后的字节序列可能不同。
具体到C语言中,规定了char和wchar_t分别表示ASCII字符和宽字符,char一定是占用8位即1字节的,而wchar_t宽字符占用的内存和采用的具体编码方式则跟平台有关,C语言标准没有明确定义,有些操作系统平台下宽字符使用16位二进制位,有些则使用32位二进制位,这和不同平台下的编译器有关。总而言之,如果你的程序仅涉及ASCII字符就可以使用char类型,如果涉及ASCII以外的字符,就要使用wchar_t了。
在Windows平台MSVC编译器下,宽字符被实现为了unsigned short,即16位的无符号短整型,宽字符编码使用UTF-16。不过这里有一个坑点,UTF-16在最初确实是16位定长的,但随着时代的发展,16个二进制位不够用了,UTF-16也变成了变长编码,编码的字符实际上会占用2或4字节,而Windows下wchar_t固定使用2字节内存,一些特殊的Unicode字符如Emoji确实会出现问题,如果要考虑4字节UTF-16编码的字符,就需要一些特殊的字符处理库了。
在Linux平台GCC编译器下,宽字符wchar_t常采用32位二进制位和UTF-32编码,因此不存在上述问题。
C语言中使用宽字符
C语言中,char和wchar_t字符串使用方式类似,但却又有区别,下面例子分别演示了ASCII字符串和宽字符字符串的用法。
#include <stdio.h>
#include <string.h>
void print_char()
{
char* str = "Hello, world!";
int len = strlen(str);
int size = sizeof(char);
printf("Value: %s Len:%d CharacterSize:%d\n", str, len, size);
}
void print_wchar()
{
wchar_t* str = L"Hello, world!";
int len = wcslen(str);
int size = sizeof(wchar_t);
wprintf(L"Value: %s Len:%d CharacterSize:%d\n", str, len, size);
}
int main()
{
// 处理单字节字符字符串
print_char();
// 处理宽字符字符串
print_wchar();
return 0;
}
宽字符的类型需要定义为wchar_t,宽字符字符串字面量在双引号前要加上L,处理宽字符也不能使用传统的strlen、printf等函数,都需要对应使用其宽字符版本,如wcslen和wprintf。
Win32API对宽字符的处理
从前面例子我们可以看到,C语言中对宽字符的处理代码是和普通字符不同的,Win32API中很多函数都需要处理字符串,难道我们的应用程序要分别编写ASCII版和宽字符版两套代码吗?这显然是不现实的。Win32API引入了一些宏定义和自定义数据类型来处理该问题。
Win32API中,在编译阶段会读取一个叫UNICODE的宏定义。首先是TCHAR类型,如果未定义UNICODE,TCHAR会被指定为char的别名;如果定义了UNICODE,则会被指定为wchar_t的别名。此外还有一个TEXT()宏,如果未定义UNICODE,TEXT的效果是将字符串字面量原样输出;如果定义了UNICODE,则会在字符串字面量前加L,也就是将其定义为一个宽字符字符串字面量。
基于char、wchar_t、TCHAR,Win32API还定义了很多其它自定义类型。
LPSTR(char*) LPCSTR(const char*)
LPWSTR(wchar_t*) LPCWSTR(const wchar_t*)
LPTSTR(TCHAR*) LPCTSTR(const TCHAR*)
上面这些类型乍一看可能让人眼花,但明白了字符和宽字符的使用区别,相信我们就能够理解了。下面例子定义了一个LPCTSTR类型的字符串,并使用了TEXT()宏。
LPCTSTR str = TEXT("Hello, world!");
基于前面的说明我们可以理解,在非Unicode下,上述代码等效为const char *str = "Hello, world!",在Unicode下,上述代码等效为const wchar_t *str = L"Hello, world!"。
至于如何指定Unicode宏,我们可以手动指定#define UNICODE,注意该宏定义一定要加在#include <windows.h>之前,不过更好的方法是在Visual Studio中进行设置,我们可以在项目属性的配置属性 -> 高级中找到相关选项。

设置该选项后,MSVC会自动在编译时指定宏参数,我们就不需要手动在源码中指定了。
补充说明:LPSTR等类型中的LP又是什么意思?这其实是一个历史遗留问题,LP是“Long Pointer”的缩写,在早期16位Windows时代,CPU的地址总线是20位,理论上能访问1MB内存,然而寄存器只有16位,因此为了访问全部内存采用了段地址:偏移地址的方式寻址。在这种内存访问模式下,“近指针(Near Pointer)”只有16位偏移量,“远指针(Far Pointer)”则是32位的段地址:偏移地址,可以跨段访问内存,而“Long Pointer”则是“Far Pointer”的一个别称。在如今是32位/64位时代,指针实际上都是平坦的,LP这个奇怪的前缀已经纯粹是一个遥远时代遗留下来的记忆了。其实微软后来又引入了PSTR、PCSTR等更清晰的宏定义,但由于开发人员和学习资料的历史惯性,大多数人还是倾向于使用传统的LPSTR、LPCSTR。