C语言 【自定义类型——结构体】(详细)

news2024/9/20 8:00:25

目录

1、结构体的定义

2、创建与初始化结构体变量

2.0 举例

2.1 结构体的特殊声明

2.1.0 匿名结构体

 2.1.1 结构体的自引用

3、结构体内存对齐

3.0 为什么要内存对齐

3.1 对齐规则

3.2 如何修改默认对齐数

4、结构体传参

5、结构体中的位段使用

5.0 什么是位段?

5.1 位段的内存分配

5.2 位段的跨平台问题

5.3 位段的应用

5.4 位段使用的注意事项


1、结构体的定义

之前我们学的数组是一些值的集合,而结构也是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

结构体的语法结构:

struct tag//结构体名称
{
    member-list;//成员列表
    
}variable-list;//变量列表(全局变量)

比如我们要描述一本书,有作者、书名、价格、卡号等。

struct Book
{
	char book_name[20]; // 书名
	float price; // 价格
	char id[18]; // 书号
	char author[15]; // 作者
}; // 分号不能漏


2、创建与初始化结构体变量

2.0 举例

✅代码:

struct Book
{
	char book_name[20]; // 书名
	float price; // 价格
	char id[18]; // 书号
	char author[15]; // 作者
}b3,b4,b5; // 分号不能漏 //b3 b4 ,b5 也是结构体变量

int main()
{
	struct Book b1 = { "结构体",33.3f,"DF202408012","等风来" };// 按顺序初始化
	struct Book b2 = { .author = "随风扬",.book_name = "联合体",.price = 55.5f,.id = "SF202408013" }; //不按顺序初始化
	printf("%s %f %s %s\n", b1.book_name, b1.price, b1.id, b1.author); // ->
	printf("%s %f %s %s\n", b2.book_name, b2.price, b2.id, b2.author);
	return 0;
}

运行结果如下:

第一个结果有点误差说明,浮点数在内存中有可能是不能精确保存的

2.1 结构体的特殊声明

2.1.0 匿名结构体

匿名结构体,顾名思义,就是在声明结构体时没有给结构体指定一个标签(tag)名。这种结构体类型只能在它被声明的地方直接使用,因为它没有名字,所以无法在其他地方引用。

在声明结构的时候,可以不完全的声明。

//匿名结构体类型
struct // 这个类型没有名字
{
	int a;
	char b;
	float c;
}s1; // 但是用这个类型创建了一个变量

struct
{
	int a;
	char b;
	float c;
}* p;  // 匿名结构体指针类型,ps为指针变量

在上面的基础上,加上

int main()
{
	ps = &s1; // ? 这种方式不予许,类型不兼容
	return 0;
}

会出现警告:

编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

 2.1.1 结构体的自引用

链表在数据结构中会学,这里先用一下。

定义一个链表的节点:

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

上面的代码是错误的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。

正确自引用写法:

struct Node 
{
    int data; // 存放数据 -- 数据域  
    struct Node* next; // 指针域,指向下一个节点  
};
  
// 或者使用typedef简化类型名  
typedef struct Node {  
    int data;  
    struct Node* next;  
} Node;

看看下面的代码,可行吗?

typedef struct Node
{
    int data;
    Node* next; // 省略了Node
} Node; // struct Node 重命名为Node

答案是不行的,虽然Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量是不行的。解决方案上面已经给出,就是在Node* next; 改为struct Node* next; 


3、结构体内存对齐

3.0 为什么要内存对齐

原因:

1. 平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;硬件平台通常对数据访问有严格的地址对齐约束。这意味着,特定类型的数据(如整数、浮点数等)必须被存储在符合其类型大小边界的内存地址上。若尝试在不满足这些对齐要求的地址上访问这些数据,硬件可能会拒绝执行该操作,导致程序崩溃或执行异常

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

-> 对于总以8字节为单位从内存中取数据的处理器,确保double类型数据地址对齐至8的倍数,即可通过单次内存操作完成读写,避免可能的效率损失。

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

代码:

struct S1
{
	char c1;
	char c2;
	int n;
};
	
struct S2
{
	char c1;
	int n;
	char c2;
};
	
int main()
{
	printf("%zd\n", sizeof(struct S1)); // 8
	printf("%zd\n", sizeof(struct S2)); // 12
}

上面代码的结果不是我们理所应当想象的6个字节,结果不是,为什么呢?我们先使用offsetof 看一下它们的内存偏移量。记得包含offsetof 头文件 #include<stddef.h>。offsetof是一个宏,可以计算结构体成员相较于结构体变量起始位置的偏移量 。

通过绘图分析我们解决了一些问题,同时又引入了新的问题,比如计算S2它内存时还要多出的三个内存单元(字节)才达到12,为什么呢?这就涉及到内存对齐规则的问题了。 

绘图如下:

3.1 对齐规则

1. 结构体的第一个成员总是从结构体变量内存分配的起始位置(偏移量为0的地址处)开始。如果该成员的大小小于编译器的默认对齐数,它仍然从该位置开始,但后面可能会有填充来满足后续成员的对齐要求。

2. 结构体的每个后续成员都会对齐到某个数字(“对齐数”)的整数倍地址处。

对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。

● VS 中默认的值为 8

● Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小

3. 结构体总大小会调整为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大值)的整数倍。

4. 如果结构体中包含了其他结构体作为成员(即嵌套结构体),那么这些嵌套的结构体成员也会遵循上述的对齐规则。嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

示例:

#include<stdio.h>
#include<stddef.h>
struct S1
{
	char c1;
	char c2;
	int n;

};

int main()
{
	struct S1 s1;
	return 0;
}

根据结构体内存对齐规则

首先,内存为结构体s1开辟内存空间,而结构体第一个成员char c1 从结构体偏移量为0的地址处开始,填充了一个字节;

第二个成员char c2 和默认对齐数(这里是在VS环境下)比较,取得较小值 1 ,我们 c2 要对齐到默认对齐数也就是 1 的整数倍地址处(实际上任何一处地址都是 1 的倍数)。所以 c2 直接存在 c1 后,char 占一个字节;

n 也一样,找 4 的倍数,我们发现它会浪费到两个字节空间后开始存储 int n,占四个字节空间。

总体上用了八个字节空间,8 是它们所以结构体成员中最大对齐数 4 的整数倍,不需要调整了,结果就是8

我们来看看s2 ,为什么它的结果是12呢?s2 跟上面步骤差不多,在对规则第三点处不同,刚刚 s1 的刚好就是算到 8 就是 4 的倍数,而s2 不是,s2 需要调整,将 9 调整为 4 的倍数,所以结果是12。

掌握了前面3 点对齐规则,下面的练习自己尝试做一下吧,我就不解析了。结果是 16。

#include<stdio.h>
#include<stddef.h>
struct S3
{
	double d;
	char c;
	int i;
};

int main()
{
	printf("%d\n", sizeof(struct S3)); // 16
	return 0;
}

还有第4 点对齐规则,代码如下:

#include<stdio.h>
#include<stddef.h>

// - 结构体嵌套问题
struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S4)); // 32
	return 0;
}

其实就是嵌套了一个结构体S3而已,和前面三点对齐规则结合用,在算到s3 时,它的最大对齐数为8 ,而我们前面练习已经知道了它的大小为16 ,所以在c1 后会浪费7 个字节再填充嵌套结构体s3 的16 个字节,刚好d 对齐数8 是24 的倍数,接着填充8 个字节。最后,它们总共偏移了 32 个字节,是它们所有包括嵌套结构体中成员的对齐数的最大整数8 的倍数,所以结果就是32 。

结合下图来理解更清晰:

那我们在设计结构体时如何做到对齐又尽量节省空间呢?

将占用空间小的成员尽量集中在一起,特别是将char、short等类型放在结构体开始的位置,可以减少填充字节。前面有相关代码S1和S2。

3.2 如何修改默认对齐数

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

