转 ASCII, DBCS,UNICODE小结

引言

  毫无疑问,我们都看到过像 TCHAR, std::string, BSTR 等各种各样的字符串类型,还有那些以 _tcs 开头的奇怪的宏。你也许正在盯着显示器发愁。本指引将总结引进各种字符类型的目的,展示一些简单的用法,并告诉您在必要时,如何实现各种字符串类型之间的 转换。

  在第一部分,我们将介绍3种字符编码类型。了解各种编码模式的工作方式是很重要的事情。即使你已经知道一个字符串是一个字符数组,你也应该阅读本部分。一旦你了解了这些,你将对各种字符串类型之间的关系有一个清楚地了解。

  在第二部分,我们将单独讲述string类,怎样使用它及实现他们相互之间的转换。

字符基础 -- ASCII, DBCS, 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

使用两个字节表示的0x0000来做结束标志。

   一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我们一会儿将看到 DBCS 字符串的微妙之处,它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。字符串"日本语 " ("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"的编码。(So on a big-endian CPU, the bytes would still be in the order shown above.)

使用字符串处理函数

   我们都已经见过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***()和wcs***()的用法及它们之间的区别。Str***()和_mbs**()之间的有区别区别呢?明白 他们之间的区别,对于采用正确的方法来遍历DBCS字符串是很重要的。下面,我们将先介绍字符串的遍历,然后回到str***()与_mbs***() 之间的区别这个问题上来。

正确的遍历和索引字符串

   因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串 中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。

  然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。

# 1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte;
# 2.永远不要使用-操作进行后向遍历。

   我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表 中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:/Program Files/MyCoolApp,那么你合成的文件名应该是C:/Program Files/MyCoolApp/config.bin。当你进行测试时,你发现程序运行正常。

  现在,想象你合成文件名的代码可能是这样的:

    bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];

// Read install dir from registry... we''ll assume it succeeds.

// Add on a backslash if it wasn''t present in the registry value.
// First, get a pointer to the terminating zero.
char* pLastChar = strchr ( szConfigFilename, ''/0'' );

// Now move it back one character.
pLastChar--;

if ( *pLastChar != ''//'' )
strcat ( szConfigFilename, "//" );

// Add on the name of the config file.
strcat ( szConfigFilename, "config.bin" );

// If the caller''s buffer is big enough, return the filename.
if ( strlen ( szConfigFilename ) >= nBuffSize )
return false;
else
{
strcpy ( pszName, szConfigFilename );
return true;
}
}

  这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个中国用户使用了你的程序,把它安装在 C:/伟大祖国。下面是这个名字在内存中的存储形式:

43
3A
5C
83 88
83 45
83 52
83 5C
00

LB TB
LB TB
LB TB
LB TB

C
:
/




EOS

 

  当使用 GetConfigFileName() 检查尾部的''//''时,它寻找安装目录名中最后的非0字节,看它是等于''//''的,所以没有重新增加一个''//''。结果是代码返回了错误的文件名。

  哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠''//''的值是0x5c。''国 ''的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。

  正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明)

    bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];

// Read install dir from registry... we''ll assume it succeeds.

// Add on a backslash if it wasn''t present in the registry value.
// First, get a pointer to the terminating zero.
char* pLastChar = _mbschr ( szConfigFilename, ''/0'' );

// Now move it back one double-byte character.
pLastChar = CharPrev ( szConfigFilename, pLastChar );

if ( *pLastChar != ''//'' )
_mbscat ( szConfigFilename, "//" );

// Add on the name of the config file.
_mbscat ( szConfigFilename, "config.bin" );

// If the caller''s buffer is big enough, return the filename.
if ( _mbslen ( szInstallDir ) >= nBuffSize )
return false;
else
{
_mbscpy ( pszName, szConfigFilename );
return true;
}
}

  上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0x5c。

   让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了'':''。如果,你使用++操作来遍历字符串,而不是使用 CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于'':''的值。

与规则2相关的关于字符串索引的规则:

2a. 永远不要使用减法去得到一个字符串的索引。

违背这条规则的代码和违背规则2的代码很相似。例如,

char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];

这和向后移动一个指针是同样的效果。

回到关于str***()和_mbs***()的区别.

   现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用 strrchr("C:// ", ''//''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''//''的指针。

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

Win32 API中的MBCS和Unicode
两组 APIs:

   尽管你也许从来没有注意过,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 SetWindowText SetWindowTextW
#else
#define SetWindowText SetWindowTextA
#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前缀。

TCHAR szNewText[] = _T("we love Bob!");

   像是用宏来隐藏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."标题下有完整的宏列表。

字符串和TCHAR typedefs

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

type
Meaning in MBCS builds
Meaning in Unicode builds

WCHAR
wchar_t
wchar_t

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*)

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*)

 

何时使用 TCHAR 和 Unicode

  到现在,你可能会问,我们为什么要使用Unicode。我已经用了很多年的char。下列3种情况下,使用Unicode将会使你受益:

# 1.你的程序只运行在Windows NT系统中。
# 2. 你的程序需要处理超过MAX_PATH个字符长的文件名。
# 3. 你的程序需要使用XP中引入的只有Unicode版本的API.

   Windows 9x 中大多数的 API 没有实现 Unicode 版本。所以,如果你的程序要在windows 9x中运行,你必须使用MBCS APIs。然而,由于NT系统内部都使用Unicode,所以使用Unicode APIs将会加快你的程序的运行速度。每次,你传递一个字符串调用MBCS API,操作系统会把这个字符串转换成Unicode字符串,然后调用对应的Unicode API。如果一个字符串被返回,操作系统还要把它转变回去。尽管这个转换过程被高度优化了,但它对速度造成的损失是无法避免的。

   只要你使用Unicode API,NT系统允许使用非常长的文件名(突破了MAX_PATH的限制,MAX_PATH=260)。使用Unicode API的另一个优点是你的程序会自动处理用户输入的各种语言。所以一个用户可以输入英文,中文或者日文,而你不需要额外编写代码去处理它们。

   最后,随着windows 9x产品的淡出,微软似乎正在抛弃MBCS APIs。例如,包含两个字符串参数的SetWindowTheme() API只有Unicode版本的。使用Unicode来build你的程序将会简化字符串的处理,你不必在MBCS和Unicdoe之间相互转换。

即使你现在不使用Unicode来build你的程序,你也应该使用TCHAR及其相关的宏。这样做不仅可以的代码可以很好地处理DBCS,而且如果将来你想用Unicode来build你的程序,你只需要改变一下预处理器中的设置就可以实现了。
 因为C语言风格的字符串容易出错且不易管理,黑客们甚至利用可能存在的缓冲区溢出bug把C语言风格的字符串作为攻击目标,所以出现了很多字符串封装类。不幸的是,在某些场合下我们不知道该使用哪个字符串类,也不知道怎样把一个C风格的字符串转换成一个字符串封装类。

   这篇文章将介绍所有在Win32 API, MFC, STL, WTL 和 Visual C++ 运行库中出现的字符串类型。我将描述每一个类的用法,告诉大家怎样创建每一个类的对象以及怎样把一个类转换成其他类。受控字符串和Visual C++ 7中的类两部分是Nish完成的。

  为了更好的从这篇文章中受益,你必须要明白不同的字符类型和编码,这些内容我在第一部分中介绍过。

Rule #1 of string classes

  使用cast来实现类型转换是不好的做法,除非有文档明确指出这种转换可以使用。

促 使我写这两篇文章的原因是字符串类型转换中经常遇到的一些问题。当我们使用cast把字符串从类型X转换到类型Z的时候,我们不知道为什么代码不能正常工 作。各种各样的字符串类型,尤其是BSTR,几乎没有在任何一个地方的文档中被明确的指出可以用cast来实现类型转换。所以我想一些人可能会使用 cast来实现类型转换并希望这种转换能够正常工作。

  除非源字符串是一个被明确指明支持转换操作符的字符串包装类,否则cast不对字符串做任何转换。对常量字符串使用cast不会起到任何作用,所以下面的代码:

    void SomeFunc ( LPCWSTR widestr );
    main()
    {
    SomeFunc ( (LPCWSTR) "C://foo.txt" );  // WRONG!
    }

  肯定会失败。它可以被编译,因为cast操作会撤消编译器的类型检查。但是,编译可以通过并不能说明代码是正确的。

  在下面的例子中,我将会指明cast在什么时候使用是合法的。

C-style strings and typedefs

   正如我在第一部分中提到的,windows APIs 是用TCHARs来定义的,在编译时,它可以根据你是否定义_MBCS或者_UNICODE被编译成MBCS或者Unicode字符。你可以参看第一部分 中对TCHAR的完整描述,这里为了方便,我列出了字符的typedefs

Type
Meaning

WCHAR
Unicode character (wchar_t)

TCHAR
MBCS or Unicode character, depending on preprocessor settings

LPSTR
string of char (char*)

LPCSTR
constant string of char (const char*)

LPWSTR
string of WCHAR (WCHAR*)

LPCWSTR
constant string of WCHAR (const WCHAR*)

LPTSTR
string of TCHAR (TCHAR*)

LPCTSTR
constant 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

OLECHAR
Unicode character (wchar_t)

LPOLESTR
string of OLECHAR (OLECHAR*)

LPCOLESTR
constant string of OLECHAR (const OLECHAR*)

 

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

COM 中的字符串 —— BSTR 和 VARIANT

  很多自动化和COM接口使用BSTR来定义字符串。BSTRs中有几个"陷阱",所以这里我用单独的部分来说明它。

   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 实际上就是一个指向字符串中第一个字符的指针。它的定义如下:

    BSTR bstr = NULL;
    bstr = SysAllocString ( L"Hi Bob!" );
    if ( NULL == bstr )
    // out of memory error
    // Use bstr here...
    SysFreeString ( bstr );

自然的,各种各样的BSTR封装类为你实现内存管理。

   另外一个用在自动化接口中的变量类型是VARIANT。它被用来在无类型(typeless)语言,如Jscript和VBScript,来传递数据。 一个VARIANT可能含有很多不同类型的数据,例如long和IDispatch*。当一个VARIANT包含一个字符串,字符串被存成一个 BSTR。当我后面讲到VARIANT封装类时,我会对VARIANT多些介绍。

