В большинстве больших проектов (программ) возникает множество ситуаций, когда программисту требуется делать большое количество предварительных объявлений функций и переменных.
Часто функции определены в других файлах. Постоянно предварительно объявлять их становится утомительно. К тому же это может повлечь за собой множество ошибок в процессе написания кода.
Процесс предварительного объявления упрощается с помощью использования заголовочных файлов, именно в них помещаются все предварительные объявления для функций и по необходимости они вызываются с помощью директивы #include
В этой статье мы создадим программу, используя знания из прошлых статей и в конце воспользуемся собственным заголовочным файлом для предварительных объявлений функций.
Знакомство с заголовками на примере функций из стандартной библиотеки
Каждый раз, когда мы используем средства ввода с клавиатуры или вывода данных в консоль (std::cin/std::cout), мы обращаемся к заголовочному файлу iostream. Делаем мы это так: #include <iostream>
Выполнение этой директивы заставляет препроцессор вставить на место директивы все, что содержится в заголовочном файле <iostream>, а там содержатся предварительные объявления для всех функций, обеспечивающих работу ввода/вывода — cin/cout
А теперь представьте, что вам нужно было бы каждый раз писать это все вручную. Чтобы понять масштаб, попробуйте (Если работаете в IDE Visual Studio) нажать правой кнопкой мыши на строчку <iostream> и далее выбрать пункт: Перейти к документу iostream
Нажмите на этот пункт и перед вами откроется заголовочный файл на 70+ строк кода.
И вот представьте, каждый раз, когда вы желаете получить доступ к вводу/выводу данных, вам необходимо написать все эти строки. Устрашающе выглядит, не правда ли?
Вот так и выходит с предварительными объявлениями пользовательских функций в больших проектах. Их становится чрезвычайно много. Постоянно держать их в голове и точно знать, какая функция за что отвечает становится сложно или вообще не представляется возможным. Проще поместить их все в один или несколько заголовочных файлов, которые потом можно будет вызвать по требованию.
Использование заголовочных файлов на примере небольшой программы
Предлагаю вместе создать программу, которая будет выполнять несколько действий в консоли и на примере этой программы я наглядно покажу вам, как пользоваться заголовочными файлами.
Хм, что же создадим?
А давайте сделаем программу, которая будет:
1. Здороваться с пользователем и запрашивать два целых числа у него.
2. Принимать с консоли целые числа.
3. Выводить в консоль четыре строки, на каждой из которых будет складывать эти числа, отнимать друг от друга, умножать и делить друг на друга.
Программу будем делать в виде нескольких блоков. Здесь нам понадобятся знания, которые вы вместе со мной получили в прошлых статьях. Если вы не прочитали все мои статьи, не обижайтесь, что вам что-то непонятно (однако, на ваши вопросы в комментариях я с удовольствием отвечу).
Для начала создадим новый проект внутри нашей IDE и напишем в открывшейся странице редактора следующее:
Пока мы не будем заниматься файлом с главной функций. К нему мы вернемся, когда опишем в виде кода все блоки, которые будут решать наши задачи.
Создадим новый файл .cpp в котором опишем функцию, которая будет здороваться с пользователем и запрашивать у него два целых числа.
Первый этап выполнен, отлично.
Теперь нам нужно получить эти числа от пользователя. Для этого создадим еще один отдельный файл .cpp и напишем там функцию, которая будет забирать числа и сохранять их в две переменные, которые мы заранее инициализируем.
Вот так. Теперь у нас есть три файла, их можно увидеть во вкладке исходные файлы. В одном из них наша главная функция, в которую мы потом вызовем функции из других файлов. А в двух других профильные функции, каждая из которых выполняет свое действие.
Так, теперь появилась задачка по сложнее. Нам нужно создать новый файл, в котором будет находиться функция, выводящая на экран арифметические действия с нашими числами. Но нам надо как-то в эту функцию передать значения, полученные от пользователя.
Давайте попробуем?
На этом скрине и в предыдущем скрине с функцией input я забыл написать в конце всех инструкций, инструкцию возврата значения return 0; уже на следующем скрине я исправился, исправьтесь и вы, а то компилятор будет ругаться.
Окей, мы создали файл, в котором описали четыре инструкции для печати всех четырех выражений и комментариев в виде текста к ним. Но, как мы видим, наша IDE не видит переменную num и num_1, которые были определены в другом файле для хранения значений, полученных от пользователя.
Если мы предварительно объявим функцию input, в которой они и были определены, нам это не поможет. Нам нужно именно получить значения из функции input. Воспользуемся для этого фишкой с аргументами и параметрами функций. Если забыли, здесь можно повторить.
Итак, мы добавили функции print два целочисленных параметра, x и y. Присмотритесь, я использовал параметры и в std::cout, в части выражения где выполняются арифметические действия. Теперь, если мы вызовем эту функцию, используя аргументы вызывающей функции, то параметры получат значения от аргументов. Откуда будем вызывать?
Смотрите:
Вызывать функцию print мы будем из файла, в котором хранится функция input, получающая числа от пользователя. Заметьте, что мы предварительно объявили print в строке 3 и вызываем ее внутри input с аргументами в виде переменных num и num_1
Про предварительное объявление читайте тут, если вдруг забыли тему
Теперь, когда print будет вызвана, она автоматически начнет выполнения кода внутри нее, предварительно получив в свои параметры, аргументы в виде двух значений, полученных от пользователя.
Фух!
Теперь надо все это объединить! Чтобы все заработало. А это значит мы возвращаемся в файл с главной функцией main и начинаем писать аж целых три предварительных объявления и два вызова для функций.
Смотрите, я предварительно объявил все функции из разных файлов и вызвал функцию hello и input внутри main. Функцию print вызывать не надо, так как ее вызывает функция input.
Теперь компилируем и запускаем...
Ура, наша программа успешно отработала и завершилась.
А теперь, когда мы повторили весь материал, перейдем к заголовкам. Это упростит нашу программу в части, где перед главной функцией main мы предварительно объявляем другие функции.
Для начала создадим заголовочный файл в папке "Файлы заголовков". Это делается так же, как вы создавали исходные файлы .cpp, только заголовки хранятся в другой папке и формат у них .h
Вот так, я создал заголовочный файл fnc и поместил все предварительные объявления функций туда, теперь если я вызову его с помощью #include препроцессор автоматически подставил вместо директивы все предварительные объявления из заголовочного файла. Напишем эту директиву в файле с главной функцией, а так же в файле input, там мы предварительно объявляли функцию print
Обратите внимание, что форма записи при включении пользовательского заголовка отличается от включения системного <iostream> и выглядит так "name.h"
Вот так. Логически становится ясно, что в масштабах реальной программы с кучей блоков и файлов, такое включение заголовка существенно сокращает количество строк кода, которые будут отводиться для предварительных объявлений. Но...
Это еще не все!
Заголовки, в которых есть заголовки
Да, так тоже бывает. Часто для заголовков требуются объявления или определения, находящиеся в других заголовочных файлов. Чтобы не плодить примеров, наглядно это можно увидеть, используя пример заголовочного файла iostream, объявляющего все необходимые функции для ввода и вывода данных.
Откройте его снова, мы это уже делали в начале статьи. Там вы увидите, что заголовочный файл iostream включает заголовок объявляющий функции необходимые для корректной работы ввода данных istream
Вот он, на строке 11:
Такие включения называют "транзитивными". Это связано с тем, что их включение "неявное". Некоторые пособие или школы программирования будут советовать вам использовать только явные включения. То есть вы вполне встретите тезисы, которые скажут:
Включи istream, если хочешь получить данные с консоли. А если еще и хочешь вывести, включи отдельно ostream.
Прививая такую практику, они будут в чем-то правы. Дело в том, что реализация некоторых заголовочных файлов может меняться со временем. И уже "завтра" (условно завтра), запустивший ваш код другой программист столкнется с тем, что он вдруг перестал работать, потому что в заголовке, который вы подключили теперь нет включения, которое там раньше было.
Вот такая вот ерунда случается, да.
В случае с пользовательскими заголовками я бы тоже советовал вам делать все объявления явными. И под каждую группу объявлений создавать свой заголовочный файл. Так вы создадите меньше путаницы для ваших коллег, которые потом будут читать ваш код и заниматься его рефакторингом.
В заключение оставлю тут рекомендации по созданию и обслуживанию заголовочных файлов.
Рекомендации по созданию, заполнению и обслуживанию заголовочных файлов
- Всегда используйте защиту заголовочных файлов — это будет темой следующей статьи и вашим стимулом прийти и прочитать. А до этого можете нагуглить, что за защита заголовков и что она делает.
- Не надо определять переменные и функции внутри заголовков. Используйте заголовочные файлы только для предварительных объявлений. У этого правила есть исключение — глобальные константы, о них мы поговорим позже.
- Именуйте заголовочные файлы так же, как файлы .cpp переменные и функции из которых они объявляют (да, в моем примере, я этого не сделал, но вы не повторяйте за мной эту ошибку). Учитесь на моем плохом примере не делать так.
- Включайте с помощью #include только необходимые вам заголовочные файлы. Не надо включать все подряд, с фразой: "Потому что могу!"
- Не включайте через #include файлы с расширением .cpp
На этом сегодня все. Спасибо за внимание.
В следующей статье мы разберем крайне важную темы "Защита заголовочных файлов".
Если вам понравился материал, ставьте лайки.
Оставляйте комментарии с пожеланиями и вопросами.
Подписывайтесь, чтобы не пропустить выход новых материалов.