完整且详细的C语言教程,从入门到精通(8.1)--C语言结构体
·
你们好,我是一名嵌入式工程师。我正在持续更新《嵌入式工程师的C语言实战》系列教程,目前已更新10篇。如果你也在学习嵌入式,欢迎点个关注,我们一起从零到一。
文章目录
前言
结构体(struct)是C语言中最核心的自定义数据类型,它允许你把不同类型的数据打包成一个整体。无论是操作系统内核、网络协议还是数据库,底层都离不开结构体。
一、什么是结构体?
1. 核心概念
结构体把多个不同类型的变量组合成一个整体,每个成员独立占用内存。
类比:像一张学生信息表,包含姓名(字符串)、年龄(整数)、成绩(浮点数)。
2. 为什么需要结构体?
没有结构体时,管理多个相关数据很麻烦:
// ❌ 没有结构体:数据分散,难以管理
char name1[50] = "张三";
int age1 = 20;
float score1 = 85.5;
char name2[50] = "李四";
int age2 = 21;
float score2 = 92.0;
// 如果学生有100个,需要维护300个变量!
有了结构体后:
// ✅ 使用结构体:数据打包,清晰易用
struct Student {
char name[50];
int age;
float score;
};
struct Student stu1 = {"张三", 20, 85.5};
struct Student stu2 = {"李四", 21, 92.0};
二、结构体的声明与定义
1. 基本语法
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
2. 三种定义方式
方式1:先声明类型,后定义变量(最常用)
struct Student {
char name[50];
int age;
float score;
};
struct Student stu1; // 未初始化
struct Student stu2 = {"张三", 20, 85.5}; // 初始化
方式2:声明类型的同时定义变量
struct Student {
char name[50];
int age;
float score;
} stu1, stu2; // 直接定义变量
// 后续可以继续使用该类型
struct Student stu3;
方式3:匿名结构体(一次性使用)
struct {
int x;
int y;
} point = {10, 20}; // 直接定义变量,类型无法再次使用
// 错误:后续不能再定义 struct point p2;
3. 使用 typedef 简化(工业界最常用)
typedef struct {
char name[50];
int age;
float score;
} Student; // 现在 Student 是一个类型别名
Student stu1 = {"张三", 20, 85.5}; // 不需要写 struct 关键字
Student *ptr = &stu1;
为什么推荐 typedef?
- 少写
struct关键字,代码更简洁。 - 与C++兼容(C++中
struct Student可以直接写Student)。
三、结构体的初始化
1. 顺序初始化
struct Student stu = {"张三", 20, 85.5};
// 按成员声明顺序依次赋值
2. 指定初始化(C99)
struct Student stu = {
.score = 85.5,
.name = "张三",
.age = 20
};
// 顺序可以打乱,更清晰
3. 部分初始化
struct Student stu = {"张三"}; // 只初始化 name,其他成员为 0
printf("%d\n", stu.age); // 0
printf("%.1f\n", stu.score); // 0.0
4. 嵌套结构体初始化
struct Date {
int year, month, day;
};
struct Employee {
char name[50];
struct Date birthday;
float salary;
};
struct Employee emp = {
"张三",
{1990, 5, 15}, // 嵌套初始化
8000.0
};
四、访问结构体成员
1. 普通变量用 .
struct Student stu = {"张三", 20, 85.5};
printf("%s\n", stu.name); // 张三
printf("%d\n", stu.age); // 20
printf("%.1f\n", stu.score); // 85.5
stu.age = 21; // 修改成员
2. 指针用 ->
struct Student stu = {"张三", 20, 85.5};
struct Student *ptr = &stu;
printf("%s\n", ptr->name); // 张三
printf("%d\n", ptr->age); // 20
ptr->age = 22; // 通过指针修改
printf("%d\n", stu.age); // 22
等价关系:
ptr->age 等价于 (*ptr).age
3. 嵌套结构体访问
struct Employee emp = {"张三", {1990, 5, 15}, 8000.0};
printf("%d\n", emp.birthday.year); // 1990
printf("%d\n", emp.birthday.month); // 5
// 指针访问
struct Employee *p = &emp;
printf("%d\n", p->birthday.year); // 1990
五、内存布局(重点难点)
1. 内存对齐规则
结构体的大小不等于成员大小之和,因为存在内存对齐。
规则:
- 每个成员相对于结构体起始地址的偏移量,必须是该成员大小的整数倍。
- 结构体总大小必须是最大成员大小的整数倍。
2. 示例分析
struct A {
char c; // 1 字节
int i; // 4 字节
char d; // 1 字节
};
printf("%zu\n", sizeof(struct A)); // 通常是 12,不是 6
内存图解:
偏移: 0 1 2 3 4 5 6 7 8 9 10 11
[c] [填充][填充][填充][i i i i][d] [填充][填充][填充]
c在偏移 0,占 1 字节。i是int,需要 4 字节对齐,所以偏移到 4。d在偏移 8,占 1 字节。- 总大小需要是 4 的倍数,所以补齐到 12。
3. 调整成员顺序可以节省内存
// 优化前:12 字节
struct A {
char c; // 1
int i; // 4
char d; // 1
};
// 优化后:8 字节
struct B {
int i; // 4
char c; // 1
char d; // 1
// 补齐 2 字节
};
printf("%zu\n", sizeof(struct A)); // 12
printf("%zu\n", sizeof(struct B)); // 8
4. 为什么要内存对齐?
- 硬件要求:某些CPU只能从对齐的地址读取数据,否则会崩溃或效率降低。
- 性能优化:对齐的数据访问速度更快。
5. 如何控制对齐?
// 方式1:使用 #pragma pack(编译器相关)
#pragma pack(push, 1) // 设置为 1 字节对齐
struct Packed {
char c;
int i;
};
#pragma pack(pop) // 恢复默认对齐
printf("%zu\n", sizeof(struct Packed)); // 5
// 方式2:使用 _Alignas(C11)
struct Aligned {
_Alignas(16) int x; // 16 字节对齐
};
六、结构体作为函数参数
1. 值传递(不推荐,会拷贝整个结构体)
void print_student(struct Student s) { // 拷贝整个结构体
printf("%s %d %.1f\n", s.name, s.age, s.score);
}
struct Student stu = {"张三", 20, 85.5};
print_student(stu); // 传值
缺点:如果结构体很大,拷贝开销大。
2. 指针传递(推荐)
void print_student(const struct Student *s) { // 只传指针
printf("%s %d %.1f\n", s->name, s->age, s->score);
}
struct Student stu = {"张三", 20, 85.5};
print_student(&stu); // 传地址
优点:
- 只拷贝指针(8 字节),效率高。
- 加
const防止函数内部修改。
3. 返回结构体
struct Point {
int x, y;
};
struct Point create_point(int x, int y) {
struct Point p = {x, y};
return p; // 返回结构体(编译器会优化,通常不产生额外拷贝)
}
int main() {
struct Point p = create_point(10, 20);
printf("(%d, %d)\n", p.x, p.y); // (10, 20)
return 0;
}
七、结构体的高级用法
1. 自引用结构体(链表节点)
struct Node {
int data;
struct Node *next; // 指向同类型的指针
};
// 创建链表
struct Node *head = NULL;
struct Node *n1 = malloc(sizeof(struct Node));
n1->data = 10;
n1->next = NULL;
head = n1;
2. 结构体数组
struct Student class[3] = {
{"张三", 20, 85.5},
{"李四", 21, 92.0},
{"王五", 19, 78.5}
};
for (int i = 0; i < 3; i++) {
printf("%s: %.1f\n", class[i].name, class[i].score);
}
3. 结构体指针数组
struct Student stu1 = {"张三", 20, 85.5};
struct Student stu2 = {"李四", 21, 92.0};
struct Student *arr[] = {&stu1, &stu2};
for (int i = 0; i < 2; i++) {
printf("%s\n", arr[i]->name);
}
4. 位域(Bit-field)
struct Flags {
unsigned int is_active : 1; // 1 位
unsigned int is_admin : 1; // 1 位
unsigned int level : 4; // 4 位
};
printf("%zu\n", sizeof(struct Flags)); // 4 字节
struct Flags f = {1, 0, 5};
printf("%u\n", f.is_active); // 1
printf("%u\n", f.level); // 5
用途:节省内存,常用于硬件寄存器、协议头。
八、完整实战示例:学生管理系统
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_STUDENTS 100
// 定义结构体
typedef struct {
char name[50];
int age;
float score;
} Student;
// 打印学生信息
void print_student(const Student *s) {
printf("姓名: %-10s 年龄: %2d 成绩: %.1f\n",
s->name, s->age, s->score);
}
// 查找最高分学生
Student* find_top(Student arr[], int n) {
if (n <= 0) return NULL;
Student *top = &arr[0];
for (int i = 1; i < n; i++) {
if (arr[i].score > top->score) {
top = &arr[i];
}
}
return top;
}
// 按成绩排序(冒泡)
void sort_by_score(Student arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j].score < arr[j + 1].score) {
Student temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
Student class[MAX_STUDENTS] = {
{"张三", 20, 85.5},
{"李四", 21, 92.0},
{"王五", 19, 78.5},
{"赵六", 22, 88.0}
};
int n = 4;
printf("=== 所有学生 ===\n");
for (int i = 0; i < n; i++) {
print_student(&class[i]);
}
printf("\n=== 最高分学生 ===\n");
Student *top = find_top(class, n);
if (top != NULL) {
print_student(top);
}
printf("\n=== 按成绩排序后 ===\n");
sort_by_score(class, n);
for (int i = 0; i < n; i++) {
print_student(&class[i]);
}
return 0;
}
输出:
=== 所有学生 ===
姓名: 张三 年龄: 20 成绩: 85.5
姓名: 李四 年龄: 21 成绩: 92.0
姓名: 王五 年龄: 19 成绩: 78.5
姓名: 赵六 年龄: 22 成绩: 88.0
=== 最高分学生 ===
姓名: 李四 年龄: 21 成绩: 92.0
=== 按成绩排序后 ===
姓名: 李四 年龄: 21 成绩: 92.0
姓名: 赵六 年龄: 22 成绩: 88.0
姓名: 张三 年龄: 20 成绩: 85.5
姓名: 王五 年龄: 19 成绩: 78.5
九、常见陷阱与最佳实践
1. 结构体不能直接比较
struct Point p1 = {1, 2}, p2 = {1, 2};
// if (p1 == p2) { } // ❌ 编译错误
// ✅ 逐个成员比较
if (p1.x == p2.x && p1.y == p2.y) { }
2. 结构体不能直接赋值字符串
struct Student s;
// s.name = "张三"; // ❌ 编译错误
// ✅ 使用 strcpy
strcpy(s.name, "张三");
3. 结构体赋值是浅拷贝
struct Student s1 = {"张三", 20, 85.5};
struct Student s2 = s1; // 逐字节拷贝,s2 是独立副本
s2.age = 30;
printf("%d\n", s1.age); // 20(不受影响)
但如果成员是指针,浅拷贝会共享内存:
struct Data {
char *name;
};
struct Data d1;
d1.name = malloc(20);
strcpy(d1.name, "Hello");
struct Data d2 = d1; // 浅拷贝:d2.name 和 d1.name 指向同一块内存
free(d1.name);
// d2.name 现在是悬空指针!
4. 最佳实践
- 用
typedef简化类型名。 - 用指针传递大结构体,加
const防止修改。 - 注意内存对齐,合理安排成员顺序。
- 深拷贝时手动复制指针成员。
📝 总结
| 特性 | 说明 |
|---|---|
| 本质 | 把不同类型的数据打包成一个整体 |
| 内存 | 各成员独立占用,存在对齐填充 |
| 访问 | 普通变量用 .,指针用 -> |
| 传参 | 推荐指针传递,避免拷贝 |
| 典型用途 | 学生信息、链表节点、网络协议包 |
记忆口诀:
结构体,打包器,不同类型放一起。
点号访问普通量,箭头专指指针体。
内存对齐要注意,顺序调整省空间。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,主播是一个马上粉丝量超10个的嵌入式内容主播。谢谢!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)