Windows核心编程之核心总结(第二章 字符和字符串处理)(2018.5.27)

学习目标

第二章是学习字符和字符串处理,为了更好理解这一章的内容,我自行添加了其他辅助性内容:存储模式(大端存储和小端存储)、字符编码方案(一看就懂)。以下是这一章的学习目标:
1.大端存储和小端存储
2.字符编码方案
3.ANSI和Unicode字符、字符串,Windows自定义数据类型(为了兼容ANSI和Unicode)
4.Windows的ANSI函数和Unicode函数
5.C运行库的ANSI和Unicode函数
6.C运行库的安全字符串函数
7.C运行库的安全字符串函数(进阶版)
8.字符串比较函数
9.宽字符和ASCII字符之间的转换函数

必备知识-大端存储和小端存储

如何理解Big Endian(大端存储)和Little Endian(小端存储)?
举个例子:
int a = 1;
a这个数本身的16进制表示是0x00 00 00 01
在内存中怎么存储呢?
如果你的CPU是intel x86架构的(基本上就是通常我们说的奔腾cpu),那么就是0x01 0x00 0x00 0x00 , 这也就是所谓的little-endian, 低字节存放在内存的低位.
如果你的CPU是老式AMD系列的(很老很老的那种,因为最新的AMD系列已经是x86架构了), 它的字节序就是big-endian, 其内存存储就是 0x00 0x00 0x00 0x01在内存中从高字节开始存放。
现在世界上绝大多数的CPU都是little-endian。

字符编码

发展流程:ASCII-扩展ASCII-GB2312-GBK-GB18030
美国是最先开始使用计算机,一个字节有八位二进制,能够组合出256种不同的状态,他们将控制字符、空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编码到了第127号,这样计算机就可以用不同字节来存储英语文字了。这时,他们这个方案就叫做ANSI的ASCII编码。后来计算机普及到了世界,因为127种不能表示其他国家的字符、文字,接着他们就想扩展ASCII,使用127后面的位数来其他的字符和文字,一直编码到状态255,从126到255这一页的字符集称作扩展字符集。等到我们中国使用计算机,那么仅仅依靠ASCII完全不够存储,中国就自己想法子解决这一问题,我们这样规定:一个小于127的字符意义还是与原来的ASCII编码相同,但两个大于127的字符连在一起时,就表示一个汉字。意思是说:一个英文字母还是一个字节存储,而一个汉字用两个字节来表示,低8字节存储在低地址位置,高8字节存储在高地址位置,如果在低地址位置存储的低8字节的第8个二进制位大于1,在高地址位置存储的高8字节的第8个二进制位也大于1,那么就会被识别为一个汉字。这样我们大概就可以组合出大约7000多个简体汉字了,在这些编码种,我们还将数学符号、连ASCII原本就有的数字、标点、字母都统统重新编码了,这就是我们遇到的“全角”字符,而原来在127号以下的就叫“半角”字符。之后,中国就叫这个汉字编码方案称为“GB2312”。后来,中国文化博大精深,有些汉字还没完全编码,所以就决定干脆只要高字节代表的字符大于127就表示该字符为汉字,然后称这个编码方案为 GBK 标准。当用GBK解码时,若高字节最高位为0,则用ASCII编码码解码;若高字节最高位为1,则用GBK编码表解码。GBK之后又有GB18030标准,因GB18030较GBK又多了几千汉字,码位不足,GB18030使用了2byte与4byte混合编码方式,这又给软件增加了难题,所以虽然GB18030推出了近5年,仍然没有得到广泛应用。前面讲的一堆汉字编码,我们总称为“DBCS”,即双字节字符集。经过前面编码的发展,逐渐出现一个严重的问题,就是当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁的计算机都不认识对方,谁也不支持对方的编码。后来,先辈们就想到,废除所有的地区性编码方案,重新搞一个包括了地球上所有文化、文字和符号的编码方案,他们称这个编码方案为“Unicode编码”。
Unicode编码有以下几种:
UTF-8:一个字节一个字符,有些字符是2个字节,有的字符是3个字节,还有的字符是4个字节。
UTF-16:大部分字符都是2个字节。Windows平台下默认的Unicode编码为Little Endian的UTF-16。
UTF-32:所有字符都是4个字节。

ANSI和Unicode字符、字符串,Windows自定义数据类型

ANSI字符就是C语言用char数据类型代表一个8位的字符。ANSI字符串是多个char数据类型组成的数组,代表多个字节的字符串。例如:

char a='a';//'a'这个常量字符在常量存储区存储为1个字节。而a在栈区存储为1个字节。
char szBuffer[10]="abcdefg";//"abcdefg"这个常量字符在常量存储区存储为8个字节。而szBuffer在栈区存储为10个字节。

在以前,Unicode字符用wchar_t代表一个两字节的宽字符(Unicode字符),以前C头文件有这样的定义:typedef unsigned short wchar_t,说明wchar_t其实也只是一个无符号短整型而已。后来C编译器将wchar_t定义为与Int一样是基本数据类型,这时候,在高版本一点的编译器你是找不到typedef unsigned short wchar_t这条语句的了。如果想表示常量字符和常量字符串为Unicode版本,那么就要在前面加个L。例如:

wchar_t c=L'a';//L‘a’这个常量字符在常量存储区存储为2个字节。而c在栈区存储为2个字节。
wchar_t szBuffer[10]=L"abcdefg";//L"abcdefg"这个常量字符在常量存储区存储为16个字节。而szBuffer在栈区存储为20个字节。

为了与C语言稍微一些区分,并且为了兼容ANSI和Unicode字符或字符串,Windows自定义了一些数据类型:TCHAR数据类型、TEXT宏。
而对于TCHAR数据类型和TEXT宏的头文件定义如下:

#ifdef  UNICODE                     // r_winnt

#ifndef _TCHAR_DEFINED
typedef WCHAR TCHAR, *PTCHAR;
typedef WCHAR TBYTE , *PTBYTE ;
#define _TCHAR_DEFINED
#endif /* !_TCHAR_DEFINED */

typedef LPWCH LPTCH, PTCH;
typedef LPCWCH LPCTCH, PCTCH;
typedef LPWSTR PTSTR, LPTSTR;
typedef LPCWSTR PCTSTR, LPCTSTR;
typedef LPUWSTR PUTSTR, LPUTSTR;
typedef LPCUWSTR PCUTSTR, LPCUTSTR;
typedef LPWSTR LP;
typedef PZZWSTR PZZTSTR;
typedef PCZZWSTR PCZZTSTR;
typedef PUZZWSTR PUZZTSTR;
typedef PCUZZWSTR PCUZZTSTR;
typedef PZPWSTR PZPTSTR;
typedef PNZWCH PNZTCH;
typedef PCNZWCH PCNZTCH;
typedef PUNZWCH PUNZTCH;
typedef PCUNZWCH PCUNZTCH;
#define __TEXT(quote) L##quote      // r_winnt

#else   /* UNICODE */               // r_winnt

#ifndef _TCHAR_DEFINED
typedef char TCHAR, *PTCHAR;
typedef unsigned char TBYTE , *PTBYTE ;
#define _TCHAR_DEFINED
#endif /* !_TCHAR_DEFINED */

typedef LPCH LPTCH, PTCH;
typedef LPCCH LPCTCH, PCTCH;
typedef LPSTR PTSTR, LPTSTR, PUTSTR, LPUTSTR;
typedef LPCSTR PCTSTR, LPCTSTR, PCUTSTR, LPCUTSTR;
typedef PZZSTR PZZTSTR, PUZZTSTR;
typedef PCZZSTR PCZZTSTR, PCUZZTSTR;
typedef PZPSTR PZPTSTR;
typedef PNZCH PNZTCH, PUNZTCH;
typedef PCNZCH PCNZTCH, PCUNZTCH;
#define __TEXT(quote) quote         // r_winnt

#endif /* UNICODE */                // r_winnt
#define TEXT(quote) __TEXT(quote) 

从头文件定义中,我们可以看出TCHAR数据类型有两种可能,如果定义了UNICODE,则是WCHAR(其实就是wchar_t,宽字符),如果定义了非UNICODE(多字节字符集),则是char(窄字符)。我们知道,当我们打开vs编译器,默认采取的是Unicode字符集,其实这个选项代表我们写的程序序加了这句代码:#define UNICODE。那说明我们写TCHAR,其实就是wchar_t。而如果我们在选项中更改字符集为多字节字符集,那么就相当于定义了非UNICODE,那说明我们写TCHAR,其实就是char。而对于TEXT宏也同样道理,如果是UNICODE字符集,那么就转定义为L##quote(代表在quote前面添加L,quote可以是字符,也可以是字符串),如果是多字节字符集,那么就转定义为quote(代表什么都不添加)。下面举个例子:

//Unicode字符集
TCHAR c=TEXT('a');//TEXT('a‘)相当于L’a‘,在常量存储区存储为2个字节。而c在栈区存储为2个字节。
TCHAR szBuffer[10]=TEXT("abcdefg");//TEXT("abcdefg")相当于L"abcdefg",在常量存储区存储为16个字节。而szBuffer在栈区存储为20个字节。
//多字节字符集
TCHAR c=TEXT('a');//TEXT('a‘)相当于’a‘,在常量存储区存储为1个字节。而c在栈区存储为1个字节。
TCHAR szBuffer[10]=TEXT("abcdefg");//TEXT("abcdefg")相当于"abcdefg",在常量存储区存储为8个字节。而szBuffer在栈区存储为10个字节。

Windows是不是很智能?兼容了ANSI和Unicode,通过TCHAR和TEXT宏可以自动采用对应编码方式进行编码。

Windows的ANSI函数和Unicode函数

  1. 在windows中,有UNICODE类型的函数和ASCII类型的函数,例如CreateWindowEx函数。
    在WinUser.h中,有如下定义:

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

    根据上面头文件,我们就知道了,CreateWindowExW函数是支持Unicode字符的,而CreateWindowExA是支持ANSI字符的。原来Windows函数也会考虑到ANSI和Unicode字符串问题,所以为了兼容这两者,就归为CreateWindowEx函数了,会自动根据情况自行选择正确的函数。其实,还有一个内部原理:CreateWindowExA函数内部实现的其实只是一个转换层,它负责分配内存,以便将ANSI字符串转换为Unicode字符串,然后内部代码会调用CreateWindowExW,并向它传递转换后的字符串,CreateWindowExW返回时,CreateWindowExA会释放它的内存缓冲区,并返回窗口句柄。这个内部原理,总结一句话就是虽然我们调用的是CreateWindowExA,但实际函数内部是先将ANSI字符串转换为Unicode字符串,再调用CreateWindowExW,最后释放内存,接着CreateWindowExA返回内部调用的CreateWindowExW返回的窗口句柄。

    C运行库的ANSI和Unicode函数

    C运行库提供了一些字符串操作函数来处理ANSI字符和Unicode字符。例如:strlen和wcslen函数,分别支持ANSI字符串和Unicode字符串。

    扫描二维码关注公众号,回复: 1058023 查看本文章
    //字符集为Unicode字符集
    char szBuffer1[5]="abcd";
    printf("%d\n", strlen(szBuffer1));
    
    TCHAR szBuffer2[5] = TEXT("abcd");
    printf("%d\n", wcslen(szBuffer2));

    而C运行库为了能智能兼容ANSI和Unicode,提供了_tcslen函数,这个函数需要头文件tchar.h,并且定义了_UNICODE。
    tchar.h头文件定义了以下宏:

    #ifdef _UNICODE
    #define _tcslen wcslen
    #else
    #define _tcslen strlen
    #endif

    如果包含了头文件tchar.h,并且字符集设置为Unicode字符集,那么就已经定义了_UNICODE,我也不知道为什么设置Unicode字符集就会自动定义_UNICODE,然后就可以直接使用_tcslen,也许是因为设置字符集这个操作内部就有#define _UNICODE这行代码吧。下面举个例子:

    //已经设置字符集为Unicode字符集了
    #include<windows.h>
    #include<tchar.h>
    int main()
    {
    TCHAR szBuffer3[5] = TEXT("abcd");
    printf("%d\n", _tcslen(szBuffer3));
    system("pause");
    return 0;
    }

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

    我们编程的时候,尽量使用安全字符串,例如strcpy就是一个非安全函数,当你再程序中使用这个函数的时候,你就会发现,编译器会出现警告,同时给出建议,请遵守。编译器会提示我们使用strcpy_s函数,此时,我们可以查找这个函数,并找到这个函数的TCHAR.h版。具体使用方法不是很难,你要使用那个字符串,就在MSDN中找相应的安全字符串函数即可。不过,对于strlen、wcslen和_tcslen等函数没有问题,可以放心使用,因为它们不会修改传入的字符串。

    C运行库的安全字符串函数(进阶版)

    C运行库还新增了一些函数,用于在执行字符串处理时提供更多的控制。例如:StringCchLength、StringCchPrintf等函数,更多函数请参考MSDN。
    下面是StringCchPrintf函数的说明:
    StringCchPrintf函数用于把格式化字符串写入指定的缓冲区中,与wsprintf函数不同之处在于,该函数还另外需要提供目标缓冲区的大小,确保不会发生越界访问。(因为wsprintf函数,若缓冲区大小不足以存储格式化字符串,则不允许写入,而且会发生崩溃。但是StringCchPrintf函数指定了目标缓冲区大小,意味着,缓冲区大小不足以存储格式化字符串,也可以截断,只存储参数1(缓冲区大小)的长度的字符串),这样就避免发生了奔溃。头文件 strsafe.h。

函数原型:
HRESULT StringCchPrintf(
Out LPTSTR pszDest,
In size_t cchDest,
In LPCTSTR pszFormat,
In ...
);

参数1:指定将要被写入的缓冲区
参数2:限制缓冲区大小
参数3:格式化字符串
参数4:可变参数

    TCHAR szBuffer[10];
    wsprintf(szBuffer, TEXT("%s"), TEXT("woainiaifbgfbfgbfgbgf"));//当目标缓冲区不够存储源缓冲区内容,则会溢出崩溃
    StringCchPrintf(szBuffer, 10, TEXT("%s"),TEXT("wwoainiaifbgfbfgbfgbgf"));//新的安全字符串函数增加了一个缓冲区大小参数,如果超过目标缓冲区大小则会自动截断,避免了溢出崩溃

下面是StringCchLength函数的说明:
StringCchLength函数用于确定字符串是否超过了规定长度。与lstrlen函数的区别在于,该函数指定了待检查的字符串的最大允许的字符数量。注意,如果待检查的字符串(双引号)长度大于最大允许的字符数量,参数3置为0。如果待检查的字符串(单引号),没有字符串结束符,则无论设置cchMax为多少,都会置参数3为0.

*函数原型:
HRESULT StringCchLength(
In LPCTSTR psz,
In size_t cchMax,
Out size_t
pcch
);
参数1:指向待检查的字符串
参数2:psz参数里最大允许的字符数量。
参数3:字符串的字符数,不包括'\0'**

    size_t iTarget1,iTarget2,iTarget3;
    TCHAR szBuffer1[10] =TEXT("但是我依然很开心呀");
    StringCchLength(szBuffer1, 5, &iTarget1);//如果待检查的字符串(双引号)长度大于最大允许的字符数量,参数3置为0。不会报错。
    TCHAR szBuffer2[3] = { L'a', L'b', L'c' };
    StringCchLength(szBuffer2, 5, &iTarget2);//如果待检查的字符串(单引号),没有字符串结束符,则无论设置cchMax为多少,都会置参数3为0.不会报错。
    TCHAR szBuffer3[10] = TEXT("但是我依然很开心呀");
    StringCchLength(szBuffer3, 10, &iTarget3);//成功了

总结,StringCch*系列的函数是安全的,因为可以指定如何截断,不会发生崩溃。

字符串比较函数

int CompareString(
  __in  LCID Locale,
  __in  DWORD dwCmpFlags,
  __in  LPCTSTR lpString1,
  __in  int cchCount1,
  __in  LPCTSTR lpString2,
  __in  int cchCount2
);
int CompareStringOrdinal(
  __in  LPCWSTR lpString1,
  __in  int cchCount1,
  __in  LPCWSTR lpString2,
  __in  int cchCount2,
  __in  BOOL bIgnoreCase
);

CompareStringOrdina和语言无关,速度更快!!!建议使用!!!因为字符串操作函数在实际应用中可以查询MSDN,我以后再填补回来。

宽字符和ASCII字符之间的转换函数

int MultiByteToWideChar(
  __in   UINT CodePage,
  __in   DWORD dwFlags,
  __in   LPCSTR lpMultiByteStr,
  __in   int cbMultiByte,
  __out  LPWSTR lpWideCharStr,
  __in   int cchWideChar
);
int WideCharToMultiByte(
  __in   UINT CodePage,
  __in   DWORD dwFlags,
  __in   LPCWSTR lpWideCharStr,
  __in   int cchWideChar,
  __out  LPSTR lpMultiByteStr,
  __in   int cbMultiByte,
  __in   LPCSTR lpDefaultChar,
  __out  LPBOOL lpUsedDefaultChar
);

因为字符串操作函数在实际应用中可以查询MSDN,我以后再填补回来。

猜你喜欢

转载自blog.51cto.com/12731497/2120935