字符和编码

特别长远很久以前,有雷同众口,他们决定就此8只可以开合的结晶管来成成不同的状态,以代表世界上之万物。他们看到8单开关状态是好之,于是他们管这名”字节约”。

   
//========================================================================
    //TITLE:
    //    如何勾勒优雅的代码(2)——#define?const?还是enum?
    //AUTHOR:
    //    norains
    //DATE:
    //    Tuesday  21-July-2009
    //Environment:
    //    WINCE5.0 + VS2005
   
//========================================================================

再次后来,他们还要举行了一些好拍卖这些字节的机器,机器开动了,可以为此字节来成有过多态,状态开始变来变去。他们相这样是好的,于是它就立刻机器称为”计算机”。

   
#define,const,enum:这三啊发生哪里关联?一个凡宏定义,一个凡静态修饰符,最后一个还是枚举类型。是休是发生接触像养麦皮打浆糊——粘不顶同?如果我们将限缩小再压缩,让三者都止局限为“固定值”,那么复杂的关系就是知晓于纸上——至少,有共同点了。
   
   
在分解啊是“固定值”之前,我们先行来了解何为“奇数”。太多之极都有规,少用“奇数”,因为这将造成代码不可维护。听起来如要算命的释语般玄之以神秘兮兮,不可捉摸,但里面的语义却是这么概括。下面这点儿独代表码段,正好说明“奇数”之糟糕:
   

 

