Урок №23. Header guards и #pragma once

В данном занятии мы изучим концепцию header guards и директиву pragma once в языке программирования C++. Рассмотрим их назначение, необходимость и правильное применение.
Проблема дублирования объявлений
Как было рассмотрено в уроке о предварительных объявлениях, идентификатор может быть связан только с одним объявлением. Поэтому программа, содержащая два объявления одной переменной, вызовет ошибку компиляции:
int main ( )
{
int x ; // это объявление идентификатора x
int x ; // ошибка компиляции: дублирование объявлений
return 0
}
То же самое относится и к возможностям:
int boo ( )
{
return 7 ;
}
int boo ( ) // ошибка компиляции: дублирование определений
{
return 7 ;
}
int main ( )
{
std :: cout << boo ( ) ;
return 0 ;
}
Хотя вышеупомянутые ошибки могут быть легко исправлены (достаточно просто удалить дублирование), с заголовочными файлами ситуация несколько иная. Легко можно столкнуться с ситуацией, когда определения одних и тех же заголовочных файлов будут подключаться более одного раза в файле .cpp. Это часто происходит при подключении одного заголовочного файла другому.
Давайте изучим следующий код:
math.h:
int getSquareSides ( )
{
return 4 ;
}
Файл main.cpp:
#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.
Функция getSquareSides, которая находится в geometry.h и уже второй раз копируется в main.cpp, является дубликатом изначальной версии из файла math.h.
По завершении всех инструкций include, содержимое main.cpp будет иметь следующий вид:
int getSquareSides ( ) // из math.h
{
return 4 ;
}
int getSquareSides ( ) // из geometry.h
{
return 4 ;
}
int main ( )
{
return 0 ;
}При использовании двух заголовочных файлов с одинаковым определением функции в main.cpp возникает дублирование определений и ошибка компиляции. В отдельных файлах ошибок нет, но при подключении их в main.cpp возникают проблемы. Например, если в geometry.h требуется функция getSquareSides(), а в main.cpp нужны и geometry.h, и math.h, как быть? Какое решение можно найти в данной ситуации?
Header guards
Для решения данной проблемы достаточно применить механизм header guards (защиты от множественного включения в языке программирования C++). Header guards представляют собой условные директивы компиляции, которые включают в себя следующее:
#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:
Header file math.h:
#ifndef MATH_H
#define MATH_H
int getSquareSides ( )
{
return 4 ;
}
#endif
Даже заголовочные файлы из Стандартной библиотеки C++ используют механизм защиты от множественного включения. При просмотре содержимого заголовочного файла iostream можно заметить следующее:
ifndef _IOSTREAM_
define _IOSTREAM_
// основная часть кода
endif
Однако вернемся к примеру с библиотекой math.h, где мы попытаемся улучшить ситуацию с использованием защиты заголовков:
Библиотека math.h:ifndef MATH_H
define MATH_H
int getSquareSides ( )
{
return 4 ;
}
endifФайл geometry.h содержит информацию о геометрических операциях.
include "math.h"
Файл main.cpp:
include "math.h"
include "geometry.h"
int main ( )
{
return 0 ;
}При включении заголовочного файла math.h в main.cpp, препроцессор обнаруживает, что MATH_H не определен, поэтому происходит определение MATH_H и содержимое math.h копируется в main.cpp. Затем main.cpp подключает заголовочный файл geometry.h, который в свою очередь также подключает math.h. Поскольку MATH_H уже был определен ранее, содержимое geometry.h не будет скопировано в main.cpp.
Вот как можно избежать повторения определений с использованием защиты заголовков.
#pragma once
Многие компиляторы поддерживают менее сложную, альтернативную версию header guards — pragma директиву:
pragma once
// основная часть кода
Директива pragma once применяется для защиты заголовочных файлов от множественного включения, но она обладает и другими преимуществами: она более компактная и менее подвержена ошибкам.
Тем не менее, pragma once не входит в стандарт языка C++ и не все компиляторы ее поддерживают (хотя большинство современных компиляторов это делают).
Я настоятельно рекомендую применять механизмы защиты заголовков (header guards) для обеспечения максимальной совместимости вашего программного кода.
Тест
Пожалуйста, внесите защиту от множественного включения в следующий заголовочный файл:
Файл add.h содержит следующий код:int add ( int x , int y ) ;
Comments