字符串封装类

   到目前为止,我已经介绍了各种各样的字符串。下面,我将说明封装类。对于每个封装类,我将展示怎样创建一个对象及怎样把它转换成一个C语言风格的字符串 指针。C语言风格的字符串指针对于API的调用,或者创建一个不同的字符串类对象经常是必需的。我不会介绍字符串类提供的其他操作,比如排序和比较。

  重复一遍,除非你确切的明白结果代码将会做什么,否则不要盲目地使用cast来实现类型转换。

CRT提供的类

_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
_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结构。

_variant_t

  _variant_t是一个对VARIANT的完整封装,它提供很多构造函数和转换函数来操作一个VARIANT可能包含的大量的数据类型。这里,我将只介绍与字符串有关的操作。

    // Constructing
    _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++语言所允许的。

STL 类

   STL只有一个字符串类,basic_string。一个basic_string管理一个以0做结束符的字符串数组。字符的类型是 basic_string模般的参数。总的来说,一个basic_string类型的变量应该被当作不透明的对象。你可以得到一个指向内部缓冲区的只读指 针,但是任何写操作必须使用basic_string的操作符和方法。
  basic_string有两个预定义的类型:包含char的string类型和包含wchar_t的wstring类型。这里没有内置的包含TCHAR的类型,但是你可以使用下面列出的代码来实现。

    // Specializations
    typedef basic_string tstring; // string of TCHARs
   
    // Constructing
    string str = "char string";         // construct from a LPCSTR
wstring wstr = L"wide char string"; // construct from a LPCWSTR
    tstring tstr = _T("TCHAR string");  // construct from a LPCTSTR
   
    // Extracting data
    LPCSTR psz = str.c_str();    // read-only pointer to str''s buffer
LPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr''s buffer
    LPCTSTR ptsz = tstr.c_str(); // read-only pointer to tstr''s buffer

  不像_bstr_t,一个basic_string变量不能在字符集之间直接转换。然而,你可以传递由c_str()返回的指针给另外一个类的构造函数(如果这个类的构造函数接受这种字符类型)。例如:

    // Example, construct _bstr_t from basic_string
    _bstr_t bs1 = str.c_str();  // construct a _bstr_t from a LPCSTR
    _bstr_t bs2 = wstr.c_str(); // construct a _bstr_t from a LPCWSTR

ATL 类
CComBSTR

   CComBSTR 是 ATL 中的 BSTR 封装类,它在某些情况下比_bstr_t有用的多。最引人注意的是CComBSTR允许访问底层的BSTR,这意味着你可以传递一个CComBSTR对象 给COM的方法。CComBSTR对象能够替你自动的管理BSTR的内存。例如,假设你想调用下面这个接口的方法:

    // Sample interface:
    struct IStuff : public IUnknown
    {
    // Boilerplate COM stuff omitted...
    STDMETHOD(SetText)(BSTR bsText);
    STDMETHOD(GetText)(BSTR* pbsText);
    };

  CComBSTR有一个操作符--BSTR方法,所以它能直接被传给SetText()函数。还有另外一个操作--&,这个操作符返回一个 BSTR*。所以,你可以对一个CComBSTR对象使用&操作符,然后把它传给需要BSTR*参数的函数。

    CComBSTR bs1;
    CComBSTR bs2 = "new text";
   
    pStuff->GetText ( &bs1 );       // ok, takes address of internal BSTR
    pStuff->SetText ( bs2 );        // ok, calls BSTR converter
    pStuff->SetText ( (BSTR) bs2 ); // cast ok, same as previous line

  CComBSTR有和_bstr_t相似的构造函数,然而却没有内置的向MBCS字符串转换的函数。因此,你需要使用一个ATL转换宏。

    // Constructing
    CComBSTR bs1 = "char string";       // construct from a LPCSTR
CComBSTR bs2 = L"wide char string"; // construct from a LPCWSTR
    CComBSTR bs3 = bs1;                 // copy from another CComBSTR
    CComBSTR bs4;

bs4.LoadString ( IDS_SOME_STR ); // load string from string table
    // Extracting data
    BSTR bstr1 = bs1;        // returns internal BSTR, but don''t modify it!
    BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous line
    BSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTR
    BSTR bstr4;
    bstr4 = bs1.Detach();  // bs1 no longer manages its BSTR
    // ...
    SysFreeString ( bstr3 );
    SysFreeString ( bstr4 );

  注意在上个例子中使用了Detach()方法。调用这个方法后,CComBSTR对象不再管理它的BSTR字符串或者说它对应的内存。这就是bstr4需要调用SysFreeString()的原因。

   做一个补充说明:重载的&操作符意味着在一些STL容器中你不能直接使用CComBSTR变量,比如list。容器要求&操作符返回一 个指向容器包含的类的指针,但是对CComBSTR变量使用&操作符返回的是BSTR*,而不是CComBSTR*。然而,有一个ATL类可以解 决这个问题,这个类是CAdapt。例如,你可以这样声明一个CComBSTR的list:

std::list< CAdapt > bstr_list;

  CAdapt提供容器所需要的操作符,但这些操作符对你的代码是透明的。你可以把一个bstr_list当作一个CComBSTR的list来使用。

CComVariant

   CComVariant是VARIANT的封装类。然而,不像_variant_t,在CComVariant中VARIANT没有被隐藏。事实上你需 要直接访问VARIANT的成员。CComVariant提供了很多构造函数来对VARIANT能够包含的多种类型进行处理。这里,我将只介绍和字符串相 关的操作。

    // Constructing
    CComVariant v1 = "char string";       // construct from a LPCSTR
CComVariant v2 = L"wide char string"; // construct from a LPCWSTR
    CComBSTR bs1 = "BSTR bob";
    CComVariant v3 = (BSTR) bs1;          // copy from a BSTR
   
    // Extracting data
    CComBSTR bs2 = v1.bstrVal;            // extract BSTR from the VARIANT

   不像_variant_t,这里没有提供针对VARIANT包含的各种类型的转换操作符。正如上面介绍的,你必须直接访问VARIANT的成员并且确保 这个VARIANT变量保存着你期望的类型。如果你需要把一个CComVariant类型的数据转换成一个BSTR类型的数据,你可以调用 ChangeType()方法。

    CComVariant v4 = ... // Init v4 from somewhere
    CComBSTR bs3;
   
    if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) ))
    bs3 = v4.bstrVal;

  像_variant_t一样,CComVariant也没有提供向MBCS字符串转换的转换操作。你需要创建一个_bstr_t类型的中间变量,使用提供从Unicode到MBCS转换的另一个字符串类,或者使用一个ATL的转换宏。

ATL转换宏

   ATL:转换宏是各种字符编码之间进行转换的一种很方便的方式,在函数调用时,它们显得非常有用。ATL转换宏的名称是根据下面的模式来命名的[源类 型]2[新类型]或者[源类型]2C[新类型]。据有第二种形式的名字的宏的转换结果是常量指针(对应名字中的"C")。各种类型的简称如下:

    A: MBCS string, char* (A for ANSI)
    W: Unicode string, wchar_t* (W for wide)
    T: TCHAR string, TCHAR*
    OLE: OLECHAR string, OLECHAR* (in practice, equivalent to W)
    BSTR: BSTR (used as the destination type only)

  所以,W2A()宏把一个Unicode字符串转换成一个MBCS字符串。T2CW()宏把一个TCHAR字符串转转成一个Unicode字符串常量。

   为了使用这些宏,需要先包含atlconv.h头文件。你甚至可以在非ATL工程中包含这个头文件来使用其中定义的宏,因为这个头文件独立于ATL中的 其他部分,不需要一个_Module全局变量。当你在一个函数中使用转换宏时,需要把USES_CONVERSION宏放在函数的开头。它定义了转换宏所 需的一些局部变量。

   当转换的目的类型是除了BSTR以外的其他类型时,被转换的字符串是存在栈中的。所以,如果你想让字符串的生命周期比当前的函数长,你需要把这个字符串 拷贝到其他的字符串类中。当目的类型是BSTR时,内存不会自动被释放,你必须把返回值赋给一个BSTR变量或者一个BSTR封装类以避免内存泄漏。

  下面是一些各种转换宏的使用例子:

    // Functions taking various strings:
    void Foo ( LPCWSTR wstr );
    void Bar ( BSTR bstr );
    // Functions returning strings:
    void Baz ( BSTR* pbstr );
    #include
    main()
    {
    using std::string;
    USES_CONVERSION;    // declare locals used by the ATL macros
    // Example 1: Send an MBCS string to Foo()
    LPCSTR psz1 = "Bob";
    string str1 = "Bob";
   
    Foo ( A2CW(psz1) );
    Foo ( A2CW(str1.c_str()) );
 
    // Example 2: Send a MBCS and Unicode string to Bar()
    LPCSTR psz2 = "Bob";
    LPCWSTR wsz = L"Bob";
    BSTR bs1;
    CComBSTR bs2;
 
    bs1 = A2BSTR(psz2);         // create a BSTR
    bs2.Attach ( W2BSTR(wsz) ); // ditto, assign to a CComBSTR
    Bar ( bs1 );
    Bar ( bs2 );
 
    SysFreeString ( bs1 );      // free bs1 memory
    // No need to free bs2 since CComBSTR will do it for us.
 
    // Example 3: Convert the BSTR returned by Baz()
    BSTR bs3 = NULL;
    string str2;
    Baz ( &bs3 );          // Baz() fills in bs3
    str2 = W2CA(bs3);      // convert to an MBCS string
    SysFreeString ( bs3 ); // free bs3 memory
    }

  正如你所看见的,当你有一个和函数所需的参数类型不同的字符串时,使用这些转换宏是非常方便的。

MFC类