代码段1: switch(mode) { case 1: //TO Do someting. break; case 2: //TO Do
someting. break; case 3: //TO Do someting. break; } 代码段2:
switch(mode) { case SLEEP: //TO Do someting. break; case POWER_OFF:
//TO Do someting. break; case POWER_ON: //TO Do someting. break; }

开计算机只于美国因此。八个之字节一共可以做有256(2之8次方)种不同之状态。

 

她俩把其中的编号从0开始之32栽状态分别规定了突出的用处,一可是极、打印机遇上约定好之这些字节被传过来时,就设开片预定的动作。遇上00×10,
终端就换行,遇上0x07, 终端就向众人嘟嘟叫,遇上0x1b,
打印机就打印反白的字,或者极端就因故彩色显示字母。他们看到这样不行好,于是就将这些0x20之下的字节状态叫做”控制码”。

   
显而易见,代码段2的可读性比代码段1一旦大多了。在这简单个实例里,像“1”,“2”,“3”这种即使给奇数,而“SLEEP”,“POWER_OFF”,“POWER_ON”就是固定值。固定值的概念在C++中产生三种方法,分别就是本文要讨论的#define,const和enum。
   
    大名鼎鼎的《Effect C++》的撰稿人Scott
Meyers就曾经建议了,凡是用const能取代#define的地方,都应有为此const。这句话不无道理,也从一方面来说,#define和const事实上很多地方还能够互用。
   
    比如:

她俩以拿装有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127声泪俱下,这样计算机就得就此不同字节来储存英语的字了。大家张这么,都感觉甚好,于是大家还管此方案叫做 ANSI 的”Ascii”编码(American
Standard Code for Information
Interchange,美国消息相互换标准代码)。当时世界上装有的处理器都因此平等的ASCII方案来保存英文字。

const DWORD DEFAULT_VOLUME = 0xFFFF; //#define DEFAULT_VOLUME 0xFFFF
… m_dwVolume = DEFAULT_VOLUME;

后来,就比如打巴比伦塔同一,世界各地的都从头运用电脑,但是多国度为此底免是英文,他们的字母里发许多凡ASCII里没有底,为了可以在计算机保存他们的契,他们控制采取127哀号以后的空位来表示这些新的假名、符号,还进入了过多画表格时需要用生至之横线、竖线、交叉等相,一直把序号编到了最后一个状态255。从128暨255即时无异于页的字符集被称”扩展字符集”。从此以后,贪婪之人类还没有初的状态可以用了,美帝国主义可能没想到还有第三世界国家之众人为期望可以为此到电脑吧!

 

等于中华人们得到计算机时,已经没好使的字节状态来表示汉字,况且有6000几近个常因此汉字需要保留也。但是及时难休倒智慧之华全民,我们不客气地管那些127声泪俱下过后的奇异符号们直接取消掉,
规定:一个低于127之字符的义与原本平,但少独超127的字符连在一起时,就意味着一个中国字,前面的一个字节(他称之为高字节)从0xA1于是到0xF7,后面一个字节(低字节)从0xA1届0xFE,这样我们虽得整合出大概7000大多单简体汉字了。在这些编码里,我们还管数学符号、罗马希腊的假名、日文的假名们还编上了,连在
ASCII
里当就有数字、标点、字母都统统重新编了区区个字节长的编码,这即是时常说之”全角”字符,而原在127声泪俱下以下的那些不畏让”半竞”字符了。

    无论你是故const还是#define来定义DEFAULT_VOLUME,对于m_dwVolume =
DEFAULT_VOLUME这语词而言都没有本质性的成形。那么,是无是意味着,是因此#define还是用const,完全在当时之情绪了?答案当然是否认的,否则本文就改为了抒情散文了。
   
   
#define有只致命之弱点,不深受作用域限制。凡是在#define之后的代码,都得以一直以#define定义的数值。
   
   
我们常常会刻画这么一个函数,用以获取有设备的DWORD值。但这函数不是回BOOL类型来表示成败,而是采用另外一栽方式:当读取成功时,返回的凡现实和设施有关的数值;当黄时,返回的凡默认数值。听起来就函数功能来接触奇怪,也存疑在什么状态下才会采取这样设计,但心疼本文主题不是讨论该函数能干啊,或相应出现叫什么地点,我们要掌握出诸如此类一种植函数即可。
   
    我们聊假要即函数原型如下:

中原布衣看看这么特别对,于是便管这种汉字方案叫做 “GB2312″。GB2312 是本着 ASCII
的中文扩展。

DWORD GetDevDW(HANDLE hDev,DWORD dwError);

唯独中国的汉字太多矣,我们很快就即意识有多人的姓名没有章程于此间打出去,特别是一些老会烦别人的国度领导人。于是我们不得不连续把 GB2312 没有以的码位找出来用上。

 

后来要么不够用,于是干脆不再要求没有字节一定是127如泣如诉以后的内码,只要第一单字节是高于127即稳定表示马上是一个汉字的开始,不管后面和的是匪是扩张字符集里之内容。结果扩展之后的编码方案被号称
GBK 标准,GBK 包括了 GB2312
的具有内容,同时还要增了临近20000个新的方块字(包括繁体字)和符号。

    调用也坏粗略:

后来少数民族也使因此微机了,于是我们又扩充,又加以了几千独新的少数民族的字,GBK
扩成了
GB18030。从此后,中华民族的学问就是得于电脑时代中承受了。

DWORD dwVal = GetDevDW(hDev,ERROR_VALUE);

华夏之程序员们盼这同一密密麻麻汉字编码的正统是好之,于是通称他们叫做
“DBCS”(Double Byte Charecter Set
双字节字符集)。在DBCS系列正式里,最酷之性状是个别字节长的字字符和均等许节长的英文字符并存于同同效编码方案里,因此他们写的顺序为支持中文处理,必须要注意字串里之各一个字节的价,如果是价值是高于127底,那么就算看一个双字节字符集里之字符出现了。那时候是被过加持,会编程的计算机僧侣们还如每天念下面这个咒语数百全方位:

 

“一个字毕竟少独英文字符!一个汉字毕竟少个英文字符……”

   
在这例子中,如果dwVal的数值等于ERROR_VALUE,那么意味着调用GetDevDW失败;不等于ERROR_VALUE才意味着调用成功。
   
   
现在咱们发出三三两两独函数,分别用来取两个设施的音。在接入下去的事例中,我们运用#define来定义固定值:

 

void GetDev1Info() { …. #define ERROR_VALUE 0
GetDevDW(NULL,ERROR_VALUE); … } void GetDev2Info() { …. #define
ERROR_VALUE 2 GetDevDW(NULL,ERROR_VALUE); … }

为当时逐一国家都像华这么抓来同样效好之编码标准,结果相互之间孰也不了解谁的编码,谁呢非支持别人的编码,连大陆和台湾如此光相隔了150海里,使用正在平等种植语言的哥们儿地区,也分头下了不同之
DBCS
编码方案——当时底神州人数怀念被电脑显示汉字,就亟须作及一个”汉字系统”,专门就此来拍卖汉字的来得、输入的题材,但是雅台湾之无知封建人士形容的算命程序就算必加装另一样效仿支持
BIG5
编码的什么”倚天汉字系统”才得以据此,装错了字符系统,显示就会乱了仿照!这怎么惩罚?而且世界民族之林中还有那些一时之所以非达电脑的清苦百姓,他们之字又岂惩罚?

 

正是计算机的巴比伦塔命题啊!

   
看起整个似乎都异常好,难道不是嘛?只可惜,编译会有警示出现:’ERROR_VALUE’
: macro redefinition。
   
   
问题的来只在乎#define的数值没有作用域的定义。更为糟糕之是,在GetDev2Info函数被使的ERROR_VALUE并无是我们所企盼的2,而是于GetDev1Info中概念之0。噢,我的圣,再为未曾于当下再度不好之行了。
   
    为了彻底解决这个警示,我们可以以GetDev2Info函数举行有格外的工作:

刚刚于此刻,大天使加百列及时出现了——一个让 ISO
(国际标谁化组织)的国际团队决定下手解决这个题目。他们采取的不二法门好粗略:废了富有的地区性编码方案,重新作一个概括了地上有所知识、所有字母和记的编码!他们打算给它们”Universal
Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE”。

void GetDev2Info() { …. #ifdef ERROR_VALUE #undef ERROR_VALUE
#endif #define ERROR_VALUE 2 GetDevDW(NULL,ERROR_VALUE); … }

UNICODE
开始制订时,计算机的存储器容量极大地向上了,空间还为不成为问题了。于是
ISO
就径直确定须用单薄独字节,也就是是16员来统一意味着所有的字符,对于ascii里的那些“半比赛”字符,UNICODE
包持其原编码不变换,只是将其尺寸由原先的8各项扩展为16各项,而另外知识和语言的字符则遍重复联合编码。由于”半斗”英文符号只需要因此到低8位,所以其高8位永远是0,因此这种大气的方案在保留英文文本时见面多浪费一加倍的空中。

 

这儿,从原始社会里倒过来的程序员开始察觉一个飞之现象:他们之strlen函数靠不停止了,一个字不再是相当给个别只字符了,而是一个!是的,从
UNICODE
开始,无论是半角的英文字母,还是全角的字,它们还是统一的”一个字符”!同时,也都是联的”两独字节”,请留意”字符”和”字节约”两单术语的不等,“字节约”是一个8号的情理存贮单元,而“字符”则是一个学问有关的记号。在UNICODE
中,一个字符就是少数只字节。一个汉字毕竟少独英文字符的时代已经急匆匆过去了。

    问题解决了,警告没有了,但代码却丑陋了。
   
    还有另外一样种植方式,更改固定值的名:

陈年多字符集存在时时,那些做多语言软件的店受上了死怪累,他们以以不同的国度销售一律效仿软件,就只好在区域化软件时为加持那个双字节字符集咒语,不仅使处处小心不要将错,还要把软件中之文字以不同之字符集中转来转去。UNICODE
对于他们的话是一个异常好的一应俱全解决方案,于是由 Windows NT 开始,MS
趁机拿它们的操作系统改了相同遍,把具备的主导代码都转移成为了于是 UNICODE
方式工作的版,从此刻起,WINDOWS
系统终于任需要加装各种本土语言体系,就可以显得全世界上装有知识的字符了。

void GetDev1Info() { …. #define DEV1_ERROR_VALUE 0
GetDevDW(NULL,DEV1_ERROR_VALUE); … } void GetDev2Info() { ….
#define DEV2_ERROR_VALUE 2 GetDevDW(NULL,DEV2_ERROR_VALUE); … }

而,UNICODE
在制订时没有设想与外一样栽现有的编码方案保持相当,这令 GBK 与UNICODE
在汉字之内码编排及全是不一样的,没有一样种植简易的算术方法好把文件内容从UNICODE编码和另外一样栽编码进行转换,这种转移必须经查表来展开。

 

如前所述,UNICODE
是为此简单个字节来代表为一个字符,他一起可组合有65535见仁见智之字符,这大概都足以覆盖世界上享有知识之号。如果还不够呢绝非涉嫌,ISO已经准备了UCS-4方案,说简练了不畏是四独字节来表示一个字符,这样咱们尽管足以组合有21亿个例外的字符出来(最高位产生外用途),这大概可以据此到银河联邦成立那同样龙吧!

   
同样,问题化解了,警告没有了,并且,代码也不到底丑陋。遗留的绝无仅有问题是,如果类似函数很多来说,我们用绞尽脑汁去叫每个错误固定值选择一个唯一的讳。呃,这对于我们这些懒人而言,这并无算是一个吓工作。既然如此,为什么非用const呢?

 

void GetDev1Info() { … const DWORD ERROR_VALUE = 0;
GetDevDW(NULL,ERROR_VALUE); …. } void GetDev2Info() { … const DWORD
ERROR_VALUE = 2; GetDevDW(NULL,ERROR_VALUE); … }

UNICODE
到时,一起赶到之还有计算机网络的兴起,UNICODE
如何以网络上传也是一个必须考虑的题材,于是面向传输的不少 UTF(UCS
Transfer
Format)标准出现了,顾名思义,UTF8就是历次8只各传输数据,而UTF16便是每次16独号,只不过为了传输时之可靠性,从UNICODE到UTF时连无是直接的呼应,而是要了有算法和规则来换。

 

遭过网络编程加持的处理器僧侣们都知情,在网里传递信息时出一个挺要紧之问题,就是对于数据高低位的解读道,一些电脑是使用低先发送的方式,例如我们PC机采用的
INTEL
架构,而另外一对凡采用高位先发送的办法,在网络中交换数据经常,为了对双方对高低位的认是否是平的,采用了平栽死便利的章程,就是于文本流的初始经常为对方发送一个标志符——如果下的公文是高位在位,那便发送”FEFF”,反之,则发送”FFFE”。不信教你可为此二进制方式打开一个UTF-X格式的公文,看看开头两单字节是勿是这点儿只字节?

    没错,仅此而已。因为const
DWORD声明的凡一个部分变量,受限于作用域的受制,所以我们当GetDev1Info跟GetDev2Info都能够运用同一之固定值名称。
   
   
这个事例也许还不足以说服你用const替代#define,那么连下的事例你应有会扭转这无异价值观——或许就例你早已遇到了。
   
   
我们出个别个class,分别就此来控制汽车的重音和功放。这半只类似都亟需以峰文件被定义MAX_VOLUME以供使用者调用,但生倒霉的凡,重音和功放的MAX_VOLUME值是例外之。
   
    如果用#define,在头文件中我们可能这么形容:

 

/////////////////////////////////// //Bass.h #define MAX_VOLUME 15

谈到这里,我们再顺便说说一个分外有名的竟现象:当您于 windows
的记事本里新建一个文书,输入”联通”两个字下,保存,关闭,然后再打开,你见面发现及时片只字就一去不复返了,代的的凡几乎个乱码!呵呵,有人说这便是联通之所以拼不过移动的案由。

/////////////////////////////////// //Amplifier.h #define MAX_VOLUME
30

实在就是为GB2312编码和UTF8编码产生了编码冲撞的因。

 

由网上引来一段于UNICODE到UTF8的转换规则:

    当半个头文件并未以使时,一切还老顺利,不是嘛?
   
   
但假如我用同时控制正在简单单高低,那么我们不怕势必须要同时include这有限只公文,像这种调用大家应该不生疏吧:

 

#include “Bass.h” #include “Amplifier.h”

Unicode

 

UTF-8

    那么问题即使老大明显:严重的警戒或无法透过编译。
   
   
为了缓解者问题,我们或只能请有const。只不过,如果要略地宣称如下:

 

/////////////////////////////////// //Bass.h const DWORD MAX_VOLUME =
15;

0000 – 007F

/////////////////////////////////// //Amplifier.h const DWORD
MAX_VOLUME = 30;

0xxxxxxx

 

 

   
那么该出现的题材还是和用#define一样,没有外实质上之改动。这时候,我们不得不请出namespace了。

0080 – 07FF

/////////////////////////////////// //Bass.h namespace Bass { const
DWORD MAX_VOLUME = 15; };

110xxxxx 10xxxxxx

/////////////////////////////////// //Amplifier.h namespace Amplifier {
const DWORD MAX_VOLUME = 30; }

 

 

0800 – FFFF

    在尚未使用using来省略命名空间的情事下,我们好这样折腾代码:

1110xxxx 10xxxxxx 10xxxxxx

DWORD dwBass = Bass::MAX_VOLUME; DWORD dwAmplifier =
Amplifier::MAX_VOLUME;

 

 

 

   
在这个例子中,命名空间从至标明作用,标明当前之MAX_VOLUME属于哪种范围,也算是意外之获取。
   
    看到此间,也许有人会咨询,如果是namespace +
#define方式可以么?很遗憾,答案是坏。正使前所说,#define不叫压作用域,所以简简单单的namespace无法套住#define这不过猛兽。
   
   
至此,我们可这么下定论,在不涉到标准编译,并且只是用固定值的前提下,我们都当据此const来替#define。
   
    基于这个规格,以下的讨论我们尽管抛弃#define,只用const。
   
    我们再度回过头来看看文章最初的事例,将其包装为一个函数:

 

BOOL SwitchMode(DWORD mode) { … switch(mode) { case SLEEP: //TO Do
someting. break; case POWER_OFF: //TO Do someting. break; case
POWER_ON: //TO Do someting. break; } … }

 

 

诸如”汉”字的Unicode编码是6C49。6C49每当0800-FFFF之间,所以只要用3字节模板:1110xxxx
10xxxxxx 10xxxxxx。将6C49描绘成二进制是:0110 1100 0100
1001,将此于特流按三字节模板的旁方法分为0110 110001
001001,依次代替模板被的x,得到:1110-0110 10-110001 10-001001,即E6 B1
89,这就算是彼UTF8的编码。

    在代码的外远在定义了之类固定值:

假定当你新建一个文件文件时,记事本的编码默认是ANSI,
如果你在ANSI的编码输入汉字,那么他其实就是是GB系列的编码方式,在这种编码下,”联通”的内码是:

const DWORD SLEEP = 0x00; const DWORD POWER_OFF = 0x02; const DWORD
POWER_ON = 0x03;

c1 1100 0001

 

aa 1010 1010

    调用的当儿:

cd 1100 1101

SwitchMode(SLEEP); … SwitchMode(POWER_OFF); …

a8 1010 1000

 

留神到了吧?第一次之单字节、第三季只字节的起始部分的都是”110″和”10″,正好跟UTF8规则里的两字节模板是一致的,于是更打开记事本时,记事本就误认为这是一个UTF8编码的文本,让我们拿第一个字节的110跟次单字节的10失去丢,我们就算收获了”00001
101010″,再把诸位对旅,补及带领的0,就获了”0000 0000 0110
1010″,不好意思,这是UNICODE的006A,也尽管是微写的字母”j”,而自此的星星点点许节用UTF8解码之后是0368,这个字符什么呢非是。这就是是只有”联通”两独字之公文并未法于记事本里正常显示的因由。

    很好,很漂亮,难道不是也?
   
    但随即样子无法担保使用者不是这么调用代码:

若果使你于”联通”之后多输入几只字,其他的字之编码不见得又刚是110以及10始的字节,这样再打开时,记事本就非见面坚持即是一个utf8编码的公文,而会就此ANSI的法解读的,这时乱码又休出现了。

SwitchMode(0x100);

 

 

当数据库里,有n前缀的字串类型就是UNICODE类型,这种类型受到,固定用半个字节来代表一个字符,无论这字符是汉字还是英文字母,或是别的什么。

   
0x100请勿是咱怀念如果的数值,在SwitchMode函数也非会见针对拖欠数值来照应的拍卖,但偏偏就可编译器的正儿八经,它见面吃这代码没有其他警示没有其它不当顺利编译通过。
   
   
也许还有人口说,谁会那么傻,直接用0x100来赋值啊?这话确实没错,直接用0x100之几率确实太少了。
   
   
但咱无能为力否认,会出这般一栽可能:有另外一个函数,其中一个固定值为如下概念:

倘你要测试”abc汉字”这个串的长,在没有n前缀的数据类型里,这个字串是7独字符的长短,因为一个中国字相当给简单单字符。而在起n前缀的数据类型里,同样的测试串长的函数将会报告你是5个字符,因为一个字就是是一个字符。

const DWORD FILE_MODE = 0x100;

 

    而我辈一代冲昏了条,又或许喝醉了酒,将欠参数误用了:

SwitchMode(FILE_MODE);

 

   
对于编译器来说,无论是0x100尚是FILE_MODE,都未曾最多意义,所以马上病态代码很易通过编译器检测;而于人口而言,因为既采用了固定值,也无意觉得马上参数是副的。两者,无论是编译器,还是我们,都吃合理地哄了。
       
   
那么,我们发主意在编译的时光,如果该数值不是我们所想使之,编译器能给使用者提示警示甚至错误呢?
   
    一切都有或!不过,这时候我们无可知应用const,而得换用enum。
   
    首先用enum定义固定值:

enum Mode { SLEEP, POWER_OFF, POWER_ON, };

 

    函数的扬言如此更换:

BOOL SwitchMode(Mode mode)

 

    调用也是和事先一样:

SwitchMode(SLEEP); … SwitchMode(POWER_OFF); …

 

    唯一的两样就是,如果您如此调用:

SwitchMode(0x100); //这时候无法编译通过 SwitchMode(FILE_MODE);
//这时候无法编译通过

 

    那么编译器就见面坚决地出抱怨:cannot convert parameter 1 from
‘int’ to ‘Mode’。
   
   
很好,编译器已经当咱们的率先道防火墙,将我们所不欲之绝不关系的数值都排除在外。难道不是不行美好吗?
   
    当然,如果你想强制让编译器通过独特的数值也未是未容许:

SwitchMode(static_cast<Mode>(0x100));

 

   
虽然0x100无处在Mode的界定之内,但依旧还是通过了编译器的检测。对斯,我们决不艺术。只是,像这种太的异教徒的做法,有多少情况下会遇见也?
   
   
    最后的末尾,我们聊总结一下:
   
    1.只是是声称单一固定值,尽可能用const。
   
    2.要是平等组固定值,并且相互有关联合,则用enum。
   
    3.请勿干条件编译,只是概念固定值的场面下,尽可能不下#define。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图