Директива #define для определения макросов-функций. Операции # и ##

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

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

#include <stdio.h>
 
#define SQ_PR(A, B)   A * B
 
int main(void)
{
    int res = SQ_PR(2, 3);
    printf("res = %d\n", res);
 
    return 0;
}

Получаем, своего рода, макрос-функцию, которая формирует результаты в зависимости от переданных значений в параметры A и B. Давайте разберемся детально, как это работает, и увидим, какие потенциальные ошибки таит в себе приведенное макроопределение.

Итак, при вызове макроса SQ_PR(2, 3) со значениями 2 и 3, параметр A будет представлять собой число 2, а параметр B – число 3. Обратите внимание, параметры A и B – это не переменные, они не хранят какие-либо значения. Это лишь некоторое условное обозначение первого и второго параметров макроса SQ_PR. Когда мы передаем 2 и 3, то вместо A появляется 2, а вместо B – 3. А далее, в теле макроса указывается формат подставляемого выражения для этого макровызова. В нашем примере, получим «2 * 3». Именно на такую конструкцию будет заменен макровызов SQ_PR(2, 3).

А вот если для примера передать значения:

int res = SQ_PR(x + 2, y - 3);

то в процессе макровызова будет подставлено выражение:

int res = x + 2 * y - 3;

Видите, как прямолинейно действует макропроцессор? Он ничего предварительно не вычисляет и не анализирует, а просто буквально берет то, что мы указываем и подставляет вместо A и B. И в результате можем получить на выходе не то, что ожидали. Поэтому в практике определения макросов-функций каждый параметр заключают в круглые скобки, а потом еще и все выражение. Например, так:

#define SQ_PR(A, B)   ((A) * (B))

Тогда последний пример даст корректное выражение:

int res = ((x + 2) * (y - 3));

Хотя и такая запись макроса не защищает нас от всех возможных ошибок при его вызове. Но это самое разумное, что можно сделать. Поэтому макроопределения, строго говоря, вполне могут таить в себе потенциальные ошибки и их использование на практике следует избегать. Кстати, это еще одна причина, по которой имена макросов прописывают заглавными буквами. Они нам, как бы говорят: внимание, здесь может скрываться ошибка!

Однако макроопределения прочно вошли в практику программирования на Си и отказаться от них теперь совсем непросто. Да и простые макросы в виде целых чисел, символов или строк вполне удобны и безопасны в использовании.

Операции # и ##

В заключение этого занятия рассмотрим две операции # и ##, которые иногда используют при определении макросов.

Первая операция # возвращает текстовое представление лексемы, например, параметра:

#include <stdio.h>
 
#define SQ_PR(A, B)     ((A) * (B))
#define TEXT(A, B)     "Square of rectangle (" #A ") x (" #B ")\n"
 
int main(void)
{
    int res = SQ_PR(2, 3);
    printf(TEXT(x-2, y-3));
 
    return 0;
}

После выполнения программы увидим строку:

Square of rectangle (x-2) x (y-3)

Обратите внимание, программа была успешно скомпилирована и выполнена, несмотря на то, что в ней нет объявлений для x и y, которые были указаны в макровызове TEXT(x-2, y-3). И в этом нет никакой магии. Как я уже говорил, макропроцессор выполняет обработку текста программы до синтаксического анализатора и до перевода программы непосредственно в машинный код. Поэтому фрагмент TEXT(x-2, y-3) заменяется на строку «Square of rectangle (x-2) x (y-3)\n», которая компилируется без каких-либо проблем.

А теперь, как она работает. В макро-функции TEXT(A, B) у нас два параметра, а затем, идет тело макроса в виде пяти фрагментов строки. Первый фрагмент – это буквально та строка, что прописана. Второй фрагмент #A формируется из представления параметра A, а он в нашем примере представляет собой выражение x-2. И это выражение преобразуется в обычную строку. И далее все остальные фрагменты. Все они соединяются в одну строку и формируют конечный результат макровызова TEXT(x-2, y-3).

Вторая операция ## работает аналогичным образом, только служит не для склейки строковых представлений, а исходных лексем – самих выражений. Например:

#include <stdio.h>
 
#define SQ_PR(A, B)     ((A) * (B))
#define TEXT(A, B)      "Square of rectangle (" #A ") x (" #B ")\n"
#define X_N(N)          x ## N
 
int main(void)
{
    int x1 = 1, x2 = -2, x4 = 10;
    printf("%d\n", X_N(4));
 
    return 0;
}

Смотрите, в программе мы объявили три переменных с именами x1, x2 и x4. А, затем, выводим значение переменной x4, используя макровызов X_N(4). Почему это сработало? В теле макро-функции X_N прописаны две лексемы: x и N, причем, вместо N подставляется переданное выражение 4. В итоге, вызов X_N(4) объединяет в единую лексему x4 выражение x и 4 и это эквивалент того, что мы в программе вместо X_N(4) прописали выражение x4, которое соответствует ранее объявленной целочисленной переменной. Вот принцип работы операции ## в макроопределениях.

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

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

Видео по теме