初始化           [url=http://jisuanjixuexiao.com]计算机学校[/url]

    初始化就是之前在定义变量的同时,就给在栈上分配的内存赋值,如: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。
    应注意上面说的规则一般都可以通过编译选项而进行一定的改变,不同的编译器将给出不同的修改方式,在此不表。
    本篇说明了如何使用类型定义符“{}”来定义自定义类型,说明了两种自定义类型,实际还有许多自定义类型的内容未说明,将在下篇介绍,即后面介绍的类及类相关的内容都可应用在联合和结构上,因为它们都是自定义类型[url=http://jisuanjixuexiao.com]计算机学校[/url]