字符集历史
莫斯码,Braille盲文
American标准
1980年,the coding used on Hollerith cards
1960年,BCDIC(Binary-Coded Decimal Interchange Code)从6位扩展到8位的EBCDIC。
1950s后期,American Standard Code for Information Interchange(ASCII), 1967年完成。7位。
The World Beyond
7位ASCII的问题是,还有很多常用的符号没有包含进来,比如拉丁文,还有汉字,日文韩文等。
Extending ASCII
一个字节8位,还有128个额外的字符可以附加进来。1981年,IBM PC引入了一些声调符,小写希腊字母等。这套扩展的字符集并不是并不适合Windows。
在Windows1.0中,微软搞了套ANSI字符集,最终定为“American National Standard for Information Processing---8-Bits Single-Byte Coded Graphic Character Sets-Part 1:Latin Alphabet No 1”,简称”Latin 1”
MS-DOS 3.3 引入了code pages的概念。所谓code pages,即为字符码集到字符集的映射。最初的IBM字符集被称为code page 437,或”MS-DOS Latin US”. 而code page 850被称为”MS-DOS Latin 1”.
在MS-DOS下,当用户设置PC的键盘,视频显示器或打印机到某一个特定的code page后,字符将由该code page定义给出。但code page的不同,会给应用程序带来很大麻烦。
双字节字符集
double-byte character set(DBCS).Windows支持四种DBCS,code page 932(日文),936(简体中文),949(韩文),950(繁体中文)。
DBCS的一个问题是,ASCII字符是单字节,而其他是双字节的字符。双字节的字符由一个lead byte和trail byte组成,通常可以通过判断一个字符的第一个字节是否是lead byte,来判断它是否是一个双字节的字符。在编程中,这样会有麻烦,比如给定一个字节流,某个字符处的指针,那么该字符的前一个字符的地址是什么呢?由于无法判断前一个字符是单字节的,还是双字节的,必须我们得从字节流的开头处重新parse。
救星Unicode
16位字符。取代混乱的多个256字符的code映射,或者DBCS,Unicode是一套统一的字符系统,每个字符为16位,支持65536个字符。
Unicode的组成是这样的,前128个位ASCII(0x0000 to 0x007F),0x0080~0x00FF为ISO 8859-1的ASCII扩展,000370~0x03FF为希腊字母,0x0400~0x04FF为西里尔字母,0x0530~0x058F为亚美尼亚语 0x0590~0x05FF为希伯来语,0x3000~0x9FFF(CJK)为中日韩的字符总集。
Unicode最好的地方在于,它只有一个字符集。
Unicode的缺点是,占内存多。另外,unicode推广不力,还没有被广泛使用,也是其弱点之一。
宽字符和C语言
ANSI C(American National Standard for Programming Languages--C)支持宽字符。
ANSI C也支持多字节字符集。
宽字符不是unicode,unicode只是宽字符的一种编码(encoding)。但在本书中,意义差不多。
char 类型
char数组的定义
1
2
|
char
a[]=
"Hello!"
;
//defined globally
static
char
a[] =
"Hello!"
//defined as a local variable
|
它们都存放在程序的静态区。都需要7个字节(还有一个0终结符)。
宽字符
1
|
typedef
unsigned
short
wchar_t
;
|
为16位的。定义宽字符及宽字符字符串的例子如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
wchar_t
c =
'A'
;
//此时c为双字节值0x0041,为unicode中的字母A
//如果机器是小端(least-significant bytes first)
//在内存中的表示为0x41 0x00
unsigned
char
* cp = (unsigned
char
*)(&c);
wchar_t
b =
'高'
;
//在内存中的表示为0xDF 0xB8
cp = (unsigned
char
*)(&b);
wchar_t
* p = L
"Hello!"
;
//L让编译器知晓,字符串中的字符是宽字节字符,
//一共占14个字节(最后的0终结符也占2个字节)
//类似的,我们可以如下定义一个宽字符数组
static
wchar_t
a[] = L
"Hello!"
;
|
宽字符库函数
1
2
3
4
5
6
7
8
9
10
11
|
wchar_t
* pw = L
"Hello!"
;
//如不加(const char *)强制转换,则会有编译错误
//cannot convert parameter 1 from 'wchar_t *' to 'const char *'
//加上强制转换后,由于宽字节字符'H'在内存中的内容为0x48,0x00(小端),所以
//使用strlen时,结果为1,碰到了0x00,而实际上这个0x00是宽字符的一部分。
int
iLength =
strlen
((
const
char
*)pw);
//上述例子中,运行库函数strlen是在运行时加载的,它期望的是单字节字符。
//为了支持宽字符,运行库加入了相应的函数版本。
//strlen的宽字符版本为wcslen(wide-character string length)
iLength = wcslen(pw);
//结果为6
|
printf的宽字符版本为wprintf。
维护一份源代码
只要修改一个宏,同一份源代码就可以编译成unicode版本或多字节版本。在windows中,一个解决方法是使用TCHAR,它不属于标准C,所以其中的每个函数和定义都有下划线前缀。在TCHAR.H头文件中,如果定义了_UNICODE,
则有
1
|
#define _tcslen wcslen
|
如果_UNICODE没有定义,则有
1
|
#define _tcslen strlen
|
同样的,TCHAR在unicode下位wchar_t,在非unicode下为char
1
2
3
4
5
6
|
//if _UNICODE defined
typedef
wchar_t
TCHAR
#define __T(x) L##x
//else
typedef
char
TCHAR
#define __T(x) x
|
所以,建议使用_TEXT或_T宏将字符串常量包起来。其中,##是token paste
宽字符和Windows
Windows NT内部使用的是16位字符的字符串。Win98只有一小部分函数支持unicode。最好只维护一份源代码,这样可以根据实际情况编译成ascii版本或unicode版本。
Windows头文件类型
WINDOWS.H 包括一系列windows头文件
WINDEF.H 定义了windows中的很多基本类型,包含WINNT.H
WINNT.H 对unicode的支持
在WINNT.H中,首先包含了C头文件CTYPE.H,其中定义了wchar_t。然后定义了CHAR和WCHAR
1
2
|
typedef
char
CHAR
;
typedef
wchar_t
WCHAR
;
|
WCHAR的匈牙利前缀为wc。
随后WINNT.H定义了6个数据类型,为指向8位字符的字符串的指针,以及四种数据类型,为指向8位字符常量字符串的指针。如下所示
1
2
|
typedef
CHAR
*
PCHAR
,*LPCH,*PCH,*NPSTR,*
LPSTR
,*
PSTR
;
typedef
CONST
CHAR
* LPCCH,*PCCH,*
LPCSTR
,*
PCSTR
;
|
N前缀表示”near”,L表示”long”,在Win16中表示指针大小的不同。在Win32中无区别。这里需注意,LPCH等都是类型char *。
类似的,WINNT.H也定义了宽字符字符串的指针和常量字符串的指针,如下
1
2
|
typedef
WCHAR
*
PWCHAR
,*LPWCH,*PWCH,*NWPSTR,*
LPWSTR
,*
PWSTR
;
typedef
CONST
WCHAR
* LPCWCH,*PCWCH,*
LPCWSTR
,*
PCWSTR
;
|
使用TCHAR以及相关的指针类型可以把UNICODE和非UNICODE的代码统一起来,如下
1
2
3
4
5
6
7
8
9
|
#ifndef 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
|
谨记,由于windows.h包含了很多基本的类型,在包含其他头文件时,最好现在最开始包含windows.h.
综上,有以下三点经验
1.如明确使用8位字符,请使用CHAR,PCHAR
2.如明确使用16位字符,请使用WCHAR,PWCHAR,以及加L的字符串常量
3.若依赖于UNICODE标识符定义如否,请使用TCHAR,PTCHAR和TEXT宏。
Windows函数调用
32位的Windows API其实没有MessageBox这个函数,只有MessageBoxA(ASCII版本)和MessageBoxW(宽字节版本)。但是程序员可以放心地使用MessageBox,原因见如下代码:
1
2
3
4
5
6
7
|
int
WINAPI MessageBoxA(
HWND
hWnd,
LPCSTR
lpText,
LPCSTR
lpCaption,
UINT
UType);
int
WINAPI MessageBoxW(
HWND
hWnd,
LPCWSTR
lpText,
LPCWSTR
lpCaption,
UINT
UType)
#ifndef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
|
Windows的字符串函数
除了C运行库提供的字符串函数外,微软自己也提供了一些变种函数。比如
1
2
3
4
5
6
7
|
int
iLength = lstrlen(pw);
pString = lstrcpy(pString1,pString2);
pString = lstrcpyn(pString1,pString2);
pString = lstrcat(pString1,pString2);
iComp = lstrcmp(pString1,pString2);
iComp = lstrcmpi(pString1,pString2);
|
使用 printf
坏消息是,在windows程序中,无法使用printf函数,好消息是,可以使用sprintf,该函数可将格式化文本写入buffer。sprintf的用例如下:
1
2
3
|
char
szBuffer[100];
sprintf
(szBuffer,
"The sum of %i and %i is %i"
,5,3,5+3);
puts
(szBuffer);
|
使用sprintf的一个麻烦之处在于,需要考虑buffer的大小。另一个win32平台的函数_snprintf解决了这个问题,它引入了表示buffer大小的参数。sprintf还有一个变形vsprintf,它只有三个参数,前两个参数与sprintf一致,第三个为指向参数数组的指针。而该指针其实是存在栈上的变量,访问这些栈上变量时,需借助于va_list,va_start,va_end等宏。
sprintf函数即可如下实现:
1
2
3
4
5
6
7
8
9
|
int
MySprintf(
char
* szBuffer,
const
char
* szFormat,...)
{
int
iReturn;
va_list
pArgs;
va_start
(pArgs,szFormat);
iReturn =
vsprintf
(szBuffer,szFormat,pArgs);
va_end
(pArgs);
return
iReturn;
}
|
1
|
测试之
|
1
2
3
4
5
6
7
|
int
inumber = 30;
float
fnumber = 90.0;
char
str[4] =
"abc"
;
char
szBuffer[100];
MySprintf(szBuffer,
"%d %f %s"
,inumber,fnumber,str);
puts
(szBuffer);
return
0;
|
上面的va_start,实际上即将变量szFormat后的变量地址赋予了pArgs。具体的宏va_list,va_end,va_start如下
1
2
3
4
5
|
typedef
char
*
va_list
;
typedef
va_start
_crt_va_start
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
typedef
va_end
_crt_va_end
#define _crt_va_end(ap) ( ap = (va_list)0 )
|
后来,微软又加了wsprintf和wvsprintf,可惜的是,它们不支持浮点数格式化。随着宽字符的引入,sprintf人丁兴盛,让人有点糊涂。先总结于下
ASCII | 宽字符 | Generic | |
参数个数可变 | |||
标准版本 | sprintf | swprintf | _stprintf |
最大长度版本 | _snprintf | _snwprintf | _sntprintf |
win版本 | wsprintfA | wsprintfW | wsprintf |
数组参数指针 | |||
标准版本 | vsprintf | vswprintf | _vstprintf |
最大长度版本 | _vsnprintf | _vsnwprintf | _vsntprintf |
win版本 | wvsprintfA | wvsprintfW | wvsprintf |
Formatting Message Box
下面的程序SCRNSIZE展示了如何实现一个MessageBoxPrintf函数,以接受可变数量的参数,并像printf那样格式化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
int
CDECL MessageBoxPrintf(
TCHAR
* szCaption,
TCHAR
* szFormat,...)
{
TCHAR
szBuffer[1024];
va_list
pArglist;
va_start
(pArglist,szFormat);
_vsntprintf(szBuffer,
sizeof
(szBuffer) /
sizeof
(
TCHAR
),szFormat,pArglist);
va_end
(pArglist);
return
MessageBox(NULL,szBuffer,szCaption,0);
}
int
WINAPI WinMain(
HINSTANCE
hInstance,
HINSTANCE
hPrevInstance,
PSTR
szCmdLine,
int
iCmdShow)
{
int
cxScreen,cyScreen;
cxScreen = GetSystemMetrics(SM_CXSCREEN);
cyScreen = GetSystemMetrics(SM_CYSCREEN);
MessageBoxPrintf(TEXT(
"ScrnSize"
),
TEXT(
"The screen is %i pixels wide by %i pixels high"
),
cxScreen,cyScreen);
return
0;
}
|
1
|
|
1
|
|
1
|
|
该程序展示了屏幕的分辨率。这里有一个CDECL需要解释一下,和__stdcall(WINAPI)一样,都是函数的调用方式。介绍如下
1.__stdcall声明的函数被调用时,主调方负责对参数压栈,而参数出栈的任务由被调函数完成,这样,被调函数必须知道压栈参数
的个数,所以,带可变数量参数的函数不能用__stdcall声明。
2.cdecl声明的函数被调用时,主调方负责对参数的压栈,并在调用返回后,再负责参数出栈,由于主调方知道压入的参数个数,
所以被调函数可带可变数量参数。这样生成的汇编码会更多,执行程序会更大(调用函数的次数总会比较比较多的嘛)。
国际化
本书不涉及,参考Developing International Software for Windows 95 and Windows NT。
本书的程序将在UNICODE设置与否的情况下成功编译,普遍使用TCHAR和TEXT.并努力不要将byte和字符混淆。