C++从零开始(九)——何谓结构

类别:编程语言 点击:0 评论:0 推荐:
 

C++从零开始(九)

——何谓结构

    前篇已经说明编程时,拿到算法后该干的第一件事就是把资源映射成数字,而前面也说过“类型就是人为制订的如何解释内存中的二进制数的协议”,也就是说一个数字对应着一块内存(可能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的成员变量,这种叫法说明结构定义了一种层次关系,也才有所谓的成员操作符。既然有成员变量,那也有成员函数,这在下篇介绍。
    前篇在映射过河方案时将其映射为char,其中的前4位表示仆人数,后4位表示商人数。对于这种使用长度小于1个字节的用法,C++专门提供了一种语法以支持这种情况,如下:
    struct Solution { ServitorCount : 4; unsigned TraderCount : 4; } sln[5];
    由于是基于二进制数的位(Bit)来进行操作,只准使用两种类型来表示数字,原码解释数字或补码解释数字。对于上面,ServitorCount就是补码解释,而TraderCount就是原码解释,各自的长度都为4位,而此时Solution::ServitorCount中依旧记录的是偏移,不过不再以字节为单位,而是位为单位。并且由于其没有类型,故也就没有成员指针了。即前篇的( sln[ cur[ curSln ] ] & 0xF0 ) >> 4等效于sln[ cur[ curSln] ].TraderCount,而sln[ cur[ curSln ] ] & 0xF0等效于sln[ cur[ curSln] ].ServitorCount,较之前具有了更好的可读性。
    应该注意,由于struct AB { long a, b; };也是一条语句,并且是一条声明语句(因为不生成代码),但就其意义上来看,更通常的叫法把它称为定义语句,表示是类型定义语句,但按照不生成代码的规则来判断,其依旧是声明语句,并进而可以放在类型定义符“{}”中,即:
    struct ABC{ struct DB { long a, *b[2]; }; long c; DB a; };
    上面的结构DB就定义在结构ABC的声明语句中,则上面就定义了四个变量,类型均为偏移类型,变量名依次为:ABC::DB::a、ABC::DB::b、ABC::c、ABC::a;类型依次为long ABC::DB::、long* (ABC::DB::)[2]、long ABC::、ABC::DB;映射的数值依次为0、4、0、4。这里称结构DB嵌套在结构ABC中,其体现出一种层次关系,实际中这经常被使用以表现特定的语义。欲用结构DB定义一个变量,则ABC::DB a;。同样也就有long* ( ABC::DB::*pB )[2] = &ABC::DB::b; ABC c; c.a.a = 10; *( c.a.b[0] ) = 20;。应注意ABC::DB::表示“ABC的DB的”而不是“DB的ABC的”,因为这里是重复的类型修饰符,是从右到左进行修饰的。
    前面在定义结构时,都指明了一个类型名,如前面的ABC、ABCD等,但应该注意类型名不是必须的,即可以struct { long a; double b; } a; a.a = 10; a.b = 34.32;。这里就定义了一个变量,其类型是一结构类型,不过这个结构类型没有标识符和其关联,以至于无法对其运用类型匹配等比较,如下:
    struct { long a; double b; } a, &b = a, *c = &a; struct { long a; double b; } *d = &a;
    上面的a、b、c都没有问题,因为使用同一个类型来定义的,即使这个类型没有标识符和其映射,但d将会报错,即使后写的结构的定义式和前面的一模一样,但仍然不是同一个,只是长得像罢了。那这有什么用?后面说明。
    最后还应该注意,当在复合语句中书写前面的声明语句以定义结构时,之前所说的变量作用域也同样适用,即在某复合语句中定义的结构,出了这个复合语句,它就被删除,等于没定义。如下:
void ABC()
{
    struct AB { long a, b; };
    AB d; d.b = 10;
}
void main()
{
    {
        struct AB{ long a, b, e; };
        AB c; c.e = 23;
    }
    AB a;  // 将报错,说AB未定义,但其他没有任何问题
}


初始化

    初始化就是之前在定义变量的同时,就给在栈上分配的内存赋值,如:long a = 10;。当定义的变量的类型有表示多个元素时,如数组类型、上面的结构类型时,就需要给出多个数字。对此,C++专门给出了一种语法,使用一对大括号将欲赋的值括起来后,整体作为一个数字赋给数组或结构,如下:
    struct ABC { long a, b; float c, d[3]; };
    ABC a = { 1, 2, 43.4f, { 213.0f, 3.4f, 12.4f } };
    上面就给出了为变量a初始化的语法,大括号将各元素括起来,而各元素之间用“,”隔开。应注意ABC::d是数组类型,其对应的初始化用的数字也必须用大括号括起来,因此出现上面的嵌套大括号。现在应该了解到“{}”只是用来构造一个具有多个元素的数字而已,因此也可以有long a = { 34 };,这里“{}”就等同于没有。还应注意,C++同意给出的大括号中的数字个数少于相应自定义类型或数组的元素个数,即:ABC a = { 1, 2, 34 }, b = { 23, { 34 }, 65, { 23, 43 } }, c = { 1, 2, { 3, { 4, 5, 6 } } };
    上面的a.d[0]、a.d[1]、a.d[2]都为0,而只有b.d[2]才为0,但c将会报错,因为嵌套的第一个大括号将{ 4, 5, 6 }也括了起来,表示c.c将被一个具有两个元素的数字赋值,但c.c的类型是float,只对应一个元素,编译器将说初始化项目过多。而之前的a和b未赋值的元素都将被赋值为0,但应注意并不是数值上的0,而是简单地将未赋值的内存的值用0填充,再通过那些补码原码之类的格式解释成数值后恰好为0而已,并不是赋值0这个数字。
    应注意,C++同意这样的语法:long a[] = { 34, 34, 23 };。这里在定义a时并没有给出元素个数,而是由编译器检查赋值用的大括号包的元素个数,由其来决定数组的个数,因此上面的a的类型为long[3]。当多维数组时,如:long a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };。因为每个元素又是需要多个元素的数字,就和前面的ABC::d一样。再回想类型修饰符的修饰顺序,是从左到右,但当是重复类型修饰符时,就倒过来从右到左,因此上面就应该是三个long[2],而不是两个long[3],因此这样将错误:long a[3][2] = { { 1, 2, 3 }, { 4, 5, 6 } };。
    还应注意,C++不止提供了上面的“{}”这一种初始化方式,对于字符串,其专门提供如:char a[] = "ABC";。这里a的类型就为char[4],因为字符串"ABC"需要占4个字节的内存空间。除了这两种初始化方式外,C++还提供了一种函数式的初始化函数,下篇介绍。


