struct 是 C 语言中一种非常重要的复合数据类型,它允许你将不同类型的数据项组合成一个单一的类型,你可以把它想象成一个“容器”或者一个“蓝图”,用来描述一个更复杂的数据对象。

为什么需要结构体?
在 C 语言中,我们基本的数据类型(如 int, char, float)只能描述单一的信息,但现实世界中的对象通常包含多个属性。
描述一个“学生”的信息,我们需要知道他的学号、姓名、年龄和成绩,这些信息分属不同的数据类型:
- 学号:可能是整数 (
int) - 姓名:字符串 (
char数组) - 年龄:整数 (
int) - 成绩:浮点数 (
float)
如果用独立的变量来表示,会非常混乱且难以管理:
int id1, id2, id3; char name1[50], name2[50], name3[50]; int age1, age2, age3; float score1, score2, score3;
使用结构体,我们可以将所有这些与一个学生相关的信息打包在一起,形成一个清晰、有组织的单元。

如何定义和使用结构体?
1 定义结构体类型
使用 struct 关键字来定义一个结构体类型,定义只是创建了一个“蓝图”,并不会在内存中分配任何空间。
struct Student {
int id; // 学号
char name[50]; // 姓名
int age; // 年龄
float score; // 成绩
};
struct是关键字。Student是结构体的“标签”(tag),你可以把它看作是这个新类型的名字。- 花括号 内部是“成员列表”(member list),每个成员都有自己的类型和名称。
2 声明结构体变量
定义好结构体类型后,你就可以像使用 int 或 char 一样声明该类型的变量了。
直接声明
struct Student student1, student2; // 声明了两个 Student 类型的变量
在定义时同时声明(更常用)
struct Student {
int id;
char name[50];
int age;
float score;
} student1, student2; // 在定义结构体后,直接声明了两个变量
3 初始化结构体变量
你可以在声明时对结构体变量进行初始化,就像初始化数组一样。
struct Student student1 = {101, "张三", 20, 95.5};
C99 标准还支持指定成员初始化(更安全):
struct Student student2 = {
.id = 102,
.name = "李四",
.age = 21,
.score = 88.0
};
4 访问结构体成员
使用成员访问运算符(,英文句点)来访问和修改结构体内部成员的值。
#include <stdio.h>
#include <string.h>
// 定义结构体
struct Student {
int id;
char name[50];
int age;
float score;
};
int main() {
// 声明并初始化
struct Student student1 = {101, "张三", 20, 95.5};
// 访问成员并打印
printf("学号: %d\n", student1.id);
printf("姓名: %s\n", student1.name);
printf("年龄: %d\n", student1.age);
printf("成绩: %.2f\n", student1.score);
// 修改成员的值
student1.score = 98.0;
student1.age = 21;
printf("\n更新后的信息:\n");
printf("学号: %d\n", student1.id);
printf("姓名: %s\n", student1.name);
printf("年龄: %d\n", student1.age);
printf("成绩: %.2f\n", student1.score);
return 0;
}
结构体数组
你可以创建一个结构体数组,来存储多个相同类型的结构体对象。
#include <stdio.h>
struct Student {
int id;
char name[20];
};
int main() {
// 定义一个可以容纳 3 个 Student 结构体的数组
struct Student students[3] = {
{101, "张三"},
{102, "李四"},
{103, "王五"}
};
// 遍历并打印数组内容
for (int i = 0; i < 3; i++) {
printf("学生 %d: ID=%d, Name=%s\n", i + 1, students[i].id, students[i].name);
}
return 0;
}
结构体指针
指向结构体的指针非常常用,尤其是在函数传递大型结构体时,可以避免大量数据拷贝,提高效率。
1 声明和使用
- 使用
->(箭头运算符)来通过指针访问成员。 pointer->member等价于(*pointer).member。
#include <stdio.h>
struct Student {
int id;
char name[20];
};
int main() {
struct Student student1 = {101, "赵六"};
struct Student *ptr; // 声明一个结构体指针
ptr = &student1; // 让指针指向 student1 的地址
// 使用 -> 访问成员
printf("通过指针访问:\n");
printf("ID: %d\n", ptr->id);
printf("Name: %s\n", ptr->name);
// 使用 (*ptr). 访问成员 (效果相同,但更繁琐)
printf("\n通过解引用访问:\n");
printf("ID: %d\n", (*ptr).id);
printf("Name: %s\n", (*ptr).name);
return 0;
}
结构体作为函数参数
结构体可以作为函数的参数传递,主要有两种方式:
-
按值传递:将整个结构体复制一份传给函数,优点是安全,函数内部修改不会影响原结构体,缺点是如果结构体很大,复制会消耗较多时间和内存。
void printStudent(struct Student s) { printf("ID: %d, Name: %s\n", s.id, s.name); } -
按指针传递:传递结构体的地址,优点是高效,不会复制数据,缺点是函数内部可以通过指针修改原结构体的内容,需要注意副作用。
void updateStudentScore(struct Student *s, float new_score) { s->score = new_score; // 直接修改原结构体 }
示例:
#include <stdio.h>
struct Student {
int id;
char name[20];
float score;
};
// 按值传递,打印信息
void printByValue(struct Student s) {
printf("[按值传递] ID: %d, Name: %s, Score: %.1f\n", s.id, s.name, s.score);
}
// 按指针传递,修改成绩
void updateByPointer(struct Student *s, float new_score) {
s->score = new_score;
printf("[按指针传递] 已将 %s 的成绩修改为 %.1f\n", s->name, s->score);
}
int main() {
struct Student s1 = {101, "钱七", 85.5};
printByValue(s1); // 传递副本
updateByPointer(&s1, 92.0); // 传递地址
printByValue(s1); // 再次打印,可以看到成绩已被修改
return 0;
}
结构体的大小与内存对齐
结构体的大小不一定是其所有成员大小之和,这涉及到内存对齐的概念。
为什么需要内存对齐?
- 平台原因:不是所有的硬件平台都能在任何地址上访问任意类型的数据,某些硬件平台只能在特定地址(如偶数地址)访问特定类型的数据(如
int)。 - 性能原因:如果数据对齐,CPU 可以一次性读取一个完整的数据(如一个 4 字节的
int),否则可能需要两次内存访问,效率降低。
规则(简化的 GCC/Clang 规则):
- 第一个成员变量的偏移量为 0。
- 其他成员变量的偏移量必须是它自身大小的整数倍。
- 结构体的总大小必须是其中“最大成员”大小的整数倍。
示例:
#include <stdio.h>
// 结构体 A
struct A {
char c1; // 1字节
int i; // 4字节
char c2; // 1字节
};
// 结构体 B
struct B {
int i; // 4字节
char c1; // 1字节
char c2; // 1字节
};
int main() {
printf("sizeof(struct A) = %zu\n", sizeof(struct A)); // 输出通常是 12
printf("sizeof(struct B) = %zu\n", sizeof(struct B)); // 输出通常是 8
return 0;
}
分析 struct A:
c1放在偏移量 0 处,占 1 字节。i需要从 4 的倍数地址开始,在c1后面填充了 3 个字节(padding),i从偏移量 4 开始,占 4 字节。c2放在偏移量 8 处,占 1 字节。- 结构体总大小需要是最大成员(
i, 4字节)的整数倍,当前总大小为 9,所以需要在末尾填充 3 个字节。 - 最终大小为 1 + 3 + 4 + 1 + 3 = 12 字节。
优化建议: 将结构体中占用空间较大的成员放在一起,可以减少填充字节,节省内存。
// 优化后的结构体
struct Optimized {
int i; // 4字节
char c1; // 1字节
char c2; // 1字节
// char padding[2]; // 编译器自动填充
};
// sizeof(struct Optimized) = 8
typedef 与结构体
typedef 可以为一个已有的类型(包括 struct)创建一个新的别名,使代码更简洁易读。
不使用 typedef:
struct Point {
int x;
int y;
};
int main() {
struct Point p1; // 每次都要写 struct
return 0;
}
使用 typedef:
// 定义结构体并同时为其创建别名 Point
typedef struct {
int x;
int y;
} Point;
int main() {
Point p1; // 直接使用 Point,无需 struct 关键字,更简洁
Point p2;
return 0;
}
或者,在已有结构体定义的基础上使用 typedef:
struct Point {
int x;
int y;
};
typedef struct Point Point; // 为 struct Point 创建别名 Point
结构体嵌套
结构体的成员也可以是另一个结构体类型。
#include <stdio.h>
#include <string.h>
// 日期结构体
struct Date {
int year;
int month;
int day;
};
// 学生结构体,嵌套了 Date 结构体
struct Student {
int id;
char name[50];
struct Date birthday; // 嵌套结构体成员
float score;
};
int main() {
struct Student student1;
student1.id = 101;
strcpy(student1.name, "孙八");
student1.birthday.year = 2003;
student1.birthday.month = 5;
student1.birthday.day = 20;
student1.score = 91.5;
printf("姓名: %s\n", student1.name);
printf("生日: %d-%d-%d\n", student1.birthday.year, student1.birthday.month, student1.birthday.day);
return 0;
}
| 特性 | 描述 | 示例 |
|---|---|---|
| 定义 | 使用 struct 关键字创建复合数据类型的蓝图。 |
struct Student {...}; |
| 声明 | 根据蓝图创建变量。 | struct Student s1; |
| 初始化 | 在声明时赋初值。 | struct Student s = {101, "Tom"}; |
| 成员访问 | 使用 运算符访问变量成员。 | s.id = 102; |
| 指针访问 | 使用 -> 运算符通过指针访问成员。 |
ptr->id = 102; |
| 数组 | 存储多个相同结构体。 | struct Student arr[10]; |
| 函数参数 | 可按值传递(安全,低效)或按指针传递(高效,可修改原数据)。 | void func(struct Student s) / void func(struct Student *s) |
| 内存对齐 | 结构体大小受对齐规则影响,不一定是成员大小之和。 | sizeof(struct A) 可能大于 sizeof(c1) + sizeof(i) |
| typedef | 为结构体类型创建简洁别名。 | typedef struct {...} Point; |
| 嵌套 | 结构体成员可以是另一个结构体。 | struct Student { struct Date birthday; }; |
结构体是 C 语言进行数据组织和面向对象编程思想的基础,掌握它对于编写复杂、高效的 C 程序至关重要。
