在 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库。
注意:头文件中的声明后面需要;,但源文件中的定义后面不需要。
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++ 和冯·诺依曼探测器的了解,顺利通过了求职面试。 由于你见过大量测试文件,你的新老板希望你能根据公司最近恢复的一些测试代码,重建相应的源文件和头文件。
在这个练习中,你要重建丢失的文件。 目前,这些文件的内容是无法使用的垃圾数据。
这个概念练习的工作流程与 Exercism 实践练习的结构非常相似。 练习的简介文本只是规格说明的一部分。 测试文件才是你解决给定问题的权威指南。 由于 C++ 编译方式的原因,在你实现测试所需的所有类、函数和枚举的最小版本之前,测试结果可能不会显示在网页界面上。 如果你遇到编译错误,在处理完所有任务和测试之后,它们可能就会消失。
你有四个任务,全都与恢复头文件和源文件中丢失的代码有关。
你查看了从测试文件中恢复出来的两行代码:
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枚举。
你又从测试中发现了更多代码行:
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添加到头文件和源文件中。
你在恢复出来的测试文件中又找到了一些有趣的代码行:
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枚举项。
在浏览测试文件的过程中,你只找到两段尚未弄清楚的代码:
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。
根据上面恢复出来的代码行,实现最后缺失的函数。