Windows日益流行,本地化Windows应用程序将是一大问题,其中一个核心的问题就如何处理不同字符集的问题。
字符编码
很久以来我们都在使用ANSI编码来表示一个字符串,它是一个“以0结尾的单字节字符数组”。但是单个字节只能表示256个符号,难以应付世界上众多语言。
之后世界上出现了DBSC双字节字符集,一个字符串中的每个字符都由1个或2个字节组成。但是每次使用都要判断哪些是一个字节,哪些是两个字节。
1988年出台的Unicode编码很好的解决了这个问题。我们有很多UTF(Unicode Transformation Format :Unicode转换格式)标准,可以提供使用:
- UTF-8:UTF-8将一些字节编码为1字节,有些编码为2字节,有些编码为3字节,有些编码为4字节。值在0x0080(ASCII范围内)以下的字符压缩为1字节,很好的应对英文格式;0x0080~0x07FF之间的字符转换为2字节,适合欧洲和中东语言;0x0800以上的都用3字节,这适合东亚地区语言;代理对(surrogate pair)被写为4字节。UTF-8相当流行,但是对大量0x0800以上字符编码的适合效率不高。
- UTF-16:UTF-16为每个字符编码为2个字节。世界上大量的语言,每个字符都可以用16位来表示,所以应用程序很容易辨认字符串并计算它的长度。遇到某些语言16位长度无法表示的时候,UTF-16支持代理(surrogate),用32位来表示字符。UTF-16在节省空间和简化代码之间做出了很好的折中。.NET Framework始终使用UTF-16编码,所以如果我们需要在本机代码和托管代码之间传递字符,使用UTF-16能改进性能减少消耗。
- UTF-32:每个字符都编码为4字节。这种格式从内存使用角度来说并不高效,使用率不高,但是并非毫无益处。如果打算写一个算法来遍历字符但是不想处理字节数不定的字符,就可以使用UTF-32。
数据类型
C语言使用char数据类型来表示一个8位的ANSI字符。
在源代码中声明字符串时,C编译器会把我们字符串中的字符转换成由8位char数据类型构成的一个数组:
char c= 'A';
char szBuffer[100] = "A String";
为了支持Unicode字符,Microsoft的C/C++编译器定义了一个内建的数据类型wchar_t,它表示一个16位的UTF-16字符。声明wchar_t字符的方法如下:
wchar_t c = L'A';
wchar_t szBuffer[100] = L"A String";
大写字母L通知编译器该字符应当编译为一个Unicode字符串。
为了和C语言进行区分,Windows头文件WinNT.h中定义了以下类型:
typedef char CHAR; //8bit字符
typedef wchar_t WCHAR; //16bit字符
//指向8bit字符(串)的指针
typedef CHAR *PCHAR;
typedef CHAR *PSTR;
typedef CONST CHAR *PCSTR;
//指向16bit字符(串)的指针
typedef WCHAR *PWCHAR;
typedef WCHAR *PWSTR;
typedef CONST WCHAR *PCWSTR;
另外WinNT.h还定义了一个宏TEXT(),用这个宏自动适配到底使用ANSI还是Unicode。
Windows中的Unicode函数和ANSI函数
目前Windows都用Unicode来构建。所有核心函数都需要Unicode字符串。调用函数如果传进的是ANSI字符串,系函数首先把字符串转为Unicode,如果希望返回ANSI,操作系统会把Unicode转为ANSI。执行这些操作会花费额外的时间和内存开销。
Windows系统函数如果参数有字符串,那么通常这个函数有两个版本,如CreateWindowEx既可以接收ANSOI,也能接收Unicode。但是这个函数的原型如下:
HWND WINAPI CreateWindowExW(………………); //W代表wide,接收16位Unicode
HWND WINAPI CreateWindowExA(………………); //A代表ANSI,接收8位ANSI
虽然有两个原型,但是我们调用的时候不必调用这两个,只需要调用CreateWindowEx。WinUser.h中定义了如下宏:
#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else
#define CreateWindowEx CreateWindowExA
#endif
虽然有两个版本,但是实际开发中最好还是使用Unicode,可以避免额外开销和bug。
如果是要开发DLL,可以在DLL中提供两个版本,其中ANSI版本的仅仅分配内存,执行字符串转换,然后调用该函数的Unicode版本。
资源编辑器编译完所有资源后,输出文件就是一个二进制形式。资源中的所有字符串都是Unicode形式保存的。如果没有定义UNICODE宏,那么操作系统将执行内部转换。
C运行库中的Unicode函数和ANSI函数
C运行库和Windows系统一样,也是一个函数提供两个版本。但是和Windows不同,C运行库中的ANSI函数不会把字符串转换为Unicode形式,再调用Unicode版本了。所有函数都是自力更生的。
必然C运行库中的字符串长度函数,strlen返回一个ANSI字符串长度,wcslen返回Unicode字符串长度。
上述两个函数都在Strlen.h中定义。为了既能用ANSI编译,也能用Unicode编译,还必须包含TChar.h,该文件有如下宏:
#ifdef _UNICODE
#define _tcslen wcslen
#else
#define _tcslen strlen
#endif
C运行库中的安全字符串函数
repel-attacks-with-visual-studio-2005-safe-c-and-c-libraries
StrSafe.h介绍
安全函数替代列表
传统方式修改字符串会有安全隐患,如果目标字符串缓冲区不够大,就会导致内存中数据被破坏。类似与strcpy这种函数根本不知道缓冲区长度,函数自己不知道可能破坏内存,所以不会向程序报告错误。
现在我们可以用安全函数来替代这些不安全的函数。(由于strlen、wcslen、_tcslen不修改字符串,所以这些函数并没有破坏内存的隐患,但是会有其他隐患:这些函数默认字符串以’\0’终止,但是实际情况却不一定)
microsoft的头文件StrSafe.h文件中定义了所有的安全字符串函数。在应用程序包含StrSafe.h时,String.h也会包含进来。要注意必须再包含其他文件之后再包含StrSafe.h。
现在的每一个字符串函数都有其新版本,前面名称相同,后面添加 _s(secure)。我们在将一个可写的缓冲区作为参数传递时,必须同时提供它的大小,这个值应该是一个字符数。我们对缓冲区用 _couontof宏(stdlib.h)可以很容易计算出这个值。
获取安全函数运行结果
安全函数首要任务是验证传入的所有参数是否合法,如果有某一项检查结果失败,函数会设置局部于线程的C运行时变量 errno,并返回一个 errno_t值来指出成功或失败。当然这些系统的函数并不会实际返回。
在调试版构建时(debug build),如果检查有错误,系统会弹出Debug Assertion Faild对话框,然后中止程序进行。在发行版构建(release build)中,不会有弹窗出现,直接退出程序。
我们可以提供自己的函数,当检测到无效参数时调用我们的函数,进行我们自己的操作。我们必须要先定义好一个函数:
void InvalidParameterHander(
PCTSTR expression, //运行时可能出现的函数调用失败,如(L"Buffer is too small" && 0)
PCTSTR function, //错误的函数名称
PCTSTR file, //源代码文件
unsigned int line, //源代码行号
uintptr_t /*pReserved*/
);
只有在测试调试版构建时才适合使用上述函数来记录错误,其余情况这些参数都应赋值为NULL。因为这些显示方式对用户并不友好。在发行版构建中应使用更加友好的方式来替换对话框。
在定义好函数后,下一步是调用 _set_invalid_parameter_handler来注册这个处理程序。然后在应用程序开头调用 **_CrtSetReportMode(_CRT_ASSERT,0)**来禁止运行时触发Dubug Assertion Failed对话框。
现在我们在使用安全函数的时候,就可以检查获取的返回值 errno_t来检查结果,所有结果在errno.h中有定义,只有返回S_OK才代表成功。
处理字符串时进行更多控制
除了以 _s结尾的安全函数之外,C运行库还有其他函数来提供更多控制,如控制填充符,或者指定如何进行截断。C运行库也为这些函数准备了ANSI(A)版本和Unicode(W)版本。部分函数如下:
HRESULT StringCchCat(PTSTR pszDest,size_t cchDest,PCTSTR pszSrc);
HRESULT StringCchCatEx(PTSTR pszDest,size_t cchDest,PCTSTR pszSrc,
PTSTR *ppszDestEnd,size_t *pcchRemaining,DWORD dwFlags);
这些函数中都有一个“Cch”,这表示Count of characters,即字符数,可以用 _countof宏来获取此值。另外除了Cch还有一系列函数中有“Cb”,这些函数要求使用字节数而不是字符数来指定大小,用sizeof操作符来获取此值。
这些函数返回的HRESULT取值如下:
HRESULT | 描述 |
---|---|
S_OK | 成功。目标缓冲区包含源字符串并以’\0’结尾 |
STRSAFE_E_INVALID_PARAMETER | 失败,将NULL传给了一个参数 |
STRSAFE_E_INSUFFICIENT_BUFFER | 失败。指定目标缓冲区太小,无法容纳整个源字符串 |
不同与安全函数,缓冲区太小,最终字符串会截断为一个空字符串。这些函数在缓冲区太小时,源缓冲区中可以被写入的那一部分会被复制,最后一个可用字符被设置为’\0’。最终需要哪种结果需要我们自行判断。