C++/c字符串系列:字符编码进阶

来源:互联网 发布:一对多sql语句 编辑:程序博客网 时间:2024/06/05 07:49

转自:

http://blog.sina.com.cn/s/blog_4b7e71290100b0rj.html

http://blog.sina.com.cn/s/blog_4b7e71290100b1ah.html

http://blog.sina.com.cn/s/blog_4b7e71290100b1aj.html

 

一、从ASCII码到UNICODE

计算机发明后,为了在计算机中表示字符,人们制定了一种编码,叫ASCII码。ASCII码由一个字节中的7位(bit)表示,范围是0x00 - 0x7F 共128个字符。他们以为这128个数字就足够表示abcd...ABCD...1234 这些字符了。

  咳......说英语的人就是“笨”!后来他们突然发现,如果需要按照表格方式打印这些字符的时候,缺少了“制表符”。于是又扩展了ASCII的定义,使用一个字节的全部8位(bit)来表示字符了,这就叫扩展ASCII码。范围是0x00 - 0xFF 共256个字符。

  咳......说中文的人就是聪明!中国人利用连续2个扩展ASCII码的扩展区域(0xA0以后)来表示一个汉字,该方法的标准叫GB-2312。后来,日文、韩文、阿拉伯文、台湾繁体(BIG-5)......都使用类似的方法扩展了本地字符集的定义,现在统一称为 MBCS 字符集(多字节字符集)。这个方法是有缺陷的,因为各个国家地区定义的字符集有交集,因此使用GB-2312的软件,就不能在BIG-5的环境下运行(显示乱码),反之亦然。

  咳......说英语的人终于变“聪明”一些了。为了把全世界人民所有的所有的文字符号都统一进行编码,于是制定了UNICODE标准字符集。UNICODE 使用2个字节表示一个字符(unsigned shor int、WCHAR、_wchar_t、OLECHAR)。这下终于好啦,全世界任何一个地区的软件,可以不用修改地就能在另一个地区运行了。虽然我用 IE 浏览日本网站,显示出我不认识的日文文字,但至少不会是乱码了。UNICODE 的范围是 0x0000 - 0xFFFF 共6万多个字符,其中光汉字就占用了4万多个。嘿嘿,中国人赚大发了:0)
在Unicode里,所有的字符被一视同仁,汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”,也就是说,所有的文字都按一个字符来处理,它们都有一个唯一的Unicode码。

二、使用Unicode编码的好处

使用Unicode编码可以使您的工程同时支持多种语言,使您的工程国际化。
  另外,Windows NT是使用Unicode进行开发的,整个系统都是基于Unicode的。如果调用一个API函数并给它传递一个ANSI(ASCII字符集以及由此派生并兼容的字符集,如:GB2312,通常称为ANSI字符集)字符串,那么系统首先要将字符串转换成Unicode,然后将Unicode字符串传递给操作系统。如果希望函数返回ANSI字符串,系统就会首先将Unicode字符串转换成ANSI字符串,然后将结果返回给您的应用程序。进行这些字符串的转换需要占用系统的时间和内存。如果用Unicode来开发应用程序,就能够使您的应用程序更加有效地运行。

下面例举几个字符的编码以简单演示ANSI和Unicode的区别:

字符AN和ANSI码41H4eHcdbaHUnicode码0041H004eH548cH

三、在程序中使用各种字符集的方法

const char * p = "Hello"; //使用 ASCII 字符集

//使用 MBCS 字符集,由于 MBCS 完全兼容 ASCII,多数情况下我们并不严格区分

const char * p = "你好";

LPCSTR p = "Hello,你好"; // 意义同上

const WCHAR * p = L"Hello,你好"; // 使用 UNICODE 字符集

LPCOLESTR p = L"Hello,你好"; // 意义同上

// 如果预定义了_UNICODE,则表示使用UNICODE字符集;如果定义了_MBCS,则表示使用 MBCS

const TCHAR * p = _T("Hello,你好");

LPCTSTR p = _T("Hello,你好"); // 意义同上

