Доброго времени суток, случайные и не случайные читатели.
На прошлой неделе, изучая код ядра Linux, я наткнулся на довольно занятный заголовочный файл. В файле объявлено два макроса и одна inline-функция. Заинтересовало же меня следующее: название одного из макросов совпадает с именем функции, равно как и набор их параметров. Я подумал -- как будет работать этот код? И решил провести эксперимент, как только появится свободное время.
На следующий день я вспомнил про этот файл -- и понял, что результат компиляции очевиден и без проведения эксперимента: достаточно вспомнить, как работает компилятор gcc.
Первым начинает работу препроцессор, который убирает комментарии из кода, обрабатывет адресованные ему директивы, разворачивает макросы и подставляет на место их вызова результат обработки.
В результате работы препроцессора красивые (с точки зрения программиста) макросы исчезают, и на их месте оказывается некий код, который может достаточно громоздким и неудобным для восприятия человеком.
После этого происходит компиляция, потом -- преобразование ассемблерного кода в машинный код и, наконец, линковка.
На этапе линковки можно увидеть (конечно же) ошибки линковки. Эти ошибки обычно сводятся к тому, что линковщику не удаётся найти в данных ему файлах функцию, на которую есть ссылка где-то в коде (undefined reference).
Если же всё прошло нормально, то в конце-концов мы получаем исполняемый файл.
Из этого алгоритма видно, что препроцессору, в общем-то, совершенно безразличны проблемы компилятора и, тем более, линковщика.
Посмотрим на примере.
Итак, допустим, у нас есть заголовочный файл и файл с исходным кодом -- назовём их test.h и test.c соответственно. Вот содержимое test.h:
#ifndef __TEST_H__
#define __TEST_H__
inline int function1(int param)
{
return param;
}
#define function1(param) function1(param)
#endif /* ifndef __TEST_H__ */
Кстати говоря, имена макросам в C принято давать в верхнем регистре, чтобы их можно было легко отличить от обычных функций и переменных. В данной же ситуации мы намеренно отступаем от этого соглашения.
А вот содержимое test.c:
#include <stdio.h>
#include "macros.h"
int main(int argc, char *argv[])
{
printf("Result of calling function1: %d\n",
function1(100));
return 0;
}
Результат работы препроцессора можно увидеть, передав gcc ключ -E, вот так:
$ gcc -E test.c -o test.i
Не буду приводить здесь этот файл test.i полностью (он достаточно объёмен) -- посмотрим лишь на последние 15 строк:
$ tail -15 test.i
inline int function1(int param)
{
return param;
}
# 3 "test.c" 2
int main(int argc, char *argv[])
{
printf("Result of calling function1: %d\n",
function1(100));
return 0;
}
Здесь мы видим, что в main() всё так же вызывается function1(), однако этот вызов был получен в результате работы препроцессора: он нашёл макрос function1, потом встретил имя function1 в main(), и подменил его результатом обработки макроса.
Попробуем сделать следующее: поменяем макрос function1() в test.h таким образом, чтобы он разворачивался в вызов несуществующей функции function2():
#define function1(param) function2(param)
Теперь, после работы препроцессора мы увидим следующую картину:
$ tail -15 test.i
inline int function1(int param)
{
return param;
}
# 3 "test.c" 2
int main(int argc, char *argv[])
{
printf("Result of calling function1: %d\n",
function2(100));
return 0;
}
Видно, что препроцессор совершенно не обратил внимание на то, что функция function2() нигде не объявлена -- он просто подменил макрос на результат его обработки. Об этой возмутительной оплошности нам скажет линковщик:
$ gcc test.c
/tmp/ccUBRByP.o: In function `main':
test.c:(.text+0x19): undefined reference to `function2' collect2:
выполнение ld завершилось с кодом возврата 1
Из этого можно сделать интересный вывод: если у нас существует функция и макрос с одинаковым именем и набором параметров, то нам не удастся вызвать функцию напрямую -- её вызов прежде будет обработан препроцессором, который подставит в место вызова результат обработки макроса. Таким образом, функция может быть "скрыта" от компилятора, или подменена. Например, поменяв макрос следующим образом:
#define function1(param) 0
И скомпилировав программу, мы увидим следующее:
$ gcc test.c
$ ./a.out
Result of calling function1: 0
Мы получили ноль, несмотря на то, что передавали в функцию число 100, так как вызов функции был принят препроцессором за вызов макроса, и заменён на тело макроса -- то есть, на ноль.
Итак, с помощью препроцессора можно делать занятные вещи -- например, подменить функцию (можно сказать, под носом у компилятора), как в рассмотренном примере. Использование же макросов может сделать код как более читаемым, так и совершенно запутать его -- достаточно вспомнить, что многие работы, участвовавшие в IOCCC, активно используют макросы.