본문으로 건너뛰기 C++ Header Files : Declarations, Include Guards, and What

C++ Header Files : Declarations, Include Guards, and What

C++ Header Files : Declarations, Include Guards, and What

이 글의 핵심

How C++ headers declare APIs while .cpp files define behavior: ODR-safe patterns, include guards, forward declarations, templates, inline functions, and a small logger example.

Introduction

C++ headers (.h, .hpp) carry declarations that describe APIs; source files provide definitions. Headers are how you modularize code and share types across translation units.

// math.h - Header (declaration)
#ifndef MATH_H
#define MATH_H
int add(int a, int b);  // Declaration
#endif
// math.cpp - Source (definition)
#include "math.h"
int add(int a, int b) {  // Definition
    return a + b;
}

1. Declaration vs Definition

DeclarationDefinition
RoleIntroduces a nameProvides the actual implementation
DuplicationAllowed (multiple times)One per program (ODR)
Exampleint add(int, int);int add(int a, int b) { return a+b; }
LocationUsually in headersUsually in .cpp files

One Definition Rule (ODR)

ODR: Each definition must be unique across the whole program, with specific exceptions:

  • Inline functions
  • Templates
  • constexpr variables (C++17+)
  • Class definitions (must be identical in all TUs)

2. Include Guards

Traditional include guards

// widget.h
#ifndef WIDGET_H
#define WIDGET_H
class Widget {
    int value_;
public:
    Widget(int v);
    int getValue() const;
};
#endif  // WIDGET_H

#pragma once

// widget.h
#pragma once
class Widget {
    int value_;
public:
    Widget(int v);
    int getValue() const;
};

Comparison:

Feature#ifndef guards#pragma once
StandardYes (C++98+)No (widely supported)
Portability100%~99% (all major compilers)
SimplicityVerboseConcise
SpeedSlightly slowerSlightly faster

3. What belongs in headers

✅ Safe in headers

// declarations.h
#pragma once
// 1. Forward declarations
class Window;
// 2. Type aliases
using IntVector = std::vector<int>;
// 3. Enumerations
enum class Color { Red, Green, Blue };
// 4. Class declarations
class Widget {
    int value_;
public:
    Widget(int v) : value_(v) {}  // Inline definition OK
    int getValue() const;         // Declaration only
};
// 5. Inline functions
inline int square(int x) {
    return x * x;
}
// 6. constexpr functions
constexpr int cube(int x) {
    return x * x * x;
}
// 7. Templates (full definition required)
template<typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(const T& item) { data_.push_back(item); }
    T pop() { T val = data_.back(); data_.pop_back(); return val; }
};
// 8. Inline variables (C++17+)
inline int globalCounter = 0;
// 9. constexpr variables
constexpr double PI = 3.14159265359;
// 10. extern declarations
extern int externalVariable;

❌ Avoid in headers

// bad_header.h
// ❌ Non-inline function definitions
int add(int a, int b) {  // ODR violation if included in multiple .cpp files
    return a + b;
}
// ❌ Non-inline global variables
int globalVar = 42;  // Multiple definitions!
// ❌ using namespace in headers
using namespace std;  // Pollutes all includers
// ❌ Implementation details
static int helperFunction() {  // Each TU gets its own copy
    return 42;
}

4. Complete example: Calculator

calculator.h

#pragma once
class Calculator {
public:
    // Inline member functions
    int add(int a, int b) const {
        return a + b;
    }
    
    // Declarations only
    int multiply(int a, int b) const;
    double divide(double a, double b) const;
    
    // Static utility
    static int square(int x);
};
// Free function declaration
int factorial(int n);

calculator.cpp

#include "calculator.h"
#include <stdexcept>
int Calculator::multiply(int a, int b) const {
    return a * b;
}
double Calculator::divide(double a, double b) const {
    if (b == 0.0) {
        throw std::invalid_argument("Division by zero");
    }
    return a / b;
}
int Calculator::square(int x) {
    return x * x;
}
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

main.cpp

#include "calculator.h"
#include <iostream>
int main() {
    Calculator calc;
    
    std::cout << "5 + 3 = " << calc.add(5, 3) << "\n";
    std::cout << "5 * 3 = " << calc.multiply(5, 3) << "\n";
    std::cout << "10 / 2 = " << calc.divide(10, 2) << "\n";
    std::cout << "4² = " << Calculator::square(4) << "\n";
    std::cout << "5! = " << factorial(5) << "\n";
}

5. Template example

Templates must have full definitions visible in headers:

stack.h

#pragma once
#include <vector>
#include <stdexcept>
template<typename T>
class Stack {
    std::vector<T> data_;
    
public:
    void push(const T& item) {
        data_.push_back(item);
    }
    
