九、结构体

到这一页,你可能已经感觉到一个问题:数组只能存「同一种东西的一串」。

一个学生的信息有姓名(字符串)、语文成绩(整数)、数学成绩(整数)、总分(整数)——用数组怎么存?开四个数组,然后靠下标 i 把它们「对齐」:

char name[105][20];
int chinese[105], math[105], total[105];
// 第 i 个学生的信息在 name[i]、chinese[i]、math[i]、total[i]

能跑,但很容易出错:排序的时候你得同时交换四个数组的对应位置,漏一个就全乱。

结构体就是来解决这个问题的——把属于同一个对象的一组数据,打包成一个整体。

这一页 2 章 11 道题。题量不大,但它是「从写小程序到写能维护的程序」的分界线。

29. 定义、访问与数组

29.1 定义自己的类型

struct Student {
    char name[25];
    int chinese;
    int math;
    int total;
};

struct 定义一个「类型」,就像 int 是一种类型一样。区别是 int 只能装一个数,Student 能装一组相关的数据。

定义完就有了一种新类型,可以像 int 一样声明变量:

Student s;                    // 一个学生
Student a[105];               // 105 个学生

访问成员用点号:

s.chinese = 90;
s.math = 85;
s.total = s.chinese + s.math;      // 180
strcpy(s.name, "张三");            // 字符数组不能直接赋值

s.total = s.chinese + s.math 这行最能说明结构体的好处——总分和它的两个组成部分在一起,一眼就知道关系。

结构体定义要放在 main 外面

struct Student { ... };     // 放在这里,全局

int main() {
    Student s;               // 这里才能用
}

定义在 main 里的结构体,别的函数用不了。一律写在 main 之前,养成习惯。

还有一个新手常犯的:结构体定义结尾的分号不能漏。

struct Student {
    int x;
};          // ← 这个分号漏了,报错信息会指向莫名其妙的地方

第一个结构体

这两题的结构体很简单,目的是让你熟悉「定义 → 声明 → 用点号访问」这套动作。

1146 如果用 sqrt 算距离,注意题目要的是「距离平方」还是「距离」——平方的话不用开根号,能省一步也不用担心精度。

29.2 结构体作为函数参数

结构体可以整个传给函数:

int calcTotal(Student s) {
    return s.chinese + s.math;
}

注意这里又是复制传参——传进去的是整个结构体的副本。所以:

  • 如果结构体很大(比如里面有长字符串),复制会有开销
  • 函数里改 s.chinese 不会影响原来的变量

需要修改原变量时,用引用:

void addBonus(Student &s) {      // 加了 &,传的是本体
    s.total += 10;
}

& 在这里是「引用」,意思是「不复制,直接用原来那个」。第 3 章 scanf 里的 & 是取地址,这里是引用——长得一样,作用都是为了「操作本体而不是副本」。

结构体与函数

731 这类题,比较逻辑写在函数里,主循环只管遍历,会清爽很多。

29.3 结构体数组

最常见的用法:一个数组装一堆对象。

Student a[105];
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
    scanf("%s %d %d", a[i].name, &a[i].chinese, &a[i].math);
    a[i].total = a[i].chinese + a[i].math;
}

注意 scanf("%s", a[i].name) 不加 &——数组名本身就是地址(第 31 章会详细解释)。

a[i].chinese 这个写法要习惯:先按下标取出第 i 个学生,再用点号取出他的语文成绩。

结构体数组

  • 谁考了第k名 —— 读入一批学生,按总分排序后输出第 k 个

这题需要排序,下一章讲。先试着把数据读进来、算好总分,排序部分可以留到下一章。

第 29 章验收

  • 会定义结构体,且知道要写在 main 外面、结尾加分号
  • 会用点号访问成员
  • 知道结构体传参是复制,要改本体得加 &
  • 会读写结构体数组

30. 结构体数组与多关键字排序

这一章解决一个具体问题:怎么按自定义规则排序。

