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 줄은 소스 파일에만 있으면 돼요.
헤더에 포함된 모든 것은 아래 예제의 string 라이브러리처럼 .cpp 파일에서도 사용할 수 있어요.
주의: ;는 헤더 파일의 선언 뒤에는 필요하지만, 소스 파일의 정의 뒤에는 필요하지 않아요.
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 클래스의 public 멤버 변수 두 개와 replicate 멤버 함수의 명세도 보여요.
헤더와 소스 파일에 replicate 함수와 public 멤버 변수 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을 반환해요.
위에서 복구된 줄을 바탕으로 마지막으로 빠진 함수들을 구현해요.
Exercism에 가입하고 C++ 트랙을 개념 19개연습 문제 100개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.