Директивы макропроцессора #define и #undef

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии мы вспомним о текстовом препроцессоре, который выполняет некоторое преобразование текста программы до ее компиляции. Нас, главным образом, будет интересовать работа макропроцессора, который выполняет команды, прописанные непосредственно в тексте программы. О чем здесь идет речь? Смотрите. Практически в любой программе на языке Си имеются, так называемые, макродирективы (или, попросту, директивы). Все они начинаются со специального символа #, за которым следует имя директивы. Например, в наших программах постоянно фигурировала макродиректива с именем include:

#include <stdio.h>

Это одна из команд для макропроцессора. О ней мы подробнее еще будем говорить, но в двух словах, как только текстовый препроцессор встречает в тексте программы эту команду, он на ее место вставляет содержимое файла (в нашем примере stdio.h). Сама же макродиректива удаляется из текста программы и на компилятор поступает программа, состоящая только из конструкций языка Си.

Все директивы в тексте программы принято писать с начала строки без отступов. Хотя, стандарт C99 разрешает это делать, в частности, ставить пробельные символы перед символом # и даже после него. Но все же лучше по возможности этого избегать.

Знакомство с командами макропроцессора мы начнем с директивы define, которая позволяет определять в программе свои собственные макроопределения (или, как часто говорят, макросы).

В простейшем случае синтаксис для описания макроопределения имеет следующий вид:

#define <макроимя> [тело макроса]

И, как правило, записывается в пределах одной строки. Строго говоря, обычный перенос строки будет означать завершение описания макроса. Хотя, при необходимости, это правило можно обойти.

Чтобы было понятно, как работает директива define, приведу конкретные примеры макроопределений:

#define MENU_TRANSL 1
#define MENU_ADD    2
#define MENU_EXIT   3
#define MENU_FMT    "You have selected menu item %d\n"
#define FIRST_LETTER    'a'

Как видите, макроимена принято записывать заглавными буквами, хотя формально можно использовать и малые. Но лучше придерживаться этой традиции, чтобы выделять макросы на фоне остальных конструкций языка Си.

В приведенном примере, имена MENU_TRANSL, MENU_ADD, MENU_EXIT связаны с целыми числовыми литералами 1, 2 и 3. Имя MENU_FMT – со строкой, а макрос FIRST_LETTER – с символом. Обратите внимание, в конце я нигде не прописывал точку с запятой. Этого не требуется. Как я уже говорил, описание макроса заканчивается с переходом на новую строку.

Итак, мы в программе ввели несколько новых макроимен. Как дальше всем этим можно воспользоваться и что это нам дает? Помните, я приводил пример реализации меню с оператором switch. Вот краткий пример:

#include <stdio.h>
 
int main(void)
{
    int item;
 
    scanf("%d", &item);
 
    switch(item) {
    case 1:
        printf("Translate words\n");
        break;
    case 2:
        printf("Add words\n");
        break;
    case 3:
        printf("Exit\n");
        break;
    default:
        printf("Incorrect menu item\n");
    }
 
    return 0;
}

Здесь в качестве меток после ключевых слов case можно использовать целые числа или константы времени компиляции. Как раз макроопределения в define вполне можно использовать и заменить числовые литералы более читаемыми именами:

    switch(item) {
    case MENU_TRANSL:
        printf("Translate words\n");
        break;
    case MENU_ADD:
        printf("Add words\n");
        break;
    case MENU_EXIT:
        printf("Exit\n");
        break;
    default:
        printf("Incorrect menu item\n");
    }

Это один маленький пример, где полезны макроопределения. На самом деле, как я уже говорил, в тексте программы нужно избегать использования конкретных чисел, так как они, во-первых, сложнее воспринимаются человеком, а, во-вторых, при изменении хотя бы одного числа, его придется искать и менять во всем проекте, что очень неудобно. Куда проще определить макроопределение и записывать его в тексте программы. Такая программа лучше воспринимается человеком и проще модифицируется.

Вернемся к нашему примеру с меню. После того, как мы прописали макросы в операторе switch, они стали не чем иным, как макровызовами. Это значит, что на этапе обработки текста препроцессором (до непосредственной компиляции программы в машинный код) все эти макровызовы будут преобразованы макропроцессором в заданные нами определения. Например, макрос MENU_TRANSL определен как целое число 1. Значит, вместо него макропроцессор подставит это значение. И также поступит с именами MENU_ADD и MENU_EXIT. В итоге, дальше на компиляцию поступит программа без этих текстовых определений, а с конкретными числами.

В общем случае мы можем задавать и использовать макросы следующими способами:

#include <stdio.h>
 
#define FIVE    5
#define TEN     2 * FIVE
#define TEXT    "Text message in one line"
#define TEXT2   "Text message in \
two line"
#define PRINT_D  printf("digit = %d\n", digit)
#define FORMAT  "digit = %d\n"
 
int main(void)
{
    int digit = FIVE;
    PRINT_D;
    
    digit = TEN;
    printf(FORMAT, digit);
    printf(TEXT "\n");
    printf(TEXT2 "\n");
    
    return 0;
}

Вначале прописаны несколько макроопределений, а затем, они используются в программе в функции main(). В частности, макровызов FIVE подставляет число 5 на свое место. Вместо PRINT_D будет подставлено определение «printf("digit = %d\n", digit)». Обратите внимание, макропроцессор не выполняет синтаксического разбора текста программы, он работает с информацией на уровне текста. Поэтому для него запись «printf("digit = %d\n", digit)» - это всего лишь строка, которую следует подставить на место макровызова PRINT_D и не более того. И так со всеми остальными макроопределениями. Вместо TEN будет подставлено выражение «2 * 5» без каких-либо вычислений, именно в таком виде, вместо FORMAT строка "digit = %d\n" и так далее.

А теперь, внимание, вопрос. Что если мы пропишем функцию printf() следующим образом:

printf("FORMAT\n");

Будет ли здесь вместо FORMAT подставлено содержимое макроса FORMAT? На самом деле нет. Это единственное исключение, когда макропроцессор пропускает макроимена, записанные внутри строк. Поэтому функция printf() просто выведет строку  «FORMAT» в консоль. И так с любыми макроименами, прописанными внутри любых строк.

Думаю, излишне говорить, что макровызовы следует делать после макроопределений. Именно поэтому, директивы #define, как правило, прописываются вначале программы. Это гарантирует, что во всем тексте можно без ограничений прописывать соответствующие макросы и позволяет легко находить место их определения.

Директива #undef

Также, разумеется, если какой-либо макрос ранее был определен, то переопределить его с новым поведением уже не получится. Если все же это требуется сделать, то сначала нужно отменить прежний макрос и прописать новый. Для отмены макроопределений используется директива #undef, например, так:

#define TEN     2 * FIVE
#undef TEN
#define TEN     10

Я думаю, вы теперь хорошо себе представляете, как можно прописывать простые макросы и использовать их в программах.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме