• C语言中的自定义类型详解(结构体 + 枚举 + 联合(共用体))


    1. 结构体

    结构体是C语言中用于表示一组相关数据的自定义数据类型。结构体允许将多个不同类型的数据组组合在一起,以便更方便地管理和操作这些数据。

    1.1 结构体的声明

    struct tag
    {
    	member-list;
    }variable-list;
    
    • 1
    • 2
    • 3
    • 4
    • 使用 struct 关键字声明结构体的名称。
    • 在大括号 {} 内部定义结构体的成员变量,成员变量的类型可以是标量、数组、指针,甚至是其他结构体,每个成员变量都有一个类型和一个名称,成员之间用分号 ; 分隔。
    • 最后以分号 ; 结束结构体的定义。

    以下代码,展示了如何定义一个简单的结构体以用来表示学生信息:

    #include 
    
    // 定义一个结构体 Student
    struct Student 
    {
        char name[20];//名字
        int age;//年龄
        double score;//成绩
    };
    
    int main() 
    {
        // 声明一个结构体变量并初始化
        struct Student s1 = { "zzb", 20, 98};
    
        // 访问结构体成员并打印信息
        printf("学生姓名:%s\n", s1.name);
        printf("学生年龄:%d\n", s1.age);
        printf("学生成绩:%.2lf\n", s1.score);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.2 结构体成员的访问

    在C语言中,结构体变量访问其成员一共有如下两种方式:

    1. 使用点运算符(.):点运算符用于访问结构体变量的成员。其语法格式如下:

    结构体变量.成员名

    例如,上面代码中有一个名为 s1 的结构体变量,并且结构体中有一个成员叫做 name,我们就可以通过以下方式访问该成员:

    s1.name = "zzb";
    
    • 1
    1. 使用箭头运算符(->):箭头运算符用于访问结构体指针所指向的结构体的成员变量。其语法格式如下:

    结构体指针->成员名

    例如,如果有一个名为 ps1 的指向结构体的指针,并且结构体中有一个成员叫做 age,我们就可以通过以下方式访问该成员:

    ps1->age = 20;
    
    • 1

    1.3 匿名结构体

    在C语言中,我们可以创建匿名结构体,这是一种没有名字的结构体,结构体在声明的时候省略掉了结构体标签。该结构体通常用于简单的数据封装或临时数据组织,而不需要定义一个有具体名字的结构体类型。
    其定义方式如下:

    • 匿名结构体在声明和定义的同时创建,不需要使用结构体名称。可以直接在需要的地方定义结构体,并在大括号 {} 内部定义结构体的成员。
    struct {
        // 成员定义
    } 结构体变量名;
    
    • 1
    • 2
    • 3
    • 注意:
      匿名结构体无法在不同的地方重复使用,因为没有结构体名称来引用它。如果需要在多个地方使用相同的结构体结构,建议定义具名的结构体类型。

    1.4 结构体的自引用

    结构体的自引用是指结构体中包含自身类型的成员。这种自引用的结构体通常用于创建复杂的数据结构,如树、链表和图等。在C语言中,要实现结构体的自引用,通常需要使用指针来解决
    以下代码,展示了如何创建一个简单的自引用结构体来表示链表节点:

    #include 
    
    // 定义一个链表节点的结构体
    struct ListNode 
    {
        int data;             // 节点数据
        struct ListNode* next; // 指向下一个节点的指针
    };
    
    int main() 
    {
        // 创建链表节点
        struct ListNode node1, node2, node3;
        node1.data = 1;
        node2.data = 2;
        node3.data = 3;
    
        // 连接节点,形成链表
        node1.next = &node2;
        node2.next = &node3;
        node3.next = NULL; // 链表结束
    
        // 遍历链表并打印节点数据
        struct ListNode *cur = &node1;
        while (cur != NULL) 
        {
            printf("%d ", cur->data);
            cur = cur->next;
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

    在C语言中,结构体内存对齐是编译器为了提高内存访问效率而采取的一种优化策略。关于结构体内存对齐的详细介绍,这里可以参考之前写的文章:如何计算一个结构体的大小?(C语言)
    这里就不再赘述了。

    1.6 结构体传参

    在C语言中,我们可以将结构体作为参数传递给函数。结构体传参的方式有两种:传值传递和传址传递(使用指针)。

    1.6.1 传值传递

    在传值传递时,函数形参是实参的一份临时拷贝,这也就意味着在函数内部操作的是结构体的一份拷贝,不会影响实参结构体。这种方式适用于结构体较小且复制开销较小的情况。这是因为函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
    以下代码,展示了结构体传值传递的方式

    #include 
    
    // 定义一个结构体
    struct Point
    {
        int x;
        int y;
    };
    
    // 以传值传递的方式
    void modify_point(struct Point p) 
    {
        p.x = 10;
        p.y = 20;
    }
    
    int main() 
    {
        struct Point p = {5, 5};
        modify_point(p);
        printf("x: %d, y: %d\n", p.x, p.y); // 输出仍为原始值
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    1.6.2 传址传递(使用指针)

    在传址传递时,函数接收的是结构体的指针,可以通过指针修改实参结构体的内容。这种方式适用于当结构体较大且复制开销较大的情况。

    #include 
    
    // 定义一个结构体
    struct Point 
    {
        int x;
        int y;
    };
    
    // 以传址传递的方式
    void modify_point(struct Point *p) 
    {
        p->x = 10;
        p->y = 20;
    }
    
    int main() 
    {
        struct Point p = {5, 5};
        modify_point(&p);
        printf("x: %d, y: %d\n", p.x, p.y); // 输出已修改后的值
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2. 位段

    2.1 什么是位段?

    位段是一种C语言的结构体成员,它可以在结构体定义时,指定某个成员变量所占用的二进制位数,这是因为有些数据在存储时并不需要占用一个完整的字节,而是需要占用一个或几个二进制位即可
    位段的主要目的是节省内存
    其声明语法如下:

    struct 
    {
        type member_name : width;
    };
    
    • 1
    • 2
    • 3
    • 4
    • type 表示要使用的整数类型(通常是 int、unsigned int 、char(整型家族)等),在C99之后也可以是其他类型。
    • member_name 是位段的名称。
    • width 是该位段占用的比特位数。

    以下是一个位段的示例:

    #include 
    
    // 定义一个包含位段的结构体
    struct A
    {
    	int _a:2;//2个比特位
    	int _b:5;//5个比特位
    	int _c:6;//10个比特位
    	int _d:8;//30个比特位
    };
    int main() 
    {
        struct A a;
    
        // 设置位段的值
        a._a = 1;
        a._b = 0;
        a._c = 5;
        a._d = 8;
    
        printf("_a: %d\n", a._a);
        printf("_b: %d\n", a._b);
        printf("_c: %d\n", a._c);
    	printf("_d: %d\n", a._d);
    	
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.2 位段的内存分配

    位段会按照其在结构体中的声明顺序依次分配内存

    • 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型。
    • 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
    • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

    我们通过下面的例子,探究以下位段在vs2013这个平台如何开辟空间的?

    #include 
    
    struct S
    {
    	char a:3;
    	char b:4;
    	char c:5;
    	char d:4;
    };
    
    int main()
    {
    	struct S s = {0};
    	s.a = 10;
    	s.b = 12;
    	s.c = 3;
    	s.d = 4;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里先假设在一个字节内部先使用低比特位且当一个字节剩余的bit位不足以存下下一个变量时,舍弃剩余的bit位,为下一个变量重新申请一个字节。
    在这里插入图片描述
    我们打开vs,进行调试,看下内存里面是如何存储的。
    在这里插入图片描述
    在vs下,具体的内存布局和我们假设的一致。

    2.3 位段的跨平台问题

    • int 位段被当成有符号数还是无符号数是不确定的。
    • 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
      器会出问题。
    • 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
    • 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
      舍弃剩余的位还是利用,这是不确定的。
      在这里插入图片描述
      因此,跟结构相比,位段可以达到同样的效果,虽然可以很好的节省空间,但是有跨平台的问题存在。

    3. 枚举

    枚举是C语言中的一种自定义数据类型,用于定义一组命名的整数常量,这些常量通常用于表示一组相关的离散值。

    3.1 枚举的定义

    其基本语法如下:

    enum enum_name 
    {
        a,
        b,
        c,
        // ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • num_name 是枚举类型的名称。
    • a, b, c等是枚举常量,它们是整数值,通常从0开始自动递增。当然在定义的时候也可以赋初值。

    以下代码展示了枚举的使用:

    #include 
    
    // 定义一个枚举类型
    enum Color 
    {
        RED,    // 0
        GREEN,  // 1
        BLUE    // 2
    };
    
    int main() {
        // 声明一个枚举类型的变量
        enum Color col;
    
        // 给枚举变量赋值
        col = RED;
    
        // 使用枚举常量
        if (col == RED) 
        {
            printf("The color is red.\n");
        } 
        else if (col == GREEN) 
        {
            printf("The color is green.\n");
        } 
        else if (col == BLUE) 
        {
            printf("The color is blue.\n");
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    3.2 枚举的好处

    枚举常量在整个程序中具有全局作用域,可以用作整数值的符号名称,以提高代码的可读性。

    • 增加代码的可读性和可维护性
    • 和#define定义的标识符比较枚举有类型检查,更加严谨。
    • 防止了命名污染(封装)
    • 便于调试
    • 使用方便,一次可以定义多个常量

    4. 联合(共用体)

    联合也是C语言中的一种自定义的数据类型,与结构体类似,但与结构体不同的是:

    • 联合的成员共用同一块内存空间,只能同时存储其中一个成员的值。这也说明联合的大小虽少是其最大成员的大小。
    • 由于联合的成员共用同一块内存空间,因此在给一个成员赋值后,其他成员的值可能会受到影响。

    下面代码的运行结果说明了其特性:

    #include 
    
    //联合类型的声明
    union Un
    {
    	char c;
    	int i;
    };
    
    int main()
    {
    	
    	//联合变量的定义
    	union Un un;
    	// 下面输出的结果是一样的吗?
    	printf("&(un.i) = %d\n", &(un.i));
    	printf("&(un.c) = %d\n", &(un.c));
    
    	//下面输出的结果是什么?
    	un.i = 0x11223344;
    	un.c = 0x55;
    	printf("%x\n", un.i);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    分析如下:
    在这里插入图片描述

    4.1 联合类型的定义

    联合的基本语法如下:

    union union_name 
    {
        // 成员定义
    };
    
    • 1
    • 2
    • 3
    • 4
    • union_name 是联合类型的名称。

    4.2 联合大小的计算

    其大小的计算规则遵循以下两个规则:

    • 联合的大小至少是最大成员的大小。
    • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

    思考一下,下面代码的运行结果是什么?

    #include 
    union Un1
    {
    	char c[5];
    	int i;
    };
    
    union Un2
    {
    	short c[7];
    	int i;
    };
    
    int main()
    {
    	//下面输出的结果是什么?
    	printf("%d\n", sizeof(union Un1));
    	printf("%d\n", sizeof(union Un2));
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    分析如下:
    在这里插入图片描述
    Un2的分析和上述分析一致,Un2的大为16。
    运行结果如下:
    在这里插入图片描述
    这里需要注意的是,不同编译器和平台对于联合的对齐方式可能会有所不同,这可能会影响联合的总大小。但无论如何,联合的大小都不会小于最大成员的大小。

    至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
    创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
    如果本篇博客有任何错误,请批评指教,不胜感激 !
    在这里插入图片描述

  • 相关阅读:
    安卓开发之性能优化
    FPGA高端项目:图像采集+GTX+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持
    1108 String复读机分数 20
    计算机网络---UDP协议
    软件设计师考试学习1
    10.6黄金还收官会跌吗?非农如何稳健布局?
    django的使用步骤详细
    Babel 插件:30分钟从入门到实战
    .NET数据交互之生成和读取YAML文件
    药品研发--质理研究人员绩效办法
  • 原文地址:https://blog.csdn.net/m0_50655389/article/details/133754793