Как защитить хедер от повторного включения

от admin

1.36.2. Защита от повторного включения заголовочного файла

В программе может возникнуть ситуация, когда при компиляции файла реализации заголовочный файл другого модуля оказывается включенным несколько раз. Такое повторное включение может привести к сообщениям об ошибках времени компиляции. Это может иметь место при наличии во включенном заголовочном файле определений типов. Такая ситуация характерна для работы со структурами (смотри раздел 1.38). Для защиты от повторного включения обычно применяются две директивы препроцессора: #ifndef … endif и #define. Для примера организуем такую защиту в заголовочном файле модуля my_math, который рассматривался в 1.35.1.

// Файл my_math.h с защитой от повторного включения #ifndef _MY_MATH_H #define _MY_MATH_H double sqr(double x); // … #endif;

Управление включением заголовочного файла my_math.h осуществляется с помощью своего рода флажка, имя которого _MY_MATH_H. Во время первого включения значение флажка не определено. Директива ifndef включает заголовочный файл, только в том случае, когда значение флажка не определено. После первого включения заголовочного файла директива #define назначает флажку некоторое значение. После этого повторное включение оказывается невозможным. Директива #endif ограничивает область действия #ifndef.

1.37. Массивы и указатели

Между массивами и указателями существует сильная связь. Начнем изложение с обсуждения общих сведений, относящихся к массивам.

1.37.1. Массивы. Общие сведения

Массивом называется упорядоченная совокупность однотипных величин, объединенных одним именем. Упорядоченность хранящихся в массиве данных достигается путем использования системы индексов. Количество индексов определяет размерность массива. Для хранения элементов массива выделяется непрерывная область памяти.

Различают одномерные массивы и многомерные массивы. При работе с элементами одномерных массивов используется один индекс, а при работе с многомерными массивами – два и более индексов. Минимальное значение индекса в языке Си принято равным нулю.

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

1.37.1.1. Классификация массивов

В языке Си имеются три разновидности массивов:

• Массивы с фиксированными размерами (МФР).

• Массивы с переменными размерами (МПР).

Память для массивов с фиксированными и переменными размерами выделяется с помощью их определения. Выделение и освобождение памяти для динамических массивов выполняется с использованием библиотечных функций.

1.37.1.2. Определение и инициализация массива

Определение массива позволяет компилятору создать массив. В языке Си определение массива можно написать только для МФР и МПР массивов. Динамические массивы создаются и уничтожаются с применением стандартных функций (malloc и др.). Память для массивов МФР можно выделить как в статической области памяти, так и в стеке. При этом память для всех массивов МФР, определения которых записываются вне функций, выделяется в статической области оперативной памяти компьютера. Память выделяется во время начала работы программы и освобождается во время завершения работы программы. Память для массивов МФР, определения которых записываются внутри функций, выделяется в стеке. Выделение памяти происходит в момент времени, когда управление достигает точки определения, а освобождается в момент времени, когда управление выходит из блока, в котором находится определение массива. Память для массивов МПР может быть выделена только в стеке. Рассмотрим этот вопрос применительно к одномерным и двумерным массивам.

Формат определения одномерного массива имеет следующий вид:

type name[expr];

Здесь type – тип элементов массива, name – имя массива, expr – выражение, определяющее количество элементов в массиве. Для МФР массивов expr должно быть константным выражением, а для МПР массивов должно быть неконстантным.

Формат определения двумерного массива имеет следующий вид:

type name[expr_row, expr_col];

Здесь type – тип элементов массива, name – имя массива, expr_row – выражение, определяющее количество строк, а expr_col – выражение, определяющее количество столбцов в массиве. Заметим, что для МФР массивов все выражения, стоящие в квадратных скобках, должны быть константными выражениями, для МПР массивов эти выражения должны быть неконстантными.

Приведем примеры определений массивов.

В приведенном выше программном коде объявлены два одномерных (x и z) и два двумерных массива (mat и mt). В одномерных массивах должны храниться элементы типа int, а в двумерных массивах – элементы типа double. Массивы x и mat являются массивами с фиксированными размерами. Размеры этих массивов задаются символическими константами. Размеры двух других массивов (z и mt) задаются с помощью переменных; n для массива z и двумя переменными mr и mc – для массива mt.

