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

За полчаса вы разложите одну функцию по паре файлов: объявление в .h, определение в .cpp, — соберёте проект и поймёте, почему компилятор ругается на «multiple definition», если сделать неправильно. Всё на C++17 и любом свежем компиляторе: g++ 11+, clang 14+ или MSVC из Visual Studio 2022. Ничего, кроме компилятора и текстового редактора, не нужно.
Что нужно иметь до начала
Проверьте, что у вас установлено, прежде чем копировать код ниже:
- компилятор с поддержкой C++17. Проверка в терминале:
g++ --versionилиclang++ --version. Для MSVC откройте «Developer Command Prompt» и введитеcl; - любой редактор — VS Code, CLion, Notepad++, хоть блокнот;
- терминал в папке проекта, где будете запускать команду сборки.
Если g++ --version выдаёт версию 4 или 5 — компилятор старый, но для этого примера хватит. Всё, что здесь используется, работает и на C++11.
Зачем вообще делить код на два файла
Пока программа в одном файле — заголовки не нужны. Проблема появляется, когда файлов становится несколько. Функцию, написанную в math_utils.cpp, из main.cpp просто так не видно: компилятор обрабатывает каждый .cpp отдельно и о содержимом соседнего файла не знает.
Заголовок (.h) — это способ сказать другим файлам: «такая функция существует, вот её имя и типы аргументов». А сама реализация лежит в .cpp. Одно предложение о том, почему так: разделение на объявление и определение позволяет компилировать файлы независимо, а компоновщику — потом собрать их вместе.
Заголовок отвечает на вопрос «что есть», исходник — на вопрос «как это работает». Если вы кладёте тело функции в .h, вы стираете эту границу и обычно получаете ошибку компоновки.
Шаги
-
Создайте файл
math_utils.hс объявлением функции. Точка с запятой в конце — вместо тела:// math_utils.h #ifndef MATH_UTILS_H #define MATH_UTILS_H // Складывает два целых числа и возвращает результат int add(int a, int b); #endif // MATH_UTILS_HПроверка: в файле нет фигурных скобок с телом функции, только объявление со
;. -
Создайте файл
math_utils.cppс реализацией. В первой строке подключите свой же заголовок:// math_utils.cpp #include "math_utils.h" int add(int a, int b) { return a + b; }Проверка: сигнатура
int add(int a, int b)совпадает с той, что в заголовке, до последнего символа. -
Создайте
main.cpp, подключите заголовок и вызовите функцию:// main.cpp #include <iostream> #include "math_utils.h" int main() { int result = add(2, 3); std::cout << "2 + 3 = " << result << std::endl; return 0; }Проверка: вы подключаете
math_utils.h, а неmath_utils.cpp. Исходники в#includeне подключают. -
Соберите оба
.cppодной командой. Заголовок в команде не указывается — он подтягивается через#include:g++ -std=c++17 main.cpp math_utils.cpp -o appДля clang замените
g++наclang++. Для MSVC:cl /std:c++17 main.cpp math_utils.cpp.Проверка: в папке появился файл
app(илиapp.exeна Windows), ошибок в терминале нет. -
Запустите:
./appПроверка: в терминале выводится строка
2 + 3 = 5. Если да — заголовок и исходник связались правильно.
Что за строки с решёткой в начале заголовка
Три строки — #ifndef, #define, #endif — это include guard*. Они защищают от того, что содержимое заголовка попадёт в один и тот же .cpp дважды. Такое случается, когда файл A.h подключает B.h, а ваш исходник подключает оба напрямую.
* Include guard — защита от повторного включения. При первом подключении MATH_UTILS_H ещё не определён, поэтому код внутри попадает в компиляцию и макрос определяется. При втором подключении #ifndef уже ложен, и блок пропускается.
Имя макроса (MATH_UTILS_H) должно быть уникальным в пределах проекта — обычно его делают из имени файла заглавными буквами. Альтернатива, которую поддерживают все современные компиляторы, — одна строка #pragma once вместо трёх. Она короче, но формально нестандартна; классический include guard работает везде.
Главное правило: что можно в заголовке, а что нет
Заголовок подключается в несколько .cpp. Если в нём лежит определение обычной функции, оно попадёт в каждый из них — и компоновщик увидит несколько одинаковых функций. Это и есть ошибка «multiple definition».
| Что | В .h (заголовок) | В .cpp (исходник) |
|---|---|---|
Объявление функции (int add(int, int);) | да | не обязательно |
| Определение обычной функции (с телом) | нет — ошибка компоновки | да, место для тела |
| Определение класса/структуры | да | редко |
Определение шаблона (template) | да, шаблоны — исключение | обычно нет |
inline-функция | да, inline снимает запрет | да |
| Глобальная переменная (определение) | нет, только extern-объявление | да |
Запомните на старте одно: в заголовок — объявления, в исходник — тела. Шаблоны и inline — исключения, к ним вернётесь позже, когда они понадобятся.
Если не получилось
undefined reference to 'add(int, int)'(илиunresolved external symbolв MSVC). Компилятор нашёл объявление, но не нашёл тело. Причина почти всегда одна: вы забыли добавитьmath_utils.cppв команду сборки. Проверьте, что вg++перечислены оба.cpp.multiple definition of 'add(int, int)'. Тело функции попало в заголовок и размножилось. Перенесите его в.cpp, в.hоставьте только объявление со;. Либо, если это осознанно, пометьте функциюinline.fatal error: math_utils.h: No such file or directory. Компилятор не нашёл заголовок. Проверьте имя в кавычках#include "math_utils.h"и то, что файл лежит в той же папке, что и исходник.redefinition of 'add'при сборке одного файла. В один.cppзаголовок попал дважды, а include guard отсутствует или у двух заголовков совпало имя макроса. Добавьте#ifndef/#define/#endifс уникальным именем.- Собирается, но при запуске выводит не то. Сигнатуры в
.hи.cppразошлись — например, в заголовкеint add(int, int), а в исходникеlong add(int, int). Компилятор считает их разными функциями. Сверьте типы посимвольно.
Что делать дальше
Когда пара файлов собирается — переходите к системе сборки, чтобы не перечислять исходники руками. Для старта хватит CMake: он сам найдёт все .cpp и пересоберёт только изменённое. Дальше по порядку стоит разобрать три темы, каждая из которых заслуживает отдельного чтения: почему шаблоны кладут целиком в заголовок; чем #include "..." в кавычках отличается от #include <...> в угловых скобках; и что такое forward declaration, которое ускоряет компиляцию и разрывает циклические зависимости между заголовками.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: cppreference — Header files и препроцессор, ISO C++ Core Guidelines: разделение интерфейса и реализации
Частые вопросы
А если у меня старый компилятор без C++17?
Можно ли положить всё в один .h и подключать его?
#pragma once или классический include guard — что выбрать?
Почему заголовок не нужно указывать в команде компиляции?
Не сработало: собралось, но ошибка undefined reference. В чём дело?
Нужно ли подключать заголовок в его собственном .cpp?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.