程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 字符集之Unicode與字符串對象

字符集之Unicode與字符串對象

編輯:關於C語言

1.unicode簡介
如今,Windows操作系統的使用已經遍及世界,為使Windows操作系統及運行在操作系統上的應用軟件更容易被世界所有國籍的用戶所使用,需要使Windows及運行在其上的應用程序本地化,即使用用戶本民族語言的字符集。字符集的不統一使得本地化變得很困難,這需要對操作系統的源代碼根據不同的字符集進行全方位的定制,還要提供API的不同字符集的版本,此外,編寫應用軟件也要針對不同的字符集開發不同的版本。
在歐美地區,字符串被當作一系列以0結尾的單字節字符,這非常自然。使用strlen函數時,會返回一個以0結尾的單字節字符數組中的字符數。但是有些語言,如漢字,字符集的符號很多,而單字節字符集最多只能提供256個符號,這是遠遠不夠的。因此,創立了雙字節字符集DBCS(double byte character set)來支持這些語言。在雙字節字符集中,字符串中的每個字符由1或2字節組成,因此也叫多字節字符集MBCS(multiple byte character set)。由於有些字符是1字節寬,而有些是2字節寬,這使得操作多字節字符串變得非常麻煩,使用strlen操作多字節字符串不能得到字符串的真正長度,而只能得到字符的字節數。

ANSI的C運行時庫中沒有支持多字節字符集的函數。VC++運行時庫中包含有支持操作多字節字符串的函數,這些函數都以_mb開頭,比如_mbstrlen。

Win32中提供了一些輔助函數來幫助操作多字節字符串。如表:

 

Win32中提供的操作多字節字符串的函數

 

函數

描述

LPTSTR CharNext(LPCTSTR lpszCurrentChar)

返回字符串中下一個字符的地址

LPTSTR CharPrev(LPCTSTR lpszStart,LPCTSTR lpszCurrentChar)

返回字符串中前一個字符的地址

BOOL IsDBcsLeadByte(BYTE bTestChar);

返回該字節是否是一個DBCS字符的第一個字節。

 

為了更方便地支持軟件的國際化,一些國際著名的公司制定了一個寬字節的字符集標准—Unicode。該標准最早由Apple和Xerox公司在1988年創立,為發展和促進這一個標准,1991年創建了Unicode聯盟,成員包括Adobe,Aldus,Apple,Borland,Digital,IBM,Lotus,Metaphore,Microsoft,

Novell,Sun,Taligent,Xerox等。

Unicode這個名稱來自三個主要特征:通用(universal)—它解決了世界語言的需要;統一(uniform)—它為了保證效率而使用固定長度的代碼;唯一(unique)—字符代碼的重復將到了最低點。

Unicode字符串中所有字符都是16位的(2個字節),沒有象MBCS字符那樣的特殊字節來指示下個字節是同一字符的一部分還是一個新的字符,這意味著可以簡單地增減一個指針來訪問字符串中的所有字符。

由於Unicode用16位值來表示每個字符,因此可以表示65536個字符,這使得可對世界上所有的書面語言字符進行編碼。目前,Unicode已經為Abrabic,Chinese bopomofo,Cyrillie(Russian),Greek,

Hebrew,Japanese kane,Korean hangul和English alphabets以及其他蚊子定義了碼點(一個符號在字符集中的位置)。大量的標點符號、數學符號、技術符號、肩頭、貨幣符號、發音符號以及其他符號也包括在這個字符集中。所有的這些符號總共約有34,000個碼點,剩余的碼點用於將來的擴展。這65536個字符分成若干個區間,如表所示。

 

Unicode字符集空間劃分

 

16位代碼

字符

0000-007F

ASCII

0080-00FF

拉丁字符

0100-017F

歐洲拉丁

0180-01FF

擴展拉丁

0250-02AF

標號准音標

02B0-02FF

修改字母

0300-036F

通用發音符號

0370-03FF

希臘字母

0400-04FF

西裡爾字母

0530-058F

亞美尼亞字母

0590-05FF

希伯萊字母

0600-06FF

阿拉伯字母

0900-097F

天城文字

3000-9FFF

中文、朝鮮文、日文

 

Unicode的字符串常量以字符串加前綴L表示,如:

wchar_t *pszInfo=L”It’s unicode string”;

 

 2. Unicode與ANSI之間的字符串轉換

多字節字符串轉換成寬字節字符串的函數:

int MultiByteToWideChar(

     UINT uCodePage,    //源字符串的字符集編號(代碼頁)

     DWORD DWFlags,         //是否區分重音符號,一般不用,傳0即可

     PSTR pMultiByteStr,//雙字節的源字符串

     int cchMultiByte,  //源字符串緩沖區大小,按字節計算

     PWSTR pWideCharStr,//寬字符的目標字符串

     int cchWideChar);  //目標字符串的長度,按字符個數計算

 

寬字節轉換成多字節的函數:

int WideCharToMultiByte(

     UINT uCodePage     ,        //代碼頁

     DWORD dwFlags,              //一般傳0

     PCWSTR pWideCharStr,   //源寬字符串

     int cchWideChar,       //源字符串的長度,按字符計算

     PSTR pMultiByteStr,    //目標雙字節緩沖區

     int cchMultiByte,      //目標緩沖區大小,按字節計算

     PCSTR pDefaultChar,    //轉換失敗的替代字符

     PBOOL pfUsedDefaultChar //傳出參數,表示是否有沒轉換成功的字符

)

 

示例:

char szGb[]="大小";

     wchar_t *pszUni;

     char *pszBig5;

     int iLen;

     const int CP_GBK=936;  //GBK代碼頁

     const int CP_BIG5=950; //繁體中文的代碼頁

 

     //測試目標緩沖區的大小

     iLen=MultiByteToWideChar(CP_ACP,0,szGb,-1,NULL,0);

     pszUni=new wchar_t[iLen];//iLen把結束標記考慮進去了

     //將GB2312字符串轉換成UNICODE的

     MultiByteToWideChar(CP_ACP,0,szGb,-1,pszUni,iLen);

   

     //測試轉換成多字節繁體字符串需要的緩沖區大小

     iLen=WideCharToMultiByte(CP_BIG5,0,pszUni,-1,NULL,0,NULL,NULL);

     pszBig5=new char[iLen];

     //轉換成繁體字符串

     WideCharToMultiByte(CP_BIG5,0,pszUni,-1,pszBig5,iLen,NULL,NULL);

   

     TRACE("%s\n",pszBig5);

     delete []pszBig5;

     delete []pszUni;

3. 建Unicode的VC++項目的一般步驟
1、   在項目設置的“菜單—Project—Settings—C/C++--Preprocessor definitions”中,將_MBCS預處理定義修改為_UNICODE;

2、   在“菜單—Tools—Debug”中,將“Display unicode strings”選項打勾;

3、   除非指定要使用char類型的字符串,否則應該將字符串常量用_T括起來,例如

//修改前  char szBuf[]="Hello!";

//修改後       TCHAR szBuf[]=_T("Hello!");

4、   有些數據類型要進行相應的轉換,如

 

原數據類型

新數據類型

char

TCHAR

char *

LPTSTR

const char *

LPCTSTR

 

5、   原有字符串處理函數進行相應轉換,一般規律為str***轉換為_tcs***,例如

 

原函數

替換後的函數

strlen

_tcslen

strcpy

_tcscpy

strcat

_tcscat

sprintf

_stprintf   注意此處有不同

 

     示例:

/*   轉換前

     char szBuf[]="Hello!";

     char szBuf2[16];

     char *pszBuf=szBuf;

     const char *pszBuf2=szBuf;

     strcpy(szBuf2,szBuf);

     int iLen=strlen(szBuf2);

     int iLen2=sizeof(szBuf)-1;

     sprintf(szBuf2,"str is:%s",szBuf);

     */

