Заголовочные файлы в C++: разделение кода с нуля
Как разбить программу на .h и .cpp, зачем нужен include guard и почему определение функции в заголовке ломает сборку. Разбираем на рабочих примерах для C++17 и новее.

Вы написали первую программу в одном файле main.cpp, и всё работало. Потом файл разросся до тысячи строк, стало неудобно, и вы решили разложить код по нескольким файлам. Первая же попытка выдала ошибку линковщика multiple definition of ... или undefined reference to ... — и стало непонятно, где ошибка, в коде или в командах компиляции.
Разберёмся, как в C++ раздаётся код между файлами: что кладут в .h, что в .cpp, зачем нужен страж включения и почему одно и то же определение нельзя писать в двух местах. Примеры проверены на g++ 13 и стандарте C++17, но всё сказанное работает и в более старых, и в более новых версиях компилятора.
Что понадобится до начала
- Установленный компилятор:
g++(GCC) илиclang++. Проверьте командойg++ --version. - Любой текстовый редактор.
- Понимание, что такое функция и как вызвать одну функцию из другой в пределах одного файла.
Систему сборки (CMake, Make) пока не подключаем — компилировать будем вручную, чтобы было видно, что происходит.
Объявление и определение — это разные вещи
Ключ к пониманию заголовочных файлов — различие между двумя понятиями.
Объявление говорит компилятору: «такая функция где-то есть, вот её имя, аргументы и тип результата». Оно не создаёт код.
int summa(int a, int b); // объявление: тело отсутствует, в конце точка с запятойОпределение — это тело функции, сам код, который выполняется.
int summa(int a, int b) { // определение: есть тело в фигурных скобках
return a + b;
}Правило, которое объясняет половину ошибок новичков: объявлять функцию можно сколько угодно раз, а определить — ровно один раз на всю программу. Это называется правилом одного определения (ODR1).
Как распределять код: заголовок и реализация
Идея разделения: в .h-файл кладут объявления — что доступно снаружи. В .cpp-файл — определения, сам рабочий код. Другой файл подключает .h, чтобы узнать имена, а определения линковщик найдёт в .cpp.
Разложим маленькую программу на три файла.
Файл mathutils.h — заголовок
#ifndef MATHUTILS_H
#define MATHUTILS_H
// объявления функций, доступных другим файлам
int summa(int a, int b);
int proizvedenie(int a, int b);
#endif // MATHUTILS_HФайл mathutils.cpp — реализация
#include "mathutils.h" // подключаем свои же объявления для проверки
int summa(int a, int b) {
return a + b;
}
int proizvedenie(int a, int b) {
return a * b;
}Файл main.cpp — точка входа
#include <iostream>
#include "mathutils.h" // узнаём об именах summa и proizvedenie
int main() {
std::cout << summa(2, 3) << "\n";
std::cout << proizvedenie(4, 5) << "\n";
return 0;
}Собираем — оба .cpp передаём компилятору вместе:
g++ -std=c++17 main.cpp mathutils.cpp -o program
./programДолжно вывестись 5 и 20. Если так — разделение работает: main.cpp и mathutils.cpp компилируются отдельно, а линковщик связывает вызов summa из main с определением в mathutils.cpp.
Зачем нужен #ifndef / #define / #endif
Три строки в начале и одна в конце заголовка — это страж включения (include guard). Он защищает от повторного включения одного заголовка.
Представьте, что main.cpp подключает mathutils.h и graph.h, а graph.h внутри тоже подключает mathutils.h. Без стража препроцессор вставит содержимое mathutils.h дважды — и вы получите повторные объявления. Страж работает так: при первом включении определяется макрос MATHUTILS_H, при втором #ifndef видит, что макрос уже есть, и пропускает всё до #endif.
Имя макроса должно быть уникальным в пределах проекта.MATHUTILS_Hхорошо,HилиHEADER— почти гарантированный конфликт с чужим заголовком.
Есть короткая альтернатива — директива #pragma once первой строкой заголовка. Она не входит в стандарт C++, но поддерживается всеми основными компиляторами (GCC, Clang, MSVC). Что использовать — вопрос соглашения в команде; классический #ifndef переносим гарантированно.
Что можно, а что нельзя класть в заголовок
Это место, где новички чаще всего спотыкаются. Если положить в .h обычное определение функции и подключить этот заголовок в двух .cpp, каждый из них получит своё определение — и линковщик выдаст multiple definition. Страж включения тут не спасает: он защищает от повторов внутри одной единицы трансляции, а не между разными .cpp.
| Что | Куда обычно кладут | Почему |
|---|---|---|
| Объявление функции | заголовок .h | можно повторять сколько угодно раз |
| Определение обычной функции | .cpp | одно на программу, иначе multiple definition |
Определение класса (class) | заголовок .h | его должны видеть все, кто использует класс |
| Тела методов класса | .cpp (обычно) | там же, где обычные функции |
Шаблоны (template) | заголовок .h | компилятору нужен весь код в месте использования |
Функция с inline | можно в .h | inline разрешает одинаковое определение в нескольких единицах |
Определение класса в заголовке — не то же самое, что определение функции. Само описание класса (список полей и объявления методов) обязано быть в заголовке, иначе другие файлы не смогут создавать объекты. А вот тела методов кладут в .cpp — тем же способом, что и обычные функции.
Если не получилось
undefined reference to 'summa(int, int)'— вы объявили функцию (подключили.h), но не передали компилятору файл с определением. Проверьте, чтоmathutils.cppесть в командеg++. Это ошибка не компиляции, а линковки.multiple definition of 'summa(int, int)'— определение (тело функции) попало в заголовок, который включён в несколько.cpp. Перенесите тело в.cppлибо, если это действительно нужно в заголовке, пометьте функциюinline.redefinition of 'class Graph'— забыли страж включения, заголовок подключился дважды в одну единицу трансляции. Добавьте#ifndef/#define/#endifили#pragma once.fatal error: mathutils.h: No such file or directory— компилятор не нашёл заголовок. Проверьте имя и путь; для заголовков рядом с исходником используют кавычки#include "mathutils.h", а не угловые скобки.- Изменили
.h, а поведение не поменялось — не пересобрали зависимые файлы. При ручной сборке пересоберите всё заново; в будущем эту проблему решает система сборки.
Что делать дальше
Как только файлов станет больше трёх-четырёх, компилировать вручную станет мучительно: придётся перечислять все .cpp и помнить, что зависит от какого заголовка. Следующий шаг — система сборки. Начните с CMake: минимальный CMakeLists.txt перечисляет исходники, а зависимости от заголовков CMake отслеживает сам. Отдельная большая тема — раздельная компиляция в объектные .o-файлы и их линковка; понимание разницы «объявление в .h — определение в .cpp» здесь пригодится напрямую.
1 ODR (One Definition Rule) — правило одного определения из стандарта C++: каждая функция, переменная или тип должны иметь ровно одно определение во всей программе, хотя объявлений может быть много.
Транскрибатор встреч: Запись созвона или диктофона — в текст с разделением по спикерам, протоколом и списком задач.
Спросить за 25 ₽Источники: cppreference — The preprocessor and include directives, cppreference — Definitions and ODR (One Definition Rule)
Частые вопросы
Чем отличается #include с кавычками от угловых скобок?
У меня C++11, а не C++17 — примеры подойдут?
Почему код собирается по одному файлу, а вместе — нет?
Можно ли вообще обойтись без заголовков и писать всё в одном файле?
Почему шаблоны нельзя разделить на .h и .cpp, как обычные функции?
Что лучше — #ifndef или #pragma once?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.