目录
前言:
“结构体”是个里程碑式的知识点!掌握了它,你就不再是那个只会处理单个数字、单个字符的“初级玩家”了,你将正式踏入“自定义数据类型”的大门,开始构建属于你自己的、更复杂的数据世界。
我知道,刚接触“结构体”这个词,你可能会觉得有点抽象,甚至有点害怕。别担心!当年我第一次看到struct这几个字母时,脑子里也是一片空白。但今天,我会用最通俗、最接地气的方式,把这个看似高深的概念掰开了、揉碎了讲给你听。我会用生活中的例子、简单的比喻,把每一个专业术语都解释得明明白白。
这篇博客我会写得稍微长一点,目的就是让你一次学透,不留任何疑问。我们不赶进度,稳扎稳打,一步一个脚印。等你读完,我保证你会拍着桌子说:“原来结构体这么简单!”
好,废话不多说,咱们这就开始这场精彩的编程之旅吧!

一、 结构体到底是个啥?—— 给数据找个“家”
1.1 为什么我们需要结构体?
想象一下,你现在要管理一个班级的学生信息。每个学生都有哪些信息呢?名字、年龄、性别、学号...对吧?
在没学结构体之前,你会怎么存这些信息?是不是这样:
char name[20] = "张三"; // 名字
int age = 20; // 年龄
char sex[5] = "男"; // 性别
char id[20] = "20230818001"; // 学号
这看起来好像没问题。但是,如果我要管理100个学生呢?难道我要定义400个变量吗?name1, name2, name3... age1, age2, age3... 这简直是一场灾难!不仅代码长得吓人,而且你根本分不清哪个名字对应哪个年龄、哪个学号。
问题的核心在于:这些数据是分散的、孤立的。它们明明是一个整体(一个学生),却被强行拆开,散落在程序的各个角落。
这就好比你搬家,把衣服、书本、锅碗瓢盆全都乱七八糟地塞进不同的纸箱里,每个箱子上还没贴标签。等到了新家,你想找一件T恤,得把所有箱子都翻一遍,效率极低,还容易出错。
结构体,就是为了解决这个问题而生的!它的核心思想就是:把相关的数据打包在一起,形成一个“数据包”或者叫“数据集合”。
你可以把结构体想象成一个定制化的收纳盒。这个盒子有好几个格子,每个格子放一种特定的东西。比如,一个“学生信息收纳盒”,第一个格子专门放名字,第二个格子放年龄,第三个格子放性别,第四个格子放学号。这样一来,所有关于“张三”的信息,都被整整齐齐地放在同一个盒子里,再也不怕弄混了!
1.2 结构体的声明:设计你的“收纳盒”图纸
知道了结构体是干什么的,下一步就是告诉计算机,我们要设计一个什么样的“收纳盒”。
在C语言里,我们用struct关键字来声明一个结构体。语法格式如下:
struct 结构体名 {
成员1的类型 成员1的名字;
成员2的类型 成员2的名字;
成员3的类型 成员3的名字;
// ... 可以有任意多个成员
};
// 注意:最后的分号 ; 千万不能丢!这是很多新手常犯的错误。
让我们用这个语法,来设计我们的“学生信息收纳盒”:
struct Stu { // Stu 是 Student 的缩写,你可以取任何你喜欢的名字
char name[20]; // 名字,用一个能存20个字符的数组
int age; // 年龄,用一个整数
char sex[5]; // 性别,用一个能存5个字符的数组(足够放“男”或“女”了)
char id[20]; // 学号,同样用字符数组
}; // 分号!分号!分号!重要的事情说三遍!
这段代码就是在告诉计算机:“嘿,我要创建一种新的数据类型,名叫Stu。这种类型包含四个部分:一个叫name的字符串,一个叫age的整数,一个叫sex的字符串,和一个叫id的字符串。”
关键概念解释:
struct: 这是C语言的关键字,意思是“结构”。它标志着后面跟着的是一个结构体的定义。
Stu: 这是我们给这个结构体起的“标签”(tag)。就像你给收纳盒贴的标签一样,以后要用到这个类型的盒子,就喊它的名字Stu。- 花括号
{}: 花括号里面的内容,就是这个“收纳盒”的内部构造图。它详细说明了这个盒子里有哪些“格子”(成员)。 - 成员 (Member):
name,age,sex,id这些就是结构体的成员。它们是构成这个结构体的基本单元。每个成员都有自己的数据类型(char[],int)和名字。 - 分号
;: 在结构体定义结束的大括号后面,必须加上分号。这是C语言的语法规则,表示“定义完毕”。
现在,struct Stu 就成了一个全新的、由你自己定义的数据类型,就像系统自带的 int, char, float 一样。只不过,它更强大,因为它可以包含多种不同类型的数据!
1.3 创建和初始化结构体变量:打造你的第一个“收纳盒”
声明好结构体类型后,下一步就是动手制作具体的“收纳盒”了。在编程术语里,这叫做创建结构体变量。
创建变量的语法非常简单,跟创建 int a; 或 char c; 是一样的:
struct Stu s1; // 创建一个名为s1的Stu类型变量
struct Stu s2; // 再创建一个名为s2的Stu类型变量
现在,内存中就有了两个独立的“学生信息收纳盒”,分别叫s1和s2。你可以把不同学生的信息分别放进这两个盒子里。
方法一:按顺序初始化(最常用)
这是最直观的方法,按照结构体定义时成员的顺序,依次给它们赋值。
#include <stdio.h>
struct Stu {
char name[20];
int age;
char sex[5];
char id[20];
};
int main() {
// 按照结构体成员的顺序初始化
struct Stu s = {"张三", 20, "男", "20230818001"};
// 打印出来看看
printf("name: %s\n", s.name);
printf("age: %d\n", s.age);
printf("sex: %s\n", s.sex);
printf("id : %s\n", s.id);
return 0;
}
运行结果:

你看,我们用一个大括号{},把要放进去的值按顺序列出来,中间用逗号,隔开。第一个值"张三"给了name,第二个值20给了age,以此类推。是不是超级清晰?
方法二:指定成员初始化(更灵活)
有时候,你可能不想按顺序来,或者只想初始化其中几个成员。这时候就可以用“指定成员初始化”的方法。
#include <stdio.h>
struct Stu {
char name[20];
int age;
char sex[5];
char id[20];
};
int main() {
// 按照指定的顺序初始化,想先给谁赋值就先给谁
struct Stu s2 = {.age = 18, .name = "李四", .id = "20230818002", .sex = "女"};
printf("name: %s\n", s2.name);
printf("age: %d\n", s2.age);
printf("sex: %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
运行结果:

这种方法的好处是,你可以在初始化列表里“指名道姓”地说清楚哪个值给哪个成员,用.成员名 = 值的形式。这样即使你打乱了顺序,或者漏掉了一些成员(未初始化的成员会被自动设为0或空),程序也能准确无误地工作。
小贴士: 对于初学者,我推荐先熟练掌握“按顺序初始化”,因为它逻辑清晰,不易出错。等你玩熟了,再尝试“指定成员初始化”来提高灵活性。
1.4 访问结构体成员:打开盒子,取出你要的东西
盒子做好了,东西也放好了,那怎么拿出来用呢?
这里就要用到一个神奇的操作符:点操作符 . (英文句点)。
它的用法很简单:结构体变量名.成员名
#include <stdio.h>
struct Stu {
char name[20];
int age;
char sex[5];
char id[20];
};
int main() {
struct Stu s = {"张三", 20, "男", "20230818001"};
// 我想单独修改年龄
s.age = 21; // 把s这个盒子里的age格子,从20改成21
// 我想单独打印名字
printf("这位同学的新年龄是:%d岁\n", s.age); // 输出:21岁
return 0;
}
你看,s.age 就像一把钥匙,精准地打开了名为s的盒子里的那个叫 age 的小格子,你可以读取它的值,也可以修改它的值。
记住这个万能公式:
想操作哪个盒子里的哪个东西?就用
盒子名.东西名!
二、 进阶玩法:匿名结构与自我引用
学完了基础,我们来玩点更酷的!这部分内容稍微有点抽象,但别怕,我会用最形象的例子带你飞。
2.1 匿名结构体:一次性使用的“神秘盒子”
有时候,你可能只需要一个结构体,并且只打算用一次。这时候,你可以创建一个“匿名结构体”。
什么叫“匿名”?就是没有名字(标签)!
// 匿名结构体类型
struct {
int a;
char b;
float c;
} x; // 这里直接定义了一个变量x,它的类型是一个匿名结构体
// 你还可以这样定义数组或指针
struct {
int a;
char b;
float c;
} arr[20], *p; // 定义了一个包含20个元素的数组arr,和一个指针p
你看,在struct后面,我们直接跟上了花括号定义成员,然后紧跟着就定义了变量x,中间没有Stu这样的标签。这就是匿名结构体。
那么问题来了:下面的代码合法吗?
struct {
int a;
char b;
float c;
} x;
struct {
int a;
char b;
float c;
} *p;
p = &x; // 这行代码合法吗?
答案是:不合法!会报错!
为什么?
虽然这两个匿名结构体的内部构造看起来一模一样(都是int a; char b; float c;),但对于编译器来说,它们是完全不同的两种类型!
想象一下,你去宜家买了两个外观、尺寸、内部隔层都完全一样的收纳盒,但一个是蓝色的,一个是红色的。虽然它们功能一样,但在你心里,它们是两个不同的盒子。编译器也是这么想的:只要是匿名的,哪怕内容一样,它也认为是不同的类型。
所以,p是指向“红色盒子”的指针,你却想让它指向“蓝色盒子”x,这当然不行!
总结一下匿名结构体的特点:
- 优点:方便快捷,适合定义那些只用一次的临时数据结构。
- 缺点:无法重用。一旦定义完,你就不能再用这个类型去创建新的变量了。因为没有名字,你没法“称呼”它。
- 重要规则:两个匿名结构体,即使成员列表完全相同,也被视为不同类型,不能互相赋值或取地址。
所以,除非你确定这个结构体只用一次,否则还是老老实实用带名字的结构体吧,省得给自己找麻烦。
2.2 结构体的自我引用:构建“链条”和“树”
这是一个非常重要的概念,尤其是在学习链表、树等数据结构时。它的应用场景非常多,比如QQ的好友列表、文件系统的目录结构等等。
什么是自我引用?
简单说,就是一个结构体里面,包含了指向“自己类型”的指针。
举个栗子🌰: 我们要定义一个“链表节点”。一个节点里有什么?一部分是它自己的数据(比如学生的成绩),另一部分是指向“下一个节点”的线索
// ❌ 错误示范!千万不能这么写!
struct Node {
int data; // 数据部分
struct Node next; // 错误!不能直接包含一个同类型的结构体变量!
};
为什么错了?
这会导致一个可怕的“无限套娃”问题!Node里面包含一个Node,而这个被包含的Node里面又包含一个Node,如此循环下去...这个结构体的大小会变成无穷大!计算机的内存是有限的,这显然是不可能的。
✅ 正确的做法:使用指针!
// ✅ 正确示范!
struct Node {
int data; // 数据部分
struct Node* next; // 正确!包含一个指向同类型结构体的指针
};
这次我们用的是struct Node* next,是一个指针,而不是一个完整的结构体变量。
指针是什么? 指针就是一个“地址簿”,它只记录某个东西在哪里,而不是把那个东西本身搬过来。一个指针的大小是固定的(通常是4或8个字节),不管你指向的东西有多大。
所以,struct Node* next 的意思就是:“我这里有一张小纸条,上面写着另一个Node盒子的存放地址。” 这样,Node结构体本身的大小就是可控的:int data占4个字节 + 一个指针占8个字节 = 总共12个字节(具体大小取决于系统)。
通过这种方式,我们可以把一个个Node节点像链条一样串起来: 节点A -> 节点B -> 节点C -> NULL(NULL表示链条结束)
进阶挑战:typedef与自引用的结合
有时候,为了代码更简洁,我们会用typedef给结构体起一个别名。
// ❌ 错误示范!
typedef struct {
int data;
Node* next; // 错误!此时Node还没有被定义!
} Node;
这个代码会报错。因为在定义结构体成员Node* next的时候,编译器还不知道Node是谁。Node是在整个typedef语句的末尾才被定义出来的。
✅ 解决方案:给结构体加上标签!
// ✅ 正确示范!
typedef struct Node { // 先给结构体一个标签:Node
int data;
struct Node* next; // 现在可以用 struct Node* 了,因为标签已经声明了
} Node; // 最后再用 typedef 给它起个别名,也叫 Node
或者,你也可以把别名起得不一样:
// ✅ 另一种正确示范!
typedef struct Node {
int data;
struct Node* next;
} ListNode; // 别名叫 ListNode,这样就不会混淆了
这两种方式都是正确的。第一种方式虽然标签和别名都叫Node,但因为struct Node和Node在语法上是不同的符号,所以不会冲突。
掌握了自引用,你就解锁了数据结构的大门!链表、二叉树、图...这些听起来高大上的东西,底层都是靠这个“自我引用”的原理构建起来的。
三、 深入理解:结构体内存对齐 —— 计算机的“强迫症”
好了,前面我们学会了怎么声明、创建和使用结构体。现在,我们来探讨一个更深层次、也更“硬核”的话题:内存对齐。
这个知识点是面试和笔试中的常客,很多同学在这里栽了跟头。别怕,我会用最通俗的方式,让你不仅知道“怎么做”,更明白“为什么”。
3.1 为什么要内存对齐?—— 效率与兼容性的博弈
首先,抛出一个问题:假设有一个结构体 S1:
struct S1 {
char c1;
int i;
char c2;
};
你觉得 sizeof(struct S1) 的结果是多少?是 1(char) + 4(int) + 1(char) = 6 吗?
如果你这么想,那就错了!实际结果很可能是 12!
Why?! 这就是内存对齐在作怪。
内存对齐的根本原因有两个:
- 平台原因(兼容性):不是所有的硬件(CPU)都能随心所欲地访问内存。有些老旧的或特殊的CPU,只能在特定的内存地址(比如4的倍数、8的倍数)上读取特定大小的数据。如果你把一个4字节的
int放在地址3的位置,CPU可能直接罢工,抛出一个硬件异常。内存对齐就是为了保证程序能在各种不同的硬件平台上稳定运行。
- 性能原因(速度):这是最主要的原因!现代CPU读取内存时,通常是以“块”为单位的,比如一次读取8个字节。如果数据是对齐的(比如一个8字节的
double正好放在8的倍数地址上),CPU一次就能读完。但如果数据是不对齐的(比如一个double横跨了两个8字节块),CPU就需要读两次内存,然后再拼凑起来,效率直接腰斩!
总结一句话:内存对齐是典型的“空间换时间”策略。 我们牺牲了一点点内存空间(填充一些用不到的字节),换取了程序运行速度的巨大提升。在当今内存白菜价的时代,这点空间浪费绝对是值得的。
3.2 对齐规则详解 —— 四大黄金法则
明白了“为什么”,我们来看“怎么做”。内存对齐遵循一套严格的规则,我把它总结为四大黄金法则:
法则一:首成员对齐起点
结构体的第一个成员,永远从偏移量为0的地方开始存放。
这很好理解,第一个成员就是“排头兵”,站在队伍的最前面。
法则二:后续成员对齐数
从第二个成员开始,每个成员都要对齐到某个“对齐数”的整数倍地址处。
对齐数 = min(编译器默认对齐数, 该成员自身的大小)
这句话是核心,也是最难理解的。我们来拆解:
- 成员自身的大小:比如
char是1字节,short是2字节,int是4字节,double是8字节。
- 编译器默认对齐数:不同的编译器和操作系统有不同的默认值。
- Windows (VS):默认是8。
- Linux (gcc):没有默认值,对齐数就是成员自身的大小。
- min(a, b):取两者中较小的那个。
举个例子,在Windows VS环境下:
- 一个
char成员,对齐数 = min(8, 1) = 1。所以它可以放在任何地址。 - 一个
int成员,对齐数 = min(8, 4) = 4。所以它必须放在4的倍数地址上(如0, 4, 8, 12...)。 - 一个
double成员,对齐数 = min(8, 8) = 8。所以它必须放在8的倍数地址上。
法则三:整体大小对齐
整个结构体的总大小,必须是其内部所有成员“对齐数”中最大值的整数倍。
为什么要这样?是为了当定义结构体数组时,每个数组元素都能满足对齐要求。
法则四:嵌套结构体对齐
如果结构体里嵌套了另一个结构体,那么这个嵌套结构体的对齐要求,要看它内部成员的最大对齐数。整个大结构体的最终大小,也要符合所有成员(包括嵌套结构体内部成员)的最大对齐数。
3.3 实战演练:亲手计算结构体大小
光说不练假把式,我们来做几道经典例题,把规则彻底吃透!
练习1:struct S1
struct S1 {
char c1; // 大小1,对齐数 min(8,1)=1
int i; // 大小4,对齐数 min(8,4)=4
char c2; // 大小1,对齐数 min(8,1)=1
};
步骤分析:
- c1: 第一个成员,从偏移量0开始。占用1个字节,下一位是偏移量1。
- i: 对齐数是4。当前偏移量是1,不是4的倍数。所以需要填充3个字节(偏移量1->2->3->4),让
i从偏移量4开始存放。i占用4个字节(4,5,6,7),下一位是偏移量8。 - c2: 对齐数是1。当前偏移量8是1的倍数,可以直接放。占用1个字节(偏移量8),下一位是偏移量9。
- 整体大小:目前占用了9个字节(0-8)。最大对齐数是
max(1,4,1)=4。9不是4的倍数,所以需要填充到12(9->10->11->12)。12是4的倍数。
结论:sizeof(struct S1) = 12
练习2:struct S2
struct S2 {
char c1; // 1, 对齐数1
char c2; // 1, 对齐数1
int i; // 4, 对齐数4
};
步骤分析:
- c1: 偏移量0,占1字节,下一位1。
- c2: 对齐数1,偏移量1是1的倍数,直接放。占1字节(偏移量1),下一位2。
- i: 对齐数4。当前偏移量2,不是4的倍数。填充2个字节(2->3->4),让
i从4开始。占4字节(4,5,6,7),下一位8。 - 整体大小:占用8字节。最大对齐数4,8是4的倍数,无需填充。
结论:sizeof(struct S2) = 8
对比发现: S1和S2的成员完全一样,只是顺序不同,但大小却从12变成了8!这就是为什么我们在设计结构体时,应该把占用空间小的成员尽量集中在一起,可以有效减少内存浪费!
练习3:struct S3
struct S3 {
double d; // 8, 对齐数 min(8,8)=8
char c; // 1, 对齐数 min(8,1)=1
int i; // 4, 对齐数 min(8,4)=4
};
步骤分析:
- d: 偏移量0,占8字节(0-7),下一位8。
- c: 对齐数1,偏移量8是1的倍数,直接放。占1字节(8),下一位9。
- i: 对齐数4。当前偏移量9,不是4的倍数。需要填充到12(9->10->11->12),让
i从12开始。占4字节(12,13,14,15),下一位16。 - 整体大小:占用16字节。最大对齐数8(来自
d),16是8的倍数,OK。
结论:sizeof(struct S3) = 16
练习4:嵌套结构体 struct S4
struct S3 { // 我们刚才算过,S3的大小是16,最大对齐数是8
double d;
char c;
int i;
};
struct S4 {
char c1; // 1, 对齐数1
struct S3 s3; // 嵌套S3,它的对齐数是S3内部的最大对齐数,即8
double d; // 8, 对齐数8
};
步骤分析:
- c1: 偏移量0,占1字节,下一位1。
- s3: 对齐数是8(因为它内部有
double)。当前偏移量1,不是8的倍数。需要填充7个字节(1->2->...->8),让s3从8开始。s3本身占16字节(8-23),下一位24。 - d: 对齐数8。当前偏移量24是8的倍数,直接放。占8字节(24-31),下一位32。
- 整体大小:占用32字节。最大对齐数是
max(1,8,8)=8,32是8的倍数,OK。
结论:sizeof(struct S4) = 32
3.4 如何修改默认对齐数?—— #pragma pack
有时候,你可能觉得编译器太“抠门”,填充了太多没用的字节,想自己控制对齐方式。C语言提供了#pragma pack指令
#include<stdio.h>
#pragma pack(1) // 设置默认对齐数为1,相当于取消对齐
struct S {
char c1; // 1
int i; // 4
char c2; // 1
};
#pragma pack() // 取消设置,恢复默认对齐
int main() {
printf("%d\n", sizeof(struct S)); // 输出结果是6!
return 0;
}
#pragma pack(1) 告诉编译器:“别管什么对不对齐了,给我紧紧地挨着放!” 所以c1, i, c2 三个成员连续存放,总大小就是1+4+1=6。
注意: 修改对齐数虽然能节省空间,但可能会降低程序运行速度,甚至在某些平台上导致程序崩溃。所以,除非有特殊需求(比如网络协议、文件格式要求严格的内存布局),否则不建议随意修改。
四、 结构体传参:如何优雅地传递“大盒子”
当你写的程序越来越复杂,结构体也会越来越大。这时候,如何把结构体作为参数传递给函数,就成了一个需要认真考虑的问题。
我们来看一个例子:
#include<stdio.h>
struct S {
int data[1000]; // 一个包含1000个整数的数组
int num;
};
void print1(struct S s) { // 传结构体本身
printf("%d\n", s.num);
}
void print2(struct S* ps) { // 传结构体的地址
printf("%d\n", ps->num);
}
int main() {
struct S s = {{1,2,3,4}, 1000};
print1(s); // 传结构体
print2(&s); // 传地址
return 0;
}
这两个函数print1和print2都能正常工作,打印出num的值。那么,哪个更好呢?
答案是:毫无疑问,选print2!
为什么?
这涉及到函数调用的底层机制。当你调用一个函数时,参数需要被“压栈”,也就是复制一份放到一个临时的内存区域(栈)里。
print1(s): 传递的是整个结构体s。s有多大?int data[1000]是4000字节,加上int num是4字节,总共4004字节!这意味着每次调用print1,系统都要吭哧吭哧地复制4004个字节的数据。这不仅慢,还浪费内存!print2(&s): 传递的是结构体s的地址。一个地址(指针)有多大?在64位系统上是8个字节。无论你的结构体有多大,传递的永远是这8个字节。效率极高!
结论:
当结构体比较大时,一定要传递它的地址(指针),而不是传递结构体本身!
另外,你会发现,在print2函数内部,我们访问成员用的是ps->num,而不是ps.num。
箭头操作符 -> 是专门用于“结构体指针”访问成员的。ps->num 等价于 (*ps).num,意思是“先通过指针ps找到它指向的结构体,然后再用点操作符访问其num成员”。
记住这个口诀:
- 变量用点
.:s.num
- 指针用箭头
->:ps->num
五、 高级技巧:位段 —— 把内存榨干到极致
最后,我们来学习一个非常高阶但也非常实用的技巧:位段 (Bit Field)。
5.1 什么是位段?—— 用比特(Bit)来定义成员
前面我们讲的结构体,成员的最小单位是字节(Byte)。比如char是1字节,int是4字节。
但有时候,我们想存储的信息非常小,根本用不着一个字节。比如:
- 一个开关状态:开/关,只需要1个比特(0或1)就够了。
- 一周的星期几:1-7,用3个比特(能表示0-7)就够了。
- 一个月的日期:1-31,用5个比特(能表示0-31)就够了。
如果我们用char(8比特)来存这些数据,就有7、5、3个比特被白白浪费了!
位段,就是允许你直接以“比特(bit)”为单位来定义结构体成员,从而实现极致的内存压缩。
位段的声明和普通结构体类似,但要在成员名后面加一个冒号和数字,表示这个成员占多少个比特。
struct A {
int _a : 2; // _a 占2个比特
int _b : 5; // _b 占5个比特
int _c : 10; // _c 占10个比特
int _d : 30; // _d 占30个比特
};
这里的_a, _b等成员,它们的类型虽然是int,但实际上只使用了指定数量的比特位。
那么,sizeof(struct A)是多少呢?
答案是:8字节(64比特)。
为什么?
因为位段的内存分配规则是:按需以4字节(int)或1字节(char)为单位开辟空间。
在这个例子里,前三个成员_a(2bit) + _b(5bit) + _c(10bit) = 17bit,小于32bit(4字节),所以它们共享第一个4字节的空间。
第四个成员_d需要30bit,剩下的空间(32-17=15bit)不够放,所以系统会再开辟一个新的4字节空间给它。
总共就是4 + 4 = 8字节。
5.2 位段的内存分配细节
我们用一个更详细的例子来观察位段是如何分配内存的
#include <stdio.h>
struct S {
char a : 3; // a占3比特
char b : 4; // b占4比特
char c : 5; // c占5比特
char d : 4; // d占4比特
};
int main() {
struct S s = {0};
s.a = 10; // 10的二进制是 1010,但我们只有3比特,所以只取低3位:010 -> 2
s.b = 12; // 12的二进制是 1100,取低4位:1100 -> 12
s.c = 3; // 3的二进制是 00011,取低5位:00011 -> 3
s.d = 4; // 4的二进制是 00100,取低4位:0100 -> 4
printf("Size of struct S: %zu\n", sizeof(s)); // 通常是2或3字节,取决于编译器
return 0;
}
内存分配过程(简化版,实际情况更复杂):
- 系统分配一个
char大小的空间(1字节,8比特)。 - 放
a(3bit),剩余5bit。 - 放
b(4bit),5bit > 4bit,放得下。剩余1bit。 - 放
c(5bit),1bit < 5bit,放不下!舍弃剩余的1bit,重新分配一个新的char空间(8bit)。 - 在新的空间里放
c(5bit),剩余3bit。 - 放
d(4bit),3bit < 4bit,又放不下!再舍弃,再分配一个新的char空间。 - 在第三个空间里放
d(4bit)。
所以,这个结构体S总共占用了3个字节。
注意: 位段的具体分配方式(是从左到右还是从右到左,舍弃的位是否利用等)是不确定的,依赖于编译器的实现。这也是位段最大的问题。
5.3 位段的致命缺点:跨平台问题
正因为位段的内存分配细节没有在C语言标准中严格规定,导致它存在严重的跨平台问题。
主要体现在:
- 符号不确定性:
int位段是有符号还是无符号?不同编译器可能不同。 - 最大位数不确定:16位机器上,位段最大可能是16位;32位机器上可能是32位。如果你写了一个30位的位段,在16位机器上就可能出错。
- 分配方向不确定:成员是从内存的高位向低位分配,还是从低位向高位分配?标准没说。
- 剩余位处理不确定:当一个分配单元(如1字节)放不下下一个成员时,是舍弃剩余位,还是尽可能利用?也没说。
总结:位段虽然能节省空间,但移植性极差。 除非你写的程序只在特定的、已知的平台上运行(比如嵌入式开发、网络协议解析),否则应尽量避免使用位段。
5.5 使用位段的注意事项
由于位段成员共享同一个字节,它们的起始位置可能不是一个完整字节的开头。因此,位段成员是没有独立的内存地址的。
后果:你不能对位段成员使用取地址符&!
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 temp = 0;
scanf("%d", &temp); // 给普通变量temp取地址,没问题
sa._b = temp; // 再把值赋给位段成员
return 0;
}
总结:
到这里,我们这篇超长的“结构体完全指南”就接近尾声了。我们从最基础的“什么是结构体”,一路讲到了高阶的“位段”,涵盖了声明、初始化、内存对齐、传参等所有核心知识点。
我希望通过这篇博客,你能彻底扫清学习结构体路上的所有障碍。记住,编程是一个循序渐进的过程,不要期望一口吃成胖子。先把基础打牢,理解结构体的本质——把相关数据打包。然后再去攻克内存对齐、自引用这些难点
Happy Coding!

制作不易,点个赞呗
980

被折叠的 条评论
为什么被折叠?