//轉換後

     TCHAR szBuf[]=_T("Hello!");

     TCHAR szBuf2[16];

     LPTSTR pszBuf=szBuf;

     LPCTSTR pszBuf2=szBuf;

     _tcscpy(szBuf2,szBuf);

     int iLen=_tcslen(szBuf2);

     int iLen2=sizeof(szBuf)/sizeof(TCHAR)-1;

     _stprintf(szBuf2,_T("str is:%s"),szBuf);

1. MFC的CString類
1.1. CString實現的機制.

   CString是通過“引用”來管理串的,象Window內核對象、COM對象等都是通過引用來實現的。而CString也是通過這樣的機制來管理分配的內存塊。實際上CString對象只有一個指針成員變量,所以任何CString實例的長度只有4字節.即:

int len = sizeof(CString);//len等於4

這個指針指向一個CStringData結構體類型對象的數據區,即從nAllocLength以後開始的區域:

struct CStringData

{

long nRefs;                // 引用計數 www.2cto.com

int nDataLength;            // 字符串長度

int nAllocLength;            // 已分配的緩沖區大小,應該大於或等於字符串長度

// TCHAR data[nAllocLength]

TCHAR* data()           // 實際的數據區

{ return (TCHAR*)(this+1); }

};

 

正因為如此,一個這樣的內存塊可被多個CString所引用,例如下列代碼:

CString str("abcd");

CString a = str;

CString b(str);

CString c;

c = b;

 

由於有了這些信息,CString就能正確地分配、管理、釋放引用內存塊。

如果你想在調試程序的時候獲得這些信息。可以在Watch窗口鍵入下列表達式:

(CStringData*)((CStringData*)(this->m_pchData)-1)或

(CStringData*)((CStringData*)(str.m_pchData)-1)//str為指CString實例

 

正因為采用了這樣的好機制,使得CString在大量拷貝時,不僅效率高,而且分配內存少。

 

 1.2. LPCTSTR 與 GetBuffer(int nMinBufLength)

這兩個函數提供了與標准C的兼容轉換。在實際中使用頻率很高,但卻是最容易出錯的地方。這兩個函數實際上返回的都是指針,但它們有區別:

1、LPCTSTR 它的執行過程其實很簡單,只是返回引用內存塊的串地址。它是作為操作符重載提供的,所以在代碼中有時可以隱式轉換,而有時卻需強制轉制。如:

CString str;

const char* p = (LPCTSTR)str;

//假設有這樣的一個函數,Test(const char* p);  你就可以這樣調用

Test(str);//這裡會隱式轉換為LPCTSTR

      2、GetBuffer(int nMinBufLength) 它與LPCTSTR類似,也會返回一個指針,不過返回的是LPTSTR

      3、一般說LPCTSTR轉換後只應該當常量使用,或者做函數的入參;而GetBuffer(...)取出指針後,可以通過這個指針來修改裡面的內容,或者做函數的入參。為什麼呢?也許經常有這樣的代碼:

       CString sBuf2=sBuf;//指向同一個內存塊

       //{{錯誤使用方法

       char *pszTemp=(char *)(LPCTSTR)sBuf;

       pszTemp[1]='Z';            //它會導致sBuf2的值也變化了

       //}}錯誤使用方法

所以LPCTSTR做轉換後,你只能去讀這塊數據,千萬別去改變它的內容。

    

    假如我想直接通過指針去修改數據的話,那怎樣辦呢?就是用GetBuffer(...):

       sBuf="TestCString";

       sBuf2=sBuf;//指向同一個內存塊

       //{{正確的使用方法

       //它導致sBuf和sBuf2指向不同的內存塊

       pszTemp=sBuf.GetBuffer(0);//如果不希望緩沖區變大,直接傳0就行了

       pszTemp[1]='Z';          

       sBuf.ReleaseBuffer();

       //}}正確的使用方法        CString str("abcd");

   為什麼會這樣?其實GetBuffer(20)調用時,它實際上另外建立了一塊新內塊存,而原來的內存塊引用計數也相應減1.  所以執行代碼後sBuf與sBuf2是指向了兩塊不同的地方,所以相安無事。

 

