结构体、位段、枚举、联合知识的总结

前言:本篇文章用一些例子介绍了结构体、位段、枚举、联合的一些基本知识,文章的重点是一定要会结构体的内存对齐那部分知识,因为这个知识很重要。如果文章那里写的有错误,还请各位朋友指出。

结构体

什么是结构体

结构体是一些值的集合,这些值称为成员变量。结构体中的每个成员都可以是不同类型的变量

结构体的声明

一般声明


    struct tag
    {
        member-list;
    }var-list;

tag为结构体标签,member-list为成员列表,var-list是变量列表。

例如:用结构体描述一个学生


    struct Stu//结构体标签
    {
        char name[20];
        short age;
        char sex[5];
        char id[20];
        //成员列表
    }s,*ps;//变量列表

特殊声明(匿名结构体声明)

匿名结构体声明就是省略结构体标签的声明,但是要直接加上变量名,而且只能使用一次

例如:


    struct
    {
        int a;
        char b;
        float c;
    }x;
    struct
    {
        int a;
        char b;
        float c;
    }*p;

这里要注意:对于上边两个代码不能使用代码p=&x,因为他们没有结构体标签,编译器认为上边的两个类型不同,所以这个操作是非法的。

结构体成员的访问

假设我们现在声明以下结构体并创建结构体变量


    struct Stu
    {
        char name[20];
        short age;
    };//声明一个结构体
    struct Stu s;//创建结构体类型
    struct Stu *ps

要访问结构体成员有两种办法:

变量.成员

例如:上边的代码访问name成员就是s.name,访问age就是s.age

注意:这里如果要给s.name赋值不能直接赋值,因为name是数组名,代表首元素地址,s.name其实是数组首元素的地址,要想给它赋值,必须用strcpy函数,例如strcpy(s.name,"zhangsan")

指针->成员

例如:上例中用指针访问name成员就是ps->name,访问age成员就是ps->age,这里的ps->nameps->age相当于(*ps).name(*ps).age

结构体的自引用

结构体的自引用就是在结构中包含一个类型为该结构体本身的成员

例如:


    struct Node
    {
        int data;
        struct Node* next;
    };

但是这里要注意的是不能像下面这么写,因为是先有结构体成员,才进行类型重命名,有一个先后顺序


    typedef struct Node
    {
        int data;
        Node* next;
    }Node;//一定不能这么写

结构体的不完整声明

结构体的不完整声明就是在A结构体成员中包含B结构体,在B结构体成员中包含A结构体,但是总是得有一个在前面声明,所以就有了不完整声明

例如:


    struct B;//不完整声明
    struct A
    {
        int a;
        struct B* pb;
    };
    struct B
    {
        int b;
        struct A* pa;
    };

结构体变量的初始化

例如:


    struct Stu
    {
        char name[20];
        short age;
    };
    struct Stu s={"zhangsan",20};
    //定义变量的同时初始化
    struct Stu
    {
        char name[20];
        short age;
    }s2={"lisi",30};

结构体的内存对齐(计算结构体大小)

结构体的内存对齐是一个很重要的知识,要想计算结构体的大小就必须学懂内存对齐

内存对齐的规则

  • 第一个成员放在与结构体变量偏移量为0的地址处
  • 剩下的其他成员对齐到对齐数的整数倍地址处。对齐数=编译器默认对齐数与该成员大小的较小值(vs默认值是8,Linux的gcc编译器是4,当然这个默认值是可以改变的,后边会讲到)
  • 结构体的总大小为最大对齐数的整数倍
  • 如果有嵌套了结构体的情况,嵌套的结构体对齐到自身的最大对齐数的整数倍处,结构体的总大小就是所有对齐数中最大对齐数的整数倍

例如:下面几个结构体的大小
例1:


    struct S
    {
        char c1;
        //第一个成员放在0偏移处  1
        int i;
        //i的大小为4,默认对齐数为8,实际对齐数为4,放在4-7偏移处
        char c2;
        //c2的大小为1,实际对齐数为1,8偏移为1的倍数,放在8偏移处
        //结构体的总大小为最大对齐数4的倍数,既浪费3个,总大小为12
    };

例二:


    struct S3
    {
        double d;
        //对齐数为8,放在0-7偏移
        char c;
        //对齐数为1,8偏移是1的倍数,可以放在8偏移
        int i;
        //对齐数为4,12是4的倍数,放在12-15偏移处
        //总大小为最大对齐数8的倍数,16刚好是8的倍数,则结构体大小为16
    };
    struct S4
    {
        char c1;
        //放在1偏移处
        struct S3 s3;
        //嵌套结构体最大对齐数是8,放在8-23偏移
        double d;
        //对齐数为8,放在24-31,32刚好为最大对齐数8的倍数,则32位结构体的总大小
    };

结构体为什么会存在内存对齐

  • 移植原因

不是所有的硬件平台都能访问任意地址的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,内存对齐可以增强程序的可移植性

  • 性能原因

访问未对齐的内存,处理器需要做两次访问,而已经对齐的内存只要做一次访问,所以说结构体的内存对齐是在用空间换取时间

