что такое define в си
Директива #define
Обратите внимание, что в этом выражении нет точки с запятой. Между идентификатором и последовательностью символов последовательность_символов может быть любое количество пробелов, но признаком конца последовательности символов может быть только разделитель строк.
Предположим, например, что вместо значения 1 нужно использовать слово LEFT (левый), а вместо значения 0 — слово RIGHT (правый). Тогда можно сделать следующие объявления с помощью директивы #define :
В результате компилятор будет подставлять 1 или 0 каждый раз, когда в вашем файле исходного кода встречается идентификатор соответственно LEFT или RIGHT. Например, следующий код выводит на экран 0 1 2:
После определения имя макроса можно использовать в определениях других имен макросов. Вот, например, код, определяющий значения ONE (один), TWO (два) и three (три):
Макроподстановка — это просто замена какого-либо идентификатора связанной с ним последовательностью символов. Поэтому если требуется определить стандартное сообщение об ошибке, то можно написать примерно следующее:
Если идентификатор находится внутри строки, заключенной в кавычки, то замены не будет. Например, при выполнении кода
Если последовательность_символов не помещается в одной строке, то эту последовательность можно продолжить на следующей строке, поместив в конце предыдущей, как показано ниже, обратную косую черту:
Программисты, пишущие программы на языке С, в именах определяемых идентификаторов часто используют буквы верхнего регистра. Если разработчики программ следуют этому правилу, то тот, кто будет читать их программу, с первого взгляда поймет, что будет происходить макрозамена. Кроме того, все директивы #define обычно лучше всего помещать в самом начале файла или в отдельном заголовочном файле, а не разбрасывать по всей программе.
Определение макросов с формальными параметрами
после макрозамены будет преобразовано в
и может привести к неправильному результату.
Использование вместо настоящих функций макросов с формальными параметрами дает одно существенное преимущество: увеличивается скорость выполнения кода, потому что в таких случаях не надо тратить ресурсы на вызов функций. Однако если у макроса с формальными параметрами очень большие размеры, то тогда из-за дублирования кода увеличение скорости достигается за счет увеличения размеров программы.
На заметку | В С99 можно определить макрос с переменным количеством формальных параметров; об этом рассказывается в части II этой книги. |
#define
Директива #define определяет идентификатор и последовательность символов, которой будет замещаться данный идентификатор при его обнаружении в тексте программы. Идентификатор также называется именем макроса, а процесс замещения называется подстановкой макроса. Стандартный вид директивы следующий:
#define имя_макроса последовательность_символов
Обратим внимание, что в данном операторе отсутствует точка с запятой. Между идентификатором и последовательностью символов может быть любое число пробелов. Макрос завершается только переходом на новую строку.
Например, если необходимо использовать TRUE для значения 1, a FALSE для 0 то можно объявить следующие два макроса:
#define TRUE 1
#define FALSE 0
В результате, если компилятор обнаружит в тексте программы TRUE или FALSE, то он заменит их на 1 и 0 соответственно. Например, следующая строка выводит на экран «0 1 2»:
printf («%d %d %d», FALSE, TRUE, TRUE + 1);
В случае, если макрос определен, он может использоваться для определения других макросов. Например, следующий код сопоставляет с именами ONE, TWO и THREE их численные значения:
#define ONE 1
#define TWO ONE + ONE
#def ine THREE ONE + TWO
В результате макроподстановки идентификаторы замещаются указанными строками. Если необходимо определить стандартное сообщение об ошибке, то можно написать что-то вроде следующего:
#define E_MS «Standart error on input.\n»
/*. */
printf(E_MS);
Если компилятор обнаруживает идентификатор E_MS, то он замещает его строкой «Standart error on input.» На самом деле компилятор увидит оператор в виде
printf(«Standart error on input.\n»);
Если идентификатор находится в строке, то подстановка не происходит. Например:
#define XYZ this is a test
/*. */
printf(«XYZ»);
выведет не «this is a test», a «XYZ».
Если строка не вмещается в одной строке, то ее можно продолжить на следующей строке, поместив в конце строки обратный слэш, как показано в следующем примере:
#define LONG_STRING «This is a very long» \
string that is used as an example.»
Программисты, пишущие на С, часто используют заглавные буквы для определения идентификаторов. Данное соглашение помогает любому человеку, читающему программу, бросив на нее один взгляд, узнать, что он имеет дело с макросом. Также вce #define лучше помещать в начале файла или вообще в отдельный заголовочный файл.
Очень часто макросы используют для определения «магических чисел», используемых в программе. Например, программа может определять массив и иметь несколько процедур для работы с ним. Вместо того, чтобы жестко кодировать размер массива, лучше определить макрос, соответствующий размеру массива, и использовать его в тех местах, где необходимо использование размера. Таким образом, если необходимо изменить размер массива, единственное, что требуется сделать, — это изменить оператор #define и перекомпилировать программу. Везде, где использовался данный макрос, произойдут автоматические изменения. Рассмотрим пример:
#define MAX_SIZE 100
/*. */
float balance[MAX_SIZE];
/*. */
float temp[MAX_SIZE];
Для изменения размеров обоих массивов просто изменим определение MAX_SIZE.
Директива #define имеет еще одну возможность: макрос может иметь аргументы. Каждый раз при встрече такого макроса аргументы макроса будут замещаться реальными аргументами программы. Такой тип макроса называется макрос типа функция. Например:
Из-за способа подстановки данная программа работает неправильно. В результате компиляции программы EVEN(9 + 1) расширится до
Как известно, оператор взятия по модулю имеет более высокий приоритет, чем оператор сложения. Это означает, что сначала выполнится взятие по модулю с числом 1, а затем результат прибавится к 9, что, естественно, не может быть равно 0. Для устранения данной проблемы следует заключить а в макросе EVEN в круглые скобки, как показано в следующей правильной версии программы:
Обратим внимание, что 9+1 вычисляется до взятия по модулю. В целом заключение параметров макроса в скобки — это достаточно хорошая идея, и она позволяет избежать множества проблем.
Использование макроподстановок вместо реальных функций имеет одно большое преимущество — существенно увеличивается скорость работы программы, поскольку нет необходимости тратить время на вызов функции и возврат из нее. Тем не менее, за данное увеличение скорости работы следует платить увеличением размера исполнимого кода программы, поскольку программа вынуждена дублировать код макроса.
Директивы препроцессора
Препроцессор
Процесс компиляции прошивки очень непростой и имеет несколько этапов, один из первых – работа препроцессора. Препроцессору можно давать команды, которые он выполнит перед компиляцией кода прошивки: это может быть подключение файлов, замена текста, условные конструкции и некоторые другие вещи. Также у препроцессора есть макросы, которые позволяют добавлять в код некоторые интересные вещи.
#include – подключить файл
Также можно указать путь к файлу, который нужно подключить. Например у нас в папке со скетчем есть папка libs, а в ней – файл mylib.h. Чтобы подключить такой файл, пишем:
Компилятор будет искать его в папке со скетчем, в подпапке libs.
#define / undef
Или быстрого и удобного отключения отладки в коде:
Или даже задефайнить целый кусок кода, используя переносы и обратный слэш
Если DEBUG задефайнен, то DEBUG_PRINT – это макро-функция, которая выводит значение в порт. А если не задефайнен – все вызовы DEBUG_PRINT просто убираются из кода и экономят память!
Если DEBUG_ENABLE задефайнен – все вызовы DEBUG() в коде будут заменены на вывод в порт. Если не задефайнен – они будут заменены НИЧЕМ, то есть просто “вырежутся” из кода! Также по DEBUG_ENABLE можно запустить сериал и получить полный контроль над отладкой: если она не нужна – убрали DEBUG_ENABLE и из кода убрался запуск порта и все выводы, что резко сокращает объём занимаемой памяти:
Проблемы
На этом сложности не заканчиваются: #define из одной библиотеки может пролезть в другую библиотеку, которая подключена после первой! Вернёмся к тому же примеру с DarkMagenta – если в моей библиотеке я задефайню это слово и подключу библиотеку до подключения FastLED – я получу ошибку компиляции! Если поменять подключение местами – ошибки не будет. Но, если я захочу использовать DarkMagenta в своём скетче, я буду неприятно удивлён =)
Что я хочу сказать в итоге: #define – гораздо более мощный инструмент, чем может показаться на первый взгляд. Использование define с невнимательным отношением к именам может привести к ошибке, которую будет непросто отловить. Это палка о двух концах: с одной стороны хочется использовать в своей библиотеке define, чтобы никто другой случайно не пролез со своими дефайнами. В то же время, своя библиотека может начать конфликтовать с другими библиотеками. Какой тут выход? Очень простой! Делать имена дефайнов максимально уникальными: если это библиотека – оставлять префикс библиотеки, если это скетч – делать префикс с именем скетча. Также можно отказаться от define в пользу констант или enum, enum кстати удобнее define в плане создания набора констант, а места занимает совсем немного!
#if – условная компиляция
Условная компиляция является весьма мощным инструментом, при помощи которого можно вмешиваться в компиляцию кода и делать его очень универсальным как для пользователя, так и для железа. Рассмотрим директивы условной компиляции:
При помощи условной компиляции можно буквально включать и выключать целые части кода из компиляции, то есть из финальной версии программы, которая будет загружена в микроконтроллер. Рассмотрим несколько конструкция для примера: [fusion_accordion type=”” boxed_mode=”” border_size=”1″ border_color=”” background_color=”” hover_color=”” divider_line=”” title_font_size=”” icon_size=”” icon_color=”” icon_boxed_mode=”” icon_box_color=”” icon_alignment=”” toggle_hover_accent_color=”” hide_on_mobile=”small-visibility,medium-visibility,large-visibility” title=”Пример 1″ open=”no”] [/fusion_toggle][fusion_toggle title=”Пример 2″ open=”no”] [/fusion_toggle][fusion_toggle title=”Пример 3″ open=”no”] [/fusion_toggle][/fusion_accordion]
Сообщения от компилятора
#pragma
Указывает компилятору, что данный файл нужно подключить только один раз. Является более удобной и современной заменой конструкции вида
Такую конструкцию вы можете встретить в 99% библиотек, файлов ядра и вообще заголовочников с кодом.
Конструкция с #pragma pack и #pragma pop позволяет более рационально распределять структуры в памяти. Тема сложная, читайте на Хабре.
Макросы
У препроцессора есть несколько интересных макросов, которыми можно пользоваться в своём коде. Рассмотрим некоторые полезные из них, которые работают на Arduino (точнее, на компиляторе avr-gcc).
__func__ и __FUNCTION__
Макросы __func__ и __FUNCTION__ “возвращают” в виде символьного массива (строки) название функции, внутри которой они вызваны. Являются аналогом друг друга. Например:
__DATE__ и __TIME__
__DATE__ возвращает дату компиляции по системному времени в виде символьного массива (строки) в формате __TIME__ возвращает время компиляции по системному времени в виде символьного массива (строки) в формате ЧЧ:ММ:СС
Работать напрямую с этим макросом очень неудобно, это ведь просто набор символов. У меня есть библиотека buildTime, которая позволяет получать отдельно каждый параметр (день, месяц, год, часы, минуты, секунды). Скачать/почитать можно здесь.
__FILE__ и __BASE_FILE__
__FILE__ и __BASE_FILE__ возвращают полный путь к текущему файлу, опять же как строку. Являются аналогами друг друга.
__LINE__
__LINE__ возвращает номер строки в документе, в которой вызван этот макрос
__COUNTER__
__COUNTER__ возвращает значение, начиная с 0. Значение __COUNTER__ увеличивается на единицу с каждым вызовом макроса в коде.
__COUNTER__ можно использовать для генерации уникальных имён переменных, но об этом мы поговорим когда нибудь в другой раз.
Препроцессор языка С
В компилятор языка программирования 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» не должно содержаться переменных, значение которых определяется в момент выполнения программы.
Придумайте и напишите программу, которая может быть скомпилирована по-разному в зависимости от того, определена или нет в ней какая-либо символьная константа.
Константы, определенные препроцессором
Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени.
Если эти константы встречаются в тексте программы, то заменяются на соответствующие строки или числа. Т.к. это происходит до компиляции, то, например, мы видим дату компиляции, а не дату запуска программы на выполнение. Программа ниже выводит значение предопределенных препроцессором имен на экран:
Урок №22. Директивы препроцессора
Обновл. 11 Сен 2021 |
Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы, препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа # и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.
Директива #include
Вы уже видели директиву #include в действии. Когда вы подключаете файл с помощью директивы #include, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строки с #include. Это очень полезно при использовании определенных данных (например, предварительных объявлений функций) сразу в нескольких местах.
Директива #include имеет две формы:
Директива #define
Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.
Есть два основных типа макросов: макросы-функции и макросы-объекты.
Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.
Макросы-объекты можно определить одним из следующих двух способов:
#define идентификатор текст_замена