На цьому уроці ми розглянемо, що таке header guards і #pragma once в мові C++, а також навіщо вони потрібні і як їх правильно використовувати.
Проблема дублювання оголошень
Як ми вже знаємо з уроку про попередні оголошення, ідентифікатор може мати тільки одне оголошення. Таким чином, програма з двома оголошеннями однієї змінної отримає помилку компіляції:
|
1 2 3 4 5 6 7 |
int main() { int x; // це оголошення ідентифікатора x int x; // помилка компіляції: дублювання оголошень return 0; } |
Те ж саме стосується і функцій:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int boo() { return 7; } int boo() // помилка компіляції: дублювання визначень { return 7; } int main() { std::cout << boo(); return 0; } |
Хоча ці помилки легко виправити (досить просто видалити дублювання), з заголовковими файлами все трохи по-іншому. Досить легко можна потрапити в ситуацію, коли визначення одних і тих же заголовків будуть підключатися більше одного разу в файл .cpp. Дуже часто це трапляється при підключенні одного заголовка іншим.
Розглянемо наступну програму:
math.h:
|
1 2 3 4 |
int getSquareSides() { return 4; } |
geometry.h:
|
1 |
#include "math.h" |
main.cpp:
|
1 2 3 4 5 6 7 |
#include "math.h" #include "geometry.h" int main() { return 0; } |
Ця, здавалося б, невинна програма, не скомпілюється! Проблема криється у визначенні функції у файлі math.h. Давайте детально розглянемо, що тут відбувається:
Спочатку main.cpp підключає заголовок math.h, внаслідок чого визначення функції getSquareSides() копіюється в main.cpp.
Після цього main.cpp підключає заголовковий файл geometry.h, який, в свою чергу, підключає math.h.
В geometry.h знаходиться копія функції getSquareSides() (з файлу math.h), яка вже вдруге копіюється в main.cpp.
Таким чином, після виконання всіх директив #include, main.cpp матиме наступний вигляд:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int getSquareSides() // з math.h { return 4; } int getSquareSides() // з geometry.h { return 4; } int main() { return 0; } |
Ми отримаємо дублювання визначень і помилку компіляції. Якщо ж розглядати кожен файл окремо, то помилок немає. Однак, в main.cpp, який підключає відразу два заголовки з одним і тим же визначенням функції, ми зіткнемося з проблемами. Якщо для geometry.h потрібна функція getSquareSides(), а для main.cpp потрібен як geometry.h, так і math.h, то яке ж рішення?
Header guards
Насправді рішення просте — використовувати header guards (захист підключення в мові C++). Header guards — це директиви умовної компіляції, які складаються з наступного:
|
1 2 3 4 5 6 |
#ifndef SOME_UNIQUE_NAME_HERE #define SOME_UNIQUE_NAME_HERE // Основна частина коду #endif |
Якщо підключити цей заголовок, то перше, що він зробить, — це перевірить, чи був раніше визначений ідентифікатор SOME_UNIQUE_NAME_HERE. Якщо ми вперше підключаємо цей заголовок, то SOME_UNIQUE_NAME_HERE ще не був визначений. Отже, ми визначаємо SOME_UNIQUE_NAME_HERE (за допомогою директиви #define) і виконується основна частина заголовку. Якщо ж ми раніше підключали цей заголовок, то SOME_UNIQUE_NAME_HERE вже був визначений. В такому випадку, при підключенні цього заголовку вдруге, його вміст буде проігноровано.
Всі ваші заголовки повинні мати header guards. SOME_UNIQUE_NAME_HERE може бути будь-яким ідентифікатором, але, як правило, в якості ідентифікатора використовується ім’я заголовка з закінченням _H. Наприклад, у файлі math.h ідентифікатором буде MATH_H:
math.h:
|
1 2 3 4 5 6 7 8 9 |
#ifndef MATH_H #define MATH_H int getSquareSides() { return 4; } #endif |
Навіть заголовкові файли зі Стандартної бібліотеки С++ використовують header guards. Якби ви поглянули на вміст заголовку iostream, то побачили наступне:
|
1 2 3 4 5 6 |
#ifndef _IOSTREAM_ #define _IOSTREAM_ // Основна частина коду #endif |
Але зараз давайте повернемося до нашого прикладу з math.h, де ми спробуємо виправити ситуацію за допомогою header guards:
math.h:
|
1 2 3 4 5 6 7 8 9 |
#ifndef MATH_H #define MATH_H int getSquareSides() { return 4; } #endif |
geometry.h:
|
1 |
#include "math.h" |
main.cpp:
|
1 2 3 4 5 6 7 |
#include "math.h" #include "geometry.h" int main() { return 0; } |
Тепер, при підключенні в main.cpp заголовку math.h, препроцесор побачить, що MATH_H ще не був визначений, і тому виконається директива визначення MATH_H і вміст math.h скопіюється в main.cpp. Потім main.cpp підключає заголовок geometry.h, який, в свою чергу, підключає math.h. Препроцесор бачить, що MATH_H вже раніше був визначений і вміст geometry.h не буде скопійовано в main.cpp.
Ось так можна боротися з дублюванням визначень за допомогою header guards.
#pragma once
Більшість компіляторів підтримують більш просту, альтернативну форму header guards, — директиву
#pragma:
|
1 2 3 |
#pragma once // Основна частина коду |
Директива #pragma once використовується в якості header guards, але має додаткові переваги: вона коротша і менш схильна до помилок.
Однак, #pragma once не є офіційною частиною мови C++, і не всі компілятори її підтримують (хоча більшість сучасних компіляторів підтримують).
Я все ж рекомендую використовувати header guards, щоб зберегти максимальну сумісність вашого коду.
Тест
Додайте header guards до наступного заголовкового файлу:
add.h:
|
1 |
int add(int x, int y); |
Відповідь
|
1 2 3 4 5 6 |
#ifndef ADD_H #define ADD_H int add(int x, int y); #endif |

Якщо я нічого не пропустив і все правильно зрозумів то є питання. Чи не буде header guards проблемою для підключення заголовкового файлу одразу до декількох файлів з кодом?
Якщо я правильно зрозумів вас, то ви маєте на увазі зв’язок #include “something.h” в іншому файлі “somethingOther.h”, і те, що вони обоє будуть підключені до головного файлу main.cpp.
Я сам не фахівець, тож в моїй відповіді можуть бути помилки, поправте в разі чого 😀
По тому, що я сам прочитав, суть в тому, щоб при компіляції всього проєкту(тобто по факту переводу всього коду з усіх файлів в один виконавчий файл) в нас було лишень унікальні функції і значення.
Тобто якщо підключити заголовковий файл одразу до декількох файлів з кодом, то звісно в реальності в нас не буде зв’язаних файлів, проте main.cpp буде посередником між ними, себто ниткою, котра пов’язує всі файли, і котра в кінцевому результаті запобіжить проблеми дублюванню одного і того самого кода в інших заголовкових файлах.