在上面的例子中,T是非常有意思的一个符号(TCHAR、LPCTSTR、LPTSTR、_T()、_TEXT()...),它表示使用一种中间类型,既不明确表示使用 MBCS,也不明确表示使用 UNICODE。那到底使用哪种字符集那?嘿嘿......编译的时候决定吧。设置条件编译的方式是:VC6中,"Project\Settings...\C/C++卡片 Preprocessor definitions" 中添加或修改 _MBCS、_UNICODE;VC.NET中,"项目\属性\配置属性\常规\字符集"然后用组合窗进行选择。使用 T 类型,是非常好的习惯,严重推荐!

四、VC++6.0中编写Unicode编码的应用程序

使用UNICODE的工程设置包括两种:UNICODE和_UNICODE,前者没有下划线,专门用于Windows头文件;后者有一个前缀下划线,专门用于C运行时头文件。换句话说,也就是在ANSI C++语言里面根据_UNICODE(有下划线)定义与否,各宏分别展开为Unicode或ANSI字符,在Windows里面根据UNICODE(无下划线)定义与否,各宏分别展开为Unicode或ANSI字符。实际使用中我们一般不加严格区分,同时定义_UNICODE和UNICODE,以实现UNICODE版本编程。

在没有定义UNICODE和_UNICODE时,所有函数和类型都默认使用ANSI的版本;在定义了UNICODE和_UNICODE之后,所有的MFC类和Windows API都变成了宽字节版本了。

附录:Unicode编码表

http://codex.wordpress.org.cn/index.php?title=Unicode%E7%BC%96%E7%A0%81%E8%A1%A8&diff=prev&oldid=64581

 

一.字符基础 -- SBCS, MBCS, Unicode

  所有的 string 类都是以C-style字符串为基础的。C-style 字符串是字符数组。所以我们先介绍字符类型。这里有3种编码模式对应3种字符类型。

第一种编码类型是单子节字符集(single-byte character set or SBCS)。在这种编码模式下,所有的字符都只用一个字节表示。ASCII是SBCS。一个字节表示的0用来标志SBCS字符串的结束。


  第二种编码模式是多字节字符集(multi-byte character set or MBCS)。一个MBCS编码包含一些一个字节长的字符,而另一些字符大于一个字节的长度。用在Windows里的MBCS包含两种字符类型,单字节字符(single-byte characters)和双字节字符(double-byte characters)。由于Windows里使用的多字节字符绝大部分是两个字节长,所以MBCS常被用DBCS代替。


  在DBCS编码模式中,一些特定的值被保留用来表明他们是双字节字符的一部分。例如,在Shift-JIS编码中(一个常用的日文编码模式),0x81-0x9f之间和 0xe0-oxfc之间的值表示"这是一个双字节字符,下一个子节是这个字符的一部分。"这样的值被称作"leading bytes",他们都大于0x7f。跟随在一个leading byte子节后面的字节被称作"trail byte"。在DBCS中,trail byte可以是任意非0值。像SBCS一样,DBCS字符串的结束标志也是一个单字节表示的0。


  第三种编码模式是Unicode。Unicode是一种所有的字符都使用两个字节编码的编码模式。Unicode字符有时也被称作宽字符,因为它比单子节字符宽(使用了更多的存储空间)。注意,Unicode不能被看作MBCS。MBCS的独特之处在于它的字符使用不同长度的字节编码。Unicode字符串使用两个字节表示的0作为它的结束标志。


  单字节字符包含拉丁文字母表,accented characters及ASCII标准和DOS操作系统定义的图形字符。双字节字符被用来表示东亚及中东的语言。Unicode被用在COM及Windows NT操作系统内部。


  你一定已经很熟悉单字节字符。当你使用char时,你处理的是单字节字符。双字节字符也用char类型来进行操作(这是我们将会看到的关于双子节字符的很多奇怪的地方之一)。Unicode字符用wchar_t来表示。Unicode字符和字符串常量用前缀L来表示。例如:

wchar_t wch = L''1''; // 2 bytes, 0x0031

wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters

二.字符在内存中是怎样存储的

单字节字符串:每个字符占一个字节按顺序依次存储,最后以单字节表示的0结束。例如。"Bob"的存贮形式如下:

42

6F

62

00

B

o

b

BOS

Unicode的存储形式,L"Bob"

42 00

6F 00

62 00

