恢复丢失的代码

概念学习C++17

简介

C++ 常把声明与定义分开。声明集中在头文件中,对应实现放在源文件中。可以把头文件看作 API:它告诉你代码提供什么,而不展开说明如何实现。

头文件与源文件

头文件最常见的扩展名是 .h,有些项目使用 .hpp,也有项目完全不使用扩展名。

定义位于独立的 .cpp 文件中。为了把声明与定义关联起来,源文件开头应包含对应头文件。

假设要编写名为 quick_math 的库,提供经常使用的 super_root 函数,文件可以这样组织:

// A file named quick_math.h
#pragma once
namespace quick_math {
    double super_root(double x, int n);
}
// A file named quick_math.cpp
#include "quick_math.h"
#include <cmath>
double quick_math::super_root(double x, int n) {
    while(n) { x = std::sqrt(x), --n;}
    return x;
}

若某个头文件仅在实现中需要,只需在源文件中添加对应的 #include。头文件已经包含的内容,在包含它的 .cpp 中也可以使用,例如下面的字符串库。注意:头文件中的声明后需要分号,源文件中的函数定义后不需要分号。

注意 许多 C++ 练习最初只提供两个近乎空白的文件:头文件和源文件。此类任务需要根据测试文件 *_test.cpp 确认函数名和命名空间等接口要求。

类与头文件

类可能变得复杂,头文件与源文件之间如何划分也容易令人困惑。一种布局方式是将实现细节放在源文件中,把所有声明和成员变量放在头文件中:

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H
#include <string>
namespace robots {
    class Flower {
        private:
            bool needs_water{};
            int size{};
            std::string name{};
        public:
            Flower(std::string name, int size = 0);
            void give_water();
            std::string get_name();
            int get_size();
            void start_next_day();
    };
}
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"
robots::Flower::Flower(std::string name, int size) {this->name = "Robotica " + name; this->size = size;}
void robots::Flower::start_next_day() {if (!needs_water) ++size; needs_water = true;}
std::string robots::Flower::get_name() {return name;}
int robots::Flower::get_size() {return size;}

当头文件用作 API 概览时,读者会在这里查找默认值等信息。因此,构造函数参数 size 的默认值应写在头文件,而不是实现中。源文件中的定义加上命名空间 robots 和类名 Flower 作为限定。

另一种方式是仅头文件库,完全不提供 .cpp 文件:

// A file named robot_flower.h
#pragma once
#include <string>
namespace robots {
    class Flower {
        private:
            bool needs_water{};
            int size{};
            std::string name{};
        public:
            Flower(std::string name, int size = 0) {this->name = "Robotica " + name; this->size = size;}
            void give_water() {needs_water = false;}
            std::string get_name() {return name;}
            int get_size() {return size;}
            void start_next_day() {if (!needs_water) ++size; needs_water = true;}
    };
}

项目可以组合这些布局。针对不同使用场景应选择哪种方式,仍有许多讨论。

防止重复包含

你可能注意到了前面头文件中的 #pragma once。它用于防止重复包含,确保同一翻译单元中该文件内容只被包含一次,避免重复定义错误。另一种较繁琐的方式以 #ifndef 开始、以 #endif 结束,目的相同,前面的 Flower 示例展示了这种写法。

任务说明

今天是你在澳大利亚信息恢复公司 Doctor Data 工作的第一天。凭借 C++ 与冯·诺依曼探测器知识,你顺利通过了面试。老板知道你看过很多测试文件,请你根据公司最近恢复的测试代码,重建对应的源文件和头文件。

本练习需要恢复丢失的文件。目前这些文件里只有无法使用的乱码数据。

注意:根据接口线索恢复实现 这道概念练习与实践练习的结构类似。简介只是规格的一部分,测试文件才是接口要求的最终依据。受 C++ 编译机制影响,只有补齐测试所需的全部类、函数和枚举的最小版本后,才能运行测试并看到结果。如果出现编译错误,请检查是否还有任务或接口尚未实现。