结构体的传参


    struct stu
    {
        int data[10];
        int count;
    };
    struct stu s = { "1,2,3,4", 100 };
    printf(s);//直接传结构体
    printf(&s);//创结构体的地址

注意:在结构体传参的时候,尽量传它的地址。因为如果结构体过大,函数传参的过程中是要压栈的,在参数压栈的过程中系统的开销比较大,如果直接传地址过去,会很大的提高效率。

默认对齐数的修改方法


    #pragma pack(4)
    ...
    代码区
    ...
    #pragma pack(4)

设置中间代码区的默认对齐数为4

位段

什么是位段

  • 1.位段声明和结构体类似
  • 2.位段的成员必须是int、unsigned int、signed int
  • 3.位段的成员名后边有一个冒号和一个数字

例如:


    struct A
    {
        int a : 2;
        int b : 5;
        int c : 10;
        int d : 30;
        //2、5、10、30代表的是比特位而不是字节
    };

位段的内存分配

位段的空间上是按需要以4个字节(int)或者是以1个字节(char)的方式来开辟的

那么上边的位段A的大小应该是多少呢?


    struct A
    {
        int a : 2;
        //先开辟4个字节的空间,也就是32个比特位
        //a占掉2个比特位,32-2=30
        int b : 5;
        //b占掉5个比特位,30-5=25
        int c : 10;
        //c占掉10个比特位,25-10=15
        int d : 30;
        //d占30个比特位,前边开辟的4个字节已经不够用了,因此在开辟四个字节
    };

所以sizeof(struct A)的结果为8

位段的跨平台问题

  • 1.int位段被当成是有符号还是无符号是不确定的
  • 2.位段中最大位的数目不能确定
  • 3.位段中的成员在内存中是从右向左还是从左向右分配的不确定
  • 4.当一个结构包含两个位段,第二个位段成员比较大,放不下在第一个位段剩余的为时,舍弃还是利用第二个位段成员是不确定的

位段和结构体相比,可以达到相同的效果,但是可以很好的节省空间,但是可移植性太差,不存在内存对齐。

枚举

枚举就是把所有可能的值一一列举出来

枚举类型的定义

例如:


    enum day
    {
        mon,
        tues,
        wed,
        thur,
        sat,
        sun
    };

上边的enum day就是一个枚举类型,括号里的内容是枚举类型的可能取值,这些可能的取值都是有值得,默认是从0开始,依次递增1,当然也可以在定义的时候自己赋值,所以又把它叫做枚举常量

枚举的优点

  • 1.增强代码的可读性和可为维护性
  • 2.和#define定义的标识符常量比较枚举有类型的检查,更加严谨
  • 3.可以很好的防止命名冲突
  • 4.便于调试
  • 5.一次可以定义多个常量,而#define一次只可以定义一个常量

枚举的使用

枚举变量存在类型检查,只能将枚举常量赋值给枚举变量,否则会出现类型不一致的错误

例如:


    enum color
    {
        RED=1,
        GREEN=2,
        BLUE=4
    };
    enum color C = RED;
    //不能给C赋值其他类型的常量,例如:C=5,这是不行的

联合

什么是联合(共用体)

联合是一种特殊的自定义类型,这种类型也包含一系列成员,特征是这些成员共用一块空间,所以它也叫做共用体

例如:


    union Un
    {
        int i;
        char c;
    };
    union Un un;

联合的特点

这里写图片描述

例如:下面代码输出结果是什么


    #include<stdio.h>
    union Un
    {
        int i;
        char c;
    };
    int main()
    {
        union Un un;
        un.i = 0x11223344;
        un.c = 0x55;
        printf("%x\n", un.i);
        return 0;
    }

联合的特点是所有成员共用一块内存空间,所以上边的代码输出结果是0x11223355

联合大小的计算

  • 1.联合的大小至少是最大成员的大小
  • 2.当最大成员大小不是最大对齐数的整数倍时,就要对齐要最大对齐数的整数倍

例如:


    #include<stdio.h>
    union Un2
    {
        char c[5];
        int i;
    };
    union Un1
    {
        short c[7];
        int i;
    };
    int main()
    {
        printf("%d\n", sizeof(union Un1));
        printf("%d\n", sizeof(union Un2));
        return 0;
    }

输出结果:16、8

联合和结构体的巧妙使用

将long类型的IP地址装换为点分十进制形式


    #include<stdio.h>
    union ip_addr
    {
        unsigned long addr;
        struct
        {
            unsigned char c1;
            unsigned char c2;
            unsigned char c3;
            unsigned char c4;
        }ip;
    };
    int main()
    {
        union ip_addr my_ip;
        my_ip.addr = 16355327675;
        printf("%d.%d.%d.%d\n", my_ip.ip.c4, my_ip.ip.c3, my_ip.ip.c2, my_ip.ip.c1);
        return 0;
    }

判断当前计算机的大小端存储

这道题在我在前面写的一篇博客《大小端详解》里提到了两种解法,一种是利用联合体巧妙的证明了计算机的大小端存储,这里贴出链接https://blog.csdn.net/hansionz/article/details/80871921,题目在博客的最后哦!

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/80884274