前面二十篇文章,我们和数据类型打了很多交道——int、double、char、数组、指针……但它们都有一个共同点:一个变量只能存一种类型的数据。
可真实世界里,一个“东西”往往由很多属性拼成。比如一个学生,有姓名(字符串)、学号(整数)、成绩(浮点数);一个坐标点,有 x 和 y 两个坐标值。如果各存各的,数据散落一地,很难管理。
C 语言给了我们一个“打包”工具:结构体(struct)。它能把多个不同类型的数据捆在一起,变成一个自定义的复合类型。今天我们就来学会怎么定义它、初始化它、访问它的成员,以及如何用指针优雅地操作它。
一、结构体的定义
定义结构体的基本语法:
struct 结构体名 {
类型 成员1;
类型 成员2;
// ... 更多成员
}; // 分号!分号!分号!
例如,定义一个“学生”结构体:
struct Student {
char name[50];
int id;
float score;
};
这就创造了一个新的类型叫 struct Student。注意:struct 关键字和 Student 是一体的,不能只用 Student(除非配合 typedef,后面会讲)。
二、声明结构体变量
有了 struct Student 这个类型,就可以像用 int、double 一样声明变量:
struct Student s1; // 未初始化
struct Student s2 = {"Alice", 1001, 92.5}; // 声明并初始化
也可以在定义结构体时顺便声明变量:
struct Point {
int x;
int y;
} p1, p2; // 同时声明两个全局变量
甚至可以不写结构体名(匿名结构体),但这种只能在那一次声明变量,后续无法再使用该类型,不常用:
struct {
int day;
int month;
int year;
} birthday;
推荐做法:老老实实写好结构体定义,然后在需要的地方声明变量。
三、初始化结构体
1. 完全初始化
按成员顺序列出所有初始值:
struct Student s = {"Bob", 1002, 88.0};
2. 部分初始化
只写前几个值,剩余的成员会被自动初始化为 0(或空字符):
struct Student s = {"Charlie"}; // id=0, score=0.0
这和数组的部分初始化规则一致。
3. C99 指定初始化器(Designated Initializer)
可以点名初始化某些成员,不按顺序也可以:
struct Student s = {.score = 95.5, .name = "Diana", .id = 1003};
这种写法清晰、不怕记错顺序,推荐使用。
四、访问结构体成员:点运算符 .
用 变量名.成员名 读写:
#include <stdio.h>
struct Student {
char name[50];
int id;
float score;
};
int main(void) {
struct Student s = {"Eve", 1004, 89.5};
printf("姓名: %s\n", s.name);
printf("学号: %d\n", s.id);
printf("成绩: %.1f\n", s.score);
s.score = 92.0; // 修改成员
printf("新成绩: %.1f\n", s.score);
return 0;
}
输出:
姓名: Eve
学号: 1004
成绩: 89.5
新成绩: 92.0
五、结构体数组
结构体本身是一种类型,自然可以拿来组成数组。比如一个班的学生:
#include <stdio.h>
struct Student {
char name[50];
int id;
float score;
};
int main(void) {
struct Student class[3] = {
{"Alice", 1001, 92.5},
{"Bob", 1002, 85.0},
{"Carol", 1003, 78.5}
};
for (int i = 0; i < 3; i++) {
printf("%s (%d): %.1f\n", class[i].name, class[i].id, class[i].score);
}
return 0;
}
class[i] 是数组的第 i 个元素,它是一个 struct Student,再用 . 访问其成员。
结构体数组结合循环和排序算法,就能做出学生成绩管理系统的基础功能。后面的实战篇会不断用到它。
六、结构体指针与箭头运算符 ->
每个结构体变量在内存中都有地址,可以声明指向它的指针:
struct Student s = {"Frank", 1005, 76.0};
struct Student *p = &s; // p 指向 s
要通过指针访问成员,有两种等价写法:
方式一:(*p).成员 —— 先解引用,再用点
printf("%s\n", (*p).name);
括号必须加,因为 . 优先级高于 *。*p.name 会被解释成 *(p.name),那是错的。
方式二:p->成员 —— 箭头运算符(推荐)
printf("%s\n", p->name); // 简洁明了
p->name 就是 (*p).name 的语法糖。几乎所有人都用箭头,所以请记住它。
示例:
#include <stdio.h>
struct Student {
char name[50];
int id;
float score;
};
int main(void) {
struct Student s = {"Grace", 1006, 88.0};
struct Student *p = &s;
printf("姓名: %s\n", p->name);
printf("学号: %d\n", p->id);
printf("成绩: %.1f\n", p->score);
p->score = 91.5; // 用指针修改成员
printf("新成绩: %.1f\n", s.score); // s.score 也变了
return 0;
}
七、结构体作为函数参数
按值传递:整个结构体被复制
void print_student(struct Student s) {
printf("%s %d %.1f\n", s.name, s.id, s.score);
}
调用时,print_student(s1) 会把整个 s1 复制一份给形参 s。如果结构体很小(比如几个 int),这没什么;但若结构体很大(比如包含一个大数组),复制开销就大了。而且,函数内修改 s 不会影响原始结构体。
按指针传递:只复制一个地址(推荐)
void print_student(const struct Student *s) {
printf("%s %d %.1f\n", s->name, s->id, s->score);
}
调用时传 &s1,只复制 4 或 8 字节的指针。const 修饰符表示函数不会修改结构体内容,让调用者放心。
同理,如果要让函数修改结构体的成员,传指针也很自然:
void raise_score(struct Student *s, float delta) {
s->score += delta;
}
结论:传递结构体时,优先传指针,并用 const 标明只读意图。
八、常见错误与陷阱
1. 结构体定义忘记末尾分号
struct Point {
int x;
int y;
} // 错误!缺少分号
这个错误会导致后面连续的代码报一堆莫名其妙的错。看到奇怪错误时,先检查前一个结构体定义是否少了 ;。
2. 用 == 比较两个结构体
struct Student a = {"Tom", 1, 80};
struct Student b = {"Tom", 1, 80};
if (a == b) { ... } // 错误!不能直接用 == 比较结构体
C 语言不允许对结构体直接使用 ==。需要自己写函数逐个成员比较,或使用 memcmp(但要小心内存对齐产生的填充字节,后面会讲)。
3. 结构体指针未初始化就使用
struct Student *p;
p->id = 100; // 危险!p 没有指向有效内存
指针必须指向已存在的结构体变量,或通过 malloc 分配空间。
4. 返回局部结构体的指针
struct Point *get_point(void) {
struct Point p = {0, 0};
return &p; // 危险!p 在函数返回后消失
}
和普通变量一样,局部结构体在栈上,函数返回后失效。如果要返回结构体,可以直接返回值(结构体类型,会复制),或返回动态分配的结构体指针。
5. 混淆 . 和 ->
- 用变量本身访问成员:
.(s.name) - 用指针访问成员:
->(p->name)
初学时常会写出 p.name(p 是指针,应该用 ->)或 s->name(s 不是指针,应该用 .)。编译器通常会给出清晰的错误提示,仔细读。
九、小结
今天你学会了把各种数据打包成一个“复合类型”——结构体:
- 用
struct关键字定义,最后别忘了分号。 - 声明变量、初始化(包括指定初始化器)。
- 用
.访问成员,用->通过指针访问成员。 - 结构体可以组成数组,解决了批量管理复杂数据的问题。
- 传参时优先传指针,既高效又安全。
结构体是 C 语言面向对象思想的雏形。后面你写的链表节点、树节点、哈希表条目,全都靠它来定义。下一篇,我们会深入结构体在内存中的真实布局——内存对齐到底是什么?为什么结构体的大小往往比成员加起来更大?以及那个诡异又实用的 柔性数组 是怎么工作的。这些底层细节,会让你对内存的理解再上一个台阶。
课后小练习
- 定义一个
struct Point(包含int x和int y),写一个函数double distance(const struct Point *a, const struct Point *b),计算两点之间的欧几里得距离并返回。在main中测试。 - 用前面定义的
struct Student创建一个包含 5 个学生的数组,从键盘输入他们的信息,然后找出成绩最高和最低的学生并打印其信息。 - 用结构体指针和动态内存分配,写一个程序:先让用户输入学生数量,然后用
malloc分配一个struct Student的动态数组,输入数据、打印、最后free。 - (陷阱修复)下面的代码有错误,请找出并修正:
struct Book { char title[100]; int pages; } int main(void) { struct Book b; b.title = "C Programming"; b.pages = 500; printf("%s\n", b.title); return 0; }
我们下期见!
1568

被折叠的 条评论
为什么被折叠?