4、不過這裡還有一點注意事項:就是GetBuffer(0)後,新分配的緩沖區長度為原來字符串的長度,即指針pszTemp它所指向的緩沖區,給它賦值時,切不可越界。

另外,當調用GetBuffer(...)後並改變其內容,一定要記得調用ReleaseBuffer(),這個函數會根據串內容來更新引用內存塊的頭部信息。

     5、最後還有一注意事項,看下述代碼:

      char* p = NULL;

      const char* q = NULL;

      {

          CString str = "abcd";

          q = (LPCTSTR)str;

          p = str.GetBuffer(20);

          AfxMessageBox(q);// 合法的

          strcpy(p, "this is test");//合法的,

      }

      AfxMessageBox(q);// 非法的

      strcpy(p, "this is test");//非法的

      這裡要說的就是,當返回這些指針後,如果CString對象生命結束,這些指針也相應

無效。

 

下面演示一段代碼執行過程

void Test()

{

    //str指向一引用內存塊(引用內存塊的引用計數為1,長度為4,分配長度為4)

       CString str("abcd");     

    //a指向一初始數據狀態,

       CString a;                          

       //a與str指向同一引用內存塊(引用內存塊的引用計數為2,長度為4,分配長度為4)

       a = str;                       

    //a、b與str指向同一引用內存塊(引用內存塊的引用計數為3,長度為4,分配長度為4)

       CString b(a);              

    {

        //temp指向引用內存塊的串首地址。

              //(引用內存塊的引用計數為3,長度為4,分配長度為4)

              LPCTSTR temp = (LPCTSTR)a;

        //a、b、d與str指向同一引用內存塊

              //(引用內存塊的引用計數為4,長度為4,分配長度為4)

              CString d = a;

        //這條語句實際是調用CString::operator=(CString&)函數。

              //b指向一新分配的引用內存塊。

              //(新分配的引用內存塊的引用計數為1,長度為5,分配長度為5)

              //同時原引用內存塊引用計數減1. a、d與str仍指向原引用內存塊

              //(引用內存塊的引用計數為3,長度為4,分配長度為4)

              b = "testa";

       //由於d生命結束,調用析構函數,導至引用計數減1

       //(引用內存塊的引用計數為2,長度為4,分配長度為4)      

    }

       //此語句也會導致重新分配新內存塊。temp指向新分配引用內存塊的串首地址

       //(新分配的引用內存塊的引用計數為1,長度為0,分配長度為10)

       //同時原引用內存塊引用計數減1. 只有str仍指向原引用內存塊

       //(引用內存塊的引用計數為1,長度為4,分配長度為4)                    

       LPTSTR temp = a.GetBuffer(10);

       //a指向的引用內存塊的引用計數為1,長度為0,分配長度為10

       strcpy(temp, "temp");

       //注意:a指向的引用內存塊的引用計數為1,長度為4,分配長度為10

       a.ReleaseBuffer();

}

//執行到此,所有的局部變量生命周期都已結束。對象str a b 各自調用自己的析構構

//函數,所指向的引用內存塊也相應減1

//注意,str a b 所分別指向的引用內存塊的計數均為0,這導致所分配的內存塊釋放

 

    通過觀察上面執行過程,我們會發現CString雖然可以多個對象指向同一引用內塊存,

但是它們在進行各種拷貝、賦值及改變串內容時,它的處理是很智能並且非常安全的,完全

做到了互不干涉、互不影響。當然必須要求你的代碼使用正確恰當,特別是實際使用中會有

更復雜的情況,如做函數參數、引用、及有時需保存到CStringList當中,如果哪怕有一小

塊地方使用不當,其結果也會導致發生不可預知的錯誤

 

 1.3. FreeExtra()的作用

   看這段代碼

   (1)   CString str("test");

   (2)   LPTSTR temp = str.GetBuffer(50);

   (3)   strcpy(temp, "there are 22 character");

   (4)   str.ReleaseBuffer();

   (5)   str.FreeExtra();

   上面代碼執行到第(4)行時,大家都知道str指向的引用內存塊計數為1,長度為22,分配長

度為50. 那麼執行str.FreeExtra()時,它會釋放所分配的多余的內存。(引用內存塊計數為

1,長度為22,分配長度為22)

 

 1.4. Format(...)  與 FormatV(...)

   這條語句在使用中是最容易出錯的。因為它最富有技巧性,也相當靈活。實際上sprintf(...)怎麼用,它就怎麼用。但是有一點需要注意:就是它的參數的特殊性,由於編譯器在編譯時並不能去校驗格式串參數與對應的變元的類型及長度。所以你必須要注意,兩者一定要對應上,

   否則就會出錯。如:

   CString str;

   int a = 12;

   str.Format("first:%l, second: %s", a, "error");//result?試試

 

 1.5. LockBuffer() 與 UnlockBuffer()

   顧名思議,這兩個函數的作用就是對引用內存塊進行加鎖及解鎖。

   但使用它有什麼作用及執行過它後對CString串有什麼實質上的影響。其實挺簡單,看下

面代碼:

   (1)   CString str("test");

   (2)   str.LockBuffer();

   (3)   CString temp = str;

   (4)   str.UnlockBuffer();

   (5)   str.LockBuffer();

   (6)   str = "error";

   (7)   str.ReleaseBuffer();

   執行完(3)後,與通常情況下不同,temp與str並不指向同一引用內存塊。你可以在watch

窗口用這個表達式(CStringData*)((CStringData*)(str.m_pchData)-1)看看。

 

 1.6. AllocSysString()與SetSysString(BSTR*)

   這兩個函數提供了串與BSTR的轉換。使用時須注意一點:當調用AllocSysString()後,

須調用它SysFreeString(...) ,例:

CString sBuf="TestCString";

       BSTR bsBuf=sBuf.AllocSysString();

       ::SysFreeString(bsBuf);

     

       bsBuf=::SysAllocString(L"Hi,I'm BSTR");

       sBuf=bsBuf;

       TRACE(_T("%s\n"),sBuf);

       ::SysFreeString(bsBuf);

 

 2. COM 中的字符串BSTR和VARIANT
2.1. BSTR字符串類型

  很多自動化和COM接口使用BSTR來定義字符串。BSTR 是 Pascal-style 字符串(字符串長度被明確指出)和C-style字符串(字符串的長度要通過尋找結束符來計算)的混合產物。一個BSTR是一個Unicode字符串,它的長度是預先考慮的,並且它還有一個0字符作為結束標記。下面是一個BSTR的示例:06 00 00 00 42 00 6F 00 62 00 00 00

--length-- B o b EOS

 

  注意字符串的長度是如何被加到字符串數據中的。長度是DWORD類型的,保存了字符串中包含的字節數,但不包括結束標記。在這個例子中,"Bob"包含3個Unicode字符(不包括結束符),總共6個字節。字符串的長度被預先存儲好,以便當一個BSTR在進程或者計算機之間被傳遞時,COM庫知道多少數據需要傳送。(另一方面,一個BSTR能夠存儲任意數據塊,而不僅僅是字符,它還可以包含嵌入在數據中的0字符。)。

  在 C++ 中,一個 BSTR 實際上就是一個指向Unicode字符串中第一個字符的指針。它的定義如下:

BSTR bstr = NULL;

bstr = SysAllocString ( L"Hi Bob!" );

      if ( NULL == bstr )

           // out of memory error

