系列文章目录
第一章 C语言基础知识
第二章 C语言控制语句
第三章 C语言函数详解
第四章 C语言数组详解
第五章 C语言操作符详解
第六章 C语言指针详解
第七章 C语言结构体详解
第八章 详解数据在内存中的存储
第九章 C语言指针进阶
文章目录
1. 结构体
1.1 声明结构体
2.2 自引用结构
1. 单链表
2. 双链表
3. 二叉树
2.3 结构体变量的定义和初始化
2.4 结构体内存对齐
2.5 修改默认对齐数
2.6 结构体传参
1. 通过值传递结构体
2. 通过指针传递结构体
2. 位段
2.1 定义位段
2.2 位段的内存分配
3. 枚举
3.1 枚举类型的定义
3.2 使用枚举
4. 联合
4.1 联合类型的定义
4.2 使用联合
4.3 联合大小的计算
1. 结构体
1.1 声明结构体
struct Tag {
type member1;
type member2;
...
type memberN;
};
struct
:关键字,用于定义结构体。Tag
:结构体的标签(可选),可以用来声明该类型的变量。member1, member2, ..., memberN
:结构体成员,可以是任意数据类型,包括整型、浮点型、字符型、数组、甚至其他结构体。
代码示例:定义一个表示人的结构体:
struct Person {
char name[50];
int age;
float height;
};
2.2 自引用结构
自引用结构是指在结构体定义中包含至少一个指向同类型结构体的指针。这个指针可以用来指向具有相同结构的其他实例,从而形成一个链式或树形结构。
示例:定义自引用结构体
链表结构,其中每个节点包含一个整数数据和指向列表中下一个节点的指针。
struct Node {
int data; // 存储的数据
struct Node *next; // 指向下一个节点的指针
};
1. 单链表
在单链表中,每个节点包含数据和一个指向下一个节点的指针。这种结构可以用来实现队列、栈或其他动态数据集合。
void printList(struct Node *node) {
while (node != NULL) {
printf("%d ", node->data);
node = node->next;
}
}
2. 双链表
在双链表中,每个节点除了有指向下一个节点的指针外,还有一个指向前一个节点的指针,允许在两个方向上遍历列表。
struct DNode {
int data;
struct DNode *prev;
struct DNode *next;
};
3. 二叉树
在二叉树的实现中,每个节点包含数据和两个指向子节点的指针(左子节点和右子节点)。这种结构用于实现高效的搜索、插入和删除操作。
struct TreeNode {
int value;
struct TreeNode *left;
struct TreeNode *right;
};
2.3 结构体变量的定义和初始化
定义结构体类型
struct Person {
char name[100];
int age;
float height;
};
定义结构体变量
struct Person person1;
//定义了一个名为 person1 的变量,其类型为 struct Person
初始化结构体变量
struct Person person1 = {"TENET", 20, 2};
初始化结构体数组
结构体数组可以在定义时初始化
struct Person team[2] = {
{"TENET", 20, 2},
{"VENOM", 21, 2.1}
};
动态初始化
结构体的初始化也可以在程序执行中进行,尤其是当结构体的值需要在运行时确定时.
struct Person person3;
strcpy(person3.name, "TENET"); // 使用 strcpy 来复制字符串
person3.age = 20;
person3.height = 2.1;
完整代码示例:
#include <stdio.h>
// 定义一个名为 Point 的结构体,包含两个整数成员:x 和 y
struct Point
{
int x; // x坐标
int y; // y坐标
} p1; // 在定义类型的同时,声明了一个 Point 类型的变量 p1
// 单独声明一个 Point 类型的变量 p2
struct Point p2;
// 初始化:定义变量的同时赋初值
struct Point p3 = {1, 2}; // 使用列表初始化的方式初始化 p3,将 x 设置为 1,y 设置为 2
// 定义一个名为 Stu 的结构体,包含一个字符数组和一个整数
struct Stu // 类型声明
{
char name[15]; // 存储名字的字符数组
int age; // 存储年龄的整型
} s = {"zhangsan", 20}; // 定义并初始化 Stu 类型的变量 s
// 定义一个名为 Node 的结构体,该结构体嵌套了 Point 结构体并包含一个指向自己类型的指针
struct Node
{
int data; // 存储一个整数数据
struct Point p; // 嵌套一个 Point 结构体
struct Node* next; // 指向下一个 Node 结构体的指针
} n1 = {10, {4,5}, NULL}, // 初始化 n1,设置 data 为 10, p 的 x 和 y 分别为 4 和 5,next 为 NULL
n2 = {20, {5, 6}, NULL}; // 同样初始化 n2,数据设置类似但不同于 n1
int main() {
// 打印 n1 和 n2 的内容以验证初始化是否成功
printf("n1 data: %d, point: (%d, %d)\n", n1.data, n1.p.x, n1.p.y);
printf("n2 data: %d, point: (%d, %d)\n", n2.data, n2.p.x, n2.p.y);
return 0;
}
2.4 结构体内存对齐
为什么需要内存对齐
内存对齐的主要目的是为了优化数据访问速度。大多数硬件平台对特定类型的数据访问有最佳的自然边界。例如,某些处理器访问一个整型数(通常是4字节)更有效率,如果它位于一个4字节的边界上。如果数据没有对齐,处理器可能需要多次访问内存来读取或写入单个数据项,这会降低程序的性能。
结构体对齐的规则
每个结构体成员相对于结构体起始的偏移量应该是该成员大小的整数倍。例如,如果一个成员是4字节整数,则其偏移量应该是4的倍数。
结构体的总大小也应该是其最宽基本类型成员大小的整数倍。如果不是,编译器会在结构体的末尾添加填充字节(padding),使其总大小符合这一规则。总的来说,就是让占用空间小的成员尽量集中在一起。
结构体示例:
struct Example {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
内存布局是:
- char a 直接放在起始位置。
- 为了确保 int b 对齐到4字节边界,编译器在 char a 后面添加3字节的填充。
- int b 放在第4字节位置。
- char c 放在 int b 后面,紧接着是额外的填充字节,以确保整个结构体的大小是4的倍数(如果需要的话)。
练习:
struct S1
{
char c1; // 占用 1 字节
int i; // 占用 4 字节,但因为对齐需求,c1 后面会有 3 字节的填充
char c2; // 占用 1 字节
};
printf("%d\n", sizeof(struct S1)); // 输出结构体大小
- 内存对齐:int 类型通常需要按 4 字节对齐。因此,char c1 后会有 3 字节的填充,以使 int i 在 4 字节边界上对齐。
- 总大小:1字节 (c1) + 3字节 (填充) + 4字节 (i) + 1字节 (c2) = 9字节。但整个结构体也可能要对齐到 4 字节边界,因此实际大小可能为 12 字节(取决于具体的编译器和平台设置)。
struct S2
{
char c1; // 占用 1 字节
char c2; // 紧接着 c1,占用 1 字节
int i; // 需要对齐到 4 字节边界,因此 c2 后有 2 字节填充
};
printf("%d\n", sizeof(struct S2)); // 输出结构体大小
- 内存对齐:为了让 int i 对齐到 4 字节边界,char c2 后需要填充 2 个字节。
- 总大小:1字节 (c1) + 1字节 (c2) + 2字节 (填充) + 4字节 (i) = 8字节。
struct S3
{
double d; // 占用 8 字节
char c; // 占用 1 字节,后面需要 7 字节填充以对齐 `int`
int i; // 占用 4 字节,需要对齐到 4 字节边界
};
printf("%d\n", sizeof(struct S3)); // 输出结构体大小
- 内存对齐:double 类型通常需要按 8 字节对齐。为了让 int i 对齐到 4 字节边界,char c 后需要 7 字节的填充。
- 总大小:8字节 (d) + 1字节 (c) + 7字节 (填充) + 4字节 (i) = 20字节。
struct S4
{
char c1; // 占用 1 字节
struct S3 s3; // 嵌套结构体 S3,前面有 7 字节填充
double d; // 占用 8 字节,按 8 字节对齐
};
printf("%d\n", sizeof(struct S4)); // 输出结构体大小
- 内存对齐:struct S3 已经分析为需要 20 字节,且需要按 8 字节对齐。char c1 后需要填充 7 字节,以确保 struct S3 s3 正确对齐。
- 总大小:1字节 (c1) + 7字节 (填充) + 20字节 (s3, 嵌套结构体的大小) + 8字节 (d) = 36字节。
2.5 修改默认对齐数
#pragma 这个预处理指令,可以改变默认对齐数。
#include <stdio.h>
#pragma pack(8) // 设置默认对齐数为 8
struct S1
{
char c1; // 1 字节
int i; // 4 字节,但由于对齐设置为 8,c1 后将填充 7 字节,使 i 对齐到 8 字节边界
char c2; // 1 字节,由于对齐设置,c2 后可能填充至 8 字节边界
};
#pragma pack() // 取消设置的默认对齐数,还原为默认
#pragma pack(1) // 设置默认对齐数为 1
struct S2
{
char c1; // 1 字节
int i; // 4 字节,紧接 c1,无需添加填充
char c2; // 1 字节,紧接 i
};
#pragma pack() // 取消设置的默认对齐数,还原为默认
int main()
{
// 输出结构体的大小
printf("%d\n", sizeof(struct S1)); // 根据对齐规则和成员定义,预期的大小
printf("%d\n", sizeof(struct S2)); // 根据对齐规则和成员定义,预期的大小
return 0;
}
对于 struct S1,其对齐设置为 8 字节:
- char c1 占用 1 字节,后面紧跟 7 字节填充,使得 int i 对齐到下一个 8 字节边界。
- int i 占用 4 字节。
- char c2 占用 1 字节,可能跟随 7 字节的填充,以确保整个结构体的大小为最大成员对齐(8 字节的倍数)。
- sizeof(struct S1) 通常为 16 字节(1 字节 c1 + 7 字节填充 + 4 字节 i + 1 字节 c2 + 3 字节填充以达到8的倍数)。
对于 struct S2,其对齐设置为 1 字节:
- char c1 占用 1 字节。
- int i 紧接 c1,占用 4 字节,不需要填充因为对齐要求已经是 1 字节。
- char c2 紧接 i,占用 1 字节。
- 因此,sizeof(struct S2) 为 6 字节(1 字节 c1 + 4 字节 i + 1 字节 c2)。
2.6 结构体传参
1. 通过值传递结构体
当结构体通过值传递给函数时,实际上是传递结构体的一个副本。这意味着函数内部对结构体成员的任何修改都不会影响原始结构体。
代码示例:
#include <stdio.h>
struct Point {
int x, y;
};
// 函数接收结构体副本
void movePoint(struct Point p) {
p.x += 1;
p.y += 1;
printf("Inside function: x = %d, y = %d\n", p.x, p.y);
}
int main() {
struct Point pt = {10, 20};
movePoint(pt); // 传递副本
printf("Outside function: x = %d, y = %d\n", pt.x, pt.y); // 原始数据未改变
return 0;
}
2. 通过指针传递结构体
当结构体通过指针传递给函数时,传递的是结构体的地址,允许函数直接访问并修改原始结构体的数据。
代码示例:
#include <stdio.h>
struct Point {
int x, y;
};
// 函数接收结构体指针
void movePoint(struct Point *p) {
p->x += 1;
p->y += 1;
printf("Inside function: x = %d, y = %d\n", p->x, p->y);
}
int main() {
struct Point pt = {10, 20};
movePoint(&pt); // 传递指针
printf("Outside function: x = %d, y = %d\n", pt.x, pt.y); // 数据已被修改
return 0;
}
2. 位段
位段(Bit-fields)是一种数据结构,用于访问结构体中的对象的指定数目的位。
2.1 定义位段
位段在结构体中定义,每个位段成员后面需要指定它所占用的位数。位段的声明通常涉及一个整数类型(如 int、unsigned int 或 signed int),后面紧跟冒号和位数。
struct TEST {
unsigned int a : 1; // 占用 1 位
unsigned int d : 5; // 填充 5 位
};
// TEST是一个位段类型
2.2 位段的内存分配
位段是连续存储的,并尽可能紧凑地放在一起,但它们的布局和总占用空间大小依赖于特定的编译器实现和对齐规则。
代码示例:
struct S
{
char a:3; // 定义一个位段 a,占用 3 位
char b:4; // 定义一个位段 b,占用 4 位
char c:5; // 定义一个位段 c,占用 5 位
char d:4; // 定义一个位段 d,占用 4 位
};
struct S s = {0}; // 使用列表初始化将所有位段初始化为 0
s.a = 10; // 给位段 a 赋值为 10
s.b = 12; // 给位段 b 赋值为 12
s.c = 3; // 给位段 c 赋值为 3
s.d = 4; // 给位段 d 赋值为 4
- 位段定义:a, b, c, 和 d 分别占用 3, 4, 5, 和 4 位。尽管它们被定义为 char 类型,这仅仅意味着这些位段是从一个 char 类型的存储单元中分配的,实际上它们占用的是指定的位数。
- 初始化:初始化时,所有位段被设为 0。这是通过 {0} 完成的,确保结构体开始时所有位都是清零的。
- 赋值:之后,为每个位段赋予了一个新值。注意赋值时要考虑位段的溢出。例如,a:3 表示 a 可以存储的最大值是 23−1=723−1=7(因为是无符号值)。因此,赋值 10 实际存储的是 10 % 8 = 2。
3. 枚举
3.1 枚举类型的定义
enum
定义。枚举的定义会创建一个新的数据类型名称,可以列举出所有可能的枚举值。每个枚举值都是一个整数(通常是 int
类型),默认从 0 开始,每个后续的值递增 1。你也可以显式指定枚举值的数值。
enum enum_name {
constant1,
constant2,
constant3,
...
};
示例:定义一个枚举
enum Day {
Sunday, // 默认为 0
Monday, // 默认为 1
Tuesday, // 默认为 2
Wednesday, // 默认为 3
Thursday, // 默认为 4
Friday, // 默认为 5
Saturday // 默认为 6
};
也可以指定特定的数值:
enum Day {
Sunday = 1,
Monday,
Tuesday = 5,
Wednesday,
Thursday = 10,
Friday,
Saturday
};
在这个例子中,Monday 将自动被赋值为 2(因为它紧跟在值为 1 的 Sunday 后面),Wednesday 为 6(紧跟在值为 5 的 Tuesday 后面),Friday 为 11(紧跟在值为 10 的 Thursday 后面)。
3.2 使用枚举
枚举类型可以用来声明变量。这些变量可以存储任何在枚举中定义的值:
enum Day today;
today = Monday;
if (today == Monday) {
printf("Start of work week.\n");
}
枚举与整数的关系
枚举在 C 语言中本质上是整数。你可以在不进行显式类型转换的情况下,将枚举值赋给整数变量:
int day = today;
enum Day tomorrow = 3; // 隐含星期四,如果从星期日开始 = 0
4. 联合
4.1 联合类型的定义
联合(共用体)是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。联合可以包含多个不同类型的成员,但任何时候只能存储其中一个成员的值。
联合通过关键字 union
定义,与结构体定义相似。联合体的定义创建了一个可以存储多个不同类型数据,但每次只能使用其中一个的数据类型。
union UnionName {
type1 member1;
type2 member2;
type3 member3;
...
};
代码示例:
定义一个联合,这个联合可以用来存储整数、浮点数和字符指针。不管哪种类型的数据被存储,它们都使用同一块内存区域。
union Data {
int i;
float f;
char *str;
};
4.2 使用联合
联合的使用类似于结构体,但需要记住的关键是联合存储的是同一块内存区域。因此,更改一个成员的值将影响到其他成员。
#include <stdio.h>
// 定义一个联合 Data,可以存储一个整数、一个浮点数或一个字符指针
union Data {
int i; // 可存储整数
float f; // 可存储浮点数
char *str; // 可存储字符指针
};
int main() {
union Data data; // 声明一个 Data 类型的联合变量
data.i = 10; // 将整数 10 存储在联合的内存中
printf("data.i : %d\n", data.i); // 打印整数字段,输出 "data.i : 10"
data.f = 220.5; // 将浮点数 220.5 存储在同一块内存中,这会覆盖之前存储的整数
printf("data.f : %.1f\n", data.f); // 打印浮点数字段,输出 "data.f : 220.5"
printf("data.i (corrupted) : %d\n", data.i); // 再次打印整数字段,但由于内存已被浮点数覆盖,输出的值是未定义的(corrupted)
data.str = "Hello World"; // 将字符串 "Hello World" 的指针存储在同一块内存中,这会覆盖之前存储的浮点数
printf("data.str : %s\n", data.str); // 打印字符串字段,输出 "data.str : Hello World"
printf("data.f (corrupted) : %.1f\n", data.f); // 再次打印浮点数字段,但由于内存已被字符串指针覆盖,输出的值也是未定义的(corrupted)
return 0;
}
联合 Data 允许其成员 i、f 和 str 共享同一内存区域。该内存区域的大小等于其最大成员的大小,这里是 float 或 char*(具体取决于平台,通常这两种类型在现代架构上大小相同,都是 4 或 8 字节)。
当联合的一个成员被赋值时,它会覆盖联合内存中的当前内容。如果随后访问同一联合的另一个成员,得到的将是一个不确定的值,这是因为存储的位模式对于其他类型可能没有意义。这在程序中体现为给 data.f 赋值后访问 data.i,以及给 data.str 赋值后访问 data.f。
4.3 联合大小的计算
联合体的大小取决于其最大成员的大小,因为所有成员共用同一块内存空间。
union Un1 {
char c[5]; // char 类型数组,占用 5 字节
int i; // int 类型,大小通常为 4 字节
};
union Un2 {
short c[7]; // short 类型数组,每个 short 占用 2 字节,总共占用 14 字节
int i; // int 类型,大小通常为 4 字节
};
- Un1 的大小将是其最大成员的大小,即 char 数组 c 的大小(5 字节)。不过,由于 int 类型可能需要按照特定的边界对齐(比如 4 字节对齐),联合体的实际大小可能需要向上取整到更大的对齐边界。具体的大小取决于编译器和平台,但如果 int 需要 4 字节对齐,Un1 的大小应该是 8 字节。
- Un2 的大小将是其最大成员的大小,即 short 数组 c 的大小(14 字节)。和 Un1 一样,如果 int 需要 4 字节对齐,那么 Un2 的大小可能需要增加额外的填充,以满足对齐要求。然而,因为 14 已经是 2 的倍数(short 类型的自然对齐边界),Un2 不需要额外的填充就可以保证 int 成员对齐,所以 Un2 的大小应该是 14 字节。