我们熟悉的ASCII码全称是美国国家信息交换标准码,它起源于20世纪50年代末,并于1967年最终定型。ASCIIS码使用7位(bit)的宽度,有26个小写字母,26个大写字母,10个数字,32个符号,33个控制码,一个空格码,共128个代码。
ASCII的使用相当普及,是一种非常可靠的标准。但是,ASCII是一个真正的美国标准,它甚至满足不了其他英语国家的需求,例如,ASCII码并没有英镑符号。
我们知道,一些语言文字系统(例如中国的汉字)的字符集有非常多的符号,但一个字节最多只能表示256个符号,这是远远不够的。为了支持这些文字系统,双字节字符集(doube-byte character set, DBCS)应运而生。在双字节字符集中,一个字符由1个或2个字节组成。对程序员来说,和双字节字符集打交道就如同一场噩梦,因为程序员需要判断每个字节是否双字节的前导字节。
与DBCS的混乱不同,Unicode统一使用16位进行编码,即UTF-16编码。UTF的全称是Unicode Transformation Format。UTF-16将每个字符编码2个字节(16位)。这样一来,应用程序很容易遍历字符串长度。
一、char数据类型
我们知道,C语言用char数据类型来表示一个8位ANSI字符。当在代码中声明一个字符串时,C编译器会把字符串中的字符转换成由8位char数据类型组成的数组。例如,
[cpp]
char c = 'A';
char szBuffer[100] = "A String";
可以定义一个指向字符串的指针:
[cpp]
char *p;
由于windows是一个32位的系统,指针变量p需要4个字节的存储空间。也可以定义并初始化一个指向字符串的指针:
[cpp]
char *p = "Hello!";
变量p和之前一样,也只是需要4个字节的空间。字符串存储在静态内存中并使用7个字节来存储——其中6个字节存储字符串,另一个字节存储结束的'\0'。
二、wchat_t类型
Microsoft的C/C++编译器定义了一个内建的数据类型wchat_t,表示一个16位的Unicode(UTF—16)字符。
声明Unicode字符和字符串的方法如下:
[cpp]
wchar_t c = L'A';
wchar_t szBuffer[100] = L"A String";
字符串前面的大写字母L通知编译器该字符串应当编译为一个Unicode字符串。当编译器将此字符串放入程序的数据段时,会使用UTF-16来编码每个字符。
为了与C语言有一些区分,Windows开发团队希望定义自己的数据类型。于是,他们定义了以下数据类型:
[cpp]
typedef char CHAR; // 一个8位的字符
typedef wchar_t WCHAR; // 一个16位的字符
除此之外,Windows还定义了一系列为我们提供方便的数据类型,可以用它们来处理字符指针和字符串指针:
[cpp]
// 指向8位字符(串)的指针
typedef CHAR *PCHAR;
typedef CHAR *PSTR;
typedef CONST CHAR *PCSTR;
// 指向16位的字符(串)指针
typedef WCHAR *PWCHAR;
typedef WCHAR *PWSTR;
typedef CONST WCHAR *PCWSTR;
三、维护一个源代码
在写代码的时候,可以使用ANSI 或Unicode字符/字符串。为使其能通过编译,windows定义了以下的类型的宏:
[cpp]
#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)
利用这些类型和宏,无论使用ANSI还是Unicode,都可以通过编译。
[cpp]
// 若定义了UNICODE,则作用16位的字符,否则使用8位的字符
TCHAR C = TEXT('A');
// 若定义了UNICODE,则作用16位的字符串,否则使用8位的字符串
TCHAR szBuffer[100] = TEXT("A String");
四、Windows中的Unicode函数和ANSI函数
如果一个Windows函数的参数列表中有字符串,则该函数通常有两个版本。例如MessageBox函数,它有两个入口点,一个名为MessageBoxA接受ANSI字符串,一个名为MessageBoxW接受Unicode字符串。
MessageBoxA的定义如下:
[cpp]
int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
MessageBoxW的定义如下:
[cpp]
int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
注意第二第三个参数分别指向8位和16位的字符串。
在编写代码时,我们只需使用MessageBox,根据是否已定义UNICODE标识符,会自动选择MessageBoxA函数还是MessageBoxW函数。
[cpp]
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
五、C运行库中的Unicode函数和ANSI函数
在C运行库中,strlen是一个返回ANSI字符串长度的函数,与之对应的是wcslen函数,它返回的是Unicode字符串的长度。
为方便使用,定义了以下的宏:
[cpp]
#ifdef _UNICODE
#define _tcslen wcslen
#else
#define _tcslen strlen
#endif
这样,只需要代码中使用_tcslen,即可以获得字符串的长度。
六、推荐的字符和字符串处理方式
1. 将文本字符串想象为字符的数组,而不是char或者字节的数组
2. 使用通用的数据类型(如TCHAR/PTSTR)来表示文本字符和字符串
3. 用明确的数据类型(如BYTE或PBYTE)表未字节,字节指针和数据缓冲区
4. 用TEXT或_T宏来表示字面常字符和字符串,但为了保持一致,请避免混用
5. UNICODE和_UNICODE符号要么同时指定,要不都不指定
6. 避免使用printf系列函数,尤其不要使用%s和%S来进行ANSI与Unicode字符串之间的转换,正确的做法是使用MultiByteToWideChar和WideCharToMultiByte函数
7. 修改有关字符串的计算。例如,函数经常希望传给它的是缓冲区大小的字符数,而不是字节数,这时应使用_countof(szBuffer),而不是sizeof(szBuffer)。如果要为一个字符串分配内存块,那么请记住内存是以字节来分配的。这意味着需使用malloc(nCharacters * sizeof(TCHAR)),而不是调用malloc(nCharacters)。