Урок 21. Заголовочные файлы

   ⁄ 

 Обновлено 24 Фев 2017  ⁄ 

 ⁄   20 

⁄   14299

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

Файлы кода C++ (с расширением .cpp) не являются единственными файлами в проектах и программах. Есть еще один тип файлов, который называется заголовочный файл (файл заголовка, подключаемый файл или header file). Они имеют расширение .h, но иногда их можно увидеть и с расширением .hpp или вообще без расширения. Целью заголовочных файлов является удобное хранение предварительных объявлений для использования другими файлами.

Заголовочные файлы стандартной библиотеки

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

Она выводит “Hello, world!” в консоль с помощью cout. Тем не менее, в этой программе нет определения cout, тогда как компилятор знает, что такое cout? Дело в том, что cout объявлен в заголовочном файле, который называется «iostream». Когда мы пишем #include <iostream>, мы делаем запрос, чтобы всё содержимое из заголовочного файла с именем «iostream» скопировалось в наш файл. Таким образом всё содержимое «iostream» становится доступным для использования.

Как правило, в заголовочных файлах записывают только объявления. Там нет определений, как что-то реализуется. Значит, если cout только объявлен в «iostream» файле, то где же он на самом деле определяется? Он реализован в C++ Runtime Support Library, которая автоматически подключается к нашей программе на этапе линкинга.

risunok1

Подумайте, что бы произошло, если бы заголовка iostream не существовало? Где бы вы ни использовали std::cout, вам пришлось бы вручную копировать все объявления, связанные с std::cout в верхнюю часть каждого вашего файла, который бы использовал эту команду! Хорошо, что можно просто #include <iostream>.



Пишем свой собственный заголовочный файл

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

add.cpp:

main.cpp:

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

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

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

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

Первая часть — header guard, об этом мы поговорим в следующем уроке (про препроцессоры). Header guards предотвращают вызовы (#includе) заголовочного файла больше одного раза с одного и того же файла.

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

add.h:

Чтобы использовать этот файл в main.cpp, мы должны его сначала подключить (include).

main.cpp, который include add.h:

add.cpp остается без изменений:

При компиляции строки #include «add.h», компилятор скопирует содержимое add.h в текущий файл. Поскольку add.h содержит прототип функции add(), то он используется в качестве предварительного объявления add()!

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

risunok2

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

Если вы получите ошибку линкера о том, что аdd() не определен, убедитесь, что вы правильно подключили файл add.cpp в ваш проект и он подключен к компиляции вместе с другими файлами!

Угловые скобки (<>) vs Кавычки («»)

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



Правило: Используйте угловые скобки для подключения заголовочных файлов, которые поставляются вместе с компилятором. А двойные кавычки для всего остального (файлов, которые создали вы).

Одни заголовочные файлы могут подключать другие заголовочные файлы. Тем не менее, мы не рекомендуем это делать. Всегда подключайте заголовочные файлы отдельно в каждом файле .cpp.

Правило: Каждый файл .cpp должен #include все необходимые для компиляции заголовочные файлы.

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

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

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

Дабы обойти эту проблему и обеспечить обратную совместимость для старых программ, был введен новый набор заголовочных файлов, которые используют одни и те же имена, но без расширения .h. Вся их функциональность находилась в std namespace. Таким образом, старые программы с #include <iostream.h> не нужно было переписывать и новые программы уже могли #include <iostream>.

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

Следует отметить, что многие заголовки в стандартной библиотеке имеют только .h версию. Для этих файлов допустимо подключать данную версию. Многие из этих библиотек имеют обратную совместимость со стандартом программирования C, а C не поддерживает namespace. Следовательно, функциональные возможности этих библиотек не будут доступны через пространство имен std. Кроме того, когда вы пишете свои собственные заголовочные файлы, вы должны им присваивать расширения .h, так как вы не будете помещать свой код в std namespace.

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

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

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

Плохой способ — записать относительный путь к файлу, который вы хотите подключить, как часть строки #include. Например:

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

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

В Visual Studio нужно щелкнуть правой кнопкой мыши по вашему проекту в Solution Explorer и выбрать «Properties», затем вкладку «VC++ Directories». Вы увидите пункт «Include Directories». Добавьте туда свои другие каталоги.

В Code::Blocks, перейдите в меню Project и выберите «Build Options», затем вкладку «Search directories». Добавьте ваши каталоги.

С помощью G++, вы можете использовать опцию –I для указания альтернативного подключения директории:

g++ -o main -I /source/includes main.cpp

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

Можно ли записывать определения функций в заголовочном файле?

C++ не будет жаловаться, если вы это сделаете, но вообще такая практика не принята.

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

Для небольших проектов, это, вероятней всего, не будет большой проблемой. Но для более крупных, это может способствовать увеличению времени компиляции (так как этот же код будет компилироваться каждый раз, когда он встречается) и размеру исполняемого файла. Если внести изменения в .cpp файл, то только этот файл нужно будет перекомпилировать. Если внести изменения в определение в заголовочном файле, то каждый файл, который #include этот заголовок, нужно будет перекомпилировать. Из-за одного небольшого изменения вам придется перекомпилировать весь проект!

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

Советы

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

 Всегда подключайте header guards.

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

 Не определяйте функции в заголовочных файлах.

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

 Называйте ваши заголовочные файлы именами исходных файлов, с которыми они связаны (например: grades.h работает с grades.cpp).

 Постарайтесь свести к минимуму количество других заголовочных файлов, которые вы #include. #include только самое необходимое.

 Не #include файлы .cpp.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (108 оценок, среднее: 4,79 из 5)
Загрузка...
Подписаться на обновления:

Комментариев: 20

  1. илья:

    для написания программ требуется мощный пк?
    у меня процессор amd a9 2 ядра
    2 гига оперативной
    достаточно?

    1. Юрий:

      Для прохождения этих уроков мощности вашего ПК будет вполне достаточно.

  2. artem:

    Если я пройду этот туториал меня возьмут в google)?

    1. Юрий:

      Если пройдете — создадите сами свой Google.

  3. painkiller:

    Добрый день!
    Меня вот какие вопросы интересуют:

    1. Для создания заголовочного файла прописывать эти строки обязательно, верно?

    Пробовал создать заголовочный файл и без этих строк — работает.

    2. Обязательно ли использовать верхний регистр для обозначения имени (в данном случае ADD_H)? Пробовал в нижнем регистре — компилятор молчит.

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

    то программа заработала и все было правильно подсчитано. Так и должно быть?

    Спасибо!

    1. painkiller:

      Уточнение к вопросу №3:
      Да, не подключая header guard, заголовочные файлы тоже работают, в каком бы порядке я не вписывал туда прототипы функций. Видимо, я в прошлый раз не скомпилировал код 🙂

    2. Юрий:

      Привет.

      1. О header guards есть отдельный урок, где рассказывается зачем они нужны и как их использовать. Их можно и не использовать — дело ваше, работать будет и без них. А качество такой работы — уже отдельная тема.

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

  4. Константин:

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

    1. Юрий:

      В каких ситуациях и для чего используются header guards — есть отдельный урок 23.

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

  5. Максим():

    У меня такой интересный вопрос: если определение базовых команд реализуется в C++ Runtime Support Library, можно ли создать самому что-то подобное для своих функций?

    1. Юрий:

      Утверждать ничего не могу, с этим вопросом детально не знаком. Но Runtime Library поставляется вместе с компилятором и вашей IDE, для ваших функций и так уже используется этот встроенный Runtime Library, т.е. переписывать уже существующую библиотеку в Visual Studio — смысла не вижу. Если же вы хотите написать свою среду разработки, то да, можно переписать эту библиотеку. Но детальнее как это делается и более углубленно рассказать об этом вопросе я не могу, так как сам не знаю.

  6. Виталий:

    "Не #include файлы .cpp" — это на каком языке? Вся статья на таком сленге. Ну неудобно же читать, тем более тем, кто не мыслит шаблонами С++.
    Имхо.

    1. Li4ik:

      В статье ведь указывается, что такое include несколько раз: "Примечание: когда вы подключаете (#include) файл, всё его содержимое вставляется сразу после строчки include в текущий файл."

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

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

  7. Aleksandr:

    1. Для (например) трех функций f1, f2, f3 созданы три файла f1.cpp, f2.cpp, f3.cpp .
    2. В общем для них (отдельные для каждой заголовочные не создавались) заголовочном f.h прописаны объявления : f1(); f2(); f3(); .
    3. В файле stdafx дополнительно прописано #include "f.h".
    4. #include "stdafx" в самом начале — есть, #include "f.h". — не прописываем.
    Задача — упростить код(ихмо). При запуске небольшого кода — работает.
    А можно так делать ?

    1. Li4ik:

      Делать можно, но не во всех случаях. Есть два нюанса.

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

      Первый нюанс. Если у вас десятки файлов, в каждом подключен stdafx.h, в котором находится подключенный ваш собственный заголовок, который используется только в 2 файлов из 30, то смысла его засовывать в stdafx.h — нет. Если же почти все ваши файлы с кодом будут использовать этот заголовок, то в качестве упрощения кода можно внести этот заголовок в stdafx.h.

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

      1. Aleksandr:

        Спасибо за развёрнутый ответ и еще раз за ваш сайт.

      2. Александр:

        Ребят не по теме но все же, подскажите хорошую литературу по с++ в бумажном варианте, перед компьютером неудобно постоянно находиться, а с книжкой было бы сподручнее. Я в книжный зашел, а там сотня разнообразных книг, боюсь что возьму не очень полезную и сломаю себе голову (уже имел опыт в других сферах с некачественной литературой, из за которой больше проблем чем пользы), подскажите автора.
        Спасибо.

        1. Li4ik:

          Лично я не могу порекомендовать вам какую-либо литературу по C++, так как сам ничего не читал. Но вот постоянно сталкиваюсь с такими книгами на разных порталах о программировании и С++:

          1. Бьёрн Страуструп (создатель C++) — «Программирование. Принципы и практика использования C++».
          2. Герберт Шилдт (у него есть целые циклы книг о C++ и других языках программирования) — «С++: базовый курс».
          3.
          Стивен Прата«Язык программирования C++».

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

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

  8. Александр:

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

    1. Li4ik:

      Пожалуйста 🙂

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ПОДПИСЫВАЙТЕСЬ

НА КАНАЛ RAVESLI В TELEGRAM

@ravesli

ПОДПИСАТЬСЯ БЕСПЛАТНО