目录
[1. 结构体类型的声明](#1. 结构体类型的声明)
[1.1 结构体回顾](#1.1 结构体回顾)
[1.1.1 结构的声明](#1.1.1 结构的声明)
[1.1.2 结构体变量的创建和初始化](#1.1.2 结构体变量的创建和初始化)
[1.2 结构的特殊声明](#1.2 结构的特殊声明)
[1.3 结构的⾃引⽤](#1.3 结构的⾃引⽤)
[2.1 对⻬规则](#2.1 对⻬规则)
[2.2 为什么存在内存对⻬?](#2.2 为什么存在内存对⻬?)
[2.3 修改默认对⻬数](#2.3 修改默认对⻬数)
[3. 结构体传参](#3. 结构体传参)
[4. 结构体实现位段](#4. 结构体实现位段)
[4.1 什么是位段](#4.1 什么是位段)
[4.2 位段的内存分配](#4.2 位段的内存分配)
[4.3 位段的跨平台问题](#4.3 位段的跨平台问题)
[4.4 位段的应⽤](#4.4 位段的应⽤)
[4.5 位段使⽤的注意事项](#4.5 位段使⽤的注意事项)
1. 结构体类型的声明
前⾯我们在学习操作符的时候,已经学习了结构体的知识,这⾥稍微复习⼀下。
1.1 结构体回顾
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1.1 结构的声明
结构原型如下:
cpp
struct tag
{
member-list;
}variable-list;
描绘一本书:
注意:最后面的分号不能丢
cpp
描绘一本书
struct Book
{
char name[20];
char author[20];
float price;
char id[13];
};
1.1.2 结构体变量的创建和初始化
cpp
//描绘一本书
struct Book
{
char name[20];
char author[20];
float price;
char id[13];
};
//结构体的创建变量和初始化
int main()
{
struct Book b1 = { "你好世界","小龙",100.5,"G430481\n" }; //根据结构体的顺序来排布
struct Book b2 = { .id = "A430481",.author = "大龙",.name = "不好世界",.price = 200.5 }; //随便排序
printf("%s %s %f %s", b1.name, b1.author, b1.price, b1.id);
printf("%s %s %f %s", b2.name, b2.author, b2.price, b2.id);
return 0;
}
输出结果:

1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明。
⽐如:
cpp
结构体的特殊声明(匿名结构体)
struct //这里的名字可以省略
{
char i;
int a;
float b;
}s = {'x',100,100.5}; //写到了这里,此时的s就是变量
int main()
{
printf("%c %d %lf\n", s.i, s.a, s.b);
return 0;
}
输出结果:

上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
cpp
在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
会警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次
1.3 结构的⾃引⽤
在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?
⽐如,定义⼀个链表的节点:
cpp
struct Node
{
int data;
struct Node next;
};
上述代码正确吗?如果正确,那 **sizeof(struct Node)**是多少?
仔细分析,其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤ ⼩就会⽆穷的⼤,是不合理的。
正确的⾃引⽤⽅式:
要加一个***** 号
cpp
struct Node
{
int data;
struct Node* next;
};
在结构体⾃引⽤使⽤的过程中,夹杂了tpyedef对匿名结构体类型重命名,也容易引⼊问题,看看下⾯的代码,可⾏吗?
cpp
typedef struct
{
int data;
Node* next;
}Node;
答案是不⾏的,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使 ⽤Node类型来创建成员变量,这是不⾏的
解决⽅案如下:定义结构体不要使⽤匿名结构体了
cpp
typedef struct Node
{
int data;
struct Node* next;
}Node;
2.结构体内存对齐
我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。
这也是⼀个特别热⻔的考点:结构体内存对⻬
2.1 对⻬规则
⾸先得掌握结构体的对⻬规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
V S 中默认的值为 :8
Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
练习1:
cpp
struct S
{
char c1; //原本占1个字节,第一个结构体成员变量对齐起始位置0,
int i; //原本占4个字节1,对齐后占4个字节,它的存放位置必须是4的倍数,所以从4开始往后存放4个字节
char c2; //原本占1个字节,对齐后占1个字节,它的存放位置必须是4的倍数,
};
int main()
{
struct S b1 = { 0 };
printf("%zd\n", sizeof(b1)); //12
return 0;
}
输出结果:

解析:
-
第一个成员在偏移量0处
-
其他成员对齐数 =
min(自身大小, 默认对齐数8) -
结构体总大小 = 最大对齐数(成员对齐数的最大值)的整数倍
第一步:char c1
-
大小:1字节
-
对齐数 = min(1, 8) = 1
-
偏移量:0
-
占用:偏移0
第二步:int i
-
大小:4字节
-
对齐数 = min(4, 8) = 4
-
下一个可用偏移是1,但1不是4的倍数
-
需要填充3字节(偏移1-3)
-
从偏移4开始存放
-
占用:偏移4-7(4个字节)
第三步:char c2
-
大小:1字节
-
对齐数 = min(1, 8) = 1
-
下一个可用偏移是8,8是1的倍数
-
占用:偏移8
当前布局:
-
偏移0: c1
-
偏移1-3: 填充(3字节)
-
偏移4-7: i(4字节)
-
偏移8: c2
-
已占用到偏移8,共9个字节
结构体总大小计算:
最大对齐数 = max(1, 4, 1) = 4
总大小必须是4的整数倍
-
当前9字节,不是4的倍数
-
需要再填充3字节到12字节(偏移9-11)
最终内存布局(共12字节):
cpp
字节0: c1
字节1: 填充
字节2: 填充
字节3: 填充
字节4-7: int i(4字节)
字节8: c2
字节9: 填充
字节10: 填充
字节11: 填充
练习2:
cpp
struct S
{
char c1; //1 ,对齐1,但是第一个结构体成员会存放在起始位置0处,然后往后存放字节
char c2; //1 ,对齐1
int i; //4 ,对齐4
};
int main()
{
struct S b1 = { 0 };
printf("%zd\n", sizeof(b1)); //8
return 0;
}
输出结果:

这里的分析和上面的一模一样,这里我就不在和大家解析了.
练习3:
cpp
struct S
{
double d; //8
char c; //1
int i; //4
};
int main()
{
struct S b1 = { 0 };
printf("%zd\n", sizeof(b1)); //16
return 0;
}
输出结果:

解析还是和上面一模一样
练习4:(嵌套结构体)
cpp
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 s4 = { 0 };
printf("%zd\n", sizeof(s4)); //16
return 0;
}
输出结果:

解析:
嵌套结构体规则:
-
结构体成员的对齐数 =
min(成员自身大小, 默认对齐数8) -
嵌套的结构体成员 的对齐数 =
min(该结构体的最大对齐数, 默认对齐数8) -
结构体总大小 = 最大对齐数(成员对齐数最大值)的整数倍
成员分析(默认对齐数=8):
-
double d(大小8)
-
对齐数 = min(8, 8) = 8
-
偏移0-7
-
-
char c(大小1)
-
对齐数 = 1
-
下一个偏移8是1的倍数 → 偏移8
-
占用偏移8
-
-
int i(大小4)
-
对齐数 = min(4, 8) = 4
-
下一个偏移9,但9不是4的倍数
-
填充3字节(偏移9-11),使偏移达到12(12是4的倍数)
-
占用偏移12-15
-
struct S3 当前占用:0-15 共16字节
计算S3总大小:
-
S3的最大对齐数 = max(8, 1, 4) = 8
-
当前已用16字节,16是8的倍数 ✓
-
sizeof(S3) = 16
内存布局图:
cpp
字节0-7: double d
字节8: char c
字节9-11: 填充
字节12-15: int i
再分析 struct S4
成员分析(默认对齐数=8):
-
char c1(大小1)
-
对齐数 = 1
-
偏移0
-
-
struct S3 s3
-
关键点 :嵌套结构体的对齐数不是它的大小16,而是S3的最大对齐数
-
S3的最大对齐数 = 8(来自double d)
-
所以s3的对齐数 = min(8, 8) = 8
-
下一个偏移是1,但1不是8的倍数
-
填充7字节(偏移1-7),使偏移达到8
-
s3占用偏移8-23(因为S3大小=16)
-
-
double d(大小8)
-
对齐数 = 8
-
下一个偏移24是8的倍数 ✓
-
占用偏移24-31
-
struct S4 当前占用:0-31 共32字节
计算S4总大小:
-
S4的最大对齐数 = max(1, 8, 8) = 8
-
当前已用32字节,32是8的倍数 ✓
-
sizeof(S4) = 32
总的图解内存布局
cpp
struct S4 内存布局(32字节):
字节0: c1
字节1-7: 填充(7字节)
字节8-23: struct S3(16字节)
字节24-31: double d(8字节)
其中struct S3内部布局:
字节8-15: double d(S3的d)
字节16: char c(S3的c)
字节17-19: 填充(3字节)
字节20-23: int i(S3的i)
2.2 为什么存在内存对⻬?
⼤ 部分的参考资料都是这样说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
让占⽤空间⼩的成员尽量集中在⼀起
cpp
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别
2.3 修改默认对⻬数
#pragma pack这个预处理指令,可以改变编译器的默认对⻬数。
cpp
#pragma pack (1) //设置对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack () ///取消设置的对⻬数,还原为默认
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}
输出结果:

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
关键知识点:#pragma pack 指令
作用:修改编译器的内存对齐规则
-
#pragma pack(n):设置新的对齐数为 n(n=1,2,4,8,16...) -
#pragma pack():恢复默认对齐数8 -
生效范围 :从声明位置开始,直到遇到另一个
#pragma pack()或文件结束
对齐规则变化:
-
未设置时:成员对齐数 =
min(自身大小, 默认对齐数8) -
设置后:成员对齐数 =
min(自身大小, n)
1. 设置 #pragma pack(1) 的含义
对齐数设为 1 表示:每个成员必须放在偏移量是 1 的倍数的位置
-
由于 1 是所有整数的因数,所以不需要任何填充
-
成员直接按顺序紧密排列
布局过程:
-
char c1
-
对齐数 = min(1, 1) = 1
-
偏移量:0(因为0是1的倍数)
-
-
int i
-
对齐数 = min(4, 1) = 1
-
下一个可用偏移:1
-
1是1的倍数 ✓
-
占用偏移:1-4(4个字节)
-
-
char c2
-
对齐数 = min(1, 1) = 1
-
下一个可用偏移:5
-
5是1的倍数 ✓
-
占用偏移:5
-
3. 结构体总大小计算
-
当前占用:偏移0-5,共6个字节
-
最大对齐数 = max(1, 1, 1) = 1
-
总大小必须是1的倍数:6是1的倍数 ✓
-
最终大小 = 6字节
内存布局图(紧密排列,无填充):
cpp
字节0: char c1
字节1: int i(第1字节)
字节2: int i(第2字节)
字节3: int i(第3字节)
字节4: int i(第4字节)
字节5: char c2
3. 结构体传参
第一个代码(传参,传值)
cpp
struct S
{
int arr[1000];
int n;
double d;
};
void print1(const struct S tmp)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("\n");
printf("%d \n", tmp.n); //访问值的时候,打印结构体用 . 号
printf("%0.2lf", tmp.d);
}
int main()
{
struct S s={{1,2,3,4,5},100,3.15 };
print1(s);
return 0;
}
输出结果:

第二个代码(传参,传地址)比上面的代码更加有效率
cpp
struct S
{
int arr[1000];
int n;
double d;
};
void print2(const struct S * ps)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
printf("%d \n", (*ps).n); //访问指针的时候,打印结构体 用 -> 号 ps->n == (*ps).n
printf("%0.2lf ",ps->d);
}
int main()
{
struct S s = { {1,2,3,4,5},100,3.15 };
print2(&s);
return 0;
}
第二个代码比第一个代码更加好,更加有效率
原因:
- 函 数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下 降。
结论:结构体传参的时候,要传结构体的地址。
4. 结构体实现位段
结构体讲完就得讲讲结构体实现位段的能⼒。
4.1 什么是位段
这里的位是指二进制位
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int 、 unsigned int 或 signed int ,在C99中位段成员的类型也可以 选择其他类型。
- 位段的成员名后边有⼀个冒号和⼀个数字。
像这种**'' : 数字 ''**这种结果的表达式,称之为 位段式结构
例如:int a : 2; 表示成员 a 只占用 2 个比特位。
cpp
struct S
{
int _a : 2; //前面加 _ 是为了好区分,这里可加可不加
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%zd\n", sizeof(struct S)); //输出8
return 0;
}
输出结果:

解析:
位段内存分配规则
知识点:
-
位段类型决定存储单元大小 :这里所有成员都是
int类型-
在32位系统中,
int通常为4字节(32位) -
位段的存储以该类型的大小为分配单位
-
-
分配顺序:
-
从第一个成员开始,在当前存储单元(4字节)中按需分配比特位
-
如果当前单元剩余空间不足,会开辟新的存储单元
-
具体分配过程
第一个存储单元(4字节 = 32位):
-
a:2→ 占用 2 位,剩余 30 位 -
b:5→ 占用 5 位,剩余 25 位 -
c:10→ 占用 10 位,剩余 15 位 -
d:30→ 需要 30 位,但当前单元只剩 15 位,不够!
需要第二个存储单元:
-
因为
d需要30位,第一个单元放不下 -
编译器会分配第二个存储单元(又一个4字节)给
d -
注意:即使第二个单元只用了一部分(30位),整个单元(4字节)都被占用
4. 总大小计算
-
第一个存储单元:4字节(完全使用)
-
第二个存储单元:4字节(用了30位,但整个单元被分配)
-
总大小 = 4 + 4 = 8字节
内存布局示意图:
cpp
存储单元1(4字节 = 32位):
[2位a][5位b][10位c][15位未使用]
存储单元2(4字节 = 32位):
[30位d][2位未使用]
4.2 位段的内存分配
位段的成员可以是 int unsigned int signed int或者是 char等类型
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
位段的空间上是按照需要以4个字节(int)或者1个字节(char)的⽅式来开辟的。
cpp
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(s)); //输出3
return 0;
}
输出结果:

解析:
-
分配原则:
-
从第一个成员开始,在当前单元内按需分配比特位
-
如果当前单元剩余空间不够放下一个成员,开启新单元
-
一个位段成员不能跨单元存储
-
-
位段的实际取值:
-
如果赋值超过位段能表示的范围,会截断高位
-
例如:
char a : 3最大存储值为 7(二进制111) -
赋值 10(二进制1010) → 截断为 010(二进制)→ 值2
-
成员规格:
cpp
char a : 3; // 需要3位
char b : 4; // 需要4位
char c : 5; // 需要5位
char d : 4; // 需要4位
总需求:3 + 4 + 5 + 4 = 16位 = 2字节
分配步骤:
第一字节(8位):
-
a:3→ 占用3位,剩余5位 -
b:4→ 需要4位,剩余5位够用 → 占用4位,剩余1位 -
c:5→ 需要5位,但只剩1位,不够!
开启第二字节:
-
c:5从第二字节开始分配 -
第二字节:
c:5→ 占用5位,剩余3位 -
d:4→ 需要4位,但只剩3位,不够!
开启第三字节:
-
d:4从第三字节开始分配 -
第三字节:
d:4→ 占用4位,剩余4位未使用
最终内存布局:
cpp
字节1:[3位a][4位b][1位未使用]
字节2:[5位c][3位未使用]
字节3:[4位d][4位未使用]
总大小:3个字节(虽然总需求只有16位,但分配方式导致需要3个字节)
成员赋值与截断分析
cpp
struct S s = { 0 };
s.a = 10; // 二进制: 1010,但a只有3位
s.b = 12; // 二进制: 1100
s.c = 3; // 二进制: 0011
s.d = 4; // 二进制: 0100
1. s.a = 10
-
10的二进制:
1010(4位) -
a只有3位,保留低3位:010 -
实际存储值 :二进制
010= 十进制2
2. s.b = 12
-
12的二进制:
1100(4位) -
b正好4位,完整存储:1100 -
实际存储值 :二进制
1100= 十进制12
3. s.c = 3
-
3的二进制:
0011(4位) -
c有5位,存储为:00011 -
实际存储值 :二进制
00011= 十进制3
4. s.d = 4
-
4的二进制:
0100(4位) -
d有4位,存储为:0100 -
实际存储值 :二进制
0100= 十进制4
4.3 位段的跨平台问题
-
int 位段被当成有符号数还是⽆符号数是不确定的。
-
位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。
-
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
-
当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在
4.4 位段的应⽤
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的。

4.5 位段使⽤的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。
cpp
struct S
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct S s = { 0 };
//scanf("%d", &(s.b)); //这个代码是错误的,因为位段是不允许取地址的
//正确的写法
int b = 0;
scanf("%d", &b); //先创建一个变量,然后在取地址,这样就可以了
s.b = b;
return 0;
}
以上就是我们的全部的内容了,谢谢大家!!!