Руководство Google по стилю в C++. Часть 2
Часть 1. Вступление
Часть 2. Заголовочные файлы
Часть 3. Область видимости
…
Все мы при написании кода пользуемся правилами оформления кода. Иногда изобретаются свои правила, в других случаях используются готовые стайлгайды. Хотя все C++ программисты читают на английском легче, чем на родном, приятнее иметь руководство на последнем.
Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.
Заголовочные файлы
Желательно, чтобы каждый .cc файл исходного кода имел парный .h заголовочный файл. Также есть известные исключения из этого правила, такие как юниттесты или небольшие .cc файлы, содержащие только функцию main().
Правильное использование заголовочных файлов может оказать огромное влияние на читабельность, размер и производительность вашего кода.
Следующие правила позволят избежать частых проблем с заголовочными файлами.
Независимые заголовочные файлы
Заголовочные файлы должны быть самодостаточными (в плане компиляции) и иметь расширение .h. Другие файлы (не заголовочные), предназначенные для включения в код, должны быть с расширением .inc и использоваться в паре с включающим кодом.
Все заголовочные файлы должны быть самодостаточыми. Пользователи и инструменты разработки не должны зависеть от специальных зависимостей при использовании заголовочного файла. Заголовочный файл должен иметь блокировку от повторного включения и включать все необходимые файлы.
Предпочтительно размещать определения для шаблонов и inline-функций в одном файле с их декларациями. И эти определения должны быть включены (include) в каждый .cc файл, использующий их, иначе могут быть ошибки линковки на некоторых конфигурациях сборки. Если же декларации и определения находятся в разных файлах, включение одного должно подключать другой. Не выделяйте определения в отдельные заголовочные файлы (-inl.h). Раньше такая практика была очень популярна, сейчас это нежелательно.
Как исключение, если из шаблона создаются все доступные варианты шаблонных аргументов или если шаблон реализует функционал, используемый только одним классом — тогда допустимо определять шаблон в одном (и только одном) .cc файле, в котором этот шаблон и используется.
Возможны редкие ситуации, когда заголовочный файл не самодостаточный. Это может происходить, когда файл подключается в нестандартном месте, например в середине другого файла. В этом случае может отсутствовать блокировка от повторного включения, и дополнительные заголовочные файлы также могут не подключаться. Именуйте такие файлы расширением .inc. Используйте их парой и старайтесь чтобы они максимально соответствовали общим требованиям.
Блокировка от повторного включения
Все заголовочные файлы должны быть с защитой от повторного включения посредством #define. Формат макроопределения должен быть:<PROJECT>_<PATH>_<FILE>_H_.
Для гарантии уникальности, используйте компоненты полного пути к файлу в дереве проекта. Например, файл foo/src/bar/baz.h в проекте foo может иметь следующую блокировку:
Предварительное объявление
По возможности, не используйте предварительное объявление. Вместо этого делайте #include необходимых заголовочных файлов.
Определение
«Предварительное объявление» — декларация класса, функции, шаблона без соответствующего определения.
- Предварительной объявление может уменьшить время компиляции. Использование #include требует от компилятора сразу открывать (и обрабатывать) больше файлов.
- Предварительное объявление позволит избежать ненужной перекомпиляции. Применение #include может привести к частой перекомпиляции из-за различных изменений в заголовочных файлах.
- Старайтесь избегать предварительного объявления сущностей, объявленных в другом проекте.
- Когда используйте функцию, объявленную в заголовочном файле, всегда #include этот файл.
- Когда используйте шаблон класса, предпочтительно #include его заголовочный файл.
Встраиваемые (inline) функции
Определяйте функции как встраиваемые только когда они маленькие, например не более 10 строк.
Определение
Вы можете объявлять функции встраиваемыми и указать компилятору на возможность включать её напрямую в вызывающий код, помимо стандартного способа с вызовом функции.
Использование встраиваемых функций может генерировать более эффективный код, особенно когда функции маленькие. Используйте эту возможность для get/set функций, других коротких и критичных для производительности функций.
Чрезмерное использование встраиваемых функций может сделать программу медленнее. Также встраиваемые функции, в зависимости от размера её, могут как увеличить, так и уменьшить размер кода. Если это маленькие функции, то код может быть уменьшен. Если же функция большая, то размер кода может очень сильно вырасти. Учтите, что на современных процессорах более компактный код выполняется быстрее благодаря лучшему использованию кэша инструкций.
Хорошим правилом будет не делать функции встраиваемыми, если они превышают 10 строк кода. Избегайте делать встраиваемыми деструкторы, т.к. они неявно могут содержать много дополнительного кода: вызовы деструкторов переменных и базовых классов!
Ещё одно хорошее правило: обычно нет смысла делать встраиваемыми функции, в которых есть циклы или операции switch (кроме вырожденных случаев, когда цикл или другие операторы никогда не выполняются).
Важно понимать, что встраиваемая функция не обязательно будет скомпилирована в код именно так. Например, обычно виртуальные и рекурсивные функции компилируются со стандартным вызовом. Вообще, рекурсивные функции не должны объявляться встраиваемыми. Основная же причина делать встраиваемые виртуальные функции — разместить определение (код) в самом определении класса (для документирования поведения или удобства чтения) — часто используется для get/set функций.
Имена и Порядок включения (include)
Вставляйте заголовочные файлы в следующем порядке: парный файл (например, foo.h — foo.cc), системные файлы C, стандартная библиотека C++, другие библиотеки, файлы вашего проекта.
Все заголовочные файлы проекта должны указываться относительно директории исходных файлов проекта без использования таких UNIX псевдонимов как . (текущая директория) или .. (родительская директория). Например, google-awesome-project/src/base/logging.h должен включаться так:
Другой пример: если основная функция файлов dir/foo.cc иdir/foo_test.cc это реализация и тестирование кода, объявленного в dir2/foo2.h, то записывайте заголовочные файлы в следующем порядке:
- dir2/foo2.h.
- — Пустая строка
- Системные заголовочные файлы C (точнее: файлы с включением угловыми скобками с расширением .h), например <unistd.h>, <stdlib.h>.
- — Пустая строка
- Заголовочные файлы стандартной библиотеки C++ (без расширения в файлах), например <algorithm>, <cstddef>.
- — Пустая строка
- Заголовочные .h файлы других библиотек.
- Файлы .h вашего проекта.
Такой порядок файлов позволяет выявить ошибки, когда в парном заголовочном файле (dir2/foo2.h) пропущены необходимые заголовочные файлы (системные и др.) и сборка соответствующих файлов dir/foo.cc или dir/foo_test.cc завершится ошибкой. Как результат, ошибка сразу же появится у разработчика, работающего с этими файлами (а не у другой команды, которая только использует внешнюю библиотеку).
Обычно парные файлы dir/foo.cc и dir2/foo2.h находятся в одной директории (например, base/basictypes_test.cc и base/basictypes.h), хотя это не обязательно.
Учтите, что заголовочные файлы C, такие как stddef.h обычно взаимозаменяемы соответствующими файлами C++ (cstddef). Можно использовать любой вариант, но лучше следовать стилю существующего кода.
Внутри каждой секции заголовочные файлы лучше всего перечислять в алфавитном порядке. Учтите, что ранее написанный код может не следовать этому правилу. По возможности (например, при исправлениях в файле), исправляйте порядок файлов на правильный.
Следует включать все заголовочные файлы, которые объявляют требуемые вам типы, за исключением случаев предварительного объявления. Если ваш код использует типы из bar.h, не полагайтесь на то, что другой файл foo.h включает bar.h и вы можете ограничиться включением только foo.h: включайте явно bar.h (кроме случаев, когда явно указано (возможно, в документации), что foo.h также выдаст вам типы из bar.h).
Например, список заголовочных файлов в google-awesome-project/src/foo/internal/fooserver.cc может выглядеть так:
Бывают случаи, когда требуется включение заголовочных файлов в зависимости от условий препроцессора (например, в зависимости от используемой ОС). Такое включение старайтесь делать как можно короче (локализованно) и размещать после других заголовочных файлов. Например:
Многофайловые программы
Запуск gcc позволяет обработать файл с исходным кодом препроцессором и далее скомпилировать его. Однако при этом сам инструмент gcc не компилирует файл исходного кода в конечный исполняемый файл. Он компилирует его в объектный файл, после чего вызывает так называемый линковщик, или компоновщик. Но зачем надо сначала получать объектный файл, а потом из него уже исполняемый? Для программ, состоящих из одного файла, такой необходимости нет. Хотя при желании здесь также можно отказаться от компоновки, если выполнить команду gcc с ключом -c:
В результате получится файл с расширением *.o. Чтобы получить из объектного файла исполняемый, надо использовать ключ -o:
Для программ, состоящих из нескольких файлов исходного кода, получение объектных файлов является необходимым. Именно из них потом компонуется единственный исполняемый файл.
Компиляция программы, состоящей из нескольких файлов исходного кода
Рассмотрим пример. Пусть в одном файле определена пара функций, а в другом, содержащем функцию main() , осуществляется их вызов.
В теле функции main() заполняется массив, состоящий из строк, а также массив указателей на эти строки. Далее в функции l2r() и r2l() передаются ссылки на первый элемент массива указателей и значение символической константы N. Эти функции осуществляют вывод элементов массива строк с отступами.
Чтобы получить исполняемый файл этой программы, надо сначала получить объектные файлы из исходных:
Тоже самое можно сделать за один вызов gcc:
Или даже вот так, если в каталоге находятся только файлы текущего проекта:
В любом случае в каталоге появятся два объектных файла: superprint.o и main.o. Далее их можно скомпилировать в один исполняемый файл так:
Обратите внимание на то, что в данном случае обязательно требуется указывать имя исполняемого файла. Такой вариант недопустим:
Если теперь запустить файл myprint, то программа будет ожидать ввода пяти слов, после чего выведет их на экран два раза по-разному (с помощью функций l2r() и r2l() ):
Задумаемся, каким образом в представленной выше программе функция main() «узнает» о существовании функций l2r() и r2l() . Ведь в исходном коде файла main.c нигде не указано, что мы подключаем файл superprint.c, содержащий эти функции. Действительно, если попытаться получить из main.c отдельный исполняемый файл, т.е. скомпилировать программу без superprint.c:
, то ничего не получиться. Компилятор сообщит об ошибке вызова неопределенных идентификаторов. Получить из файла superprint.c отдельный исполняемый файл вообще невозможно, т.к. там отсутствует функция main() . А вот получить из этих файлов отдельные объектные файлы можно. Представим, что одни объектные файлы как бы «выставляют наружу» имена определенных в них функций и глобальных переменных, а другие — вызовы этих имен из тел других функций. Дальше объектные файлы «ожидают», что имена будут связаны с их вызовами. Связывание происходит при компиляции исполняемого файла из объектных.
Создание заголовочных файлов
Продолжим разбирать приведенную выше программу. Что будет, если в функции main() осуществить неправильный вызов функций l2r() и r2l() ? Например, указать неверное количество параметров. В таком случае создание объектных файлов пройдет без ошибок, и скорее всего удастся получить исполняемый файл; но вот работать программа будет неправильно. Такое возможно потому, что ничего не контролирует соответствие вызовов прототипам (объявлениям) функций.
Куда правильней сообщать о неверном вызове функций уже при получении объектного файла. Поэтому хотя можно обойтись и без этого, но очень желательно сообщать функции main() прототипы функций, которые из нее вызываются. Это можно сделать, прописав объявления функций в файле main.c:
Теперь, если мы передадим неправильные параметры, ошибка возникнет уже на этапе получения объектных файлов.
А теперь представим, что программа у нас несколько больше и содержит десяток файлов исходного кода. Файл aa.c требует функций из файла bb.c, dd.c, ee.c. В свою очередь dd.c вызывает функции из ee.c и ff.c, а эти два последних файла активно пользуются неким файлом stars.c и одной из функций в bb.c. Программист замучится сверять, что чего вызывает откуда и куда, где и какие объявления надо прописывать. Поэтому все прототипы (объявления) функций проекта, а также совместно используемые символические константы и макросы выносят в отдельный файл, который подключают к каждому файлу исходного кода. Такие файлы называются заголовочными; с ними мы уже не раз встречались. В отличие от заголовочных файлов стандартной библиотеки, заголовочные файлы, которые относятся только к вашему проекту, при подключении к файлу исходного кода заключаются в кавычки, а не скобки. Об этом упоминалось в предыдущем уроке.
Итак, более грамотно будет не добавлять объявления функций в файл main.c, а создать заголовочный файл, например, myprint.h и поместить туда прототипы функций l2r() и r2l() . А в файле main.c следует прописать директиву препроцессора:
В принципе смысла подключать myprint.h к файлу superprint.c в данном случае нет, т.к. последний не использует никаких сторонних функций, кроме стандартной библиотеки. Но если планируется расширять программу и есть вероятность, что в файле superprint.c будут вызываться сторонние для него функции, то будет надежней сразу подключить заголовочный файл.
Обратим внимание еще на один момент. Стоит ли в описанном в этом уроке примере выносить константу N в заголовочный файл? Здесь нельзя дать однозначный ответ. Если ее туда вынести, то она станет доступна в обоих файлах, и поэтому можно изменить прототипы функций так, чтобы они принимали только один параметр (указатель), а значение N будет известно функциям их заголовочного файла. Однако стоит ли так делать? В функции r2l() второй параметр изменяется в процессе ее выполнения, что делать с константой будет невозможно. Придется переписывать тело функции. Кроме того, вдруг в последствии нам захочется использовать файл superprint.c в другом проекте, где будут свои порядки, и константы N в заголовочном файле не найдется.
В таком случае лучше N не выносить в заголовочный файл. Хотя имейте в виду, в каких-то проектах символическая константа может использоваться так часто и во множестве функций, что ее будет лучше поместить в заголовочный файл.
Особенности использования глобальных переменных
Помните, если можно отказаться от использования глобальных переменных, то лучше это сделать. Желательно стремиться к тому, чтобы любой файл проекта, скажем так, «не лез к соседу за данными, а сосед не разбрасывал эти данные в виде глобальных переменных». Обмен данными между функциями должен осуществлять за счет передачи данных в качестве параметров и возврата значений с помощью оператора return . (Массивов это не касается.)
Однако в языке программирования C есть проблема. С помощью return можно вернуть только одно значение. Но могут быть случаи, когда функция должна изменить несколько переменных (здесь не имеются ввиду элементы массива). В таком случае без глобальных переменных обойтись сложно.
Вынести функцию в отдельный файл C/C++
Для того чтобы вынести функцию в отдельный файл нам понадобится разбить нашу программу на несколько частей.
Обычно это делают так:
- помещаются в заголовочный файл (.h)
- Реализации (определения) функций помещаются в отдельный .cpp файл
- В файле (с main()), где вызываются функции, подключается файл с прототипами (.h)
Теперь разберем как это сделать на примере.
Допустим у нас есть программа в которой мы запрашиваем X, а на экран должно вывестись X²
#include <stdio.h>
#include <conio.h>
#include <locale.h>
int x;
int main() <
setlocale(0, "Russian");
printf("Введите X\n");
scanf_s("%d", &x); //тут будем вызывать нашу функцию _getch();
return 0;
>
1. Заголовочный файл (.h)
Создаем заголовочный файл и помещаем туда прототип нашей функции:
Обязательно используем конструкцию для защиты от множественных включений в один файл:
#ifndef Header_h
#define Header_h
//Код
#endif //Header
#ifndef Header_h
#define Header_h void func(); //прототип функции #endif //Header
2. Реализация функции (.c/.cpp)
Создаем отдельный .cpp файл (function.cpp):
2.10 – Заголовочные файлы
По мере того, как программы становятся больше (и используют больше файлов), становится всё более утомительным давать предварительные объявления каждой функции, которую вы хотите использовать, и которая определена в другом файле. Было бы неплохо, если бы вы могли поместить все свои предварительные объявления в одно место, а затем импортировать их, когда они вам понадобятся?
Исходные файлы кода C++ (с расширением .cpp ) – это не единственные файлы, которые обычно встречаются в программах на C++. Другой тип файлов – это заголовочный файл (иногда просто заголовок). Заголовочные файлы обычно имеют расширение .h , но иногда вы можете встретить их с расширением .hpp или вообще без расширения. Основная цель заголовочного файла – распространять объявления в исходные файлы кода.
Ключевой момент
Заголовочные файлы позволяют нам размещать объявления в одном месте, а затем импортировать их туда, где они нам нужны. Это может сэкономить много времени при наборе текста в проектах из нескольких файлов.
Использование заголовочных файлов стандартной библиотеки
Рассмотрим следующую программу:
Эта программа печатает « Hello, world! » в консоль с помощью std::cout . Однако эта программа никогда не предоставляла определение или объявление для std::cout , поэтому как компилятор узнает, что такое std::cout ?
Ответ заключается в том, что std::cout был предварительно объявлен в заголовочном файле « iostream ». Когда мы пишем #include <iostream> , мы запрашиваем, чтобы препроцессор скопировал всё содержимое (включая предварительные объявления для std::cout ) из файла с именем « iostream » в файл, выполняющий #include .
Ключевой момент
Когда вы включаете файл с помощью #include , содержимое включаемого файла вставляется в точке включения. Это удобный способ извлечения объявлений из другого файла.
Подумайте, что бы произошло, если бы заголовок iostream не существовал. Каждый раз, когда вы хотели бы использовать std::cout , вам приходилось бы вручную вводить или копировать все объявления, связанные с std::cout , в начало каждого файла, который использовал бы std::cout ! Для этого потребуется много знаний о том, как реализован std::cout , и потребуется много работы. Хуже того, если бы прототип функции изменился, нам пришлось бы вручную обновлять все предварительные объявления. Намного проще просто включить iostream с помощью #include !
Когда дело доходит до функций и переменных, стоит помнить, что заголовочные файлы обычно содержат только объявления функций и переменных, а не их определения (в противном случае может произойти нарушение правила одного определения). std::cout объявлен в заголовке iostream, но определен как часть стандартной библиотеки C++, которая автоматически подключается к вашей программе на этапе линкера.
Рисунок 1 – Диаграмма процесса сборки
Лучшая практика
Заголовочные файлы обычно не должны содержать определений функций и переменных, чтобы не нарушать правило одного определения. Исключение сделано для символьных констант (которые мы рассмотрим в уроке «4.14 – const, constexpr и символьные константы»).
Написание собственных заголовочных файлов
А теперь вернемся к примеру, который мы обсуждали в предыдущем уроке. Когда мы закончили, у нас было два файла, add.cpp и main.cpp , которые выглядели так:
(Если вы воссоздаете этот пример с нуля, не забудьте добавить add.cpp в свой проект, чтобы он компилировался).
В этом примере мы использовали предварительное объявление, чтобы при компиляции main.cpp компилятор знал, что такое идентификатор add . Как упоминалось ранее, добавление предварительных объявлений для каждой функции, которую вы хотите использовать, и которая находится в другом файле, вручную может быстро стать утомительным.
Давайте напишем заголовочный файл, чтобы избавиться от этого бремени. Написать заголовочный файл на удивление легко, поскольку файлы заголовков состоят только из двух частей:
- защита заголовка, о которой мы поговорим более подробно в следующем уроке («2.11 – Защита заголовков»);
- фактическое содержимое файла заголовка, которое должно быть предварительными объявлениями для всех идентификаторов, которые мы хотим, чтобы другие файлы могли видеть.
Добавление заголовочного файла в проект работает аналогично добавлению исходного файла (рассматривается в уроке «2.7 – Программы с несколькими файлами исходного кода»). Если вы используете IDE, выполните такие же действия и при появлении запроса выберите Файл заголовка (или C/C++ header) вместо Файла С++ (или C/C++ source). Если вы используете командную строку, просто создайте новый файл в своем любимом редакторе.
Лучшая практика
При именовании файлов заголовков используйте расширение .h .
Заголовочные файлы часто идут в паре с файлами исходного кода, при этом заголовочный файл предоставляет предварительные объявления для соответствующего исходного файла. Поскольку наш заголовочный файл будет содержать предварительное объявление для функций, определенных в add.cpp , мы назовем наш новый заголовочный файл add.h .
Лучшая практика
Если заголовочный файл идет в паре с файлом исходного кода (например, add.h с add.cpp ), они оба должны иметь одинаковое базовое имя ( add ).
Вот наш завершенный заголовочный файл:
Чтобы использовать этот заголовочный файл в main.cpp , мы должны включить его с помощью #include (используя кавычки, а не угловые скобки).
Когда препроцессор обрабатывает строку #include "add.h" , он копирует содержимое add.h в текущий файл в эту точку. Поскольку наш add.h содержит предварительное объявление для функции add , это предварительное объявление будет скопировано в main.cpp . Конечным результатом является программа, которая функционально аналогична той, в которой мы вручную добавили предварительное объявление вверху main.cpp .
Следовательно, наша программа будет правильно компилироваться и компоноваться.
Рисунок 2 – Диаграмма процесса сборки
Включение заголовочного файла в соответствующий исходный файл
Позже вы увидите, что большинство исходных файлов включают свой соответствующий заголовочный файл, даже если он им не нужен. Зачем?
Включение заголовочного файла в исходный файл увеличивает прямую совместимость. Очень вероятно, что в будущем вы добавите больше функций или измените существующие таким образом, что им нужно будет знать о существовании друг друга.
Когда мы углубимся в изучение стандартной библиотеки, вы будете включать множество заголовочных файлов библиотек. Если вам потребовалось включение в заголовочном файле, оно, вероятно, понадобилось вам для объявления функции. Это означает, что вам также потребуется такое же включение в исходный файл. Это приведет к тому, что в исходном файле у вас будет копия включений заголовочного файла. Включив заголовочный файл в исходный файл, исходный файл получит доступ ко всему, к чему имел доступ заголовочный файл.
При разработке библиотеки включение заголовочного файла в исходный файл может даже помочь в раннем обнаружении ошибок.
Лучшая практика
При написании исходного файла включите в него соответствующий заголовочный файл (если он существует), даже если он вам пока не нужен.
Поиск и устранение проблем
Если вы получаете ошибку компилятора, указывающую, что add.h не найден, убедитесь, что файл действительно называется add.h . В зависимости от того, как вы его создали и назвали, возможно, файл может иметь имя вроде add (без расширения), add.h.txt или add.hpp . Также убедитесь, что он находится в том же каталоге, что и остальные исходные файлы.
Если вы получаете сообщение об ошибке компоновщика о том, что добавление функции не определено, убедитесь, что вы добавили в проект файл add.cpp , чтобы определение для функции add можно было слинковать в программе.
Угловые скобки и двойные кавычки
Вам, наверное, интересно, почему мы используем угловые скобки для iostream и двойные кавычки для add.h . Возможно, что заголовочные файлы с таким же именем могут существовать в нескольких каталогах. Использование угловых скобок и двойных кавычек помогает компилятору понять, где ему следует искать заголовочные файлы.
Когда мы используем угловые скобки, мы сообщаем препроцессору, что это заголовочный файл, который мы не писали сами. Компилятор будет искать заголовок только в каталогах, указанных в каталогах включаемых файлов (include directories). Каталоги включаемых файлов настраиваются как часть вашего проекта / настроек IDE / настроек компилятора и обычно по умолчанию используются для каталогов, содержащих заголовочные файлы, которые поставляются с вашим компилятором и/или ОС. Компилятор не будет искать заголовочный файл в каталоге исходного кода вашего проекта.
Когда мы используем двойные кавычки, мы сообщаем препроцессору, что это заголовочный файл, который написали мы. Компилятор сначала будет искать этот заголовочный файл в текущем каталоге. Если он не сможет найти там подходящий заголовочный файл, он будет искать его в каталогах включаемых файлов.
Правило
Используйте двойные кавычки, чтобы включать заголовочные файлы, которые написали вы или которые, как ожидается, будут найдены в текущем каталоге. Угловые скобки используйте, чтобы включать заголовочные файлы, которые поставляются с вашим компилятором, ОС или сторонними библиотеками, которые вы установили в другом месте своей системы.
Почему у iostream нет расширения .h ?
Другой часто задаваемый вопрос: «Почему iostream (или любой другой заголовочный файл стандартной библиотеки) не имеет расширения .h ?». Ответ заключается в том, что iostream.h – это другой заголовочный файл, отличающийся от iostream ! Для объяснения требуется небольшой урок истории.
Когда C++ был только создан, все файлы в стандартной библиотеке оканчивались расширением .h . Жизнь была последовательной, и это было хорошо. Исходные версии cout и cin были объявлены в iostream.h . Когда комитет ANSI стандартизировал язык, они решили переместить все функции стандартной библиотеки в пространство имен std , чтобы избежать конфликтов имен с пользовательскими идентификаторами. Однако это представляло проблему: если бы они переместили всю функциональность в пространство имен std , ни одна из старых программ (включая iostream.h ) больше не работала бы!
Чтобы обойти эту проблему, был представлен новый набор заголовочных файлов, которые используют те же имена, но не имеют расширения .h . Все функции в этих новых заголовочных файлах находятся в пространстве имен std . Таким образом, старые программы, содержащие #include <iostream.h> , не нужно переписывать, а новые программы могут использовать #include <iostream> .
Кроме того, многие библиотеки, унаследованные от C, которые всё еще используются в C++, получили префикс c (например, stdlib.h стал cstdlib ). Функциональные возможности этих библиотек также были перенесены в пространство имен std , чтобы избежать конфликтов имен.
Лучшая практика
При включении заголовочного файла из стандартной библиотеки используйте версию без расширения (без .h ), если она существует. Пользовательские заголовочные файлы по-прежнему должны использовать расширение .h .
Включение заголовочных файлов из других каталогов
Другой распространенный вопрос связан с тем, как включать заголовочные файлы из других каталогов.
Один (плохой) способ сделать это – добавить относительный путь к заголовочному файлу, который вы хотите включить как часть строки #include . Например:
Хотя это будет компилироваться (при условии, что файлы существуют в этих относительных каталогах), обратная сторона этого подхода состоит в том, что он требует от вас отражения структуры каталогов в вашем коде. Если вы когда-нибудь обновите структуру каталогов, ваш код больше не будет работать.
Лучший способ – сообщить вашему компилятору или 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, а не каждый файл кода.
Заголовочные файлы могут включать другие заголовочные файлы
Обычно для заголовочных файлов требуется объявление или определение, которое находится в другом заголовочном файле. Из-за этого заголовочные файлы часто включают с помощью #include другие заголовочные файлы.
Когда ваш исходный файл включает с помощью #include первый заголовочный файл, вы также получите любые другие заголовочные файлы, которые были включены в первый заголовочный файл (и любые заголовочные файлы, которые были включены в предыдущие, и т.д.). Эти дополнительные заголовочные файлы иногда называют «транзитивными включениями», поскольку они включаются неявно.
Содержимое этих транзитивных включений доступно для использования в вашем файле исходного кода. Однако не следует полагаться на содержимое заголовков, которые включены транзитивно. Реализация заголовочных файлов может со временем меняться или отличаться в разных системах. Если это произойдет, ваш код может компилироваться только на определенных системах или может компилироваться сейчас, но перестать в будущем. Этого легко избежать, явно включив все заголовочные файлы, необходимые для содержимого вашего файла исходного кода.
Лучшая практика
Каждый файл должен явно включать с #include все заголовочные файлы, необходимые для компиляции. Не полагайтесь на заголовочные файлы, включенные транзитивно из других заголовков.
К сожалению, нет простого способа определить, полагается ли ваш файл кода случайно на содержимое заголовочного файла, который был включен другим заголовочным файлом.
Вопрос: Я не включил <someheader.h> , и моя программа всё равно работала! Почему?
Это один из наиболее часто задаваемых вопросов. Ответ: скорее всего, он работает, потому что вы включили какой-то другой заголовок (например, <iostream> ), который сам включает <someheader.h> . Несмотря на то, что ваша программа будет компилироваться, в соответствии с приведенными выше рекомендациями вам не следует полагаться на это. То, что компилируется у вас, может не компилироваться на машине друга.
Порядок #include заголовочных файлов
Если ваши заголовочные файлы написаны правильно и включают с #include всё, что им нужно, порядок включения не имеет значения. Однако включение заголовочных файлов в определенном порядке может помочь выявить ошибки, когда заголовочные файлы могут не включать в себя всё, что им нужно.
Лучшая практика
Упорядочьте свои включения с #include следующим образом: сначала ваши собственные пользовательские заголовки, затем заголовки сторонних библиотек, затем заголовки стандартных библиотек; заголовки в каждом разделе должны быть отсортированы в алфавитном порядке.
Таким образом, если в одном из ваших пользовательских заголовков отсутствует #include для заголовка сторонней библиотеки или стандартной библиотеки, это с большей вероятностью вызовет ошибку компиляции, чтобы вы могли ее исправить.
Рекомендации по использованию заголовочных файлов
Вот еще несколько рекомендаций по созданию и использованию заголовочных файлов.