结构体与自定义数据类型
学习目标
学完本篇,你会理解:
- 为什么需要结构体这种复合类型。
- 如何定义和使用
struct。 typedef如何简化类型名。- 如何访问结构体成员。
- 嵌套结构体的基本用法。
- 结构体数组的初步概念。
为什么需要结构体
之前我们学的变量都是单一类型:int、char、double。但现实中很多事物需要多个属性一起描述。比如一个学生,有学号、姓名、成绩,这些属性类型不同,却属于同一个人。
如果每个属性都用单独的变量,数据零散、容易混乱。结构体可以把这些属性打包成一个整体。
struct Student {
int id;
char name[20];
float score;
};
这一整包就是一个“学生”类型。
结构体的定义与使用
定义结构体类型的同时可以创建变量:
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[20];
float score;
};
int main(void) {
struct Student s1;
s1.id = 1;
strcpy(s1.name, "Alice");
s1.score = 89.5;
printf("学号:%d\n", s1.id);
printf("姓名:%s\n", s1.name);
printf("成绩:%.1f\n", s1.score);
return 0;
}
注意,字符串不能直接用 s1.name = "Alice"; 赋值,因为 name 是数组名,不能整体赋值。要用 strcpy。
typedef 简化类型名
每次写 struct Student 有点啰嗦。用 typedef 可以给它起个短名字:
#include <stdio.h>
typedef struct Student {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student s1; // 不用再加 struct
s1.id = 2;
printf("学号:%d\n", s1.id);
return 0;
}
也可以定义时直接省略标签名:
typedef struct {
int id;
char name[20];
float score;
} Student;
两种写法都可以,第一种写法保留了 struct Student 这个完整名字,某些场景更灵活。
访问结构体成员
用 . 运算符访问成员:
Student s;
s.id = 100;
strcpy(s.name, "Bob");
s.score = 92.0;
也可以在定义时直接初始化:
Student s = {100, "Bob", 92.0};
按成员顺序写,清晰直观。
嵌套结构体
结构体里还可以放另一个结构体。比如学生有出生日期:
#include <stdio.h>
typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
int id;
char name[20];
Date birthday; // 嵌套结构体
float score;
} Student;
int main(void) {
Student s = {1, "Carol", {2005, 8, 15}, 88.0};
printf("%s 的生日是 %d-%d-%d\n",
s.name, s.birthday.year, s.birthday.month, s.birthday.day);
return 0;
}
访问嵌套成员时,用多个 . 逐层深入:s.birthday.year。
结构体的内存对齐
你可能注意到一个现象:一个包含 char 和 int 的结构体,其 sizeof 可能不等于各成员大小之和。这是因为编译器会在成员之间插入"填充字节"来对齐内存,以提高 CPU 的访问效率。
struct Example {
char c; // 1 字节
// 这里会填充 3 个字节
int i; // 4 字节
};
printf("%zu\n", sizeof(struct Example)); // 通常输出 8,而不是 5
初学阶段无需深究,只需要知道 sizeof 结构体 >= 各成员大小之和,不要凭"感觉"假设结构体的大小即可。
结构体数组初步
既然 int 可以组成数组,结构体当然也可以:
Student class[3] = {
{1, "Alice", {2004, 1, 10}, 90.0},
{2, "Bob", {2005, 3, 22}, 85.5},
{3, "Carol", {2005, 8, 15}, 88.0}
};
常见错误
- 忘记
typedef时写Student s;。如果你写的是struct Student { ... };,后面就必须用struct Student s;。 - 字符串直接赋值给字符数组。
s.name = "Alice";是错的,用strcpy。 - 结构体之间比较。不能直接用
==比较两个结构体,要逐个成员比较。
结构体可能包含填充字节
struct Example {
char flag;
int value;
};
sizeof(struct Example) 可能大于两个成员大小之和,因为编译器会插入填充以满足对齐。不要把结构体内存直接当成稳定的文件或网络格式;不同 ABI、编译选项和平台的布局可能不同。
协议序列化应逐字段编码,并明确字节序和宽度。
结构体赋值会逐成员复制值
Student b = a;
若成员都是整数和内嵌数组,结构体赋值会复制这些成员。若成员含指针,只会复制指针值,不会自动复制其指向的数据。这与后面 C++ 浅拷贝问题是同一根源。
大结构体可用只读指针传参
void print_student(const Student *student) {
if (student == NULL) return;
printf("%s\n", student->name);
}
按值传递会复制整个结构体;指针传递避免复制,并通过 const 表明不修改。小型结构体按值传递也完全合理,选择取决于语义和成本,而不是死记“结构体必须传指针”。
指定初始化让字段更清楚
Student student = {
.id = 1,
.name = "Alice",
.score = 95.0f
};
C99 指定初始化不依赖成员书写顺序,新增字段时也更不容易把值放错位置。
小结
- 结构体把不同类型的数据打包成一个整体,便于描述复杂对象。
- 用
.访问成员,定义时可以用typedef简化类型名。 - 结构体可以嵌套,也可以组成数组。
下一篇预告
下一篇《结构体指针与结构体数组》会讲指向结构体的指针、-> 运算符,以及结构体数组的遍历和传参。