CString

   因为一个MFC CString类的对象包含TCHAR类型的字符,所以确切的字符类型取决于你所定义的预处理符号。大体来说,CString 很像STL string,这意味着你必须把它当成不透明的对象,只能使用CString提供的方法来修改CString对象。CString有一个string所不 具备的优点:CString具有接收MBCS和Unicode两种字符串的构造函数,它还有一个LPCTSTR转换符,所以你可以把CString对象直 接传给一个接收LPCTSTR的函数而不需要调用c_str()函数。

    // Constructing
    CString s1 = "char string";  // construct from a LPCSTR
    CString s2 = L"wide char string";  // construct from a LPCWSTR
    CString s3 ( '' '', 100 );  // pre-allocate a 100-byte buffer, fill with spaces
    CString s4 = "New window text";
   
    // You can pass a CString in place of an LPCTSTR:
    SetWindowText ( hwndSomeWindow, s4 );
 
    // Or, equivalently, explicitly cast the CString:
    SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );

  你可以从你的字符串表中装载一个字符串,CString的一个构造函数和LoadString()函数可以完成它。Format()方法能够从字符串表中随意的读取一个具有一定格式的字符串。    

    // Constructing/loading from string table
    CString s5 ( (LPCTSTR) IDS_SOME_STR );  // load from string table
    CString s6, s7;
    // Load from string table.
    s6.LoadString ( IDS_SOME_STR );
 
    // Load printf-style format string from the string table:
    s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );

   第一个构造函数看起来有点奇怪,但是这实际上是文档说明的装入一个字符串的方法。注意,对一个CString变量,你可以使用的唯一合法转换符是 LPCTSTR。转换成LPTSTR(非常量指针)是错误的。养成把一个CString变量转换成LPTSTR的习惯将会给你带来伤害,因为当你的程序后 来崩溃时,你可能不知道为什么,因为你到处都使用同样的代码而那时它们都恰巧正常工作。正确的得到一个指向缓冲区的非常量指针的方法是调用 GetBuffer()方法。下面是正确的用法的一个例子,这段代码是给一个列表控件中的项设定文字:

    CString str = _T("new text");
    LVITEM item = {0};
    item.mask = LVIF_TEXT;
    item.iItem = 1;
    item.pszText = (LPTSTR)(LPCTSTR) str; // WRONG!
item.pszText = str.GetBuffer(0); // correct
 
    ListView_SetItem ( &item );
    str.ReleaseBuffer();  // return control of the buffer to str

   pszText成员是一个LPTSTR变量,一个非常量指针,因此你需要对str调用GetBuffer()。GetBuffer()的参数是你需要 CString为缓冲区分配的最小长度。如果因为某些原因,你需要一个可修改的缓冲区来存放1K TCHARs,你需要调用GetBuffer(1024)。把0作为参数时,GetBuffer()返回的是指向字符串当前内容的指针

   上面划线的语句可以被编译,在这种情况下,甚至可以正常起作用。但这并不意味着这行代码是正确的。通过使用非常量转换,你已经破坏了面向对象的封装,并 对 CString的内部实现作了某些假定。如果你有这样的转换习惯,你终将会陷入代码崩溃的境地。你会想代码为什么不能正常工作了,因为你到处都使用同样的 代码而那些代码看起来是正确的。

  你知道人们总是抱怨现在的软件的bug是多么的多吗?软件中的bug是因为程序员写了不正确的代码。难道你真的想写一些你知道是错误的代码来为所有的软件都满是bug这种认识做贡献吗?花些时间来学习使用CString的正确方法让你的代码在任何时间都正常工作把。

  CString 有两个函数来从一个 CString 创建一个 BSTR。它们是 AllocSysString() 和SetSysString()。

    // Converting to BSTR
    CString s5 = "Bob!";
    BSTR bs1 = NULL, bs2 = NULL;
    bs1 = s5.AllocSysString();
    s5.SetSysString ( &bs2 );
    SysFreeString ( bs1 );
    SysFreeString ( bs2 );

    COleVariant

   COleVariant和CComVariant.很相似。COleVariant继承自VARIANT,所以它可以传给接收VARIANT的函数。然 而,不像CComVariant,COleVariant只有一个LPCTSTR构造函数。没有对LPCSTR 和LPCWSTR的构造函数。在大多数情况下这不是一个问题,因为不管怎样你的字符串很可能是LPCTSTRs,但这是一个需要意识到的问题。 COleVariant还有一个接收CString参数的构造函数。

    // Constructing
    CString s1 = _T("tchar string");
    COleVariant v1 = _T("Bob"); // construct from an LPCTSTR
    COleVariant v2 = s1; // copy from a CString

   像CComVariant一样,你必须直接访问VARIANT的成员。如果需要把VARIANT转换成一个字符串,你应该使用ChangeType() 方法。然而,COleVariant::ChangeType()如果失败会抛出异常,而不是返回一个表示失败的HRESULT代码。

    // Extracting data
    COleVariant v3 = ...; // fill in v3 from somewhere
    BSTR bs = NULL;
    try
    {
    v3.ChangeType ( VT_BSTR );
    bs = v3.bstrVal;
    }
    catch ( COleException* e )
    {
    // error, couldn''t convert
    }
    SysFreeString ( bs );
 

WTL 类
CString

  WTL的CString的行为和MFC的 CString完全一样,所以你可以参考上面关于MFC的 CString的介绍。

CLR 和 VC 7 类

   System::String是用来处理字符串的.NET类。在内部,一个String对象包含一个不可改变的字符串序列。任何对String对象的操 作实际上都是返回了一个新的String对象,因为原始的对象是不可改变的。String的一个特性是如果你有不止一个String对象包含相同的字符序 列,它们实际上是指向相同的对象的。相对于C++的使用扩展是增加了一个新的字符串常量前缀S,S用来代表一个受控的字符串常量(a managed string literal)。

    // Constructing
    String* ms = S"This is a nice managed string";

  你可以传递一个非受控的字符串来创建一个String对象,但是样会比使用受控字符串来创建String对象造成效率的微小损失。这是因为所有以S作为前缀的相同的字符串实例都代表同样的对象,但这对非受控对象是不适用的。下面的代码清楚地阐明了这一点:

    String* ms1 = S"this is nice";
    String* ms2 = S"this is nice";
    String* ms3 = L"this is nice";
    Console::WriteLine ( ms1 == ms2 ); // prints true
    Console::WriteLine ( ms1 == ms3);  // prints false

正确的比较可能没有使用S前缀的字符串的方法是使用String::CompareTo()

    Console::WriteLine ( ms1->CompareTo(ms2) );
Console::WriteLine ( ms1->CompareTo(ms3) );

   上面的两行代码都会打印0,0表示两个字符串相等。 String和MFC 7 CString之间的转换是很容易的。CString有一个向LPCTSTR的转换操作,而String有两个接收char* 和 wchar_t*的构造函数,因此你可以把一个CString变量直接传给一个String的构造函数。

    CString s1 ( "hello world" );
    String* s2 ( s1 );  // copy from a CString

反方向的转换也很类似

    String* s1 = S"Three cats";
    CString s2 ( s1 );

  这也许会使你感到一点迷惑,但是它确实是起作用的。因为从VS.NET 开始,CString 有了一个接收String 对象的构造函数。

   CStringT ( System::String* pString );

对于一些快速操作,你可能想访问底层的字符串:

    String* s1 = S"Three cats";
    Console::WriteLine ( s1 );
    const __wchar_t __pin* pstr = PtrToStringChars(s1);
    for ( int i = 0; i < wcslen(pstr); i++ )
    (*const_cast<__wchar_t*>(pstr+i))++;
    Console::WriteLine ( s1 );

  PtrToStringChars()返回一个指向底层字符串的const __wchar_t* ,我们需要固定它,否则垃圾收集器或许会在我们正在管理它的内容的时候移动了它。

在 printf-style 格式函数中使用字符串类

   当你在printf()或者类似的函数中使用字符串封装类时你必须十分小心。这些函数包括sprintf()和它的变体,还有TRACE和 ATLTRACE宏。因为这些函数没有对添加的参数的类型检查,你必须小心,只能传给它们C语言风格的字符串指针,而不是一个完整的字符串类。

  例如,要把一个_bstr_t 字符串传给ATLTRACE(),你必须使用显式转换(LPCSTR) 或者(LPCWSTR):

    _bstr_t bs = L"Bob!";
    ATLTRACE("The string is: %s in line %d/n", (LPCSTR) bs, nLine);

  如果你忘了使用转换符而把整个_bstr_t对象传给了函数,将会显示一些毫无意义的输出,因为_bstr_t保存的内部数据会全部被输出。

所有类的总结

  两个字符串类之间进行转换的常用方式是:先把源字符串转换成一个C语言风格的字符串指针,然后把这个指针传递给目的类型的构造函数。下面这张表显示了怎样把一个字符串转换成一个C语言风格的字符串指针以及哪些类具有接收C语言风格的字符串指针的构造函数。

Class
string type
convert to char*?
convert to const char*?
convert to wchar_t*?
convert to const wchar_t*?
convert to BSTR?
construct from char*?
construct from wchar_t*?

_bstr_t
BSTR
yes cast1
yes cast
yes cast1
yes cast
yes2
yes
yes

_variant_t
BSTR
no
no
no
cast to
_bstr_t3
cast to
_bstr_t3
yes
yes

string
MBCS
no
yes c_str() method
no
no
no
yes
no

wstring
Unicode
no
no
no
yes c_str() method
no
no
yes

CComBSTR
BSTR
no
no
no
yes cast to BSTR
yes cast
yes
yes

CComVariant
BSTR
no
no
no
yes4
yes4
yes
yes

CString
TCHAR
no6
in MBCS
builds, cast
no6
in Unicode
builds, cast
no5
yes
yes

COleVariant
BSTR
no
no
no
yes4
yes4
in MBCS
builds
in Unicode
builds

  • 1、即使 _bstr_t 提供了向非常量指针的转换操作符,修改底层的缓冲区也会已引起GPF如果你溢出了缓冲区或者造成内存泄漏。
  • 2、_bstr_t 在内部用一个 wchar_t* 来保存 BSTR,所以你可以使用 const wchar_t* 来访问BSTR。这是一个实现细节,你可以小心的使用它,将来这个细节也许会改变。
  • 3、如果数据不能转换成BSTR会抛出一个异常。
  • 4、使用 ChangeType(),然后访问 VARIANT 的 bstrVal 成员。在MFC中,如果数据转换不成功将会抛出异常。
  • 5、这里没有转换 BSTR 函数,然而 AllocSysString() 返回一个新的BSTR。
  • 6、使用 GetBuffer() 方法,你可以暂时地得到一个非常量的TCHAR指针。
  • UTF-8和unicode终极解答

    UTF-8和unicode终极解答

    (完全摘自互联网:http://www.linuxforum.net/books/UTF-8-Unicode.html

    UTF-8 and Unicode FAQ

    by Markus Kuhn

    中国LINUX论坛翻译小组 xLoneStar[译] 2000年2月

    这 篇文章说明了在 POSIX 系统 (Linux,Unix) 上使用 Unicode/UTF-8 所需要的信息. 在将来不远的几年里, Unicode 已经很接近于取代 ASCII 与 Latin-1 编码的位置了. 它不仅允许你处理处理事实上存在于地球上的任何语言文字, 而且提供了一个全面的数学与技术符号集, 因此可以简化科学信息交换.

    UTF-8 编码提供了一种简便而向后兼容的方法, 使得那种完全围绕 ASCII 设计的操作系统, 比如 Unix, 也可以使用 Unicode. UTF-8 就是 Unix, Linux 已经类似的系统使用 Unicode 的方式. 现在是你了解它的时候了.

    什么是 UCS 和 ISO 10646?

    国际标准 ISO 10646 定义了 通用字符集 (Universal Character Set, UCS). UCS 是所有其他字符集标准的一个超集. 它保证与其他字符集是双向兼容的. 就是说, 如果你将任何文本字符串翻译到 UCS格式, 然后再翻译回原编码, 你不会丢失任何信息.

    UCS 包含了用于表达所有已知语言的字符. 不仅包括拉丁语,希腊语, 斯拉夫语,希伯来语,阿拉伯语,亚美尼亚语和乔治亚语的描述, 还包括中文, 日文和韩文这样的象形文字, 以及 平假名, 片假名, 孟加拉语, 旁遮普语果鲁穆奇字符(Gurmukhi), 泰米尔语, 印.埃纳德语(Kannada), Malayalam, 泰国语, 老挝语, 汉语拼音(Bopomofo), Hangul, Devangari, Gujarati, Oriya, Telugu 以及其他数也数不清的语. 对于还没有加入的语言, 由于正在研究怎样在计算机中最好地编码它们, 因而最终它们都将被加入. 这些语言包括 Tibetian, 高棉语, Runic(古代北欧文字), 埃塞俄比亚语, 其他象形文字, 以及各种各样的印-欧语系的语言, 还包括挑选出来的艺术语言比如 Tengwar, Cirth 和 克林贡语(Klingon). UCS 还包括大量的图形的, 印刷用的, 数学用的和科学用的符号, 包括所有由 TeX, Postscript, MS-DOS,MS-Windows, Macintosh, OCR 字体, 以及许多其他字处理和出版系统提供的字符.

    ISO 10646 定义了一个 31 位的字符集. 然而, 在这巨大的编码空间中, 迄今为止只分配了前 65534 个码位 (0x0000 到 0xFFFD). 这个 UCS 的 16位子集称为 基本多语言面 (Basic Multilingual Plane, BMP). 将被编码在 16 位 BMP 以外的字符都属于非常特殊的字符(比如象形文字), 且只有专家在历史和科学领域里才会用到它们. 按当前的计划, 将来也许再也不会有字符被分配到从 0x000000 到 0x10FFFF 这个覆盖了超过 100 万个潜在的未来字符的 21 位的编码空间以外去了. ISO 10646-1 标准第一次发表于 1993 年, 定义了字符集与 BMP 中内容的架构. 定义 BMP 以外的字符编码的第二部分 ISO 10646-2 正在准备中, 但也许要过好几年才能完成. 新的字符仍源源不断地加入到 BMP 中, 但已经存在的字符是稳定的且不会再改变了.

    UCS 不仅给每个字符分配一个代码, 而且赋予了一个正式的名字. 表示一个 UCS 或 Unicode 值的十六进制数, 通常在前面加上 "U+", 就象 U+0041 代表字符"拉丁大写字母A". UCS 字符 U+0000 到 U+007F 与 US-ASCII(ISO 646) 是一致的, U+0000 到 U+00FF 与 ISO 8859-1(Latin-1) 也是一致的. 从 U+E000 到 U+F8FF, 已经 BMP 以外的大范围的编码是为私用保留的.

    什么是组合字符?

    UCS里有些编码点分配给了 组合字符. 它们类似于打字机上的无间隔重音键. 单个的组合字符不是一个完整的字符. 它是一个类似于重音符或其他指示标记, 加在前一个字符后面. 因而, 重音符可以加在任何字符后面. 那些最重要的被加重的字符, 就象普通语言的正字法(orthographies of common languages)里用到的那种, 在 UCS 里都有自己的位置, 以确保同老的字符集的向后兼容性. 既有自己的编码位置, 又可以表示为一个普通字符跟随一个组合字符的被加重字符, 被称为 预作字符(precomposed characters). UCS 里的预作字符是为了同没有预作字符的旧编码, 比如 ISO 8859, 保持向后兼容性而设的. 组合字符机制允许在任何字符后加上重音符或其他指示标记, 这在科学符号中特别有用, 比如数学方程式和国际音标字母, 可能会需要在一个基本字符后组合上一个或多个指示标记.

    组合字符跟随着被修饰的字符. 比如, 德语中的元音变音字符 ("拉丁大写字母A 加上分音符"), 既可以表示为 UCS 码 U+00C4 的预作字符, 也可以表示成一个普通 "拉丁大写字母A" 跟着一个"组合分音符":U+0041 U+0308 这样的组合. 当需要堆叠多个重音符, 或在一个基本字符的上面和下面都要加上组合标记时, 可以使用多个组合字符. 比如在泰国文中, 一个基本字符最多可加上两个组合字符.

    什么是 UCS 实现级别?

    不是所有的系统都需要支持象组合字符这样的 UCS 里所有的先进机制. 因此 ISO 10646 指定了下列三种实现级别:

    级别1
    不支持组合字符和 Hangul Jamo 字符 (一种特别的, 更加复杂的韩国文的编码, 使用两个或三个子字符来编码一个韩文音节)
    级别2
    类 似于级别1, 但在某些文字中, 允许一列固定的组合字符 (例如, 希伯来文, 阿拉伯文, Devangari, 孟加拉语, 果鲁穆奇语, Gujarati, Oriya, 泰米尔语, Telugo, 印.埃纳德语, Malayalam, 泰国语和老挝语). 如果没有这最起码的几个组合字符, UCS 就不能完整地表达这些语言.
    级别3
    支持所有的 UCS 字符, 例如数学家可以在任意一个字符上加上一个 tilde(颚化符号,西班牙语字母上面的~)或一个箭头(或两者都加).

    什么是 Unicode?

    历史上, 有两个独立的, 创立单一字符集的尝试. 一个是国际标准化组织(ISO)的 ISO 10646 项目, 另一个是由(一开始大多是美国的)多语言软件制造商组成的协会组织的 Unicode 项目. 幸运的是, 1991年前后, 两个项目的参与者都认识到, 世界不需要两个不同的单一字符集. 它们合并双方的工作成果, 并为创立一个单一编码表而协同工作. 两个项目仍都存在并独立地公布各自的标准, 但 Unicode 协会和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO 10646 标准的码表兼容, 并紧密地共同调整任何未来的扩展.

    那么 Unicode 和 ISO 10646 不同在什么地方?

    Unicode 协会公布的 Unicode 标准 严密地包含了 ISO 10646-1 实现级别3的基本多语言面. 在两个标准里所有的字符都在相同的位置并且有相同的名字.

    Unicode 标准额外定义了许多与字符有关的语义符号学, 一般而言是对于实现高质量的印刷出版系统的更好的参考. Unicode 详细说明了绘制某些语言(比如阿拉伯语)表达形式的算法, 处理双向文字(比如拉丁与希伯来文混合文字)的算法和 排序与字符串比较 所需的算法, 以及其他许多东西.

    另一方面, ISO 10646 标准, 就象广为人知的 ISO 8859 标准一样, 只不过是一个简单的字符集表. 它指定了一些与标准有关的术语, 定义了一些编码的别名, 并包括了规范说明, 指定了怎样使用 UCS 连接其他 ISO 标准的实现, 比如 ISO 6429 和 ISO 2022. 还有一些与 ISO 紧密相关的, 比如 ISO 14651 是关于 UCS 字符串排序的.

    考虑到 Unicode 标准有一个易记的名字, 且在任何好的书店里的 Addison-Wesley 里有, 只花费 ISO 版本的一小部分, 且包括更多的辅助信息, 因而它成为使用广泛得多的参考也就不足为奇了. 然而, 一般认为, 用于打印 ISO 10646-1 标准的字体在某些方面的质量要高于用于打印 Unicode 2.0的. 专业字体设计者总是被建议说要两个标准都实现, 但一些提供的样例字形有显著的区别. ISO 10646-1 标准同样使用四种不同的风格变体来显示表意文字如中文, 日文和韩文 (CJK), 而 Unicode 2.0 的表里只有中文的变体. 这导致了普遍的认为 Unicode 对日本用户来说是不可接收的传说, 尽管是错误的.

    什么是 UTF-8?

    首 先 UCS 和 Unicode 只是分配整数给字符的编码表. 现在存在好几种将一串字符表示为一串字节的方法. 最显而易见的两种方法是将 Unicode 文本存储为 2 个 或 4 个字节序列的串. 这两种方法的正式名称分别为 UCS-2 和 UCS-4. 除非另外指定, 否则大多数的字节都是这样的(Bigendian convention). 将一个 ASCII 或 Latin-1 的文件转换成 UCS-2 只需简单地在每个 ASCII 字节前插入 0x00. 如果要转换成 UCS-4, 则必须在每个 ASCII 字节前插入三个 0x00.

    在 Unix 下使用 UCS-2 (或 UCS-4) 会导致非常严重的问题. 用这些编码的字符串会包含一些特殊的字符, 比如 '/0' 或 '/', 它们在 文件名和其他 C 库函数参数里都有特别的含义. 另外, 大多数使用 ASCII 文件的 UNIX 下的工具, 如果不进行重大修改是无法读取 16 位的字符的. 基于这些原因, 在文件名, 文本文件, 环境变量等地方, UCS-2 不适合作为 Unicode 的外部编码.

    在 ISO 10646-1 Annex RRFC 2279 里定义的 UTF-8 编码没有这些问题. 它是在 Unix 风格的操作系统下使用 Unicode 的明显的方法.

    UTF-8 有一下特性:

    • UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容). 这意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的.
    • 所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集. 因此, ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分.
    • 表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里, 并指出这个字符包含多少个字节. 多字节串的其余字节都在 0x80 到 0xBF 范围里. 这使得重新同步非常容易, 并使编码无国界, 且很少受丢失字节的影响.
    • 可以编入所有可能的 231个 UCS 代码
    • UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长.
    • Bigendian UCS-4 字节串的排列顺序是预定的.
    • 字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到.

    下列字节串用来表示一个字符. 用到哪个串取决于该字符在 Unicode 中的序号.

    U-00000000 - U-0000007F:
    0xxxxxxx

    U-00000080 - U-000007FF:
    110xxxxx 10xxxxxx

    U-00000800 - U-0000FFFF:
    1110xxxx 10xxxxxx 10xxxxxx

    U-00010000 - U-001FFFFF:
    11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

    U-00200000 - U-03FFFFFF:
    111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

    U-04000000 - U-7FFFFFFF:
    1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

    xxx 的位置由字符编码数的二进制表示的位填入. 越靠右的 x 具有越少的特殊意义. 只用最短的那个足够表达一个字符编码数的多字节串. 注意在多字节串中, 第一个字节的开头"1"的数目就是整个串中字节的数目.

    例如: Unicode 字符 U+00A9 = 1010 1001 (版权符号) 在 UTF-8 里的编码为:

    11000010 10101001 = 0xC2 0xA9

    而字符 U+2260 = 0010 0010 0110 0000 (不等于) 编码为:

    11100010 10001001 10100000 = 0xE2 0x89 0xA0

    这种编码的官方名字拼写为 UTF-8, 其中 UTF 代表 UCS Transformation Format. 请勿在任何文档中用其他名字 (比如 utf8 或 UTF_8) 来表示 UTF-8, 当然除非你指的是一个变量名而不是这种编码本身.

    什么编程语言支持 Unicode?

    在大约 1993 年之后开发的大多数现代编程语言都有一个特别的数据类型, 叫做 Unicode/ISO 10646-1 字符. 在 Ada95 中叫 Wide_Character, 在 Java 中叫 char.

    ISO C 也详细说明了处理多字节编码和宽字符 (wide characters) 的机制, 1994 年 9 月 Amendment 1 to ISO C 发表时又加入了更多. 这些机制主要是为各类东亚编码而设计的, 它们比处理 UCS 所需的要健壮得多. UTF-8 是 ISO C 标准调用多字节字符串的编码的一个例子, wchar_t 类型可以用来存放 Unicode 字符.

    在 Linux 下该如何使用 Unicode?

    在 UTF-8 之前, 不同地区的 Linux 用户使用各种各样的 ASCII 扩展. 最普遍的欧洲编码是 ISO 8859-1 和 ISO 8859-2, 希腊编码 ISO 8859-7, 俄国编码 KOI-8, 日本编码 EUC 和 Shift-JIS, 等等. 这使得 文件的交换非常困难, 且应用软件必须特别关心这些编码的不同之处.

    最终, Unicode 将取代所有这些编码, 主要通过 UTF-8 的形式. UTF-8 将应用在

    • 文本文件 (源代码, HTML 文件, email 消息, 等等)
    • 文件名
    • 标准输入与标准输出, 管道
    • 环境变量
    • 剪切与粘贴选择缓冲区
    • telnet, modem 和到终端模拟器的串口连接
    • 以及其他地方以前用ASCII来表示的字节串

    在 UTF-8 模式下, 终端模拟器, 比如 xterm 或 Linux console driver, 将每次按键转换成相应的 UTF-8 串, 然后发送到前台进程的 stdin 里. 类似的, 任何进程在 stdout 上的输出都将发送到终端模拟器, 在那里用一个 UTF-8 解码器进行处理, 之后再用一种 16 位的字体显示出来.

    只有在功能完善的多语言字处理器包里才可能有完全的 Unicode 功能支持. 而广泛用在 Linux 里用于取代 ASCII 和其他 8 位字符集的方案则要简单得多. 第一步, Linux 终端模拟器和命令行工具将只是转变到 UTF-8. 这意味着只用到 级别1 的 ISO 10646-1 实现 (没有组合字符), 且只支持那些不需要更多处理的语言象 拉丁, 希腊, 斯拉夫 和许多科学用符号. 在这个级别上, UCS 支持与 ISO 8859 支持类似, 唯一显著的区别是现在我们有几千种字符可以用了, 其中的字符可以用多字节串来表示.

    总有一天 Linux 会当然地支持组合字符, 但即便如此, 对于组合字符串, 预作字符(如何可用的话)仍将是首选的. 更正式地, 在 Linux 下用 Unicode 对文本编码的首选的方法应该是定义在 Unicode Technical Report #15 里的 Normalization Form C.

    在 今后的一个阶段, 人们可以考虑增加在日文和中文里用到的双字节字符的支持 (他们相对比较简单), 组合字符支持, 甚至也许对从右至左书写的语言如希伯来文 (他们可不是那么简单的) 的支持. 但对这些高级功能的支持不应该阻碍简单的平板 UTF-8 在 拉丁, 希腊, 斯拉夫和科学用符号方面的快速应用, 以取代大量的欧洲 8 位编码, 并提供一个象样的科学用符号集.

    我该怎样修改我的软件?

    有 两种途径可以支持 UTF-8, 我称之为软转换与硬转换. 软转换时, 各处的数据均保存为 UTF-8 形式, 因而需要修改的软件很少. 在硬转换时, 程序将读入的 UTF-8 数据转换成宽字符数组, 以在应用程序内部处理. 在输出时, 再把字符串转换回 UTF-8 形式.

    大多数应用程序只用软转换就可以工作得很好了. 这使得将 UTF-8 引入 Unix 成为切实可行的. 例如, 象 cat 和 echo 这样的程序根本不需要修改. 他们仍然可以对输入输出的是 ISO 8859-2 还是 UTF-8 一无所知, 因为它们只是搬运字节流而没有处理它们. 它们只能识别 ASCII 字符和象 '/n' 这样的控制码, 而这在 UTF-8 下也没有任何改变. 因此, 这些应用程序的 UTF-8 编码与解码将完全在终端模拟器里完成.

    而那些通过数字节数来获知字符数量的程序则需要一些小修改. 在 UTF-8 模式下, 它们必须不数入 0x80 到 0xBF 范围内的字节, 因为这些只是跟随字节, 它们本身并不是字符. 例如, ls 程序就必须要修改, 因为它通过数文件名中字符数来排放给用户的目录表格布局. 类似地, 所有的假定其输出为定宽字体, 并因此而格式化它们的程序, 必须学会怎样数 UTF-8 文本中的字符数. 编辑器的功能, 如删除单个字符, 必须要作轻微的修改, 以删除可能属于该字符的所有字节. 受影响有编辑器 (vi,emacs, 等等)以及使用 ncurses 库的程序.

    Linux 核心使用软转换也可以工作得很好, 只需要非常微小的修改以支持 UTF-8. 大多数处理字符串的核心功能 (例如: 文件名, 环境变量, 等等) 都不受影响. 下列地方也许必须修改:

    • 控制台显示与键盘驱动程序 (另一个 VT100 模拟器) 必须能编码和解码 UTF-8, 必须要起码支持 Unicode 字符集的几个子集. 从 Linux 1.2 起这些功能已经有了.
    • 外部文件系统驱动程序, 例如 VFAT 和 WinNT 必须转换文件名字符编码. UTF-8 已经加入可用的转换选项的列表里了, 因此 mount 命令必须告诉核心驱动程序用户进程希望看到 UTF-8 文件名. 既然 VFAT 和 WinNT 无论如何至少已经用了 Unicode了, 那么 UTF-8 在这里就可以发挥其优势, 以保证转换中无信息损失.
    • POSIX 系统的 tty 驱动程序支持一种 "cooked" 模式, 有一些原始的行编辑功能. 为了让字符删除功能工作正常, stty 必须在 tty 驱动程序里设置 UTF-8 模式, 因此它就不会把 0x80 到 0xBF 范围内的跟随字符也数进去了. Bruno Haible 那里已经有了一些 stty 和核心 tty 驱动 程序的 Linux 补丁 了.

    C 对 Unicode 和 UTF-8 的支持

    从 GNU glibc 2.1 开始, wchar_t 类型已经正式定为只存放独立于当前 locale 的, 32位的 ISO 10646 值. glibc 2.2 开始将完全支持 ISO C 中的多字节转换函数 (wprintf(),mbstowcs(),等等), 这些函数可以用于在 wchar_t 和包括 UTF-8 在内的任何依赖于 locale 的多字节编码间进行转换.

    例如, 你可以写

      wprintf(L"Sch鰊e Gre!/n");

    然后, 你的软件将按照你的用户在环境变量 LC_CTYPE (例如, en_US.UTF-8de_DE.ISO_8859-1) 中选择的 locale 所指定的编码来打印这段文字. 你的编译器必须运行在与该 C 源文件所用编码相应的 locale 中, 在目标文件中以上的宽字符串将改为 wchar_t 字符串存储. 在输出时, 运行时库将把 wchar_t 字符串转换回与程序执行时的 locale 相应的编码.

    注意, 类似这样的操作:

      char c = L"a"; 

    只允许从 U+0000 到 U+007F (7 位 ASCII) 范围里的字符. 对于非 ASCII 字符, 不能直接从 wchar_tchar 转换.

    现在, 象 readline() 这样的函数在 UTF-8 locale 下也能工作了.

    怎样激活 UTF-8 模式?

    如 果你的应用程序既支持 8 位字符集 (ISO 8859-*,KOI-8,等等), 也支持 UTF-8, 那么它必须通过某种方法以得知是否应使用 UTF-8 模式. 幸运的是, 在未来的几年里, 人们将只使用 UTF-8, 因此你可以将它作为默认, 但即使如此, 你还是得既支持传统 8 位字符集, 也支持 UTF-8.

    当前的应用程序使用许许多多的不同的命令行开关来激活它们各自的 UTF-8 模式, 例如:

    • xterm 命令行选项 "-u8" 和 X resource "XTerm*utf8:1"
    • gnat/gcc 命令行选项 "-gnatW8"
    • stty 命令行选项 "iutf8"
    • mined 命令行选项 "-U"
    • xemacs elisp 包裹 以在 UTF-8 和内部使用的 MULE 编码间转换
    • vim 'fileencoding' 选项
    • less 环境变量 LESSCHARSET=utf-8

    记住每一个应用程序的命令行选项或其他配置方法是非常单调乏味的, 因此急需某种标准方法.

    如果你在你的应用程序里使用硬转换, 并使用某种特定的 C 库函数来处理外部字符编码和内部使用的 wchar_t 编码的转换工作, 那么 C 库会帮你处理模式切换的问题. 你只需将环境变量 LC_CTYPE 设为正确的 locale, 例如, 如果你使用 UTF-8, 那就是en.UTF-8, 而如果是 Latin-1, 并需要英语的转换, 则设为 en.ISO_8859-1.

    然而, 大多数现存软件的维护者选择用软转换来代替, 而不使用 libc 的宽字符函数, 不仅因为它们还未得到广泛应用, 还因为这会使得软件进行大规模修改. 在这种情况下, 你的应用程序必须自己来获知何时使用 UTF-8 模式. 一种方式是做以下工作:

    按照环境变量 LC_ALL, LC_CTYPE, LANG 的顺序, 寻找第一个有值的变量. 如果该值包含 UTF-8 子串 (也许是小写或没有"-") 则默认为 UTF-8 模式 (仍然可以用命令行开关来重设), 因为这个值可靠又恰当地指示了 C 库应该使用一种 UTF-8 locale.

    提供一个命令行选项 (或者如果是 X 客户程序则用 X resource 的值) 将仍然是有用的, 可以用来重设由 LC_CTYPE 等环境变量指定的默认值.

    我怎样才能得到 UTF-8 版本的 xterm?

    XFree86 里带的 xterm 版本最近已经由 Thomas E. Dickey 加入了支持 UTF-8 的扩展. 使用方法是, 获取 xterm patch #119 (1999-10-16) 或更新版本, 用 "./configure --enable-wide-chars ; make" 来编译, 然后用命令行选项 -u8 来调用 xterm, 使它将输入输出转换为 UTF-8. 在 UTF-8 模式里使用一个 *-ISO10646-1 字体. 当你在 ISO 8859-1 模式里时也可以使用 *-ISO10646-1 字体, 因为 ISO 10646-1 字体与 ISO 8859-1 字体是完全向后兼容的.

    新的支持 UTF-8 的 xterm 版本, 以及一些 ISO 10646-1 字体, 将被收录入 XFree86 4.0 版里.

    xterm 支持组合字符吗?

    Xterm 当前只支持级别1的 ISO 10646-1, 就是说, 不提供组合字符的支持. 当前, 组合字符将被当作空格字符对待. xterm 将来的修订版很有可能加入某些简单的组合字符支持, 就是仅仅将那个有一个或多个组合字符的基字符加粗 (logical OR-ing). 对于在基线以下的和在小字符上方的重音符来说, 这样处理的结果还是可以接受的. 对于象泰国文字体那样使用特别设计的加粗字符的文字, 这样处理也能工作的很好. 然而, 对于某些字体里, 在较高的字符上方组合上的重音符, 特别是对于 "fixed" 字体族, 产生的结果就不完全令人满意了. 因此, 在可用的地方, 应该继续优先使用预作字符.

    xterm 支持半宽与全宽 CJK 字体吗?

    Xterm 当前只支持那种所有字形都等宽的 cell-spaced 的字体. 将来的修订版很有可能为 CJK 语言加入半宽与全宽字符支持, 类似于 kterm 提供的那种. 如果选择的普通字体是 X×Y 象素大小, 且宽字符模式是打开的, 那么 xterm 会试图装入另外的一个 2X×Y 象素大小的字体 (同样的 XLFD, 只是 AVERAGE_WIDTH 属性的值翻倍). 它会用这个字体来显示所有在 Unicode Technical Report #11 里被分配了East Asian Wide (W)East Asian FullWidth (F) 宽度属性的 Unicode 字符. 下面这个 C 函数用来测试一个 Unicode 字符是否是宽字符并需要用覆盖两个字符单元的字形来显示:

      /* This function tests, whether the ISO 10646/Unicode character code
    * ucs belongs into the East Asian Wide (W) or East Asian FullWidth
    * (F) category as defined in Unicode Technical Report #11. In this
    * case, the terminal emulator should represent the character using a
    * a glyph from a double-wide font that covers two normal (Latin)
    * character cells. */

    int iswide(int ucs)
    {
    if (ucs < 0x1100)
    return 0;

    return
    (ucs >= 0x1100 && ucs <= 0x115f) || /* Hangul Jamo */
    (ucs >= 0x2e80 && ucs <= 0xa4cf && (ucs & ~0x0011) != 0x300a &&
    ucs != 0x303f) || /* CJK ... Yi */
    (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
    (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
    (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
    (ucs >= 0xff00 && ucs <= 0xff5f) || /* Fullwidth Forms */
    (ucs >= 0xffe0 && ucs <= 0xffe6);
    }

    某些 C 库也提供了函数

      #include 
    int wcwidth(wchar_t wc);
    int wcswidth(const wchar_t *pwcs, size_t n);

    用来测定该宽字符 wc 或由 pwcs 指向的字符串中的 n 个宽字符码 (或者少于 n 个宽字符码, 如果在 n 个宽字符码之前遇到一个空宽字符的话) 所要求的列位置的数量. 这些函数定义在 Open Group 的 Single UNIX Specification 里. 一个拉丁/希腊/斯拉夫/等等的字符要求一个列位置, 一个 CJK 象形文字要求两个, 而一个组合字符要求零个.

    最终 xterm 是否会支持从右到左的书写?

    此 刻还没有给 xterm 增加从右到左功能的计划. 希伯来与阿拉伯用户因此不得不靠应用程序在将希伯来文与阿拉伯文字符串送到终端前按左方向翻转它们, 换句话说, 双向处理必须在应用程序里完成, 而不是在 xterm 里. 至少, 希伯来与阿拉伯文在预作字形的可用性的形式上, 以及提示表格上的支持, 比 ISO 8859 要有所改进. 现在还远没有决定 xterm 是否支持双向文字以及该怎样工作. ISO 6429 = ECMA-48Unicode bidi algorithm 都提供了可供选择的开始点. 也可以参考 ECMA Technical
    Report TR/53
    . Xterm 也不处理阿拉伯文, Hangul 或 印度文本的格式化算法, 而且现在还不太清楚在 VT100 模拟器里处理是否可行和值得, 或者应该留给应用软件去处理. 如果你打算在你的应用程序里支持双向文字输出, 看一下 FriBidi, Dov Grobgeld 的 Unicode 双向算法的自由实现.

    我在哪儿能找到 ISO 10646-1 X11 字体?

    在过去的几个月里出现了相当多的 X11 的 Unicode 字体, 并且还在快速增多.

    • Markus Kuhn 正和其他许多志愿者一起工作于手动将旧的 -misc-fixed-*-iso8859-1 字体扩展到覆盖所有的欧洲字符表 (拉丁, 希腊, 斯拉夫, 国际音标字母表. 数学与技术符号, 某些字体里甚至有亚美尼亚语, 乔治亚语, 片假名等). 更多信息请参考 Unicode fonts and tools for X11 页. 这些字体将与 XFree86 一起分发. 例如字体
        -misc-fixed-medium-r-semicondensed--13-120-75-75-c-60-iso10646-1

      (旧的 xterm 的 fixed 缺省字体的一个扩展, 包括超过 3000 个字符) 已经是 XFree86 3.9 snapshot 的一部分了.

    • Markus 也做好了 X11R6.4 distribution 里所有的 Adobe 和 B&H BDF 字体的 ISO 10646 版本. 这些字体已经包含了全部 Postscript 字体表 (大约 30 个额外的字符, 大部分也被 CP1252 MS-Windows 使用, 如 smart quotes, dashes 等), 在 ISO 8859-1 编码下是没有的. 它们在 ISO 10646-1 版本里是完全可用的.
    • XFree86 4.0 将携带一个集成的 TrueType 字体引擎, 这使得你的 X 应用程序可以将任何 Apple/Microsoft 字体用于 ISO 10646-1 编码.
    • 将 来的 XFree86 版本很有可能从分发版中去除大多数旧的 BDF 字体, 取而代之的是 ISO 10646-1 编码的版本. X 服务器则会增加一个自动编码转换器, 只有当旧的 8 位软件请求一个类似于 ISO 8859-* 编码的字体时, 才虚拟地从 ISO 10646-1 字体文件中创建一个这样的字体. 现代软件应该优先地直接使用 ISO 10646-1 字体编码.
    • ClearlyU (cu12) 是一个非常有用的 X11 的 12 点阵, 100 dpi 的 proportional ISO 10646-1 BDF 字体, 包含超过 3700 个字符, 由 Mark Leisher 提供 (样例图象).
    • Roman Czyborra 的 GNU Unicode font 项目工作于收集一个完整的与免费的 8×16/16×16 pixel Unicode 字体. 目前已经覆盖了 34000 个字符.
    • etl-unicode 是一个 ISO 10646-1 BDF 字体, 由 Primoz Peterlin 提供.

    Unicode X11 字体名字以 -ISO10646-1 结尾. 这个 X 逻辑字体描述器 (X Logical Font Descriptor, XLFD) 的 CHARSET_REGISTRY 和 CHARSET_ENCODING 域里的值已经为所有 Unicode 和 ISO 10646-1 的 16 位字体而正式地注册了. 每个 *-ISO10646-1 字体都包含了整个 Unicode 字符集里的某几个子集, 而用户必须弄清楚他们选择的字体覆盖哪几个他们需要的字符子集.

    *-ISO10646-1 字体通常也指定一个 DEFAULT_CHAR 值, 指向一个非 Unicode 字形, 用来表示所有在该字体里不可用的字符 (通常是一个虚线框, 一个 H 的大小, 位于 0x1F 或 0xFFFE). 这使得用户至少能知道这儿有一个不支持的字符. xterm 用的小的定宽字体比如 6x13 等, 将永远无法覆盖所有的 Unicode, 因为许多文字比如日本汉字只能用比欧洲用户广泛使用的大的象素尺寸才能表示. 欧洲使用的典型的 Unicode 字体将只包含大约 1000 到 3000 个字符的子集.

    我怎样才能找出一个 X 字体里有哪些字形?

    X 协议无法让一个应用程序方便地找出一个 cell-spaced 字体提供哪些字形, 它没有为字体提供这样的量度. 因此 Mark LeisherErik van de Poel (Netscape) 指定了一个新的 _XFREE86_GLYPH_RANGES BDF 属性, 告诉应用程序该 BDF 字体实现了哪个 Unicode 子集. Mark Leisher 提供了一些样例代码以产生并扫描这个属性, 而 Xmbdfed 3.9 以及更高版本将自动将其加入到由它产生的每个 BDF 文件里.

    与 UTF-8 终端模拟器相关的问题是什么?

    VT100 终端模拟器接受 ISO 2022 (=ECMA-35) ESC 序列, 用于在不同的字符集间切换.

    UTF-8 在 ISO 2022 的意义里是一个 "其他编码系统" (参考 ECMA 35 的 15.4 节). UTF-8 是在 ISO 2022 SS2/SS3/G0/G1/G2/G3 世界之外的, 因此如果你从 ISO 2022 切换到 UTF-8, 所有的 SS2/SS3/G0/G1/G2/G3 状态都变得没有意义了, 直到你离开 UTF-8 并切换回 ISO 2022. UTF-8 是一个没有国家的编码, 也就是一个自我终结的短字节序列完全决定了它代表什么字符, 独立于任何国家的切换. G0 与 G1 在 ISO 10646 里与在 ISO 8859-1 里相同, 而 G2/G3 在 ISO 10646 里不存在, 因为任何字符都有固定的位置, 因而不会发声切换. 在 UTF-8 模式下, 你的终端不会因为你偶然地装入一个二进制文件而切换入一种奇怪图形字符模式. 这使得一个终端在 UTF-8 模式下比在 ISO 2022 模式下要健壮得多, 而且因此可以有办法将终端锁在 UTF-8 模式里, 而不会偶然地回到 ISO 2022 世界里.

    ISO 2022 标准指定了一系列的 ESC % 序列, 以离开 ISO 2022 世界 (指定其他的编码系统, DOCS), 用于 UTF-8 的许多这样的序列已经注册进了 ISO 2375 International Register of Coded Character Sets:

    • ESC %G 从 ISO 2022 里激活一个未指定实现级别的 UTF-8 模式且允许再返回 ISO 2022.
    • ESC %@ 从 UTF-8 回到 ISO 2022, 条件是通过 ESC %G 进入的 UTF-8
    • ESC %/G 切换进 UTF-8 级别 1 且不返回.
    • ESC %/H 切换进 UTF-8 级别 2 且不返回.
    • ESC %/I 切换进 UTF-8 级别 3 且不返回.

    当一个终端模拟器在 UTF-8 模式时, 任何 ISO 2022 逃脱码序列例如用于切换 G2/G3 等的都被忽略. 一个在 UTF-8 模式下的终端模拟器唯一会执行的 ISO 2022 序列是 ESC %@ 以从 UTF-8 返回 ISO 2022 方案.

    UTF-8 仍然允许你使用象 CSI 这样的 C1 控制字符, 尽管 UTF-8 也使用 0x80-0x9F 范围里的字节. 重要的是必须理解在 UTF-8 模式下的终端模拟器必须在执行任何控制字符前对收到的字节流运用 UTF-8 解码器. C1 字符与其他任何大于 U+007F 的字符一样需先经过 UTF-8 解码.

    已经有哪些支持 UTF-8 的应用程序了?

    • YuditGaspar Sinai 的自由 X11 Unicode 编辑器
    • Mined 98Thomas Wolff 提供, 是一个可以处理 UTF-8 的文本编辑器.
    • less 版本 346 或更高, 支持 UTF-8
    • C-Kermit 7.0 在传输, 终端, 及文件字符集方面支持 UTF-8.
    • Sam 是 Plan9 的 UTF-8 编辑器, 类似于 vi, 也可用于 Linux 和 Win32. (Plan9 是第一个完全转向 UTF-8, 将其作为字符编码的操作系统.)
    • 9termMatty Farrow 提供, 是一个 Plan9 操作系统的 Unicode/UTF-8 终端模拟器的 Unix 移植.
    • Wily 是一个 Plan9 Acme 编辑器的 Unix 实现.
    • ucm-0.1Juliusz Chroboczek 的 Unicode 字符映射表, 一个小工具, 使你可以选中任何一个 Unicode 字符并粘贴进你的应用程序.

    有哪些用于改善 UTF-8 支持的补丁?

    Postscript 字形的名字与 UCS 代码是怎么关联的?

    参考 Adobe 的 Unicode and Glyph Names 指南.

    X11 的剪切与粘贴工作在 UTF-8 时是如何完成的?

    参考 Juliusz Chroboczek客户机间 Unicode 文本的交换 草案, 对 ICCCM 的一个扩充的建议, 用一个新的可用于属性类型(property type)和选中(selection)目标的原子 UTF8_STRING 来处理 UTF-8 的选中.

    现在有没有用于处理 Unicode 的免费的库?

    各种 X widget 对 Unicode 支持的现状如何?

    有什么关于这个话题的好的邮件列表?

    你确实应该订阅的是 unicode@unicode.org 邮件列表, 这是发现标准的作者和其他许多领袖的话语的最好办法. 订阅方法是, 用 "subscribe" 作为标题, "subscribe YOUR@EMAIL.ADDRESS unicode" 作为正文, 发一条消息到 unicode-request@unicode.org.

    也有一个专注与改进通常用于 GNU/Linux 系统上应用程序的 UTF-8 支持的邮件列表 linux-utf8@nl.linux.org. 订阅方法是, 以 "subscribe linux-utf8" 为内容, 发送消息到 majordomo@nl.linux.org. 你也可以浏览 linux-utf8 archive

    其他相关的还有 XFree86 组的 "字体" 与 "i18n" 列表, 但你必须成为一名正式的开发者才能订阅.

    更多参考

    -------------------------------------------------------------------------
    ASCII, DBCS, Unicode【下】

    Windows环境下Unicode编程总结 (1)

    UNICODE环境设置

    在安装Visual Studio时,在选择VC++时需要加入unicode选项,保证相关的库文件可以拷贝到system32下。

    UNICODE编译设置:

    C/C++, Preprocessor difinitions 去除_MBCS,加_UNICODE,UNICODE

    在ProjectSetting/link/output 中设置Entry为wWinMainCRTStartup

    反之为MBCS(ANSI)编译。

    Unicode :宽字节字符集

    1. 如何取得一个既包含单字节字符又包含双字节字符的字符串的字符个数?

    可以调用Microsoft Visual C++的运行期库包含函数_mbslen来操作多字节(既包括单字节也包括双字节)字符串。

    调用strlen函数,无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。

    2. 如何对DBCS(双字节字符集)字符串进行操作?

    函数 描述

    PTSTR CharNext ( LPCTSTR ); 返回字符串中下一个字符的地址

    PTSTR CharPrev ( LPCTSTR, LPCTSTR ); 返回字符串中上一个字符的地址

    BOOL IsDBCSLeadByte( BYTE ); 如果该字节是DBCS字符的第一个字节,则返回非0值

    3. 为什幺要使用Unicode

    (1) 可以很容易地在不同语言之间进行数据交换。

    (2) 使你能够分配支持所有语言的单个二进制.exe文件或DLL文件。

    (3) 提高应用程序的运行效率。

    Windows 2000是使用Unicode从头进行开发的,如果调用任何一个Windows函数并给它传递一个ANSI字符串,那幺系统首先要将字符串转换成Unicode,然后将Unicode字符串传递给操作系统。如果希望函数返回ANSI字符串,系统就会首先将Unicode字符串转换成ANSI字符串,然后将结果返回给你的应用程序。进行这些字符串的转换需要占用系统的时间和内存。通过从头开始用Unicode来开发应用程序,就能够使你的应用程序更加有效地运行。

    Windows CE 本身就是使用Unicode的一种操作系统,完全不支持ANSI Windows函数

    Windows 98 只支持ANSI,只能为ANSI开发应用程序。

    Microsoft公司将COM从16位Windows转换成Win32时,公司决定需要字符串的所有COM接口方法都只能接受Unicode字符串。

    4. 如何编写Unicode源代码?

    Microsoft公司为Unicode设计了WindowsAPI,这样,可以尽量减少代码的影响。实际上,可以编写单个源代码文件,以便使用或者不使用Unicode来对它进行编译。只需要定义两个宏(UNICODE_UNICODE),就可以修改然后重新编译该源文件。

    _UNICODE宏用于C运行期头文件,而UNICODE宏则用于Windows头文件。当编译源代码模块时,通常必须同时定义这两个宏。

    5. Windows定义的Unicode数据类型有哪些?

    数据类型 说明

    WCHAR Unicode字符

    PWSTR 指向Unicode字符串的指针

    PCWSTR 指向一个恒定的Unicode字符串的指针

    对应的ANSI数据类型为CHAR,LPSTR和LPCSTR。

    ANSI/Unicode通用数据类型为TCHAR,PTSTR,LPCTSTR。

    6. 如何对Unicode进行操作?

    字符集 特性 实例

    ANSI 操作函数以str开头 strcpy

    Unicode 操作函数以wcs开头 wcscpy

    MBCS 操作函数以_mbs开头 _mbscpy

    ANSI/Unicode 操作函数以_tcs开头 _tcscpy(C运行期库)

    ANSI/Unicode 操作函数以lstr开头 lstrcpy(Windows函数)

    所有新的和未过时的函数在Windows2000中都同时拥有ANSI和Unicode两个版本。ANSI版本函数结尾以A表示;Unicode版本函数结尾以W表示。Windows会如下定义:

    #ifdef UNICODE

    #define CreateWindowEx CreateWindowExW

    #else

    #define CreateWindowEx CreateWindowExA

    #endif // !UNICODE

    7. 如何表示Unicode字符串常量?

    字符集 实例

    ANSI “string”

    Unicode L“string”

    ANSI/Unicode T(“string”)或_TEXT(“string”)if( szError[0] == _TEXT(‘J’) ){ }

    8. 为什幺应当尽量使用操作系统函数?

    这将有助于稍稍提高应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程Explorer.exe所使用。由于这些函数使用得很多,因此,在应用程序运行时,它们可能已经被装入RAM。

    如:StrCat,StrChr,StrCmp和StrCpy等。

    9. 如何编写符合ANSIUnicode的应用程序?

    (1) 将文本串视为字符数组,而不是chars数组或字节数组。

    (2) 将通用数据类型(如TCHAR和PTSTR)用于文本字符和字符串。

    (3) 将显式数据类型(如BYTE和PBYTE)用于字节、字节指针和数据缓存。

    (4) 将TEXT宏用于原义字符和字符串。

    (5) 执行全局性替换(例如用PTSTR替换PSTR)。

    (6) 修改字符串运算问题。例如函数通常希望在字符中传递一个缓存的大小,而不是字节。这意味着不应该传递sizeof(szBuffer),而应该传递(sizeof(szBuffer)/sizeof(TCHAR)。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那幺请记住要按字节来分配内存。这就是说,应该调用

    malloc(nCharacters *sizeof(TCHAR)),而不是调用malloc(nCharacters)。

    10. 如何对字符串进行有选择的比较?

    通过调用CompareString来实现。

    标志 含义

    NORM_IGNORECASE 忽略字母的大小写

    NORM_IGNOREKANATYPE 不区分平假名与片假名字符

    NORM_IGNORENONSPACE 忽略无间隔字符

    NORM_IGNORESYMBOLS 忽略符号

    NORM_IGNOREWIDTH 不区分单字节字符与作为双字节字符的同一个字符

    SORT_STRINGSORT 将标点符号作为普通符号来处理

    11. 如何判断一个文本文件是ANSI还是Unicode

    判断如果文本文件的开头两个字节是0xFF和0xFE,那幺就是Unicode,否则是ANSI。

    12. 如何判断一段字符串是ANSI还是Unicode

    用IsTextUnicode进行判断。IsTextUnicode使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此 IsTextUnicode有可能返回不正确的结果。

    13. 如何在UnicodeANSI之间转换字符串?

    Windows函数MultiByteToWideChar用于将多字节字符串转换成宽字符串;函数WideCharToMultiByte将宽字符串转换成等价的多字节字符串。

    14. Unicode和DBCS之间的区别

    Unicode使用(特别在C程序设计语言环境里)“宽字符集”。「Unicode中的每个字符都是16位宽而不是8位宽。」在Unicode中,没有单单使用8位数值的意义存在。相比之下,在“双位组字符集”中我们仍然处理8位数值。有些位组自身定义字符,而某些位组则显示需要和另一个位组共同定义一个字符。

    处理DBCS字符串非常杂乱,但是处理Unicode文字则像处理有秩序的文字。您也许会高兴地知道前128个Unicode字符(16位代码从0x0000到0x007F)就是ASCII字符,而接下来的128个Unicode字符(代码从0x0080到0x00FF)是ISO 8859-1对ASCII的扩展。Unicode中不同部分的字符都同样基于现有的标准。这是为了便于转换。希腊字母表使用从0x0370到0x03FF的代码,斯拉夫语使用从0x0400到0x04FF的代码,美国使用从0x0530到0x058F的代码,希伯来语使用从0x0590到0x05FF的代码。中国、日本和韩国的象形文字(总称为CJK)占用了从0x3000到0x9FFF的代码。Unicode的最大好处是这里只有一个字符集,没有一点含糊。

    15.衍生标准

    Unicode是一个标准。UTF-8是其概念上的子集,UTF-8是具体的编码标准。而UNICODE是所有想达到世界统一编码标准的标准。UTF-8标准就是Unicode(ISO10646)标准的一种变形方式,

    UTF的全称是:Unicode/UCS Transformation Format,其实有两种UTF,一种是UTF-8,一种是UTF-16,

    不过UTF-16使用较少,其对应关系如下:

    在Unicode中编码为 0000 - 007F 的 UTF-8 中编码形式为: 0xxxxxxx

    在Unicode中编码为 0080 - 07FF 的 UTF-8 中编码形式为: 110xxxxx 10xxxxxx

    在Unicode中编码为 0000 - 007F 的 UTF-8 中编码形式为: 1110xxxx 10xxxxxx 10xxxxxx

    utf-8是unicode的一个新的编码标准,其实unicode有过好几个标准.我们知道一直以来使用的unicode字符内码都是16位,它实际上还不能把全世界的所有字符编在一个平面系统,比如中国的藏文等小语种,所以utf-8扩展到了32位,也就是说理论在utf-8中可容纳二的三十二次方个字符. UNICODE的思想就是想把所有的字符统一编码,实现一个统一的标准.big5、gb都是独立的字符集,这也叫做远东字符集,把它拿到德文版的WINDOWS上可能将会引起字符编码的冲突....早期的WINDOWS默认的字符集是ANSI.notepad中输入的汉字是本地编码,但在NT/2000内部是可以直接支持UNICODE的。notepad.exe在WIN95和98中都是ANSI字符,在NT中则是UNICODE.ANSI和UNICODE可以方便的实现对应映射,也就是转换 ASCII是8位范围内的字符集,对于范围之外的字符如汉字它是无法表达的。unicode是16位范围内的字符集,对于不同地区的字符分区分配,unicode是多个IT巨头共同制定的字符编码标准。如果在unicode环境下比如WINDOWS NT上,一个字符占两字节16位,而在ANSI环境下如WINDOWS98下一个字符占一个字节8位.Unicode字符是16位宽,最多允许65,535字符,数据类型被称为WCHAR。

    对于已有的ANSI字符,unicode简单的将其扩展为16位:比如ANSI"A"=0x43,则对应的UNICODE为

    "A"= 0x0043

    而ASCII用七存放128个字符,ASCII是一个真正的美国标准,所以它不能满足其他国家的需要,例如斯拉夫语的字母和汉字于是出现了Windows ANSI字符集,是一种扩展的ASCII码,用8位存放字符,低128位仍然存放原来的ASCII码,

    而高128位加入了希腊字母等

    if def UNICODE

    TCHAR = wchar

    else

    TCHAR = char

    你需要在ProjectSettingsC/C++Preprocesser definitions中添加UNICODE和_UNICODE

    UINCODE,_UNICODE都要定义。不定义_UNICODE的话,用SetText(HWND,LPCTSTR),将被解释为SetTextA(HWND,LPTSTR),这时API将把你给的Unicode字符串看作ANSI字符串,显示乱码。因为windows API是已经编译好存在于dll中的,由于不管UNICODE还是ANSI字符串,都被看作一段buffer,如"0B A3 00 35 24 3C 00 00"如果按ANSI读,因为ANSI字串是以''结束的,所以只能读到两字节"0B A3 ",如果按UNICODE读,将完整的读到''结束。

    由于UNICODE没有额外的指示位,所以系统必须知道你提供的字串是哪种格式。此外,UNICODE好象是ANSI C++规定的,_UNICODE是windows SDK提供的。如果不编写windows程序,可以只定义UNICODE。

    开发过程:

    围绕着文件读写、字符串处理展开。文件主要有两种:.txt和.ini文件

    1. 在unicode和非unicode环境下字符串做不同处理的,那么需要参考以上9,10两条,以适应不同环境得字符串处理要求。

    对文件读写也一样。只要调用相关接口函数时,参数中的字符串前都加上_TEXT等相关宏。如果写成的那个文件需要是unicode格式保存的,那么在创建文件时需要加入一个字节头。

    CFile file;

    WCHAR szwBuffer[128];

    WCHAR *pszUnicode = L"Unicode string "; // unicode string

    CHAR *pszAnsi = "Ansi string "; // ansi string

    WORD wSignature = 0xFEFF;

    file.Open(TEXT("Test.txt"), CFile::modeCreate|CFile::modeWrite);

    file.Write(&wSignature, 2);

    file.Write(pszUnicode, lstrlenW(pszUnicode) * sizeof(WCHAR));

    // explicitly use lstrlenW function

    MultiByteToWideChar(CP_ACP, 0, pszAnsi, -1, szwBuffer, 128);

    file.Write(szwBuffer, lstrlenW(szwBuffer) * sizeof(WCHAR));

    file.Close();

    //以上这段代码在unicode和非unicode环境下都有效。这里显式的指明用Unicode来进行操作。

    2. 在非unicode环境下,缺省调用的都是ANSI格式的字符串,此时TCHAR转换为CHAR类型的,除非显式定义WCHAR。所以在这个环境下,如果读取unicode文件,那么首先需要移动2个字节,然后读取得字符串需要用MultiByteToWideChar来转换,转换后字符串信息才代表unicode数据。

    3. 在unicode环境下,缺省调用得都是unicode格式得字符串,也就是宽字符,此时TCHAR转换为WCHAR,相关得API函数也都调用宽字符类型的函数。此时读取unicode文件也和上面一样,但是读取得数据是WCHAR的,如果要转换成ANSI格式,需要调用WideCharToMultiByte。如果读取ANSI的,则不用移动两个字节,直接读取然后视需要转换即可。

    某些语言(如韩语)必须在unicode环境下才能显示,这种情况下,在非unicode环境下开发,就算用字符串函数转换也不能达到显示文字的目的,因为此时调用得API函数是用ANSI的(虽然底层都是用UNICODE处理但是处理结果是按照程序员调用的API来显示的)。所以必须用unicode来开发。

    已标记关键词 清除标记
    ©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页