Почему в заголовочных файлах C++ не пишут реализацию кода?

Ответ

В C++ заголовочные файлы (.h, .hpp) предназначены в первую очередь для объявлений (declarations), а исходные файлы (.cpp) — для определений (definitions, реализаций). Это разделение обусловлено моделью компиляции и линковки языка.

Основные причины:

  1. Нарушение правила одного определения (ODR - One Definition Rule): Если определение функции или глобальной переменной поместить в заголовочный файл, который включается в несколько единиц трансляции (.cpp файлов), линковщик получит несколько одинаковых определений и выдаст ошибку.
  2. Время компиляции: Каждый .cpp файл, включающий заголовок с реализацией, будет компилировать этот код заново, что значительно увеличивает общее время сборки.
  3. Инкапсуляция и зависимость: Изменение реализации в заголовке приводит к перекомпиляции всех файлов, которые его включают. Разделение позволяет менять реализацию в .cpp, не затрагивая зависимые модули.

Исключения, когда код допустим/обязателен в заголовках:

  • Шаблоны (templates): Их полное определение должно быть видно в точке инстанциации, поэтому обычно размещается в заголовках.
  • Встраиваемые функции (inline): Чтобы компилятор мог подставить код, inline-функции определяются в заголовке.
  • constexpr / consteval функции: Часто определяются в заголовках, так как могут вычисляться на этапе компиляции.
  • Переменные const / constexpr со внутренней линковкой (например, static const).

Пример:

// header.h - Объявления
#ifndef MY_HEADER_H
#define MY_HEADER_H

void publicFunction(); // Объявление - правильно
int calculate(int a, int b); // Объявление

// Исключения (определения в заголовке):
inline int helper(int x) { return x * 2; } // inline-функция

template<typename T>
T max(T a, T b) { return (a > b) ? a : b; } // шаблон

#endif
// source.cpp - Определения
#include "header.h"

void publicFunction() {
    // Реализация функции
    int result = calculate(5, 3);
}

int calculate(int a, int b) {
    return a + b + helper(a); // Используем inline-функцию из заголовка
}

Ответ 18+ 🔞

А, ну это же классика, ёпта! Смотри, чтобы не накосячить с заголовками в C++, нужно запомнить одну простую, но охуенно важную вещь.

Представь, что заголовочный файл (.h или .hpp) — это как меню в ресторане. Там написано, какие блюда есть, их описание и цена. А исходник (.cpp) — это уже сама кухня, где всё это готовят, режут, жарят и пиздят у повара сигареты. Так вот, если ты начнёшь прямо в меню писать рецепты и там же готовить, то получится пиздец, бардак и волнение ебать. Официанты будут друг другу на голову кастрюли с борщом выливать.

Почему так, а не иначе? Три главные причины, блядь:

  1. Правило одного определения (ODR). Это святое, ёбаный насос! Если ты впихнёшь реализацию функции в хедер, а потом этот хедер #include'нешь в десять разных .cpp файлов, у тебя получится десять одинаковых реализаций одной и той же функции. Линковщик потом соберёт это всё, увидит десять одинаковых calculate(), и у него просто терпения ноль ебать. Он орет: «Мужик, я хуй пойму, какую из них использовать-то?!» — и вываливает тебе ошибку линковки. Сам от себя охуеешь.

  2. Время компиляции. Если в каждом .cpp файле компилятор будет видеть не только объявления, но и целые горы кода из хедера, он будет это всё компилировать заново для каждого файла. Сборка твоего проекта начнёт занимать времени овердохуища, как будто на дворе 2002-й год и ты компилируешь ядро Linux на Pentium III.

  3. Зависимости и инкапсуляция. Допустим, ты поменял какую-то мелкую хуйню в реализации внутри хедера. Поскольку этот хедер включён в сто пятьсот файлов, компилятор будет вынужден пересобрать все эти сто пятьдесят файлов, даже если публичное объявление не изменилось. Это пиздопроебибна по эффективности. А если реализация спрятана в .cpp, то меняешь её там — и пересобирается только этот один файл. Красота!

Но, как и везде, есть исключения, чувак. В некоторых случаях код в заголовке не просто можно, а надо писать:

  • Шаблоны (templates). Вот тут вообще пипец. Компилятор должен видеть полное определение шаблона в момент, когда ты его используешь с конкретным типом. Поэтому шаблоны функций и классов почти всегда пихают прямо в заголовки. Иначе не скомпилируется — проверено, ебать копать.
  • Встраиваемые функции (inline). Ключевое слово inline — это как намёк компилятору: «Братан, можешь заинлайнить этот код прямо на месте вызова». Чтобы он это сделал, ему нужно видеть тело функции. Поэтому inline-функции определяют в хедере.
  • constexpr / consteval функции. Они тоже часто вычисляются на этапе компиляции, им тоже нужно быть на виду. Так что их тоже частенько определяют в заголовках.
  • Константы со внутренней линковкой. Например, static const int SIZE = 1024; в заголовке — это норм, у каждой единицы трансляции будет своя собственная копия, конфликта не будет.

Смотри на живом примере, как это должно выглядеть:

// header.h - Это наше меню, только объявления!
#ifndef MY_HEADER_H
#define MY_HEADER_H

void publicFunction(); // Объявили, что такая функция есть. Что делает? Хуй его знает, смотри в .cpp.
int calculate(int a, int b); // Тоже просто заявка на блюдо.

// А вот исключения — тут уже готовим прямо в меню:
inline int helper(int x) { return x * 2; } // inline-функция, можно тут

template<typename T>
T max(T a, T b) { return (a > b) ? a : b; } // шаблон, без вариантов, только тут

#endif
// source.cpp - А это наша кухня, закрытая от глаз.
#include "header.h"

// Вот тут уже делаем всё по-взрослому:
void publicFunction() {
    // Реализация, которую никто не увидит, пока не слинкует.
    int result = calculate(5, 3);
}

int calculate(int a, int b) {
    return a + b + helper(a); // helper мы взяли из хедера, он inline, всё ок.
}

Короче, суть в чём: держи реализацию подальше от хедера, если только ты не шаблон или не inline. Соблюдаешь это — и у тебя не будет проблем с линковкой, сборка будет быстрой, а архитектура не превратится в одно большое хитрая жопа. Всё гениальное просто, ёпта.