C++에서는 선언과 정의가 분리되는 경우가 많아요. 선언은 이른바 헤더 파일에 모아 두고, 각각의 구현은 소스 파일에 넣어요. 헤더 파일은 API라고 생각하면 돼요. 헤더 파일은 코드베이스가 _무엇_을 제공하는지 알려 주고, 어떻게 하는지에 대한 세부 사항까지는 들어가지 않아요.
헤더 파일의 가장 흔한 확장자는 .h예요.
어떤 프로젝트는 .hpp를 쓰기도 하고, 확장자를 아예 생략하기도 해요.
정의는 별도의 .cpp 파일에 들어 있어요.
이 둘을 다시 하나로 합치기 위해, 소스 파일은 해당 헤더 파일을 _포함_하는 것으로 시작해요.
자주 쓰고 싶은 "super_root" 함수를 제공하는 "quick_math"라는 라이브러리를 작성한다면, 파일은 이렇게 생겼어요:
// 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 <cmath>
#include "quick_math.h"
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();
};
} // namespace robots
#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;
}
};
} // namespace robots
프로젝트는 이런 구성들을 조합해서 쓰기도 하고, 각각의 사용 사례에 무엇이 가장 잘 맞는지를 두고 많은 논의가 있어요.
위의 예시 헤더 파일에서 #pragma once 줄을 눈치챘을 수도 있어요.
이것을 인클루드 가드라고 해요. 인클루드 가드는 컴파일할 때 파일의 내용이 오류를 피하기 위해 딱 한 번만 포함되도록 보장해요.
#ifndef로 시작해서 #endif로 끝나는, 좀 더 복잡한 인클루드 가드 변형도 있는데, 아래에서 자세히 다뤄요.
C++ 코드는 절차적으로 평가돼요. 함수를 사용하려면, 사용하는 시점에 컴파일러가 그 함수를 알고 있어야 해요. 때로는 소스 코드에 정의를 선형으로 배치하는 방법을 찾을 수 없을 때도 있어요. 아래 예시를 한번 봐요:
int myFunction(int n) {
if (n < 10) {
return n;
} else {
return myOtherFunction(n / 10);
}
}
int myOtherFunction(int m) { return myFunction(m / 2); }
myFunction이 정의되는 시점에 컴파일러는 아직 myOtherFunction을 몰라요.
안타깝게도, 이 순환 참조 문제는 순서를 바꾼다고 해결되지 않아요.
C++은 전방 선언을 제공해서, myFunction과 myOtherFunction이 정의되기 전에 컴파일러에게 알려 줄 수 있어요.
컴파일러는 선언 이후 어느 시점에 정의가 뒤따를 것이라고 가정해요.
다음 예시는 함수에 전방 선언을 어떻게 사용하는지 보여줘요.
int myFunction(int n); // Forward declaration of myFunction
int myOtherFunction(int m); // Forward declaration of myOtherFunction
// Definition of myFunction
int myFunction(int n) {
if (n < 0) {
return 0;
} else {
return myOtherFunction(n - 2);
}
}
// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }
프로젝트 안에서 같은 파일이 여러 번 포함되는 것은 문제가 되지 않아요. 헤더 파일에는 정의가 들어가면 안 돼요. 프로젝트 전체에서 같은 정의가 두 번 이상 있어서는 안 돼요. 이것을 "하나의 정의 규칙"이라고 해요. 이 규칙은 컴파일러가 강제해요.
인클루드 가드를 사용하면 같은 정의가 의도치 않게 여러 번 포함되는 것을 쉽게 피할 수 있어요.
인클루드 가드는 컴파일 단계에서 특별한 절차를 통해 만들어져요.
목표는 특정 변수가 설정되지 않았을 때만 파일을 포함하고, 파일이 포함되면 그 변수를 설정하는 거예요.
이 변수의 이름은 흔히 파일 이름을 변형해서 정해요.
또 다른 방법은 UUID를 생성해서 같은 이름을 실수로 두 번 쓰는 위험을 줄이는 거예요.
아래에서 MY_HEADER_FILE_H를 변수로 사용한 문법을 볼 수 있어요.
#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H
// file content
#endif
#pragma once의 문제는, 프래그마가 C++ 언어의 공식적인 일부가 아니고 구현이 컴파일러마다 다르다는 점이에요.
많은 대형 프로젝트가 더 간단한 프래그마 방식을 택했지만, 몇몇은 여전히 조심스러워해요.