// Use bstr here...

      SysFreeString ( bstr );    

 

  另外一個用在自動化接口中的變量類型是VARIANT。它被用來在無類型(typeless)語言,如Jscript和VBScript,來傳遞數據。一個VARIANT可能含有很多不同類型的數據,例如long和IDispatch*。當一個VARIANT包含一個字符串,字符串被存成一個BSTR。後面講到VARIANT封裝類時,會對VARIANT多些介紹。

 

 2.2. BSTR的包裝類_bstr_t

  _bstr_t是一個對BSTR的完整封裝類,實際上它隱藏了底層的BSTR。它提供各種構造函數和操作符來訪問底層的C語言風格的字符串。然而,_bstr_t卻沒有訪問BSTR本身的操作符,所以一個_bstr_t類型的字符串不能被作為輸出參數傳給一個COM方法。如果你需要一個BSTR*參數,使用ATL類CComBSTR是比較容易的方式。

  一個_bstr_t字符串能夠傳給一個接收參數類型為BSTR的函數,只是因為下列3個條件同時滿足。首先,_bstr_t有一個向wchar_t*轉換的轉換函數;其次,對編譯器而言,因為BSTR的定義,wchar_t*和BSTR有同樣的含義;第三,_bstr_t內部含有的wchar_t*指向一片按BSTR的形式存儲數據的內存。所以,即使沒有文檔說明,_bstr_t可以轉換成BSTR,這種轉換仍然可以正常進行。

// Constructing

void Test_bstr_t()

{

       _bstr_t bs1 = "char string";       // construct from a LPCSTR

       _bstr_t bs2 = L"wide char string";       // construct from a LPCWSTR

       _bstr_t bs3 = bs1;              // copy from another _bstr_t

       _variant_t v = "Bob";

       _bstr_t bs4 = v;                // construct from a _variant_t that has a string

       // Extracting data

       LPCSTR psz1 = bs1;            // automatically converts to MBCS string

       LPCSTR psz2 = (LPCSTR) bs1;   // cast OK, same as previous line

       LPCWSTR pwsz1 = bs1;         // returns the internal Unicode string

       LPCWSTR pwsz2 = (LPCWSTR) bs1;  // cast OK, same as previous line

       BSTR    bstr = bs1.copy();      // copies bs1, returns it as a BSTR

       // ...

       SysFreeString ( bstr );    

}

  注意_bstr_t也提供char*和wchar_t*之間的轉換操作符。但不提倡使用,因為即使它們是非常量字符串指針,你也一定不能使用這些指針去修改它們指向的緩沖區的內容,因為那將破壞內部的BSTR結構。

 

 2.3. _variant_t

  _variant_t是一個對VARIANT的完整封裝,它提供很多構造函數和轉換函數來操作一個VARIANT可能包含的大量的數據類型。這裡,介紹與字符串有關的操作。

void TestVariant()

{

       _variant_t v1  = "char string";             // construct from a LPCSTR

       _variant_t v2  = L"wide char string";   // construct from a LPCWSTR

       _bstr_t bs1             = "Bob";

       _variant_t v3  = bs1;                                 // copy from a _bstr_t object

       // Extracting data

       _bstr_t bs2             = v1;                                   // extract BSTR from the VARIANT

       _bstr_t bs3             = (_bstr_t) v1;                            // cast OK, same as previous line    

}

注意:

  如果類型轉換不能被執行,_variant_t方法能夠拋出異常,所以應該准備捕獲_com_error異常。

 

還需要注意的是:

  沒有從一個_variant_t變量到一個MBCS字符串的直接轉換。你需要創建一個臨時的_bstr_t變量,使用提供Unicode到MBCS轉換的另一個字符串類或者使用一個ATL轉換宏。

  不像_bstr_t,一個_variant_t變量可以被直接作為參數傳遞給一個COM方法。_variant_t繼承自VARIANT類型,所以傳遞一個_variant_t來代替VARIANT變量是C++語言所允許的。


摘自 hlfkyo的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved