Препроцессор
Препроцессор является обязательным компонентом компилятора языка Си. Вообще весь процесс компиляции программы на языке Си разбивается на три этапа:
- Препроцессор
- Компиляция
- Линковка
Препроцессор обрабатывает исходный текст программы до ее непосредственной компиляции. Результатом работы препроцессора является полный текст программы, который передается на компиляцию в исполняемый файл.
Затем компилятор компилирует обработанный препроцессором исходный код в объектные файлы.
И на последнем этапе линкер (линковщик) объединяет (линкует) объектные файлы в один исполняемый файл или файл динамической библиотеки.
Для управления препроцессором применяются директивы, каждая из которых начинается с символа решетки # и располагается на отдельной строке. Препроцессор просматривает текст программы, находит эти директивы и должным образом обрабатывает их.
Мы можем использовать следующие директивы:
- #define : определяет макрос или препроцессорный идентификатор
- #undef : отменяет определение макроса или идентификатора
- #ifdef : проверяет, определен ли идентификатор
- #ifndef : проверяет неопределенности идентификатор
- #include : включает текст из файла
- #if : проверяет условие выражение (как условная конструкция if)
- #else : задает альтернативное условие для #if
- #endif : окончание условной директивы #if
- #elif : задает альтернативное условие для #if
- #line : меняет номер следующей ниже строки
- #error : формирует текст сообщения об ошибке трансляции
- #pragma : определяет действия, которые зависят от конкретной реализации компилятора
- # : пустая директива, по сути ничего не делает
Рассмотрим основные из этих директив.
Директива #include. Включение файлов
Ранее уже использовалась директива #include . Эта директива подключает в исходный текст файлы. Она имеет следующие формы применения:
#include // имя файла в угловых скобках #include "имя_файла" // имя файла в кавычках
Например, если нам надо задействовать в приложении консольный ввод-вывод с помощью функций printf() или scanf() , то нам надо подключить файл «stdio.h», который содержит определение этих функций:
#include
При выполнении этой директивы препроцессор вставляет текст файла stdio.h . Данный файл еще называется заголовочным. Заголовочные файлы содержат прототипы функций, определения и описания типов и констант и имеют расширение .h .
Поиск файла производится стандартных системных каталогах. Вообще есть стандартный набор встроенных заголовочных файлов, который определяется стандартом языка и которые мы можем использовать:
- 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 : содержит дополнительные возможности для работы с символами
Однако стоит отметить, что в различных средах к этому набору могут добавляться дополнительные встроенные заголовочные файлы для тех или иных целей, например, для работы с графикой.
Определение заголовочных файлов
Кроме стандартных заголовочных файлов мы можем подключать и свои файлы. Например, в той же папке, где находиться главный файл программы, определим еще один файл, который назовем numbers.c .
Определим в нем следующий код:
int number = 5;
Здесь просто определена одна переменная. Теперь подключим этот файл в главный файл программы, который, допустим, называется app.c :
#include #include "numbers.c" int main(void) < printf("Number: %d", number); // Number: 5 return 0; >
При подключении своих файлов их имя указывается в кавычках. И несмотря на то, что в программе не определена переменная number, она будет браться из подключенного файла numbers.c . Но опять же отмечу, важно, что в данном случае файл numbers.c располагается в одной папке с главным файлов программы.
Определение заголовочных файлов
В то же время данный способ прекрасно работает в GCC. Но для разных сред программирования способ подключения файлов может отличаться. Например, в Visual Studio мы получим ошибку. И более правильный подход будет состоять в том, что определить объявление объекта (переменной/константы) или функции в дополнительном заголовочном файле, а определение объекта или функции поместить в стандартный файл с расширением .c .
Например, в нашем в файле numbers.c уже есть определение переменной number. Теперь в ту же папку добавим новый файл numbers.h — файл с тем же названием, но другим расширением.
И определим в numbers.h следующий код:
extern int number;
Ключевое слово extern указывает, что данный объект является внешним. И в этом случае мы могли бы его подключить в файл исходного кода:
#include #include «numbers.h» // объявление или описание объекта int main(void)
Далее чтобы скомпилировать эту программу, передадим компилятору GCC оба файла с исходным кодом — app.c и numbers.c :
gcc app.c numbers.c -o app.exe && app
Компилятору передаются файлы через пробел и компилируются в один исполняемый файл.
Примечание для Visual Studio
Если разработка ведется в Visual Studio , то не надо подключать файл с исходным кодом ( numbers.c ), однако чтобы Visual Studio видела функционал файла в процессе разработки, может потребоваться подключить заголовочный файл ( numbers.h ). При этом заголовочный файл numbers.h помещается в папку Headers Files .
Препроцессор языка С
В компилятор языка программирования C входит препроцессор, который осуществляет подготовку программы к компиляции. Среди прочего он, например, включает содержимое одних файлов в другие, заменяет в тексте исходного кода имена констант на их значения, удаляет символы конца строки (которые нужны только программисту, чтобы код можно было легко читать, но не нужны компилятору). Что-то препроцессор делает по-умолчанию, а какие-то его действия программируются с помощью специальных директив в исходном коде. Директивы препроцессора начинаются со знака # и заканчиваются переходом на новую строку. В отличие от законченного выражения на языке C, в конце директив не надо ставить точку с запятой. Ниже рассматриваются наиболее распространенные директивы препроцессора и некоторые его свойства, но это далеко не все, что может делать препроцессор.
Директива #include
С этой директивой мы уже не раз встречались, подключая заголовочные файлы стандартной библиотеки языка, содержащие объявления (прототипы) функций. Когда препроцессор встречает такую директиву, то понимает, что после нее идет имя файла, и включает все содержимое указанного файла в исходный код программы. Поэтому объем кода вашей программы после обработки ее препроцессором может сильно увеличиться.
Если имя файла после директивы #include заключено в угловые скобки (например, ), то поиск заголовочного файла производится в стандартном (специально оговоренном системой) каталоге. Однако в тексте программы может встречаться и такая запись:
#include "ext.h"
В таком случае заголовочный файл в первую очередь будет искаться в текущем каталоге. Таким образом, программист сам может определять заголовочные файлы для своих проектов. Кроме того, можно указывать адрес заголовочного файла:
#include "/home/iam/project10/const.h"
Директива #define
Символические константы
С директивой препроцессора #define мы также уже знакомы. С ее помощью объявляются и определяются так называемые символические константы. Например:
#define N 100 #define HELLO "Hello. Answer the questions."
Когда перед компиляцией исходный код будет обработан препроцессором, то все символьные константы (в примере это N и HELLO) в тексте исходного кода на языке C будут заменены на соответствующие им числовые или строковые константы.
Символические константы можно определять в любом месте исходного кода. Однако чтобы переопределить их (изменить значение), необходимо отменить предыдущее определение. Иначе возникнет если не ошибка, то предупреждение. Для удаления символической константы используют директиву #undef :
#include #define HELLO "Hello. Answer the questions.\n" int main () { printf(HELLO); #undef HELLO #define HELLO "Good day. Tell us about.\n" printf(HELLO); }
Если в этом примере убрать строку #undef HELLO , то при компиляции в GNU/Linux появляется предупреждение: «HELLO» переопределён.
Символические константы принято писать заглавными буквами. Это только соглашение для удобства чтения кода.
Макросы как усложненные символьные константы
С помощью директивы #define можно заменять символьными константами не только числовые и строковые константы, но почти любую часть кода:
#include #define N 100 #define PN printf("\n") #define SUM for(i=0; i int main () { int i, sum = 0; SUM; printf("%d", sum); PN; }
Здесь в теле функции main() PN заменяется препроцессором на printf(«\n») , а SUM на цикл for . Такие макроопределения (макросы) в первую очередь удобны, когда в программе часто встречается один и тот же код, но выносить его в отдельную функцию нет смысла.
В примере выше PN и SUM являются макросами без аргументов. Однако препроцессор языка программирования C позволяет определять макросы с аргументами:
#include #define DIF(a,b) (a)>(b)?(a)-(b):(b)-(a) int main () { int x = 10, y = 30; printf("%d\n", DIF(67,90)); printf("%d\n", DIF(876-x,90+y)); }
Вызов макроса DIV(67,90) в тексте программы приводит к тому, что при обработке программы препроцессором туда подставляется такое выражение (67) > (90) ? (67)-(90) : (90)-(67) . В этом выражении вычисляется разница между двумя числами с помощью условного выражения (см. урок 3). В данном случае скобки не нужны. Однако при таком разворачивании (876-x) > (90+y) ? (876-x)-(90+y) : (90+y)-(876-x) скобки подчеркивают порядок операций. Если бы вместо сложения и вычитания фигурировали операции умножения или деления, то наличие скобок было бы принципиальным.
Обратите внимание, что в имени макроса не должно быть пробелов: DIF(a,b) . Первый пробел после идентификатора означает конец символической константы и начало выражения для подстановки.
- Напишите программу, содержащую пару макросов: один вычисляет сумму элементов массива, другой выводит элементы массива на экран.
- Напишите программу, содержащую макросы с аргументами, вычисляющие площади различных геометрических фигур (например, квадрата, прямоугольника, окружности).
Директивы условной компиляции
Так называемая условная компиляция позволяет компилировать или не компилировать части кода в зависимости от наличия символьных констант или их значения.
Условное выражение для препроцессора выглядит в сокращенном варианте так:
#if … … #endif
То, что находится между #if и #endif выполняется, если выражение при #if возвращает истину. Находится там могут как директивы препроцессора так и исходный код на языке C.
Условное включение может быть расширено за счет веток #else и #elif .
Рассмотрим несколько примеров.
Если в программе константа N не равна 0, то цикл for выполнится, и массив arr заполнится нулями. Если N определена и равна 0, или не определена вообще, то цикл выполняться не будет:
#include #define N 10 int main() { int i, arr[100]; #if N for(i=0; iN; i++) { arr[i] = 0; printf("%d ", arr[i]); } #endif printf("\n"); }
Если нужно выполнить какой-то код в зависимости от наличия символьной константы, а не ее значения, то директива #if будет выглядеть так:
#if defined(N)
Или сокращенно (что тоже самое):
#ifdef N
Когда нет уверенности, была ли определена ранее символьная константа, то можно использовать такой код:
#if !defined(N) #define N 100 #endif
Таким образом мы определим константу N, если она не была определена ранее. Такие проверки могут встречаться в многофайловых проектах. Выражение препроцессора #if !defined(N) может быть сокращено так:
#ifndef N
Условную компиляцию иногда используют при отладке программного кода, а также с ее помощью компилируют программы под конкретные операционные системы.
Помните, что препроцессор обрабатывает программу до компиляции. В двоичном коде уже отсутствуют какие-либо условные выражения для препроцессора. Поэтому в логическом выражении «препроцессорного if» не должно содержаться переменных, значение которых определяется в момент выполнения программы.
Придумайте и напишите программу, которая может быть скомпилирована по-разному в зависимости от того, определена или нет в ней какая-либо символьная константа.
Константы, определенные препроцессором
Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени.
- __DATE__ — дата компиляции;
- __FILE__ — имя компилируемого файла;
- __LINE__ — номер текущей строки исходного текста программы;
- __STDC__ — равна 1, если компилятор работает по стандарту ANSI для языка C;
- __TIME__ — время компиляции.
Если эти константы встречаются в тексте программы, то заменяются на соответствующие строки или числа. Т.к. это происходит до компиляции, то, например, мы видим дату компиляции, а не дату запуска программы на выполнение. Программа ниже выводит значение предопределенных препроцессором имен на экран:
#include #define NL printf("\n") int main () { printf(__DATE__); NL; printf("%d",__LINE__); NL; printf(__FILE__); NL; printf(__TIME__); NL; printf("%d",__STDC__); NL; }
Nov 18 2020 7 les20/10_const.c 13:27:37 1
Курс с решением части задач:
pdf-версия
Директивы препроцессора в Си
Препроцессор — это специальная программа, являющаяся частью компилятора языка Си. Она предназначена для предварительной обработки текста программы. Препроцессор позволяет включать в текст программы файлы и вводить макроопределения.
Работа препроцессора осуществляется с помощью специальных директив (указаний). Они отмечаются знаком решетка #. По окончании строк, обозначающих директивы в языке Си, точку с запятой можно не ставить.
Основные директивы препроцессора
#include — вставляет текст из указанного файла
#define — задаёт макроопределение (макрос) или символическую константу
#undef — отменяет предыдущее определение
#if — осуществляет условную компиляцию при истинности константного выражения
#ifdef — осуществляет условную компиляцию при определённости символической константы
#ifndef — осуществляет условную компиляцию при неопределённости символической константы
#else — ветка условной компиляции при ложности выражения
#elif — ветка условной компиляции, образуемая слиянием else и if
#endif — конец ветки условной компиляции
#line — препроцессор изменяет номер текущей строки и имя компилируемого файла
#error — выдача диагностического сообщения
#pragma — действие, зависящее от конкретной реализации компилятора.
Директива #include
Директива #include позволяет включать в текст программы указанный файл. Если заголовочный файл содержит описание библиотечных функций и находится в папке компилятора, он заключается в угловые скобки <> .
Если файл находится в текущем каталоге проекта, он указывается в кавычках «» . Для файла, находящегося в другом каталоге необходимо в кавычках указать полный путь.
Препроцессор Си
Препроцессор С/С++ — программный инструмент, изменяющий код программы для последующей компиляции и сборки, используемый в языках программирования Си и его потомка — C++. Этот препроцессор обеспечивает использование стандартного набора возможностей:
- Замена триграфов??=, ??(, ??) (и других) символами #, [, ]
- Замена комментариев пустыми строками
- Включение файла — #include
- Макроподстановки — #define
- Условная компиляция — #if , #ifdef , #elif , #else , #endif
Важной областью применения препроцессоров С является условная компиляция. При подготовке программы к компиляции разработчик может с помощью нескольких изменений адаптировать программу к текущей ситуации (например, к определенной модели процессора).
Препроцессор языка Си — низкоуровневый, лексический препроцессор, потому что он требует только лексического анализа, то есть он обрабатывает только исходный текст перед парсингом, выполняя простую замену лексем и специальных символов заданными последовательностями символов, в соответствии с правилами, установленными пользователями.
Директивы
Директивой препроцессора (или командной строкой препроцессора [1] ) называется строка в исходном коде, которая начинается с символа # и следующего за ним ключевого слова препроцессора. Есть чётко определённый список ключевых слов:
- define — задаёт макроопределение (макрос) или символическую константу
- undef — отменяет предыдущее определение
- include — вставляет текст из указанного файла
- if — осуществляет условную компиляцию при истинности константного выражения
- ifdef — осуществляет условную компиляцию при определённости символической константы
- ifndef — осуществляет условную компиляцию при неопределённости символической константы
- else — ветка условной компиляции при ложности выражения
- endif — конец ветки условной компиляции
- line — препроцессор изменяет номер текущей строки и имя компилируемого файла
- error — выдача диагностического сообщения
- pragma — действие, зависящее от конкретной реализации компилятора
- пустое слово — пустое действие.
Функции
Включение
Препроцессор Си, встречая следующие директивы:
#include ". "
#include
полностью копирует содержимое указанного файла в файл, в котором указана эта директива, в месте вызова директивы. Эти файлы обычно содержат определение интерфейса для различных функций библиотек и типов данных, которые должны быть подключены перед их использованием; таким образом, директива #include обычно указывается в начале (заголовке) файла. По этой причине подключаемые файлы и называются заголовочными. Некоторые содержат примеры из стандартной библиотеки Си ( и ), обеспечивая математические функции и функции ввода-вывода соответственно.
При использовании препроцессора одновременно с упрощением использования кода его использование несколько замедляет написание кода из-за недостаточной эффективности и требует дополнительного использования условной компиляции для предотвращения множественных включений указанного заголовочного файла.
Начиная с 1970-х среди программистов все большее распространение и известность получают альтернативные способы переиспользования подключения файлов, применяемых в большинстве языков программирования: Java и Common Lisp используют пакеты, Паскаль использует юниты (единицы), Modula, OCaml, Haskell и Python используют модули, а D, разработанный как замена языков Си и C++, использует импорт.
Макросы
Макросы в языке Си преимущественно используются для определения небольших фрагментов кода. Во время обработки кода препроцессором, каждый макрос заменяется соответствующим ему определением. Если макрос имеет параметры, то они указываются в теле макроса; таким образом, макросы языка Си могут походить на Си-функции. Распространенная причина использования — избежание накладных расходов при вызове функции в простейших случаях, когда небольшого кода, вызываемого функцией, достаточно для ощутимого снижения производительности.
#define max(a,b) ((a) > (b) ? (a) : (b))
определяет макрос max, использующий два аргумента a и b. Этот макрос можно вызывать как любую Си-функцию, используя схожий синтаксис. То есть, после обработки препроцессором,
z = max(x,y);
z = ((x) > (y) ? (x) : (y));
Однако, наряду с преимуществами использования макросов в языке Си, например, для определения типобезопасных общих типов данных или отладочных инструментов, они также несколько снижают эффективность их применения и даже могут привести к ошибкам.
Например, если f и g — две функции, вызов
z = max(f(), g());
не вычислит один раз f()и один раз g(), и поместит наибольшее значение в z, как этого можно было ожидать. Вместо этого одна из функций будет вычислена дважды. Если функция имеет побочные эффекты, то вероятно, что её поведение будет отличаться от ожидаемого.
Макросы Си могут походить на функции, создавая новый синтаксис в некоторых пределах, а также могут быть дополнены произвольным текстом (хотя компилятор Си требует, чтобы текст был без ошибок написанным Си-кодом или оформлен как комментарий), но у них есть некоторые ограничения как у программных конструкций. Макросы, схожие с функциями, например, могут быть вызваны как «настоящие» функции, но макрос не может быть передан другой функции при помощи указателя, по той причине, что макрос сам по себе не имеет адреса.
Некоторые современные языки обычно не используют такой способ метапрограммирования с использованием макросов как дополнений строк символов, в расчете или на автоматическое или на ручное подключение функций и методов, а вместо этого другие способы абстракции, такие как шаблоны, общие функции или параметрический полиморфизм. В частности, встраиваемые функции позволяют избежать одного из главных недостатков макросов в современных версия Си и C++, так как встроенная функция обеспечивает преимущество макросов в снижении накладных расходов при вызове функции, но её адрес можно передавать в указателе для косвенных вызовов или использовать в качестве параметра. Аналогично, проблема множественных вычислений, упомянутая выше в макросе max, для встроенных функций неактуальна.
Условная компиляция
Основная статья: Include guard
Препроцессор языка Си предоставляет возможность компиляции с условиями. Это допускает возможность существования различных версий одного кода. Обычно, такой подход используется для настройки программы под платформу компилятора, состояние (отлаживаемый код может быть выделен в результирующем коде), или возможность проверки подключения файла строго один раз.
В общем случае, программисту необходимо использовать конструкцию наподобие этой:
#ifndef FOO_H #define FOO_H . (код заголовочного файла). #endif
Такая «защита макросов» предотвращает двойное подключение заголовочного файла путем проверки существования этого макроса, который имеет то же самое имя, что и заголовочный файл. Определение макроса FOO_H происходит, когда заголовочный файл впервые обрабатывается препроцессором. Затем, если этот заголовочный файл вновь подключается, FOO_H уже определен, в результате чего препроцессор пропускает полностью текст этого заголовочного файла.
То же самое можно сделать, включив в заголовочный файл директиву:
#pragma once
Условия препроцессора можно задавать несколькими способами, например:
#ifdef x . #else . #endif