【C语言】你真的了解结构体吗

引言✨
我们知道C语言中存在着整形(int、short...),字符型(char),浮点型(float、double)等等内置类型,但是有时候,这些内置类型并不能解决我们的需求,因为我们无法用这些单一的内置类型来描述一些复杂的对象,如一个学生,一本书等等。出于这个原因,C语言还给我们提供了一些自定义的数据类型使我们可以自己来构建类型,如结构体、枚举、联合体。其中最常使用的就是我们本期的主题:结构体。
可能有很多人已经使用过结构体类型来解决一些实际问题了。但是对于结构体,还是有很多细节值得我们去深挖的,下面就让我们来看看吧!
温馨提示:可以通过目录进行快速定位哦😍
结构体的声明💫
2.1 结构体的基础知识
在开启本期内容之前,我们先来回顾以下结构体的基本概念:
结构体是C语言中一个非常重要的数据类型。该数据类型是由一组称为成员变量的数据组成,其中每个成员可以是不同类型的变量,甚至可以是另一个结构体变量。结构体通常用来表示类型不同但又相关的若干数据。
2.2 结构体的声明
结构体的声明格式如下:
struct tag
{member-list;
}variable-list;
struct是结构体关键字,我们要定义结构体类型时必须使用它
tag是结构体标签,它用来区分不同的结构体类型
结构体关键词与标签共同组成了结构体的类型,与int,float这些是一个意思,我们可以使用struct tag+变量名来定义一个结构体变量。
member-list代表成员列表,它包含了结构体的成员变量。
variable-list表示变量列表,我们可以在声明结构体类型的同时创建结构体变量。当然我们也可以不写,仅声明一个结构体类型。
结构体大括号后面的分号必不可少。
例如,我们可以这样使用结构体来描述一个学生:
//声明一个学生类型
struct Student
{char name[20];//姓名char sex[5];//性别char id[20];//学号int age;//年龄float score;//绩点
};int main()
{struct Student s1;//定义一个学生结构体变量s1
}
当然,如果你嫌结构体的类型名太长,写起来麻烦,可以使用typedef对类型进行重命名,如下:
//声明一个学生类型,并用typedef类型重定义为Stu
typedef struct Student
{char name[20];//姓名char sex[5];//性别char id[20];//学号int age;//年龄float score;//绩点
}Stu;int main()
{Stu s1;//相当于sturuct Student s1
}
2.3 特殊的声明
除以上的声明方式,我们也可以使用不完全的声明。例如:
//声明匿名结构体类型
struct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}a[20], *p;
上面两个结构体的声明省略了结构体标签tag,我们把这样的结构体类型称作匿名结构体类型。
但是,这样子的声明往往是一次性的。由于我们省略了标签,我们就无法在其他地方使用这个类型来创建一个结构体变量。毕竟连名字都没有,怎么用来定义变量。
当然,如果你只想用一次你创建的类型,或者你不想要这个结构体类型被别人使用,你可以声明一个匿名结构体类型。
那么问题来了:
int main()
{//在上面匿名结构体声明的基础上,下面的代码合法吗?p = &x;
}
答案是编译器会报警告:

尽管两个匿名结构体的成员列表一模一样,但是编译器依然会将其当作两个完全不同的类型,两个不透类型的指针相互赋值自然是非法的。
结构体的自引用🌟
我们在创建链表时,往往用结构体来表示链表的结点。结构体的成员分为数据域与指针域:
数据域:用来存储当前结点的值
指针域:用来存储指向下一结点的地址
typedef int ListDataType;
struct ListNode
{ListDataType val;//数据域struct ListNode* next;//指针域
};
我们将上面这种结构体中包含有指向自身结构体变量的指针的方式称作结构体的自引用。其中val占4个字节,next是个指针,占4/8个字节,结构体具有一个确定的大小。
那既然我们这样声明结点目的是为了能够找到下一结点的位置,那我们可不可以这样设计结点:
typedef int ListDataType;
struct ListNode
{ListDataType val;//数据域struct ListNode next;//保存下一结点
};
答案是不行的。假如可以这样设计,那么sizeof( struct ListNode)的大小该是多少呢?我们是求不出来的,因为假设我们用这个类型创建了一个结构体变量n,那么n中包含着next,next也是结构体变量,又包含着一个next变量,next又包含着next...,这样下去就变成了无限套娃。既然不知道大小,我们又要如何分配空间给结构体变量呢?
注意:
//这样写代码,可行否?
typedef struct
{int data;Node* next;
}Node;
显然是不行的,凡是都要讲究个先来后到。当我们在成员列表中定义Node*类型的变量时,此时编译器还不知道Node是什么鬼东西,自然会报错。我们可以这样修改代码:
//解决方案:
typedef struct Node
{int data;struct Node* next;
}Node;Node* pn;//定义一个结构体指针pn
4. 结构体变量的定义和初始化🌊
有了结构体类型,那我们要如何定义变量呢?实则很简单
struct Point
{int x;int y;
}p1; //声明类型的同时定义变量p1struct Stu //类型声明
{char name[15];//名字int age; //年龄
};
int main()
{//定义结构体变量p2struct Point p2; //初始化:定义变量的同时赋初值。struct Point p3 = { 3, 4 };//初始化struct Stu s = { "zhangsan", 20 };
}
结构体嵌套结构体的初始化方式如下:
struct Point
{int x;int y;
}p1; //声明类型的同时定义变量p1
struct Node
{int data;struct Point p;struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化int main()
{struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化
}
5.结构体的内存对齐
🔉快醒醒,别睡了
终于到了本期的重点内容了,我们来看下面例题:
//练习
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{printf("%d\n", sizeof(struct S1));printf("%d\n", sizeof(struct S2));return 0;
}
答案如下:

这里可能有人就纳闷了,欸,char类型占1个字节,int类型占4个字节,s1与s2的大小不应该都是1+1+4=6吗?怎么会是12和8呢?这就要谈到结构体在内存中的存储了,即结构体的内存对齐。
实际上S1在内存中的存储方式是这样子的:

我们看到c1存放完后,i并不是紧挨着c1进行存放,而是从偏移量为4的地方开始存储,中间空出三个字节的空间。这就是结构体的内存对齐,下面我们来了解其内存对齐的规则:
结构体的第一个成员在与结构体变量偏移量为0的地址处
其他成员变量要对齐到某个数字(我们称作对齐数)的整数倍的地址处
对齐数=编译器默认的一个对齐数与该变量大小的较小值。vs的默认对齐数为8
结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
对于嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们可以模拟一下s1的内存对齐方式:

同样,S2的内存对齐方式如下:

如果你还是不确定,C语言给我们提供了offsetof宏来计算结构体成员的偏移量,原型如下:

需要注意:使用时我们需要先包含stddef.h头文件:
#include<stddef.h>
#include<stdio.h>
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{printf("结构体S1中c1的偏移量为%zd\n",offsetof(struct S1,c1 ));printf("结构体S1中i的偏移量为%zd\n", offsetof(struct S1, i));printf("结构体S1中c2的偏移量为%zd\n", offsetof(struct S1, c2));printf("结构体S2中c1的偏移量为%zd\n", offsetof(struct S2, c1));printf("结构体S2中c2的偏移量为%zd\n", offsetof(struct S2, c2));printf("结构体S2中i的偏移量为%zd\n", offsetof(struct S2, i));return 0;
}
结果如下,与我们上述的分析过程如出一辙:

我们再来看一个例子:
//结构体嵌套问题
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));return 0;
}

怎么样,你做对了吗👀
步骤如下:
根据内存对齐算出s3所占的空间大小为16
根据对齐规则的第5点得出s3的要对齐到8的整数倍,即对齐到偏移量为8处
double d的对齐数为8,因此对齐到偏移量为24处
最终大小为最大偏移量8的整数倍,即为32。
想必有人会有疑问,内存对齐那么麻烦,为什么存在内存对齐?主要有以下两点原因:
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器可能需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足内存,又要节省空间,我们要如何做到:
让占用空间小的成员尽量集中在一起
//例如:
//c1与c2不相邻
struct S1
{char c1;int i;char c2;
};//c1与c2相邻
struct S2
{char c1;char c2;int i;
};
虽然S1和S2类型的成员一模一样,但是S1占12个字节,S2占8个字节,这就是合理安排位置所带来的好处。
6.默认对齐数的修改🌷
在C语言中,我们也可以修改结构体的默认对齐数,只需用#pragma这个预处理指令即可。如下:
#include<stdio.h>
#pragma pack(1) //修改默认对齐数为1
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};
int main()
{printf("%d\n", sizeof(struct S1));printf("%d\n", sizeof(struct S2));return 0;
}
上面我们将默认对齐数设置成1,由于对齐数是默认对齐数和成员大小较小者,因此默认对齐数为1相当于不对齐,S1与S2的结果相同都为6:

7. 结构体的传参
话不多说,我们直接上代码来说明:
#include<stdio.h>
struct S
{int data[1000];int size;
};
//传值
void print(struct S s)
{printf("%d", s.size);
}
//传址
void print(struct S* sp)
{printf("%d", sp->size);
}
int main()
{struct S s1;print1(s1);print2(&s1);
}
print1()和print2()哪个函数好呢?
答案是print2()函数。
为什么呢?
print1()和print2()分别对应着传值调用和传址调用。我们知道无论是传值还是传址,函数在将要调用时实参都会形成临时拷贝并压入栈中。压栈的这个过程是需要成本的,成本体现在时间和空间上。
如果传递一个结构体对象的时候,结构体过大(例如我们上面的s1),参数压栈的的成本比较大,就会导致性能的下降。所以我们传递像结构体这种数据量较大的变量时,一般传地址,地址占4个或者8个字节,极大程度上减少了所需的成本。
综上所述,我们进行结构体传参时要传结构体的地址。
8.位段🌸
8.1 位段的特征与声明
讲完结构体后我们就必须再来讲讲结构体实现位段的能力,位段满足以下两点特征:
1.位段的成员必须是int、unsigned int或者char这些整型家族的成员
2.位段的成员名后有一个冒号和数字,数字表示成员占多少个二进制位(bit位)
3.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
例如下面的A就是一个位段类型:
struct A
{char a : 1;char b : 4;char c : 5;char d : 5;
};
其中a占1个二进制位,b占4个二进制位,c占5个二进制位,d占5个二进制位。那么位段A的大小是多少呢?这就要来谈谈位段的内存分配了。
8.2 位段的内存分配
事实上,C语言并没有明确规定位段的内存分配方式,也就是说:
1.我们并不知道位段中的成员在内存中是从左向右分配二进制位还是从右向左分配二进制位
2.我们不清楚当一个结构包含两个以上位段,第二个位段成员比较大,第一个位段剩余的二进制位无法容纳第二个位段,是舍弃剩余的位还是将其利用,这是不确定的。
正因如此,位段在不同的编译环境下所展现出来的效果很可能会有所不同。我们可以探究一下A当其从右向左分配并且不足时舍去剩余位时的内存分配情况,如下(VS2022环境下):
struct A
{char a : 1;char b : 4;char c : 5;char d : 5;
}s={0};
int main()
{s.a = 11;s.b = 12;s.c = 3;s.d = 4;printf("%d", sizeof(s));//计算s所占大小return 0;
}


我们发现按照我们的假设计算出来的结果与vs2022监视器中内存的分配结果一模一样,因此我们可以得知在vs2022编译器下位段是从右向左分配且不足时舍弃剩余位。
8.3 位段的跨平台问题
由于以下问题的存在,位段的可移植性很差,即存在着跨平台问题:
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
8.4 位段的应用
位段在网络中的应用比较多,例如以下ip数据包格式:

当我们在网络上给某人发送一个消息时,这个消息就会封装成如上图所示的一个数据包用于在网络上精确地找到接收人。我们可以看出每一行都恰好的被设计成了int型宽度,每个部分我们使用位段来进行排列封装,使得空间最大利用。而如果我们使用结构体来进行封装每个部分,由于内存对齐的原因,势必会额外浪费空间造成数据包变得巨大,从而使网络状态变差。
总的来说,跟结构体相比,位段也可以达到一样的效果,其可以帮助我们节省空间,但是也带来了跨平台性的问题。
以上,就是本期的全部内容啦🌸
制作不易,能否点个赞再走呢🙏
相关文章:

【C语言】你真的了解结构体吗
引言✨我们知道C语言中存在着整形(int、short...),字符型(char),浮点型(float、double)等等内置类型,但是有时候,这些内置类型并不能解决我们的需求,因为我们无法用这些单一的内置类型来描述一些复杂的对象,…...

血氧仪是如何得出血氧饱和度值的?
目录 一、血氧饱和度概念 二、血氧饱和度监测意义 三、血氧饱和度的监测方式 四、容积脉搏波计算血氧饱和度原理 五、容积脉搏波波形的测量电路方案 1)光源和光电探测器的集成测量模块:SFH7050—反射式 2)模拟前端 六、市面上血氧仪类型…...
Java全栈知识(3)接口和抽象类
1、抽象类 抽象类就是由abstract修饰的类,其中没有只声明没有实现的方法就是抽象方法,抽象类中可以有0个或者多个抽象方法。 1.1、抽象类的语法 抽象类不能被final修饰 因为抽象类是一种类似于工程中未完成的中间件。需要有子类进行继承完善其功能,所…...
JavaScript == === Object.is()
文章目录JavaScript & & Object.is() 相等运算符 全等运算符Object.is() 值比较JavaScript & & Object.is() 相等运算符 相等运算符,会先进行类型转换,将2个操作数转为相同的类型,再比较2个值。 console.log("10&…...

GPT4论文翻译 by GPT4 and Human
GPT-4技术报告解读 文章目录GPT-4技术报告解读前言:摘要1 引言2 技术报告的范围和局限性3 可预测的扩展性3.1 损失预测3.2 人类评估能力的扩展4 能力评估4.1 视觉输入 !!!5 限制6 风险与缓解:7 结论前言: 这篇报告内容太多了!&am…...

inode和软硬链接
文章目录:一、理解文件系统1.1 什么是inode1.2 磁盘了解1.2.1磁盘的硬件结构1.2.2 磁盘的分区1.2.3 EXT2文件系统二、软硬链接2.1 软链接2.2 硬链接一、理解文件系统 1.1 什么是inode inodes 是文件系统中存储文件元数据的数据结构。每个文件或目录都有一个唯一的 …...

简单分析Linux内核基础篇——initcall
写过Linux驱动的人都知道module_init宏,因为它声明了一个驱动的入口函数。 除了module_init宏,你会发现在Linux内核中有许多的驱动并没有使用module_init宏来声明入口函数,而是看到了许多诸如以下的声明: static int __init qco…...

硬件速攻-AT24CXX存储器
AT24C02是什么? AT24CXX是存储芯片,驱动方式为IIC协议 实物图? 引脚介绍? A0 地址设置角 可连接高电平或低电平 A1 地址设置角 可连接高电平或低电平 A2 地址设置角 可连接高电平或低电平 1010是设备前四位固定地址 …...
C# tuple元组详解
概念 本质就是个数据结构,它是将多个数据元素分组成一个轻型数据结构。 如何声明元组变量(针对.net framework 4.7 和 .net core 2.0) 不带字段名称元组 ## t1就是个变量 它的类型是元组类型 ## 左侧括号定义的是参数列表 等于号右侧就是个t1赋值 #…...

1、Linux初级——linux命令
下载镜像:http://cn.ubuntu.com/dowload 一、基本命令 1、alias(给命令取别名) 例如:alias clls -la(只是临时的) 2、配置文件$ vim ~/.bashrc $ vim ~/.bashrc // 使用vim打开配置文件 (1)在配置文件…...

ChatGPT助力校招----面试问题分享(四)
1 ChatGPT每日一题:电阻如何选型 问题:电阻如何选型 ChatGPT:电阻的选型通常需要考虑以下几个方面: 额定功率:电阻的额定功率是指电阻能够承受的最大功率。在选型时,需要根据电路中所需要的功率确定所选…...

【设计模式】创建型设计模式
文章目录1. 基础①如何学习设计模式② 类模型③ 类关系2. 设计原则3. 模板方法① 定义②背景③ 要点④ 本质⑤ 结构图⑥ 样例代码4. 观察者模式① 定义②背景③ 要点④ 本质⑤ 结构图⑥ 样例代码5. 策略模式① 定义②背景③ 要点④ 本质⑤ 结构图⑥ 样例代码1. 基础 ①如何学习…...

Linux 信号(signal):信号的理解
目录一、理解信号1.信号是什么2.信号的种类二、简单理解信号的生命周期一、理解信号 1.信号是什么 Linux中的信号其实和日常生活中的信号还是挺像的,LInux中的信号是一种事件通知机制,通知进程发生了某个事件。进程接收到信号后,就会中断当前…...

Vulnhub项目:Web Machine(N7)
靶机地址:Web Machine(N7)渗透过程:kali ip:192.168.56.104,靶机ip,使用arp-scan进行查看靶机地址:192.168.56.128收集靶机开放端口:nmap -sS -sV -T5 -A 192.168.56.128开放了80端口࿰…...

Qt基础之三十三:海量网络数据实时显示
开发中我们可能会遇到接收的网络数据来不及显示的问题。最基础的做法是限制UI中加载的数据行数,这样一来可以防止内存一直涨,二来数据刷新非常快,加载再多也来不及看。此时UI能看到数据当前处理到什么阶段就行,实时性更加重要,要做数据分析的话还得查看日志文件。 这里给出…...
linux console快捷键
Ctrl C:终止当前正在运行的程序。Ctrl D:关闭当前终端会话。Ctrl Z:将当前程序放入后台运行。Ctrl L:清除当前屏幕并重新显示命令提示符。Ctrl R:在历史命令中进行逆向搜索。Ctrl A:将光标移动到行首…...
弗洛伊德龟兔赛跑算法(弗洛伊德判圈算法)
弗洛伊德( 罗伯特・弗洛伊德)判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm),是一个可以在有限状态机、迭代函数或者链表上判断是否存在环,以及判断环的起点与长度的算法。昨晚刷到一个视频&…...

nodejs篇 express(1)
文章目录前言express介绍安装RESTful接口规范express的简单使用一个最简单的服务器,仅仅只需要几行代码便可以实现。restful规范的五种接口类型请求信息req的获取响应信息res的设置中间件的使用自定义中间件解决跨域nodejs相关其它内容前言 express作为nodejs必学的…...

Java实习生------Redis常见面试题汇总(AOF持久化、RDB快照、分布式锁、缓存一致性)⭐⭐⭐
“年轻人,就要勇敢追梦”🌹 参考资料:图解redis 目录 谈谈你对AOF持久化的理解? redis的三种写回策略是什么? 谈谈你对AOF重写机制的理解?AOF重写机制的具体过程? 谈谈你对RDB快照的理解&a…...

seata服务搭建
它支持两种存储模式,一个是文件,一个是数据库,下面我们分别介绍一下这两种配置nacos存储配置,注意如果registry.conf中注册和配置使用的是file,就会去读取file.config的配置,如果是nacos则通过nacos动态读取…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...
机器学习的数学基础:线性模型
线性模型 线性模型的基本形式为: f ( x ) ω T x b f\left(\boldsymbol{x}\right)\boldsymbol{\omega}^\text{T}\boldsymbol{x}b f(x)ωTxb 回归问题 利用最小二乘法,得到 ω \boldsymbol{\omega} ω和 b b b的参数估计$ \boldsymbol{\hat{\omega}}…...
TJCTF 2025
还以为是天津的。这个比较容易,虽然绕了点弯,可还是把CP AK了,不过我会的别人也会,还是没啥名次。记录一下吧。 Crypto bacon-bits with open(flag.txt) as f: flag f.read().strip() with open(text.txt) as t: text t.read…...
嵌入式面试常问问题
以下内容面向嵌入式/系统方向的初学者与面试备考者,全面梳理了以下几大板块,并在每个板块末尾列出常见的面试问答思路,帮助你既能夯实基础,又能应对面试挑战。 一、TCP/IP 协议 1.1 TCP/IP 五层模型概述 链路层(Link Layer) 包括网卡驱动、以太网、Wi‑Fi、PPP 等。负责…...

篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...