类型的运用

    char a = -34; unsigned char b = ( unsigned char )a;
    上面的b等于222,将-34按照补码格式写成二进制数11011110,然后将这个二进制数用原码格式解释,得数值222。继续:
    float a = 5.6f; unsigned long b = ( unsigned long )a;
    这回b等于5。为什么?不是应该将5.6按照IEEE的real*4的格式写成二进制数0X40B33333(这里用十六进制表示),然后将这个二进制数用原码格式解释而得数值1085485875吗?因为类型转换是语义上的类型转换,而不是类型变换。
    两个类型是否能够转换,要视编译器是否定义了这两个类型之间的转换规则。如char和unsigned char,之所以前面那样转换是因为编译器把char转unsigned char定义成了那样,同样float转unsigned long被编译器定义成了取整而不是四舍五入。
    为什么要有类型转换?有什么意义?的确,像上面那样的转换,毫无意义,仅仅只是为了满足语法的严密性而已,不过由于C++定义了指针类型的转换,而且定义得非常地好,以至于有非常重要的意义。
    char a = -34; unsigned char b = *( unsigned char* )( &a );
    上面的结果和之前的一样,b为222,不过是通过将char*转成unsigned char*,然后再用unsigned char来解释对应的内存而得到222,而不是按照编译器的规定来转换的,即使结果一样。因此:
    float a = 5.6f; unsigned long b = *( unsigned long* )( &a );
    上面的b为1085485875,也就是之前以为的结果。这里将a的地址所对应的内存用unsigned long定义的规则来解释,得到的结果放在b中,这体现了类型就是如何解释内存中的内容。上面之所以能实现,是因为C++规定所有的指针类型之间的转换,数字的数值没有变化,只有类型变化(但由于类的继承关系也是可能会改变,下篇说明),因此上面才说b的值是用unsigned long来解释a对应的内存的内容所得的结果。因此,前篇在比较oldLayout[ curSln ][0~3]和oldLayout[ i ][0~3]时写了四个“==”以比较了四次char的数字,由于这四个char数字是连续存放的,因此也可如下只比较一次long数字即可,将节约多余的三次比较时间。
    *( long* )&oldLayout[ curSln ] == *( long* )&oldLayout[ i ]
    上面只是一种优化手段而已,对于语义还是没有多大意义,不过由于有了自定义类型,因此:
    struct AB { long a1; long a2; }; struct ABC { char a, b; short c; long d; };
    AB a = { 53213, 32542 }; ABC *pA = ( ABC* )&a;
    char aa = pA->a, bb = pA->b, cc = pA->c; long dd = pA->d;
    pA->a = 1; pA->b = 2; pA->c = 3; pA->d = 4;
    long aa1 = a.a1, aa2 = a.a2;
    上面执行后,aa、bb、cc、dd的值依次为-35、-49、0、32542,而aa1和aa2的值分别为197121和4。相信只要稍微想下就应该能理解为什么没有修改a.a1和a.a2,结果它们的值却变了,因为变量只不过是个映射而已,而前面就是利用指针pA以结构ABC来解释并操作a所对应的内存的内容。
    因此,利用自定义类型和指针转换,就可以实现以什么样的规则来看待某块内存的内容。有什么用?传递给某函数一块内存的引用(利用指针类型或引用类型),此函数还另有一个参数,比如是long类型。当此long类型的参数为1时,表示传过去的是一张定单;为2时,表示传过去的是一张发货单;为3时表示是一张收款单。如果再配上下面说明的枚举类型,则可以编写出语义非常完善的代码。
    应注意由于指针是可以随便转换的,也就有如下的代码,实际并没什么意义,在这只为加深对成员指针的理解:
    long AB::*p = ( long AB::* )( &ABC::b ); a.a1 = a.a2 = 0; a.*p = 0XAB1234CD;
    上面执行后,a.a1为305450240,a.a2为171,转成十六进制分别为0X1234CD00和0X000000AB。


