• C语言学习系列—>一篇带你了解结构体


    在这里插入图片描述

    前言

    结构体是C语言中自定义类型之一,当内置类型不能满足的时候,我们就可以使用自定义类型,在后续数据结构的学习过程中会遇到很多关于结构体的内容,所以,小编将在学习结构体时的笔记分享一番。

    结构体类型

    概述

    结构体是一个集合,里面的成员变量可以是不同类型的。

    声明

    struct tag  //tag是标签
    {
    member-list;   //成员列表
    }variable-list;  //变量名称
    
    • 1
    • 2
    • 3
    • 4

    code

    struct Stu
    {
    	char name[20];   //名字
    	int age;         //年龄
    	float scr;       //分数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    特殊声明

    声明结构体的时候,可以不完全声明:

    //匿名结构体类型
    struct
    {
    	int a;
    	char b;
    	float c;
    }x;
    struct
    {
    	int a;
    	char b;
    	float c;
    }a[20], 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结构体的自引用

    结构体的自引用:在结构体里面包含一个为该结构体本身的成员。

    比如,定义一个链表结点:

    struct Node
    {
    	int data;
    	struct Node* next;
    };
    
    
    //错误做法:
    struct Node
    {
    	int data;
    	struct Node next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    为什仫错误做法是错误做法?
    这里的next是同一结构体类型中的next,next中又有一个next,无限套娃,是不行的。
    正确的自引用是,在结构体声明里面包含一个结构体类型的指针。

    注意!!

    //错误:
    typedef struct Node
    {
    	int data;
    	Node* next;
    }Node;
    
    //正确:
    typedef struct Node
    {
    	int data;
    	struct Node* next;
    
    }Node;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使⽤Node类型来创建成员变量,这是不⾏的。

    结构体变量的创建和初始化

    声明的同时定义变量为S1

    struct S
    {
    	int x;
    	int y;
    }S1;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    单独利用类型定义变量

    struct S
    {
    	int x;
    	int y;
    };         //声明结构体
    
    struct S S2;  //定义全局结构体变量
    
    int main()
    {
    	struct S S3;     //定义一个局部结构体变量
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结构体初始化

    struct S
    {
    	int x;
    	int y;
    }s1={0,0};
    
    struct S s2 = {1,2}; //初始化
    
    int main()
    {
    	struct S s3 = {3,4};//初始化
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结构成员访问操作符

    结构成员访问操作符有两个⼀个是 . ,⼀个是 -> .

    形式:

    结构体变量.成员变量名
    结构体指针—>成员变量名
    
    • 1
    • 2

    code

    #include 
    #include 
    struct Stu
    {
    	char name[15];//名字
    	int age;
    	//年龄
    };
    void print_stu(struct Stu s)
    {
    	printf("%s %d\n", s.name, s.age);
    }
    void set_stu(struct Stu* ps)
    {
    	strcpy(ps->name, "李四");
    	ps->age = 28;
    }
    int main()
    {
    	struct Stu s = { "张三", 20 };
    	print_stu(s);
    	set_stu(&s);
    	print_stu(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
    • 23
    • 24
    • 25

    输出结果

    张三 20
    李四 28
    
    • 1
    • 2

    结构体内存对齐

    code

    #include
    
    struct S1
    {
    	char a;
    	int c;
    	char b;
    };
    
    struct S2
    {
    	char a;
    	char b;
    	int c;
    };
    
    int main()
    {
    	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

    输出结果

    12
    8
    
    • 1
    • 2

    为什么呢??

    ⾸先得掌握结构体的对⻬规则:

    1. 结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处
    2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
      对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
    • VS中默认的值为8
    • Linux中没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
    1. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
    2. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

    对上述代码内存进行解析:

    struct S1:

    在这里插入图片描述
    这里为struct S1开辟一块空间

    首先给char a开辟空间,char a是结构体第一个成员,根据规则:结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处,即,图中绿色位置

    接下来,为第二个成员,int c开辟空间,根据规则: 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。 这里的c是一个整型变量,自身大小为4,小编编译器是VS2019,默认对齐数为8,根据规则:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。 所以,c的对齐数为4。偏移量1,2,3都不是4的倍数,因此从4开始,开辟4个字节,即图中深红色的位置。

    最后为 char b开辟空间,b是一个字符类型变量,自身大小为1,编译器的默认对齐数是8,和开辟 int c 一样,因此b的对齐数是1,偏移量8就是1的倍数,因此从8开始,开辟1个字节,即图中蓝色位置。

    从0~8一共,此时结构体9个字节,根据规则:结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。所以需要再浪费几个空间,浪费到偏移量为11时,此时刚好开辟了12(12是4的倍数)个字节。

    struct S2:
    在这里插入图片描述
    这里为struct S1开辟一块空间

    首先给char a开辟空间,char a是结构体第一个成员,根据规则:结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处,即,图中绿色位置

    接下来,为第二个成员,char b 开辟空间,根据规则: 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。,这里的b是一个字符类型变量,自身大小为1,VS默认对齐数为8,因此对齐数是1,偏移量1是1的倍数,从1开始开辟1个字节,即图中蓝色位置。

    最后为 int c开辟空间,c是整型变量,自身大小为4,VS默认对齐数为8,因此对齐数为4,根据规则: 对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。 偏移量2,3都不是4的倍数,从偏移量4开始,开辟4个字节。

    此时,struct S2 开辟了8个字节,8是4的倍数,因此不需要再浪费空间了。

    内存对齐的原因

    参考资料:

    1. 平台原因(移植原因):
      不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2. 性能原因:
      数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地
      址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。

    总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

    如何既要满足内存对齐又要节省空间??

    让占用空间小的成员在一起

    例如:

    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    //写成:
    struct S2
    {
    	char c1;
    	char c2;
    	int i;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别

    修改默认对齐方式

    #pragma 这个预处理指令,可以改变编译器的默认对⻬数。

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

    输出结果

    6
    
    • 1

    在这里插入图片描述
    此时VS默认对齐数为1,int i 的自身大小为4,根据规则:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。 因此,对齐数为1,偏移量1是1的倍数,和上面的一个代码就不一样了。

    结构体传参

    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

    ⾸选print2函数。

    原因:
    函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
    如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

    结论:
    结构体传参的时候,要传结构体的地址。

    在这里插入图片描述

  • 相关阅读:
    玩家们最关心的绝地求生游戏作战秘籍,分享大神干货,助您轻松提升战斗力!
    LC15.三数之和、LC22括号生成
    Java基础:Stream流和方法引用
    【SpringCloud】服务注册与发现(01)-Eureka
    快速查找qt pro文件中的用qmake language写的库函数
    Jest和Mocha两者之间有哪些区别?
    SpringBoot+Vue实现excel导入带格式化的时间参数(moment格式化明天日期并设置el-date-picker默认值)
    Springboot 配置线程池创建线程和配置 @Async 异步操作线程池
    力扣思路题:丑数
    (附源码)app学生社团管理系统 毕业设计 191850
  • 原文地址:https://blog.csdn.net/weixin_73397765/article/details/132947310