C语言-----结构体详解

news2024/12/23 14:10:48

前面已经向大家介绍过一点结构体的知识了,这次我们再来深度了解一下结构体。结构体是能够方便表示一个物体具有多种属性的一种结构。物体的属性可以转换为结构体中的变量。

1.结构体类型的声明

1.1 结构体的声明

struct tag
{
   member-list;//结构体成员变量
}variable-list;

举例:

struct Stu
{
	int age; //年龄
	char name[20]; //名字
	char sex;  //性别
};

上述结构体就是一个关于学生的一个结构体,里面包含了年龄,名字,性别的信息。

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

struct Stu
{
	int age; //年龄
	char name[20]; //名字
	char sex[5];  //性别
};
int main()
{
	struct Stu s1 = { 18,"zhangsan","男" };  //有序的初始化
	//有序打印
	printf("%d\n", s1.age);
	printf("%s\n", s1.name);
	printf("%s\n", s1.sex);
	printf("\n");
	struct Stu s2 = {.age = 18,.sex = "男",.name = "lisi" }; //无序的初始化
	printf("%s\n", s2.name);
	printf("%d\n", s2.age);
	printf("%s\n", s2.sex);
	return 0;
}

结构体的初始化用到了 点操作符( . )。

1.3 结构体的特殊声明

结构体在声明时可以不完全声明,也就是省略了struct后面的结构体的名字。如下面代码

struct 
{
   member-list;//结构体成员变量
}variable-list;

虽然在写匿名结构体可以省一点功夫,但是匿名结构体也是有弊端的。我们先看一段代码。

struct
{
	char c;
	int age;
}s;
struct
{
	char c;
	int age;
}*p;
int main()
{
	*p = &s;
	return 0;
}

当我们运行上面代码的时候就会报错。

什么原因呢?

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

由此我们得出结论:如果结构体在匿名的并且没有重命名的情况下,匿名结构体基本就只能使用一次。

1.4 结构体的自引用

在结构体中包含一个该结构本身的成员是否可以呢?

我们也是先来看一段代码

struct Node
{
	int age;
	struct Node next;
}; 
int main()
{
	return 0;
}

当我们运行上面代码的时候是会报错的。为什么呢?

我们可以从struct Node 的大小这个点出发。当我们在一个结构体中包含一个该本身的结构体时,我们会发现它会一直循环调用本身结构体,可以当成陷入死递归了,无限开发空间,这样就会导致内存非常大,这样看实际上就不合理了。

我们可以写成一下形式

struct Node
{
	int age;
	struct Node* next;
}; 
int main()
{
	return 0;
}

无非就是要找下一个结构体的内容嘛,我们只需知道其地址就行了嘛,所以我们可以用一个结构体指针。 

当我们使用结构体的自引用时,需要注意用typedef对匿名结构体类型重命名的情况。如下面代码

typedef struct 
{
	int age;
	Node* next;
}Node; 
int main()
{
	return 0;
}

我们第一眼看到这个代码可能觉得没什么问题,我们已经将匿名结构体重命名为Node,里面也就可以写成Node* next了。但是后来发现Node是对前面匿名结构体的重命名,我们已经在结构体提前使用Node类型来创建变量了,这样明显是不行的。

2.结构体内存对齐

结构体内存对齐也是结构体里边一个比较热门的知识点,结构体内存对齐涉及到计算结构体的大小。

将这个内容之前,我们先来看一道题

struct S
{
	char a;
	int b;
	char c;

}s1;

int main()
{
	printf("%d", sizeof(s1));
	return 0;
}

这道题就是计算一个结构体的的大小,它的值是多少呢?

答案是该结构体的大小为12个字节。

这就很奇怪了,char类型大小明明为一个字节,int类型为4个字节,按道理来说不应该是6个字节大小吗? 

其实结构体大小的计算规则并没有那么简单,接下来让我向大家介绍下结构体大小的计算规则。

2.1 对齐规则

我们首先得掌握结构体的内存对齐规则

1, 结构体成员里面的第一个成员必须对齐到结构体变量起始位置偏移量为0的地址处。

上面是对结构体已经设计好了的一块内存,右边的数字就是相对于结构体起始位置的偏移量。

由于结构体的第一个成员要放在偏移量为0的地址处,所以对于上面的char a 要放在对应0的方格处。一个方格代表一个字节。 如下图

 2.其它结构体成员要对应到对齐数的整数倍的地址处。

对齐数的介绍

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

VS中默认的对齐数为8,Linux的gcc中没有默认的对齐数,对齐数就是成员变量大小。

了解了这些我们就可以开始分析剩下的int b和 char c了。以vs编译器来讲。

先分析a,a为int型,大小为4个字节,而vs默认的对齐数为8,4比8小,所以放置a时的对齐数为4。则要将a对应到偏移量为4的整数倍处,并且占4个字节。

