Что такое #define в C?
Если вы можете сказать, что код на показанном изображении не C, то поздравляю, потому что вы знаете более одного языка. Это достижение, которое вы должны отпраздновать и гордиться собой.
тем не менее, вернемся к основной теме. В языке C вы наверняка видели что-то подобное
#include <stdio.h>
#ifndef __MAIN__
#define __MAIN__
int main(){
printf("helllo world");
return 0;
}
#endif
вначале вас могут попросить игнорировать #define или другие ключевые слова, начинающиеся с символа #. Конечно, в начале вашего пути к изучению C можно игнорировать их и сосредоточиться непосредственно на том, что находится внутри функции main. но вы любопытное существо и вам интересно, что это такое, и зачем нам вообще их писать?
Что ж, я помогу вам ответить на эти вопросы. Но сначала давайте разберемся с контекстом языка программирования C.
C об оптимизации
C — это язык, разработанный специально для управления оборудованием. Вот почему большинство операционных систем, аппаратных драйверов и программного обеспечения для микроконтроллеров написаны на C. Одна вещь, которую вы должны учитывать при написании программного кода для аппаратного обеспечения, — это то, что вам нужно подумать об оптимизации, а C — это все.
Таким образом, чтобы оптимизировать окончательный двоичный файл программного обеспечения, C имеет препроцессор C. Препроцессор C, или мы будем называть его просто Препроцессор, по существу является инструментом, который анализирует исходный код C и выполняет включение заголовочного файла, раскрытие макросов, условную компиляцию и управление строками перед кодом. компилируется компилятором. Сам препроцессор является частью общего инструмента компилятора.
Условная компиляция, в частности, очень важна для оптимизации кода. мы увидим подробности позже, но в целом это позволяет нам скомпилировать определенные строки кода или исключить строки кода, которые не нужны для окончательного двоичного файла. Основной вариант использования этого — если у вас есть аппаратно-зависимый код. Например, при обработке данных вы поступаете по-разному, если целевое оборудование использует прямой или прямой порядок байтов. Если вы скомпилируете исходный код для оборудования с прямым порядком байтов, препроцессор может полностью удалить весь код, использующий обработку с прямым порядком байтов, перед компиляцией кода.
Команды препроцессора
Проще говоря, команды препроцессора — это ключевые слова, начинающиеся с #. Иногда их также называют директивами.
+-----------+---------------------------------------------+ | Macro | description | +-----------+---------------------------------------------+ | #include | for file inclusion | | #define | define a macro (the heck what is macro?) | | #undef | opposite of #define | | #ifdef | conditional if macro has been defined | | #ifndef | conditional if macro has not been defined | | #if | conditional if with argument | | #else | just else part | | #elif | it is 'else if' | | #endif | closure of if | | #error | to print error message | | #pragma | special command to computer | +-----------+---------------------------------------------+
Не так много, верно? Давайте обсудим их всех, основываясь на том, что они делают.
Включение файла
Мы не будем подробно говорить об этом. по сути, это просто способ импортировать другой файл в ваш исходный код, потому что вы хотите использовать функцию, константу или переменную из этого файла. Обычно вы включаете заголовочный файл, содержащий объявление функции, константы или переменной, которые экспортируются этим заголовком.
вы будете использовать директиву #include the, за которой следует имя заголовочного файла, заключенное в угловую скобку. Например, вы пишете #include <stdio.h>, чтобы использовать функцию printf, потому что printf определено внутри stdio.h. Иногда заголовочный файл заключен в двойные кавычки, например #include "someapi.h". Вы используете ", если ваш заголовочный файл является частью вашего исходного кода, а угловая скобка используется, если вы включаете стандартную библиотеку.
Определение и расширение макроса
Директивы #define и #undef используются для расширения макроса. Я всегда думаю об этом как о замене текста. Директивы #define создадут макрос и, возможно, токен замены.
#define <identifier> <replacement_token> //example #define PI 3.14
В приведенном выше примере мы объявляем макрос PI с токеном замены 3.14. Затем препроцессор будет искать в исходном коде макрос PI и заменит его на 3.14.
int area(int radius){
return PI*r*r;
}
========== after pre processing ========
int area(int radius){
return 3.14*r*r
}
в приведенном выше примере вы можете видеть, что после предварительной обработки препроцессор по существу просто заменяет любые PI на 3.14, вот и все.
Магия, однако, заключается в том, что вы также можете определить функцию с расширением макроса. наш пример выше с PI — это то, что называется объектно-подобным макросом, в то время как другой, функциональный макрос, — это то место, где вы можете объявить макрос как функцию.
#define <identifier>(parameters list) <replacement_token>
Давайте используем функцию area в качестве примера для объявления функционального макроса.
#define PI 3.14
#define area(r) (PI*r*r)
int main(){
int area1 = area(2);
return 0;
}
========== after preprocessing ========
int main(){
int area = 3.14*2*2;
return 0;
}
как вы видите в приведенном выше примере, весь макрос area будет заменен только операцией, которую мы объявляем в токене замены макроса. Так что, в конце концов, это действительно просто замена текста.
Однако, как я уже говорил, замена токена необязательна. Итак, в примере с нашей программой hello world в начале статьи мы объявляем макрос __MAIN__ с помощью #define, но без токена замены.
Когда вы объявляете макрос без маркера замены, препроцессор запоминает это и использует каждый раз, когда вы используете директиву #if. Мы обсудим это позже в следующем разделе.
Условная компиляция
Условная компиляция позволяет компилировать или исключать из компиляции определенные коды. Это очень удобно, когда вы хотите оптимизировать код. Мы можем использовать директивы #ifdef, #ifndef, #if, #elif, #else и #endif. давайте посмотрим на пример ниже, который я взял из Википедии: P
#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64 // we are compiling for a 32-bit system #else // we are compiling for a 64-bit system #endif
Вы можете спросить, а можем ли мы просто создавать функции для 32-битной и 64-битной отдельно и вызывать их в соответствии с системой? Что ж, приведенный выше пример может показать, насколько незначительна условная компиляция. Но помните, C написан для аппаратного обеспечения, и у вас могут быть сотни различных аппаратных конфигураций. Некоторый код может вызывать ошибки компиляции, если он скомпилирован для архитектуры ARM, но отлично работает в архитектуре x86.
Препроцессор по существу удалит весь код для 32-разрядных, если аргумент #if равен false, и наоборот для 64-разрядных. Это не только оптимизирует код, но также устраняет любые ошибки при компиляции, поскольку некоторые переменные или функции недоступны в другой архитектуре.
Строковое определение токена
Я думаю, что это очень редко используется. По своей работе я разрабатываю операционную систему для смарт-карт, и я никогда не сталкивался со строкой токенов. Но ладно, давайте немного поговорим об этом. Согласно Википедии, он просто создаст строковый литерал из макроса. вот и все.
Специальные директивы и макросы
#error просто выводит сообщение об ошибке на стандартный ввод-вывод, а #pragma заслуживает целой статьи об этом. #pragma специфичен для компилятора, но обычно мы находим его в распараллеливании с GPU, чтобы обеспечить параллельное выполнение определенного кода несколькими ядрами GPU.
Есть также несколько предопределенных макросов, которые поддерживаются большинством компиляторов. Это __FILE__, __LINE__, __DATE__, __TIME__, __TIMESTAMP__.
Как работает препроцессор
Теперь, когда мы знакомы с предварительной обработкой и макросом, мы увидим, как именно это работает, рассмотрев несколько примеров позже. так что подготовьте свой редактор кода и приготовьтесь написать код.

Как вы можете видеть на иллюстрации выше, предварительная обработка фактически является первым шагом компиляции. На самом деле это имеет смысл, потому что препроцессор сначала проанализирует код, чтобы оптимизировать его, прежде чем исходный код будет скомпилирован и обработан. Давайте попробуем написать код и посмотреть результат препроцессинга.
В этой демонстрации я буду использовать компилятор gcc. Я использую версию 7.5.0 в Ubuntu 18.04. рассмотрим образец ниже
#define THREE_COMMA_PI
#ifdef THREE_COMMA_PI
#define PI 3.142
#else
#define PI 3.14
#endif
#define area(r) (PI*r*r)
int main(){
int a = area(3);
return 0;
}
после этого сохраняем этот файл на helloworld.c в любую папку. После этого мы можем вызвать эту команду для создания предварительно обработанного файла.
gcc -c -save-temps helloworld.c
Эта команда создаст 3 файла: helloworld.i, helloworld.o и helloworld.s. если вы откроете helloworld.i, вы увидите что-то вроде этого
# 1 "helloworld.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "helloworld.c"
# 10 "helloworld.c"
int main(){
int a = (3.142*3*3);
return 0;
}
вы можете игнорировать строки с # перед ними, но вы не можете их игнорировать, вы можете прочитать эту статью. Если вы сосредоточитесь на функции main, вы увидите, что area уже заменено токеном замены, так же как и PI.
Заключение
То, что я описал выше, — это только поверхностная часть предварительной обработки кода C. Но, по крайней мере, я надеюсь, что это поможет вам узнать больше об этом.
Я думаю, что довольно интересно обсудить подробнее, почему вы используете макрос, похожий на функцию, а не просто пишете простую функцию. Дайте мне знать, если вы хотите, чтобы я написал об этом
Если вы считаете эту статью полезной, подписывайтесь на меня на Medium или в Твиттере, и, конечно же, не забывайте хлопать в ладоши.