zoukankan      html  css  js  c++  java
  • Programming Windows 第五版读书笔记 第二章 Unicode

    Programming windows 5th Edition Chapter 2 Unicode Introduction

    1. 首先要在这里说明的是,可能是本书成文的较早(1998年成文),所以,本章中对Unicode的讲解可能有点错误,因为之前我看过的Joel写的 Unicode的文章和这里的说法就不一样。比如本文说Unicode就是使用2个字节来表示一个字符的字符集,用2个字节表示一个字符,最多就只可能表 示65536个字符,这显然是有问题的。有可能,本文所讲的Unicode的东西在Windows里面就是这样的(比如至少在winnt中是这样的),但 是应该不能放之四海皆准。从我JAVA开发和其他语言平台开发的经验来看,我觉得Joel说的Unicode的观点更可靠一些。可以去参考以前发过的 Joel写的那篇Unicode的文章。

    2. OK,现在开始本章的读书笔记。Unicode扩展自ASCII字符集,Unicode使用16bit来表示一个字符,Windows NT从底层开始支持Unicode,所以WinNT所有的带字符串的API(带字符串参数,带字符串返回值等),都有最后带W的版本(也就是宽字符的版 本)。但是可惜的是,win9x只是一小部分支持Unicode,所以我们在MSDN中查询带字符串逻辑的API函数的时候,经常有一段针对win9x的 remark,告知这个函数在win9x中是否能够使用。此外,从C语言的发展历史来看,C语言由于先天被ANSI束缚(所以叫ANSI C嘛),所以是通过对宽字符集的支持来支持Unicode的,下面会看到。

    3. 美国标准的ASCII码,ASCII码用7位来表示一个字符,当初用6位还是7位还是8位来表示一个字符有很强的争议,因为6位太少,8位成本太高,最后决定用7位。所以ASCII码能表示128个字符。

    但是ASCII码的最大问题就是他是美国标准,不能满足其他国家和地区的需要。于是就有人开始动8bit来表示一个字符的脑筋。1981 年,IBM推出的PC中,就已经使用了一个256个字符的字符集。此时的IBM在后128个字符中定义了一些重音符号,一些块型和线状的图形字符。但是对 于微软来说,IBM定义的这后128个字符并不十分实用,因为windows本身就是一个图形界面的系统,所以微软不会在这宝贵的后128个字符中浪费字 符去表示什么块型,线型的图形字符。于是,微软做了Windows字符集,也称为ANSI字符集(因为遵循了ANSI草案和ISO标准)。ANSI草案和 ISO标准最终称为了ISO-8859-1-1987,也就是现在我们很熟悉的latin1字符集。在 附件1 中我们贴出了当初的windows字符集(ANSI字符集)的全貌,这个字符集是基于ISO-8859-1的。不要混淆的是windows字符集和 ISO-8859-1不是完全相同的,只是基于他而已,比如ISO-8859-1中没有控制字符DEL,而windows字符集中就有。

    4. OK,利用后128位,人们现在能使用256个字符了,对于使用英语为基础的国家来说,是够了,但是如果把这些国家的所有文字集中在一起做成一个字符集, 那256是远远不够的,比如带重音符号的字符,拉丁字符等。于是,MS-DOS在1987年引进了代码页的概念(codepage),windows也使 用这个概念。他的基本思路就是对于一个字符习惯(国家/地区),就有一个代码页,这样,对于单个国家或地区的人们来说,256个字符够了,针对不同的国家 和地区定制不同的codepage。但这样带来的问题就是,国家或地区内部的人们是没问题了,但是如果要在国家和地区之间交换文件的话,就乱套了,由于大 家的codepage不一样,也就是说,定义在128号开外的字符,大家都不一致,所以一个国家的文件拿给另外一个国家的电脑上,根本无法正常显示。唯一 可行的办法就是应用程序将代码页和文件一起保存来试图减少这种问题,但这里面还是会牵涉到codepage互相转换的问题。

    5. OK,从上面可以看到,人们在字符集方面已经做了不少工作来试图减少问题,包括7位的ASCII,扩展的ASCII,代码页等等。但是事情还远没有结束, 比如中国,日本这样的国家,他们的字符可远不止256个,他们大概需要21000个字符。如何容纳这些语言而且还要和ASCII兼容呢?

    于是,DBCS(double-byte character set)双字节字符集出现了。在DBCS中,前128个字符仍然保持和ASCII兼容,从128开始,定义的字符代码需要和后一个字节拼在一起,来表示一 个字符,这样一来,我们就在128的基础上多出了128*256=32768个字符(后面的一个字节能表示256个可能)。Windows支持四个不同的 双字节字符集:代码页932(日文)、936(简体中文)、949(韩语)和950(繁体汉字)。只有为这些国家生产的windows才支持DBCS。可 以看到,windows这里仍然使用代码页的概念来表示扩展出来的字符。

    6. Unicode. Unicode很简单,就是用2个字节来表示一个字符,这样就可以表示65536个字符。本书中说到65536个字符用来容纳世界上所有的字符和符号,足够了(其实我觉得不是这样)。

    明白Unicode和DBCS之间的区别很重要。Unicode使用宽字符集(特别是在C语言环境里),Unicode中每个字符都是16位而不 是8位宽,也就是说,在Unicode中,不处理任何8位的东西。而DBCS不一样,DBCS处理8位的东西,前128个字符就是8位来表示的,后面的字 符也需要将后一个8位字节和前面一个字节的4个字节联合起来解释。这就是区别。

    处理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的代码(我算了一下,这一段是28671个字符)。

    7. 到这里可能有人会问,用代码页,DBCS的方式不是已经能解决问题了么?为什么还要发明Unicode呢?我是这样理解的,用DBCS,codepage 确实能解决问题,但不同的代码页,DBCS表示了不同地区的字符,也就是说,没有一个统一的字符集能表示所有的字符,这样当两个代码页所处的人们交换信息 的时候,就会存在问题。比如中国人写的程序(codepage 936)拿到日本人那边,程序的文字就错的一塌糊涂或根本无法显示了,因为日本人的电脑上可能根本就没安装codepage 936。而Unicode不一样,Unicode这一个字符集中就包含了世界上所有的字符,所以,基于Unicode字符集的程序放到什么地方都可以正常 运行,前提条件就是大家都安装Unicode字符集就OK了。

    Unicode的缺点就是占用内存和文件的空间较大。

    8. ANSI C通过支持宽字符集来支持Unicode(ANSI/ISO 9899-1990),在windows中,我们可以把宽字符集和Unicode作为同义语。下面就开始讲解windows中的Unicode和 ASCII了哦!里面会牵涉到很多我们经常看到的windows中的数据类型,读完这部分就再也不会对windows中那么多的数据类型(尤其是字符,字 符串类型)头晕了哦,相当棒!所以请仔细阅读下面的内容!

    9. 在ASCII的C中,我们这样定义一个字符和字符串,char c='A', char a[10]="Hello!", char *p = "Hello!",用sizeof返回字符的个数。事实上,sizeof返回字节数,由于ASCII中一个字符就是一个字节,所以我们说sizeof返回 字符数。

    OK,现在针对宽字符,C是这样定义的:

    typedef unsigned short wchar_t; // 无符号short,16个bit

    定义一个宽字符:wchar_t c='A',这里c中存储的是0x0041,就是Unicode中的A(然而,因为Intel微处理器从最小的字节开始储存多字节数值,该字节实际上是以 0x41、0x00的顺序保存在内存中。如果检查Unicode文字的计算机储存应注意这一点。)

    定义一个宽字符串:wchar_t *p = L"Hello!", 注意紧接在第一个引号前面的大写字母L(代表「long」)。这将告诉编译器该字符串按宽字符保存-即每个字符占用2个字节。通常,指针变量p要占用4个 字节,而字符串变量需要14个字节-每个字符需要2个字节,末尾的0还需要2个字节。虽然占用的字节数翻番了,但是p++这样在字符串中移动指针,还是能 正确取出每个字符的,因为类型被定义成了wchar_t,每次指针加加会移动两个字节(其实是废话,C中的指针++运算符都会根据指针指向的数据类型来决 定移动多少字节 :) )。

    10. 目前为止,一切看起来都非常的好。但在使用字符串处理函数的时候,问题来了。比如:char *pc="Hello"; int iLength=strlen(pc); 此时工作正常,但是:wchar_t *pw="Hello"; int iLength=strlen(pw); ,此时,在编译的时候编译器就会提醒我们,在strlen函数中,我们试图把unsigned short强转成const char *,运行之后更糟,strlen返回1,为什么会这样呢?很简单,因为Hello的Unicode是0x0048 0x0065 0x006C 0x006C 0x006F 0x0021,而在内存里,Hello字符串被存成了:

    48 00 65 00 6C 00 6C 00 6F 00 21 00

    所以很自然strlen返回1了。

    看到这里,我们可能已经感觉到了--为了支持宽字符,我们需要重写ANSI C库中所有和字符串有关系的函数,没错!不过不需要我们写,这个工作已经完成了,比如strlen的宽字符集版就是wcslen,printf的宽字符版 是wprintf,这些函数在WCHAR.H和标准函数表中有说明。

    11. 维护单一代码。从这里开始慢慢靠近windows的逻辑了。Unicode的缺点就是占用的空间较大,所以很自然的想法是,能不能写一份代码,然后这份代 码中设计的字符串能按照要求,要么变成ASCII,要么使用Unicode呢?这样,起码在美国,程序就可以使用ASCII,体积就小多了。

    微软就是这么做的。首先,我们不谈windows中的做法,微软在Visual C++中(也就是微软的C库),就加入了TCHAR.H头文件,该头文件中,定义了很多新的数据类型和函数原型,他们的特点就是都是带T的。其中的函数一 律以 _ 打头,因为这些函数不是ANSI C标准中的东西,为了避免和ANSI C中的函数冲突,所以一律以 _ 打头(如_tprintf, _tcslen)。微软的TCHAR.H中通过条件编译,来实现ASCII和Unicode自动切换的逻辑。这个条件编译的变量就是_UNICODE(后 面会讲到一个条件编译变量UNICODE,不带_的哦,这是winnt.h中定义的,是windows中的一套东西,不要和这里混淆了,这里就是微软C语 言的library)。比如:

    #ifdef _UNICODE

    #define _tcslen wcslen
    typedef wchar_t TCHAR;
    #define __T(x) L##x

    #else

    #define _tcslen strlen
    typedef char TCHAR;
    #define __T(x) x

    看懂了吧?很简单哦,呵呵。通过这样的做法,我们通过定义/不定义 _UNICODE 这个条件编译变量,就可以让我们的字符串相应的变成宽字符/8位ASCII字符了。前提是我们代码里所有的字符和字符串都声明成TCHAR这样的数据类型哦。

    上面唯一需要说明的是__T(x)这个宏,用L##x是什么意思呢,这里两个#号就表示将L附到参数x的前面,所以,使用这个宏就等于将字符串Hello定义成了L"Hello",也就是宽字符版本的常量字符串了。

    此外,还有两个宏和__T定义相同:

    #define _T(x) __T(x)
    #define _TEXT(x) __T(x)

    12. Windows和宽字符。Windows中针对宽字符的处理和前面看到的TCHAR.H中的做法如出一辙。在第一章中说到了,windows对 Unicode支持体现在WINNT.H这个头文件中,WINNT.H中include了CTYPE.H,这是C的众多头文件之一,包括了wchar_t 的定义。首先,WINNT.H中针对宽字符和非宽字符分别做了单独定义,出来了一堆名称不同,但其实都是一码事的数据类型(真搞不懂微软为什么喜欢搞这么 多数据类型出来,光是名字不一样,可能是不同的数据类型名字给不同的开发组用吧,这样省的将来各部门之间打架):

    typedef char CHAR;
    typedef wchar_t WCHAR;

    typedef CHAR * PCHAR, * LPCH, * PCH, * NPSTR, * LPSTR, * PSTR;
    typedef CONST CHAR * LPCCH, * PCCH, * LPCSTR, * PCSTR;

    typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NWPSTR, * LPWSTR, * PWSTR;
    typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR;

    仔细看,有一点规律。CHAR和WCHAR是非宽和宽字符。然后是一堆字符串指针。首先是以N和L打头的指针,他们表示near和long型的指 针,near占用2个字节,long占用4个字节,不过这都是16位windows下的产物,在32为windows下,他们都占用4个字节,所以我们可 以用那些不带N也不带L的指针,他们是32位windows下的产物。其实在32位windows下,这些指针占用的空间都是4个字节。然后看宽字符的字 符串指针名字中都带有W,const类型的不管宽字符还是非宽字符指针,名字中都带有C。

    OK,这是分开定义的,然后和TCHAR.H中一样,WINNT.H中也定义了一种中间数据类型,能根据条件编译转换成ASCII或Unicode,如下:

    #ifdef UNICODE
    typedef WCHAR TCHAR, * PTCHAR ;
    typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR ;
    typedef LPCWSTR LPCTSTR ;
    #else
    typedef char TCHAR, * PTCHAR ;
    typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR ;
    typedef LPCSTR LPCTSTR ;
    #endif

    看到了吧?通过定义UNICODE这个条件编译变量,注意和TCHAR.H中不同,这个变量前面没有下划线。定义出了TCHAR,LPCTSTR等东西。和TCHAR.H中的__T, _T宏一样,WINNT.H中也定义了这样的宏:

    #ifdef UNICODE
    #define __TEXT(quote) L##quote
    #else
    #define __TEXT(quote) quote
    #endif

    #define TEXT(quote) __TEXT(quote)

    至此,看到了吧?知道为什么我们在第一章的HelloMsg中,需要用TEXT宏将字符串包起来了吧?因为这样能通过对UNICODE条件编译变量的控制,从而控制字符串变成ASCII的,或宽字符的。

    13. Windows函数呼叫。上面定义了一堆数据类型,然后就是函数了。像WINNT这样底层支持UNICODE的windows,每个和字符串相关的API都有三个函数,我们以MessageBox来说吧,首先MessageBox本身定义为:

    int WINAPI MessageBox (HWND, LPCSTR, LPCSTR, UINT);

    看到了吧?字符串参数都是定义成LPCSTR的,带T的,也就是能自适应的字符串类型。

    然后还会定义两个附加函数,一个最后带A,表示处理ASCII字符串,一个最后带W,表示处理Unicode字符串:

    WINUSERAPI int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
    WINUSERAPI int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);

    #ifdef UNICODE
    #define MessageBox MessageBoxW
    #else
    #define MessageBox MessageBoxA
    #endif

    这种特性上一篇我在研究OutputDebugString函数的时候已经谈到了。

    14. Windows中的字符串函数。前面看到了,ANSI C中已经针对宽字符和非宽字符定义了不同的字符串处理函数,就如strlen和wcslen。Microsoft C完全包括了ANSI C中的这些字符串函数。此外,微软还另外做了一部分,这些字符串函数接受 T 类型的字符串参数,从而能根据用户是否定义了Unicode条件编译变量,来决定最后的字符串是宽字符还是非宽字符。这些函数有:

    ILength = lstrlen (pString);
    pString = lstrcpy (pString1, pString2);
    pString = lstrcpyn (pString1, pString2, iCount);
    pString = lstrcat (pString1, pString2);
    iComp = lstrcmp (pString1, pString2);
    iComp = lstrcmpi (pString1, pString2);

    同样的,上述这些函数也有A和W两个版本,就像MessageBoxA, MessageBoxW一样。可以在MSDN中查询这些函数,从这些函数的文档就可以索引到MSDN中有关String的完整文档内容。

    15. Printf, sprintf和windows。这里讲述printf系列函数在windows中的体现。首先,windows中是没有printf这个函数的,只有 fprintf,但是sprintf系列函数是有的。很多C语言的开发者在进行windows编程的时候,更愿意使用C中的内存函数(如malloc)或 文件I/O函数,但是windows中不推荐我们这样做,windows针对这些函数做了一堆等效的函数,我们应当尽量使用这样的函数。如malloc在 windows中我们会用GlobalAlloc来代替。

    下面列出了windows中的printf, sprintf, vsprintf这些系列函数的对应表:

    附件2

    上表中标准版,最大长度版是Microsoft C Library中的函数,windows版的函数是只能用在windows下的。简单来说,上述的函数在windows下都是能用的(只要我们使用的是微 软的编译器)。但是,在这种表中仔细看能发现,windows版本的函数是没有带n的系列函数的(也就是能指定操作buffer字节数的那种安全的能防止 缓冲区溢出的函数)!

    16. 文章最后给出了一个支持printf输入格式的MessageBox改造函数--MessageBoxPrintf,代码在这:

    Code: Select all
    /*------------------------------------------------------------
        ScrnSize.cpp -- Display screen size in a message box
        Eric Zhang 2007
    --------------------------------------------------------------*/

    #include <windows.h>
    #include <tchar.h>
    #include <stdio.h>

    /*
    * Pay attention to this function
    * The MessageBox's window handle(First argument) is always NULL
    */
    int CDECL MessageBoxPrintf(TCHAR *szCaption, TCHAR *szFormat, ...)
    {
       TCHAR szBuffer[1024];
       va_list pArgList;

       // The va_start macro (defined in STDARG.H) is usally equivalent to:
       // pArgList = (char *) &szFormat + sizeof(szFormat);
       va_start(pArgList, szFormat);

       // Fill the arguments into the string
       _vsntprintf(szBuffer, sizeof(szBuffer)/sizeof(TCHAR), szFormat, pArgList);

       // The va_end macro just zeroes out pArgList for no good reason
            va_end(pArgList);

       return MessageBox(NULL, szBuffer, szCaption, 0);
    }

    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR szCmdLine, int iCmdShow)
    {
       int cxScreen, cyScreen;

       cxScreen = GetSystemMetrics(SM_CXSCREEN);
       cyScreen = GetSystemMetrics(SM_CYSCREEN);
       MessageBoxPrintf(TEXT("ScrnSize"),
                       TEXT("The screen is %i pixels wide by %i pixels high."),
                    cxScreen, cyScreen);

       return 0;
    }


    上面的代码很好理解,这里说几个重要的点:

    1. 上面代码中对v系列函数的注释让我们了解到了为什么v系列的字符串格式化函数能处理不定数量的参数。其实很简单,va_start宏将一个指针移动到了参 数szFormat的后面,然后挨个往后就能取出所有的参数了。然后将这些参数依次填到szFormat这个字符串中就OK了。

    2. 代码中调用了_vsntprintf函数,这个函数是安全的,能防止缓冲区溢出的函数,上面说了,windows中没有带n系列的函数,所以只能用这个(也就是Microsoft C Library中的)。所以我们需要在开头include tchar.h

    3. MessageBoxPrintf函数使MessageBox具备了接受printf格式输入字符串的功能,但是上面代码中,MessageBox的第一个参数始终是NULL,将来可以考虑改造MessageBoxPrintf,加入一个参数,用来传入窗口句柄。
  • 相关阅读:
    非foreach情况下取某一节点的position()
    MorningSale 介绍
    XSLT中的内容自动换列
    十年相伴一首歌
    Navicat的强大自动定时备份
    Xshell6评估期已过的解决方法
    Ext JS继承: 关于object, constructor和prototype
    WCF+Silverlight 异常处理
    Adding/removing fields and columns drag & drop bug's fix
    WCF部署至IIS问题二则
  • 原文地址:https://www.cnblogs.com/super119/p/2011324.html
Copyright © 2011-2022 走看看