枚举

    上面欲说明1时为定单,2时为发货单而3时为收款单,则可以利用switch或if语句来进行判断,但是语句从代码上将看见类似type == 1或type == 2之类,无法表现出语义。C++专门为此提供了枚举类型。
    枚举类型的格式和前面的自定义类型很像,但意义完全不同,如下:
    enum AB { LEFT, RIGHT = 2, UP = 4, DOWN = 3 }; AB a = LEFT;
    switch( a )
    {
        case LEFT:;  // 做与左相应的事
        case UP:;    // 做与上相应的事
    }
    枚举也要用“{}”括住一些标识符,不过这些标识符即不映射内存地址也不映射偏移,而是映射整数,而为什么是整数,那是因为没有映射浮点数的必要,后面说明。上面的RIGHT就等同于2,注意是等同于2,相当于给2起了个名字,因此可以long b = LEFT; double c = UP; char d = RIGHT;。但注意上面的变量a,它的类型为AB,即枚举类型,其解释规则等同于int,即编译成在16位操作系统上运行时,长度为2个字节,编译成在32位操作系统上运行时为4个字节,但和int是属于不同的类型,而前面的赋值操作之所以能没有问题,可以认为编译器会将枚举类型隐式转换成int类型,进而上面没有错误。但倒过来就不行了,因为变量a的类型是AB,则它的值必须是上面列出的四个标识符中的一个,而a = b;则由于b为long类型,如果为10,那么将无法映射上面的四个标识符中的一个,所以不行。
    注意上面的LEFT没有写“=”,此时将会从其前面的一个标识符的值自增一,由于它是第一个,而C++规定为0,故LEFT的值为0。还应注意上面映射的数字可以重复,即:
    enum AB { LEFT, RIGHT, UP = 5, DOWN, TOP = 5, BOTTOM };
    上面的各标识符依次映射的数值为0、1、5、6、5、6。因此,最开始说的问题就可以如下处理:
    enum OperationType { ORDER = 1, INVOICE, CHECKOUT };
    而那个参数的类型就可以为OperationType,这样所表现的语义就远远地超出原来的代码,可读性高了许多。因此,当将某些人类世界的概念映射成数字时,发现它们的区别不表现在数字上,比如吃饭、睡觉、玩表示一个人的状态,现在为了映射人这个概念为数字,也需要将人的状态这个概念映射成数字,但很明显地没有什么方便的映射规则。这时就强行说1代表吃饭,2代表睡觉,3代表玩,此时就可以使用将1、2、3定义成枚举以表现语义,这也就是为什么枚举只定义为整数,因为没有定义成浮点数的必要性。