Отметим очень важное отличие между массивами МФР и МПР, которое состоит в том, что массивы МФР могут быть инициализированы во время определения, а массивы МПР не могут. Для выполнения инициализации необходимо в конце определения массива записать математический знак равенства ( = ), затем в фигурных скобках указать список инициализаторов. В простейшем случае количество инициализаторов должно совпадать с количеством элементов в массиве. Если инциализаторов оказалось меньше количества элементов массива, то компьютер дополняет этот список справа нулями. Ситуация, когда инициализаторов оказалось больше, чем это требуется, рассматривается как ошибка. Компилятор в этом случае выдает сообщение об ошибке. Приведем примеры инициализации массивов.

int x[4] = <1, 3, 5, 7>;// x[0] == 1, x[1] == 3,x[2]== 5, // x[3] == 7 int y[4] = <2, 4>; // y[0] == 2, y[1] == 4, y[2] == 0, y[3]== 0 int z[4] = <0>; // Все элементы массива – нулевые int w[4] = <1, 2, 3, 4, 5>// Сообщение об ошибке int v[] = <1, 2, 3>;// v[0] == 1, v[1] == 2, v[2] == 3 int v[2][3] = < <1, 2, 3>, // Первая строка <2, 4, 6>// Вторая строка >;

Как защитить хедер от повторного включения

Header Guards in C++ are conditional compilation directives that help to avoid errors that arise when the same function or variable is defined more than once by the mistake of a programmer. According to C++, when a function or a variable is defined more than once, it yields an error. Below is the program to illustrate the same:

Program 1:

Output:

Explanation:
In the above program , a function fool() has been defined twice which is causing a fatal error.

Program 2:

Output:

Explanation:
In the above program, the variable x has been defined twice which is causing a fatal error.

These are the most commonly occurring errors and can be fixed with a very basic knowledge of programming. But if a situation arises where a Header file is included in a program and inadvertently a forward declaration of a function is done which has been a part of the already included header file. In that case, the content of the header file is not remembered, no matter whether it’s user-defined or predefined.

So further, a fatal error is encountered which sometimes becomes unresolvable. For this, C++ is armed with some preprocessor directives that avoid this error.

2.11 – Защита заголовков

В уроке «2.6 – Предварительные объявления и определения» мы отметили, что идентификатор переменной или функции может иметь только одно определение (правило одного определения). Таким образом, программа, которая определяет идентификатор переменной более одного раза, вызовет ошибку компиляции:

Точно так же программы, которые определяют функцию более одного раза, также вызовут ошибку компиляции:

Хотя эти программы легко исправить (удалить повторяющееся определение), с помощью заголовочных файлов довольно легко попасть в ситуацию, когда определение в заголовочный файл включается более одного раза. Это может произойти, если заголовочный файл включает с #include другой заголовочный файл (что является обычным явлением).

Рассмотрим следующий академический пример:

Эта, казалось бы, невинно выглядящая программа не компилируется! Вот что происходит. Во-первых, main.cpp включает square.h , что копирует определение функции getSquareSides в main.cpp . Затем main.cpp включает geometry.h , который сам включает square.h . Это копирует содержимое square.h (включая определение функции getSquareSides ) в geometry.h , которое затем копируется в main.cpp .

Таким образом, после разрешения всех директив #include файл main.cpp будет выглядеть так:

Повторяющиеся определения и ошибка компиляции. Каждый файл по отдельности нормальный. Однако, поскольку main.cpp дважды включает через #include содержимое square.h , мы столкнулись с проблемами. Если для geometry.h требуется getSquareSides() , а для main.cpp требуются и geometry.h , и square.h , как бы вы решили эту проблему?

Защита заголовка

Хорошей новостью является то, что мы можем избежать указанной выше проблемы с помощью механизма, называемого защитой заголовка (или защитой включения). Защита заголовка – это директивы условной компиляции, которые имеют следующую форму:

Когда этот заголовок включается, препроцессор проверяет, был ли ранее определен SOME_UNIQUE_NAME_HERE . Если это первый раз, когда мы включаем этот заголовок, SOME_UNIQUE_NAME_HERE не будет определен. Следовательно, он с помощью #define определяет SOME_UNIQUE_NAME_HERE и включает содержимое файла. Если заголовок включен в тот же файл повторно, SOME_UNIQUE_NAME_HERE уже будет определен из первого раза, когда содержимое заголовка было включено, и в этот раз содержимое заголовка будет проигнорировано (благодаря #ifndef ).

Все ваши заголовочные файлы должны иметь защиту заголовков. Имя SOME_UNIQUE_NAME_HERE может быть любым, но по соглашению устанавливается равным полному имени заголовочного файла, набранному заглавными буквами, с использованием подчеркивания вместо пробелов и знаков препинания. Например, у square.h будет защита заголовка будет следующей:

Даже заголовочные файлы стандартной библиотеки используют защиту заголовков. Если бы вы взглянули на заголовочный файл iostream из Visual Studio, вы бы увидели:

Для продвинутых читателей

В больших программах возможно наличие двух отдельных заголовочных файлов (включенных из разных каталогов) с одинаковыми именами (например, directoryA\config.h и directoryB\config.h ). Если для защиты включения используется только имя файла (например, CONFIG_H ), эти два файла могут в конечном итоге использовать одно и то же защитное имя. Если это произойдет, любой файл, который включает (прямо или косвенно) оба файла config.h , не получит содержимое включаемого файла, который будет включен вторым. Это, вероятно, вызовет ошибку компиляции.
Из-за этой возможности конфликтов защитных имен многие разработчики рекомендуют использовать более сложные/уникальные имена в защитах заголовков. Некоторые хорошие предложения – это соглашение об именах <ПРОЕКТ>_<ПУТЬ>_<ФАЙЛ>_H , <ФАЙЛ>_<БОЛЬШОЕ СЛУЧАЙНОЕ ЧИСЛО>_H или <ФАЙЛ>_<ДАТА СОЗДАНИЯ>_H .

Обновление нашего предыдущего примера с помощью защиты заголовков

Вернемся к примеру со square.h , добавив в него защиту заголовков. Чтобы быть последовательными мы также добавим защиту заголовков и в geometry.h .

Читать:
Как сменить иконку exe файла

После того, как препроцессор разрешит все включения, эта программа будет выглядеть так:

Как видно из примера, второе включение содержимого square.h (из geometry.h ) игнорируется потому, что SQUARE_H уже был определен при первом включении. Следовательно, функция getSquareSides включается только один раз.

Защита заголовков не препятствует одиночным включениям заголовка в разные файлы исходного кода.

Обратите внимание, что цель защиты заголовков – предотвратить получение файлом исходного кода более одной копии защищенного заголовка. По замыслу, защита заголовков не препятствует включению данного заголовочного файла (однократно) в отдельные исходные файлы. Это также может вызвать непредвиденные проблемы. Рассмотрим следующую возможность:

Обратите внимание, что square.h включается как из main.cpp , так и из square.cpp . Это означает, что содержимое square.h будет включено один раз в square.cpp и один раз в main.cpp .

Давайте разберемся, почему это происходит более подробно. Когда square.h включается из square.cpp , SQUARE_H определяется до конца square.cpp . Это определение предотвращает повторное включение square.h в square.cpp (что является целью защиты заголовков). Однако после завершения square.cpp SQUARE_H больше не считается определенным. Это означает, что когда препроцессор начинает работу над main.cpp , SQUARE_H в main.cpp изначально не определен.

Конечным результатом является то, что и square.cpp , и main.cpp получают копию определения getSquareSides . Эта программа будет компилироваться, но компоновщик будет жаловаться на то, что ваша программа имеет несколько определений для идентификатора getSquareSides !

Лучший способ обойти эту проблему – просто поместить определение функции в один из файлов .cpp , чтобы заголовок содержал только предварительное объявление:

Теперь, когда программа скомпилирована, функция getSquareSides будет иметь только одно определение (через square.cpp ), так что компоновщик будет счастлив. Файл main.cpp может вызывать эту функцию (даже если она находится в square.cpp ), потому что он включает square.h , в котором есть предварительное объявление для этой функции (компоновщик соединит вызов getSquareSides из main.cpp с определение getSquareSides в square.cpp ).

