CHAP6&7 (1018807), страница 2
Текст из файла (страница 2)
Последняя строка в таблице 1 относится к другому спорному вопросу. Условная операция - это простой оператор. Она используется лишь в выражении и передает значение. Условная операция является не очень привычной заменой для оператора if/else, но не менее, чем операции && или || приемлемы для замены простого if. Хотя большинство людей и не принимают во внимание замену:
if ( z )
i = j;
else
i = k;
на:
z && (i = j);
z || (i = k);
Мне довелось случайно увидеть подобное этому, но с использованием условной операции:
z ? (i = j) : (i = k) ;
Все предыдущие фрагменты в равной мере способны сбить с толку. Следующий код показывает, как надлежащим образом использовать условную операцию, и ее результат яснее (т.е. лучше), чем у равноценного оператора if/else:
i = z ? j : k ;
81.2. Помещайте тело макроса и его аргументы в круглые скобки.
Это правило одно из основных, но я обнаружил, что множество людей, пользующихся С ежедневно, его забыли. Вот классическая задача:
#define TWO_K 1024 + 1024
что при использовании в:
10 * TWO_K
расширяется до:
10* 1024 + 1024
вычисляемого как:
(10 * 1024) + 1024
Решаем эту задачу при помощи круглых скобок:
#define TWO_K (1024 + 1024)
Вот сходная задача в следующем фрагменте кода:
#define SQUARE(x) (x * x)
Определено:
SQUARE(y + 1);
что расширяется макросом до:
y + 1 * y + 1
и вычисляется как:
y + (1 * y) + 1
И вновь круглые скобки приходят на помощь. Следующее определение:
#define SQUARE(x) ((x) * (x))
расширяется до:
((y + 1) * (y + 1))
82. enum и const лучше, чем макрос.
Директива #define должна быть вашим последним средством при определении значения константы. Рассмотрим следующую рассмотренную ранее распространенную ошибку:
#define TWO_K 1024 + 1024
x = TWO_K * 10
что в результате вычисления дает 11264 (1024+(1024*10)) вместо требуемых 20480. Определение перечисления типа:
enum { two_k = 1024 + 1024 };
или константы типа:
const int Two_k = 1024 + 1024;
не вызывает трудностей, связанных с макросом. И круглые скобки не требуются.
Перечисление enum на несколько очков превосходит константу: во-первых, определение const int в языке С на самом деле выделяет память под тип int и инициализирует ее. Вы не можете модифицировать эту область памяти, но память при этом занята. Следовательно, определение константы в С нельзя поместить в заголовочном файле; вы нужно будет воспользоваться модификатором extern как для какой-нибудь глобальной переменной. (В С++ все это несущественно, так как там память выделяется лишь тогда, когда вы определяете адрес константы или передаете его по ссылке. Определения констант в С++ могут - а на деле часто и должны - помещаться в заголовочном файле).
Перечисление отличается тем, что память для него никогда не выделяется. Подобно макросу, оно может вычисляться во время компиляции. Следовательно, при использовании вами перечисления не происходит потери производительности.
Второй проблемой является порча области глобальных имен. Область действия перечисления легко ограничивается. Например, в следующем фрагменте кода перечисление default_i действует лишь внутри функции f():
void f( int i )
{
enum { default_i = 1024 };
if ( !i )
i = default_i ;
}
В фрагменте:
void f( int i )
{
#define DEFAULT_I 1024
if ( !i )
i = DEFAULT_I ;
}
макрос DEFAULT_I виден всем функциям, чьи определения следуют после определения этого макроса. Если DEFAULT_I определяется в заголовочном файле, то он будет виден в нескольких файлах - даже если он не используется кодом в этих файлах. Та же самая проблема касается также константы, определенной на глобальном уровне.
Перечислитель enum особенно полезен в С++, потому что он может быть ограничен областью действия класса и инициализироваться в самом определении класса вместо конструктора. Эти вопросы рассматриваются далее в той главе книги, что посвящена правилам С++.
Наконец, перечислитель может быть использован в качестве аргумента оператора case и размера при объявлении массива. Ни в одной из указанных ситуаций константа использоваться не может.
83. Аргумент параметризированного макроса не должен появляться в правой части более одного раза.
Макрос SQUARE() даже в своем модифицированном виде представил выше серьезную проблему. Дано:
#define SQUARE(x) ((x)*(x))
Выражение SQUARE(++x) дважды инкрементирует x. После чего макрос в этом случае дает неверный результат. Если x вначале содержит 2, то SQUARE(++x) вычисляется как 3 * 4. Такое поведение есть пример побочного эффекта макроса - ситуации, когда макрос ведет себя неожиданно.
SQUARE(++x) также показывает пример ситуации, в которой использование макроса просто слишком рискованно для оправдания сложностей сопровождения. Встроенная функция С++ или шаблон, расширяемый до встроенной функции, являются более лучшими решениями. Даже в С простую функцию с неудачными аргументами легче сопровождать, чем эквивалентный макрос:
double square( double x )
{
return x * x;
}
Но тем не менее, у меня есть серьезное сомнение в том, что использование функции для скрытия простого умножения является стоящим делом.
83.1. Никогда не используйте макросы для символьных констант.
Например:
#define SPACE ' '
имеет смысл, если только вы намерены использовать вместо пробела другой символ (как если бы вы испытывали, например, программу для замены символов табуляции).
Никогда не делайте так:
#define SPACE 0x20
Действительное значение символьной константы для пробела (' ') изменяется компилятором так, чтобы оно соответствовало операционной среде, для которой ведется компиляция. Для среды, поддерживающей ASCII, это значение будет 0x20, а для EBDCDIC - уже нечто другое. Не думайте, что у какого-то символа свое постоянное значение.
84. Если все альтернативы отпали, то используйте препроцессор.
Мы увидим в главе, посвященной С++, что препроцессор С не играет большой роли в С++. Хотя есть немного мест, где он все еще кстати. Вот первое из них:
#ifdef DEBUG
# define D(x) x
#else
# define D(X) /* пусто */
#endif
Вместо макроса D() подставляется его аргумент, если вы занимаетесь отладкой, иначе он расширяется до пустой строки. Он используется так:
f()
{
D( printf("Это отладочная информация\n"); )
}
В данном случае аргументом D() является целиком оператор printf(), который исчезает после того, как вы закончите отладку.
Другой подобный вариант использования кстати, когда вы должны инициализировать те несколько неизбежных глобальных переменных в большой программе. Проблема заключается в синхронизации объявлений переменных (в заголовочном файле) с их определениями (в файле .c), где реально выделяется память и переменные инициализируются. Вот образец заголовочного файла:
#ifdef ALLOC
# define I(x) x
# define EXPORTED /* пусто */
#else
# define I(x) /* пусто */
# define EXPORTED extern
#endif
EXPORTED int glob_x[10] I( ={1, 2, 3, 4} );
EXPORTED some_object glob_y I( ("конструктор", "аргументы") );
В определенном месте своей программы (я обычно делаю это в файле с именем globals.cpp) вы помещаете следующие строки:
#define ALLOC
#include "globals.h"
Далее везде вы просто включаете этот файл без предварительной директивы #define ALLOC. Когда вы компилируете globals.cpp, директива #define ALLOC вызывает следующую подстановку:
/* пусто */ int glob_x[10] ={1, 2, 3, 4} ;
/* пусто */ some_object glob_y ("конструктор", "аргументы") );
Отсутствие #define ALLOC везде приводит к следующей подстановке:
extern int glob_x[10] /* пусто */ ;
extern some_object glob_y /* пусто */ ;
Последним примером использования препроцессора будет макрос ASSERT(), который выводит сообщение об ошибке и завершает программу, лишь если вы осуществляете отладку (директивой #define определена константа DEBUG) и аргумент ASSERT() имеет значение "ложь". Он очень удобен для тестирования, например, аргументов типа указателей со значением NULL. Вариант ASSERT(), используемый в виде:
f( char *p)
{
ASSERT( p, "f() : Неожиданный аргумент NULL." );
}
определяется следующим образом:
#ifdef DEBUG
#define ASSERT(условие, сообщение)
if ( !(условие) ) \
{\
fprintf(stderr, "ASSERT(" #условие ") НЕ ВЫПОЛНЕНО "\
"[Файл " __FILE__ ", Строка %d]:\n\t%s\n",\
__LINE__, (сообщение) );\
exit( -1 );\
}\
else
#else
# define ASSERT(c,m) /* пусто */
#endif
В вышеуказанном примере ASSERT() выводит следующую строку при отрицательном результате проверки:
ASSERT(p) НЕ ВЫПОЛНЕНО [Файл whatever.cpp, Строка 123]:
f() : Неожиданный аргумент NULL.
и затем выходит в вызывающую программу. Он получает текущее имя файла и номер строки от препроцессора, используя предопределенные макросы __FILE__ и __LINE__. Условие, вызывающее отрицательный результат, выводится посредством оператора получения строки ANSI С (символ #), который фактически окружает расширенный аргумент кавычками после выполнения подстановки аргумента. Строка #условие расширяется до "p" в настоящем примере). Затем вступает в действие обычная конкатенация строк С для слияния вместе разных строк, создавая единый отформатированный аргумент для fprintf().
Вы должны использовать здесь препроцессор, потому что вам нужно вывести на консоль имя файла и номер строки, для которых выполнена проверка. Встроенная функция С++ может вывести лишь имя того файла с номером строки, в котором определена встроенная функция.
Все компиляторы, поддерживающие стандарт ANSI С, должны реализовывать макрос assert(expr) в заголовочном файле assert.h, но макрос ANSI С не может выводить заказанное сообщение об ошибке. Макрос ANSI С assert() действует, если не определена константа NDEBUG (вариант по умолчанию).
Глава
7
Правила, относящиеся к языку С
В этой главе рассматриваются специфичные для С правила программирования, не встречавшиеся в предыдущих разделах.
85. Подавляйте демонов запутанности (Часть 2).
Демоны запутанности особенно опасны в С. Кажется, что этот язык сам собой поощряет выбор неестественно усложненных решений для простых задач. Последующие правила посвящаются этой проблеме.
85.1. Устраняйте беспорядок.
Язык С предоставляет богатый набор операторов и, как следствие, предлагает множество способов ничего не делать, что и иллюстрируется примерами из таблицы 2.
Таблица 2. Как ничего не делать в С.
| Плохо | Хорошо | Комментарии |
| type *end = array; end += len-1; | type *end = array+(len-1) | Инициализируйте при объявлении. |
| while (*p++ != '\0') | while ( *p++) | |
| while (gets(buf) != NULL) | while ( gets() ) | |
| if ( p != NULL ) | if ( p ) | !=0 ничего не делает в выражении |
| if ( p == NULL ) | if ( !p ) | отношения |
| if (условие != 0) | if ( условие ) | |
| if (условие == 0) | if ( !условие ) | |
| if( условие ) return TRUE; else return FALSE; | return условие; | (или return условие != 0). Если оно не было верным, то вы не сможете выполнить return TRUE. |
| return условие?0:1; return условие?1:0; | return !условие; return условие!=0; | Используйте соответствующий оператор. Операторы отношения типа ! и != выполняют по определению сравнение с 1 или 0. |
| ++x; f(x); --x; | f( x-1 ); | Не модифицируйте значение, если вам после этого не нужно его использовать более одного раза. |
| return ++x; | return x+1; | Смотрите предыдущее правило. |
| int x; f( (int)x ); | f(x); | Переменная x и так имеет тип int. |
| (void)printf("все в порядке"); | printf("все в порядке"); | Попросту опускайте возвращаемый тип, если он вам не нужен. |
| if (x > y) else if (x < y) else if (x ==y) | if ( x > y ) else if ( x < y ) else | Если первое значение не больше и не меньше второго, то они должны быть равны. |
| *(p+i) | p[i]; | Это по сути единственное исключение из приводимого ниже в данной главе правила об использовании указателей. При реализации действительно случайного доступа к элементам массива запись со скобками легче читается, чем вариант с указателем, в равной степени неэффективный при случайном доступе. |
Раз мы уже заговорили о ничегонеделаньи, то имейте в виду, что С с удовольствием допускает выражения, которые ничего не делают. Например, следующий оператор, показываемый полностью, совершенно законен и даже не вызовет предупреждающего сообщения компилятора:














