目录
[1.2 结构的特殊声明](#1.2 结构的特殊声明)
[1.3 结构的自引用](#1.3 结构的自引用)
[2.1 内存对齐规则](#2.1 内存对齐规则)
[练习1:struct S1](#练习1:struct S1)
[练习2:struct S2](#练习2:struct S2)
[练习3:struct S3](#练习3:struct S3)
[练习4:struct S4(嵌套S3)](#练习4:struct S4(嵌套S3))
[24 、修改默认对⻬数](#24 、修改默认对⻬数)
[3、 结构体传参](#3、 结构体传参)
1、结构体类型声明
我们在学习操作符时已经接触过结构体的相关知识,这里先简单回顾一下。
1.1、结构体回顾
cpp
1、结构体类型声明
结构体(Structure)是C语言中一种重要的复合数据类型,它允许将不同类型的数据组合成一个整体。结构体类型的声明使用`struct`关键字,基本语法格式如下:
```c
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
//...更多成员
数据类型 成员n;
}variable_list;;
其中:
struct
是声明结构体的关键字- 结构体名是用户自定义的标识符
- 大括号内是该结构体包含的成员变量声明
- 每个成员声明后需要加分号
- 整个结构体声明以分号结束
variable-list
是可选的,可以在定义结构体的同时声明该类型的变量。
c
struct Point {
int x;
int y;
} p1, p2;
上面的代码定义了一个名为 Point
的结构体,包含两个整型成员 x
和 y
,同时声明了两个 Point
类型的变量 p1
和 p2
。结构体定义后,可以通过成员访问运算符 .
来访问其成员变量:
c
p1.x = 10;
p1.y = 20;
在C语言中,结构体定义后使用时必须带上 struct
关键字,除非使用 typedef
进行重命名:
c
typedef struct Point {
int x;
int y;
} Point;
Point p3; // 不需要写 struct Point
在C++中,结构体定义后可以直接使用结构体名称声明变量,无需 struct
关键字。
示例1:声明一个表示学生的结构体
c
struct Student {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
char gender; // 性别
};
示例2:声明一个表示日期的结构体
c
struct Date {
int year;
int month;
int day;
};
结构体声明时需要注意:
- 结构体声明本身不分配内存,只有定义了结构体变量才会分配内存
- 结构体成员可以是任何数据类型,包括基本类型、数组、指针,甚至是其他结构体
- 结构体可以嵌套声明,例如:
c
struct Person {
char name[20];
struct Date birthday; // 嵌套Date结构体
};
结构体声明通常放在头文件(.h)中,以便多个源文件共享使用。
1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明。
1.2.1、匿名结构体类型分析
匿名结构体(unnamed struct)是指在声明时没有提供结构体标签(tag)的结构体类型。这种结构体可以直接定义变量,但不能在其他地方复用该类型。
代码示例1:
c
struct {
int a;
char b;
float c;
} x;
这里定义了一个匿名结构体并直接声明了一个变量x
。由于没有标签,后续无法通过struct tag
的方式声明其他变量。
代码示例2:
c
struct {
int a;
char b;
float c;
} a[20], *p;
同样定义了一个匿名结构体,但声明了一个数组a[20]
和一个指针p
。由于没有标签,无法在其他地方声明相同类型的变量。
1.2.2、匿名结构体的特点
匿名结构体的成员可以正常访问,例如:
c
x.a = 10;
p->b = 'X';
但编译器会将两个匿名结构体视为不同的类型,即使它们的成员完全一致。因此以下代码会报错:
c
p = &x; // 错误:类型不兼容
1.2.3、匿名结构体的适用场景
- 临时使用的结构体,不需要复用类型。
- 结构体仅用于单个变量或少量变量时。
- 嵌套在联合体或其他结构体中作为匿名成员(C11标准支持)。
如果需要复用结构体类型,应使用带标签的声明方式:
c
struct named_tag {
int a;
char b;
float c;
};
struct named_tag y, z; // 合法
1.3 结构的自引用
结构体中是否可以包含自身类型的成员?例如,定义链表节点时:
cpp
struct Node
{
int data;
struct Node next;
};
这段代码是否正确?如果正确,sizeof(struct Node) 的值是多少?经过仔细分析,这段代码存在不合理之处。因为结构体中如果包含同类型的结构体变量,会导致结构体大小无限增长,这是不合理的实现方式。正确的自引用方式应该是使用指针。
c
struct Node
{
int data;
struct Node* next;
};
该代码定义了一个结构体 Node
,其中包含一个 int
类型的 data
成员和一个 struct Node
类型的 next
成员。这种定义方式会导致结构体无限递归,因为 next
成员本身又是一个完整的 Node
结构体,而 Node
又包含 next
,如此循环下去,无法计算其大小。
结构体不能直接包含自身类型的成员变量,因为这样会导致:
- 结构体大小无法计算(无限递归)。
- 编译器无法分配内存,因为
sizeof(struct Node)
会无限增长。
在使用结构体自引用时,若结合typedef对匿名结构体类型进行重命名,可能会引发问题。请观察以下代码示例,其可行性如何?
cpp
typedef struct
{
int data;
Node* next;
}Node;
答案是不行的。因为Node是对前面的匿名结构体类型的重命名,但在匿名结构体内部提前使用Node类型来创建成员变量,这是不允许的。
优化后的表达如下:
定义结构体不要使用匿名结构体了
c
typedef struct Node {
int data;
struct Node* next;
} Node;
2、结构体内存对齐
我们已经掌握了结构体的基本用法。现在让我们深入探讨一个关键问题:如何计算结构体的大小。这同时也是面试中的高频考点。
2.1 内存对齐规则
结构体的内存对齐需遵循以下规则:
-
结构体首成员从偏移量为0的地址开始存放
-
其余成员需对齐到对齐数的整数倍地址:
- 对齐数 = min(编译器默认对齐数,该成员大小)
- VS默认对齐数为8
- Linux gcc无默认对齐数,对齐数等于成员自身大小
-
结构体总大小为最大对齐数的整数倍(取所有成员对齐数的最大值)
-
嵌套结构体的情况:
- 嵌套的结构体成员对齐到其内部最大对齐数的整数倍处
- 整体结构体大小须为所有最大对齐数(含嵌套结构体)的整数倍
2.2、结构体大小计算分析
结构体的大小计算涉及内存对齐原则,不同编译器和平台可能有不同对齐规则。以下基于常见对齐规则(如默认4字节对齐)进行分析:

练习1:struct S1
c
struct S1 {
char c1; // 1字节
int i; // 4字节(对齐到4的倍数)
char c2; // 1字节
};
c1
占用1字节,起始偏移0i
需要4字节对齐,因此在c1
后填充3字节(偏移1→4)c2
占用1字节,偏移8- 结构体总大小需为最大成员(
int
)对齐值的整数倍,最终填充到12字节
输出结果 :12
练习2:struct S2
c
struct S2 {
char c1; // 1字节
char c2; // 1字节
int i; // 4字节(对齐到4的倍数)
};
c1
和c2
连续存放,占用2字节(偏移0-1)i
需要4字节对齐,在c2
后填充2字节(偏移2→4)- 结构体总大小为8字节(无需额外填充)
输出结果 :8
练习3:struct S3
c
struct S3 {
double d; // 8字节
char c; // 1字节
int i; // 4字节(对齐到4的倍数)
};
d
占用8字节,起始偏移0c
占用1字节,偏移8i
需要4字节对齐,在c
后填充3字节(偏移9→12)- 结构体总大小为16字节(无需额外填充)
输出结果 :16
练习4:struct S4(嵌套S3)
c
struct S4 {
char c1; // 1字节
struct S3 s3; // 16字节(对齐到8的倍数)
double d; // 8字节
};
c1
占用1字节,起始偏移0s3
需要8字节对齐(因其最大成员为double
),在c1
后填充7字节(偏移1→8)s3
自身大小为16字节(偏移8-23)d
占用8字节,偏移24(已对齐)- 结构体总大小为32字节(无需额外填充)
输出结果 :32
2.3、内存对齐的必要性
内存对齐主要基于两个关键原因:
-
硬件兼容性 并非所有硬件平台都支持任意地址的数据访问。某些平台只能在特定地址读取特定类型的数据,否则会触发硬件异常。这种限制使得内存对齐成为跨平台兼容的重要考量。
-
性能优化 对齐的数据结构(特别是栈结构)能显著提升访问效率。处理器访问未对齐内存需要两次操作,而对齐内存只需一次。例如:一个8字节读取的处理器,若double类型数据都按8字节对齐存储,就能单次完成读写;否则数据可能跨越两个内存块,导致需要两次访问。
简而言之,内存对齐是以空间换取时间的优化策略。
在设计结构体时,如何兼顾内存对齐和空间节省:让占用空间小的成员尽量集中在⼀起
cpp
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了一些区别。
24 、修改默认对⻬数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
cpp
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
3、 结构体传参
cpp
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;
}
print1
和 print2
函数哪个更好?答案是:优先选择 print2
。
原因:函数传参时需要进行参数压栈操作,会产生时间和空间上的系统开销。当传递大型结构体对象时,由于参数压栈的系统开销过大,会导致性能显著下降。
4、结构体实现位段
4.1、位段的概念
位段(Bit-field)是C语言中结构体的一种特殊用法,允许按位来定义成员变量的大小。通过位段可以有效节省内存空间,特别适合存储开关标志或状态码等小范围数据。
4.2、位段的语法
在结构体定义中,通过在成员变量后加上冒号和位数来声明位段:
c
struct 结构体名 {
类型 成员名1 : 位数1;
类型 成员名2 : 位数2;
...
};
- 类型 :通常为
int
、unsigned int
或signed int
(C99后支持_Bool
)。 - 位数:指定该成员占用的二进制位数(1到类型位宽之间)。
4.3、位段的特性
- 内存分配单位:位段成员按定义顺序从低位到高位布局,具体对齐方式由编译器决定。
- 跨字节处理 :当位段总位数超过一个存储单元(如
int
的位数)时,可能跨越多个单元。 - 未命名位段 :可定义无名位段实现填充,例如
unsigned : 4;
表示跳过4位。 - 零宽度位段 :定义长度为0的位段(如
unsigned : 0;
)会强制下一个位段从新存储单元开始。
4.4、示例代码
c
#include <stdio.h>
struct Status {
unsigned flag1 : 1; // 1位,表示布尔值
unsigned flag2 : 3; // 3位,范围0~7
unsigned : 4; // 4位填充(未使用)
unsigned mode : 2; // 2位,范围0~3
};
int main() {
struct Status s;
s.flag1 = 1;
s.flag2 = 5;
s.mode = 2;
printf("Sizeof struct: %zu bytes\n", sizeof(s)); // 通常输出1或4(依赖对齐)
return 0;
}
4.5、注意事项
- 可移植性:位段的具体内存布局因编译器和平台而异,跨平台时需谨慎。
- 地址操作 :无法对位段成员取地址(如
&s.flag1
是非法的)。 - 符号处理 :使用
signed int
时,最高位会被视为符号位。