Разве мы не можем просто избежать определений в файлах заголовков?

Обычно мы советуем вам не включать определения функций в заголовки. Итак, вам может быть интересно, зачем вам включать защиту заголовков, если она защищают вас от чего-то, чего вы не должны делать.

В будущем мы покажем вам довольно много случаев, когда в файл заголовка необходимо поместить определения, не являющиеся функциями. Например, C++ позволяет вам создавать свои собственные типы. Эти пользовательские типы обычно определяются в файлах заголовков, чтобы эти определения можно было распространить на исходные файлы, которые должны их использовать. Без защиты заголовков ваши исходные файлы могут иметь несколько идентичных копий этих определений, что приведет к ошибке компиляции повторяющихся определений.

Таким образом, хотя на данном этапе этой серии обучающих статей не обязательно иметь защиту заголовков, но мы вырабатываем хорошие привычки, чтобы вам не приходилось отказываться от вредных привычек позже.

#pragma once

Многие компиляторы поддерживают более простую альтернативную форму защиты заголовков с помощью директивы #pragma :

#pragma когда-то служила той же цели, что и защита заголовков, а ее дополнительное преимущество заключается в том, что она короче и менее подвержена ошибкам.

Однако #pragma once не является официальной частью языка C++, и не все компиляторы поддерживают ее (хотя большинство современных компиляторов поддерживает).

В целях совместимости мы рекомендуем придерживаться традиционной защиты заголовков. Она не требуют много работы и гарантированно поддерживаются всеми компиляторами.

Резюме

Защита заголовков предназначена для того, чтобы содержимое заданного заголовочного файла не копировалось более одного раза в любой отдельный файл, чтобы предотвратить дублирование определений.

Обратите внимание, что дублирование объявлений – это нормально, поскольку объявление может быть объявлено несколько раз без инцидентов, но даже если ваш заголовочный файл состоит только из объявлений (без определений), всё равно рекомендуется включать защиту заголовков.

Обратите внимание, что защита заголовков не предотвращает копирование содержимого заголовочного файла (один раз) в отдельные файлы проекта. Это хорошо потому, что нам часто нужно ссылаться на содержимое заданного заголовка из разных файлов проекта.

Как происходит компиляция

Напомним, что на прошлой практике был создан проект, состоящий из трёх файлов:

  • 1.cpp — main (вызывает foo() )
  • 2.cpp — foo — определение
  • 2.h — foo — объявление

Заметим, что этот же код можно было написать, без использования 2.h .

Возникает законный вопрос: зачем был создан файл 2.h ?

Для этой небольшой программы он действительно избыточен, однако в больших проектах его использование крайне желательно. Действительно, ведь во-первых использование этой директивы позволяет значительно сократить код (всего одна строчка, тогда как объявлений может быть достаточно много), а во-вторых, перенося все объявления в один файл, мы значительно сократим стоимость внесения модификаций в программу. Так, например, при желании добавить еще один параметр функции, нам нужно будет изменить всего два файла (заголовочный, и соответствующий файл с определением), тогда как в противном случае пришлось бы вносить изменения в каждый файл, данную функцию использующий.

Защита от повторного включения заголовочных файлов

Иногда может возникнуть ситуация, когда один заголовочный файл включён не один, а несколько раз. Посмотрим, к каким проблемам это может привести. Если в файле содержались только объявления (как в файле 2.h из примера), то ничего страшного не произойдёт (стол останется столом, сколько об этом не объявляй), но не всегда всё так удачно. Действительно, ведь заголовочный файл кроме объявлений файлов может содержать и определения (например констант или классов). Тогда его повторное включение приведет к ошибке компоновки. Чтобы этого избежать, все заголовочные файлы следует защищать от повторного включения.

Делается это так:

Здесь директива #ifndef указывает препроцессору, что участок кода до #endif следует компилировать только в случае, если объявления _2_H_ не было. Директива #define же указывает, что _2_H_ следует объявить.

Допустим, что есть файл 3.cpp .

Что произойдет при препроцессинге этого файла?