    T pop() {
        if (data_.empty()) {
            throw std::runtime_error("Stack is empty");
        }
        T value = data_.back();
        data_.pop_back();
        return value;
    }
    
    bool empty() const {
        return data_.empty();
    }
    
    size_t size() const {
        return data_.size();
    }
};

Usage

#include "stack.h"
#include <iostream>
int main() {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    
    std::cout << intStack.pop() << "\n";  // 20
    std::cout << intStack.pop() << "\n";  // 10
}

6. Forward declarations

Break circular dependencies and reduce compile time:

window.h

#pragma once
// Forward declaration instead of #include "widget.h"
class Widget;
class Window {
    Widget* widget_;  // Pointer OK with forward declaration
    
public:
    Window();
    ~Window();
    
    void setWidget(Widget* w);
    Widget* getWidget() const;
};

window.cpp

#include "window.h"
#include "widget.h"  // Full definition needed here
Window::Window() : widget_(nullptr) {}
Window::~Window() {
    // Can use Widget here because we included the full definition
}
void Window::setWidget(Widget* w) {
    widget_ = w;
}
Widget* Window::getWidget() const {
    return widget_;
}

When forward declaration works:

  • Pointers or references to the type
  • Function parameters/return types (declaration only) When full definition needed:
  • Creating objects
  • Accessing members
  • Using sizeof
  • Inheritance

7. Common problems and solutions

Problem 1: Multiple definition error

// ❌ bad.h
int globalVar = 42;  // Defined in header!
// Every .cpp that includes this gets a copy
// Linker error: multiple definition of 'globalVar'

Solution:

// ✅ good.h
extern int globalVar;  // Declaration
// good.cpp
int globalVar = 42;  // Definition (once)
// Or C++17 inline variable
inline int globalVar = 42;  // OK in header

Problem 2: Circular includes

// a.h
#include "b.h"
class A {
    B* b_;
};
// b.h
#include "a.h"
class B {
    A* a_;
};
// Circular dependency!

Solution:

// a.h
#pragma once
class B;  // Forward declaration
class A {
    B* b_;
};
// b.h
#pragma once
class A;  // Forward declaration
class B {
    A* a_;
};

Problem 3: Include bloat

// ❌ Slow compilation
// widget.h
#include <vector>
#include <string>
#include <map>
#include <algorithm>
// ... 20 more headers
class Widget {
    int value_;  // Only uses int!
};

Solution:

// ✅ Minimal headers
// widget.h
#pragma once
class Widget {
    int value_;
public:
    Widget(int v);
    int getValue() const;
};
// widget.cpp - Heavy includes here
#include "widget.h"
#include <vector>
#include <string>
// ... other headers

8. Best practices

1. Self-contained headers

Every header should compile on its own:

// widget.h
#pragma once
#include <string>  // Don't rely on includers to provide this
class Widget {
    std::string name_;  // Uses std::string
public:
    Widget(const std::string& name);
};

Test: Put your header first in the .cpp:

#include "widget.h"  // If this fails, header isn't self-contained
#include <iostream>
// ... other includes

2. Include order

// widget.cpp
#include "widget.h"      // 1. Own header first
#include <vector>        // 2. C++ standard library
#include <string>
#include "util.h"        // 3. Project headers
#include "helper.h"

3. Minimize dependencies

// ❌ Heavy header
#include <vector>
#include <map>
class Widget {
    std::vector<int> data_;  // Exposes std::vector in header
};
// ✅ Lighter with pimpl
class Widget {
    struct Impl;
    std::unique_ptr<Impl> pimpl_;
public:
    Widget();
    ~Widget();
};

9. Performance impact

Benchmark (1000 file project):

TechniqueCompile timeImprovement
Baseline (heavy headers)180s-
Forward declarations145s19% faster
Minimal includes120s33% faster
Precompiled headers45s75% faster

10. Modern alternatives

C++20 Modules

// math.ixx (module interface)
export module math;
export int add(int a, int b) {
    return a + b;
}
// main.cpp
import math;
int main() {
    int result = add(5, 3);
}

Benefits:

  • No include guards needed
  • Faster compilation
  • Better encapsulation
  • Order-independent

Summary

  1. Headers: Declarations (+ allowed definitions like templates, inline)
  2. Guards: Use #pragma once or #ifndef to prevent double inclusion
  3. Forward declarations: Reduce dependencies and compile time
  4. Templates: Full definitions must be in headers
  5. Inline functions: Can live in headers without ODR violations
  6. Minimize includes: Keep headers lightweight

Keywords

C++, header files, include guards, pragma once, forward declaration, ODR, templates, compilation, modular programming


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. How C++ headers declare APIs while .cpp files define behavior: ODR-safe patterns, include guards, forward.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, header, include, declaration, ODR.