在 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。
實作上述恢復的程式碼行中最後缺少的函式。