00 00

B

o

b

BOS

使用两个字节表示的0来做结束标志。
  一眼看上去,DBCS 字符串很像 SBCS 字符串,但是它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。

字符串"日本語" ("nihongo")在内存中的存储形式如下(LB和TB分别用来表示 leading byte 和 trail byte):

93 FA

96 7B

8C EA

00

LB TB

LB TB

LB TB

EOS

EOS

值得注意的是,"ni"的值不能被解释成WORD型值0xfa93,而应该看作两个值93和fa以这种顺序被作为"ni"的编码。

三.字符串处理函数

我们都已经见过C语言中的字符串函数,strcpy(), sprintf(), atoll()等。这些字符串只应该用来处理单字节字符字符串。标准库也提供了仅适用于Unicode类型字符串的函数,比如wcscpy(), swprintf(), wtol()等。


  微软还在它的CRT(C runtime library)中增加了操作DBCS字符串的版本。str***()函数都有对应名字的DBCS版本_mbs***()。如果你料到可能会遇到DBCS字符串(如果你的软件会被安装在使用DBCS编码的国家,如中国,日本等,你就可能会),你应该使用_mbs***()函数,因为他们也可以处理SBCS字符串。(一个DBCS字符串也可能含有单字节字符,这就是为什么_mbs***()函数也能处理SBCS字符串的原因)


  让我们来看一个典型的字符串来阐明为什么需要不同版本的字符串处理函数。我们还是使用前面的Unicode字符串 L"Bob":

42 00

6F 00

62 00

00 00

B

o

b

BOS


因为x86CPU是little-endian,值0x0042在内存中的存储形式是42 00。你能看出如果这个字符串被传给strlen()函数会出现什么问题吗?它将先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()将会返回1。如果把"Bob"传给wcslen(),将会得出更坏的结果。wcslen()将会先看到0x6f42,然后是0x0062,然后一直读到你的缓冲区的末尾,直到发现00 00结束标志或者引起了GPF。


  str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\\ ", ''\\''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''\\''的指针。


  关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。

四.Win32 API中的MBCS和Unicode


  尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。


  当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 Unicode APIs。如果,你曾经用过VC向导并且没有改过预处理的设置,那表明你用的是MBCS版本。那么,既然没有 SetWindowText() API,我们为什么可以使用它呢?

winuser.h头文件包含了一些宏,例如:

BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );

BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );

#ifdef UNICODE

#define SetWindowTextSetWindowTextW

#else

#define SetWindowTextSetWindowTextA

#endif

当使用MBCS APIs来build程序时,UNICODE没有被定义,所以预处理器看到:

#define SetWindowText SetWindowTextA

  这个宏定义把所有对SetWindowText的调用都转换成真正的API函数SetWindowTextA。(当然,你可以直接调用SetWindowTextA() 或者 SetWindowTextW(),虽然你不必那么做。)


  所以,如果你想把默认使用的API函数变成Unicode版的,你可以在预处理器设置中,把_MBCS从预定义的宏列表中删除,然后添加UNICODE和_UNICODE。(你需要两个都定义,因为不同的头文件可能使用不同的宏。) 然而,如果你用char来定义你的字符串,你将会陷入一个尴尬的境地。考虑下面的代码:

HWND hwnd = GetSomeWindowHandle();

char szNewText[] = "we love Bob!";

SetWindowText ( hwnd, szNewText );

在预处理器把SetWindowText用SetWindowTextW来替换后,代码变成:

HWND hwnd = GetSomeWindowHandle();

char szNewText[] = "we love Bob!";

SetWindowTextW ( hwnd, szNewText );

  看到问题了吗?我们把单字节字符串传给了一个以Unicode字符串做参数的函数。解决这个问题的第一个方案是使用 #ifdef 来包含字符串变量的定义:

HWND hwnd = GetSomeWindowHandle();

#ifdef UNICODE

wchar_t szNewText[] = L"we love Bob!";

#else

char szNewText[] = "we love Bob!";

#endif

SetWindowText ( hwnd, szNewText );

你可能已经感受到了这样做将会使你多么的头疼。完美的解决方案是使用TCHAR。

五.使用TCHAR

