深入C(关键字)七八

1.14,struct关键字

    struct是个神奇的关键字,它将一些相关联的数据打包成一个整体,方便使用。

    在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改,非常容易出错。这个时候只需要一个结构体就能搞定。平时我们要求函数的参数尽量不多于4个,如果函数的参数多于4个使用起来非常容易出错(包括每个参数的意义和顺序都容易弄错),效率也会降低(与具体CPU有关,ARM芯片对于超过4个参数的处理就有讲究,具体请参考相关资料)。这个时候,可以用结构体压缩参数个数。

1.14.1,空结构体多大?

    结构体所占的内存大小是其成员所占内存之和(关于结构体的内存对齐,请参考预处理那章)。这点很容易理解,但是下面的这种情况呢?

    structstudent

    {

    }stu;

sizeof(stu)的值是多少呢?在VisualC++6.0上测试一下。

    很遗憾,不是0,而是1。为什么呢?你想想,如果我们把structstudent看成一个模子的话,你能造出一个没有任何容积的模子吗?显然不行。编译器也是如此认为。编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?假设结构体内只有一个char型的数据成员,那其大小为1byte(这里先不考虑内存对齐的情况).也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占的空间大吧。这就麻烦了,空结构体的大小既不能为0,也不能大于1,怎么办?定义为0.5个byte?但是内存地址的最小单位是1个byte,0.5个byte怎么处理?解决这个问题的最好办法就是折中,编译器理所当然的认为你构造一个结构体数据类型是用来打包一些数据成员的,而最小的数据成员需要1个byte,编译器为每个结构体类型数据至少预留1个byte的空间。所以,空结构体的大小就定位1个byte。


1.14.2,柔性数组

    也许你从来没有听说过柔性数组(flexiblearray)这个概念,但是它确实是存在的。

    C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

    柔性数组到底如何使用呢?看下面例子:

    typedefstructst_type

    {

    inti;

    inta[0];

    }type_a;

    有些编译器会报错无法编译可以改成:

    typedefstructst_type

    {

    inti;

    inta[];

    }type_a;

这样我们就可以定义一个可变长的结构体,用sizeof(type_a)得到的只有4,就是sizeof(i)=sizeof(int)。那个0个元素的数组没有占用空间,而后我们可以进行变长操作了。通过如下表达式给结构体分配内存:

    type_a*p=(type_a*)malloc(sizeof(type_a)+100*sizeof(int));

    这样我们为结构体指针p分配了一块内存。用p->item[n]就能简单地访问可变长元素。但是这时候我们再用sizeof(*p)测试结构体的大小,发现仍然为4。是不是很诡异?我们不是给这个数组分配了空间么?

    别急,先回忆一下我们前面讲过的“模子”。在定义这个结构体的时候,模子的大小就已经确定不包含柔性数组的内存大小。柔性数组只是编外人员,不占结构体的编制。只是说在使用柔性数组时需要把它当作结构体的一个成员,仅此而已。再说白点,柔性数组其实与结构体没什么关系,只是“挂羊头卖狗肉”而已,算不得结构体的正式成员。

    需要说明的是:C89不支持这种东西,C99把它作为一种特例加入了标准。但是,C99所支持的是incompletetype,而不是zeroarray,形同intitem[0];这种形式是非法的,C99支持的形式是形同intitem[];只不过有些编译器把intitem[0];作为非标准扩展来支持,而且在C99发布之前已经有了这种非标准扩展了,C99发布之后,有些编译器把两者合而为一了。

    当然,上面既然用malloc函数分配了内存,肯定就需要用free函数来释放内存:

    free(p);

    经过上面的讲解,相信你已经掌握了这个看起来似乎很神秘的东西。不过实在要是没掌握也无所谓,这个东西实在很少用。

1.14.3,struct与class的区别

    在C++里struct关键字与class关键字一般可以通用,只有一个很小的区别。struct的成员默认情况下属性是public的,而class成员却是private的。很多人觉得不好记,其实很容易。你平时用结构体时用public修饰它的成员了吗?既然struct关键字与class关键字可以通用,你也不要认为结构体内不能放函数了。

    当然,关于结构体的讨论远没有结束,在指针与数组那一章,你还会要和它打交道的。


1.15,union关键字

    union关键字的用法与struct的用法非常类似。

    union维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。例子如下:

    unionStateMachine

    {

    charcharacter;

    intnumber;

    char*str;

    doubleexp;

    };

    一个union只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大长度是double型态,所以StateMachine的空间大小就是double数据类型的大小。

    在C++里,union的成员默认属性页为public。union主要用来压缩空间。如果一些数据不可能在同一时间同时被用到,则可以使用union。

1.15.1,大小端模式对union类型数据的影响

    下面再看一个例子:

    union

    {

    inti;

    char a[2];

    }*p,u;

    p=&u;

    p->a[0]=0x39;

    p->a[1]=0x38;

