• C语言自定义类型【结构体】


    结构体的概念

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

    1.结构体的声明

    1.1普通声明

    我们假设要创建一本书的类型,那我们需要书名,作者,价格,书的ID
    代码如下:

    struct Book
    {
    	char BName[20];//书名
    	char Author[20];//作者
    	float Price;//价格
    	char BId;//书的ID
    }Book;//分号前的名字可以省略,但分号不能省略
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.2结构体的初始化

    struct Book
    {
    	char BName[20];
    	char Author[20];
    	float Price;
    	char BId;
    };
    
    //结构体的初始化方式
    int main()
    {
    	struct Book b1 = { "C语言程序设计" , "张三", 29.9,"B100001" };//按照结构体的内部顺序初始化
    	struct Book b2 = { .Price = 59.9, .BId = "B100002", .Author = "李四" ,.BName = "C语言进阶" };
    	//		也可以乱序来初始化,但格式为 成员变量.初始化值
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    1.3结构体的特殊声明

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

    //匿名结构体类型基本上只能使用一次
    struct 
    {
    	char c;
    	int i;
    	float f;
    	double d;
    }s = {'x',100,3.1f,3.14};
    int main()
    {
    	struct s;//error(这是错误的)
    	//需要将上面代码删除或屏蔽
    	printf("%c %d %f %lf", s.c, s.i, s.f, s.d);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    那我们如果想让他能够重复使用该怎么办呢?
    我们可以用 typedef 对匿名结构体进行重命名

    typedef struct
    {
    	char c;
    	int i;
    	float f;
    	double d;
    }s;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但没有意义,我匿名了又给他取个名字,这就是饶了一圈又回到了普通声明了
    这就有点多此一举了,还不如直接用普通声明呢。

    1.4结构体的自引用

    结构体内部包含一个自己类型的成员可以吗?
    例如:定义一个链表的节点

    #define NODEDATA int//给int起一个别名
    typedef struct Node
    {
    	NODEDATA data;
    	struct Node next;
    }Node;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个正确吗?
    其实是不正确的,
    仔细看就能发现⼀个结构体中再包含⼀个同类型的结构体变量
    这样结构体变量的大小就会无穷的⼤
    正确的自引用方式:

    #define NODEDATA int
    typedef struct Node
    {
    	NODEDATA data;
    	struct Node* next;
    }Node;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在结构体自引用使用的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引出问题,看看下面的代码,看他是否可行:

    #define NODEDATA int
    typedef struct
    {
    	NODEDATA data;
    	Node* next;
    }Node;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样可以吗
    答案肯定是不行的
    因为Node是typedef对这个匿名结构体进行重命名而产生的
    但是在匿名结构体内部提前使用Node类型来创建成员变量是不行的
    解决方式:定义结构体的时候不使用匿名结构体

    #define NODEDATA int
    typedef struct Node
    {
    	NODEDATA data;
    	struct Node* next;
    }Node;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.结构体的内存对齐

    先看代码:

    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    int main()
    {
    	struct S1 s1 = { 0 };
    	
    	printf("S1大小为:%d\n", sizeof(s1));
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    看看这段代码中s1的大小为多少?
    答案是6吗(c1 占一个字节,i 占四个字节, c2 占一个字节)
    其实是12
    在这里插入图片描述

    那为什么是12呢,我们就需要知道结构体内存对齐的概念了 (这也是一个热门的考点)

    2.1内存对齐的规则

    1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
    2.其他成员变量要对齐某个数字(对齐数)的整数倍的地址处
    对齐数 = = 编译器默认的一个对齐数 与 该成员变量的大小 进行比较得出的较小值
    VS中的默认对齐数是8
    Linux中gcc编译器是没有默认对齐数的,对齐数就是成员本身的大小
    3.结构体的总大小为成员变量中对齐数最大的整数倍
    4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

    那我们来看看为什么上面的代码结果会是12吧

    在这里插入图片描述
    在这里插入图片描述

    2.2为什么要有内存对齐

    大部分参考资料是这样说的

    1.平台原因(移植原因)

    不是所有的硬件平台都可以访问任意地址上的任意数据;某些硬件平台只能在某些地址取某些特定类型的数据,否者会抛出硬件异常

    2.性能原因

    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要进行两次内存访问;而已对齐的内存只需要进行一次访问。

    假设一个处理器总是从内存中取8个字节,如果我们能保证所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读/写值了。否者,我们可能需要进行两次内存访问才能拿到一个完整的double类型的数据,因为对象可能被放在两个8字节的内存中。

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

    那我们在设计结构体的时候,然后满足对齐,又节省空间呢?
    解决方法:将小的类型尽量聚集在一起

    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    struct S2
    {
    	char c1;
    	char c2;
    	int i;
    };
    
    int main()
    {
    	struct S1 s1 = { 0 };
    	struct S2 s2 = { 0 };
    
    	printf("S1大小为:%d\n", sizeof(s1));
    	printf("S2大小为:%d\n", sizeof(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

    在这里插入图片描述

    2.3修改默认对齐数

    使用#pragma这个预处理指令,可以修改编译器的默认对齐数

    #pragma pack(1)//将默认对齐数改为1
    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    int main()
    {
    	printf("S1大小为%d\n", sizeof(struct S1));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    #pragma pack()//不输入就改回原本的默认对齐数
    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    int main()
    {
    	printf("S1大小为%d\n", sizeof(struct S1));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    当结构体在对齐方式不合适的时候,我们就可以自己修改默认对齐数

    3.结构体传参

    先看代码:

    struct S1
    {
    	int data[1000];//4000个字节的大小
    	char c1;//1
    };
    
    struct S1 s1 = { {1,2,3,4,5,6,7,8,9,10},'A'};
    
    void print1(struct S1 s1)
    //这里的参数其实是s1的临时拷贝,会复制一个和s1大小相同的空间(4004个字节)
    {
    	for (int i = 0; i < 10; i++)
    	{
    		printf("%d ", s1.data[i]);
    	}
    	printf(" %c", s1.c1);
    }
    
    void print2(struct S1* s)
    //这里的参数是接收s1地址的指针变量,指针变量的大小就8/4个字节
    {
    	for (int i = 0; i < 10; i++)
    	{
    		printf("%d ", s->data[i]);
    	}
    	printf(" %c", s->c1);
    }
    int main()
    {
    	print1(s1);//传结构体
    	printf("\n");
    	print2(&s1);//传地址
    }
    
    • 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

    代码中的print1和print2函数哪个好?
    答案是print2函数

    原因

    1.在函数传参的时候,参数是需要压栈的,会有时间和空间的开销
    2.如果在传递一个结构体对象的时候,结构体过大,参数压栈的系统开销就比较大,会导致性能的下降

    结论:结构体传参的时候,最好传结构体的地址

    4.结构体实现位段

    4.1什么是位段

    位段的声明和结构类似,但是有两个不同

    1.位段的成员必须是int、unsigned int或signed int,但在C99标准中位段成员的类型也可以是其他类型。
    2.位段的成员名后面一定要跟着一个冒号(:)和一个数字,具体为–> type name:number;

    //结构体的位断
    struct Str
    {
    	int a : 2;
    	int b : 1;
    	int c : 16;
    	int d : 16;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意:位段的单位是bit位
    我们来猜猜他的大小,正常来说一个int类型占4个字节的空间,但是位段后的单位都是bit位了,所以a占2个bit位,b占1个bit位,c和d都占16个bit位,一共是35个bit,按理来说6个字节就能存放了,但事实是不是这样呢?
    我们来看看运行结果吧
    在这里插入图片描述
    为什么会是8呢?
    这就需要了解位段在内存中的分配了

    4.2位段的内存分配

    1.位段的成员可以是int家族和char类型
    2.位段的空间是根据需求,一次以4个字节(int)或1个字节(char)开辟的
    3.位段涉及很多不确定因素,位段是不跨平台的,重点在可移植的程序应该避免使用位段

    现在我们来看看为什么上面代码的结果是8吧
    如图
    在这里插入图片描述
    由于剩余的空间不够存放d,VS会再开辟一个int类型大小的空间用来存放d,这样大小就来到了8个字节(2个int的大小)

    代码2

    下面代码也是关于位段在内存的分配我们来看看吧

    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;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    4.3位段的跨平台问题

    前面说到了位段是不跨平台的,为什么不跨平台呢,我们来看看原因吧

    1.在不同的环境下,int位段被当成有符号还是不符号是不确定的
    2.位段中最大位的数目不确定,(早期16位机器的int类型大小为2个字节,32位和64位机器上int大小为4个字节),所有如果位段为16以上,在16位机器就会出现问题

    3.位段中的成员在内存中是从左向右分配还是从右向左分配的标准是未定义的(VS是从右向左)
    4.当一个结构体包含两个位段,第二个位段成员比较大,第一个位段后剩余的位无法容纳第二个位段时,是舍弃剩余的位还是利用,这是未定义的(VS是舍弃)

    总结:与结构相比,位段可以达到同样的效果,同时也能很好的节省空间,但是有跨平台的问题存在(当你想要多平台使用且节约时间可以使用结构,当你想要节省空间可以使用位段)
    解决方式:根据不同的平台写不同的代码(这样会比较麻烦)

    4.4位段使用时的注意事项

    有时候位段的几个成员会共用一个字节,这样有些成员的起始位置并不是字节的起始位置,那么这些位段是没有地址的。(内存会给每个字节分配一个地址,但字节内的bit位是没有地址的)
    所以不能对位段的成员使用 &操作符,这样就不能用scanf直接给位段的成员输入值,只能是先输入到一个变量里,再将变量赋值给位段的成员

    struct A
    {
    	int _a : 2;
    	int _b : 5;
    	int _c : 10;
    	int _d : 30;
    };
    int main()
    {
    	struct A sa = { 0 };
    	scanf("%d", &sa._b);//这是错误的
    
    	//正确的⽰范
    	int b = 0;
    	scanf("%d", &b);
    	sa._b = b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    结语

    最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言。如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢

  • 相关阅读:
    初识 WebSocket
    Java 简易版王者荣耀
    Vue3 - 组件通信(父传子)
    MySQL主从复制-读写分离
    算法每日一题(python,2024.05.27) day.9
    EMC学习笔记(二)模块划分及特殊器件的布局
    【C++ 程序】级数求和
    Sentinel微服务限流、熔断、降级基本用法(二)
    Qt (QInputDialog 、QMessageBox、QMessageBox)对话框实战
    Spring Cloud
  • 原文地址:https://blog.csdn.net/2301_80216111/article/details/138168504