✅结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数,一般我们设计为2 的倍数

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
	char c1; // 1 1 1
	int i;   // 4 1 1
	char c2; // 1 1 1
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S)); // 6
	return 0;
}


4、结构体传参

✅代码:

#include <stdio.h>
struct S
{
	int arr[1000];
	int n;
	char ch;

};

void Print1(struct S tmp) //结构体传参-传值调用
{
	int i = 0;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", tmp.arr[i]);
	}
	printf("\n");
	printf("n = %d\n", tmp.n);
	printf("ch = %c\n", tmp.ch);
}

void Print2(struct S *ps) //结构体地址传参
{
	int i = 0;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
	printf("n = %d\n", ps->n);
	printf("ch = %c\n", ps->ch);
}

int main()
{
	struct S s = { {1,2,3,4,5,6,7,8,9,10},10,'f' };
	Print1(s);
	Print2(&s);
	return 0;
}

打印结果:

print1 和 print2 函数,我们首选print2 函数

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

所以结构体传参的时候,要传结构体的地址


5、结构体中的位段使用

5.0 什么是位段?

定义:位段是一种通过结构体实现的数据存储结构,它可以把数据以位的形式紧凑地储存,并允许程序员对此结构的位进行操作。位段中的位指的是二进制的位(bit)。


性质:位段的成员必须是整型家族的成员(如int、unsigned int、signed int等),且每个成员后面跟有一个冒号和一个数字,该数字表示该成员在内存中占用的二进制位大小。在C99中位段成员的类型也可以选择其他类型。

✅如下面这段代码,当我们设计位段时是8 字节,当我们不设计时,为16 字节。

可见,位段的出现本质上还是节省空间

5.1 位段的内存分配

1. 位段的成员可以是 int、unsigned int、 signed int 或者是 char 等类型

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

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

✅代码:

// 例如:
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s;
	printf("%zd\n", sizeof(struct S)); // 3
	return 0;
}

1、给定了空间后,在空间内部是从左向右使用,还是从右向左使用,这个不确定。

假设:从右向左
2、当剩下的空间不足以存放下一个成员的时候,空间是浪费还是使用不确定。

假设:浪费 

 我们根据上面的假设得到的结果确实是3 ,那我们的分析是否正确呢?下面来验证一下。

✅代码:

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;
	printf("%zd\n", sizeof(struct S)); // 3
	return 0;
}

根据下图来看确实和假设一样。 

5.2 位段的跨平台问题

1、数值类型的解释不确定性
有符号与无符号:位段中的int成员被当成有符号数还是无符号数是不确定的。这可能导致在不同平台上对相同位段值的解释不一致。

2、最大位数的不确定性
位数限制:位段中成员的最大位数依赖于具体平台的位宽。

例如,在16位机器上,位段成员的最大位数可能是16,而在32位机器上可能是32。如果编写的位段成员位数超出了当前平台的限制(如声明了一个27位的int成员在16位机器上),则可能导致编译错误或运行时问题。

3、内存分配方向的不确定性
分配方向:位段中的成员在内存中的分配方向(从左到右或从右到左)尚未有统一的标准。

4、剩余位处理的不确定性
剩余位利用:当一个结构体包含两个位段,且第二个位段成员较大,无法容纳于第一个位段剩余的位时,是否舍弃剩余的位还是利用这些位是不确定的。这种不确定性可能导致数据在不同平台间的传输或存储时出现不一致。

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

5.3 位段的应用

在网络协议中,IP数据报的格式设计得非常紧凑,其中许多属性仅需要几个比特(bit)来描述。位段的应用优化了IP数据报的存储与传输,对维护网络流畅性起到了积极作用。

5.4 位段使用的注意事项

位段成员共享字节空间时,它们的起始位置可能不直接映射到内存地址的边界,而是位于字节内部的某个bit位置,这些内部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;
}

⛳ 点赞☀收藏 ⭐ 关注!

如有不足欢迎评论区指出

Respect!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2044695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

printf、fprintf、sprintf的使用和区别