p.i的值应该为多少呢?

    这里需要考虑存储模式:大端模式和小端模式。

    大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。

    小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。

    union型数据所占的空间等于其最大的成员所占的空间。对union型的成员的存取都是相对于该联合体基地址的偏移量为0处开始,也就是联合体的访问不论对哪个变量的存取都是从union的首地址位置开始。如此一解释,上面的问题是否已经有了答案呢?


1.15.2,如何用程序确认当前系统的存储模式?

    上述问题似乎还比较简单,那来个有技术含量的:请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1。

    先分析一下,按照上面关于大小端模式的定义,假设int类型变量i被初始化为1。

    以大端模式存储,其内存布局如下图:


    以小端模式存储,其内存布局如下图:

    变量i占4个字节,但只有一个字节的值为1,另外三个字节的值都为0。如果取出低地址上的值为0,毫无疑问,这是大端模式;如果取出低地址上的值为1,毫无疑问,这是小端模式。既然如此,我们完全可以利用union类型数据的特点:所有成员的起始地址一致。到现在,应该知道怎么写了吧?参考答案如下:

    intcheckSystem()

    {

    unioncheck

    {

    inti;

    charch;

    }c;

    c.i=1;

    return(c.ch==1);

    }

    现在你可以用这个函数来测试你当前系统的存储模式了。当然你也可以不用函数而直接去查看内存来确定当前系统的存储模式。如下图:


    图中0x01的值存在低地址上,说明当前系统为小端模式。

    不过要说明的一点是,某些系统可能同时支持这两种存储模式,你可以用硬件跳线或在编译器的选项中设置其存储模式。

    留个问题:

    在x86系统下,输出的值为多少?

    #include <stdio.h>

    intmain()

    {

    inta[5]={1,2,3,4,5};

    int*ptr1=(int*)(&a+1);

    int*ptr2=(int*)((int)a+1);

    printf("%x,%x",ptr1[-1],*ptr2);

    return0;

    }


1.16,enum关键字

    很多初学者对枚举(enum)感到迷惑,或者认为没什么用,其实枚举(enum)是个很有用的数据类型。

1.16.1, 枚举类型的使用方法

一般的定义方式如下:

    enumenum_type_name

    {

    ENUM_CONST_1,

    ENUM_CONST_2,

    ...

    ENUM_CONST_n

    }enum_variable_name;

    注意:enum_type_name是自定义的一种数据数据类型名,而enum_variable_name为enum_type_name类型的一个变量,也就是我们平时常说的枚举变量。实际上enum_type_name类型是对一个变量取值范围的限定,而花括号内是它的取值范围,即enum_type_name类型的变量 enum_variable_name只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或者警告。ENUM_CONST_1、ENUM_CONST_2、...、

ENUM_CONST_n,这些成员都是常量,也就是我们平时所说的枚举常量(常量一般用大写)。enum变量类型还可以给其中的常量符号赋值,如果不赋值则会从被赋初值的那个常量开始依次加1,如果都没有赋值,它们的值从0开始依次递增1。如分别用一个常数表示不同颜色:

    enumColor

    {

    GREEN=1,

    RED,

    BLUE,

    GREEN_RED=10,

    GREEN_BLUE

    }ColorVal;

    其中各常量名代表的数值分别为:

    GREEN=1

    RED=2

    BLUE=3

    GREEN_RED=10

    GREEN_BLUE=11

1.16.2,枚举与#define宏的区别

    下面再看看枚举与#define宏的区别:

1),#define宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。

3),枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个。

留两个问题:

A),枚举能做到事,#define宏能不能都做到?如果能,那为什么还需要枚举?B),sizeof(ColorVal)的值为多少?为什么?

1.17,伟大的缝纫师----typedef关键字


1.17.1,关于马甲的笑话

    有这样一个笑话:一个猎人在河边抓捕一条蛇,蛇逃进了水里。过一会,一个乌龟爬到岸边。猎人一把抓住这个乌龟,大声的说道:小样,别你为你穿了个马甲我就不认识你了!

    typedef关键字是个伟大的缝纫师,擅长做马甲,任何东西穿上这个马甲就立马变样。它可以把狼变成一头羊,也能把羊变成一头狼。甚至还可以把长着翅膀的鸟人变成天使,同样也能把美丽的天使变成鸟人。所以,你千万不要得罪它,一定要掌握它的脾气,不然哪天我把你当鸟人,你可别怪我。^_^。

