字符集之 Unicode与字符串对象

来源:互联网 发布:哪些淘宝店衣服好看 编辑:程序博客网 时间:2024/06/08 11:43

1.unicode简介

如今,Windows操作系统的使用已经遍及世界,为使Windows操作系统及运行在操作系统上的应用软件更容易被世界所有国籍的用户所使用,需要使Windows及运行在其上的应用程序本地化,即使用用户本民族语言的字符集。字符集的不统一使得本地化变得很困难,这需要对操作系统的源代码根据不同的字符集进行全方位的定制,还要提供API的不同字符集的版本,此外,编写应用软件也要针对不同的字符集开发不同的版本。

在欧美地区,字符串被当作一系列以0结尾的单字节字符,这非常自然。使用strlen函数时,会返回一个以0结尾的单字节字符数组中的字符数。但是有些语言,如汉字,字符集的符号很多,而单字节字符集最多只能提供256个符号,这是远远不够的。因此,创立了双字节字符集DBCSdouble byte character set)来支持这些语言。在双字节字符集中,字符串中的每个字符由12字节组成,因此也叫多字节字符集MBCS(multiple byte character set)。由于有些字符是1字节宽,而有些是2字节宽,这使得操作多字节字符串变得非常麻烦,使用strlen操作多字节字符串不能得到字符串的真正长度,而只能得到字符的字节数。

ANSIC运行时库中没有支持多字节字符集的函数。VC++运行时库中包含有支持操作多字节字符串的函数,这些函数都以_mb开头,比如_mbstrlen

Win32中提供了一些辅助函数来帮助操作多字节字符串。如表:

 

Win32中提供的操作多字节字符串的函数

 

函数

描述

LPTSTR CharNext(LPCTSTR lpszCurrentChar)

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

LPTSTR CharPrev(LPCTSTR lpszStart,LPCTSTR lpszCurrentChar)

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

BOOL IsDBcsLeadByte(BYTE bTestChar);

返回该字节是否是一个DBCS字符的第一个字节。

 

为了更方便地支持软件的国际化,一些国际著名的公司制定了一个宽字节的字符集标准—Unicode。该标准最早由AppleXerox公司在1988年创立,为发展和促进这一个标准,1991年创建了Unicode联盟,成员包括Adobe,Aldus,Apple,Borland,Digital,IBM,Lotus,Metaphore,Microsoft,

Novell,SunTaligent,Xerox等。

Unicode这个名称来自三个主要特征:通用(universal)—它解决了世界语言的需要;统一(uniform)—它为了保证效率而使用固定长度的代码;唯一(unique)—字符代码的重复将到了最低点。

Unicode字符串中所有字符都是16位的(2个字节),没有象MBCS字符那样的特殊字节来指示下个字节是同一字符的一部分还是一个新的字符,这意味着可以简单地增减一个指针来访问字符串中的所有字符。

由于Unicode16位值来表示每个字符,因此可以表示65536个字符,这使得可对世界上所有的书面语言字符进行编码。目前,Unicode已经为Abrabic,Chinese bopomofo,Cyrillie(Russian),Greek,

Hebrew,Japanese kane,Korean hangulEnglish 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;                //引用计数

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的兼容转换。在实际中使用频率很高,但却是最容易出错的地方。这两个函数实际上返回的都是指针,但它们有区别:

1LPCTSTR它的执行过程其实很简单,只是返回引用内存块的串地址。它是作为操作符重载提供的,所以在代码中有时可以隐式转换,而有时却需强制转制。如:

CString str;

const char* p = (LPCTSTR)str;

//假设有这样的一个函数,Test(const char* p) 你就可以这样调用

Test(str);//这里会隐式转换为LPCTSTR

      2GetBuffer(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;//指向同一个内存块

       //{{正确的使用方法

       //它导致sBufsBuf2指向不同的内存块

       pszTemp=sBuf.GetBuffer(0);//如果不希望缓冲区变大,直接传0就行了

       pszTemp[1]='Z';           

       sBuf.ReleaseBuffer();

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

   为什么会这样?其实GetBuffer(20)调用时,它实际上另外建立了一块新内块存,而原来的内存块引用计数也相应减1. 所以执行代码后sBufsBuf2是指向了两块不同的地方,所以相安无事。

 

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;                           

       //astr指向同一引用内存块(引用内存块的引用计数为2,长度为4,分配长度为4

       a = str;                        

    //abstr指向同一引用内存块(引用内存块的引用计数为3,长度为4,分配长度为4

       CString b(a);               

    {

        //temp指向引用内存块的串首地址。

              //(引用内存块的引用计数为3,长度为4,分配长度为4

              LPCTSTR temp = (LPCTSTR)a;

        //abdstr指向同一引用内存块

              //(引用内存块的引用计数为4,长度为4,分配长度为4

              CString d = a;

        //这条语句实际是调用CString::operator=(CString&)函数。

              //b指向一新分配的引用内存块。

              //(新分配的引用内存块的引用计数为1,长度为5,分配长度为5

              //同时原引用内存块引用计数减1. adstr仍指向原引用内存块

              //(引用内存块的引用计数为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)后,与通常情况下不同,tempstr并不指向同一引用内存块。你可以在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"包含3Unicode字符(不包括结束符),总共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)语言,如JscriptVBScript,来传递数据。一个VARIANT可能含有很多不同类型的数据,例如longIDispatch*。当一个VARIANT包含一个字符串,字符串被存成一个BSTR。后面讲到VARIANT封装类时,会对VARIANT多些介绍。

 

 2.2. BSTR的包装类_bstr_t

  _bstr_t是一个对BSTR的完整封装类,实际上它隐藏了底层的BSTR。它提供各种构造函数和操作符来访问底层的C语言风格的字符串。然而,_bstr_t却没有访问BSTR本身的操作符,所以一个_bstr_t类型的字符串不能被作为输出参数传给一个COM方法。如果你需要一个BSTR*参数,使用ATLCComBSTR是比较容易的方式。

  一个_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变量,使用提供UnicodeMBCS转换的另一个字符串类或者使用一个ATL转换宏。

  不像_bstr_t,一个_variant_t变量可以被直接作为参数传递给一个COM方法。_variant_t继承自VARIANT类型,所以传递一个_variant_t来代替VARIANT变量是C++语言所允许的。

 

原创粉丝点击