Лицензия Creative Commons
Содержимое блога доступно по лицензии Creative Commons Атрибуция — С сохранением условий
(Attribution-ShareAlike) 3.0 Unported
, если не указано иное.

понедельник, 26 ноября 2012 г.

Препроцессор C

Доброго времени суток, случайные и не случайные читатели.

На прошлой неделе, изучая код ядра 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, активно используют макросы.