C++ Header Files | "헤더 파일" 가이드
이 글의 핵심
C++ Header Files에 대한 실전 가이드입니다.
들어가며
C++에서 헤더 파일(.h, .hpp)은 선언(declaration)을 담는 파일입니다. 코드를 모듈화하고 재사용성을 높이는 핵심 요소입니다.
1. 헤더 파일 기본
선언 vs 정의
// math.h (헤더 파일 - 선언)
#ifndef MATH_H
#define MATH_H
int add(int a, int b); // 선언 (declaration)
int subtract(int a, int b);
#endif
// math.cpp (소스 파일 - 정의)
#include "math.h"
int add(int a, int b) { // 정의 (definition)
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
// main.cpp (사용)
#include <iostream>
#include "math.h"
int main() {
std::cout << add(3, 5) << std::endl; // 8
std::cout << subtract(10, 3) << std::endl; // 7
}
선언 vs 정의 비교
| 구분 | 선언 (Declaration) | 정의 (Definition) |
|---|---|---|
| 위치 | 헤더 파일 (.h) | 소스 파일 (.cpp) |
| 역할 | 존재를 알림 | 실제 구현 |
| 중복 | 가능 | 불가능 (ODR 위반) |
| 예시 | int add(int, int); | int add(int a, int b) { return a + b; } |
핵심 개념:
- 선언: 컴파일러에게 “이런 함수가 있다”고 알림
- 정의: 실제 구현 코드
- ODR (One Definition Rule): 정의는 프로그램 전체에서 하나만
2. 인클루드 가드
중복 포함 문제
// myheader.h (가드 없음)
class MyClass {};
// main.cpp
#include "myheader.h"
#include "myheader.h" // 중복 포함!
// 컴파일 에러: MyClass가 두 번 정의됨
인클루드 가드 사용
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
class MyClass {
public:
void doSomething();
};
#endif // MYHEADER_H
동작 원리:
- 첫 번째 포함:
MYHEADER_H가 정의되지 않았으므로 내용 포함 - 두 번째 포함:
MYHEADER_H가 이미 정의되어 있으므로 내용 건너뜀
#pragma once
// myheader.h
#pragma once
class MyClass {
public:
void doSomething();
};
인클루드 가드 vs #pragma once
| 방식 | 장점 | 단점 |
|---|---|---|
#ifndef | 표준, 호환성 좋음 | 코드가 길다, 매크로 이름 충돌 가능 |
#pragma once | 간결, 빠름 | 비표준 (대부분 지원) |
실전 팁:
- 개인 프로젝트:
#pragma once(간결) - 라이브러리:
#ifndef(호환성) - 둘 다 사용해도 됨 (중복 방지)
3. 실전 예제
예제 1: 기본 헤더
// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
public:
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
private:
int lastResult;
};
#endif
// calculator.cpp
#include "calculator.h"
#include <stdexcept>
int Calculator::add(int a, int b) {
lastResult = a + b;
return lastResult;
}
int Calculator::subtract(int a, int b) {
lastResult = a - b;
return lastResult;
}
int Calculator::multiply(int a, int b) {
lastResult = a * b;
return lastResult;
}
int Calculator::divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("0으로 나눌 수 없습니다");
}
lastResult = a / b;
return lastResult;
}
// main.cpp
#include <iostream>
#include "calculator.h"
int main() {
Calculator calc;
std::cout << calc.add(10, 5) << std::endl; // 15
std::cout << calc.subtract(10, 5) << std::endl; // 5
std::cout << calc.multiply(10, 5) << std::endl; // 50
std::cout << calc.divide(10, 5) << std::endl; // 2
}
예제 2: 템플릿 헤더
// stack.h
#ifndef STACK_H
#define STACK_H
#include <vector>
#include <stdexcept>
template<typename T>
class Stack {
private:
std::vector<T> data;
public:
void push(const T& value) {
data.push_back(value);
}
T pop() {
if (data.empty()) {
throw std::runtime_error("스택이 비어있습니다");
}
T value = data.back();
data.pop_back();
return value;
}
const T& top() const {
if (data.empty()) {
throw std::runtime_error("스택이 비어있습니다");
}
return data.back();
}
bool empty() const {
return data.empty();
}
size_t size() const {
return data.size();
}
};
#endif
// main.cpp
#include <iostream>
#include "stack.h"
int main() {
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << "Top: " << intStack.top() << std::endl; // 3
std::cout << "Pop: " << intStack.pop() << std::endl; // 3
std::cout << "Size: " << intStack.size() << std::endl; // 2
}
템플릿 헤더 규칙:
- 템플릿은 헤더에 전체 구현을 넣어야 함
- 컴파일러가 인스턴스화할 때 정의가 필요
- 소스 파일로 분리하면 링크 에러 발생
예제 3: 전방 선언
// window.h
#ifndef WINDOW_H
#define WINDOW_H
#include <string>
class Window {
public:
Window(const std::string& title);
void show();
void hide();
};
#endif
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
class Window; // 전방 선언 (window.h 포함 불필요)
class Widget {
private:
Window* window; // 포인터만 사용
public:
void setWindow(Window* w);
Window* getWindow() const;
};
#endif
// widget.cpp
#include "widget.h"
#include "window.h" // 여기서 포함
void Widget::setWindow(Window* w) {
window = w;
}
Window* Widget::getWindow() const {
return window;
}
전방 선언 장점:
- 컴파일 시간 단축
- 헤더 의존성 감소
- 순환 의존성 해결
예제 4: 인라인 함수
// utils.h
#ifndef UTILS_H
#define UTILS_H
#include <algorithm>
// 인라인 함수는 헤더에 정의 가능
inline int max(int a, int b) {
return a > b ? a : b;
}
inline int min(int a, int b) {
return a < b ? a : b;
}
inline int clamp(int value, int minVal, int maxVal) {
return std::min(std::max(value, minVal), maxVal);
}
// 템플릿 인라인 함수
template<typename T>
inline T square(T value) {
return value * value;
}
#endif
// main.cpp
#include <iostream>
#include "utils.h"
int main() {
std::cout << max(10, 20) << std::endl; // 20
std::cout << min(10, 20) << std::endl; // 10
std::cout << clamp(15, 0, 10) << std::endl; // 10
std::cout << square(5) << std::endl; // 25
std::cout << square(3.5) << std::endl; // 12.25
}
4. 헤더에 넣을 수 있는 것
헤더 파일 구성
// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#include <string>
#include <vector>
// 1. 전역 상수 (inline 또는 constexpr)
inline constexpr int MAX_SIZE = 100;
constexpr double PI = 3.14159;
// 2. 타입 정의
using UserID = int;
using UserList = std::vector<std::string>;
// 3. 열거형
enum class Status {
Success,
Error,
Pending
};
// 4. 클래스 선언
class MyClass {
public:
void publicMethod();
private:
int data;
};
// 5. 인라인 함수 정의
inline int square(int x) {
return x * x;
}
// 6. 템플릿 정의
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 7. 함수 선언
void globalFunction();
// 8. extern 변수 선언
extern int globalCounter;
#endif
헤더 vs 소스 파일
| 항목 | 헤더 (.h) | 소스 (.cpp) |
|---|---|---|
| 클래스 선언 | ✅ | ❌ |
| 함수 선언 | ✅ | ❌ |
| 함수 정의 | ❌ (예외: inline, template) | ✅ |
| 전역 변수 선언 | ✅ (extern) | ❌ |
| 전역 변수 정의 | ❌ | ✅ |
| 상수 | ✅ (constexpr, inline) | ✅ |
| 인라인 함수 | ✅ | ✅ |
| 템플릿 | ✅ | ❌ |
5. 자주 발생하는 문제
문제 1: 중복 정의 (ODR 위반)
// ❌ 헤더에 변수 정의
// myheader.h
int globalVar = 10; // 여러 cpp에서 포함하면 중복 정의!
// ✅ 선언만 (헤더)
// myheader.h
extern int globalVar;
// 정의 (소스)
// myheader.cpp
int globalVar = 10;
// ✅ 또는 inline 사용 (C++17)
// myheader.h
inline int globalVar = 10;
에러 메시지:
error: multiple definition of 'globalVar'
문제 2: 순환 인클루드
// ❌ 순환 의존성
// a.h
#ifndef A_H
#define A_H
#include "b.h"
class A {
B* b; // B의 전체 정의 필요
};
#endif
// b.h
#ifndef B_H
#define B_H
#include "a.h"
class B {
A* a; // A의 전체 정의 필요
};
#endif
// ✅ 전방 선언으로 해결
// a.h
#ifndef A_H
#define A_H
class B; // 전방 선언
class A {
B* b; // 포인터만 사용
};
#endif
// b.h
#ifndef B_H
#define B_H
class A; // 전방 선언
class B {
A* a;
};
#endif
문제 3: 불필요한 인클루드
// ❌ 모든 헤더 포함 (컴파일 시간 증가)
// myclass.h
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <string>
// 실제로는 string만 사용
// ✅ 필요한 것만 포함
// myclass.h
#include <string>
class MyClass {
std::string name;
};
실전 팁:
- 헤더에서 사용하는 타입만 포함
- 소스 파일에서 추가 헤더 포함
- 전방 선언 활용
문제 4: 인클루드 순서
// ✅ 권장 순서
// myclass.cpp
#include "myclass.h" // 1. 자신의 헤더 (의존성 확인)
#include <iostream> // 2. C++ 표준 라이브러리
#include <vector>
#include <sys/types.h> // 3. 시스템 헤더
#include "other.h" // 4. 프로젝트 헤더
// ❌ 잘못된 순서
#include <iostream>
#include "other.h"
#include "myclass.h" // 자신의 헤더가 마지막
자신의 헤더를 먼저 포함하는 이유:
- 헤더가 독립적인지 확인 (누락된 include 발견)
- 의존성 문제를 조기에 발견
6. 모범 사례
좋은 헤더 파일 예제
// user.h
#ifndef USER_H
#define USER_H
#include <string>
#include <vector>
// 전방 선언
class Database;
class Logger;
// 상수
constexpr int MAX_USERNAME_LENGTH = 50;
// 클래스 선언
class User {
public:
// 생성자
User(const std::string& name, int age);
// Getter
const std::string& getName() const;
int getAge() const;
// Setter
void setName(const std::string& name);
void setAge(int age);
// 비즈니스 로직
bool isAdult() const;
void save(Database* db);
private:
std::string name;
int age;
// 헬퍼 함수 선언
bool validateName(const std::string& name) const;
};
// 인라인 함수 (간단한 getter)
inline const std::string& User::getName() const {
return name;
}
inline int User::getAge() const {
return age;
}
// 유틸리티 함수
inline bool isValidAge(int age) {
return age >= 0 && age <= 150;
}
#endif // USER_H
헤더 파일 체크리스트
// ✅ 좋은 헤더 파일
#pragma once // 또는 #ifndef
#include <필요한_헤더>
// 전방 선언
class ForwardDeclared;
// 상수
constexpr int CONSTANT = 100;
// 클래스 선언
class MyClass {
// public → protected → private 순서
};
// 인라인 함수
inline int helper() { return 0; }
#endif
모범 사례:
- 최소 의존성: 필요한 헤더만 포함
- 전방 선언: 포인터/참조는 전방 선언 활용
- 인클루드 가드: 중복 포함 방지
- 인라인 함수: 간단한 함수는 헤더에
- 템플릿: 전체 구현을 헤더에
7. 실전 예제: 라이브러리 설계
예제: 간단한 로거 라이브러리
// logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <string>
#include <fstream>
// 로그 레벨
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
// Logger 클래스
class Logger {
public:
static Logger& getInstance();
void setLogLevel(LogLevel level);
void setOutputFile(const std::string& filename);
void debug(const std::string& message);
void info(const std::string& message);
void warning(const std::string& message);
void error(const std::string& message);
private:
Logger();
~Logger();
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
void log(LogLevel level, const std::string& message);
std::string levelToString(LogLevel level) const;
LogLevel currentLevel;
std::ofstream outputFile;
};
// 편의 매크로
#define LOG_DEBUG(msg) Logger::getInstance().debug(msg)
#define LOG_INFO(msg) Logger::getInstance().info(msg)
#define LOG_WARNING(msg) Logger::getInstance().warning(msg)
#define LOG_ERROR(msg) Logger::getInstance().error(msg)
#endif // LOGGER_H
// logger.cpp
#include "logger.h"
#include <iostream>
#include <ctime>
Logger& Logger::getInstance() {
static Logger instance;
return instance;
}
Logger::Logger() : currentLevel(LogLevel::Info) {}
Logger::~Logger() {
if (outputFile.is_open()) {
outputFile.close();
}
}
void Logger::setLogLevel(LogLevel level) {
currentLevel = level;
}
void Logger::setOutputFile(const std::string& filename) {
outputFile.open(filename, std::ios::app);
}
void Logger::debug(const std::string& message) {
if (currentLevel <= LogLevel::Debug) {
log(LogLevel::Debug, message);
}
}
void Logger::info(const std::string& message) {
if (currentLevel <= LogLevel::Info) {
log(LogLevel::Info, message);
}
}
void Logger::warning(const std::string& message) {
if (currentLevel <= LogLevel::Warning) {
log(LogLevel::Warning, message);
}
}
void Logger::error(const std::string& message) {
log(LogLevel::Error, message);
}
void Logger::log(LogLevel level, const std::string& message) {
std::string levelStr = levelToString(level);
std::string output = "[" + levelStr + "] " + message;
std::cout << output << std::endl;
if (outputFile.is_open()) {
outputFile << output << std::endl;
}
}
std::string Logger::levelToString(LogLevel level) const {
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
default: return "UNKNOWN";
}
}
// main.cpp
#include "logger.h"
int main() {
Logger& logger = Logger::getInstance();
logger.setLogLevel(LogLevel::Debug);
logger.setOutputFile("app.log");
LOG_DEBUG("디버그 메시지");
LOG_INFO("정보 메시지");
LOG_WARNING("경고 메시지");
LOG_ERROR("에러 메시지");
}
정리
핵심 요약
- 헤더 파일: 선언을 담는 파일 (.h, .hpp)
- 인클루드 가드: 중복 포함 방지 (
#ifndef,#pragma once) - 전방 선언: 컴파일 시간 단축, 순환 의존성 해결
- 템플릿: 헤더에 전체 구현
- 인라인 함수: 헤더에 정의 가능
헤더 파일 설계 원칙
| 원칙 | 설명 |
|---|---|
| 최소 의존성 | 필요한 헤더만 포함 |
| 자기 완결성 | 헤더만으로 컴파일 가능 |
| 인클루드 가드 | 중복 포함 방지 |
| 전방 선언 활용 | 컴파일 시간 단축 |
| ODR 준수 | 정의는 소스 파일에 |
실전 팁
-
컴파일 시간 최적화
- 전방 선언 적극 활용
- 불필요한 헤더 제거
- Precompiled Header (PCH) 사용
-
유지보수성
- 명확한 네이밍 (파일명 = 클래스명)
- 주석으로 목적 설명
- 일관된 코딩 스타일
-
디버깅
- 자신의 헤더를 먼저 포함
- 컴파일러 경고 활성화 (
-Wall) - 의존성 분석 도구 사용
다음 단계
- C++ Include Path
- C++ Header Guards
- C++ Preprocessor Directives
관련 글
- C++ include 에러 |
- C++ 헤더 가드 완벽 가이드 | #ifndef vs #pragma once 실전 비교
- C++ Include Path | 인클루드 경로 가이드
- C++ 전방 선언 |
- C++20 Modules 완벽 가이드 | 헤더 파일을 넘어서