Практический курс по C/C++: https://stepik.org/course/193691
Пришло время ближе познакомиться с директивой #include, которую мы
прописывали с самых первых программ. Что она делает, я также кратко уже
озвучивал и говорил, что как только макропроцессор встречает эту директиву, то
вставляет содержимое указанного файла на ее место. В частности, в программе:
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
после обработки
текстовым препроцессором вместо первой строчки будет подставлено содержимое
файла stdio.h. В данном
примере это нужно, чтобы мы могли использовать библиотечную функцию printf() в своей
программе. Но в самом файле stdio.h нет реализации
этой функции, а только ее описание (прототип) в виде:
int
printf(const char* format, ...);
Кстати, если
вместо директивы прописать в явном виде сигнатуру функции printf():
int printf(const char* format, ...);
int main(void)
{
printf("Hello, World!\n");
return 0;
}
то программа
также откомпилируется и запустится. Однако делать в реальных проектах так не
стоит, т.к. детали описания функций могут несколько меняться.
В целом, этого
понимания работы директивы #include вполне достаточно для
программирования на Си. Отмечу только один нюанс. Имя файла после директивы #include можно заключать
или в угловых скобках, или в кавычках. Например, если ее записать в виде:
то ничего не
изменится и программа также успешно откомпилируется. Но отличия в работе
директивы все же есть. Согласно стандарту C99 файл
прописанный в угловых скобках сначала ищется в системных каталогах, и если не
будет найден, то в текущем рабочем каталоге проекта. Если же прописываются
двойные кавычки, то поиск работает наоборот, сначала файл ищется в рабочем
каталоге проекта, а затем только в системных каталогах.
В
действительности, эти правила не всегда соблюдаются и компиляторы могут
реализовывать свою логику поиска файлов. Мало того, в последних стандартах
языка Си алгоритм поиска файлов несколько изменен и стал таким неопределенным,
что разработчики компиляторов этот пункт часто просто игнорируют. Благо, нам,
как программистам на Си важно лишь, что макропроцессор будет искать указанный
файл и добавлять его содержимое в наш проект. А правило использования угловых
скобок и кавычек в сообществе разработчиков стало таким:
Если нужно
подключить стандартные файлы, поставляемые с компилятором языка Си, то следует
писать угловые скобки. Если же подключаются свои собственные заголовочные
файлы, то их имена следует заключать в двойные кавычки.
Например,
давайте в нашем проекте создадим подкаталог tmp и в нем
разместим файл с именем printf.h и содержимым:
int printf(const char *format, ...);
Затем, подключим
этот файл в программе следующим образом:
#include "tmp/printf.h"
int main(void)
{
printf("Hello, World!\n");
return 0;
}
Как видите, мы
использовали здесь двойные кавычки, т.к. файл printf.h наш собственный
и, кроме того, указали подкаталог tmp относительно рабочего каталога,
т.к. файл printf.h находится в
этом подкаталоге.
Директивы условной компиляции
Следующий набор
директив, который мы разберем, - это, так называемые, директивы условной компиляции:
#if,
#endif, #elif, #else, #ifdef, #ifndef, #elifdef, #elifndef
В основном они используются,
чтобы оставить или убрать определенный фрагмент текста программы в зависимости
от какого-либо условия. Например, пишется программа, которую предполагается
компилировать с использованием компилятора Си, а также с использованием
компилятора С++. Но в этих языках программирования имеются некоторые отличия в используемых
конструкциях и часть программного кода должна различаться в зависимости от языка.
Так вот, чтобы написать универсальный текст программы и иметь возможность
компилировать его как Си, так и С++, можно воспользоваться условными
директивами следующим образом:
#define LANG_C
#if defined(LANG_C)
# include <stdio.h>
#else
# include <iostream>
#endif
int main(void)
{
int x=5;
#ifdef LANG_C
printf("%d\n", x);
#else
std::cout << x << std::endl;
#endif
return 0;
}
Смотрите,
вначале определено макроимя LANG_C с помощью
директивы #define. Затем,
прописана директива #if, в которой проверяется условие:
определено ли макроимя LANG_C в текущем
модуле. Если это так (как в нашем примере), то макропроцессор оставляет в
программе все, что записано после этой директивы либо до следующей условной
директивы, либо до директивы #endif. В приведенном примере, остается
строчка «# include <stdio.h>» и
удаляется строка «# include <iostream>».
Соответственно, директива #include также, затем, обрабатывается
макропроцессором. В итоге, после обработки, у нас получается следующий текст программы:
# include <stdio.h>
int main(void)
{
int x=5;
printf("%d\n", x);
return 0;
}
Разумеется,
директива #include здесь также
впоследствии преобразуется макропроцессором. А директива #ifdef – это
сокращенный вариант записи конструкции #if defined.
По сути,
директивы условной компиляции #if, #else, #endif работают
подобно условным операторам if-else, о которых мы с
вами уже говорили. Но, конечно же, есть и отличия. Первый важный момент: в
условиях директив можно использовать исключительно целочисленные литералы и
макроимена. С этими элементами можно выполнять все булевы операции сравнения:
==, !=, <,
>, <=, >=
логические
связки:
&&, ||,
!
все бинарные арифметические
и битовые операции:
+, -, *, /, %,
&, |, ^
и применять
оператор defined, которые
возвращает 1, если указанное макроимя существует и 0 – в противном случае. Есть
еще несколько экзотических конструкций, вроде условной тернарной операции,
которые допустимо прописывать в условиях директив, но в основном используются
те операции, что перечислены выше. Обратите внимание, никаких переменных,
функций и прочих конструкций, значение которых определяется в процессе работы
программы, здесь применять нельзя.
Второй важный
момент. Директивы условной компиляции не образуют своих собственных внутренних
блоков. Поэтому для указания того, что попадает внутрь таких директив, в конце
обязательно следует прописать директиву #endif – метку,
означающую конец текущей директивы условной компиляции.
Третий важный
момент. Директивы препроцессора анализируют программу как текст (на уровне
лексем). Это означает, что они не учитывают области видимости: локальные,
глобальные и т.п. Поэтому все директивы принято записывать с самого начала
строки (с левого края). В частности, именно поэтому символ # у директив include записан на
одном уровне с другими директивами, т.к. никакого реального вложения здесь нет,
и это мы подчеркиваем оформлением. То же самое при записи директив внутри
функции main(). Для всех
этих директив функции не имеют никакого значения – это просто текст. Поэтому
все они прописаны с самого начала строки.
Использование директив условной компиляции в заголовочных файлах
Если мы
посмотрим на содержимое какого-либо стандартного заголовочного файла, например,
того же stdio.h, то вначале
увидим такие строчки:
#ifndef _INC_STDIO
#define _INC_STDIO
А ниже
обязательно будет записана директива:
С какой целью
эти директивы здесь используются? На самом деле это защита от повторного
включения содержимого заголовочного файла в текущий модуль. Например, если мы в
программе дважды напишем строчки:
#include <stdio.h>
#include <stdio.h>
То программа
скомпилируется и отработает без ошибок, так как содержимое файла stdio.h было добавлено
макропроцессором в текущий модуль только один раз. Почему так произошло?
Смотрите. Когда файл stdio.h подключался
первый раз, то макроимя _INC_STDIO отсутствовало и
условие директивы #ifndef оказалось истинным, так как #ifndef – это аналог #if !defined. Раз условие
истинно, то все, что определено до директивы #endif включается в
файл, в том числе и директива «#define _INC_STDIO», которая
определяет макроимя _INC_STDIO. Теперь оно
существует в текущем модуле. Это значит, при повторном включении файла stdio.h условие
директивы #ifndef окажется ложным
и фрагмент дублироваться не будет.
Вот так просто и
элегантно можно с помощью директив условной компиляции создавать защиту от
повторных включений заголовочных файлов. И это очень важно в современной
практике программирования, когда проект содержит десятки и сотни файлов и они
подключаются друг к другу. Дублирования здесь избежать практически невозможно.
Я думаю, в целом
понятна работа директив условной компиляции. В заключение этого занятия приведу
расшифровку всех этих директив:
Директива
|
Описание
|
#if
|
Проверка
произвольного условия.
|
#else
|
Определение
ветки «иначе».
|
#endif
|
Директива
(метка) завершения фрагмента для условия.
|
#ifdef
|
Сокращение
от if defined. Позволяет
делать проверку на наличие макроимени в текущем модуле.
|
#ifndef
|
Сокращение
от if !defined. Позволяет
делать проверку на отсутствие макроимени в текущем модуле.
|
#elif
|
Сокращение
от else if. Позволяет делать
проверку по ветке «иначе».
|
#elifdef
|
Сокращение от else if defined. Для реализации проверки наличия
макроимени по ветке «иначе».
|
#elifndef
|
Сокращение
от else if !defined. Для
реализации проверки отсутствия макроимени по ветке «иначе».
|
В целом, все они
работают аналогичным образом и должны быть теперь вам понятны.
Практический курс по C/C++: https://stepik.org/course/193691