1 字元集简史
1.1 美国标准
美国资讯交换标准码(ASCII:American Standard Code For Information Interchange)起始于50年代后期,最后完成于1967年。最终的资源码就有26个小写字母、26个大写字母、10个数字、32个符号、33个代号和一个空格,总共128个字元码。
ASCII码表:
0- | 1- | 2- | 3- | 4- | 5- | 6- | 7- | |
---|---|---|---|---|---|---|---|---|
-0 | NULL | DEL | SP | 0 | @ | P | ` | p |
-1 | SOH | DC1 | ! | 1 | A | Q | a | q |
-2 | STX | DC2 | “ | 2 | B | R | b | r |
-3 | ETX | DC3 | # | 3 | C | S | c | s |
-4 | EOT | DC4 | $ | 4 | D | T | d | t |
-5 | ENQ | NAK | % | 5 | E | U | e | u |
-6 | ACK | SYN | & | 6 | F | V | f | v |
-7 | BEL | ETB | ‘ | 7 | G | W | g | w |
-8 | BS | CAN | ( | 8 | H | X | h | x |
-9 | HT | EM | ) | 9 | I | Y | i | y |
-A | LF | SUB | * | : | J | Z | j | z |
-B | VT | ESC | + | ; | K | [ | k | { |
-C | FF | FS | , | < | L | \ | l | | |
-D | CR | GS | - | = | M | ] | m | } |
-E | SO | RS | . | > | N | ^ | n | ~ |
-F | SI | US | / | ? | O | _ | o | DEL |
1.2 Unicode解决方案
Unicode使用(特别是在C程式设计语言环境里)【宽子元集】。Unicode中的每个字元都是16位元宽而不是8位元宽。
2 宽字节和C
2.1 char资料形态
假定我们都非常熟悉在C程式中使用char资料型态来定义和存储字元跟字串。但为了便于理解C如何处理宽字元,让我们先回顾一下可能在Win32程式中出现的标准字元定义。
下面的语句定义并初始化了一个只包含一个字元的变量:
char c = 'A';
变量c需要一个字节来保存,并将用十六进制位数0x41初始化,这是字母A的ASCII代码。
您可以像这样定义一个指向字符串的指针:
char *p;
因为Windows是一个32位元的作业系统,所以指针变量p需要4个字节来保存。您还可以初始化一个指向字符串的指针:
char *p = "Hello!";
像前面一样,变量p也需要用4个字节保存。该字串保存在静态记忆体中并占用7个字节-6个保存字符串,1个保存终止符号0.
您还可以像这样定义字符串:
char a[10];
在这种情况下,编译器为该字符串分配10个字节的存储空间。运算符sizeof(a)将返回10。如果字符串定义在所有函式之外,您可以使用下面的语句来初始化一个字符串:
char a[] = "Hello!";
如果您将该阵列定义为一个函式的区域变量,则必须将它定义为一个静态变量,如下:
static char a[] = "Hello!";
无论哪种情况,字串都存储在静态程式记忆体中,并在末尾添加0,这样就需要7个字节的存储空间。
2.2 宽字节:
Unicode或者宽字节都没有改变char资料形态在C中的含义。char继续表示1个字节的存储空间,sizeof(char)继续返回1。理论上,C中1个字节可以比8位bit长,但对我们大多数人来说,1个字节是8位bit宽。
C中的宽字节是基于wchar_t资料型态,它在几个头文件中包括WCHAR.H中都有定义:
typedef unsigned short wchar_t;
因此,wchar_t资料型态和无符号短整型相同,都是2个字节宽。要定义一个宽字节的变量,可以使用以下语句:
wchar_t c = 'A';
变量c是一个双字节值0x0041,是Unicode表示的字母A。(然而,因为Intel微处理器从最小的字节开始存储多字节数值,该变量实际上是以0x41、0x00的顺序保存在内存中。)您还可以定义宽字节字符串的指针:
wchar_t *p = L"Hello!";
注意紧接在第一个引号前面的大写字母L。这将告诉编译器该字串按宽字节保存。同样,您还可以用下面的语句定义宽字符串:
static wchar_t a[] = L"Hello!";
虽然看上去更像是一个印刷符号,但第一个引号前面的L非常重要,并且在两个符号之间必须没有空格。只有带有L,编译器才知道您需要将字符串存为每个字符2个字节。
您还可以在单个字符前使用L字首,来表示它们应该解释为宽字符。如下所示:
wchar_t c = L'A';
但通常这是不必要的,C编译器会对该字元进行扩充,使它成为宽字节。
2.3 宽字节程式库函式
我们都知道如何获取字符串长度。例如,如果我们已经像下面这样定义了一个字符串指针:
char *pc = "Hello!";
我们可以调用:
iLength = strlen(pc);
这时变量iLength将等于6,也就是字符串中的字符数。现在让我们试一下宽字符操作:
wchar_t *pw = L"Hello!";
iLength = strlen(pw);
此时编译器可能会提示一条错误消息:错误 C2664 “size_t strlen(const char *)”: 无法将参数 1 从“wchar_t *”转换为“const char *” 。strlen函式的宽字节版是wcslen(wide-character string length:宽字节长度),并且在STRING.H和WCHAR.H中均有说明。strlen函式说明如下:
size_t _cdecl strlen(const char *);
而wcslen函式则声明如下:
size_t _cdecl wcslen(const wchar_t *);
这时我们知道,要得到宽字符串的长度可以调用
iLength = wcslen(pw);
函式将返回字符串中字符数6。请记住,改成宽字符后,字符串中的长度不变,只是字节长度变了。您熟悉的所有带字串参数的C执行时期程式库函式都有宽字节版。例如,wprintf是printf的宽字节版。这些函式在WCHAR.H中含有标准函式说明的表头档案中说明。
2.4 维护单一的原始码
当然,使用Unicode也有缺点。第一点也是最重要的一点,程式中的每个字串都占用两倍的存储空间。此外,您将发现宽字节执行时期程式库中的函式比常规的函式大。出于这个原因,您也许想建立两个版本的程式-一个处理ASCII 字串,另一个处理Unicode字串。最好的解决办法是维护既能按ASCII编译又能按Unicode编译的单一原始码档案。
虽然只是一小段程序,但由于执行时期程式库函数有不同的名称,您也要定义不同的字节。这将在处理前面带有L的字串文字时遇到麻烦。
一个办法是使用TCHAR.H头文件。该头文件不是ANSI C标准的一部分,因此那里定义的每个函式和巨集定义的前面都有一条底线。TCHAR.H为需要字串参数的标准执行时期程式库函式提供了一系列的替代名称(例如,_tprintf和_tcslen)。
如果定义了_UNICODE的识别字,并且程式中包含了TCHAR.H表头档案,那么,_tcslen就定义为wcslen:
#define _tcslen wcslen
如果没有定义_UNICODE的识别字,则_tcslen定义为strlen:
#define _tcslen strlen
等等。TCHAR.H还用一个新的资料型态TCHAR来解决两个字节资料型态的问题。如果定义了_UNICODE识别字,那么TCHAR就是wchar_t:
typedef wchar_t TCHAR;
否则,TCHAR就是char
typedef char TCHAR;
现在开始讨论字串文字中的L问题。如果定义了_UNICODE识别字,那么一个称作_T的巨集就定义如下:
#define __T(x) L##x
这是相当晦涩的语法,但合乎ANSI C标准的前置处理器规范。那一对##称为粘贴符号,它将字母L添加到巨集引数上。因此如果巨集引数是”Hello”,则L##x就是L”Hello”。如果没有定义_UNICODE识别字,则__T巨集只是简单的定义如下:
#define __T(x) x
此外,还有两个巨集与__T定义相同:
#define _T(x) __T(x)
#define _TEXT(x) __T(x)
在Win32 console程式中使用哪个巨集,取决于您喜欢简洁还是详细。基本地,必须按照下述方法在_T或_TEXT巨集内定义字串文字:_TEXT(“Hello!”)
这样的话,如果定义了_UNICODE,那么该字串将解释为宽字节的组合,否则解释为8位的字节字符串。
3 宽字节和Windows
3.1 Windows头文件类型
正如您在第一章所看到的那样,一个Windows程式包括表头档案WINDOWS.H。该档案包括许多其它表头档案,包括WINDEF.H,该档案中有许多在Windows中使用的基本型态定义,而且它本身也包括WINNT.H。WINNT.H处理基本的Unicode支持。
WINNT.H的前面包含了C的表头档案CTYPE.H,这是C的众多档案之一,包括wchar_t的定义。WINNT.H定义了新的资料型态,称作CHAR和WCHAR:
typedef char CHAR;
typedef wchar_t WCHAR; // wc
当您需要定义8位字节或16位字节宽时,推荐您在Windows程式中使用的资料型态是CHAR和WCHAR。WCHAR定义后面的注释是匈牙利标记法的建议:一个基于WCHAR资料型态的变数可在前面加上字母wc以说明一个宽字节。
WINNT.H表头档案进而定义了可用作8位字节字符串的指针的六种资料型态和四种可用作const 8位字节字符串指针的资料型态。这里精选了头文件中一些实用的声明资料型态语句。
typedef CHAR * PCHAR, *LPCH, *PCH, *NPSTR, *LPSTR, *PSTR;
typedef CONST CHAR * LPCCH, *PCCH, *LPCSTR, *PCSTR;
字首N和L表示【near】和【long】,指的是16位Windows操作系统中两种大小不同的指针。在Win32中near和long没有区别。类似地,WINNT.H定义了可作为16位宽的字符串指针资料型态和四种可作为const 16位宽字符串指针的资料型态。
typedef WCHAR *PWCHAR, *LPWCH, *PWCH, *NWPSTR, *LPWSTR, *PWSTR;
typedef CONST WCHAR *LPCWCH, *PCWCH, *LPCWSTR, *PCWSTR;
至此,我们有了资料型态CHAR和WCHAR,以及指向CHAR和WCHAR的指标。与TCHAR.H一样,WINNT.H将TCHAR定义为一般的字元类型。如果定义了识别字UNICODE(没有底线),则TCHAR和指向TCHAR的指针就分别定义为WCHAR和指向WCHAR的指针。如果没有定义UNICODE,则TCHAR和指向TCHAR的指针就分别定义为char和指向char的指针。
#ifdef UNICODE
typedef WCHAR TCHAR, *PTCHAR;
typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR;
typedef LPCWSTR LPCTSTR;
#else
typedef char TCHAR, *PTCHAR;
typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR;
typedef LPCSTR LPCTSTR;
#endif
如果已经在某个头文件或者其它头文件中定义了TCHAR类型,那么WINNT.H和WCHAR.H头文件都能防止其重复定义。不过,无论何时在程式中使用其他头文件时,都应该在所有其他头文件中包含WINDOWS.H。
WINNT.H头文件还定义了一个巨集,该巨集将L添加到字串的第一个引号前。如果定义了UNICODE识别字,则一个称作__TEXT的巨集定义如下:
#define __TEXT(quote) L##quote
如果没有定义识别字UNICODE,则像这样定义__TEXT巨集:
#define __TEXT(quote) quote
此外,TEXT巨集可以这样定义:
#define TEXT(quote) __TEXT(quote)
这与TCHAR.H中定义_TEXT巨集的方法一样,只是不必操心底线。
3.2 Windows函式呼叫
从Windows1.0 到Windows3.1的16位元Windows中,MessageBox函式位于动态连接程式库USER.EXE。在Windows3.1软件开发套件的WINDOWS.H中,MessageBox函式定义如下:
int WINAPI MessageBox(HWND, LPCSTR, LPCSTR, UINT);
注意,函式的第二个、第三个参数是指向常数字串的指针。当编译连接一个Win16程式时,Windows并不处理MessageBox呼叫。程式.EXE档案中的表格允许Windows将该程式的呼叫与USER中的MessageBox函式动态连接起来。
32位的Windows(即所有版本的Windows NT,以及Windows 95和Windows 98)除了含有与16位相容的USER.EXE以外,还有一个称为USER32.DLL的动态连接程式库,该动态连接程式库含有32位元使用者界面函式的进入点,包括32位元的MessageBox。
这就是Windows支援Unicode的关键:在USER32.DLL中,没有32位元MessageBox函式的进入点。实际上,有两个进入点,一个名为MessageBoxA(ASCII版),另一个名为Message BoxW(宽字元版)。用字符串作参数的每个Win32函式都在作业系统中有两个进入点!幸运的是,您通常不用担心这个问题,程式中只需要使用MessageBox。与TCHAR表头档案一样,每个Windows表头档案都有我们需要的技巧。
下面是MessageBoxA在WINUSER.H中定义的方法。这与MessageBox早期的定义很相似:
WINUSERAPI int WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
下面是MessageBoxW:
WINUSERAPI int WINAPI MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
注意:MessageBoxW函式的第二个参数和第三个参数是指向宽字元的指针。如果需要同时使用并分别匹配ASCII和宽字符函式呼叫,那么您可在Windows程式中明确地使用MessageBoxA和MessageBoxW函式。但大多数程式写作者将继续使用MessageBox。根据是否定义了UNICODE,MessageBox将与MessageBoxA或MessageBoxW一样。在WINUSER.H中完成这一技巧时,程序相当琐碎:
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
注意:Windows 98不支持Unicode版的Windows函式。
3.3 在Windows中使用printf
在Windows中不能使用printf。虽然Windows程式中可以使用大多数C的执行时期程式库-实际上,许多程式写作者更愿意使用C记忆体管理和档案I/O函式而不是Windows中等效的函式-Windows对标准输入和输出没有概念。在Windows程式中可使用fprintf,而不是printf。
但是,在Windows中仍然可以使用sprintf及sprintf系列中的其他函式来显示文字。这些函式除了将内容格式化输出到函式第一个参数所提供的字串缓冲区以外,其功能与printf相同。然后便可对该字串进行操作。
如果您从未使用过sprintf,这里有一个简短的执行体,printf函式声明如下:
int printf(const char *szFormat, ...);
第一个参数是一个格式化字串,后面与格式字串中的代码相对应的不同类型的多个参数。sprintf函式声明如下:
int sprintf(char *szBuffer, const char *szFormat, ...);
第一个参数是字符串的缓冲区,后面是一个格式字串,sprintf不是将格式化字串结果标准输出,而是将其存入szBuffer。该函式返回一个字串的长度。在文字模式设计中,
printf("The sum of %i and %i is %i", 5, 3, 5 + 3);
的功能相等于
char szBuffer[100];
sprintf(szBuffer, "The sum of %i and %i is %i", 5, 3, 5 + 3);
printf(szBuffer);
在Windows中,使用MessageBox显示结果优于puts。
几乎每个人都经历过,当格式字串与被格式化的变量不合时,可能使printf执行错误并可能造成程式崩溃。使用sprintf时同样需要考虑这个问题,而且还要考虑定义的字串缓冲区足够大。Microsoft专用的_snprintf解决了这个问题,此函式引进了另一个参数,表示以字元计算的缓冲区大小。
vsprintf是sprintf的一个变形,它只有三个参数。vsprintf用于执行有多个参数的自定函式,类似printf格式。vsprintf的前两个参数与sprintf相同。第三个参数是指向格式化参数阵列的指针。实际上,该指针指向在堆叠中供函式呼叫的堆叠指针。本章最后的ScreenSize程式展示了使用这些巨集的方法。使用vsprintf函式,sprintf函式可以这样编写:
int sprintf(char *szBuffer, const char *szFormat)
{
int iReturn;
va_list pArgs;
va_start(pArgs, szFormat);
iReturn = vsprintf(szBuffer, szFormat, pArgs);
va_end(pArgs);
return iReturn;
}
va_start巨集将pArg设置为指向一个堆叠的变量,该变量在堆叠参数szFormat的上面。由于Windows早期程式使用了sprintf和vsprintf,最终导致Microsoft向Windows API中增添了两个相似的函式。Windows的wsprintf和wvsprintf函式在功能上与sprintf和vsprintf相同,但它们不能处理浮点格式数据。
当然,随着宽字元的发明,sprintf类型的函式增加许多,使得函式名称变得极为混乱。下表列出了Microsoft的C执行时期程式库和Windows支援的所有sprintf函式。
ASCII | 宽字元 | 常规 | |
---|---|---|---|
参数的变数个数 | |||
标准版 | sprintf | swprintf | _stprintf |
最大长度版 | _snprintf | _snwprintf | _sntprintf |
Windows版 | wsprintfA | wsprintfW | wsprintf |
参数阵列的指针 | |||
标准版 | vsprintf | vswprintf | _vstprintf |
最大长度版 | _vsnprintf | _vsnwprintf | _vsntprintf |
Windows版 | wvsprintfA | wvsprintfW | wvsprintf |
在宽字元版的sprintf函式中,将字串缓冲区定义为宽子串。在宽子元版的所有这些函式中,格式字串必须是宽子串。不过,您必须确保传递给这些函式的其他字串也必须有宽字元组成。
3.4 格式化讯息方块
程式ScreenSize展示了如何实作MessageBoxPrintf函式,该程序有许多参数并能像printf那样编排它们的格式。
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int CDECL MessageBoxPrintf(TCHAR *szCaption, TCHAR *szFormat, ...)
{
TCHAR szBuffer[1024];
va_list pArgList;
// The va_start macro (defined in STDARG.H) is usually equivalent to:
// pArgList = (char *) &szFormat + sizeof(szFormat);
va_start(pArgList, szFormat);
// the last argument to _vsnwprintf_s points to the arguments
// the va_end macro just zeros out pArgList for no good reason
va_end(pArgList);
return MessageBox(NULL, szBuffer, szCaption, MB_OK);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
int iCxScreen, iCyScreen;
iCxScreen = GetSystemMetrics(SM_CXSCREEN);
iCyScreen = GetSystemMetrics(SM_CYSCREEN);
MessageBoxPrintf(TEXT("ScreenSize"), TEXT("The Screen Size is %i pixels width by %i pixels high."), iCxScreen, iCyScreen);
return 0;
}
通过GetSystemMetrics函式得到的资讯,该程式以像素为单位显示了视讯显示的宽度和高度。GetSystemMetrics是一个能用来获得Windows中不同物件的尺寸资讯的函式。