Макрофункции
В среде программистов, давно пишущих на С, существует тенденция писать макросы вместо функций для очень коротких вычислений, которые будут часто вызываться: операции ввода-вывода, такие как getchar, и проверки символов вроде isdigit – это, так сказать, официально утвержденные примеры. Причина – в производительности: у макросов нет "накладных расходов", которые свойственны вызовам функций.
На самом деле этот аргумент был не слишком убедительным уже в те времена, когда С только появился, – в эпоху медленных машин и "дорогих" вызовов функций; теперь же он просто нелеп. Для современных машин и компиляторов недостатки макрофункций перевешивают их достоинства.
Избегайте макрофункций.
В C++ встраиваемые (inline) функции делают использование макрофункций ненужным; в Java макросов вообще не существует. В С они больше проблем создают, чем решают.
Одна из наиболее серьезных проблем, связанных с макрофункциями: параметр, который появляется в определении более одного раза, может быть вычислен также более одного раза; если же аргумент вызова включает в себя выражение с побочными эффектами, то результатом будет трудно отлавливаемая ошибка. В приведенном коде сделана попытка самостоятельно реализовать одну из проверок символов <ctype.h>:
? << define isupper(c) ((с) >= 'A' && (с) <= 'Z')
Обратите внимание на то, что параметр с дважды появляется в теле макроса. Если же наш макрос isupрег вызывается в контексте вроде такого:
? while (isupper(c = getchar())) ?
То каждый раз, когда вводимый символ будет больше или равен А, он будет пропущен, и для сравнения с Z будет считан еще один символ. Стандарт С написан таким образом, чтобы функции, аналогичные isupper, можно было реализовать как макросы, но только если они гарантируют, что аргумент будет вычисляться лишь единожды, так что приведенная выше реализация не отвечает требованиям стандарта.
Всегда лучше использовать уже имеющиеся функции ctype, чем реализовывать их самостоятельно; кроме того, функции с побочными эффектами, типа getchaг, лучше не применять внутри составных выражений. Если разбить условие цикла на два выражения, то оно будет более понятно для читателя и, кроме того, даст возможность четко отследить конец файла:
while ((с = getchar())!= EOF && isupper(c))
Иногда многократные вычисления не несут в себе прямых ошибок, но снижают производительность.
Рассмотрим такой пример:
Вычисление квадратного корня будет производиться в два раза чаще, чем требуется (он передается как аргумент, который дважды участвует в вычислении). Даже при задании простого аргумента сложное выражение вроде ROUND_TO__INT преобразуется во множество машинных команд, которые лучше хранить в одной функции, вызываемой при необходимости. Обращения к макросу увеличивают размер скомпилированной программы. (Встраиваемые (inline) функции в C++ имеют тот же недостаток.)
Заключайте тело макроса и аргументы в скобки.
Если вы все же решили использовать макрофункции, будьте с ними осторожны. Макрос работает за счет простой подстановки текста: параметры в описании заменяются аргументами вызова, и результат (текст!) замещает собой текст вызова. В этом состоит важное отличие макросов от функций, делающее макросы столь ненадежными. Так, выражение:
1 / square(x)
Будет работать отлично, если square – это функция, однако если это макрос вроде следующего:
? << define square(x) (x) * (х)
То выражение будет преобразовано в ошибочное:
? 1 / (х) * (х)
Этот макрос надо переписать так:
<< define square(x) ((x) * (х))
Скобки здесь необходимы, однако даже грамотная расстановка скобок не спасет макрос от его главной беды – многократного вычисления аргументов. Если операция настолько сложна или настолько часто повторяется, что ее стоит вынести в отдельный блок, используйте для этого функцию.
В C++ встраиваемые (inline) функции позволяют избежать синтаксических проблем, сохраняя при этом высокую производительность, присущую макросам. Они хорошо подходят для коротких функций, которые получают или устанавливают только одно значение.
Упражнение 1.9
Определите все проблемы, связанные с приведенным описанием макроса:
? << define ISDIGIT(c) ((с >= '0') && (с <= '9'))? 1: 0