《Windows核心编程》读书笔记二 字符和字符串处理

来源:互联网 发布:sql语句统计各个数量 编辑:程序博客网 时间:2024/06/05 15:27

第二章 字符和字符串处理


本章内容

2.1 字符编码

2.2 ANSI字符和Unicode字符与字符串数据类型

2.3 Windows中的Unicode函数和ANSI函数

2.4 C运行库中的Unicode函数和ANSI函数

2.5 C运行库中的安全字符串函数

2.6 为何要用Unicode

2.7 推荐的字符和字符串处理方式

2.8 Unicode与ANSI字符串的转换


Windows Vista开始提供Unicode 5.0的支持(参考 Extend The Global Reach Of Your Applications With Unicode 5.0 MSDN上的网址已经过期)

缓冲区溢出错误已经成为针对应用乃至操作系统的各个组件发起安全攻击的媒介。


2.1 字符编码

双字节字符集(double-byte character set, DBCS)为了支持不同国家语言的字符。

例如日文字符 第一个字符值在 0x81~0x9F 或者 0xE0 ~0xFC之间,就必须检查下一个字节,才能判断一个完整的汉字。


Unicode是1988年由apple和Xerox共同建立的一项标准。参考 www.Unicode.org


在Windows Vista中,每个Unicode字符采用UTF-16编码。 UTF的全程是Unicode Transformation Format(Unicode转换格式) 每个字符编码为2个字节。

在Windows系统上讨论Unicode除非特别申明,默认都是指代UTF-16。 能够表示大部分国家的语言。

某些语言无法表示,UTF-16支持代理(surrogate)后者使用32位来表示一个字符。所以UTF-16在节省空间和简化编码这两个目标之间提供了一个折中。

.net frameworkd使用UTF-16来进行编码。


还有其他UTF标准

UTF-8  编码采用1~4字节。

值在  0x0080 以下采用1个字节 支持ASCII

0x0080~0x07ff采用2个字节  支持欧洲和中东地区语言

0x0800  以上采用3个字节,适合东亚地区

代理(surrogate pair)被写为4个字节

UTF-8在对值0x0800以上进行编码和处理的时候不如UTF-16高效。


UTF-32 所有字符编码为4个字节。如果想写一个算法遍历字符(支持任何语言),但又不想处理字节数补丁的字符,这种编码方式就非常有用。

例如采用UTF-32 就不用关心(surrogate)的问题。


Unicode还为个国家语言提供了码位。


2.2 ANSI字符和Unicode字符与字符串数据类型

c语言默认采用char 表示ANSI8位字符

wchar_t 来存储utf16字符

早期编译器不支持wchar_t采用 typedef unsigned short wchar_t来支持。

wchar_t c = L'A';

wchar_t = szBuffer[] = L"A String";

L表示字符串以UTF16编码


Windows开发团队定义了自己的数据类型

typedef char CHAR; // 8 bit chararcter

typedef wchar_t WCHAR; // 16 bit character


还有winnt.h中

typedef CHAR * PCHAR;

typedef CHAR * PSTR;

typedef CONST CHAR * PCSTR;


typedef WCHAR * PWCHAR;

typedef WCHAR* PWSTR;

typedef CONST WCHAR *PCWSTR;


在源码中使用哪种数据类型并不重要,重要的是保持数据类型一致性。如果开发WINDOWS应用建议使用Windows数据类型,与MSDN文档相符。

也可以使用TEXT宏支持ANSI和Unicode编译切换。


2.3 Windows中的Unicode函数和ANSI函数

windows NT开始内部全部采用Unicode构建。 如果调用Win32 API传入ANSI字符内部会进行转换,反之亦然。

Windows一般会提供两套函数一个支持Unicode 一个支持ANSI

例如

WINUSERAPIHWNDWINAPICreateWindowExA(    _In_ DWORD dwExStyle,    _In_opt_ LPCSTR lpClassName,    _In_opt_ LPCSTR lpWindowName,    _In_ DWORD dwStyle,    _In_ int X,    _In_ int Y,    _In_ int nWidth,    _In_ int nHeight,    _In_opt_ HWND hWndParent,    _In_opt_ HMENU hMenu,    _In_opt_ HINSTANCE hInstance,    _In_opt_ LPVOID lpParam);WINUSERAPIHWNDWINAPICreateWindowExW(    _In_ DWORD dwExStyle,    _In_opt_ LPCWSTR lpClassName,    _In_opt_ LPCWSTR lpWindowName,    _In_ DWORD dwStyle,    _In_ int X,    _In_ int Y,    _In_ int nWidth,    _In_ int nHeight,    _In_opt_ HWND hWndParent,    _In_opt_ HMENU hMenu,    _In_opt_ HINSTANCE hInstance,    _In_opt_ LPVOID lpParam);


通常调用CreateWindowEx 具体调用哪个版本有UNICODE宏确定

#ifdef UNICODE#define CreateWindowEx  CreateWindowExW#else#define CreateWindowEx  CreateWindowExA#endif // !UNICODE

在WindowsVIsta中CreateWindowExA 只做ANSI到Unicode的转换然后调用CreateWindowExW,

当CreateWindowExW返回时,CreateWindowExA再释放堆中的变量。

为了避免转换ANSI和Unicode的系统开支,建议直接默认使用Unicode


如果是创建DLL 也可以考虑这种技术提供两种版本的导出函数,根据宏选择对应的版本。ANSI版本只负责字符转换,内部调用Unicode版本。


Windows API中一些老的api 例如WinExec和OpenFile为了兼容老的16位windows程序只有ANSI版本,应该用CreateProcess和CreateFile函数来替代。

从VISTA开始微软趋向只提供Unicode的版本。如ReadDirectoryChangesW 和 CreateProcessWithLogonW


COM默认也采用16位Unicode


默认编译的资源也是Unicode,如果调用LoadStringA 其实是读取了Unicode以后被转换成了ANSI版本。


2.4 C运行库中的Unicode函数和ANSI函数

c运行库中的ANSI版本是自力更生的,不会先转换再调用Unicode版本。例如strlen

Unicode版本也是自力更生,不会转换再调用ANSI版本。例如wcslen

还有宏 _tcslen

_UNICODE宏是C运行库的unicode标识。


2.5 C运行库中的安全字符串函数

例如strcpy  wcscpy并不检查缓冲区大小(堆空间大小)

使用_tcscpy_s 代替 _tcscpy



2.5.1 初识新的安全字符串函数

包含StrSafe.h 默认会包含string.h (包含在最末尾)

_s的函数增加了  一个字段  size_t numberOfCharacters 也就是buff的大小(字符数)

通过调用_countof宏获取


_s的函数会做各种检查才继续执行,如果检查失败会设置C运行库的运行时变量errno 然后返回errno_t值来指出成功或失败。

在Debug中会直接弹出Assert

在Release版中直接终止应用程序


C运行时实际上允许我们提供自己的函数,这样一来,他在检测到一个无效参数时,就会调用此函数。然后这个函数中,我们可以记录失败,附上一个调试器等。

首先定义一个函数

void InvalidParameterHandler(PCTSTR expression, PCTSTR function,PCTSTR file, unsigned int line, uintptr_t /*pReserved*/);

expression描述了C运行时可能出现的函数调用失败。function,file和line显示了出错的函数,文件 和行号。 但是不应该像最终用户显示这些。


再非DEBUG中默认这些参数都为NULL。不应该直接显示详细的调试信息。而应该计入日志,或者重启应用程序等。


下一步是调用_set_invalid_parameter_handler来注册处理程序。 但是Debug Assert对话框还是会出现

应该在程序开头的地方设置 _CrtSetReportMode(_CRT_ASSERT, 0)从而禁止C运行时触发的所有Debug Assertion Failed对话框。


调用安全函数时候检查返回值errno_t为S_OK时才表明调用是成功的。

测试代码

#include <tchar.h>#include <windows.h>int main(int argc, char* argv[]){TCHAR szBefore[5] = TEXT("BBBB");TCHAR szBuffer[10] = TEXT("---------");TCHAR szAfter[5] = TEXT("AAAA");errno_t result = _tcscpy_s(szBuffer, _countof(szBuffer),TEXT("0123456789"));return 0;}

执行_tscpy_s之前


执行后





Buffer被填充了 00 00 fe fe ....

(因为编译器进行运行时检查标志的结果)


2.5.2 在处理字符串时如何获得更多控制

C运行库增加了一些函数,在执行字符串处理时提供更多的控制


所有函数都有Cch 表明支持Count of characters 通常使用_countof宏来取得此值

还有一些带Cb的函数,StringCbCat, StringCbCopy 和 StringCbPrintf。这些函数要求用字节数来指定大小,而不是字符数 (Count Byte) 通常用sizeof来取得此值

返回值HRESULT



不同于安全_s 函数,如果buff太小,这些函数会执行截断。。截断的值被复制,末尾被设置为'\0'


以上函数有一个带Ex版本,有3个额外参数。





2.5.3 Windows字符串函数

格式化操作 StrFormatKBSize

StrFormatByteSize


对比字符串以进行排序

CompareString

WINBASEAPIintWINAPICompareStringW(    _In_ LCID Locale,    _In_ DWORD dwCmpFlags,    _In_NLS_string_(cchCount1) PCNZWCH lpString1,    _In_ int cchCount1,    _In_NLS_string_(cchCount2) PCNZWCH lpString2,    _In_ int cchCount2    );
第一个参数指定一个区域设置ID,用来标识一种语言。 使用此ID来对比字符串。以符合当地语言的习惯来比较(较慢)

CompareStringOrdinal 基于数的比较

根据GetThreadLocale() 获取线程的区域ID

dwCmpFlags标志

其他两个count指定了字符数

如果要使用更高级的语言选项请使用CompareStringEx函数


CompareStringOrdinal返回0 表明调用失败, 返回 CSTR_LESS_THAN 表明String1 小于String2

CSTR_EQUAL 表明两个String相等

CSTR_GREATER_THAN  表明string1 大于 string2.

为了方便可以降返回值减去2  就是strcmp运行结果一致。


2.6 为何要用Unicode

Unicode有利于应用程序的本地化

使用Unicode,只需发布一个二进制文件,可以支持所有语言。

Unicode提升应用程序的效率。因为代码执行速度更快,占用内存更少。

使用Unicoe容易与COM和.net集成也容易操作自己的资源。


2.7 推荐的字符和字符串处理方式

将文本字符串抽象为字符数组,使用TCHAR

数据类型明确用BYTE表示而不用char

用TEXT宏

执行全局替换

修改字符相关的计算

注意malloc的参数是字节,而某些字符串处理函数的参数是字符的个数。

避免使用printf系的函数,使用MultiByteToWideChar  和 WideCharToMultiByte

同时开启UNICODE和_UNICODE 或同时关闭


对于字符串处理函数

始终使用安全处理函数_s  或者前缀为StringCch的函数

不使用不安全的C运行库字符串处理函数  使用(memcpy_s, memmove_s, wmemcpy_s, wmemmove_s)

利用/GS 和 /RTCs编译标志来自动检测缓冲区溢出(buffer overflow)

不使用lstrcat和lstrcpy

程序内部的字符串比较采用CompareStringOrdinal

界面显示的字符串比较采用CompareString


2.8 Unicode 与 ANSI字符的转换

MultiByteToWideChar 将多字节串转换为宽字符串,

intWINAPIMultiByteToWideChar(    _In_ UINT CodePage,    _In_ DWORD dwFlags,    _In_NLS_string_(cbMultiByte) LPCCH lpMultiByteStr,    _In_ int cbMultiByte,    _Out_writes_to_opt_(cchWideChar, return) LPWSTR lpWideCharStr,    _In_ int cchWideChar    );
cbMultiByte指定字符的长度,如果传-1 会自动计算长度。

cchWideChar 缓冲区长度,只有当缓冲区足矣容纳转换成功的字符数才会转换成功


常用步骤

1)调用MultiByteToWideChar,为lpWideCharStr参数传入NULL,为cchWideChar传入0, 为cbMultiByte参数传入-1

2)分配一个足够容纳转换后的Unicode字符串的内存。他的大小是步骤一的返回值  x sizeof(wchar_t)

3)  再次调用MultiByteToWideChar, 这一次将缓冲区地址作为lpWideCharStr的参数传入, 将步骤1的返回值x sizeof(wchar_t)作为参数传给cchWideChar

4)使用转换后的字符

5)释放Unicode字符串占用的内存块


例子

//szId  is char *int nLen = MultiByteToWideChar(CP_ACP, 0, szId, -1, NULL, 0);wchar_t* pwCtrId = new wchar_t[nLen];MultiByteToWideChar(CP_ACP, 0, szId, -1, pwCtrId, nLen);//...delete[] pwCtrId;


WideCharToMultiByte 将宽字符转换为多字节


intWINAPIWideCharToMultiByte(    _In_ UINT CodePage,    _In_ DWORD dwFlags,    _In_NLS_string_(cchWideChar) LPCWCH lpWideCharStr,    _In_ int cchWideChar,    _Out_writes_bytes_to_opt_(cbMultiByte, return) LPSTR lpMultiByteStr,    _In_ int cbMultiByte,    _In_opt_ LPCCH lpDefaultChar,    _Out_opt_ LPBOOL lpUsedDefaultChar    );

