跳至内容
Topos Uranos

摘要

到目前为止,本课程的每种集合都只保存一种类型的数据:成绩、身高、表格的格子。而问题中的实体却有多个不同类型的属性:一个学生有姓名和成绩;一本书有书名、作者和年份。本课介绍记录,即程序员自己用 struct 定义的复合类型,用来把这些属性归在一个名字之下。本课先讲记录存在的理由,即并行数组的脆弱性;接着讲 struct 的定义、带初始值的字段,以及用花括号和指派初始化器创建记录;然后讲记录作为函数的参数和结果;再讲记录的向量,它用结构化绑定遍历,按某个字段排序和查找,并讨论稳定排序的问题;最后讲如何用反映问题领域的类型为问题建模。

学习目标

  1. 定义带字段和初始值的 struct,并用花括号初始化创建记录,包括使用指派初始化器。
  2. 编写接收和返回记录的函数,并选择传递方式。
  3. 用带投影的 std::ranges::sort、std::ranges::stable_sort 和 std::ranges::find,按某个字段对记录的向量排序和查找,并区分稳定排序与不稳定排序。
  4. 用记录为问题的数据建模,并说明选择哪些字段和类型的理由。

组合数据

并行数组

假设一个程序必须为一个班的每个学生保存姓名、成绩和年龄。凭借到表格一课为止所学的内容,现成的解法是三个向量,每个属性一个,并约定下标 ii 在三个向量中指同一个人:names[i]、grades[i] 和 ages[i]。这样的向量称为并行数组。表格不行,因为它的格子必须全部是同一类型,而姓名不是数。

C++ 程序
#include <algorithm>#include <cstddef>#include <print>#include <string>#include <vector>​int main(){    const std::vector<std::string> names{"安娜", "路易斯", "伊娃"};    std::vector<int> grades{6, 4, 7};    const std::vector<int> ages{19, 22, 20};    std::ranges::sort(grades);    for (std::size_t i{0}; i < names.size(); ++i) {        std::println("{}:成绩 {},{} 岁", names[i], grades[i], ages[i]);    }}
输出
安娜:成绩 4,19 岁路易斯:成绩 6,22 岁伊娃:成绩 7,20 岁

这个程序本想按成绩给学生排序,却只排了成绩。它编译时没有任何警告,内存错误检测器 AddressSanitizer 和未定义行为检测器 UndefinedBehaviorSanitizer 也都没有什么可报告的;然而它的输出是错的:安娜原本是 6 分,输出里却成了 4 分。把三个向量联系在一起的约定没有写在编译器能读到的任何地方,所以没有什么能阻止它被破坏。由此可见并行数组的脆弱性:凡是移动、添加或删除元素的操作,都必须在所有向量中以同样的方式、按同样的顺序重复一遍;只要漏掉一个,数据就会悄无声息地被破坏。

例题错位的行与整张移动的卡片

三个并行数组与一个卡片向量的对照,每张卡片含有一个人的三项数据。

证明

  1. 第 0 列描述一个人:names[0]、grades[0] 和 ages[0] 是安娜的姓名、成绩和年龄。把它们联系起来的只有下标。

  2. std::ranges::sort(grades) 只重排了一行。另外两行并不知情:第 0 列现在拼凑的是安娜的姓名、路易斯的成绩和安娜的年龄,它们不属于任何人。

  3. 另一种做法:只用一个向量,它的元素是卡片。每张卡片含有一个人的三项数据,把它们联系起来的不再是关于下标的约定,而是卡片本身。

  4. 按成绩排序时移动的是整张卡片:路易斯的卡片连同他的姓名和年龄一起移到第一位。任何一项数据都不可能与其余数据分离,因为这个操作根本不会把它们分开看待。

若是如此,问题所要求的就是一种值为卡片的类型:一个值,含有一个姓名、一个成绩和一个年龄,并且作为一个整体被复制、传给函数、存进向量和排序。C++ 没有现成的这种类型,因为每个问题的卡片各不相同;但它允许程序员自己定义。这种类型就是记录。