struct 是 C 语言中一种非常重要的用户自定义数据类型(User-Defined Data Type, UDT),它允许你将不同类型的数据项组合成一个单一的类型,这就像创建一个新模板,然后用这个模板来声明变量,这些变量可以包含多个不同类型的成员。

为什么需要结构体?
假设你要描述一个学生,学生有姓名、年龄、学号和成绩等信息。
name是一个字符串(字符数组)。age是一个整数。id是一个整数。score是一个浮点数。
如果你不使用结构体,你可能需要定义多个独立的变量:
char name[50]; int age; int id; float score;
这样做的缺点很明显:
- 数据分散:所有关于一个学生的信息都散落在不同的变量中,难以管理。
- 传递不便:如果你想将一个学生的所有信息传递给一个函数,你必须传递所有这些变量,非常麻烦。
- 逻辑不清晰:代码无法直观地表现出这些变量是属于同一个“学生”实体的。
结构体就是为了解决这些问题而生的,它可以将这些相关的数据项打包成一个逻辑单元。

如何定义和声明结构体?
1 定义结构体类型
你需要定义一个结构体的“蓝图”或“模板”,这通常使用 struct 关键字完成。
语法:
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
...
};
示例:定义一个 Student 结构体
struct Student {
char name[50];
int age;
int id;
float score;
};
这里的 struct Student 就是一个新的数据类型,就像 int 或 float 一样。
2 声明结构体变量
定义好结构体类型后,你就可以用它来声明变量了。
语法:
struct 结构体名 变量名;
示例:
struct Student student1; // 声明一个名为 student1 的 Student 结构体变量 struct Student student2, student3; // 声明两个变量
你也可以在定义结构体类型的同时声明变量:
struct Student {
char name[50];
int age;
int id;
float score;
} student1, student2; // 在这里直接声明了两个变量
如何访问和修改结构体成员?
要访问结构体变量中的成员,我们使用成员访问运算符,也称为点运算符 ()。
语法:
结构体变量名.成员名
示例:
// 给 student1 的成员赋值
strcpy(student1.name, "张三"); // 字符串不能直接用 = 赋值,需要 strcpy
student1.age = 20;
student1.id = 1001;
student1.score = 95.5;
// 访问并打印 student1 的成员
printf("姓名: %s\n", student1.name);
printf("年龄: %d\n", student1.age);
printf("学号: %d\n", student1.id);
printf("成绩: %.2f\n", student1.score);
// 修改成员的值
student1.score = 98.0;
printf("修改后的成绩: %.2f\n", student1.score);
结构体的初始化
你可以在声明结构体变量的同时对其进行初始化,就像初始化数组一样。
示例:
struct Student student4 = {"李四", 21, 1002, 88.5};
// 如果只初始化部分成员,其余成员会被自动初始化为 0 (对于数值类型) 或 '\0' (对于字符)
struct Student student5 = {"王五", 22}; // age=22, id=0, score=0.0
结构体数组
你可以创建一个结构体数组,其中每个元素都是一个结构体变量。
示例:
struct Student class[3]; // 定义一个包含3个学生的数组
// 初始化数组
struct Student class[3] = {
{"赵一", 19, 1011, 92.0},
{"钱二", 20, 1012, 85.5},
{"孙三", 21, 1013, 91.0}
};
// 访问数组中的结构体成员
printf("第一个学生的姓名: %s\n", class[0].name);
printf("第二个学生的成绩: %.2f\n", class[1].score);
// 遍历结构体数组
for (int i = 0; i < 3; i++) {
printf("学生 %d: %s, 成绩: %.2f\n", i+1, class[i].name, class[i].score);
}
结构体指针
你可以声明一个指向结构体的指针,这在进行函数传参和动态内存分配时非常有用。
语法:
struct Student *ptr;
通过结构体指针访问成员,有两种方式:
-
使用
->运算符(推荐)->是(*ptr).的缩写,专门用于通过指针访问结构体成员,更简洁。 -
使用 运算符 需要先对指针进行解引用
(*ptr)。
示例:
struct Student student6 = {"周六", 23, 1021, 76.5};
struct Student *ptr_student = &student6; // 指针指向 student6 的地址
// 使用 -> 访问成员
printf("通过指针访问姓名: %s\n", ptr_student->name);
printf("通过指针访问年龄: %d\n", ptr_student->age);
// 使用 . 访问成员 (需要解引用)
printf("通过解引用指针访问成绩: %.2f\n", (*ptr_student).score);
// 通过指针修改成员
ptr_student->score = 80.0;
printf("修改后的成绩: %.2f\n", student6.score); // 原变量也被修改了
结构体作为函数参数
结构体可以作为函数的参数传递,主要有两种方式:
a) 传值调用
将整个结构体变量的副本传递给函数,函数内部对成员的修改不会影响原始变量,这种方式对于大型结构体来说效率较低,因为需要复制大量数据。
示例:
// 函数:打印学生信息
void printStudent(struct Student s) {
printf("--- 学生信息 ---\n");
printf("姓名: %s\n", s.name);
printf("年龄: %d\n", s.age);
printf("成绩: %.2f\n", s.score);
printf("-----------------\n");
}
int main() {
struct Student student7 = {"吴七", 24, 1022, 99.9};
printStudent(student7); // 传递的是 student7 的副本
return 0;
}
b) 传址调用(指针传递)
将指向结构体的指针传递给函数,函数通过指针可以直接访问和修改原始结构体,这种方式更高效,特别是对于大型结构体。
示例:
// 函数:修改学生成绩
void updateScore(struct Student *s, float new_score) {
s->score = new_score; // 通过指针修改原始数据
}
int main() {
struct Student student8 = {"郑八", 25, 1023, 65.0};
printf("修改前的成绩: %.2f\n", student8.score);
updateScore(&student8, 75.5); // 传递地址
printf("修改后的成绩: %.2f\n", student8.score);
return 0;
}
结构体的大小与内存对齐
sizeof 运算符可以返回一个结构体变量所占用的字节数。
printf("Student 结构体的大小: %zu 字节\n", sizeof(struct Student));
你可能发现,sizeof(struct Student) 的结果不一定等于 sizeof(name) + sizeof(age) + sizeof(id) + sizeof(score),这是因为编译器会进行内存对齐。
内存对齐的目的:
- 提高性能:CPU 读取内存时,通常是按字或双字等特定大小的块进行的,对齐后的数据可以被一次性读取,避免了多次访问,从而提高效率。
- 硬件要求:某些体系结构的 CPU 要求特定类型的数据必须对齐到特定的地址上,否则会引发硬件异常。
对齐规则(简化的通用规则):
- 结构体的第一个成员从偏移量 0 的地址开始存放。
- 其他成员变量存放的地址,必须是该成员大小的整数倍(对齐数)。
- 结构体的总大小,必须是其中最大成员大小的整数倍。
- 如果嵌套了结构体,嵌套的结构体对齐到自己的成员大小的整数倍处,结构体的整体大小是所有成员(包括嵌套结构体的成员)中最大大小的整数倍。
示例:
struct MyStruct {
char c; // 1 字节
int i; // 4 字节
short s; // 2 字节
};
// 内存布局 (假设 int 对齐数为 4):
// [c] (偏移 0)
// [ 填充3字节 ] (偏移 1-3)
// [i] (偏移 4)
// [s] (偏移 8)
// [ 填充2字节 ] (为了让总大小是4的倍数)
// 总大小 = 12 字节
// sizeof(c) + sizeof(i) + sizeof(s) = 1 + 4 + 2 = 7,但实际是12。
// 为了节省内存,可以调整成员顺序:
struct OptimizedStruct {
char c; // 1
short s; // 2
int i; // 4
};
// 内存布局:
// [c] (偏移 0)
// [s] (偏移 2)
// [i] (偏移 4)
// 总大小 = 8 字节 (4的倍数)
实践建议:将占用空间小的成员放在一起,大的成员放在一起,可以减少内存填充,节省空间。
结构体与 typedef
使用 typedef 可以为结构体类型定义一个更简短、更易读的别名,避免每次声明变量时都写 struct。
语法:
typedef struct 结构体名 {
// 成员列表
} 结构体别名;
示例:
// 定义 Student 类型,并起别名为 Stu
typedef struct Student {
char name[50];
int age;
float score;
} Stu;
// 现在可以直接使用 Stu 来声明变量,而无需 struct
Stu student9 = {"陈九", 26, 88.0};
Stu *ptr_stu = &student9;
printf("学生姓名: %s\n", student9.name);
printf("学生成绩: %.2f\n", ptr_stu->score);
| 特性 | 描述 | 示例 |
|---|---|---|
| 定义 | 创建一个包含多种数据类型的复合数据类型。 | struct Point { int x; int y; }; |
| 声明 | 根据结构体类型创建变量。 | struct Point p1; |
| 成员访问 | 使用点运算符 () 访问成员。 | p1.x = 10; |
| 指针访问 | 使用箭头运算符 (->) 通过指针访问成员。 |
struct Point *ptr = &p1; ptr->y = 20; |
| 初始化 | 在声明时为成员赋初值。 | struct Point p2 = {0, 0}; |
| 数组 | 创建结构体数组。 | struct Point points[10]; |
| 函数参数 | 可以传值(副本)或传址(指针)。 | void func(struct Point p); / void func(struct Point *p); |
typedef |
为结构体类型创建别名,简化代码。 | typedef struct Point Pt; Pt p1; |
| 内存对齐 | 编译器为了性能和硬件要求进行的内存布局优化。 | sizeof(struct Point) 可能大于成员大小之和。 |
结构体是 C 语言进行数据组织和建模的基石,掌握它对于编写复杂、结构化的程序至关重要。