30.1 sort 的第三个参数

第 24 章用过 sort(a, a+n),那是按默认规则(从小到大)排。现在要按「总分从高到低」排,就得告诉 sort 你的规则。

规则写成一个函数:

bool cmp(Student a, Student b) {
    return a.total > b.total;      // 总分大的排前面
}

sort(stu, stu + n, cmp);

cmp(a, b) 的含义是:「a 应该排在 b 前面吗?」

  • 返回 true:a 排前面
  • 返回 false:b 排前面

所以 a.total > b.total 表示「a 的总分比 b 大时,a 排前面」,也就是从大到小。

想从小到大就改成 a.total < b.total。

cmp 里千万别写 >=

bool cmp(Student a, Student b) {
    return a.total >= b.total;     // 错!可能导致程序崩溃
}

排序算法要求 cmp(a, b) 和 cmp(b, a) 不能同时为真。a == b 时,a >= b 和 b >= a 都返回 true,这会破坏排序算法的前提,某些情况下直接段错误。

一律用 > 或 <,不用 >= 或 <=。

30.2 多关键字排序

真实题目经常要求「总分相同看语文,语文相同看数学」:

bool cmp(Student a, Student b) {
    if (a.total != b.total) return a.total > b.total;    // 第一关键字
    if (a.chinese != b.chinese) return a.chinese > b.chinese;  // 第二关键字
    return a.math > b.math;                               // 第三关键字
}

写法套路:逐级比较,前面的分出胜负就直接返回,分不出才看下一级。

这个结构几乎是所有多关键字排序的标准写法,记住它。

还有一种更紧凑的写法(利用「与」的短路特性):

bool cmp(Student a, Student b) {
    if (a.total != b.total) return a.total > b.total;
    return a.chinese > b.chinese;
}

多关键字排序

737 做完,自己构造一组「总分相同」的数据测一下,看看第二关键字有没有生效。

30.3 排序的稳定性与原始顺序

有个细节值得知道:sort 是不稳定的——两个「相等」的元素,排完之后谁在前谁在后,不确定。

如果题目要求「分数相同时,按输入顺序输出」,你有两个办法:

办法一:在 cmp 里加一个「编号」关键字

struct Student {
    int id;          // 输入顺序
    int score;
};

bool cmp(Student a, Student b) {
    if (a.score != b.score) return a.score > b.score;
    return a.id < b.id;      // 分数相同,编号小的在前
}

办法二:用 stable_sort(稳定排序,代价是稍慢)

stable_sort(a, a + n, cmp);

推荐办法一——它不依赖库的行为,而且逻辑更明确。

稳定性与顺序

这两题都会碰到「相同怎么排」的问题。只要看到题目里有「相同时按……」,就要在 cmp 里多加一个关键字。

30.4 更复杂的排序题

综合

740 是典型的多关键字:先比金牌,再比银牌,再比铜牌。按 30.2 的套路写就行。

738 如果涉及统计,先用桶或 map 统计好,再转成结构体数组排序。

结构体板块总结

11 道题做完,你应该能:

  • 判断「这题该不该用结构体」(数据是成组的、要一起移动的 → 用)
  • 写出 cmp 函数,包括多关键字和稳定性处理
  • 知道 cmp 里不能用 >=

结构体真正的价值不是「能存更多东西」,而是「让数据之间的关系显式化」。 一个 Student 里放着总分和它的两个组成部分,读代码的人不需要猜它们有什么关系。

这也是为什么下一章讲字符串时,很多题会用 struct 把「单词 + 出现次数」打包在一起——打包 = 关系明确 = 少出 bug。

延伸阅读

服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。

  • 可以先看:结构体 —— 结构体的完整说明,含成员访问和初始化

看不懂是正常的,不是你的问题

OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。

© HiJudge · 内容采用 CC BY-NC-SA 4.0 许可协议            此页面修订于: 2026-11-01 03:44:10

results matching ""

    No results matching ""