Вместо каждого #include «2.h» будет подставлено содержимое соответствующего файла. На момент первой подстановки _2_H_ еще не определено, по этому произойдут подстановка объявления функции и объявление _2_H_ . На момент же, когда препроцессор перейдет ко второму включению, _2_H_ уже определено, и потому подстановка выполнена не будет.

Так как для совершение столь распространенного действия приходится писать целых три директивы, а к тому же следить за уникальностью объявляемых констант — была придумана директива #pragma once , которая, будучи помещенной в начало заголовочного файла , позволяет добиться того же результата. Однако пользоваться ей надо осторожно, так как в стандарт она не вошла, и потому поддерживается не всеми компиляторами (gcc и компилятор компании Microsoft — поддерживают).

Подробнее о препроцессоре

Препроцессор — весьма мощное средство, и в языке C он использовался весьма широко.

Возьмем для примера функцию, вычисляющую максимум:

В языке C нет перегрузки функций.

Поэтому, при необходимости написать функцию максимума, принимающая аргументы как целого, так и действительного типов приходится использовать препроцессор:

  • Происходит текстовая подстановка в макрос. (Результат работы программы будет 2, а не 1)
  • В случае если не все параметры заключены в скобки, возможны неожиданности с приоритетами операций.

Сборка

Вспомним, как происходит процесс сборки:

Из этой картинки хорошо видно, зачем нужен Makefile . Процесс сборки весьма громоздок, причем большая часть процесса проделывается для каждого файла независимо от других. Значит, если проделывать каждый шаг лишь для тех файлов, для которых это необходимо (зависимости которых обновились) сборка значительно ускорится. (а для больших проектов, например OpenOffice, она может занимать несколько часов)

Многие компиляторы умеют сами обнаруживать зависимости и создавать Makefile .

Библиотеки

Какие бывают библиотеки?

  • Статические ( *.a , *.lib ) — код функций вставляется в исполняемый файл
  • Динамические ( *.so , *.dll ) — в исполняемый файл вставляется имя функции и ее адрес в библиотеке. Для работы необходим файл библиотеки

Отличие библиотек от программ — нет точки входа int main() .

Пример подключения библиотек:

g++ это синоним для gcc -lstd++ , что указывает, что нужно линковать со стандартной библиотекой с++. По умолчанию приложение собирается с динамическими библиотеками, в случае, если требуется собрать со статическими необходимо явно указать ключ -static . Кстати, как уже говорилось, gcc — это огромный конвейер, содержащий несколько различных компиляторов, и вызов g++ ничего не говорит, о языке написанной нами программы. Информацию о том, какой компилятор следует использовать gcc получает из расширения файла.

Упражнение 1. Попробовать собрать статическую/динамическую библиотеку и использовать ее в программе.

Подробнее об объектных файлах

В объектных файлах не содержится информации о языке, на котором была написана программа (их формат одинаков для различных языков, различен для разных платформ). По этому можно использовать библиотеки, написанные на языке отличном от языка программы. В каждом объектном файле написана написана информация, об объявленных функциях, переменных — она необходима линковщику. Посмотреть эту информацию, можно, например с помощью программы objdump .

Упражнение 2. Посмотреть содержимое объектного файла.

Можно заметить, что в объектных файлах, скомпилированных из исходников, написанных на C++ названия функций изменены, по сравнению с теми, что были им даны в программе.

Почему компилятор переименовывает функции?

В языке C++ возможна перегрузка функций, т.е. есть возможность создать несколько функций с одинаковыми названиями, но разными типами принимаемых и возвращаемых значений.

Так как в объектном файле информации о языке уже не сохраняется (в том числе о типах передаваемых/возвращаемых значений), то для того, чтобы эти функции различать (иначе бы при линковке возникла ошибка повторного определения) компилятор дописывает к названиям функций информацию о типах (этот процесс называется маскированием). Восстановить объявление функции по ее расширенному названию можно, например, с помощью программы c++filt.

Как уже говорилось, названия функций можно восстановить из объектного файла, а часто эта информация совершенно излишня. В случаях, когда размер исполняемой программы или библиотеки критичен, его можно уменьшить, заменив названия функций на более короткие. Одна из программ, разработанных специально для этого случая: strip.

Похожие статьи