Урок 19. Препроцессор языка С

Особенности языка С. Учебное пособие

В компилятор языка программирования C входит так называем препроцессор, который осуществляет подготовку программы к компиляции. Среди прочего он, например, включает содержимое одних файлов в другие, заменяет в тексте исходного кода имена констант на их значения, удаляет символы конца строки (которые нужны только программисту, чтобы код можно было легко читать, но не нужны компилятору). Что-то препроцессор делает по-умолчанию, а какие-то его действия программируются с помощью специальных директив в исходном коде. Директивы препроцессора начинаются со знака # и заканчиваются переходом на новую строку. В отличие от законченного выражения на языке C, в конце директив не надо ставить точку с запятой. Ниже рассматриваются наиболее распространенные директивы препроцессора и некоторые его свойства, но это далеко не все, что может делать препроцессор.

Директива #include

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

Если имя файла после директивы #include заключено в угловые скобки (например, ), то поиск заголовочного файла производится в стандартном (специально оговоренном системой) каталоге. Однако в тексте программы может встречаться и такая запись:

#include "ext.h"

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

#include "/home/iam/project10/const.h"

Директива #define

Символические константы

С директивой препроцессора #define мы также уже знакомы. С ее помощью объявляются и определяются так называемые символические константы. Например:

#define N 100
#define HELLO "Hello. Answer the next questions, please."

Когда перед компиляцией исходный код будет обработан препроцессором, то все символьные константы (в примере это N и HELLO) в тексте исходного кода на языке C будут заменены на соответствующие им числовые или строковые константы.

Символические константы можно определять в любом месте исходного кода. Однако чтобы переопределить их (изменить значение), необходимо отменить предыдущее определение. Иначе возникнет если не ошибка, то предупреждение. Для удаления символической константы используют директиву #undef:

#include <stdio.h>
 
#define HELLO "Hello. Answer the next questions, please.\n"
 
main () {
	printf(HELLO);
	#undef HELLO
	#define HELLO "Good day. Tell us about you.\n"
	printf(HELLO);
}

Если в этом примере убрать строку #undef HELLO, то при компиляции в GNU/Linux появляется предупреждение: "HELLO" переопределён.

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

Макросы как усложненные символьные константы

С помощью директивы #define можно заменять символьными константами не только числовые и строковые константы, но почти любую часть кода:

#include <stdio.h>
 
#define N 100
#define PN printf("\n")
#define SUM for(i=0; i<N; i++) sum += i
 
main () {
	int i, sum = 0;
 
	SUM;
	printf("%d", sum);
	PN;
}

Здесь в теле функции main() PN заменяется препроцессором на printf("\n"), а SUM на цикл for. Такие макроопределения (макросы) в первую очередь удобны, когда в программе часто встречается один и тот же код, но выносить его в отдельную функцию нет смысла.

В примере выше PN и SUM являются макросами без аргументов. Однако препроцессор языка программирования C позволяет определять макросы с аргументами:

#include <stdio.h>
 
#define DIF(a,b) (a) > (b) ? (a)-(b) : (b)-(a)
 
main () {	
	int x = 10, y = 30;	
 
	printf("%d\n", DIF(67,90));
	printf("%d\n", DIF(876-x,90+y));
}

Вызов макроса DIV(67,90) в тексте программы приводит к тому, что при обработке программы препроцессором туда подставляется такое выражение (67) > (90) ? (67)-(90) : (90)-(67). В этом выражении вычисляется разница между двумя числами с помощью условного выражения (см. урок 3). В данном случае скобки не нужны. Однако при таком разворачивании (876-x) > (90+y) ? (876-x)-(90+y) : (90+y)-(876-x) скобки подчеркивают порядок операций. Если бы вместо сложения и вычитания фигурировали операции умножения или деления, то наличие скобок было бы принципиальным.
Обратите внимание, что в имени макроса не должно быть пробелов: DIF(a,b). Первый пробел после идентификатора означает конец символической константы и начало выражения для подстановки.

Задание

  1. Напишите программу, содержащую пару макросов: один вычисляет сумму элементов массива, другой выводит элементы массива на экран.
  2. Напишите программу, содержащую макросы с аргументами, вычисляющие площади различных геометрических фигур (например, квадрата, прямоугольника, окружности).

Директивы условной компиляции

Так называемая условная компиляция позволяет компилировать или не компилировать части кода в зависимости от наличия символьных констант или их значения.

Условное выражение для препроцессора выглядит в сокращенном варианте так:

#if …#endif

То, что находится между #if и #endif выполняется, если выражение при #if возвращает истину. Находится там могут как директивы препроцессора так и исходный код на языке C.

Следует знать, что условное включение может быть расширено за счет веток #else и #elif.

Рассмотрим несколько примеров.

Если в программе константа N не равна 0, то цикл for выполнится, и массив arr заполнится нулями. Если N определена и равна 0, или не определена вообще, то цикл выполняться не будет:

#include <stdio.h>
 
#define N 10
 
main() {
	int i, arr[100];
 
	#if N
		for(i=0; i<N; i++) {
			arr[i] = 0;
			printf("%d ", arr[i]);
		}		
	#endif
 
	printf("\n");
}

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

#if defined(N)

Или сокращенно (что тоже самое):

#ifdef N

Когда нет уверенности, была ли определена ранее символьная константа, то можно использовать такой код:

#if !defined(N)
	#define N 100	
#endif

Таким образом мы определим константу N, если она не была определена ранее. Такие проверки могут встречаться в многофайловых проектах. Выражение препроцессора #if !defined(N) может быть сокращено так:

#ifndef N

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

Помните, что препроцессор обрабатывает программу до компиляции. В двоичном коде уже отсутствуют какие-либо условные выражения для препроцессора. Поэтому в логическом выражении "препроцессорного if" не должно содержаться переменных, значение которых определяется в момент выполнения программы.

Задание
Придумайте и напишите программу, которая может быть скомпилирована по-разному в зависимости от того, определена или нет в ней какая-либо символьная константа.

Константы, определенные препроцессором

Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени.

  • __DATE__ - дата компиляции;
  • __FILE__ - имя компилируемого файла;
  • __LINE__ - номер текущей строки исходного текста программы;
  • __STDC__ - равна 1, если компилятор работает по стандарту ANSI для языка C;
  • __TIME__ - время компиляции.

Если эти константы встречаются в тексте программы, то заменяются на соответствующие строки или числа. Т.к. это происходит до компиляции, то, например, мы видим дату компиляции, а не дату запуска программы на выполнение. Программа ниже выводит значение предопределенных препроцессором имен на экран:

#include <stdio.h>
 
#define NL printf("\n")
 
main () {
	printf(__DATE__); NL;
	printf("%d",__LINE__); NL;
	printf(__FILE__); NL;
	printf(__TIME__); NL;
	printf("%d",__STDC__); NL;
}
Результат:
Mar 22 2012 
7 
macronames.c 
10:07:04 
1