字符格式基础
首先说说字符编码的两种格式。
一种是将文本字符串编码成一组以0结尾的单字节字符,即ansi字符,每个字符占8位,即一个字节。这种编码方式的缺陷是,对于字符本地化的问题不能很好的解决,因为8位空间最多容纳256个字符,不能完全唯一的表示世界上所有的字符。
另一种是Unicode字符,每个字符都使用UTF-16编码(一般来说,除非特别声明不使用UTF-16编码),即一个字符占16位空间(2字节)。基于这种情况,世界上大多数语言的字符都可以用一个唯一的2字节空间表示。
传统的ansi字符在C语言中用char数据类型表示(1字节)。在源代码中声明一个字符串,则编译器会转换为由8位char数据类型构成的一个数组(以"/0"结尾)
char c='a';//一个8位的字符
char szbuffer[100]="a string";//含有99个8位字符和1个8位结束符(/0)的数组
微软的c/c++编译器为unicode定义了一个内建的数据结构,wchar_t,它表示一个16位的unicode(UTF-16)字符。
wchar_t c=L'a';//一个16位的字符
wchar_t szbuffer[100]=L"a string";//含有99个16位字符和1个16位结束符(/0)的数组
此外,windows为了和c语言加以区别,则定义了自己的数据类型。
//WinNT.h
typedef char CHAR;//一个8位的字符
typedef wchar_t WCHAR;//一个16位的字符
typedef CHAR *PCHAR;//指向8位字符的指针
typedef CHAR *PSTR;//指向8位字符的指针
typedef CONST CHAR *PCSTR;//指向8位字符的常量指针
typedef WCHAR *PWCHAR;//指向16位字符的指针
typedef WCHAR *PWSTR;//指向16位字符的指针
typedef CONST WCHAR *PCWSTR;//指向16位字符的常量指针
在实际使用中,使用哪种类型并不重要,但最好保持一致,这样有利于维护。另外如果是windows编程,最好使用windows定义的数据类型,这样可与MSDN保持一致,有利于开发。
windows还定义了如下宏和通用数据类型,来方便开发人员。
#ifdef UNICODE
typedef WCHAR TCHAR ,*PTCHAR,PTSTR;
typedef CONST WCHAR *PCTSTR;
#define __TEXT(quote) quote
#define __TEXT(quote) L##quote
#else
typedef CHAR TCHAR ,*PTCHAR,PTSTR;
typedef CONST CHAR *PCTSTR;
#define __TEXT(quote) quote
#endif
#define __TEXT(quote) L##quote
其中黑体部分的类型都是通用数据类型,即无论使用ansi还是unicode,都能通过编译。
TCHAR c=TEXT('A');//如果定义了unicode,则a是16位字符;如果定义了ansi,则a是8位字符
TCHAR SZBUFFER[100]=TEXT("A STRING");//如果定义了unicode,则SZBUFFER中的字符为16位;否则,字符位8位。
自windowsNT开始,windows所有版本都使用unicode构建。即如果我们的程序使用ansi字符,那么操作系统将会执行转换为unicode,这些转换都是对用户透明的,且会产生时间和内存的开销。所以,我们应尽量使用unicode构建程序。
至此为止,总结一下,就是定义字符用TCHAR,指针类型使用*TCHAR足以,定义字面值用_T宏。
windows中的函数的参数列表中如果有字符串,则该函数通常有两个版本,一个ansi,一个unicode。例如CreateWindowEx接受这两种类型的字符串作为参数。实际上,CreateWindowEx只是一个宏,定义如下:
//WinUser.h
#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else
#define CreateWindowEx CreateWindowExA
#endif
很明显,这和前面的通用数据类型使用的是同一伎俩。其中CreateWindowExW接受Unicode,CreateWindowExA接受ansi。
在windows vista中,CreateWindowExA只是一个转换层,负责分配内存,然后将ansi转换为unicode,然后在内部调用CreateWindowExW。
C运行库中的安全字符串处理函数
通常,修改字符串的函数都存在一个安全隐患,即目标字符串的缓冲不够大,则会导致内存中的数据被破坏。
例如:
WCHAR szbuffer[3]=L"";
wcscpy(szbuffer,L"abc");
以上的例子是以0结尾的,需要szbuffer[4]才可以容纳。但编译时并不会有任何报错或警告。
针对以上问题,我们必须使用“安全字符串处理函数”,这一类的函数以_s结尾(secure之意),我们来看一下这类函数的原型.
PTSTR _tcscpy(PTSTR strDestination,PCTSTR strSource);
errno_t _tcscpy_s(PTSTR strDestination,size_t numberOfCharacters,PCTSTR strSource);
PTSTR _tcscat(PTSTR strDestination,PCTSTR strSource);
errno_t _tcscat_s(PTSTR strDestination,size_t numberOfCharacters,PCTSTR strSource);
可以看到,在将一个缓冲区作为目标缓冲区时,必须提供这个缓冲区的大小(可容纳的字符个数),通过调用_countof宏计算出来。
这个缓冲区大小参数的主要任务就是验证缓冲区是否足以容纳结果数据.
现在,我们调用这些安全函数的时候,就可以检查返回的errno_t值。只有返回S_OK值,才表明函数调用成功。其他的值可以参照errno.h中的定义。
来看一个例子:
TCHAR szbuffer[3]=L"";
_tcscpy_s(szbuffer,_countof(szbuffer),_T("abc"));
执行完后, szbuffer的第一个字符被设置为'/0',而其他所有字节全部被设置为0xfd(填充符)。
在处理字符串时获得更多的控制
除了安全字符串处理函数,c运行库还增加了一些函数,用于在处理字符串时获得更多的控制。例如,可以控制如何截断字符串。
这类函数具有两个版本,对应ansi和unicode。
HRESULT StringCchCat( LPTSTR pszDest,size_t cchDest,LPCTSTR pszSrc);
HRESULT StringCchCatEx( LPTSTR pszDest,size_t cchDest, LPCTSTR pszSrc,LPTSTR *ppszDestEnd,size_t *pcchRemaining,DWORD dwFlags);
HRESULT StringCchCopy( LPTSTR pszDest,size_t cchDest, LPCTSTR pszSrc);
HRESULT StringCchCopyEx(LPTSTR pszDest,size_t cchDest, LPCTSTR pszSrc, LPTSTR *ppszDestEnd,size_t *pcchRemaining,DWORD dwFlags);
可以看出,在所有方法的名称中,都含有一个“Cch”,这表示count of characters,即字符个数。通常使用_countof宏获得。
另外还有一些函数含有“cb”,这表示函数要求用字节数来指定大小。通常使用sizeof()获得。
这些函数返回HRESULT,具体的值
S_OK | 成功。目标缓冲区中包含源字符串,并以/0终止 |
STRSAFE_E_INVALID_PARAMETER | 失败。将NULL传给了一个参数 |
STRSAFE_E_INSUFFICIENT_BUFFER | 失败。指定的目标缓冲区太小,无法容纳整个源字符串 |
不同于安全字符串处理函数,这类函数运行时,当缓冲区太小,则会执行截断。
windows字符串处理函数
在shlwapi.h定义了大量好用的字符串函数,可以用来对操作系统有关的数值进行格式化操作。
在实际编程中,经常需要比较两个字符串是否相等。对于这样的需求可以使用CompareString(Ex)或者CompareStringOrdinal。
CompareString(Ex)一般用于向用户显示的字符串。
CompareStringOrdinal一般用于比较程序内部的字符串,如路径名,注册表项值等。
CompareString(Ex)的处理速度相对于CompareStringOrdinal较慢。
这两个函数的返回0,则表示调用失败;
返回1(CSTR_LESS_THAN)表明string1小于string2;
返回2(CSTR_GREATER_THAN)表明string1大于string2.
使用unicode的好处:
1、有利于应用程序的本地化。
2、代码执行效率更高,因为windows基于unicode构建,如果我们也保持一致,则windows处理时不必耗费额外的资源转换。
3、容易与com集成
4、容易与.net集成
推荐的字符处理方式
1、使用通用数据类型和宏,如TCHAR和_T
2、将字符串想象成字符的数组,而非char或字节的数组。
3、与字符串有关的计算需修改。尤其要搞清楚很多的函数要求传递的是字符数还是字节数,如是前者,则用_countof()获取;如是后者,则用(字符数*sizeof(字符类型))获取。
4、避免使用printf系列函数转换ansi和unicode。正确的做法是使用MultiByteToWideChar或WideCharToMultiByte。
5、始终使用安全的字符串处理函数,_s或stringcch,如要控制截断,则使用后者。
6、如果一个缓冲区处理函数的参数中不包括目标缓冲区的长度,那么避免使用。
7、比较字符串时,使用CompareStringOrdina和CompareString(Ex)。前者速度快,适合处理程序内部的字符串;后者用于处理UI的字符串,因为它是以区域设置来适当排序,速度较慢。