【C语言】你真的了解结构体吗
创始人
2025-05-31 18:50:36
  1. 引言✨

我们知道C语言中存在着整形(int、short...),字符型(char),浮点型(float、double)等等内置类型,但是有时候,这些内置类型并不能解决我们的需求,因为我们无法用这些单一的内置类型来描述一些复杂的对象,如一个学生,一本书等等。出于这个原因,C语言还给我们提供了一些自定义的数据类型使我们可以自己来构建类型,如结构体、枚举、联合体。其中最常使用的就是我们本期的主题:结构体。

可能有很多人已经使用过结构体类型来解决一些实际问题了。但是对于结构体,还是有很多细节值得我们去深挖的,下面就让我们来看看吧!
温馨提示:可以通过目录进行快速定位哦😍
  1. 结构体的声明💫

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

答案是编译器会报警告:

尽管两个匿名结构体的成员列表一模一样,但是编译器依然会将其当作两个完全不同的类型,两个不透类型的指针相互赋值自然是非法的。
  1. 结构体的自引用🌟

我们在创建链表时,往往用结构体来表示链表的结点。结构体的成员分为数据域与指针域:

数据域:用来存储当前结点的值
指针域:用来存储指向下一结点的地址
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
#include
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;
}
怎么样,你做对了吗👀
步骤如下:
  1. 根据内存对齐算出s3所占的空间大小为16

  1. 根据对齐规则的第5点得出s3的要对齐到8的整数倍,即对齐到偏移量为8处

  1. double d的对齐数为8,因此对齐到偏移量为24处

  1. 最终大小为最大偏移量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
#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
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型宽度,每个部分我们使用位段来进行排列封装,使得空间最大利用。而如果我们使用结构体来进行封装每个部分,由于内存对齐的原因,势必会额外浪费空间造成数据包变得巨大,从而使网络状态变差。

总的来说,跟结构体相比,位段也可以达到一样的效果,其可以帮助我们节省空间,但是也带来了跨平台性的问题。

以上,就是本期的全部内容啦🌸

制作不易,能否点个赞再走呢🙏

相关内容

热门资讯

杭州重点高中有哪些,杭州有哪些... 杭州重点高中有哪些目录杭州重点高中有哪些杭州有哪些重点高中杭州哪所高中最好杭州重点高中有哪些 ...
女为悦己者容意思,士为知己者死... 女为悦己者容意思目录女为悦己者容意思士为知己者死,女为悦己者容.是什么意思.女人无需为悦己者容悦人不...
北戴河是海吗 极速百科网 极速... 北戴河是海吗目录北戴河是海吗北戴河是海吗 北戴河是海。北戴河古称渝水,清光绪年间,因沙河流经戴...
外婆是什么样的关系,姥姥与我的... 外婆是什么样的关系目录外婆是什么样的关系姥姥与我的关系叫什么关系姥姥是指外婆还是奶奶??我与外婆是什...
头歌--第1关:Linux文件... 任务描述 假设系统中存在一个文件File,修改该文件的权限,根据实际需求...
【Spring从成神到升仙系列... 👏作者简介:大家好,我是爱敲代码的小黄,独...
梦见蜈蚣是什么意思,做梦梦见蜈... 梦见蜈蚣是什么意思目录梦见蜈蚣是什么意思做梦梦见蜈蚣什么意思梦见蜈蚣是什么意思,哪里有解释啊梦见蜈蚣...
小区车位比一般是多少,车库配比... 小区车位比一般是多少目录小区车位比一般是多少车库配比是什么小区总户数8200,总车位是1450个,配...
车锁上的lock什么意思,汽车... 车锁上的lock什么意思目录车锁上的lock什么意思汽车上lock是什么意思?车子上“lock标志”...
kirin710是什么处理器,... kirin710是什么处理器目录kirin710是什么处理器海思kirin710是高通多少?骁龙71...
程序的循环结构和random库...   第三个参数就是步长     引入文件时记得指明字符格式,否则读入不了 ...
跟着文档制作cocos第一个游... 背景 近期打算学习一下cocos creator,想着开发自己的游戏,是...
乌干达是什么梗,网络语乌干达什... 乌干达是什么梗目录乌干达是什么梗网络语乌干达什么意思?乌干达是什么梗乌干达是什么梗乌干达是什么梗 ...
车载电子狗怎么用,怎样使用电子... 车载电子狗怎么用目录车载电子狗怎么用怎样使用电子狗怎么使用电子狗求简答车载电子狗怎么使用车载电子狗怎...
梦见偷东西是什么意思,梦见自己... 梦见偷东西是什么意思目录梦见偷东西是什么意思梦见自己偷东西是什么意思?做梦梦见自己偷东西好不好梦见偷...
黄金瞳到底是什么,黄金瞳电视剧... 黄金瞳到底是什么目录黄金瞳到底是什么黄金瞳电视剧什么时候上映?《黄金瞳》的结局是什么?电视剧《黄金瞳...
前端-session、jwt 目录:   (1)session (2&#x...
企业即时通讯怎样为企业实现移动... 对于企业来说,在办公过程中少不了工作人员相互传递信息和数据传输,企业内部...
骑行选择什么自行车 极速百科网... 骑行选择什么自行车目录骑行选择什么自行车骑行选择什么自行车 1. 山地自行车:适合崎岖不平的路...
蓝色都有哪几种,蓝色都有什么颜... 蓝色都有哪几种目录蓝色都有哪几种蓝色都有什么颜色的蓝图片,蓝色都有什么颜色的蓝二年级蓝色有哪些种类蓝...
如何自学游泳要安全的,初学游泳... 如何自学游泳要安全的目录如何自学游泳要安全的初学游泳的人需要准备哪些东西,注意哪些事项?如何自学游泳...
一年级家长的话怎么写评语,一年... 一年级家长的话怎么写评语目录一年级家长的话怎么写评语一年级学生评价手册家长寄语怎么写一年级最佳家长评...
EEG微状态的功能意义 导读大脑的瞬时全局功能状态反映在其电场结构上。聚类分析方法一致地提取了四种头表面脑电场结构ÿ...
docker 镜像管理 查看本地镜像 docker images 可以查看本地下载的镜像 docker images [O...
k8s-1.22.15部署ng... 1.介绍 在前面文章中已经提到,Service对集群之外暴露服务的主要方式有两种&#x...
革命烈士寄语怎么写,清明节缅怀... 革命烈士寄语怎么写目录革命烈士寄语怎么写清明节缅怀先烈的寄语有哪些呢?革命烈士寄语怎么写 革命...
5万元以下新车推荐,5万以下买... 本篇文章极速百科给大家谈谈5万元以下新车推荐,5万以下买什么车好,以及5万以下的新车哪款最好对应的知...
真皮沙发翻新一般多少钱?(真皮... 本篇文章极速百科给大家谈谈真皮沙发翻新一般多少钱?,以及真皮沙发翻新一般多少钱一个对应的知识点,希望...
磨皮什么意思(磨皮是啥?) 磨... 本篇文章极速百科给大家谈谈磨皮什么意思,以及磨皮是啥?对应的知识点,希望对各位有所帮助,不要忘了收藏...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...