• C语言自定义类型(上)


    大家好,我们又见面了,这一次我们来学习一些C语言有关于自定义类型的结构。
    在这里插入图片描述

    目录

    1.结构体
    2位段

    1.结构体

    前面我们已经学习了一些有关于结构体的知识,现在我们进行深入的学习有关于它的知识。

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

    结构体的声明

    struct tag
    {
    member-list;
    }variable-list;
    
    • 1
    • 2
    • 3
    • 4

    我们要注意的是结构体的关键字是struct,后面的就是我们自己定义的,而括号里面的叫做成员变量,它可以是任意类型的。例如我们自己创造一个学生结构体:stu就是我们创建的结构体变量,而名字,年龄,性别,学号就是成员变量。

    struct Stu
    {
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
    }; //分号不能丢
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    匿名结构体

    struct 
    {
    	char neme[20];
    	int age;
    	char sex[5];//一个汉字2个字符
    	float score;
    }s1,s2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如上代码所示,可以去掉结构体的名字 匿名结构体类型,但只能用一次,后面再想定义变量不可以(只可以使用s1和s2)

    结构体的自引用

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

    让我们看到下面这一段代码:

    struct Node
    {
    int data;
    struct Node next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果我们要算结构体的大小这段代码能够实现吗?答案是否定的,因为我们这个结构体中包含着下一个结构体,我们要计算结构体大小的时候不仅是整型的大小还要加上下一个结构体的大小,那么下一个结构体中又包含了一个结构体是无法计算的,编译器会报错。那么正确的自引用是什么的样的呢?

    正确的自引用:

    struct Node
    {
    int data;
    struct Node* next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个代码可以运行,用指针的话,指针存放下一个节点的地址,指针本身的大小也是固定的,所以可以计算出来。

    结构体变量的定义和初始化

    结构体的定义:

    struct Point
    {
    int x;
    int y;
    }p1; //声明类型的同时定义变量p1
    
    struct Point p2; //定义结构体变量p2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结构体的初始化:

    struct Point p3 = {x, y};
    
    
    struct Stu //类型声明
    {
    char name[15];//名字
    int age; //年龄
    };
    struct Stu s = {"zhangsan", 20};//初始化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    结构体内存对齐

    1. 第一个成员在与结构体变量偏移量为0的地址处。
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8
    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    这里我们定义两个结构体变量:

    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    struct S2
    {
    	char c1;
    	char c2;
    	int i;
    };
    int main()
    {
    	/*printf("%d\n", offsetof(struct S2, c1));
    	printf("%d\n", offsetof(struct S2, c2));
    	printf("%d\n", offsetof(struct S2, i));*/
    
    	printf("%d\n", sizeof(struct S1));
    	printf("%d\n", sizeof(struct S2));
    
    	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个字节,一个字符型是一个字节,那么这两个结构体的大小是不是6个字节呢?

    在这里插入图片描述
    很显然是不对的,那为什么会造成这个结果呢?那是因为其中成员对于起始位指定的偏移量造成的,这里我们就要了解一个宏,offsetof这个宏就是专门来计算偏移量的。

    #include
    #include 
    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    struct S2
    {
    	char c1;
    	char c2;
    	int i;
    };
    
    int main()
    {
    	printf("%d\n", offsetof(struct S1, c1));
    	printf("%d\n", offsetof(struct S1, c2));
    	printf("%d\n", offsetof(struct S1, i));
    	printf("%d\n", offsetof(struct S2, c1));
    	printf("%d\n", offsetof(struct S2, c2));
    	printf("%d\n", offsetof(struct S2, i));
    	/*printf("%d\n", sizeof(struct S1));
    	printf("%d\n", sizeof(struct S2));*/
    
    	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

    在这里插入图片描述
    在这里我们看到每个成员变量对于初始位置的偏移量都是不同的,这就是因为内存对齐导致的。那么我们有什么办法来计算结构体的大小呢?
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    这里以s2为例我们看到c1是一个字节和vs默认的八个对齐数,所以c1的对齐数就是1,第一个成员的偏移量为0,所以c1就在下标为0的位置,而i是四个字节和八个字节相比,对齐数是4,因为下标4才是4的倍数,所以我们的i从下标为4的位置开始储存,而c2的对齐数为1,所以它的偏移量为i的倍数就直接放在i的后面,因为结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,最大是4,所以是12.

    那么我们为什么要存在内存对齐呢?

    1. 平台原因(移植原因):
      不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2. 性能原因:
      数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
      总体来说:
      结构体的内存对齐是拿空间来换取时间的做法。那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。

    修改默认对齐数

    #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

    看到我们的代码:

    #include 
    #pragma pack(8)//设置默认对齐数为8
    struct S1
    {
    char c1;
    int i;
    char c2;
    };
    #pragma pack()//取消设置的默认对齐数,还原为默认
    
    #pragma pack(1)//设置默认对齐数为1
    struct S2
    {
    char c1;
    int i;
    char c2;
    };
    #pragma pack()//取消设置的默认对齐数,还原为默认
    int main()
    {
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述
    这里我们将默认对齐数改了之后所得出来的结果就明显的不同了,根据我们使用的方法计算结果就是12和6。

    结构体传参

    struct S
    {
    int data[1000];
    int num;
    };
    struct S s = {{1,2,3,4}, 1000};
    //结构体传参
    void print1(struct S s)
    {
    printf("%d\n", s.num);
    }
    //结构体地址传参
    void print2(struct S* ps)
    {
    printf("%d\n", ps->num);
    }
    int main()
    {
    print1(s); //传结构体
    print2(&s); //传地址
    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.位段的成员必须是 int、unsigned int 或signed int 。
    2.位段的成员名后边有一个冒号和一个数字。

    例如:

    struct A
    {
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么我们位段的大小是多少呢?

    struct A
    {
    	int _a : 2;
    	int _b : 5;
    	int _c : 10;
    	int _d : 30;
    };
    
    int main()
    {
    	printf("%d\n", sizeof(struct A));
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    这里我们看到程序运行的结果是8,而我们根据自己的计算得到结构体占了47个比特位,而每个字节占了8个比特位,我们换算出来就是将近6个字节,那么为什么不是6个字节呢?这就要了解它的内存分布了。

    位段的内存分配

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

    在这里插入图片描述

    我们定义的位段它的类型的是char型,而我们位段的内存分配是一个字节一个字节申请的,我们先拿到一个字节的空间,首先我们要给它三个比特位,而且是从低地址到高地址分配的,我们的a是等于10的,它的二进制的前三位是010放到这个分配的字节中,而后面也是一样b在这个字节中占了4个比特位,然后这里只剩下一个比特位就放不下了,就再申请一个字节的空间,这个里面就拿来存放c的二进制,而我们看到这个字节的空间就剩下了三个比特位了,而d还要四个比特位的空间,所以我们得再申请一个字节的空间。所以这个位段的大小就是三个字节,我们在给二进制换算的十进制,所以这三个字节的内存就是62 03 04。

    位段的跨平台问题:

    1. int 位段被当成有符号数还是无符号数是不确定的。
    2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
    3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
    4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

    总的来说:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

    好了今天的学习就到这里,我们下次再见了。

  • 相关阅读:
    《算法竞赛进阶指南》 双端队列
    docker容器常用命令
    MyISAM和InnoDB的对比
    注册 ,登录, 注销功能实现 springBoot集成redis实现token ——yml配置逻辑删除
    Shell常用脚本:Nexus批量上传本地仓库增强版脚本(强烈推荐)
    代码随想录算法训练营第四十八天|121. 买卖股票的最佳时机 122.买卖股票的最佳时机II
    漫谈:C语言 C++ 迷惑的语句、分号、大括号
    SpringCloud Gateway用法详解
    Linux计划任务at和cron命令的使用
    c++ vector的模拟实现以及迭代器失效问题
  • 原文地址:https://blog.csdn.net/Lehjy/article/details/133245676