联合

    前面说过类型定义符的前面可以接struct、class和union,当接union时就表示是联合型自定义类型(简称联合),它和struct的区别就是后者是串行分布来定义成员变量,而前者是并行分布。如下:
    union AB { long a1, a2, a3; float b1, b2, b3; }; AB a;
    变量a的长度为4个字节,而不是想象的6*4=24个字节,而联合AB中定义的6个变量映射的偏移都为0。因此a.a1 = 10;执行后,a.a1、a.a2、a.a3的值都为10,而a.b1的值为多少,就用IEEE的real*4格式来解释相应内存的内容,该多少是多少。
    也就是说,最开始的利用指针来解释不同内存的内容,现在可以利用联合就完成了,因此上面的代码搬到下面,变为:
    union AB
    {
        struct { long a1; long a2; };
        struct { char a, b; short c; long d; };
    };
    AB a = { 53213, 32542 };
    char aa = a.a, bb = a.b, cc = a.c; long dd = a.d;
    a.a = 1; a.b = 2; a.c = 3; a.d = 4;
    long aa1 = a.a1, aa2 = a.a2;
    结果不变,但代码要简单,只用定义一个自定义类型了,而且没有指针变量的运用,代码的语义变得明显多了。
    注意上面定义联合AB时在其中又定义了两个结构,但都没有赋名字,这是C++的特殊用法。当在类型定义符的中间使用类型定义符时,如果没有给类型定义符定义的类型绑定标识符,则依旧定义那些偏移类型的变量,不过这些变量就变成上层自定义类型的成员变量,因此这时“{}”等同于没有,唯一的意义就是通过前面的struct或class或union来指明变量的分布方式。因此可以如下:
    struct AB
    {
        struct { long a1, a2; };
        char a, b;
        union { float b1; double b2; struct { long b3; float b4; char b5; }; };
        short c;
    };
    上面的自定义类型AB的成员变量就有a1、a2、a、b、b1、b2、b3、b4、b5、c,各自对应的偏移值依次为0、4、8、9、10、10、10、14、18、19,类型AB的总长度为21字节。某类型的长度表示如果用这个类型定义了一个变量,则编译器应该在栈上分配多大的连续空间,C++为此专门提供了一个操作符sizeof,其右侧接数字或类型名,当接数字时,就返回那个数字的类型需要占的内存空间的大小,而接类型名时,就返回那个类型名所标识的类型需要占的内存空间的大小。
    因此long a = sizeof( AB ); AB d; long b = sizeof d;执行后,a和b的值都为40。怎么是40?不应该为21吗?而之前的各成员变量对应的偏移也依次实际为0、4、8、9、16、16、16、20、24、32。为什么?这就是所谓的数据对齐。
    CPU有某些指令,需要处理多个数据,则各数据间的间隔必须是4字节或8字节或16字节(视不同的指令而有不同的间隔),这被称作数据对齐。当各个数据间的间隔不符合要求时,CPU就必须做附加的工作以对齐数据,效率将下降。并且CPU并不直接从内存中读取东西,而要经一个高速缓冲(CPU内建的一个存取速度比内存更快的硬件)缓冲一下,而此缓冲的大小肯定是2的次方,但又比较小,因此自定义类型的大小最好能是2的次方的倍数,以便高效率的利用高速缓冲。
    在自定义类型时,一个成员变量的偏移值一定是它所属的类型的长度的倍数,即上面的a和b的偏移必须是1的倍数,而c的偏移必须是2的倍数,b1的偏移必须是4的倍数。但b2的偏移必须是8的倍数,而b1和b2由于前面的union而导致是并行布局,因此b1的偏移必须和b2及b3的相同,因此上面的b1、b2、b3的偏移变成了8的倍数16,而不是想象的10。
    而一个自定义类型的长度必须是其成员变量中长度最长的那个成员变量的长度的倍数,因此struct { long b3; float b4; char b5; };的长度是4的倍数,也就是12。而上面的无名联合的成员变量中,只有double b2;的长度最长,为8个字节,所以它的长度为16,并进而导致c的偏移为b1的偏移加16,故为32。由于结构AB中的成员变量只有b2的长度最长,为8,故AB的长度必须是8的倍数40。因此在定义结构时应尽量将成员和其长度对应起来,如下:
    struct ABC1 { char a, b; char d; long c; };
    struct ABC2 { char a, b; long c; char d; };
    ABC1的长度为8个字节,而ABC2的长度为12个字节,其中ABC1::c和ABC2::c映射的偏移都为4。
    应注意上面说的规则一般都可以通过编译选项而进行一定的改变,不同的编译器将给出不同的修改方式,在此不表。
    本篇说明了如何使用类型定义符“{}”来定义自定义类型,说明了两种自定义类型,实际还有许多自定义类型的内容未说明,将在下篇介绍,即后面介绍的类及类相关的内容都可应用在联合和结构上,因为它们都是自定义类型。

本文地址:http://com.8s8s.com/it/it25040.htm