1.17.2,历史的误会----也许应该是typerename

    很多人认为typedef是定义新的数据类型,这可能与这个关键字有关。本来嘛,type是数据类型的意思;def(ine)是定义的意思,合起来就是定义数据类型啦。不过很遗憾,这种理解是不正确的。也许这个关键字该被替换为“typerename”或是别的词。

    typedef的真正意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型。比如:华美绝伦的芍药,就有个别名---“将离”。中国古代男女交往,往往以芍药相赠,表达惜别之情,送芍药就意味着即将分离。所以文人墨客就给芍药取了个意味深长的别名-----“将离”。这个新的名字就表达了那种依依不舍的惜别之情…这样新的名字与原来的名字相比,就更能表达出想要表达的意思。

    在实际项目中,为了方便,可能很多数据类型(尤其是结构体之类的自定义数据类型)需要我们重新取一个适用实际情况的别名。这时候typedef就可以帮助我们。例如:

    typedef struct student

    {

    //code

    }Stu_st,*Stu_pst; //命名规则请参考本章前面部分

    A),struct student stu1;和Stu_st stu1;没有区别。

    B),struct student * stu2;和Stu_pst stu2;和Stu_st * stu2;没有区别。

    这个地方很多初学者迷惑,B)的两个定义为什么相等呢?其实很好理解。我们把“struct student{/*code*/}” 看成一个整体,typedef就是给“struct student{/*code*/}”取了个别名叫“Stu_st”;同时给“structstudent{/*code*/}* ”取了个别名叫“Stu_pst”。只不过这两个名字同时取而已,好比你给你家小狗取了个别名叫“大黄”,同时你妹妹给小狗带了小帽子,然后给它取了个别名叫“小可爱”。^_^。

    好,下面再把typedef 与const放在一起看看:

    C),const Stu_pst stu3;

    D),Stu_pst const stu4;

    大多数初学者认为C)里const修饰的是stu3指向的对象;D)里const修饰的是stu4这个指针。很遗憾,C)里const修饰的并不是stu3指向的对象。那const这时候到底修饰的是什么呢?我们在讲解const int i 的时候说过const放在类型名“int”前后都行;而const int *p与int *const p则完全不一样。也就是说,我们看const修饰谁都时候完全可以将数据类型名视而不见,当它不存在。反过来再看“const Stu_pst stu3”,Stu_pst是“struct student{/*code*/}*”的别名, “struct student{/*code*/}*”是一个整体。对于编译器来说,只认为Stu_pst是一个类型名,所以在解析的时候很自然的把“Stu_pst”这个数据类型名忽略掉。现在知道const到底修饰的是什么了吧?^_^。

1.17.3,typedef与#define的区别

    噢,上帝!这真要命!别急,要命的还在后面呢。看如下例子:

    E), #define INT32 int

    unsigned INT32 i=10;

    F),typedef int int32;

    unsigned int32 j=10;

    其中F)编译出错,为什么呢?E)不会出错,这很好理解,因为在预编译的时候INT32被替换为int,而unsigned int i=10;语句是正确的。但是,很可惜,用typedef取的别名不支持这种类型扩展。另外,想想typedef static int int32行不行?为什么?

下面再看一个与#define宏有关的例子:

    G),#define PCHAR char*

    PCHAR p3,p4;

    H),typedef char* pchar;

    pcharp1,p2;

    两组代码编译都没有问题,但是,这里的p4却不是指针,仅仅是一个char类型的字符。这种错误很容易被忽略,所以用#define的时候要慎之又慎。关于#define当然还有很多话题需要讨论,请看预处理那一章。当然关于typedef的讨论也还没有结束,在指针与数组那一章,我们还要继续讨论。

1.17.4,#define a int[10]与typedef int a[10];

    留两个问题:

    1),#defineaint[10]

    A),a[10] a[10];

    B),a[10] a;

    C),int    a[10];

    D),int    a;

    E),a    b[10];

    F),a    b;

    G),a*    b[10];

    H),a*    b;

    2),typedef int a[10];

    A),a[10] a[10];

    B),a[10] a;

    C),int    a[10];

    D),int    a;

    E),a    b[10];

    F),a    b;

    G),a*    b[10];

    H),a*    b;

    3),#define a int*[10]

    A),a[10] a[10];

    B),a[10] a;

    C),int    a[10];

    D),int    a;

    E),a    b[10];

    F),a    b;

    G),a*    b[10];

    H),a*    b;

4),typedef int* a[10];

   A),a[10] a[10];

   B),a[10] a;

   C),int    a[10];

   D),int    a;

   E),a    b[10];

   F),a    b;

   G),a*    b[10];

   H),a*    b;

5),#define *a int[10]

    A),a[10] a[10];

    B),a[10] a;

    C),int    a[10];

    D),int    a;

    E),a    b[10];

    F),a    b;

    G),a*    b[10];

    H),a*    b;

6),typedefint (*a)[10];

   A),a[10] a[10];

   B),a[10] a;

   C),int    a[10];

   D),int    a;

   E),a    b[10];

   F),a    b;    G),a*    b[10];

    H),a*    b;

7),#define *a *int[10]

    A),a[10] a[10];

    B),a[10] a;

    C),int    a[10];

    D),int    a;

    E),a    b[10];

    F),a    b;

    G),a*    b[10];

    H),a*    b;

8),typedefint* (*a)[10];

    A),a[10] a[10];

    B),a[10] a;

    C),int    a[10];

    D),int    a;

    E),a    b[10];

    F),a    b;

    G),a*    b[10];

    H),a*    b;

   请判断这里面哪些定义正确,哪些定义不正确。另外,int[10]和a[10]到底该怎么用?



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值