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字符串。

    //字符集爲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,我以後再填補回來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章