TCHAR是一种字符串类型,它让你在以MBCS和UNNICODE来build程序时可以使用同样的代码,不需要使用繁琐的宏定义来包含你的代码。TCHAR的定义如下:

#ifdef UNICODE

typedef wchar_t TCHAR;

#else

typedef char TCHAR;

#endif

所以用MBCS来build时,TCHAR是char,使用UNICODE时,TCHAR是wchar_t。还有一个宏来处理定义Unicode字符串常量时所需的L前缀。

#ifdef UNICODE

#define _T(x) L##x

#else

#define _T(x) x

#endif

##是一个预处理操作符,它可以把两个参数连在一起。如果你的代码中需要字符串常量,在它前面加上_T宏。如果你使用Unicode来build,它会在字符串常量前加上L前缀。像是用宏来隐藏SetWindowTextA/W的细节一样,还有很多可以供你使用的宏来实现str***()和_mbs***()等字符串函数。例如,你可以使用_tcsrchr宏来替换strrchr()、_mbsrchr()和wcsrchr()。_tcsrchr根据你预定义的宏是_MBCS还是UNICODE来扩展成正确的函数,就像SetWindowText所作的一样。


  不仅str***()函数有TCHAR宏。其他的函数如, _stprintf(代替sprinft()和swprintf()),_tfopen(代替fopen()和_wfopen())。 MSDN中"Generic-Text Routine Mappings."标题下有完整的宏列表。


六.字符串宏定义

由于Win32 API文档的函数列表使用函数的常用名字(例如,"SetWindowText"),所有的字符串都是用TCHAR来定义的。(除了XP中引入的只适用于Unicode的API)。下面列出一些常用的typedefs,你可以在MSDN中看到他们。

type

Meaning in MBCS builds

Meaning in Unicode builds

LPSTR

zero-terminated string of char (char*)

zero-terminated string of char (char*)

LPCSTR

constant zero-terminated string of char (const char*)

constant zero-terminated string of char (const char*)

WCHAR

wchar_t

wchar_t

LPWSTR

zero-terminated Unicode string (wchar_t*)

zero-terminated Unicode string (wchar_t*)

LPCWSTR

constant zero-terminated Unicode string (const wchar_t*)

constant zero-terminated Unicode string (const wchar_t*)

TCHAR

char

wchar_t

LPTSTR

zero-terminated string of TCHAR (TCHAR*)

zero-terminated string of TCHAR (TCHAR*)

LPCTSTR

constant zero-terminated string of TCHAR (const TCHAR*)

constant zero-terminated string of TCHAR (const TCHAR*)

一个增加的字符类型是OLETYPE。它表示自动化接口(如word提供的可以使你操作文档的接口)中使用的字符类型。这种类型一般被定义成wchar_t,然而如果你定义了OLE2ANSI预处理标记,OLECHAR将会被定义成char类型。我知道现在已经没有理由定义OLE2ANSI(从MFC3以后,微软已经不使用它了),所以从现在起我将把OLECHAR当作Unicode字符。


这里给出你将会看到的一些OLECHAR相关的typedefs:

Type

Meaning

OLECHAR

Unicode character (wchar_t)

LPOLESTR

string of OLECHAR (OLECHAR*)

LPCOLESTR

constant string of OLECHAR (const OLECHAR*)

还有两个用于字符串和字符常量的宏定义,它们可以使同样的代码被用于MBCS和Unicode builds :

Type

Meaning

_T(x)

Prepends L to the literal in Unicode builds.

OLESTR(x)

Prepends L to the literal to make it an LPCOLESTR.

在文档或例程中,你还会看到好多_T的变体。有四个等价的宏定义,它们是TEXT, _TEXT, __TEXT和__T,它们都起同样的做用。

七.字符串扩展类型及封装类

字符串的种类多种多样,有些可以应用在很多场合(比如C字符串的基础char类型),也有一些可能只是在特定情况下才会使用(比如使用MFC一般会用到CString,而开发COM组件一般会涉及到BSTR或CComBSTR)。希望通过下面的总结能让大家清楚地了解到各种不同类型字符串的出处和通常情况下的使用场合。

封装类

出处

功能简介

使用

BSTR

COM

特殊的数据结构,可以保存字符串的长度

