第8章 结构体数组和表

8.1 问题从哪来#

上一章用 struct Student 把一个学生的学号、姓名、分数绑在了一起。一条记录已经清楚了,但班里不会只有一个学生。

上一章的程序只做了一件事:把数据放进去,再用循环打出来。如果要做的不只是打印,而是往表里添加新学生、按学号查找某个学生、打印所有记录,就需要一套更有组织的写法。

一个班有几十个学生,程序要支持这些操作:

操作做什么
添加输入学号、姓名、分数,存进表里
查找给一个学号,找到对应的学生并打印
打印把表里所有学生逐行输出

这三个操作,用结构体数组加一个 count 变量就能实现。


8.2 先看一个例子#

假设表一开始是空的,count = 0。用户连续添加三名学生:

  • 学号 1,Alice,92 分
  • 学号 2,Bob,78 分
  • 学号 3,Carol,85 分

每添加一条,程序都把新记录写到 students[count],再把 count 加一。三次添加后,students[0]students[2] 是有效记录,count = 3,下一条会写到 students[3]

添加推进 count,查找和打印只读表

接着按学号 2 查找,程序从 students[0] 开始比对,读到 students[1] 时找到 Bob。打印全部记录时,也只打印 students[0]students[2]。这两个操作只读已有记录,不会改变表里的内容,也不会改变 count


8.3 最小实验#

这段代码带了一个简单菜单。读的时候先看三件事:students[count] 写入新记录,for 循环只扫 count 条,打印时也只打印已经使用的记录。

#include <stdio.h>
#include <string.h>

struct Student {
    int id;
    char name[32];
    int score;
};

void clear_line(void)
{
    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF) {
        // 丢掉本行剩余字符
    }
}

int main(void)
{
    struct Student students[100];   // 最多 100 条记录
    int count = 0;                  // 当前有 0 条
    int choice;

    while (1) {
        printf("\n1-Add  2-Search  3-Print  0-Exit\n");
        printf("Choice: ");
        if (scanf("%d", &choice) != 1) {
            clear_line();
            printf("Please enter a number.\n");
            continue;
        }
        clear_line();

        if (choice == 0) {
            break;
        } else if (choice == 1) {
            // 添加一条记录
            if (count >= 100) {
                printf("Table full, cannot add.\n");
                continue;
            }
            printf("ID: ");
            if (scanf("%d", &students[count].id) != 1) {
                clear_line();
                printf("ID must be an integer.\n");
                continue;
            }
            clear_line();

            printf("Name: ");
            if (scanf("%31s", students[count].name) != 1) {
                clear_line();
                printf("Failed to read name.\n");
                continue;
            }
            clear_line();

            printf("Score: ");
            if (scanf("%d", &students[count].score) != 1) {
                clear_line();
                printf("Score must be an integer.\n");
                continue;
            }
            clear_line();

            count = count + 1;          // 记录数加 1
            printf("Added successfully, total records: %d\n", count);

        } else if (choice == 2) {
            // 按学号查找
            int target;
            printf("Enter ID to search: ");
            if (scanf("%d", &target) != 1) {
                clear_line();
                printf("ID must be an integer.\n");
                continue;
            }
            clear_line();

            int found = 0;              // 标记是否找到
            for (int i = 0; i < count; i++) {
                if (students[i].id == target) {
                    printf("Found: ID %d, Name %s, Score %d\n",
                           students[i].id,
                           students[i].name,
                           students[i].score);
                    found = 1;
                    break;              // 找到就停
                }
            }
            if (!found) {
                printf("Student with ID %d not found.\n", target);
            }

        } else if (choice == 3) {
            // 打印所有记录
            if (count == 0) {
                printf("Table is empty.\n");
                continue;
            }
            printf("\nID  Name  Score\n");
            printf("----------------\n");
            for (int i = 0; i < count; i++) {
                printf("%d  %s  %d\n",
                       students[i].id,
                       students[i].name,
                       students[i].score);
            }
            printf("----------------\n");
            printf("Total records: %d\n", count);

        } else {
            printf("Invalid choice, please try again.\n");
        }
    }

    printf("Exited.\n");
    return 0;
}

8.4 编译运行#

保存成 table.c,编译:

$ gcc table.c -o table

运行,依次操作:

1-Add  2-Search  3-Print  0-Exit
Choice:
$ 1
ID:
$ 1
Name:
$ Alice
Score:
$ 92
Added successfully, total records: 1

1-Add  2-Search  3-Print  0-Exit
Choice:
$ 1
ID:
$ 2
Name:
$ Bob
Score:
$ 78
Added successfully, total records: 2

1-Add  2-Search  3-Print  0-Exit
Choice:
$ 1
ID:
$ 3
Name:
$ Carol
Score:
$ 85
Added successfully, total records: 3

1-Add  2-Search  3-Print  0-Exit
Choice:
$ 2
Enter ID to search:
$ 2
Found: ID 2, Name Bob, Score 78

1-Add  2-Search  3-Print  0-Exit
Choice:
$ 3

ID  Name  Score
----------------
1  Alice  92
2  Bob  78
3  Carol  85
----------------
Total records: 3

1-Add  2-Search  3-Print  0-Exit
Choice:
$ 0
Exited.

8.5 数据/内存/流程里发生了什么#

8.5.1 结构体数组是一排记录#

struct Student students[100];

students 是一个数组,有 100 个元素,每个元素都是一个 struct Student。每个元素内部有三个字段:idnamescore

可以把 students 想象成一排记录。第 0 个元素是一条学生记录,第 1 个元素又是一条学生记录,一直排到第 99 个元素:

结构体数组是一排记录,每行包含 id、name、score 三个字段

每个记录内部的字段在内存里是相邻的,和上一章看到的一样:

每个记录内部有多个字段,字段之间相邻

8.5.2 count 指向下一条可写位置#

count 记录表里当前有多少条有效数据。它的另一个含义是:下一条新记录应该写到哪里。

count 的值含义
0表是空的,第一条记录写到 students[0]
1表里有 1 条,下一条写到 students[1]
3表里有 3 条,下一条写到 students[3]

count 指向下一条可写位置,count 之前的数据是有效的

添加操作就是两步:

students[count].id = ...;       // 写入新记录
count = count + 1;              // count 往后移一格

students[i] 的地址 = students 的地址 + i × sizeof(struct Student)。这和第 5 章的 scores[i] 原理一样:数组名是首元素地址,下标就是偏移几个元素。只是每个元素从 int 的 4 字节变成了 struct Student 的约 40 字节,偏移的距离也跟着变了。

students[0] 的地址 = students + 0 × 40
students[1] 的地址 = students + 1 × 40
students[2] 的地址 = students + 2 × 40

这也解释了为什么 students[i].id 能找到正确的字段:程序先算出 students[i] 的起始地址,再从这个地址偏移 id 字段的距离。地址偏移是数组和结构体在内存里工作的共同基础。

8.5.3 查找:从第 0 条扫到第 count-1 条#

查找的逻辑很简单:从第 0 条开始,逐条比对学号,找到就停下来。

for (int i = 0; i < count; i++) {
    if (students[i].id == target) {
        // 找到了,打印这条记录
        break;
    }
}

假设 count = 3,表里有三条记录,查找学号 2:

轮次istudents[i].idtarget 比较结果
1011 == 2?不等,继续
2122 == 2?相等,找到了

最多需要比较 count 次。如果要找的学号不在表里,整个循环走完也找不到。

线性查找从第 0 条开始逐条比对,找到就停

这种从头到尾逐个检查的方法叫线性查找(linear search)。它的优点是简单、不需要排序;缺点是比较次数会随着记录数增加。1000 条记录最坏情况要比较 1000 次。

8.5.4 打印:同样是遍历#

打印和查找的遍历方式一样,只是不比较,而是逐条输出:

for (int i = 0; i < count; i++) {
    printf("%d  %s  %d\n",
           students[i].id,
           students[i].name,
           students[i].score);
}

循环条件始终是 i < count,不是 i < 100。这样只打印有效数据,不会输出后面没用过的空记录。


8.6 常见坑#

坑 1:忘记更新 count

// 添加学生
students[count].id = 1;
snprintf(students[count].name, sizeof(students[count].name), "%s", "Alice");
students[count].score = 92;
// 漏了 count = count + 1;

结果:下次添加时,新记录会覆盖同一条。count 始终是 0,打印和查找也会认为表是空的。

忘记更新 count 时,下一次添加会覆盖同一格

坑 2:添加前不检查 count 是否已满。

if (count >= 100) {
    printf("Table full, cannot add.\n");
    continue;
}

数组只有 100 个位置。如果 count 已经到了 100 还继续往 students[count] 写,就是下标越界,后果不确定。

警告:C 语言本身不会替你检查数组下标是否越界。越界写入可能改坏内存里其他变量的值,导致程序行为异常。

坑 3:scanf 读姓名时用 %s,姓名不能有空格。

scanf("%31s", students[count].name);

%31s 最多读 31 个字符,给末尾的 \0 留出 1 个位置。它仍然遇到空格就停。如果输入 De La Cruzscanf 只会把 De 放进 name

本章的完整程序在读完一个值后调用 clear_line(),会把这一行剩下的 La Cruz 丢掉。这样程序不会因为剩余输入读错后面的分数,但姓名仍然只能保存 De。要读带空格的姓名,可以用 fgets 读整行,再自己处理换行。

坑 4:查找时用 = 而不是 ==

if (students[i].id = target) {    // 错:这是赋值,不是比较

单等号 = 是赋值,双等号 == 才是比较。这个错误编译器通常会给警告,但不会报错。

坑 5:查找只找第一个就停,但没有标记"找到没有"。

for (int i = 0; i < count; i++) {
    if (students[i].id == target) {
        printf("Found\n");
        break;
    }
}
// 循环结束后,不知道是找到了 break 出来的,还是自然走完的

所以需要用一个 found 变量做标记:

int found = 0;
for (int i = 0; i < count; i++) {
    if (students[i].id == target) {
        // 打印记录
        found = 1;
        break;
    }
}
if (!found) {
    printf("Not found.\n");
}

8.7 自己试试看#

Q1:添加一个"按姓名查找"的功能。

提示:字符串比较不能用 ==,要用 strcmpstrcmp(a, b) == 0 表示两个字符串相等。需要 #include <string.h>

Q2:添加一个"删除"功能——输入学号,把对应记录从表中移除。

提示:找到要删除的记录后,把它后面的所有记录往前挪一格,然后 count 减一。

for (int j = i; j < count - 1; j++) {
    students[j] = students[j + 1];
}
count = count - 1;

Q3:修改程序,让"查找"在表为空时直接给出提示。打印功能已经会提示"表是空的",查找也可以照这个思路处理。

Q4:把添加、查找、打印分别写成独立的函数,用 main 调用它们。思考:函数怎么修改 count

提示:如果函数接收 count 的值(而不是地址),函数里 count + 1 不会影响 main 里的 count。这和上一章"结构体传参是副本"的问题一样。要让函数修改外面的变量,需要传地址——这涉及指针。


下一章的问题#

程序运行时,students 数组保存在内存里,添加、查找、打印都没问题。

但只要程序一退出,这块内存就被操作系统回收了。下次再运行,数组是空的,count 回到 0,之前添加的学生记录全部消失。

怎样把数据保存下来,让下次启动时还能用?