Практический курс по 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);
то в процессе
макровызова будет подставлено выражение:
Видите, как
прямолинейно действует макропроцессор? Он ничего предварительно не вычисляет и
не анализирует, а просто буквально берет то, что мы указываем и подставляет
вместо 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