你需要完成四个任务,恢复头文件与源文件中的代码。

1. 提取最初的线索

从测试文件中恢复了两行代码:

heaven::Vessel bob{"Robert Johansson", 1};
heaven::Vessel will{"Riker", 2, star_map::System::BetaHydri};

你立即识别出 heaven 命名空间和 Vessel 类。构造函数可以接收两个或三个参数,第一个似乎是字符串,第二个是数字。第三个参数来自 star_map 命名空间,是 System 类型的枚举项,其中已经发现了 BetaHydri。

根据这些信息准备头文件和源文件。需要 heaven、star_map 两个命名空间。heaven 中定义 Vessel 类,允许用两个或三个参数构造。star_map 中定义 System 枚举,包含 BetaHydri。后续若发现其他枚举项,及时补充。

2. 寻找更多细节

你又找到了以下测试代码:

heaven::Vessel bob{"Robert Johansson", 1};
REQUIRE(bob.current_system == star_map::System::Sol);
REQUIRE(bob.generation == 1);
heaven::Vessel bob5 = bob.replicate("Mario");
REQUIRE(bob5.current_system == star_map::System::Sol);
REQUIRE(bob5.generation == 2);

新线索揭示了 Vessel 的成员函数 replicate,它接收字符串,返回另一个 Vessel。这些代码还提示了上一任务构造函数第三个参数的默认值,以及两个公开成员变量和复制生成新飞船的行为。

在头文件和源文件中加入 replicate,以及公开成员变量 current_system、generation。

3. 研究内部机制

又从测试文件中恢复了几行值得注意的代码:

heaven::Vessel bob6{"Homer", 3, star_map::System::EpsilonEridani};
REQUIRE(bob6.busters == 0);
bob6.make_buster();
REQUIRE(bob6.busters == 1);
bool success = bob6.shoot_buster();
REQUIRE(success);
REQUIRE(bob6.busters == 0);
success = bob6.shoot_buster();
REQUIRE_FALSE(success);

Vessel 似乎有一个 busters 成员,可以通过 make_buster 和 shoot_buster 修改。在没有其他信息时,先假设 make_buster 返回 void。由于测试检查了 shoot_buster 的返回值,可以推测其返回 bool。

为 Vessel 添加这两个函数及对应成员变量,并继续寻找其他 System 枚举项。

4. 补齐最后的部分

扫描测试文件后,只剩以下两处代码尚未覆盖:

heaven::Vessel bob1{"Bob", 1, star_map::System::AlphaCentauri};
heaven::Vessel marv{"Marvin", 2, star_map::System::DeltaEridani};
heaven::Vessel milo{"Milo", 3, star_map::System::DeltaEridani};
heaven::Vessel howie{"Howard", 4, star_map::System::Omicron2Eridani};

REQUIRE("Bob" == heaven::get_older_bob(bob1, marv));
REQUIRE(heaven::in_the_same_system(marv, milo));
REQUIRE_FALSE(heaven::in_the_same_system(marv, howie));

这里有两个并非 Vessel 成员的函数,因为它们不通过实例调用。get_older_bob 比较两个 Vessel 并返回字符串,in_the_same_system 比较两个 Vessel 并返回布尔值。

根据上面的测试代码实现最后缺失的函数。

需要一点帮助?查看分步提示

提示

通用提示

  • 留意遇到的所有枚举项。
  • 补齐每个任务的接口后才能运行测试;先写出函数与类的最小骨架即可查看测试结果。
  • 测试使用的每个函数、类和枚举都必须在头文件中声明。

1. 提取最初的线索

  • 类与枚举位于不同命名空间。
  • System 使用 enum class。
  • 可以提供两个构造函数,也可以提供一个带默认参数的构造函数。

2. 寻找更多细节

  • 第一条 REQUIRE 检查默认 System。
  • 新复制出的飞船,其 generation 似乎增加 1。
  • 复制过程似乎不改变所属的 System。

3. 研究内部机制

  • shoot_buster 的行为似乎取决于内部计数器。