printf、fprintf、sprintf的使用和区别 1、sprintf 函数 sprintf函数用于将格式化的数据写入字符串&#xff0c;其原型为&#xff1a; #include <stdio.h>/* *描述&#xff1a;将格式化的数据写入字符串 * *参数&#xff1a; * [out] str&#xff1a; 输出缓冲区…

Python聊天机器人-NoneBot2入门(2024新版)

1. NoneBot2 安装与使用 NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架&#xff08;下称 NoneBot&#xff09;&#xff0c;它基于 Python 的类型注解和异步优先特性&#xff08;兼容同步&#xff09;&#xff0c;能够为你的需求实现提供便捷灵活的支持。同时&…

煤炭检测实验室信息管理系统LIMS

在煤矿行业&#xff0c;实验室作为质量控制与技术创新的核心部门&#xff0c;其管理效率与数据准确性直接关系到企业的生产安全与经济效益。随着信息技术的飞速发展&#xff0c;实验室信息管理系统(LIMS)在煤矿行业的应用日益广泛&#xff0c;成为提升实验室管理水平、优化检测…

【动态规划,dp】P1044[NOIP2003 普及组] 栈 题解

题意 给定一个 n ( 1 ≤ n ≤ 18 ) n(1 \leq n \leq 18) n(1≤n≤18)&#xff0c;表示一个操作数序列&#xff0c; 1 , 2 , … , n 1,2,…,n 1,2,…,n&#xff08;图示为 1 到 3 的情况&#xff09;&#xff0c;栈 A 的深度大于 n n n。 现在可以进行两种操作&#xff0c; …

如何选出高品质 SD 存储卡 —— 具备高耐用度且防水防震抗冲击

SD卡&#xff08;Secure Digital Memory Card&#xff09;是一种广泛使用的存储器件&#xff0c;因其快速的数据传输速度、可热插拔的特性以及较大的存储容量&#xff0c;广泛应用于各种场景&#xff0c;例如在便携式设备如智能手机、平板电脑、运动相机等&#xff0c;用于存储…

录屏为什么录制不进去,没有声音?屏幕录制中的声音问题及解决方案

在数字时代&#xff0c;屏幕录制已成为我们日常工作和生活中不可或缺的一部分。无论是制作教学视频、记录在线课程&#xff0c;还是捕捉游戏精彩瞬间&#xff0c;一个好的屏幕录制软件都能让我们的工作更加高效&#xff0c;生活更加丰富。然而&#xff0c;许多用户在使用屏幕录…

谈一谈数据虚拟化的技术核心和应用架构

数据虚拟化&#xff08;Data Virtualization&#xff09;是对数据资源的抽象&#xff0c;通过屏蔽数据资源的存储位置和访问方式&#xff0c;能够将不同数据源、不同格式的数据资源&#xff0c;进行逻辑上的整合集成。这一技术方案与过去面对传统数仓的弊端&#xff0c;业界过去…

为什么说凤凰雪球期权是震荡市场中的稳健选择?

在当前股市波动的背景下&#xff0c;投资者会发现传统的投资策略难以适应市场的快速变化。在这样的环境下&#xff0c;一些创新的金融产品&#xff0c;如凤凰雪球&#xff0c;因其相对较高的安全性和潜在的收益性&#xff0c;逐渐受到市场的关注。 近期&#xff0c;股市呈现出…

大语言模型的简易可扩展增量预训练策略

前言 原论文&#xff1a;Simple and Scalable Strategies to Continually Pre-train Large Language Models翻译文件已整理至Github项目Some-Paper-CN&#xff0c;欢迎大家Star&#xff01; 摘要 大语言模型&#xff08;LLMs&#xff09;通常需要在数十亿个tokens上进行预训…

存储实验:华为异构存储在线接管与在线数据迁移(Smart Virtualization Smart Migration 特性)

目录 目的实验环境实验步骤参考文档1. 主机安装存储多路径2. v2存储创建Lun&#xff0c;映射给主机&#xff1b;主机分区格式化&#xff0c;写数据3. 将v2存储映射该成映射到v3存储上(v3存储和v2之间链路搭建&#xff0c;测通&#xff0c;远端设备&#xff09;&#xff08;Smar…

【深度学习】DDPM公式详解(第一期)

原论文&#xff1a;Denoising Diffusion Probabilistic Models (1)-1 p θ ( x 0 : T ) : p ( x T ) ∏ t 1 T p θ ( x t − 1 ∣ x t ) p_{\theta}(x_0:T) : p(x_T) \prod_{t1}^{T} p_{\theta}(x_{t-1} \mid x_t) pθ​(x0​:T):p(xT​)t1∏T​pθ​(xt−1​∣xt​) 这个…

AI预测福彩3D采取888=3策略+和值012路或胆码测试8月16日新模型预测第58弹

经过近60期的测试&#xff0c;当然有很多彩友也一直在观察我每天发的预测结果&#xff0c;得到了一个非常有价值的信息&#xff0c;那就是9码定位的命中率非常高&#xff0c;57期一共只错了5次&#xff0c;这给喜欢打私房菜的朋友提供了极高价值的预测结果~当然了&#xff0c;大…

IoTSharp:基于 .NET 6.0 的开源物联网平台

目录 前言 项目介绍 为什么会有 IoTSharp&#xff1f; IoTSharp 能做什么&#xff1f; IoTSharp 的亮点 项目技术 1、编程语言 2、系统框架 3、数据库支持 4、消息队列与 EventBus 5、EventBus 存储 项目使用 1、下载 2、启动 3、注册服务 4、初始化influxdb 5…

实现清除默认样式的操作

1、在npm官网中搜索reset.scss->点击第一个。&#xff08;有时候会更新一些代码&#xff0c;第一个出现的不一定就是我图片中的这个&#xff0c;不一定要跟我图片中的代码一致&#xff0c;只需要选择第一个出现的即可&#xff09; 2、点击Code选项。 3、打开Code选项下面的t…

在私有化过程中不要忽视LLMs的双重逻辑能力:医学领域的数据密集型分析

链接&#xff1a;https://arxiv.org/abs/2309.04198 原标题&#xff1a;Don’t Ignore Dual Logic Ability of LLMs while Privatizing: A Data-Intensive Analysis in Medical Domain 日期&#xff1a;Submitted on 8 Sep 2023 摘要 大量的研究致力于通过喂养特定领域的数据…

智能楼宇控制系统的革新——M31系列分布式IO模块

随着物联网技术的飞速发展&#xff0c;智能楼宇控制系统正逐渐成为现代建筑的标配。它不仅能够提高建筑的能源效率&#xff0c;还能提升用户的生活品质和舒适度。在这样的背景下&#xff0c;分布式IO模块作为智能楼宇控制系统的核心组成部分&#xff0c;发挥着至关重要的作用。…

深度学习9--目标检测

1.概念介绍 目标检测不仅可以检测数字&#xff0c;而且可以检测动物的种类、汽车的种类等。例如&#xff0c;自动驾驶车辆需要自动识别前方物体是车辆还是行人&#xff0c;需要自动识别道路两 旁的指示牌和前方的红绿灯颜色。对于自动检测的算法&#xff0c;有两个要求&#xf…

聊聊场景及场景测试

在我们进行测试过程中&#xff0c;有一种黑盒测试叫场景测试&#xff0c;我们完全是从用户的角度去理解系统&#xff0c;从而可以挖掘用户的隐含需求。 场景是指用户会使用这个系统来完成预定目标的所有情况的集合。 场景本身也代表了用户的需求&#xff0c;所以我们可以认为…

项目管理工具的秘密:如何选出最佳系统

国内外主流的 10 款project项目管理系统对比&#xff1a;PingCode、Worktile、用友、泛微、蓝凌、Zoho Projects、Asana、Trello、Basecamp、Jira。 在寻找合适的项目管理系统时&#xff0c;你是否感到选择众多、功能复杂让人难以抉择&#xff1f;这正是许多项目经理面临的痛点…

计算机毕业设计选题推荐-个性化智能学习系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…