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
| Declaration | Definition | |
|---|---|---|
| Role | Introduces a name | Provides the actual implementation |
| Duplication | Allowed (multiple times) | One per program (ODR) |
| Example | int add(int, int); | int add(int a, int b) { return a+b; } |
| Location | Usually in headers | Usually 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 |
|---|---|---|
| Standard | Yes (C++98+) | No (widely supported) |
| Portability | 100% | ~99% (all major compilers) |
| Simplicity | Verbose | Concise |
| Speed | Slightly slower | Slightly 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):
| Technique | Compile time | Improvement |
|---|---|---|
| Baseline (heavy headers) | 180s | - |
| Forward declarations | 145s | 19% faster |
| Minimal includes | 120s | 33% faster |
| Precompiled headers | 45s | 75% 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
- Headers: Declarations (+ allowed definitions like templates, inline)
- Guards: Use
#pragma onceor#ifndefto prevent double inclusion - Forward declarations: Reduce dependencies and compile time
- Templates: Full definitions must be in headers
- Inline functions: Can live in headers without ODR violations
- Minimize includes: Keep headers lightweight
Related posts
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.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 헤더 가드 완벽 가이드 | #ifndef vs #pragma once 실전 비교
- C++ Preprocessor Directives | ‘전처리 지시자’ 가이드
- C++ One Definition Rule | ‘단일 정의 규칙’ 가이드
Keywords Covered in This Article (Related Search Terms)
This article covers C++, header, include, declaration, ODR.