目录
- 1.结构体类型的声明
-
- [1.1 结构体回顾](#1.1 结构体回顾)
-
- [1.1.1 结构的声明](#1.1.1 结构的声明)
- [1.1.2 结构体变量的创建和初始化](#1.1.2 结构体变量的创建和初始化)
- [1.2 结构的特殊申明](#1.2 结构的特殊申明)
-
- [1.2.1 匿名结构体变量的使用](#1.2.1 匿名结构体变量的使用)
- [1.2.2 匿名结构体指针的使用](#1.2.2 匿名结构体指针的使用)
- [1.3 结构体的自引用](#1.3 结构体的自引用)
- [2. 结构体内存对齐](#2. 结构体内存对齐)
-
- [2.1 对齐规则](#2.1 对齐规则)
-
- [2.1.1 练习1](#2.1.1 练习1)
- [2.2.2 练习2](#2.2.2 练习2)
- [2.2.3 练习3](#2.2.3 练习3)
- [2.2.4 练习4](#2.2.4 练习4)
- [2.2 为什么存在内存对齐?](#2.2 为什么存在内存对齐?)
- [2.3 修改默认参数](#2.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.结构体类型的声明
前面我们在讲解C语言操作符详解时,讲到了结构体的相关知识点,我们再来复习一下。
1.1 结构体回顾
结构是一些值的集合,这些值就叫做成员变量。结构的每个成员可以是不同类型的变量。
1.1.1 结构的声明
c
struct Tag //tag是自定义名字
{
member-list; //成员列表:1个或者是多个
}variable-list; //变量列表
//分号不要掉了
描述一个学生:
c
struct Student
{
char name[20];
int ahe;
double score;
};
1.1.2 结构体变量的创建和初始化
c
#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);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .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;
}
1.2 结构的特殊申明
在声明结构的时候,可以不完全的声明。
c
//匿名结构体类型
struct
{
int a;
char b;
float c;
}t;
struct
{
int a;
char b;
float c;
}* p = &t;
在结构体声明的时候,匿名结构体类型 可能会触发警告:
- 编译器会将上面两个结构体的声明当成两个完全不同的类型,所以是非法的。 编译器会将上面两个结构体的声明当成两个完全不同的类型,所以是非法的。 编译器会将上面两个结构体的声明当成两个完全不同的类型,所以是非法的。
- 匿名的结构体,如果没有对结构体进行重命名的,基本上只能使用一次。 匿名的结构体,如果没有对结构体进行重命名的,基本上只能使用一次。 匿名的结构体,如果没有对结构体进行重命名的,基本上只能使用一次。
注意:每写一次匿名的结构体,都会生成一个新的结构体类型 注意:每写一次匿名的结构体,都会生成一个新的结构体类型 注意:每写一次匿名的结构体,都会生成一个新的结构体类型
1.2.1 匿名结构体变量的使用
c
struct
{
int a;
float b;
char c;
}x;
这里,我就定义了结构体变量x,因为结构体没有名字,所以我们只能通过变量x来访问它
c
int main()
{
x.a = 4;
x.b = 5.5;
x.c = 'Z';
return 0;
}
完整示例:
c
#include <stdio.h>
struct
{
int a;
float b;
char c;
}x;
int main()
{
x.a = 4;
x.b = 5.5;
x.c = 'Z';
printf("%d %f %c", x.a, x.b, x.c);
return 0;
}
画图讲解:

1.2.2 匿名结构体指针的使用
如果想要使用指针结构体,并且让它指向对应的匿名结构体,就要声明在同一块空间中(写在同一次声明中),指针和变量一般一起声明。
c
struct
{
int a;
char b;
float c;
} x, *p = &x;
这样我们就可以通过结构体指针变量p来访问x:
c
#include <stdio.h>
struct
{
int a;
float b;
char c;
}x , *p = &x;
int main()
{
x.a = 4;
x.b = 5.5;
x.c = 'Z';
printf("%d %f %c", p->a, p->b, p->c);
return 0;
}
画图讲解:

所以在开始的时候的代码是有问题的,我们的正确的写法一般有两种办法:
- 给结构体起名字 : 1. 给结构体起名字: 1.给结构体起名字:
c
#include <stdio.h>
struct Student
{
int a;
char b;
float c;
} x;
struct Student *p = &x;
int main()
{
return 0;
}
- 使用 t y p e d e f 2.使用typedef 2.使用typedef
c
#include <stdio.h>
typedef struct
{
int a;
char b;
float c;
} Student;
Student x;
Student *p = &x;
int main()
{
return 0;
}
Student 是通过 typedef 给匿名结构体 起的类型别名 , Student 代表这个结构体类型。
1.3 结构体的自引用
如果在一个结构体中包含一个类型为结构本身的成员可以吗?
c
struct Node
{
struct Node s1;
int a;
}s2;
上述代码会造成无穷递归 的问题,我们来画图演示一下:

正确的自引用方式:
c
struct Node
{
struct Node* next;
int a;
}s2;
在结构体⾃引⽤使⽤的过程中,夹杂了typedef 对匿名结构体类型重命名,也容易引⼊问题,看看下⾯的代码,可⾏吗?
c
typedef struct
{
int data;
Node* next;
}Node;
答案是不行的,Node 是通过 typedef 起的类型别名,但是,Node 这个别名只有在整个结构体定义结束之后才会生效,也就是说,当编译器读到结构体内部的Node* next时,Node还没有定义完成,编译器还不知道Node是什么类型,所以会报错。
正确的写法是给结构体加上结构体标签名:
c
typedef struct Node
{
int data;
struct Node* next;
} Node;
这样的话,编译器就知道Node是结构体类型了。
2. 结构体内存对齐
在前面,我们掌握了结构体的使用,现在,我们要深入探讨一个问题:计算结构体的大小 ,这就要涉及到结构体内存对齐的问题。
2.1 对齐规则
结构体的对齐规则有如下几点:
-
结构体的第1个成员 对⻬到和结构体变量起始位置偏移量为
0的地址处。 -
从第 2 个成员开始,每个成员都要放在对应 对齐数 的整数倍地址处。
对齐数 = 默认对齐数 和 成员自身大小 的较小值。
| 环境 | 默认对齐数 |
|---|---|
| VS | 8 |
| Linux gcc | 通常按成员自身大小对齐 |
- 结构体总大小 必须是最大对齐数 的整数倍 ,其中,最大对齐数是所有结构体成员中对齐数最大的那一个。
- 如果出现了结构体嵌套的情况:
- 嵌套结构体 作为成员时,其起始位置 要对齐到自身最大对齐数 的整数倍地址处。
- 整个结构体大小 要是所有最大对齐数 - 含嵌套结构体成员的对齐数的整数倍。
2.1.1 练习1
c
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
画图讲解:

2.2.2 练习2
c
#include <stdio.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
画图演示:

2.2.3 练习3
c
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S3));
return 0;
}
画图演示:

2.2.4 练习4
c
//结构体嵌套问题
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;
}
画图演示:

2.2 为什么存在内存对齐?
内存对齐本质上是:用一定的空间浪费,换取更高的访问效率和更好的平台兼容性。
一、平台原因:提高可移植性 一、平台原因:提高可移植性 一、平台原因:提高可移植性
不同硬件平台对内存访问的要求不同。有些平台可以访问任意地址上的任意数据; 有些平台只能在特定地址读取特定类型的数据。
如果数据没有按照平台要求对齐,可能会导致:
- 程序运行异常
- 数据读取错误
- 跨平台兼容性变差
二、性能原因:提高访问效率 二、性能原因:提高访问效率 二、性能原因:提高访问效率
数据结构通常会尽量按照 自然边界 对齐。
这样 CPU 访问内存时,可以减少访问次数,提高读取效率。
| 情况 | 内存访问次数 | 说明 |
|---|---|---|
| 已对齐 | 1 次 | 数据刚好位于合适的内存边界 |
| 未对齐 | 2 次或更多 | 数据可能跨越多个内存块,需要多次读取 |
假设处理器一次从内存中读取 8 个字节。
如果一个 double 类型数据的地址是 8 的倍数 :地址:0、8、16、24 ...,那么数据就可以一次性读取完成,如果地址不是8的倍数,例如6,该 double 数据可能会跨越两个 8 8 8 字节内存块,CPU 就需要进行两次内存访问,效率更低。
总结:结构体的内存对齐是一种拿空间 换取时间的做法。
在设计结构体的时候,我们既要满足内存对齐,还要节省空间,我们就可以让占用空间小一点的成员在一起。
c
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
我们来画图比较一下s1,s2两者的不同:

2.3 修改默认参数
#pragma 这个预处理指令,可以改变编译器的默认对⻬数。
c
#include <stdio.h>
#pragma pack(1) //设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() //取消设置的对齐数,还原为默认
int main()
{
printf("%zu\n", sizeof(struct S1));
return 0;
}
画图演示:

3.结构体传参
c
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
void print1(struct S p1)
{
for (int i = 0; i < 4;i++)
printf("%d ", p1.data[i]);
printf("\n");
printf("%d\n", p1.num);
}
void print2(const struct S* p2)
{
for (int i = 0; i < 4;i++)
printf("%d ", p2->data[i]);
printf("\n");
printf("%d\n", p2->num);
}
int main()
{
struct S s = { {1,2,3,4},4 };
print1(s); //传值调用
print2(&s);//传址调用
return 0;
}
画图演示:

一般在结构体传参的时候,我们都会选print2函数,原因:
函数传参的时候,参数时需要压栈的,会有时间和空间上的系统开销。
如果传递给一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以导致系统性能下降。
结论:结构体传参的时候,要传结构体的地址。 结论:结构体传参的时候,要传结构体的地址。 结论:结构体传参的时候,要传结构体的地址。
4. 结构体实现位段
4.1 位段是什么?
位段的声明和结构体很相似,有两个不同:
-
位段的成员必须是
int,unsigned int或sigend int,在C99中位段成员的类型也可以选择其他整型家族类型,比如:char. -
位段的成员名后面有一个冒号和数字 。
比如:
c
struct S
{
int _a : 1;
int _b : 5;
int _c : 10;
int _d : 30;
};
S就是一个位段类型。
4.2 位段的内存分配
- 位段成员通常可以是:
int,unsigned int,signed int,char。 : 后面的数字表示该成员所占用的是二进制位数(比特位)。- 位段不是以完整变量大小存储,而是按位来分配空间。常见开辟单位:
| 位段类型 | 可能的开辟单位 |
|---|---|
int |
4 字节 |
char |
1 字节 |
- 位段的内存分配特点:
- 多个位段成员会尽量放在同一个存储单元中。
- 如果当前存储单元放不下新的位段成员,就会重新开辟新的空间。
- 位段可以节省空间。
- 位段是不跨平台的,注重可移植性的程序应尽量避免使用位段
举一个例子:
c
//⼀个例⼦
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;
return 0;
}
画图演示:

4.3 位段的跨平台问题
| 序号 | 知识点 | 说明 | 影响 |
|---|---|---|---|
| 1 | 位段符号性不确定 | int 位段成员是否为有符号数,由编译器决定 |
跨平台结果可能不同 |
| 2 | 位段最大位数不确定 | 位段宽度不能超过其底层类型大小;如 16 位机器最大 16,32 位机器最大 32 | 超出位数可能报错或异常 |
| 3 | 位段内存分配方向不确定 | 位段成员可能从左向右分配,也可能从右向左分配 | 内存布局依赖编译器 |
| 4 | 剩余位处理不确定 | 当前位段剩余空间不足以存放下一个成员时,是否浪费或继续利用不确定 | 结构体大小可能不同 |
| 5 | 跨平台问题 | 位段的符号、位宽、分配方向、空间利用方式都存在实现差异 | 不适合强依赖二进制布局的场景 |
| 6 | 使用优势 | 位段可达到类似结构体的表达效果,并节省空间 | 适合状态标志、位级数据存储 |
| 7 | 总结 | 位段能节省内存,但可移植性较差 | 使用时需考虑平台和编译器差异 |
4.4 位段的应用
位段常用于网络协议 中的数据包格式,下图是⽹络协议中,IP数据报的格式。当某些属性只需几个 bit 表示时,使用位段可以节省空间,减小数据包大小,提高网络传输效率。

4.5 位段使用的注意事项
- 位段按 bit 存储,多个成员可能共用一个字节。
- 字节有地址,但字节内部的 bit 没有独立地址。
- 不能 对位段成员使用 & 取地址,因此不能直接用
scanf输入,应先输入到普通变量,再赋值给位段成员。
c
#include <stdio.h>
struct A
{
int _a : 3;
int _b : 6;
int _c : 10;
int _d : 30;
};
int main()
{
struct A s = { 0 };
scanf("%d %d %d %d", &s._a, &s._b, &s._c, &s._d); //err
//不能直接取地址
//true
int b;
scanf("%d", &b);
s._b = b;
printf("%d\n", s._b);
return 0;
}
