目录

1、结构体类型声明

1.1、结构体回顾

1.2 结构的特殊声明

1.2.1、匿名结构体类型分析

1.2.2、匿名结构体的特点

1.2.3、匿名结构体的适用场景

1.3 结构的自引用

2、结构体内存对齐

2.1 内存对齐规则

2.2、结构体大小计算分析

练习1:struct S1

练习2:struct S2

练习3:struct S3

练习4:struct S4(嵌套S3)

2.3、内存对齐的必要性

24 、修改默认对⻬数

3、 结构体传参

4、结构体实现位段

4.1、位段的概念

4.2、位段的语法

4.3、位段的特性

4.4、示例代码

4.5、注意事项

1、结构体类型声明 我们在学习操作符时已经接触过结构体的相关知识,这里先简单回顾一下。

1.1、结构体回顾

1、结构体类型声明

结构体(Structure)是C语言中一种重要的复合数据类型,它允许将不同类型的数据组合成一个整体。结构体类型的声明使用`struct`关键字,基本语法格式如下:

```c

struct 结构体名 {

数据类型 成员1;

数据类型 成员2;

//...更多成员

数据类型 成员n;

}variable_list;;

其中:

struct是声明结构体的关键字结构体名是用户自定义的标识符大括号内是该结构体包含的成员变量声明每个成员声明后需要加分号整个结构体声明以分号结束variable-list 是可选的,可以在定义结构体的同时声明该类型的变量。

struct Point {

int x;

int y;

} p1, p2;

上面的代码定义了一个名为 Point 的结构体,包含两个整型成员 x 和 y,同时声明了两个 Point 类型的变量 p1 和 p2。结构体定义后,可以通过成员访问运算符 . 来访问其成员变量:

p1.x = 10;

p1.y = 20;

在C语言中,结构体定义后使用时必须带上 struct 关键字,除非使用 typedef 进行重命名:

typedef struct Point {

int x;

int y;

} Point;

Point p3; // 不需要写 struct Point

在C++中,结构体定义后可以直接使用结构体名称声明变量,无需 struct 关键字。

示例1:声明一个表示学生的结构体

struct Student {

int id; // 学号

char name[20]; // 姓名

float score; // 成绩

char gender; // 性别

};

示例2:声明一个表示日期的结构体

struct Date {

int year;

int month;

int day;

};

结构体声明时需要注意:

结构体声明本身不分配内存,只有定义了结构体变量才会分配内存结构体成员可以是任何数据类型,包括基本类型、数组、指针,甚至是其他结构体结构体可以嵌套声明,例如:

struct Person {

char name[20];

struct Date birthday; // 嵌套Date结构体

};

结构体声明通常放在头文件(.h)中,以便多个源文件共享使用。

1.2 结构的特殊声明 在声明结构的时候,可以不完全的声明。

1.2.1、匿名结构体类型分析 匿名结构体(unnamed struct)是指在声明时没有提供结构体标签(tag)的结构体类型。这种结构体可以直接定义变量,但不能在其他地方复用该类型。

代码示例1:

struct {

int a;

char b;

float c;

} x; 这里定义了一个匿名结构体并直接声明了一个变量x。由于没有标签,后续无法通过struct tag的方式声明其他变量。

代码示例2:

struct {

int a;

char b;

float c;

} a[20], *p; 同样定义了一个匿名结构体,但声明了一个数组a[20]和一个指针p。由于没有标签,无法在其他地方声明相同类型的变量。

1.2.2、匿名结构体的特点 匿名结构体的成员可以正常访问,例如:

x.a = 10;

p->b = 'X'; 但编译器会将两个匿名结构体视为不同的类型,即使它们的成员完全一致。因此以下代码会报错:

p = &x; // 错误:类型不兼容 1.2.3、匿名结构体的适用场景临时使用的结构体,不需要复用类型。结构体仅用于单个变量或少量变量时。嵌套在联合体或其他结构体中作为匿名成员(C11标准支持)。 如果需要复用结构体类型,应使用带标签的声明方式:

struct named_tag {

int a;

char b;

float c;

};

struct named_tag y, z; // 合法 1.3 结构的自引用结构体中是否可以包含自身类型的成员?例如,定义链表节点时:

struct Node

{

int data;

struct Node next;

};

这段代码是否正确?如果正确,sizeof(struct Node) 的值是多少?经过仔细分析,这段代码存在不合理之处。因为结构体中如果包含同类型的结构体变量,会导致结构体大小无限增长,这是不合理的实现方式。正确的自引用方式应该是使用指针。

struct Node

{

int data;

struct Node* next;

};

该代码定义了一个结构体 Node,其中包含一个 int 类型的 data 成员和一个 struct Node 类型的 next 成员。这种定义方式会导致结构体无限递归,因为 next 成员本身又是一个完整的 Node 结构体,而 Node 又包含 next,如此循环下去,无法计算其大小。

结构体不能直接包含自身类型的成员变量,因为这样会导致:

结构体大小无法计算(无限递归)。编译器无法分配内存,因为 sizeof(struct Node) 会无限增长。 在使用结构体自引用时,若结合typedef对匿名结构体类型进行重命名,可能会引发问题。请观察以下代码示例,其可行性如何?

typedef struct

{

int data;

Node* next;

}Node;

答案是不行的。因为Node是对前面的匿名结构体类型的重命名,但在匿名结构体内部提前使用Node类型来创建成员变量,这是不允许的。

优化后的表达如下:

定义结构体不要使用匿名结构体了

typedef struct Node {

int data;

struct Node* next;

} Node;

2、结构体内存对齐 我们已经掌握了结构体的基本用法。现在让我们深入探讨一个关键问题:如何计算结构体的大小。这同时也是面试中的高频考点。

2.1 内存对齐规则 结构体的内存对齐需遵循以下规则:

结构体首成员从偏移量为0的地址开始存放

其余成员需对齐到对齐数的整数倍地址:

对齐数 = min(编译器默认对齐数,该成员大小)VS默认对齐数为8Linux gcc无默认对齐数,对齐数等于成员自身大小结构体总大小为最大对齐数的整数倍(取所有成员对齐数的最大值)

嵌套结构体的情况:

嵌套的结构体成员对齐到其内部最大对齐数的整数倍处整体结构体大小须为所有最大对齐数(含嵌套结构体)的整数倍2.2、结构体大小计算分析结构体的大小计算涉及内存对齐原则,不同编译器和平台可能有不同对齐规则。以下基于常见对齐规则(如默认4字节对齐)进行分析:

练习1:struct S1

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

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

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)

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字节对齐存储,就能单次完成读写;否则数据可能跨越两个内存块,导致需要两次访问。

简而言之,内存对齐是以空间换取时间的优化策略。

在设计结构体时,如何兼顾内存对齐和空间节省:让占用空间小的成员尽量集中在⼀起

//例如:

struct S1

{

char c1;

int i;

char c2;

};

struct S2

{

char c1;

char c2;

int i;

};

S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了一些区别。

24 、修改默认对⻬数 #pragma 这个预处理指令,可以改变编译器的默认对齐数。

#include

#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、 结构体传参

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、位段的语法 在结构体定义中,通过在成员变量后加上冒号和位数来声明位段:

struct 结构体名 {

类型 成员名1 : 位数1;

类型 成员名2 : 位数2;

...

};

类型:通常为int、unsigned int或signed int(C99后支持_Bool)。位数:指定该成员占用的二进制位数(1到类型位宽之间)。4.3、位段的特性内存分配单位:位段成员按定义顺序从低位到高位布局,具体对齐方式由编译器决定。跨字节处理:当位段总位数超过一个存储单元(如int的位数)时,可能跨越多个单元。未命名位段:可定义无名位段实现填充,例如unsigned : 4;表示跳过4位。零宽度位段:定义长度为0的位段(如unsigned : 0;)会强制下一个位段从新存储单元开始。4.4、示例代码

#include

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时,最高位会被视为符号位。