轨道
/
C++
C++
/
练习
/
数据博士
数据博士

数据博士

学习练习

简介

在 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文件里也同样可用,比如下面例子中的string库。 注意:头文件中的声明后面需要;,但源文件中的定义后面不需要。

Note

Exercism 上许多 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++ 和冯·诺依曼探测器的了解,顺利通过了求职面试。 由于你见过大量测试文件,你的新老板希望你能根据公司最近恢复的一些测试代码,重建相应的源文件和头文件。

在这个练习中,你要重建丢失的文件。 目前,这些文件的内容是无法使用的垃圾数据。

Note

这个概念练习的工作流程与 Exercism 实践练习的结构非常相似。 练习的简介文本只是规格说明的一部分。 测试文件才是你解决给定问题的权威指南。 由于 C++ 编译方式的原因,在你实现测试所需的所有类、函数和枚举的最小版本之前,测试结果可能不会显示在网页界面上。 如果你遇到编译错误,在处理完所有任务和测试之后,它们可能就会消失。

你有四个任务,全都与恢复头文件和源文件中丢失的代码有关。

1. 试着提取一些最初的线索

你查看了从测试文件中恢复出来的两行代码:

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

你敏锐的眼睛立刻认出了一个命名空间heaven。 你还看出,一定存在一个名为Vessel的类。 这个构造函数显然可以用两个或三个实参来调用。 第一个实参的类型似乎是string,第二个是一个数字。 还可以用第三个实参来初始化Vessel类。 第三个实参来自一个star_map命名空间。 它是System类型的枚举项。 你甚至还得到了其中一个枚举项:BetaHydri。

根据你发现的这些信息,准备好源文件和头文件。 你需要两个命名空间:heaven和star_map。 heaven命名空间中有一个类Vessel,它可以用两个或三个实参来调用。 System枚举要放在star_map命名空间中,并且有一个BetaHydri枚举项。 留意其他枚举项,这样你就能不断更新System枚举。

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实例。 从上一个任务中,你能大致了解构造函数第三个实参的默认值。 你还看到了Vessel类的两个公有成员变量,以及replicate成员函数的规格说明。

把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实例并返回一个string。 in_the_same_system比较两个Vessel实例并返回一个bool。

根据上面恢复出来的代码行,实现最后缺失的函数。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
C++ Exercism

准备好开始 数据博士 了吗?

注册 Exercism,借助 19 个概念100 个练习 和真人导师指导,学习并掌握 C++,全部免费。