需要调用专门的API函数,容易造成内存泄漏。建议使用封装类。比如,CComBSTR。

VARIANT

COM

特殊结构,被设计用来实现跨语言的特性

用来在无类型(typeless)语言(如Jscript和VBScript)来传递数据。

CString

MFC

封装TCHAR类型的字符串

MFC中使用

COleVariant

MFC

继承自VARIANT

很少用

_bstr_t

CRT

是一对BSTR的完整封装类,隐藏了底层的BSTR。

_variant_t

CRT

是一个对VARIANT的完整封装,隐藏了底层的VARIANT。

basic_string

STL

类模版

string

wstring

CComBSTR

ATL

BSTR 封装类,允许直接访问底层BSTR。

它在某些情况下比_bstr_t有用的多。

CComVariant

ATL

VARIANT的封装类,但是VARIANT没有被隐藏。

CString

WTL

行为和MFC的 CString完全一样。

System::String

CLR 和 VC 7 类

一个String对象包含一个不可改变的字符串序列。

八.常用字符串转换

1、函数 WideCharToMultiByte(),转换 UNICODE 到 MBCS。使用范例:

LPCOLESTR lpw = L"Hello,你好";

size_t wLen = wcslen( lpw ) + 1; // 宽字符字符长度,+1表示包含字符串结束符

int aLen=WideCharToMultiByte( // 第一次调用,计算所需 MBCS 字符串字节长度

CP_ACP,

0,

lpw,// 宽字符串指针

wLen, // 字符长度

NULL,

0,// 参数0表示计算转换后的字符空间

NULL,

NULL);

LPSTR lpa = new char [aLen];

WideCharToMultiByte(

CP_ACP,

0,

lpw,

wLen,

lpa,// 转换后的字符串指针

aLen, // 给出空间大小

NULL,

NULL);

// 此时,lpa 中保存着转换后的 MBCS 字符串

... ... ... ...

delete [] lpa;

2、函数 MultiByteToWideChar(),转换 MBCS 到 UNICODE。使用范例:

LPCSTR lpa = "Hello,你好";

size_t aLen = strlen( lpa ) + 1;

int wLen = MultiByteToWideChar(

CP_ACP,

0,

lpa,

aLen,

NULL,

0);

LPOLESTR lpw = new WCHAR [wLen];

MultiByteToWideChar(

CP_ACP,

0,

lpa,

aLen,

lpw,

wLen);

... ... ... ...

delete [] lpw;

3、使用 ATL 提供的转换宏。

A2BSTR

OLE2A

T2A

W2A

A2COLE

OLE2BSTR

T2BSTR

W2BSTR

A2CT

OLE2CA

T2CA

W2CA

A2CW

OLE2CT

T2COLE

W2COLE

A2OLE

OLE2CW

T2CW

W2CT

A2T

OLE2T

T2OLE

W2OLE

A2W

OLE2W

T2W

W2T


上表中的宏函数,其实非常容易记忆:

2

好搞笑的缩写,to 的发音和 2 一样,所以借用来表示“转换为、转换到”的含义。

A

ANSI 字符串,也就是 MBCS。

W、OLE

宽字符串,也就是 UNICODE。

T

中间类型T。如果定义了 _UNICODE,则T表示W;如果定义了 _MBCS,则T表示A

C

const 的缩写

使用范例:

#include <atlconv.h>

void fun()

{

USES_CONVERSION; // 只需要调用一次,就可以在函数中进行多次转换

LPCTSTR lp = OLE2CT( L"Hello,你好") );

... ... ... ...

// 不用显式释放 lp 的内存,因为

// 由于 ATL 转换宏使用栈作为临时空间,函数结束后会自动释放栈空间。

}

  使用 ATL 转换宏,由于不用释放临时空间,所以使用起来非常方便。但是考虑到栈空间的尺寸(VC 默认2M),使用时要注意几点:


1、只适合于进行短字符串的转换;
2、不要试图在一个次数比较多的循环体内进行转换;
3、不要试图对字符型文件内容进行转换,因为文件尺寸一般情况下是比较大的;
4、对情况 2 和 3,要使用 MultiByteToWideChar() 和 WideCharToMultiByte();