что делает директива include
Директива #include в C/C++ с примерами
Директива
Эта директива читается препроцессором и приказывает ему вставить в данную программу содержимое пользовательского или системного заголовочного файла. Эти файлы в основном импортируются из внешнего источника в текущую программу.
Вот два типа файлов, которые могут быть включены с помощью #include :
1. Заголовочный файл или стандартный файл. Это файл, который содержит C/C++ объявления функций и макроопределения для совместного использования между несколькими исходными файлами.
Синтаксис
Включая использование » «. При использовании двойных кавычек (» «) препроцессор получает доступ к текущей директории, в которой находится исходный «header_file». Этот тип в основном используется для доступа к любым заголовочным файлам пользовательской программы или пользовательским файлам.
Пример 1. Здесь показан импорт системного заголовка ввода/вывода или стандартного файла.
Пример 2. Создание и импорт пользовательского файла.
Создание пользовательского заголовка с именем «process.h».
Созданный основной файл, в который будет включен вышеприведенный «process.h».
Объяснение
Двойные кавычки » « предписывают препроцессору просмотреть текущую папку или стандартную папку всех заголовочных файлов, если они не найдены в данной папке.
Если вместо » » используются угловые скобки, то необходимо сохранить ее в стандартную папку заголовочных файлов. При использовании » » необходимо убедиться, что созданный заголовочный файл сохранен в той же папке, в которой находится текущий C-файл, использующий этот заголовочный файл.
Что делает директива include
Подключаемый файл это файл, содержащий определения функций и переменных, а также макроопределения вместе с некоторыми исходными файлами. Для использования в программе подключаемых файлов применяется директива препроцессора ‘#include’.
Обычно подключаемые файлы заканчиваются на ‘.h’ и следует избегать использования других стандартов.
Как файлы пользователя, так и системные файлы включаются в программу с использованием директивы препроцессора ‘#include’. Она имеет три модификации:
Синтаксис такой модификации директивы ‘#include’ довольно специфичен, потому как комментарии внутри ‘ ‘ не распознаются. Поэтому в строке ‘#include *y>’ последовательность символов ‘*’ не начинает комментарий, а указанная директива включает в программу файл с именем ‘x/*y’.
Аргумент FILE не может содержать символа ‘>’, хотя он может содержать символ ‘
Аргумент FILE не может содержать символов ‘»‘. Символы backslash интерпретируются как отдельные символы, а не начало escape последовательности. Таким образом, директива ‘#include «x\n\\y»‘ указывает имя файла, содержащего три символа backslash.
Эта возможность позволяет определять макросы, что дает возможность изменять имена подключаемых файлов. Эта возможность, например, используется при переносе программ с одной операционной системы на другие, где требуются разные подключаемые файлы.
Директива ‘#include’ указывает С препроцессору обработать указанный файл перед обработкой оставшейся части текущего файла. Информация, выдаваемая препроцессором, содержит уже полученные данные, за которыми следуют данные, получаемые при обработке подключаемого файла, а за которыми, в свою очередь, следуют данные, получаемые при обработке текста, следующего после директивы ‘#include’. Например, дан следующий подключаемый файл ‘header.h’:
Подключаемый файл может содержать начало или окончание сиснтаксической конструкции, такой как определение функции.
Срока, следующая за директивой ‘#include’ всегда является пустой и добавляется С препроцессором даже если подключаемый файл не содержит завершающий символ перевода строки.
Часто случается, что подключаемый файл включает в себя другой файл. Это может привести к тому, что отдельный файл будет подключаться неоднократно, что может привести к возникновению ошибок, если файл определяет типы структур или определения типов. Поэтому следует избегать многократного подключения файлов.
Обычно это достигается путем заключения в условие всего содержимого этого файла, как показано ниже:
Препроцессор GNU C построен таким образом, что обработке подключаемого файла он проверяет наличие определенных конструкций и наиболее рационально их обрабатывает. Препроцессор специально отмечает полное вложение файла в условие ‘#ifndef’. Если в подключаемом файле содержится директива ‘#include’, указывающая на обрабатываемый файл, или макрос в директиве ‘#ifndef’ уже определен, то обрабатываемый файл полностью игнорируется.
Существует также специальная директива, указывающая препроцессору, что файл должен быть включен не более одного раза. Эта директива называется ‘#pragma once’. Она использовалась в дополнение к директиве ‘#ifndef’ и в настоящее время она устарела и не должна прменяться.
В объектно ориентированном языке С существует модификация директивы ‘#include’, называемая ‘#import’, которая используется для вкючения файла не более одного раза. При использовании директивы ‘#import’ вместо ‘#include’ не требуется наличия условных оборотов для предотвращения многократной обработки файла.
«Наследование» это то, что происходит, когда какой либо объект или файл образует некоторую часть своего содержимого путем виртуального копирования из другого объекта или файла. В случае подключаемых С файлов наследование означает, что один файл включает другой файл, а затем заменяет или добавляет что-либо.
Если наследуемый подключаемый файл и основной подключаемый файл имеют различные имена, то такое наследование называется прямым. При этом используется конструкция ‘#include «BASE»‘ в наследуемом файле.
Иногда необходимо чтобы у наследуемого и основного подключаемого файла были одинаковые имена.
Например, предположим, что прикладная программа использует системный подключаемый файл ‘sys/signal.h’, но версия файла ‘/usr/include/sys/signal.h’ на данной системе выполняет того, что требуется в прикладной программе. Будет удобнее определить локальную версию, возможно с именем ‘/usr/local/include/sys/signal.h’ для замены или добавления к версии, поставляемой с системой.
Это можно выполнить с применением опции ‘-I.’ при компиляции, а также созданием файла ‘sys/signal.h’ который выполняет требуемые программе функции. Но сделать так, чтобы этот файл включал стандартный файл ‘sys/signal.h’ не так просто. При включении строки ‘#include ‘ в этот файл произойдет подключение новой версии файла, а не стандартной системной версии. Это приведет к рекурсии и ошибке при компиляции.
При использовании директивы `#include ‘ нужный файл будет найден, но этот способ является не эфективным, так как содержит полный путь к системному файлу. Это может отразиться на содержании системы, так как это означает, что любые изменения местоположения системных файлов потребуют дополнительных изменений где-либо еще.
Более эффективным решением этой проблемы является применение директивы ‘#include_next’, которая используется для подключения следующего файла с таким же именем. Эта директива функционирует также как и директива ‘#include’ за исключением поиска требуемого файла. Она начинает поиск списка каталогов подключаемых файлов после каталога, где был найден текущий файл.
Препроцессор
Препроцессор является обязательным компонентом компилятора языка Си. Препроцессор обрабатывает исходный текст программы до ее непосредственной компиляции. Результатом работы препроцессора является полный текст программы, который передается на компиляцию в исполняемый файл.
Для управления препроцессором применяются директивы, каждая из которых начинается с символа решетки # и располагается на отдельной строке. Препроцессор просматривает текст программы, находит эти директивы и должным образом обрабатывает их.
Мы можем использовать следующие директивы:
#define : определяет макрос или препроцессорный идентификатор
#undef : отменяет определение макроса или идентификатора
#ifdef : проверяет, определен ли идентификатор
#ifndef : проверяет неопределенности идентификатор
#include : включает текст из файла
#if : проверяет условие выражение (как условная конструкция if)
#else : задает альтернативное условие для #if
#endif : окончание условной директивы #if
#elif : задает альтернативное условие для #if
#line : меняет номер следующей ниже строки
#error : формирует текст сообщения об ошибке трансляции
#pragma : определяет действия, которые зависят от конкретной реализации компилятора
# : пустая директива, по сути ничего не делает
Рассмотрим основные из этих директив.
Директива #include. Включение файлов
Поиск файла производится стандартных системных каталогах. Вообще есть стандартный набор встроенных заголовочных файлов, который определяется стандартом языка и которые мы можем использовать:
assert.h : отвечает за диагностику программ
complex.h : для работы с комплексными числами
ctype.h : отвечает за преобразование и проверку символов
errno.h : отвечает за проверку ошибок
fenv.h : для доступа к окружению, которое управляет операциями с числами с плавающей точкой
float.h : отвечает за работу с числами с плавающей точкой
inttypes.h : для работы с большими целыми числами
iso646.h : содержит ряд определений, которые расширяют ряд логических операций
limits.h : содержит предельные значения целочисленных типов
locale.h : отвечает за работу с локальной культурой
math.h : для работы с математическими выражениями
setjmp.h : определяет возможности нелокальных переходов
signal.h : для обработки исключительных ситуаций
stdalign.h : для выравнивания типов
stdarg.h : обеспечивает поддержку переменного числа параметров
stdatomic.h : для выполнения атомарных операций по разделяемым данным между потоками
stdbool.h : для работы с типом _Bool
stddef.h : содержит ряд вспомогательных определений
stdint.h : для работы с целыми числами
stdio.h : для работы со средствами ввода-вывода
stdlib.h : содержит определения и прототипы функций общего пользования
stdnoreturn.h : содержит макрос noreturn
string.h : для работы со строками
tgmath.h : подключает math.h и complex.h плюс добавляет дополнительные возможности по работе с математическими вычислениями
threads.h : для работы с потоками
time.h : для работы с датами и временем
uchar.h : для работы с символами в кодировке Unicode
wchar.h : для работы с символами
wctype.h : содержит дополнительные возможности для работы с символами
Однако стоит отметить, что в различных средах к этому набору могут добавляться дополнительные встроенные заголовочные файлы для тех или иных целей, например, для работы с графикой.
Здесь просто определена одна переменная. Теперь подключим этот файл в программу:
При подключении своих файлов их имя указывается в кавычках. И несмотря на то, что в программе не определена переменная number, она будет браться из подключенного файла main.c. Но опять же отмечу, важно, что в данном случае файл main.c располагается в одной папке с главным файлов программы.
Ключевое слово extern указывает, что данный объект является внешним. И в этом случае мы могули бы его подключить в файл исходного кода:
Этот пример также будет работать в GCC, однако как уже было рассмотрено выше, подключение файла main.h для GCC необязательно.
Препроцессор языка С
В компилятор языка программирования C входит препроцессор, который осуществляет подготовку программы к компиляции. Среди прочего он, например, включает содержимое одних файлов в другие, заменяет в тексте исходного кода имена констант на их значения, удаляет символы конца строки (которые нужны только программисту, чтобы код можно было легко читать, но не нужны компилятору). Что-то препроцессор делает по-умолчанию, а какие-то его действия программируются с помощью специальных директив в исходном коде. Директивы препроцессора начинаются со знака # и заканчиваются переходом на новую строку. В отличие от законченного выражения на языке C, в конце директив не надо ставить точку с запятой. Ниже рассматриваются наиболее распространенные директивы препроцессора и некоторые его свойства, но это далеко не все, что может делать препроцессор.
Директива #include
С этой директивой мы уже не раз встречались, подключая заголовочные файлы стандартной библиотеки языка, содержащие объявления (прототипы) функций. Когда препроцессор встречает такую директиву, то понимает, что после нее идет имя файла, и включает все содержимое указанного файла в исходный код программы. Поэтому объем кода вашей программы после обработки ее препроцессором может сильно увеличиться.
Если имя файла после директивы #include заключено в угловые скобки (например, ), то поиск заголовочного файла производится в стандартном (специально оговоренном системой) каталоге. Однако в тексте программы может встречаться и такая запись:
В таком случае заголовочный файл в первую очередь будет искаться в текущем каталоге. Таким образом, программист сам может определять заголовочные файлы для своих проектов. Кроме того, можно указывать адрес заголовочного файла:
Директива #define
Символические константы
С директивой препроцессора #define мы также уже знакомы. С ее помощью объявляются и определяются так называемые символические константы. Например:
Когда перед компиляцией исходный код будет обработан препроцессором, то все символьные константы (в примере это N и HELLO) в тексте исходного кода на языке C будут заменены на соответствующие им числовые или строковые константы.
Символические константы можно определять в любом месте исходного кода. Однако чтобы переопределить их (изменить значение), необходимо отменить предыдущее определение. Иначе возникнет если не ошибка, то предупреждение. Для удаления символической константы используют директиву #undef :
Символические константы принято писать заглавными буквами. Это только соглашение для удобства чтения кода.
Макросы как усложненные символьные константы
С помощью директивы #define можно заменять символьными константами не только числовые и строковые константы, но почти любую часть кода:
В примере выше PN и SUM являются макросами без аргументов. Однако препроцессор языка программирования C позволяет определять макросы с аргументами:
Директивы условной компиляции
Так называемая условная компиляция позволяет компилировать или не компилировать части кода в зависимости от наличия символьных констант или их значения.
Условное выражение для препроцессора выглядит в сокращенном варианте так:
То, что находится между #if и #endif выполняется, если выражение при #if возвращает истину. Находится там могут как директивы препроцессора так и исходный код на языке C.
Рассмотрим несколько примеров.
Если в программе константа N не равна 0, то цикл for выполнится, и массив arr заполнится нулями. Если N определена и равна 0, или не определена вообще, то цикл выполняться не будет:
Если нужно выполнить какой-то код в зависимости от наличия символьной константы, а не ее значения, то директива #if будет выглядеть так:
Или сокращенно (что тоже самое):
Когда нет уверенности, была ли определена ранее символьная константа, то можно использовать такой код:
Условную компиляцию иногда используют при отладке программного кода, а также с ее помощью компилируют программы под конкретные операционные системы.
Помните, что препроцессор обрабатывает программу до компиляции. В двоичном коде уже отсутствуют какие-либо условные выражения для препроцессора. Поэтому в логическом выражении «препроцессорного if» не должно содержаться переменных, значение которых определяется в момент выполнения программы.
Придумайте и напишите программу, которая может быть скомпилирована по-разному в зависимости от того, определена или нет в ней какая-либо символьная константа.
Константы, определенные препроцессором
Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени.
Если эти константы встречаются в тексте программы, то заменяются на соответствующие строки или числа. Т.к. это происходит до компиляции, то, например, мы видим дату компиляции, а не дату запуска программы на выполнение. Программа ниже выводит значение предопределенных препроцессором имен на экран:
Препроцессорные директивы в C/C++ (#include, #define и прочее)
ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку. |
Причин для этого несколько.
Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме
Любая статья является изложением знаний в общем случае. У многих людей мышление устроено так, что прочтя на форуме конкретный вопрос и конкретный ответ на этот вопрос, у них появится бОльшее понимание, чем после прочтения теоретических выкладок (даже если они подкреплены конкретными примерами). Ссылки на такие обсуждения я, как правило, включаю в последний раздел статьи.
Начинающие, как правило, поиск ответов на свои вопросы ведут именно в форуме, а не в блогах. А потому конкретный вопрос и конкретный ответ для них будет более удобным и полезным именно на форуме. Многие люди умеют работать методом тыка, лишь бы был конкретный пример в качестве образца. А потому такое обсуждение будет им полезным даже без прочтения статьи
Исторически сложилось, что раньше (когда ещё не было блога) статьи располагались на форуме и представлены были в виде двух тем. Первая тема создавалась в специально отведённой свалке и представляла собой черновик, который со временем дорабатывался до законченной статьи. После этого статья переезжала во вторую тему в тематическом разделе. А первая тема оставалась дополнительной свалкой для замечаний и мелких вопросов по теме. Ссылку на старое местоположение данной свалки я помещаю в начале статьи. Вопросы, по возможности, прошу создавать в отдельных темах, но если вопрос действительно мелкий, то можно его задать и в указанной свалке.
1. Что такое препроцессор
Во всех компиляторах с языков C/C++ есть некая фаза, называемая препроцессированием. Фаза эта запускается автоматически и по большому счёту является прозрачной для пользователя (программиста). Т.е. пользователь в большинстве случаев препроцессор самостоятельно НЕ запускает.
Замечу также, что на этапе препроцессирования удаляются из текста все комментарии
1.2. Как посмотреть результат работы препроцессирования
1.2.1 Компилятор gcc (он же mingw под windows)
Что означают первые 4 строки в препроцессорной выдаче, будет рассказано в разделе 6
1.2.2 Microsoft Visual C
FIXME написать по человечески
1.2.3 Borland Builder
FIXME написать по человечески
Люди, кто знает, есть ли возможность посмотреть препроцессированный текст непосредственно из Qt-Creator’а (не запуская ручками g++ в командной строке)
2. Директива #include
Директива #include ничего умного не делает, она просто целиком подставляет файл, который передан параметром директиве. Допустим, мы имеем следующие исходники:
После препроцессирования файла t1.c получим следуюший текст. То, что начинается со стрелки «
3. Директивы #define и #undef
3.1. Директива #define
Директива #define определяет так называемые макросы. Грубо говоря, если мы напишем «#define TRAM 10», то в процессе работы препроцессора все вхождения буквосочетания «TRAM» будут в текстовом виде заменены на «10». При этом надо отметить, что «TRAM» должно быть отдельным токеном (т.е. вокруг должны быть либо пробельные символы, либо знаки препинания). Т.е. «TRAM+TRAM» будет заменено на «10+10», а вот «TRAMPAMPAM» останется без изменений
Для следующего примера
во время препроцессирования все вхождения «N» будут заменены на «20». Т.е. после препроцессирования мы будем иметь следующий текст
и именно этот текст попадёт в компилятор, который даже не будет знать о том, что в исходнике написано «N», он будет видеть только «20». Данное свойство иногда запутывает начинающих, когда в момент выдачи ошибки компилятор говорит, к примеру, что у тебя мол в тексте синтаксическая ошибка перед «20», но при этом в исходнике у пользователя вообще нет буквосочетания «20», т.к. оно подцепилось из системных include’ов
Что в итоге мы имеем?. В конкретно данном примере этим макросом N я задал размер массива, а потом везде работал именно через макрос. Можно сразу написать 20, с точки зрения конкретно данного примера ничего не поменяется. НО. Если мне нужно поменять размер массива с 20 на 30, то я просто меняю значение define’а, а во всех остальных местах это фактически изменится автоматически. Если же писать непосредственно 20, то потом во всех местах надо менять 20 на 30, а таких мест может быть много
Работать с define’ами надо аккуратно. Если написать такой код:
то в функции func НЕ будет заведена локальная переменная, как это вроде бы ожидалось. После препроцессирования текст будет такой:
3.2. Директива #undef
Для того, чтобы отменить макрос, существует директива #undef. Как только препроцессор встречает такую директиву, он «забывает» опеределённый ранее макрос и больше не заменяет его. Для демонстрации этого свойства опять вернёмся к предыдущему примеру и добавим туда макрос #undef
В результате чего препроцессированный текст будет таким:
3.3. Директива #define с параметрами
3.3.1. Общие сведения
Пояснить проще всего на примере. Зададим макрос, в результате подстановки которого мы будем получать квадрат значения параметра, подаваемого в макрос
Во время препроцессирования в случае, описанном в предыдущем разделе, была простая замена «N» на «20». В этом же примере заменяться будет текстовые вхождения «SQUARE ( )» на «val * val», а в качестве val будет подставляться » «. В результате чего после препроцессирования получим вот такой текст:
Макрос можно определять с произвольным количеством параметров, при этом они должны иметь разные имена и разделяться запятыми. Пример макроса для вычисления суммы двух значений
3.3.2. Злосчастный пробел
При написании макроса с параметрами одной из распространённых ошибок начинающих, является наличие пробела между именем макроса и открывающей скобкой при описании директивы #define. Это неправильно, открывающая скобка должна идти впритык к имени макроса
Т.е. при неправильном описании макроса
препроцессированный текст будет выглядеть
В результате чего во-первых получили совсем не то, что хотели, а во-вторых выдача ошибки компилятора на строку кода «a = SQUARE (x);» скорее всего начинающего введёт в ступор
При этом критичным является лишь то, что между именем макроса и открывающей скобкой не должно быть пробелов. Во всех остальных местах пробелы можно ставить в произвольном количестве
Ещё одна распространённая ошибка демонстрируется следующим примером
текст программы выглядит так, как будто бы в переменную a должен записаться квадрат значения «x+1». Однако это не так. По результату работы препроцессора текст будет выглядеть следующим образом:
и с учётом того, что приоритет операции умножения выше, чем приоритет операции сложения, то в переменную a запишется значение «x + 1*x + 1» что эквивалентно «2*x + 1», но никак не «(x+1)*(x+1)». Чтобы избежать таких проблем при написании макросов с параметрами во всех местах использования параметров их надо заключать в круглые скобки. Таким образом правильным вариантом будет следующее:
что после препроцессирования превратится
Но это ещё не всё. Если рассмотреть немного изменённый пример
то после препроцессирования
опять получим неверный код. Дабы избежать этого, нужно ещё и всё тело макроса заключить в круглые скобки:
Однако даже такой вариант не сможет отработать корректно в 100% случаев. Макрос, в теле которого параметр используется более одного раза в общем случае работает некорректно (в том смысле, что отработает не так, как от него ожидали). Если в качестве параметра подать конструкцию, значение которой меняется при каждом обращении, то получим некорректный код. Например, глядя на текст
кажется, что мы прочитаем один символ из файла и возведём его значение в квадрат, однако после препроцессирования получим следующее
И таким образом за одно обращение к макросу будет прочитано два байта из файла и их значения будут перемножены, что опять-таки не соответствует тому, чего мы ожидали. Точно так же будет проблема, если в качестве параметра подать выражение типа «i++». О таких моментах надо всегда помнить, а потому пользоваться макросами с осторожностью. Применительно к данному примеру в языке C++ более правильным было бы реализовать inline-функцию, но это уже выходит за рамки данной статьи (если дойдут руки, то напишу отдельную)
3.3.4. Оператор # для параметров макроса
Препроцессор работает с файлом строго как с текстом. То же самое касается и параметров директивы #define. Следовательно, препроцессору ничего не стоит делать с ними некоторые простые преобразования. Одним из них является символ ‘#’. Если его поставить перед параметром макроса, то в результате подстановки этот параметр будет взят в кавычки
А в качестве более-менее живого примера можно написать макрос для отладочной печати целочисленной переменной
Начинающим наверно это покажется несколько мутным, но тем не менее данный код рабочий. В языках C/C++ запись строковой константы можно распиливать на несколько строковых констант, между которыми находятся знаки пробела, табуляции, переноса строк или вообще ничего не находится. Т.е. запись
компиляторы воспринимают как
Исходя из этого, данный макрос раскроется в код, эквивалентный
При этом в качестве параметра макроса совсем необязательно подавать только переменную. Можно подавать и выражение
что раскроется в код, эквивалентный
Правда такой вариант макроса отладочной печати неудобен тем, что им можно распечатать только int’ы. Для тех, кто работает на Си++, эта проблема легко устраняется переписыванием макроса в виде
А вот тем, кто работает на C, придётся добавить дополнительный параметр в макрос для правильного формата printf’а. При этом программист должен будет сам контролировать, что для каждой переменной подан правильный формат
FIXME Написать про оператор #, применённый к строковому литералу: https://www.cyberforum.ru/faq/. ost4465754
3.3.5. Оператор ## в теле макроса
Ещё одним оператором для параметров макроса является ##. Если его поставить между двумя «словами» в теле макроса, то эти два «слова» будут склеены в одно (так называемый оператор конкатенации):
и в кажом месте использования писать такую портянку. Но это ещё полбеды. Теперь если вдруг окажется, что программа должна работать ещё на какой-то ОС с каким-то новым префиксом, то все подобные места надо будет исправлять ручками. Поэтому лучше поступать следующим образом
в этом случае после препроцессирования linux’овыми компиляторами ны выходе будем иметь текст
а после препроцессирования solaris’овым компилятором будем иметь вот такой текст:
Достоинство этого метода более чем очевидно: макрос SYS_NUM реализуется только в одном месте (в общем заголовочном файле). Если нужно добавить новую ОС, то правка делается только в одном месте (а не в миллионах мест, как это было бы при реализации «в лоб»)
3.3.6. Директива #define с переменным числом параметров
FIXME Написать, а пока ссылка на пример использования
В качестве примера использовать синтетический тест. Создаём макрос, который вычисляет сумму своих аргументов. Типа SUM(2,x,y), SUM(5,x,y,z,a,b). В качестве реального примера сослаться на реализацию syscall’ов в linux’овом unustd.h
3.3.7. Круглые скобки или запятая среди параметров макроса
FIXME Написать, а пока ссылка на пример использования
3.4. Директива #define, растянутая на несколько строк
При написании макроса, его тело может оказаться довольно длинным. В этом случае для удобства можно его разбивать на несколько строк, но при этом в конце каждой строки, кроме самой последней, надо будет ставить символ обратного слэша, который по сути дела говорит препроцессору, что последующий символ перевода строки надо будет проигнорировать и считать, что вся следующая строка как бы является продолжением текущей
Что после препроцессирования раскроется в
Заметим, что при этом в препроцессированном тексте вся эта конструкция выразится в одну строку (т.е. знаки перевода строки пропадут), что несколько неудобно для просмотра препроцессорной выдачи глазами. Дополнительно появившиеся пробелы являются следствием аккуратного форматирования текста (отступы в начале каждой строки), что так же затрудняет просмотр глазами
3.5. Использование в директиве #define имён других макросов
Пока написано тут и тут
FIXME перенести сюда и аккуратно переписать (хотя вроде бы там написал всё как надо)
Ещё один интересный момент тут
FIXME перенести сюда и аккуратно переписать
4. Директивы условной компиляции
4.1. Общие сведения об условной компиляции
В любой программе есть условные ветки исполнения кода. Точно так же препроцессор позволяет условно включать те или иные фрагменты исходника программы. Для этого используется препроцессорная директива «#if «. Прежде, чем делать подробные объяснения, попробую сначала показать на простом примере, как это использовать.
Допустим мы пишем программу, которая содержит отладочные печати. Эти печати имеет смысл включать только тогда, когда разработчик программы занимается её отладкой. Версия, которую программист отдаёт пользователю, этих печатей не должна содержать. Для этого, например, можно в программе завести переменную и ставить все печати под условие:
В простых случаях это действительно будет выходом из ситуации. Если переменную debug установить в 0, то компилятор, видя условие «if (debug)», а также то, что переменная имеет модификатор const т равна нулю, скорее всего вообще удалит вызов printf’а, как мёртвый код, в который программа никогда не попадёт. Но если программа состоит из нескольких исходных файлов, то такой фокус не пройдёт, потому как переменная должна быть определена только в одном модуле, а остальные модули не будут видеть значения переменной, а потому не смогут удалить мёртвый код. При этом вызов printf’а в коде программы останется, хоть и будет стоять под условием, которое никогда не будет равно true. По большому счёту и это тоже терпимо, т.к. десяток или сотня вызовов printf’а принципиально размер бинарного фала не увеличат (т.е. увеличение будет составлять единицы процентов, но не в разы). Хуже обстоит дело, когда мы вызваем не printf, а какую-то «нашу» функцию, которая нужна только для отладочных печатей.
В этом случае в код программы попадут все такие функции, которые никогда не будут запускаться, но место в бинарном фале занимать. И когда таких функций много, то программа может ощутимо «потяжелеть».
В качестве примера я привёл отладочную печать, но вместо отладочной печати может вообще стоять какая-то функциональность. Многие программные продукты распространяются в том виде, что полная версия делается платной, а свободных (бесплатных) версиях отключается часть функциональности. Если это делать приведёным выше способом, то можно попросту покопаться в бинарнике и на месте переменной debug воткнуть единицу, после чего урезанная программа превращается в полную (условно говоря).
Чтобы избежать этих проблем, ненужный код надо физически вырезать из программы. И для этих целей удобно использовать директиву условной компиляции «#if»
Эта директива на этапе препроцессирования управляет тем, что попадёт в препроцессорную выдачу, а что нет. Для данного примера после препроцессора будем иметь вот такой текст:
Теперь если мы значение макроса DEBUG поменяем на 0, то получим совсем другую выдачу из-под препроцессора:
Немного забегая вперёд, скажу, что конкретно это место я бы переписал чуть-чуть по другому. Об этом более подробно расскажу в разделе 8, т.к. здесь описываю только принцип условной компиляции
Аналогично следует поступать, когда из программы нужно вырезать некоторую функциональность, только получится здесь чуточку сложнее. Допустим, у нас есть программа, которую следует собирать в трёх конфигурациях: lite, medium, full. Схематично выглядеть будет примерно так:
4.2. Директивы #if, #else, #elif, #endif
Что касается условия, то хочется ещё раз отметить, что препроцессирование делается отдельно от компиляции, а потому в условии директивы «#if» НЕ могут использоваться никакие переменные из программы. В условии могут использоваться только целочисленные константные значения (которые могут быть значениями других макросов). Над этими константами можно выполнять операции сравнения «==», «!=», » «, «>=». В условии могут использоваться логические операции «&&», «||», «!», круглые скобки, а так же некая конструкция «defined «, значение которой истинно, если макрос определён, в противном случае значение ложно. Препроцессорные «#if’ы», так же, как и языковые, могут быть вложены друг в друга. В строках с директивами можно использовать комментарии
Если обратиться к примерам из предыдущего раздела и скомбинировать их, то можно продемонстрировать один из примеров использования. Ситуация следующая: в программе была найдена ошибка, но аккуратное её исправление требует много времени, а исправить надо быстро. Поэтому пока делаем в виде затычки.
Жизнь показала, что не всем понятно, что такое «затычка» по своей сути. Здесь приведён хороший пример с пояснением. Пример основан на реальной ошибке в коде
4.3. Директивы #ifdef, #ifndef
5. Директивы #error и #warning
5.2. Директива #error
В качестве примера можно взять следующее. Допустим мы пишем программу, которая может работать на разных платформах. При этом в момент компиляции нужно знать, наша платформа big endian или little endian. На разных платформах работают компиляторы от разных разработчиков. Как правило, каждый компилятор выставляет некие предопределённые макросы, в том числе и макросы, определяющие тип процессора. Например, большинство компиляторов под intel’овскую архитектуру выставляют макрос __i386__, компиляторы под sparc’овскую архитектуру выставляют макрос __sparc__ и т.д. Но каких-то более-менее единых макросов, относящихся к endian’у архитектуры нет. Поэтому наиболее надёжным способом будет самим взвести какой-то макрос в зависимости от архитектуры. Можно это сделать следующим образом:
Подобные коды как правило помещаются в некоторый настроечный файл *.h. При этом, если мы компилируем код на intel’е или aplha’е, то у нас взведётся макрос LITTLE_ENDIAN, а при компиляции на sparc’е взведётся макрос BIG_ENDIAN. А уже во всей остальной программе мы используем именно макросы LITTLE_ENDIAN и BIG_ENDIAN для выбора нужной ветки компиляции. Теперь, предположим, мы пытаемся скомпилировать код на MIPS’е. Ни один из указанных макросов (__i386__ и т.д.) у нас не будет взведённым, поэтому мы попадаем в ветку «#else», где у нас произойдёт слом на компиляции (с привязкой к файлу исходника и номеру строки). Поглядев на этот код программисту останется только добавить проверку макроса __mips__ в ветку для little endian, после чего программа начнёт собираться.
Смысл директивы #error именно в том, чтобы обнаруживать такие неподдерживаемые или недопустимые комбинации настроечных макросов и уже на этапе компиляции программы выдавать ошибку.
5.3. Директива #warning
Директива «#warning» работает аналогичным образом, но выдаёт не ошибку, а предупреждение. Как правило делается это в тех случаях, когда хотят что-то поменять, но не сразу, а плавно: в какой-то период времени будут работать «старый» и «новый» варианты, но со временем старый вариант будет удалён.
Если программу пишет сто человек, то такой резкий переход с ходу сделать не получится. Поэтому нужен какой-то временный период, в течение которого все люди смогут постепенно перейти от использования макроса DEBUG к макросу DEBUG_LEVEL. Проще всего в точке, где в нынешней реализации определяется макрос DEBUG, поступить следующим образом:
После этого в течение какого-то времени люди избавляются от макроса DEBUG. Практика показывает, что в данных случаях нужно включать выдачу предупреждения, чтобы оно постоянно мозолило глаза и те, кто откладывает это дело на «потом», не забыли. Как вариант предупреждение можно делать многострочным, чтобы бросалось в глаза. После того, как все поправят свои коды, весь наш промежуточный код можно будет удалить и оставить только определения макроса DEBUG_LEVEL. На какой-то период можно будет добавить для контроля такой код (потому что опять-таки практика показывает, что всегда найдутся те, кто в танке):
7. Директивы, но не относящиеся к препроцессору (#pragma, #import и т.п.)
8. Примеры использования препроцессорных директив на практике
9. Когда следует использовать макросы, а когда конструкции языка программирования
10. Ссылки на темы, где обсуждались подобные вопросы