前篇已经说明编程时,拿到算法后该干的第一件事就是把资源映射成数字,而前面也说过“类型就是人为制订的如何解释内存中的二进制数的协议”,也就是说一个数字对应着一块内存(可能4字节,也可能20字节),而这个数字的类型则是附加信息,以告诉编译器当发现有对那块内存的操作语句(即某种操作符)时,要如何编写机器指令以实现那个操作。比如两个char类型的数字进行加法操作符操作,编译器编译出来的机器指令就和两个long类型的数字进行加法操作的不一样,也就是所谓的“如何解释内存中的二进制数的协议”。由于解释协议的不同,导致每个类型必须有一个唯一的标识符以示区别,这正好可以提供强烈的语义。
typedef
提供语义就是要尽可能地在代码上体现出这句或这段代码在人类世界中的意义,比如前篇定义的过河方案,使用一char类型来表示,然后定义了一数组char sln[5]以期从变量名上体现出这是方案。但很明显,看代码的人不一定就能看出sln是solution的缩写并进而了解这个变量的意义。但更重要的是这里有点本末倒置,就好像这个东西是红苹果,然后知道这个东西是苹果,但它也可能是玩具、CD或其它,即需要体现的语义是应该由类型来体现的,而不是变量名。即char无法体现需要的语义。
对此,C++提供了很有意义的一个语句——类型定义语句。其格式为typedef <源类型名> <标识符>;。其中的<源类型名>表示已存在的类型名称,如char、unsigned long等。而<标识符>就是程序员随便起的一个名字,符合标识符规则,用以体现语义。对于上面的过河方案,则可以如下:
typedef char Solution; Solution sln[5];
上面其实是给类型char起了一个别名Solution,然后使用Solution来定义sln以更好地体现语义来增加代码的可读性。而前篇将两岸的人数分布映射成char[4],为了增强语义,则可以如下:
typedef char PersonLayout[4]; PersonLayout oldLayout[200];
注意上面是typedef char PersonLayout[4];而不是typedef char[4] PersonLayout;,因为数组修饰符“[]”是接在被定义或被声明的标识符的后面的,而指针修饰符“*”是接在前面的,所以可以typedef char *ABC[4];但不能typedef char [4]ABC*;,因为类型修饰符在定义或声明语句中是有固定位置的。
上面就比char oldLayout[200][4];有更好的语义体现,不过由于为了体现语义而将类型名或变量名增长,是否会降低编程速度?如果编多了,将会发现编程的大量时间不是花在敲代码上,而是调试上。因此不要忌讳书写长的变量名或类型名,比如在Win32的Security SDK中,就提供了下面的一个函数名:
BOOL ConvertSecurityDescriptorToStringSecurityDescriptor(…);
很明显,此函数用于将安全描述符这种类型转换成文字形式以方便人们查看安全描述符中的信息。
应注意typedef不仅仅只是给类型起了个别名,还创建了一个原类型。当书写char* a, b;时,a的类型为char*,b为char,而不是想象的char*.因为“*”在这里是类型修饰符,其是独立于声明或定义的标识符的,否则对于char a[4], b;,难道说b是char[4]?那严重不符合人们的习惯。上面的char就被称作原类型。为了让char*为原类型,则可以:typedef char *PCHAR; PCHAR a, b, *c[4];。其中的a和b都是char*,而c是char**[4],所以这样也就没有问题:char **pA = &a;。
结构
再次考虑前篇为什么要将人数布局映射成char[4],因为一个人数可以用一个char就表示,而人数布局有四个人数,所以使用char[4].即使用char[4]是希望只定义一个变量就代表了一个人数分布,编译器就一次性在栈上分配4个字节的空间,并且每个字节都各自代表一个人数。所以为了表现河岸左侧的商人数,就必须写a[0],而左侧的仆人数就必须a[1].坏处很明显,从a[0]无法看出它表示的是左岸的商人数,即这个映射意义(左岸的商人数映射为内存块中第一个字节的内容以补码格式解释)无法从代码上体现出来,降低了代码的可读性。
上面其实是对内存布局的需要,即内存块中的各字节二进制数如何解释。为此,C++提出了类型定义符“{}”。它就是一对大括号,专用在定义或声明语句中,以定义出一种类型,称作自定义类型。即C++原始缺省提供的类型不能满足要求时,可自定义内存布局。其格式为:<类型关键字> <名字> { <声明语句> …}.<类型关键字>只有三个:struct、class和union.而所谓的结构就是在<类型关键字>为struct时用类型定义符定义的原类型,它的类型名为<名字>,其表示后面大括号中写的多条声明语句,所定义的变量之间是串行关系(后面说明),如下:
struct ABC { long a, *b; double c[2], d; } a, *b = &a;
上面是一个变量定义语句,对于a,表示要求编译器在栈上分配一块4+4+8*2+8=32字节长的连续内存块,然后将首地址和a绑定,其类型为结构型的自定义类型(简称结构)ABC.对于b,要求编译器分配一块4字节长的内存块,将首地址和b绑定,其类型为结构ABC的指针。
上面定义变量a和b时,在定义语句中通过书写类型定义符“{}”定义了结构ABC,则以后就可以如下使用类型名ABC来定义变量,而无需每次都那样,即:
ABC &c = a, d[2];
现在来具体看清上面的意思。首先,前面语句定义了6个映射元素,其中a和b分别映射着两个内存地址。而大括号中的四个变量声明也生成了四个变量,各自的名字分别为ABC::a、ABC::b、ABC::c、ABC::d;各自映射的是0、4、8和24;各自的类型分别为long ABC::、long* ABC::、double (ABC::) [2]、double ABC::,表示是偏移。其中的ABC::表示一种层次关系,表示“ABC的”,即ABC::a表示结构ABC中定义的变量a.应注意,由于C++是强类型语言,它将ABC::也定义为类型修饰符,进而导致出现long* ABC::这样的类型,表示它所修饰的标识符是自定义类型ABC的成员,称作偏移类型,而这种类型的数字不能被单独使用(后面说明)。由于这里出现的类型不是函数,故其映射的不是内存的地址,而是一偏移值(下篇说明)。与之前不同了,类型为偏移类型的(即如上的类型)数字是不能计算的,因为偏移是一相对概念,没有给出基准是无法产生任何意义的,即不能:ABC::a; ABC::c[1];。其中后者更是严重的错误,因为数组操作符“[]”要求前面接的是数组或指针类型,而这里的ABC::c是double的数组类型的结构ABC中的偏移,并不是数组类型。
注意上面的偏移0、4、8、24正好等同于a、b、c、d顺次安放在内存中所形成的偏移,这也正是struct这个关键字的修饰作用,也就是前面所谓的各定义的变量之间是串行关系。
为什么要给偏移制订映射?即为什么将a映射成偏移0字节,b映射成偏移4字节?因为可以给偏移添加语义。前面的“左岸的商人数映射为内存块中第一个字节的内容以补码格式解释”其实就是给定内存块的首地址偏移0字节。而现在给出一个标识符和其绑定,则可以将这个标识符起名为LeftTrader来表现其语义。
由于上面定义的变量都是偏移类型,根本没有分配内存以和它们建立映射,它们也就很正常地不能是引用类型,即struct AB{ long a, &b; };将是错误的。还应注意上面的类型double (ABC::)[2],类型修饰符“ABC::”被用括号括起来,因为按照从左到右来解读类型操作符的规则,“ABC::”实际应该最后被解读,但其必须放在标识符的左边,就和指针修饰符“*”一样,所以必须使用括号将其括住,以表示其最后才起修饰作用。故也就有:double (*ABCD::)[2]、double (**ABCD::)[2],各如下定义:
struct ABCD { double ( *pD )[2]; double ( **ppD )[2]; };
但应注意,“ABCD::”并不能直接使用,即double ( *ABCD:: pD )[2];是错误的,要定义偏移类型的变量,必须通过类型定义符“{}”来自定义类型。还应注意C++也允许这样的类型double ( *ABCD::* )[2],其被称作成员指针,即类型为double ( *ABCD:: )[2]的指针,也就是可以如下:
double ( **ABCD::*pPPD )[2] = &ABC::ppD, ( **ABCD::**ppPPD )[2] = &pPPD;
上面很奇怪,回想什么叫指针类型。只有地址类型的数字才能有指针类型,表示不计算那个地址类型的数字,而直接返回其二进制表示,也就是地址。对于变量,地址就是它映射的数字,而指针就表示直接返回其映射的数字,因此&ABCD::ppD返回的数字其实就是偏移值,也就是4.
为了应用上面的偏移类型,C++给出了一对操作符——成员操作符“。”和“->”。前者两边接数字,左边接自定义类型的地址类型的数字,而右边接相应自定义类型的偏移类型的数字,返回偏移类型中给出的类型的地址类型的数字,比如:a.ABC::d;。左边的a的类型是ABC,右边的ABC::d的类型是double ABC::,则a.ABC::d返回的数字是double的地址类型的数字,因此可以这样:a.ABC::d = 10.0;。假设a对应的地址是3000,则a.ABC::d返回的地址为3000+24=3024,类型为double,这也就是为什么ABC::d被叫做偏移类型。由于“。”左边接的结构类型应和右边的结构类型相同,因此上面的ABC::可以省略,即a.d = 10.0;。而对于“->”,和“。”一样,只不过左边接的数字是指针类型罢了,即b->c[1] = 10.0;。应注意b->c[1]实际是( b->c )[1],而不是b->( c[1] ),因为后者是对偏移类型运用“[]”,是错误的。
还应注意由于右边接偏移类型的数字,所以可以如下:
double ( ABC::*pA )[2] = &ABC::c, ( ABC::**ppA )[2] = &pA;
( b->**ppA )[1] = 10.0; ( a.*pA )[0] = 1.0;
上面之所以要加括号是因为数组操作符“[]”的优先级较“*”高,但为什么不是b->( **ppA )[1]而是( b->**ppA )[1]?前者是错误的。应注意括号操作符“()”并不是改变计算优先级,而是它也作为一个操作符,其优先级被定得很高罢了,而它的计算就是计算括号内的数字。之前也说明了偏移类型是不能计算的,即ABC::c;将错误,而刚才的前者由于“()”的加入而导致要求计算偏移类型的数字,故编译器将报错。
还应该注意,成员指针是偏移类型的指针,即装的是偏移,则可以程序运行时期得到偏移,而前面通过ABC::a这种形式得到的是编译时期,由编译器帮忙映射的偏移,只能实现静态的偏移,而利用成员指针则可以实现动态的偏移。不过其实只需将成员定义成数组或指针类型照样可以实现动态偏移,不过就和前篇没有使用结构照样映射了人数布局一样,欠缺语义而代码可读性较低。成员指针的提出,通过变量名,就可以表现出丰富的语义,以增强代码的可读性。现在,可以将最开始说的人数布局定义如下:
struct PersonLayout{ char LeftTrader, LeftServitor, RightTrader, RightServitor; };
PersonLayout oldLayout[200], b;
因此,为了表示b这个人数分布中的左侧商人数,只需b.LeftTrader;,右侧的仆人数,只需b.RightServitor;。因为PersonLayout::LeftTrader记录了偏移值和偏移后应以什么样的类型来解释内存,故上面就可以实现原来的b[0]和b[3].很明显,前者的可读性远远地高于后者,因为前者通过变量名(b和PersonLayout::LeftTrader)和成员操作符“。”表现了大量的语义——b的左边的商人数。
注意PersonLayout::LeftTrader被称作结构PersonLayout的成员变量,而前面的ABC::d则是ABC的成员变量,这种叫法说明结构定义了一种层次关系,也才有所谓的成员操作符。既然有成员变量,那也有成员函数,这在下篇介绍。