6. Макроопределения с параметрами и без них

6.1. Еще раз о директиве #include

Мы уже использовали директиву #include для включения в текст нашей программы заголовочных файлов. У детей может сложиться впечатление, что только заголовочные файлы посредством директивы #include и подключают. На самом деле это не так (точнее, обычно именно так ее и используют, но смысл не состоит в подключении именно заголовочных файлов). Эта директива просто включает текст из указанного в ней файла в данный. (В Паскале имеется аналог — {$i}.) Следует обязательно обратить внимание на то, что недаром #include называют директивой препроцессора. Смысл этого "пре" в том, что эта директива (равно как и другие, с которыми мы далее познакомимся) выполняется до того, как текст программы поступает на вход компилятора.

Замечание. На самом деле в различных реализациях Си механизм препроцессора реализован по-разному. Но удобно представлять себе его именно так, как описано выше (и так же объяснять детям): сначала над текстом программы работает препроцессор (обрабатывает директивы, начинающиеся со знака #), а потом то, что получилось, поступает на вход компилятора.

Это, кстати, хорошо согласуется с тем, что директивы препроцессора могут располагаться только на внешнем уровне, "внутрь" функций препроцессор "не лезет", а компилятор, в свою очередь, директив препроцессора не понимает.

Приведем короткий пример, поясняющий сказанное, Мы уже выносили заголовки функций в отдельный файл. Можно довести ситуацию до абсурда и вообще все функции разнести по отдельным файлам:

Файл dayftmc.h

int DayInRussian(int) ;

int DayInEnglish(int);

Файл main.с

vo.id main(void)
{ <...> }

Файл russian.c

int DayInRussian(int n)
{ <...> }

Файл english.c

int DayInEnglish(int n)
 
{ <...> }

И, наконец, файл program.с, содержащий только одни лишь директивы #include:

#include <stdio.h>

#include "dayfunc.h"

#include "main.c"
#include "russian.c"

#include "english.c"

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

6.2. Директива #define без параметров

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

#include <stdio.h>
#define N 10
 void main(void)
  { int i;

     for (i=0;i<N;i++) printf("\nПривет, мир!");

}

Обычно у детей возникает ряд вопросов (у тех, кто программировал на Паскале и использовал const, вопросов бывает меньше), кроме того, они делают ряд типичных ошибок. Итак, по порядку..

Неужели нельзя было просто написать i<10?

В этом примере, конечно, можно. Но представим себе другую ситуацию (пока именно представим, ибо соответствующие примеры имеются ниже). Пусть мы выполняем рисование на экране посредством некоторой "черепашки", которая передвигается с фиксированным шагом step (заданным в пикселях). Сколько раз встретится в такой программе значение шага? Уж наверняка не один раз. И не два, и не три... А если мы захотим изменить значение шага? Придется "лазить" по всей программе и искать соответствующие значения. А если мы снова захотим изменить это значение...

Кроме того, с помощью #define обычно определяются "популярные" константы (пи, е и т.п.). Ведь даже если каждая такая константа в программе используется единожды (что маловероятно), написать

#define PI 3.14159265358979323846

и использовать в самой программе имя PI гораздо удобнее, чем записывать числовое значение непосредственно в программе.

Замечание, Опять кто-то тянет руку? Да, да, конечно! Все "популярные" числовые константы и так уже определены в заголовочном файле math.h. Чтобы узнать их имена, можно посмотреть на содержимое файла. Число пи в нем определено константой М_Р1, число е — константой М_Е. Почему перед ними стоит префикс "М_"? Достоверно не знаем, наверное, потому, что они находятся в math.h.

Откуда берутся ошибки Expression syntax in function ... ?

Вообще говоря, откуда угодно, но имеется типичная ситуация, связанная с #define, которая вызывает такие ошибки: точка с запятой после директивы. Вот так:

#define N 10;

Здесь самое время обсудить очень важный вопрос, который является ключевым в понимании механизма работы директивы #define без параметров. На самом деле эта директива, которая (обратите внимание!) является директивой препроцессора (т.е. выполняется до компиляции программы), выполняет простую контекстную замену первого своего параметра на второй. Конечно, заменяются лишь отдельные лексемы — отдельные слова, проще говоря. Переменная Nsymb не превращается в l0symb, а вот отдельно стоящее слово N превращается. То есть в нашем примере на вход компилятора поступает не строка

for (i=0;i<N;i++) printf("\nПривет, мир!");

а строка

for (i=0;i<10;i++) printf("\nПривет, мир!");

Теперь должно быть понятно, что если мы поставим после #define точку с запятой, то получим ровно то, "что хотели":

for (i=0;i<10;;i++) printf("\nПривет, мир!");

Но здесь-то и кроется ошибка!

Последнее, на чем мы хотим остановиться, — область действия #define. Директива действует с точки своего появления и до конца файла. То есть фактически она выполняет команду редактора "заменить все ниже курсора". Только курсор — она сама.

6.3. Директива #define с параметрами. Макроподстановка

Выше мы рассмотрели директиву препроцессора #define без параметров и выяснили, что с ее помощью выполняется обычная контекстная замена. Часто используется также "другой #define" — директива #define с параметрами. Как всегда, приведем сначала простой пример. В отличие от Паскаля в Си нет функции sqr (возведение в квадрат), хотя для записи некоторых выражений (например, формулы расстояния между двумя точками в пространстве) эта функция очень пригодилась бы. Конечно, написать такую функцию совсем просто, но в Си такие задачки обычно решаются посредством макроподстановок: #define с параметрами. Итак, пример:

#include <stdio.h>
#define SQR(x) (x)
*(х)
void main(void)
 
{ float a;

printf ("\nВведите число:");scanf("%f",&a) ;

printf ("Его квадрат: %f\n",SQR(a));

}

Фактически директива #define с параметрами работает так же как и без параметров, - заменяет все вхождения своего первого аргумента ( в данном случае sqr (х) ) на второй аргумент (в данном случае (х) * (х) ). Особенность этой директивы в том, что параметры ее формальные (это видно и из приведенного примера). То есть заменяются не только записи SQR (х) , но и все записи вида SQR (выражение) на записи вида (выражение) * (выражение) , Приведем еще несколько типичных примеров использования директивы #define с параметрами:

#define ABS(х) ( (х) >=0) ? (х) : (-(х) )
#define МАХ(х,у) ( (х)>(у))?(х):(у)

Замечание. Опять кто-то тянет руку? Он наверняка хочет спросить: а зачем это мы так много скобок понаставили? Почему в первом примере не написать просто: # define SQR(x) x*x?

Такую запись не следует использовать, чтобы не наступить на самые известные "грабли", которые традиционно предлагает нам #define. Дело в том, что #define с параметрами ведь не функция! Это та же самая контекстная замена, хоть и с параметрами! Давайте опустим скобки:

#define SQR(x) x*x

и посмотрим, во что превратится запись SQR (1+2). Только сделайте на секунду паузу и задумайтесь, а потом читайте дальше...

Запись эта превратится в 1+2*1+2. Еще раз повторяем: #define с параметрами не функция, в нем не вычисляются параметры, а лишь "честно" выполняется контекстная замена. Когда мы ставим скобки, мы гарантируем, что выражение будет преобразовано ровно в то, что требуется (так, выражение SQR (х) в "правильном" #define с параметрами преобразуется в (1+2)* (1+2)),

Замечание. В примерах с использованием #define мы всегда использовали прописные буквы для констант и названий макросов и строчные для формальных параметров макросов. Придерживаться этого правила вовсе не обязательно, но обычно делают именно так. Так написаны и стандартные заголовочные файлы Си.

TopList