У цьому уроці ми розглянемо, що таке 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, який #include відразу два заголовка з одним і тим же визначенням функції, ми зіткнемося з проблемами. Якщо для 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 |