一个学生通常有姓名、年龄、性别和学号。这些数据的类型并不相同:姓名是字符数组,年龄是整数。如果把它们拆成 name、age、sex、id 四个变量,数据一多,很快就会忘记哪些变量属于同一个人。
结构体把一组有关系、类型可以不同的数据放进同一个对象。本文按课件的主线讲清楚声明和初始化、成员访问、结构体指针、函数传参。每个完整示例都用 C11 编译运行,并给出输出。
1. 先把“学生”描述出来
结构体声明的基本形状是 struct 标签名 { 成员声明; };。
例如,学生可以定义为 typedef struct Stu { char name[20]; int age; char sex[5]; char id[20]; } Stu;。
最后的分号不能漏。struct Stu 是结构体类型的原名,Stu 是通过 typedef 起的别名。之后写 Stu 就可以声明变量,不必每次都写 struct Stu。

现在创建一个学生变量,并在定义时初始化:
#include <stdio.h>
typedef struct Stu {
char name[20];
int age;
char sex[5];
char id[20];
} Stu;
int main(void) {
Stu s = {"Li Hua", 20, "M", "C2024001"};
printf("name=%s\nage=%d\nsex=%s\nid=%s\n", s.name, s.age, s.sex, s.id);
return 0;
}
运行结果:
name=Li Hua
age=20
sex=M
id=C2024001
初始化列表会按成员声明顺序对应:第一个字符串给 name,第二个值给 age,依次类推。顺序写错,编译器不一定能替你发现,读代码时要对照成员声明。
2. 声明变量和嵌套初始化
有了结构体类型后,可以在声明类型时创建变量,也可以稍后单独创建。例如 struct Point { int x; int y; } p1; 会在声明类型的同时创建 p1,struct Point p2; 则单独创建 p2。结构体成员还可以是另一个结构体,甚至可以是指向自身的指针。这是链表结点的雏形:
#include <stdio.h>
typedef struct Point {
int x;
int y;
} Point;
typedef struct Node {
int data;
Point point;
struct Node *next;
} Node;
int main(void) {
Node n = {10, {4, 5}, NULL};
printf("data=%d\npoint=(%d, %d)\nnext=%s\n",
n.data, n.point.x, n.point.y,
n.next == NULL ? "NULL" : "not NULL");
return 0;
}
运行结果:
data=10
point=(4, 5)
next=NULL
{10, {4, 5}, NULL} 的第二项对应 point,里面的 {4, 5} 再对应 Point 的 x 和 y。next 暂时没有指向下一个结点,所以初始化为 NULL。
3. 用点操作符访问结构体变量的成员
当手里有的是结构体变量,使用点操作符(.)。例如在前面的完整程序中,n.data 读取整数,n.point.x 则先找到内层的 point,再取它的 x。
s.age 这样的成员表达式既可以读,也可以写。字符数组却不能在定义后用 = 整体赋值;修改字符串时可以使用 strcpy,但要确保数组空间足够。
4. 有结构体指针时,用箭头操作符
函数经常接收结构体指针,而不是整个结构体对象:
#include <stdio.h>
typedef struct Stu {
char name[20];
int age;
} Stu;
static void print_by_pointer(const Stu *ps) {
printf("dot: name=%s age=%d\n", (*ps).name, (*ps).age);
printf("arrow: name=%s age=%d\n", ps->name, ps->age);
}
int main(void) {
Stu s = {"Zhang San", 20};
print_by_pointer(&s);
return 0;
}
运行结果:
dot: name=Zhang San age=20
arrow: name=Zhang San age=20
(*ps).age 和 ps->age 等价。前者先解引用指针,再用点操作符取成员;后者是 C 语言提供的简写。括号不能省:*ps.age 会被理解成 *(ps.age),指针 ps 本身并没有 age 成员。
5. 结构体传参:复制一份,还是传地址?
C 语言的普通参数传递是值传递。把结构体直接传给函数,函数会得到一份副本:
#include <stdio.h>
typedef struct Score {
int chinese;
int math;
} Score;
static void add_bonus_copy(Score score) {
score.math += 10;
printf("inside copy: math=%d\n", score.math);
}
static void add_bonus_in_place(Score *pscore) {
pscore->math += 10;
printf("inside pointer: math=%d\n", pscore->math);
}
int main(void) {
Score s = {88, 75};
add_bonus_copy(s);
printf("after copy: math=%d\n", s.math);
add_bonus_in_place(&s);
printf("after pointer: math=%d\n", s.math);
return 0;
}
运行结果:
inside copy: math=85
after copy: math=75
inside pointer: math=85
after pointer: math=85
第一次调用改的是副本,所以 main 里的 s.math 仍然是 75。第二次传入 &s,函数通过 pscore->math 改到了原对象。
如果结构体很大,按值传参还会复制更多数据。复制可能有开销,因此大结构体只需读取或修改原对象时,通常传指针更合适;小结构体需要独立副本时,按值传递也完全合理。只读函数可以加上 const,让编译器帮我们拦住误修改。
6. 把结构体放进数组
结构体可以组成数组。下面保存三名学生,打印名单,再找出分数最高的人:
#include <stdio.h>
typedef struct Student {
char name[20];
int age;
int score;
} Student;
static void print_student(const Student *student) {
printf("%-8s age=%d score=%d\n",
student->name, student->age, student->score);
}
static const Student *best_student(const Student students[], size_t count) {
const Student *best = &students[0];
for (size_t i = 1; i < count; i++) {
if (students[i].score > best->score) {
best = &students[i];
}
}
return best;
}
int main(void) {
Student class1[] = {
{"Li Hua", 20, 86},
{"Wang Wei", 19, 92},
{"Chen Yu", 20, 88}
};
size_t count = sizeof class1 / sizeof class1[0];
for (size_t i = 0; i < count; i++) {
print_student(&class1[i]);
}
printf("best: %s\n", best_student(class1, count)->name);
return 0;
}
运行结果:
Li Hua age=20 score=86
Wang Wei age=19 score=92
Chen Yu age=20 score=88
best: Wang Wei
&class1[i] 是第 i 个结构体的地址,所以 print_student 用箭头操作符访问成员。sizeof class1 / sizeof class1[0] 只在 class1 仍是数组的地方计算长度;数组传给函数后会转换成指针,函数本身不知道元素个数,所以要把 count 一起传进去。best_student 假设 count > 0,调用时需满足这个条件。
7. 容易踩的几个坑
结构体声明末尾要有分号。手里是结构体变量时用点操作符,手里是结构体指针时用箭头操作符。要让函数修改调用者的对象,就传地址;按值传参改的是副本。
还有一个常见错误是给字符数组直接赋字符串:s.name = "Li Hua" 会在编译时失败,因为数组不能整体赋值。可以在定义变量时初始化,或在容量足够时使用 string.h 提供的 strcpy(s.name, "Li Hua")。目标数组必须放得下字符串以及末尾的 \0。
结构体的语法不算多。先判断手里是对象还是指针,再选点操作符或箭头操作符;传参时先想清楚函数要不要修改原对象,以及复制整个对象值不值得。
练习
给上面的 Student 增加 char id[20] 成员。写一个按学号查找学生的函数,找不到时返回 NULL;再写一个加分函数,让所有学生的分数提高 5 分,但最高不超过 100。测试时别忘了“学号不存在”和“原本是 98 分”这两种情况。

1332

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