在分析c,c为char型,大小为1个字节,vs默认的对齐数为8,1比8小,所以放置c时的对齐数为1,则要将c对应到偏移量为1的整数倍处,并且占一个字节。

如图

可按上图不还是只有10个字节吗?怎么有12个字节。则就涉及到第3条对齐规则。

3.结构体的总大小为所有成员中最大对齐数的整数倍。

a的对齐数为1,b的对齐数为4,c的对齐数为1,所以最大对齐数为4。

所以最终结构体的大小要是4的整数倍,所以要对齐到4的倍数。

如图

打X的地方是为了符合对齐规则而浪费的内存,也属于结构体的大小。

根据上面的分析,我们最终就得到了该结构体的大小为12个字节。 

 4. 结构体中嵌套了一个结构体的大小,嵌套的结构体就要对齐到自己成员中最大对齐数的整数倍处,结构体的总大小要是包括嵌套结构体成员之内的最大对齐数的整数倍。

看题

struct S
{
	char a;
	int b;
	char c;
}s1;
struct S2
{
	char d;
	struct S s1;
	char e;
}s2;
int main()
{
	printf("%zd", sizeof(s2));
}

画出下图 

得出该结构体的大小为20个字节。 

2.2 为什么存在内存对齐

1.平台原因

不是所有的硬件平台能够随意访问任意地址上的任意内容,有些硬件平台只能访问特定地址上的数据内容,否则就会显示硬件异常。

2.性能原因

数据结构(尤其是栈)要尽可能的对齐内存的自然边界。原因是硬件访问不对齐的数据时有可能需要访问2次或两次以上,访问次数越多就有可能出现差错。而访问对齐的内存的时候只访问1次就够了。

如上图,上图就是对齐与不对齐的分别。

假设我们的硬件一次能访问4个字节的空间,则内存不对齐时,当c占了1个字节后,会紧接着c存放完后放置i,这样当当我们访问内存时,一次访问4个字节,访问完一次之后,发现i还有一个字节的内容没被访问,要再一次访问4个字节的空间才能访问i剩下的一个字节的内容。这样读取i时就访问了2次。

内存对齐时,当我们存完c之后,先跳过3个字节再存放i,由于一次能够访问4个字节,这样一次访问就能取到一个内容。

总体来说:结构体的内存对齐是用空间来换取效率。

所以当我们在设计结构体的时候要尽量把内存比较小的成员变量集中在一起。

struct S1
 {
   char c1;
   int i;
   char c2;
 };

 struct S2
 {
   char c1;
   char c2;
   int i;
 };

如上面的代码,虽然成员变量一样,但是s2的内存要比s1的存小。 

2.3 修改默认对齐数

#pragma能够修改默认对齐数

看代码

#pragma pack(1) //将对齐数改为1
struct S
{
	char a;
	int b;
	char c;
}s1;
struct S2
{
	char d;
	struct S s1;
	char e;
}s2;
#pragma pack()  //取消修改对齐数,还原为默认对齐数
int main()
{
	printf("%zd", sizeof(s2));
}

因为修改了对齐数,所以导致之前的20变为了8。 

3.结构体传参

我们知道传参有传值和传地址的两种形式。那么结构体也有传值和传地址的两种形式。

看一下代码

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;
}

以上代码就实现了结构体传参的传值和传地址的两种形式,那么那种传参形式更好呢?

我们学过函数栈帧,当我们传值时,要实现压栈的操作,并为其形参创建空间,这样就在一定成都上浪费了空间和时间。

当我们将地址传过去的时候,我们就可以直接通过地址对结构体进行访问和使用,这样就可以节约时间和空间了。

所以,结构体传参的时候,传地址是一个更好的选择。

4.结构体实现位段

当我们介绍完结构体的内存对齐规则,我们会发现有时候这样会很浪费空间,有没有一些解决方法呢?

答案就是位段。

4.1 什么是位段

位段的声明和结构体是类似的,主要有两个不同。

1.位段的成员必须是 int,signed int和unsigned int 类型的。但是在C99中位段的成员可以是其他类型的。

2.位段成员的后面必须要有一个冒号且冒号后面要有一个数字。

举例

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

上述代码就是一个位段的声明。

既然我们说合理的位段设计可以节省空间了,那么其是如何节省空间的呢?紧接着来介绍位段的内存分配原则

4.2 位段的内存分配原则

首先我们要知道位段中的位是二进制位的意思,冒号后面的数字就是该位段成员所占的二进制位数。

1.位段上的空间是按需以1个字节或者4个字节来分配空间。

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;
	printf("%zd ", sizeof(s));
}

上面的位段内存大小是多少呢?

我们慢慢分析,由于位段是char类型的,内存按需分配,分配1个字节,也就是8个比特位

假设上面是一段设计好了的空间,按需分配一个字节,也就是8个字节。

分析a为10,其二进制写法为1001,又由于在位段中a占3个字节,所以再分配的一个字节的空间中占3个比特位,那么是先从左开始占还是从有边开始占呢?很可惜,这没有一个明确的规定。在不同的平台中,占法可能就不同,这就是我们前面提到的位段的跨平台行。vs中规定是从右向左开始占。如下图

分析完a,再来分析b,b为12,二进制写法为1011,在位段设计中占了4个字节,这时我们发现之前分配的1个字节的空间还剩有足够的空间来存储位段成员b,所以接着a继续向右存。如下图

紧接着来分析c,c的值为3,转换为二进制101,在位段中又占了5个字节,但这时发现之前分配的一个字节的空间已经不够 存储c了,那么是先把剩下的一个字节的空间占了,在从又分配的一个字节的空间占够剩下的内存呢?还是把之前剩下的空间直接浪费掉,直接在重新分配的一个空间占狗5个空间呢?

答案是直接浪费掉之前剩下的空间。

通过分析得到上图

剩下的也是一样的原则。4的二进制位100

那些浪费的空间会放置0。

由上述分析得知,该位段的大小位3个字节。

总结:虽然位段也会浪费一点空间,但只要设计的合理,相较于之前的结构体,位段还是比较节省空间的,不过存在跨平台的问题。

4.3 位段的应用

上图是网络协议中,IP数据的格式报,我们看到格式报里面有很多功能,且很多属性都占据了几个bit的空间。根据各自属性所占的空间,使用位段为其分配空间,能够更好的节省空间并且使运行效率更高。

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;
}

 

 

 

 

 

 

 

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

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

相关文章

VLC-Qt实现简单的视频播放器

VLC-Qt是一个结合了Qt应用程序和libVLC的免费开源库。它提供了用于媒体播放的核心类,以及用于快速开发媒体播放器的GUI类。由于集成了整个libVLC,VLC-Qt具备了libVLC的所有特性, 例如:libVLC实例和播放器、单个文件和列表播放、音…

海山数据库(He3DB)原理剖析:浅析Doris跨源分析能力

Doris湖仓分析背景: Doris多数据源功能演进 Doris的生态近年来围绕湖仓分析做了较多工作,Doris一直在积极拓宽大数据生态的OLAP分析市场,Doris2.0之后为了满足湖仓分析场景,围绕multi-catalog、数据缓存、容错、pipeline资源管理…

LibRadtran使用教程

LibRadtran使用教程 1.简介2.基本语法规则3.例子3.1 例子13.2 例子2 1.简介 关于LibRadtran的介绍以及安装可以参考另一篇博文&#xff1a;Windows系统LibRadtran安装。这里将针对LibRadtran的基础使用&#xff0c;以及基本语法进行介绍。 2.基本语法规则 uvspec < input…

【前端】layui table表格勾选事件,以及常见模块

欢迎来到《小5讲堂》&#xff0c;大家好&#xff0c;我是全栈小5。 这是《前端》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 表格勾选事…

itop4412编译内核时garbage following instruction -- `dmb ish‘ 解决方案

王德法 没人指导的学习路上磕磕绊绊太耗费时间了 今天编译4412开发板源码时报 garbage following instruction – dmb ish’ 以下是解决方案&#xff1a; 1.更新编译器 sudo apt-get install gcc-arm-linux-gnueabi 更新后修改Makefile 中编译器路径如下图 2.你以为更新完就可…

OpenHarmony实例应用:【常用组件和容器低代码】

介绍 本篇Codelab是基于ArkTS语言的低代码开发方式实现的一个简单实例。具体实现功能如下&#xff1a; 创建一个低代码工程。通过拖拽的方式实现任务列表和任务信息界面的界面布局。在UI编辑界面实现数据动态渲染和事件的绑定。 最终实现效果如下&#xff1a; 相关概念 低代…

【Blockchain】连接智能合约与现实世界的桥梁Chainlink

去中心化预言机试图实现依赖因果关系而不是个人关系的去信任和确定性结果。它以与区块链网络相同的方式实现这些结果&#xff0c;即在许多网络参与者之间分配信任。通过利用许多不同的数据源并实施不受单个实体控制的预言机系统&#xff0c;去中心化的预言机网络有可能为智能合…

【Python习题】用turtle库直角三角形,底边长150,斜边长300,底角60度,线条粗6像素,线条颜色为蓝色,填充颜色为红色

完整题干&#xff1a; &#xff08;1&#xff09;从Python官网下载Python3.7安装包&#xff0c;安装并熟悉 Python IDLE编程环境。 &#xff08;2&#xff09;在 Python IDLE Shell 窗口中编写程序计算圆的周长。 &#xff08;3&#xff09;编写程序&#xff0c;绘制如图1.10…

Java基础第十一课——类与对象(2)

由于类与对象这一部分的知识点很多&#xff0c;而且操作方法也有很多&#xff0c;所以这次将继续深入讨论一下关于类与对象中方法传参、方法重载、构造方法以及this关键字使用方面的知识。 一、方法传参 1.return关键字 return关键字作用 作用场景&#xff1a;方法内 作用…

C语言 函数——函数封装与程序的健壮性

目录 函数封装&#xff08;Encapsulation&#xff09; 如何增强程序的健壮性&#xff1f; 如何保证不会传入负数实参&#xff1f; 函数设计的基本原则 函数封装&#xff08;Encapsulation&#xff09; 外界对函数的影响——仅限于入口参数 函数对外界的影响——仅限于一个…

降额的秘密——不要挑战datasheet!

原文来自微信公众号&#xff1a;工程师看海&#xff0c;与我联系&#xff1a;chunhou0820 看海原创视频教程&#xff1a;《运放秘籍》 大家好&#xff0c;我是工程师看海。 什么是降额设计&#xff1f;我们为什么要降额&#xff1f; 额指的是额定工作状态&#xff0c;降额就是…

数据结构——单链表(C语言版)

文章目录 一、链表的概念及结构二、单链表的实现SList.h链表的打印申请新的结点链表的尾插链表的头插链表的尾删链表的头删链表的查找在指定位置之前插入数据在指定位置之后插入数据删除pos结点删除pos之后的结点销毁链表 三、完整源代码SList.hSList.ctest.c 一、链表的概念及…

基于YOLOv5s的电动车入梯识别系统(数据集+权重+登录界面+GUI界面+mysql)

本文目录 1.UI界面 2.注册登录 3.算法准确率 4.数据集 1.UI界面 本人训练的yolov5s模型&#xff0c;准确率在98.6%左右&#xff0c;可准确完成电梯内检测电动车任务&#xff0c;并搭配了GUI检测界面&#xff0c;支持权重选择、图片检测、视频检测、摄像头检测、识别结果拍照…

喜报!成都爱尔眼科医院再次获得成都市医学科技三等奖!

2024年4月10日&#xff0c;“2024年全市医疗管理和科教服务工作暨培训会”在成都市血液中心召开。会议为期一天&#xff0c;落实2024年全国、全省医政管理工作会和全省、全市卫生健康工作会等相关会议精神&#xff0c;总结2023年全市医疗管理和科教服务工作情况&#xff0c;部署…

go语言基础 -- 反射

反射的基本介绍 反射可以在运行时动态获取变量的信息&#xff0c;如变量的类型&#xff08;type&#xff09;&#xff0c;类别(kind)。如果是结构体变量&#xff0c;还可以获取到变量的字段、方法等结构体本身信息&#xff1b;通过反射&#xff0c;可以修改变量的值或调用关联…

【蓝桥杯】第十五届填空题a.握手问题

题解&#xff1a; 根据问题描述&#xff0c;总共有 50 人参加会议&#xff0c;每个人除了与自己以外的其他所有人握手一次。但有 7 个人彼此之间没有进行握手&#xff0c;而与其他所有人都进行了握手。 首先&#xff0c;计算所有人进行握手的总次数&#xff1a; 总人数为 50 …

LabVIEW电信号傅里叶分解合成实验

LabVIEW电信号傅里叶分解合成实验 电信号的分析与处理在科研和工业领域中起着越来越重要的作用。系统以LabVIEW软件为基础&#xff0c;开发了一个集电信号的傅里叶分解、合成、频率响应及频谱分析功能于一体的虚拟仿真实验系统。系统不仅能够模拟实际电路实验箱的全部功能&…

对给定向量旋转

对给定向量旋转 顺时针&#xff1a; 逆时针&#xff1a; 源码&#xff1a; QPointF rotateVector(const QPointF& dir, double angle, bool flag){double rad (angle * M_PI) / 180;QPointF res;if (flag){float x static_cast<float>(dir.x() * std::cos(rad) …

YOLOv8使用设备摄像头实时监测

代码如下&#xff1a; from ultralytics import YOLO import cv2 from cv2 import getTickCount, getTickFrequency yoloYOLO(./yolov8n.pt)#摄像头实时检测cap cv2.VideoCapture(0) while cap.isOpened():loop_start getTickCount() #记录循环开始的时间&#xff0c;用于计…

Rust腐蚀服务器常用参数设定详解

Rust腐蚀服务器常用参数设定详解 大家好我是艾西&#xff0c;一个做服务器租用的网络架构师上期我们分享了rust腐蚀服务器的windows系统搭建方式&#xff0c;其中启动服务器bat参数因为涉及的东西比较多所以想通过这篇文章给大家做一下详细的分享。 &#xff08;注本文中xxxx…