将cbMultiByte参数的值传-1 (书上写的是0 实际值是-1),会返回目标缓冲区需要的大小。步骤类似上面,唯一的区别就是返回值直接就是转换成功所需的字节数(因为char类型本身就是1个字节),所以无需执行乘法运算。
//wszGBK is wchar_t *int len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL);char *szGBK = new char[len];memset(szGBK, 0, len);WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL);// ...delete[] szGBK;

如果进行文件名相关的转换需要特别注意转换成功与失败可能会影响最终文件的写入。注意lpDefaultChar 和 lpUsedDefaultChar两个值。



2.8.1 导出ANSI和Unicode DLL函数

一个逆转字符串的例子

Unicode版本

BOOL StringReverseW(PWSTR pWideCharStr, DWORD cchLength) {// Get a pointer to the last character in the string.PWSTR pEndOfStr = pWideCharStr + wcsnlen_s(pWideCharStr, cchLength) - 1;wchar_t cCharT;// Repeat until we reach the center character in the string.while (pWideCharStr < pEndOfStr) {// swap the first char and the last charcCharT = *pWideCharStr;*pWideCharStr = *pEndOfStr;*pEndOfStr = cCharT;pWideCharStr++;pEndOfStr--;}return TRUE;}

ANSI版本 不做字符串操作,只做编码转换 ,

(注意原书上的例子因为对字符串的操作使用了安全函数,ANSI版本的函数中转换的中间字符串没有额外分配结束符'\0' 和  L'\0'

仅仅是作为中间变量使用尚可,倘若是作为字符串参数传给系统函数必然导致错误。虽然原书上推荐使用安全函数_s来操作这些结尾可能不为'\0'的字符串,但是考虑到有些参数可能要作为参数传递给dll。因为我自己做了修改。在生成中间串的时候预留了结尾'\0'的空间。另外这两个字符串转换完字符以后需要设置第四个参数为-1 才会在结尾填充'\0' 和 L'\0'。否则需要在转换之前调用memset先设置0。

BOOL StringReverseA(PSTR pMultiByteStr, DWORD cchLength) {// Get a pointer to the last character in the string.PWSTR pWideCharStr;int nLenOfWideCharStr;BOOL fOk = FALSE;// calc the target length required.nLenOfWideCharStr = MultiByteToWideChar(CP_ACP, 0,pMultiByteStr, -1/*cchLength*/, NULL, 0);// Allocate memory from the process' default heap to// accomodate the size of the wide-character string.// Don't forget that MultiByteToWideChar returns the// number of characters, not the the number of bytes,// so you must multiply by the size of a wide character.pWideCharStr = (PWSTR)HeapAlloc(GetProcessHeap(), 0,nLenOfWideCharStr * sizeof(wchar_t));if (pWideCharStr == NULL)return fOk;// Convert the multibyte string to a wide-character string.MultiByteToWideChar(CP_ACP, 0, pMultiByteStr, -1/*cchLength*/,pWideCharStr, nLenOfWideCharStr);// call the wide-character version of the func.fOk = StringReverseW(pWideCharStr, cchLength);if (fOk) {// Convert the wide-char string back to// a multibyte string.WideCharToMultiByte(CP_ACP, 0, pWideCharStr, -1/*cchLength*/,pMultiByteStr, (int)strlen(pMultiByteStr), NULL, NULL);}// Free the memory containing the wide-character string.HeapFree(GetProcessHeap(), 0, pWideCharStr);return fOk;}
随后在头文件中做如下定义


#ifdef UNICODE#define StringReverse StringReverseW#else#define StringReverse StringReverseA#endif // !UNICODE


2.8.2 判断文本是ANSI还是Unicode

IsTextUnicode函数有助于分辨打开的文本是ANSI字符还是Unicode

WINADVAPIBOOLWINAPIIsTextUnicode(    _In_reads_bytes_(iSize) CONST VOID* lpv,    _In_        int iSize,    _Inout_opt_ LPINT lpiResult    );

采用了一系列统计性和确定性方法来猜测缓冲区的内容。由于这种方法并不精确,可能会返回错误的结果。

如果测试的是Unicode文本则返回TRUE,反之返回FALSE。

在lpiResult中指定测试哪些具体项目,则返回前还会设定此整数中的相应位,以反映每个测试项目的结果。



阅读全文
0 0
原创粉丝点击