C++字符串完全指引之一 —— Win32 字符编码

 

  1. 原著:Michael Dunn

     

    翻译:Chengjie Sun

     

     

    原文出处:CodeProject:The Complete Guide to C++ Strings, Part I

     

    引言

     

    毫无疑问,我们都看到过像 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

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

     

    一眼看上去,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"的编码。

     

    使用字符串处理函数

     

    我们都已经见过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。

  2. 1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte;
  3. 2.永远不要使用-操作进行后向遍历。
  4. 我们先来阐述规则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, '''' );
     
        // 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() 检查尾部的''

            _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:// ", ''

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值