C++ Header Files | "헤더 파일" 가이드

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

동작 원리:

  1. 첫 번째 포함: MYHEADER_H가 정의되지 않았으므로 내용 포함
  2. 두 번째 포함: 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

모범 사례:

  1. 최소 의존성: 필요한 헤더만 포함
  2. 전방 선언: 포인터/참조는 전방 선언 활용
  3. 인클루드 가드: 중복 포함 방지
  4. 인라인 함수: 간단한 함수는 헤더에
  5. 템플릿: 전체 구현을 헤더에

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("에러 메시지");
}

정리

핵심 요약

  1. 헤더 파일: 선언을 담는 파일 (.h, .hpp)
  2. 인클루드 가드: 중복 포함 방지 (#ifndef, #pragma once)
  3. 전방 선언: 컴파일 시간 단축, 순환 의존성 해결
  4. 템플릿: 헤더에 전체 구현
  5. 인라인 함수: 헤더에 정의 가능

헤더 파일 설계 원칙

원칙설명
최소 의존성필요한 헤더만 포함
자기 완결성헤더만으로 컴파일 가능
인클루드 가드중복 포함 방지
전방 선언 활용컴파일 시간 단축
ODR 준수정의는 소스 파일에

실전 팁

  1. 컴파일 시간 최적화

    • 전방 선언 적극 활용
    • 불필요한 헤더 제거
    • Precompiled Header (PCH) 사용
  2. 유지보수성

    • 명확한 네이밍 (파일명 = 클래스명)
    • 주석으로 목적 설명
    • 일관된 코딩 스타일
  3. 디버깅

    • 자신의 헤더를 먼저 포함
    • 컴파일러 경고 활성화 (-Wall)
    • 의존성 분석 도구 사용

다음 단계

  • C++ Include Path
  • C++ Header Guards
  • C++ Preprocessor Directives

관련 글

  • C++ include 에러 |
  • C++ 헤더 가드 완벽 가이드 | #ifndef vs #pragma once 실전 비교
  • C++ Include Path | 인클루드 경로 가이드
  • C++ 전방 선언 |
  • C++20 Modules 완벽 가이드 | 헤더 파일을 넘어서