Этапы обработки текста программы включение текстов из заголовочных файлов

Обновлено: 04.07.2024

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

Текст программы урока

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

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

Пояснения по тексту программы урока

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

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

Ничего особенного в трех функциях представленного примера нет. Предполагается, что общие сведения по синтаксису и основным элементам языка C++ у вас уже есть или вы изучаете их по какому-нибудь учебнику. Внимания, возможно, заслуживает только строки 21-23 записи цикла for. Вообще, цикл for в языке C++ имеет интересные особенности. Особенно для тех, кто пришел в язык C++ из Pascal или Fortran. Дополнительную особенность, указанным строкам, придает использование операторов "запятая". Сама шапка цикла for, состоит из трех частей, которые разделяются через символ "точка с запятой". Подробности записи оператора for следует прочитать в учебнике. Для сравнения, приведем четыре часто используемых формы записи цикла for.

Разделение текста программы урока на три компиляционных листа

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

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

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

Файл add-up-to-limit.cpp

Файл incrementator.cpp

Файл main.cpp

Выполнение команды должно пройти без проблем. Действительно, файл add-up-to-limit.cpp состоит из одной только функции addUpToLimit(). Внутри файла не содержится ни одной инструкции препроцессора, так как для компиляции функции определены все символы, и, следовательно, компилятор найдет все необходимое для того, чтобы перевести данный исходный текст на языке C++ в язык ассемблера, и, потом, синтезировать по нему последовательность процессорных инструкций, которые будут соответствовать исходному тексту. Таким образом, первый компиляционный лист у нас полностью соответствует тексту файлу add-up-to-limit.cpp.

Сможем ли мы так же поступить с файлом incrementator.cpp? При попытке выполнить аналогичную команду компиляции мы оказываемся перед решением следующей проблемы.

Согласно описанию ошибки, компилятор сообщает нам о том, что при попытки откомпилировать функцию incrementator() он столкнулся с тем, что ему ничего не известно о символе addUpToLimit в данной области видимости, т.е. внутри текущего компиляционного листа. Следовательно, наш компиляционый лист не является полным и не готов к компиляции.

Возникает задача. Как решить эту проблему? Если мы добавим всю реализацию функции addUpToLimit() в файл incrementator.cpp, то рушится наша задача разделения исходного текста приложения по файлам.

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

Итак, если мы правы, то нам необходимо добавить объявление (declaration) функции addUpToLimit() в файл incrementator.cpp. Следовательно файл incrementator.cpp изменится до следующего состояния.

Исправленный файл incrementator.cpp

Если поставить себе задачу упростить запись объявления функции addUpToLimit(), то его можно записать по следующему образцу, без имени параметров.

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

Теперь создание объектного файла по файлу incrementator.cpp должно пройти без проблем. Выполним для этого следующую команду.

Таким образом, второй компиляционный лист нашего примера представляет собой несколько исправленный файл incrementator.cpp в который было добавлен объявлении функции addUpToLimit().

Итак, мы видим уже известные проблемы и можем предложить известное решение. Добавим объявление требуемых функций в файл main.cpp.

Исправленный файл main.cpp

Третий компиляционный лист нашего исходного примера будет представлять собой исправленный файл main.cpp в который были добавлены объявления функций addUpToLimit() и incrementator(), и в который, командой препроцессора include было "влито" содержимое заголовочного файла iostream.

Теперь у нас есть все необходимое для линковки объектных файлов сделанных по трем компиляционным листам. Выполним для этого следующую команду.

Символ "звездочка" символизирует любой символ любое количество раз и сочетание *.o представляет собой специальную маску (типа wildcard), которая соответствует всем объектным файлам, которые будут найдены в текущей директории исполнения команды. В нашем случае, это соответствует файлам add-up-to-limit.o, incrementator.o и main.o.

Для тех, кто предпочитает более подробную запись, команду линковки можно написать следующим образом.

В результате выполнения последней команды, в операционной системе Linux будет образован исполняемый файл a.out, инструкции которого соответствуют исходному тексту нашей исходной программе, которая была разнесена на три файла.

Использование заголовочных файлов

Представленная в предыдущем разделе система сборки файлов является достаточно общей. Она демонстрирует крайнюю примитивность компилятора C++ в вопросах получения сведений об элементах при обработке ситуаций использования этих элементов. Мы можем раскидать определения (definitions) функций по любому количеству файлов и на основе этих файлов сделать компиляционные листы добавляя в каждый из них объявления (declarations) необходимых символов. Однако в таком решении кроются следующие неудобства.

  1. Если множество символов из одного файла используются во многих других файлах, то в каждом их этих файлов придется повторять множество объявлений этих символов.
  2. Если делается изменения в одном из символов файла, то придется копировать эти изменение во все файлы, где используется этот символ.

Понятно, что такое решение мягко говоря неудобно, а в случае реализации каких-то больших библиотек функций становится просто неприемлемым.

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

Файл add-up-to-limit.h

Файл incrementator.h

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

Файл add-up-to-limit.cpp

Файл incrementator.cpp

Файл main.cpp

Исправленный файл incrementator.h

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

Рассмотрим подробнее, как это работает.

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

Реализация программных библиотек

Представим себе случай разработки каких-нибудь инструментальных библиотек, которые предоставляют для дальнейшего использования большое количество символов (например, функций). Можно представить себе библиотеки для работы с графикой, текстом, видео, звуком, и многое другое. В репозиториях *nix можно найти бесчиссленное множество таких библиотечных пакетов.

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

Следует задуматься вот над чем. Если вы пишете большую библиотеку для C++, то как передать результаты этой работы потребителям или заказчикам?

Для того, чтобы выполнить компиляцию кода использующего символы сторонней библиотеки, требуются определения (declarations) этих символов. Тут вопрос удачно решается предоставлением набора заголовочных файлов, где будут определены эти символы. Таким образом, разработчики сторонней библиотеки должны предоставить набор заголовочных файлов. В системах *nix такие заголовочные файлы из сторонних библиотек обычно устанавливаются соответствующими инсталляторами в директории типа /usr/include и /usr/local/include/.

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

Рассмотрим, какие решения были применены для создания специальных файловых архивов, реализуемых на объектных файлах.

Статические библиотеки (статические архивы)

Рассмотрим пример текущего урока в контексте создания и использования статической библиотеки. После того, как исходный код примера был разбит на файлы, сборка программы производилась по трем компиляционным листам из которых были сделаны три объектных файла: add-up-to-limit.o, incrementator.o и main.o. Чтобы смоделировать на данном материале создание и использование библиотеки, можно посчитать, исходный код нашей сторонней библиотеки составляют следующие файлы.

    Список исходных файлов "сторонней" библиотеки

При таком предположении, получается, что файл main.cpp является потребителем этой сторонней библиотеки и ему для компиляции и последующей линковки нужны следующие артефакты.

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

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

Рассмотрим подробнее команду ar, которая используется для создания статических архивов. Опции cr, в нашем случае, означают создать (create) архив с именем файла libmytools.a и вставить (insert) в него объектные файлы с именами add-up-to-limit.o и incrementator.o.

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

Компилятор не смог найти указанный заголовочный файл так как он был перемещен из директории проекта. Решить вопрос можно двумя способами. Во-первых, можно скопировать нужные заголовочные файлы из директории нашей библиотеки в директорию проекта. Во-вторых, в команде копиляции можно использовать ключ -I, через значение которого можно указать директорию размещения наших заголовочных файлов. Выберем второй путь, как наиболее правильный.

Таким образом, для нашего случая, команда линковки объектного файла main.o исходной программы с файлом статической библиотеки libmytools.a выглядит следующим образом. Обратите внимания на каламбур с именами директории размещения библиотеки и имени архива.

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

Далее мы рассмотрим динамические библиотеки, которые являются более сложным случаем использования кода сторонних библиотек.

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

Все действия будут производиться на Ubuntu версии 16.04.
Используя компилятор g++ версии:

Состав компилятора g++

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

Зачем нужно компилировать исходные файлы?

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

Этапы компиляции:

Перед тем, как приступать, давайте создадим исходный .cpp файл, с которым и будем работать в дальнейшем.

driver.cpp:

1) Препроцессинг

Самая первая стадия компиляции программы.

Получим препроцессированный код в выходной файл driver.ii (прошедшие через стадию препроцессинга C++ файлы имеют расширение .ii), используя флаг -E, который сообщает компилятору, что компилировать (об этом далее) файл не нужно, а только провести его препроцессинг:

Взглянув на тело функции main в новом сгенерированном файле, можно заметить, что макрос RETURN был заменен:

В новом сгенерированном файле также можно увидеть огромное количество новых строк, это различные библиотеки и хэдер iostream.

2) Компиляция

На данном шаге g++ выполняет свою главную задачу — компилирует, то есть преобразует полученный на прошлом шаге код без директив в ассемблерный код. Это промежуточный шаг между высокоуровневым языком и машинным (бинарным) кодом.

Ассемблерный код — это доступное для понимания человеком представление машинного кода.

Используя флаг -S, который сообщает компилятору остановиться после стадии компиляции, получим ассемблерный код в выходном файле driver.s:

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

3) Ассемблирование

Так как x86 процессоры исполняют команды на бинарном коде, необходимо перевести ассемблерный код в машинный с помощью ассемблера.

Ассемблер преобразовывает ассемблерный код в машинный код, сохраняя его в объектном файле.

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

Далее возможно сохранение данного объектного кода в статические библиотеки для того, чтобы не компилировать данный код снова.

Получим машинный код с помощью ассемблера (as) в выходной объектный файл driver.o:

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

4) Компоновка

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

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

Получим исполняемый файл driver:

5) Загрузка

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

Запустим нашу программу:

Заключение

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

Вы когда-нибудь задумывались над тем, как именно создаются исполняемые файлы с расширением .exe из исходных кодов? Какова роль компилятора и компоновщика? В этой очень короткой заметке, я опишу этот процесс очень просто, не вдаваясь в подробности его реализации.

Объединенная единым алгоритмом совокупность описаний и операторов образует программу на алгоритмическом языке. Для того чтобы выполнить программу, требуется перевести ее на язык, понятный процессору — в машинные коды. Этот процесс состоит из нескольких этапов. Рисунок ниже иллюстрирует эти этапы для языка С++.

Этапы создания исполняемой программы на C++

Этапы создания исполняемой программы на C++

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

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

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

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

По мере того, как программы становятся больше (и используют больше файлов), становится всё более утомительным давать предварительные объявления каждой функции, которую вы хотите использовать, и которая определена в другом файле. Было бы неплохо, если бы вы могли поместить все свои предварительные объявления в одно место, а затем импортировать их, когда они вам понадобятся?

Исходные файлы кода C++ (с расширением .cpp ) – это не единственные файлы, которые обычно встречаются в программах на C++. Другой тип файлов – это заголовочный файл (иногда просто заголовок). Заголовочные файлы обычно имеют расширение .h , но иногда вы можете встретить их с расширением .hpp или вообще без расширения. Основная цель заголовочного файла – распространять объявления в исходные файлы кода.

Ключевой момент

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

Использование заголовочных файлов стандартной библиотеки

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

Эта программа печатает « Hello, world! » в консоль с помощью std::cout . Однако эта программа никогда не предоставляла определение или объявление для std::cout , поэтому как компилятор узнает, что такое std::cout ?

Ключевой момент

Когда дело доходит до функций и переменных, стоит помнить, что заголовочные файлы обычно содержат только объявления функций и переменных, а не их определения (в противном случае может произойти нарушение правила одного определения). std::cout объявлен в заголовке iostream, но определен как часть стандартной библиотеки C++, которая автоматически подключается к вашей программе на этапе линкера.

Рисунок 1 Диаграмма процесса сборки

Рисунок 1 – Диаграмма процесса сборки

Лучшая практика

Заголовочные файлы обычно не должны содержать определений функций и переменных, чтобы не нарушать правило одного определения. Исключение сделано для символьных констант (которые мы рассмотрим в уроке «4.14 – const, constexpr и символьные константы»).

Написание собственных заголовочных файлов

А теперь вернемся к примеру, который мы обсуждали в предыдущем уроке. Когда мы закончили, у нас было два файла, add.cpp и main.cpp , которые выглядели так:

(Если вы воссоздаете этот пример с нуля, не забудьте добавить add.cpp в свой проект, чтобы он компилировался).

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

Давайте напишем заголовочный файл, чтобы избавиться от этого бремени. Написать заголовочный файл на удивление легко, поскольку файлы заголовков состоят только из двух частей:

  1. защита заголовка, о которой мы поговорим более подробно в следующем уроке («2.11 – Защита заголовков»);
  2. фактическое содержимое файла заголовка, которое должно быть предварительными объявлениями для всех идентификаторов, которые мы хотим, чтобы другие файлы могли видеть.

Добавление заголовочного файла в проект работает аналогично добавлению исходного файла (рассматривается в уроке «2.7 – Программы с несколькими файлами исходного кода»). Если вы используете IDE, выполните такие же действия и при появлении запроса выберите Файл заголовка (или C/C++ header) вместо Файла С++ (или C/C++ source). Если вы используете командную строку, просто создайте новый файл в своем любимом редакторе.

Лучшая практика

При именовании файлов заголовков используйте расширение .h .

Заголовочные файлы часто идут в паре с файлами исходного кода, при этом заголовочный файл предоставляет предварительные объявления для соответствующего исходного файла. Поскольку наш заголовочный файл будет содержать предварительное объявление для функций, определенных в add.cpp , мы назовем наш новый заголовочный файл add.h .

Лучшая практика

Если заголовочный файл идет в паре с файлом исходного кода (например, add.h с add.cpp ), они оба должны иметь одинаковое базовое имя ( add ).

Вот наш завершенный заголовочный файл:

Следовательно, наша программа будет правильно компилироваться и компоноваться.

Рисунок 2 Диаграмма процесса сборки

Рисунок 2 – Диаграмма процесса сборки

Включение заголовочного файла в соответствующий исходный файл

Позже вы увидите, что большинство исходных файлов включают свой соответствующий заголовочный файл, даже если он им не нужен. Зачем?

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

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

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

Лучшая практика

При написании исходного файла включите в него соответствующий заголовочный файл (если он существует), даже если он вам пока не нужен.

Поиск и устранение проблем

Если вы получаете ошибку компилятора, указывающую, что add.h не найден, убедитесь, что файл действительно называется add.h . В зависимости от того, как вы его создали и назвали, возможно, файл может иметь имя вроде add (без расширения), add.h.txt или add.hpp . Также убедитесь, что он находится в том же каталоге, что и остальные исходные файлы.

Угловые скобки и двойные кавычки

Вам, наверное, интересно, почему мы используем угловые скобки для iostream и двойные кавычки для add.h . Возможно, что заголовочные файлы с таким же именем могут существовать в нескольких каталогах. Использование угловых скобок и двойных кавычек помогает компилятору понять, где ему следует искать заголовочные файлы.

Когда мы используем угловые скобки, мы сообщаем препроцессору, что это заголовочный файл, который мы не писали сами. Компилятор будет искать заголовок только в каталогах, указанных в каталогах включаемых файлов (include directories). Каталоги включаемых файлов настраиваются как часть вашего проекта / настроек IDE / настроек компилятора и обычно по умолчанию используются для каталогов, содержащих заголовочные файлы, которые поставляются с вашим компилятором и/или ОС. Компилятор не будет искать заголовочный файл в каталоге исходного кода вашего проекта.

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

Правило

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

Почему у iostream нет расширения .h ?

Другой часто задаваемый вопрос: «Почему iostream (или любой другой заголовочный файл стандартной библиотеки) не имеет расширения .h ?». Ответ заключается в том, что iostream.h – это другой заголовочный файл, отличающийся от iostream ! Для объяснения требуется небольшой урок истории.

Когда C++ был только создан, все файлы в стандартной библиотеке оканчивались расширением .h . Жизнь была последовательной, и это было хорошо. Исходные версии cout и cin были объявлены в iostream.h . Когда комитет ANSI стандартизировал язык, они решили переместить все функции стандартной библиотеки в пространство имен std , чтобы избежать конфликтов имен с пользовательскими идентификаторами. Однако это представляло проблему: если бы они переместили всю функциональность в пространство имен std , ни одна из старых программ (включая iostream.h ) больше не работала бы!

Кроме того, многие библиотеки, унаследованные от C, которые всё еще используются в C++, получили префикс c (например, stdlib.h стал cstdlib ). Функциональные возможности этих библиотек также были перенесены в пространство имен std , чтобы избежать конфликтов имен.

Лучшая практика

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

Включение заголовочных файлов из других каталогов

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

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

Лучший способ – сообщить вашему компилятору или IDE, что у вас есть куча заголовочных файлов в каком-то другом месте, чтобы он смотрел туда, когда не может найти их в текущем каталоге. Обычно это можно сделать, установив путь включения (include path) или каталог поиска (search directory) в настройках проекта в IDE.

Для пользователей Visual Studio

Кликните правой кнопкой мыши на своем проекте в обозревателе решений и выберите Свойства (Properties), затем вкладку Каталоги VC++.(VC++ Directories). Здесь вы увидите строку с названием «Включаемые каталоги» (Include Directories). Добавьте каталоги, в которых компилятор должен искать дополнительные заголовочные файлы.

Для пользователей Code::Blocks

В Code:: Blocks перейдите в меню Project (Проект) и выберите Build Options (Параметры сборки), затем вкладку Search directories (Каталоги поиска). Добавьте каталоги, в которых компилятор должен искать дополнительные заголовочные файлы.

Для пользователей GCC/G++

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

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

Заголовочные файлы могут включать другие заголовочные файлы

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

Лучшая практика

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

Вопрос: Я не включил <someheader.h> , и моя программа всё равно работала! Почему?

Это один из наиболее часто задаваемых вопросов. Ответ: скорее всего, он работает, потому что вы включили какой-то другой заголовок (например, <iostream> ), который сам включает <someheader.h> . Несмотря на то, что ваша программа будет компилироваться, в соответствии с приведенными выше рекомендациями вам не следует полагаться на это. То, что компилируется у вас, может не компилироваться на машине друга.

Лучшая практика

Рекомендации по использованию заголовочных файлов

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

Читайте также: