Перейти к содержимому
Теория · Сборка и препроцессор · урок 4 из 8

Защита от повторного включения

Вернёмся в контору. Одна памятка иногда ссылается на другую: «к этой странице приложи копию config.h». Если отдел заказал обе памятки, секретарь вложит config.h в папку дважды — один раз напрямую, второй раз вместе с той памяткой, что на неё ссылается.

С заголовками происходит то же. Заголовок config.h определяет константу constexpr int kMaxUsers = 100;. Заголовок users.h подключает config.h, а main.cpp подключает оба — и не компилируется:

C++
#include "config.h"
#include "users.h"  // ✗

int main() {
    return kMaxUsers;
}
TEXT
error: redefinition of 'kMaxUsers'

Откуда второе определение

Препроцессор вставляет текст и ничего в нём не проверяет. В main.cpp текст config.h попадает дважды: напрямую и через users.h. В единице трансляции оказываются два определения kMaxUsers, и компилятор их не принимает. Объявление функции можно повторить сколько угодно раз, а определение — нет.

Вставляйте #include по одному и следите, сколько раз текст config.h попадает в main.cpp, а потом включите защиту.

ВживуюЧто вставляет #include#include: 0 из 4
файлы
config.h
users.h
main.cpp
report.cpp
единицы трансляции
main.cpp
  1. #include "config.h"ждёт
  2. #include "users.h"ждёт
  3. int main() {
  4. return kMaxUsers;
  5. }
препроцессор ещё работает
report.cpp
  1. #include "config.h"ждёт
  2. int report() {
  3. return kMaxUsers;
  4. }
препроцессор ещё работает
компоновка — после компиляции

Препроцессор идёт по строкам #include сверху вниз и вставляет вместо каждой текст файла. Нажмите кнопку.

Вставляйте по одному #include, потом включите защиту

Две формы защиты

Защита от повторного включения не даёт тексту заголовка попасть в единицу трансляции дважды. В конторе это отметка на папке: «config.h уже вложен». У защиты две формы.

#pragma once

Короче всего защиту записать одной строкой в начале заголовка:

C++
#pragma once

constexpr int kMaxUsers = 100;

Когда препроцессор второй раз встречает #include этого файла в той же единице трансляции, он пропускает файл. #pragma once не входит в стандарт C++, но его понимают GCC, Clang и MSVC.

#ifndef, #define и #endif

Стандартная форма — три директивы:

  • #ifndef CONFIG_H в начале файла читается как «если макрос CONFIG_H не определён»;
  • #define CONFIG_H сразу за ней;
  • #endif в конце файла.
C++
#ifndef CONFIG_H
#define CONFIG_H

constexpr int kMaxUsers = 100;

#endif

Макрос CONFIG_H создают без текста: он нужен только как отметка «заголовок уже вставлен». Препроцессор проходит этот файл так:

  1. При первом включении CONFIG_H ещё не определён: #ifndef пропускает текст дальше, и #define определяет макрос.
  2. При втором включении CONFIG_H уже определён, и #ifndef пропускает всё до #endif.

Имя макроса берут из имени файла, заглавными буквами, чтобы у разных заголовков оно не совпало.

Одна единица трансляции

Защита действует внутри одной единицы трансляции. Каждый другой .cpp-файл, который подключает заголовок, получает свою копию его текста. Отметка стоит на одной папке, а в других папках её нет.

Частая ошибка

Заголовок users.h создали копией config.h и забыли поменять имя макроса:

C++
#ifndef CONFIG_H  // ✗
#define CONFIG_H

int count_users();

#endif

Файл main.cpp подключает сначала config.h, потом users.h и вызывает count_users(). Когда препроцессор доходит до users.h, макрос CONFIG_H уже определён, и весь текст users.h пропадает. Компилятор не находит объявления:

TEXT
error: use of undeclared identifier 'count_users'

Дайте каждому заголовку свой макрос: USERS_H для users.h.

Главное

  • #include вставляет текст слово в слово, поэтому заголовок может попасть в единицу трансляции дважды.
  • Второе определение в одной единице трансляции компилятор не принимает.
  • #pragma once или тройка #ifndef, #define, #endif пропускают повторную вставку.
  • Защита действует внутри одной единицы трансляции, а не на всю программу.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом