Жанр: Учеба

Правила программирования на Си и Си плюс плюс.

сторінка №9

то:

a();
x = b();

Если вам все равно, какое значение имеет макрос, то вы можете сделать
нечто подобное, используя знак плюс вместо запятой. (Выражение:


Препроцессор 109

a()+b();

в отдельной строке совершенно законно для Си, где не требуется, чтобы
результат сложения был где-нибудь сохранен). Тем не менее, при знаке
плюс порядок выполнения не гарантируется; функция b() может быть
вызвана первой. Не путайте приоритеты операций с порядком
выполнения. Приоритет просто сообщает компилятору, где неявно
размещаются круглые скобки. Порядок выполнения вступает в силу после
того, как все круглые скобки будут расставлены по местам. Невозможно
добавить дополнительные скобки к ((a())+(b())). Операций
последовательного вычисления гарантированно выполняется слева
направо, поэтому в нем такой проблемы нет.
Я должен также отметить, что операция последовательного
вычисления слишком причудлива, чтобы появляться в нормальном коде.
Я использую ее лишь в макросах, сопровождая все обширными
комментариями, объясняющими, что происходит. Никогда не используйте
запятую там, где должна быть точка к запятой. (Я видел людей, которые
делали это, чтобы не использовать фигурные скобки, но это страшно даже
пересказывать).
Второе решение использует фигурные скобки, но с одной уловкой:

#define two_things() \
do \
{ \
a(); \
b(); \
} while( 0 )

if
( x )
two_things();
else
something_else();

что расширяется до:

if
( x )
do
{
a();
b();
} while ( 0 ) ; // "== точка с запятой связывается с
// оператором while ( 0 )
else
something_else();

Вы можете также попробовать так:

#define two_things() \
if
( 1 ) \


Правила программирования на Си и Си++

110


{ \
a(); \
b(); \
} else

но я думаю, что комбинация do с while (0) незначительно лучше.
Так как вы можете объявить переменную после любой открытой
фигурной скобки, то у вас появляется возможность использования
предшествующего метода для определения макроса, имеющего по
существу свои собственные локальные переменные. Рассмотрим
следующий макрос, осуществляющий обмен значениями между двумя
целочисленными переменными:

#define swap_int(x,y) \
do \
{ \
int x##y; \
x##y = x; \
x = y; \
y = x##y \
} \
while (0)

Сочетание ## является оператором конкатенации в стандарте ANSI Си. Я
использую его здесь для обеспечения того, чтобы имя временной
переменной не конфликтовало с любым из имен исходных переменных.
При данном вызове:

swap(laurel, hardy);

препроцессор вначале подставляет аргументы обычным порядком
(заменяя x на laurel, а y на hardy), давая в результате следующее имя
временной переменной:

int laurel##hardy;

Затем препроцессор удаляет знаки решетки, давая

int laurelhardy;

Дополнительная польза от возможности замены макросов функциями
заключается в отладке. Иногда вы хотите, чтобы что-то было подобно
макросу по эффективности, но вам нужно при отладке установить в нем
точку прерывания. Используйте для этого в Си++ встроенные функции, а
в Си используйте следующие:

#define _AT_LEFT(this) ((this)-"left_child_is_thread ? NULL\
:(this)-"left)

#ifdef DEBUG
static tnode *at_left(tnode *this) { return _AT_LEFT(this); }


Препроцессор 111

#else
# define at_left(this) _AT_LEFT(this)
#endif

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

Таблица 1. Макросы, эквивалентные условным операторам

Этот код: Делает то же самое, что и:
( a && f() ) if ( a )
f();
( b || f() ) if ( !b )
f();
( z ? f() : g()) if ( z )
f();
else
g();

Первые два выражения опираются на тот факт, что вычисления в
выражении с использованием операций && и || гарантированно
осуществляются слева направо и прекращаются сразу, как только
устанавливается истина или ложь. Возьмем для примера выражение a &&
f()
. Если a ложно, то тогда не важно, что возвращает f(), так как все
выражение ложно, если любой из его операндов значит ложь.

Следовательно, компилятор никогда не вызовет f(), если a ложно, но он
должен вызвать f(), если a истинно. То же самое применимо и к b, но
здесь f() вызывается, если b, напротив, ложно.

81.1. Операция ?: не то же самое, что и оператор
if/else

Последняя строка в таблице 1 относится к другому спорному вопросу.
Условная операция - это простой оператор. Она используется лишь в
выражении и передает значение. Условная операция является не очень
привычной заменой для оператора if/else, но не менее, чем операции
&&
или || приемлемы для замены простого if. Хотя большинство людей


Правила программирования на Си и Си++

112


и не принимают во внимание замену:

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);


Препроцессор 113

что расширяется макросом до:

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 как для какойнибудь
глобальной переменной. (В Си++ все это несущественно, так как
там память выделяется лишь тогда, когда вы определяете адрес константы
или передаете его по ссылке. Определения констант в Си++ могут - а на
деле часто и должны - помещаться в заголовочном файле).
Перечисление отличается тем, что память для него никогда не
выделяется. Подобно макросу, оно может вычисляться во время
компиляции. Следовательно, при использовании вами перечисления не


Правила программирования на Си и Си++

114


происходит потери производительности.
Второй проблемой является порча области глобальных имен. Область
действия перечисления легко ограничивается. Например, в следующем
фрагменте кода перечисление 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))

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


Препроцессор 115

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


Правила программирования на Си и Си++

116


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,


Препроцессор 117

директива #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
# efine ASSERT(c,m) /* пусто */
#endif

В вышеуказанном примере ASSERT() выводит следующую строку при
отрицательном результате проверки:
ASSERT(p) НЕ ВЫПОЛНЕНО [Файл whatever.cpp, Строка 123]:
f() : Неожиданный аргумент NULL.

и затем выходит в вызывающую программу. Он получает текущее имя
файла и номер строки от препроцессора, используя предопределенные
макросы __FILE__ и __LINE__. Условие, вызывающее отрицательный
результат, выводится посредством оператора получения строки ANSI Си
(символ #), который фактически окружает расширенный аргумент
кавычками после выполнения подстановки аргумента. Строка #условие
расширяется до "p" в настоящем примере). Затем вступает в действие


Правила программирования на Си и Си++

118


обычная конкатенация строк Си для слияния вместе разных строк,
создавая единый отформатированный аргумент для fprintf().
Здесь следует использовать препроцессор, потому что вам нужно
вывести на консоль имя файла и номер строки, для которых выполнена
проверка. Встроенная функция Си++ может вывести лишь имя того файла
с номером строки, в котором определена встроенная функция.
Все компиляторы, поддерживающие стандарт ANSI Си, должны
реализовывать макрос assert(expr) в заголовочном файле assert.h, но
макрос ANSI Си не может выводить заказанное сообщение об ошибке.
Макрос ANSI Си assert() действует, если не определена константа
NDEBUG
(вариант по умолчанию).



Часть

Правила, относящиеся к языку Си

В этой главе рассматриваются специфичные для Си правила
программирования, не встречавшиеся в предыдущих разделах.


Правила программирования на Си и Си++

120


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(buf) )

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("
все в
порядке"); Попросту опускайте
возвращаемый тип, если


Правила, от носящиеся к языку Си 121

он вам не нужен.
if ( x " y )
else if ( x " y )
else if ( x ==y )
if ( x " y )
else if ( x " y )
else
Если первое значение не
больше и не меньше
второго, то они должны
быть равны.
*(p+i) p[i]; Это, по сути,
единственное исключение
из приводимого ниже в
данной главе правила об
использовании указателей.

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

Раз мы уж заговорили о ничегонеделаньи, то имейте в виду, что Си с
удовольствием допускает выражения, которые ничего не делают.
Например, следующий оператор, показываемый полностью, совершенно
законен и даже не вызовет предупреждающего сообщения компилятора:

a + b;

Конечно, если вы хотели записать:

a += b;

то вы, должно быть, попали в беду.

85.2. Избегайте битовых масок; используйте
битовые поля
Многие программисты, в особенности те, кто начинал жизнь с языком
ассемблера, привыкли пользоваться битовыми масками, а не битовыми
полями. Мне довелось видеть много программ, подобных следующей:

struct fred
{
int status;

// ...
};

#define CONDITION_A 0x01
#define CONDITION_B 0x02

Комментарий в языке Си должен быть заключен в /* */. - Ред.


Правила программирования на Си и Си++

122


#define CONDITION_C 0x03

#define SET_CONDITION_A(p) ((p)-"status |= CONDITION_A)
#define SET_CONDITION_B(p) ((p)-"status |= CONDITION_B)
#define SET_CONDITION_C(p) ((p)-"status |= CONDITION_C)

#define CLEAR_CONDITION_A(p) ((p)-"status &= ~CONDITION_A)
#define CLEAR_CONDITION_B(p) ((p)-"status &= ~CONDITION_B)
#define CLEAR_CONDITION_C(p) ((p)-"status &= ~CONDITION_C)

#define IN_CONDITION_A(p) ((p)-"status & CONDITION_A)
#define IN_CONDITION_B(p) ((p)-"status & CONDITION_B)
#define IN_CONDITION_C(p) ((p)-"status & CONDITION_C)

#define POSSIBILITIES(x) ((x) & 0x0030)
#define POSSIBILITY_A 0x0000
#define POSSIBILITY_B 0x0010
#define POSSIBILITY_C 0x0020
#define POSSIBILITY_D 0x0030

Это означает необходимость в дополнение к полю из структуры данных
сопровождать 17 макросов, которые к тому же будут, вероятно, спрятаны
где-то в заголовочном файле, а не в том, где они используются. Ситуация
еще более ухудшится, если вы не включите эти макросы и организуете
проверки прямо в программе. Что-нибудь типа:

if ( struct.status &= ~CONDITION_A )
// ...

по меньшей мере, с трудом читается. Еще хуже нечто, подобное
следующему:

struct
.status = POSSIBILITY_A;
if ( POSSIBILITIES(struct.status) == POSSIBILITY_A )
// ...

Лучшее решение использует битовые поля; они не требуют
дополнительного места и заведомо эффективно реализуются на
большинстве машин. (Некоторые люди утверждают, что второй пример
лучше, чем битовое поле, потому что здесь нет неявного сдвига, но
многие машины поддерживают команду проверки бита, которая устраняет
какую-либо потребность в сдвиге, который в случае своего использования
вызывает очень незначительные накладные расходы. Необходимость в
устранении ненужной путаницы обычно перевешивает подобные
соображения о снижении эффективности).

enum { possibility_a, possibility_b, possibility_b,
possibility_d };

struct fred


Правила, от носящиеся к языку Си 123

{
unsigned in_condition_a : 1;
unsigned in_condition_b : 1;
unsigned in_condition_c : 1;

unsigned possibilities : 2;
};

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

struct fred flintstone;
flintstone.in_condition_a = 1;
if ( flintstone.in_condition_a )
// ...

flintstone.possibilities = possibility_b;

if ( flintstone.possibilities == possibility_a )
// ...

Единственным очевидным исключением из этого правила является
взаимодействие с архитектурами со страничной организацией памяти;
битовые поля не гарантируют какого-то упорядочивания в типе int, из
которого выделяются биты.

85.3. Не используйте флагов завершения
Флаг завершения типа "готов" едва ли нужен в Си или Си++. Его
использование просто добавляет одну лишнюю переменную в процедуру.
Не делайте так:
BOOL готов = FALSE;
while ( !
готов )
{
if ( некоторое_условие() )
готов = 1;
}

Поступайте следующим образом:

while ( 1 )
{
if ( некоторое_условие() )
break
;
}

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


Правила программирования на Си и Си++

124


Единственным исключением из этого правила является выход из
вложенных циклов в Си++, где оператор goto может привести к пропуску
программой вызова конструктора или деструктора. Эта проблема была
рассмотрена в правиле 54.
85.4. Рассчитывайте, что ваш читатель знает Си
Не делайте чего-то подобного этому:

#define SHIFT_LEFT(x, bits) ((x) "" (bits))

Программисты на Си знают, что "" означает "сдвиг влево". Аналогично,
не делайте таких вещей:
x++; // инкрементировать x

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

85.5. Не делайте вид, что Си поддерживает булевый
тип (#define TRUE
)
Нижеследующее может скорее вызвать проблемы, чем нет:

#define TRUE 1
#define FALSE 0

Любая отличная от нуля величина в Си означает истину, поэтому в
следующем фрагменте f() может вернуть совершенно законное значение
"истина", которое не совпало с 1, и проверка даст отрицательный
результат:

if
( f() == TRUE ) // Вызов не выполняется, если f() возвращает
// значение "истина", отличное от 1.
// ...

Следующий вариант надежен, но довольно неудобен. Я не думаю, что
можно рекомендовать что-либо из подобной практики:

#define FALSE 0
if
( f() != FALSE )
// ...

В действительности здесь проявляется настоящая проблема, связанная с
непониманием различий между языком Си и Паскалем. Си, в отличие от
Паскаля, не поддерживает встроенный булевый тип, и полагать обратное
означает просто навлечь на себя неприятности.
Ча

Список сторінок

Поділитися у соцмережах

☏ Замовлення реклами: +3805044-68-872

© Асоціація електронних бібліотек України

☝ Усі матеріали сайту (зокрема статті, зображення, рекламні оголошення тощо) призначені лише для попереднього ознайомлення. Усі права на публікації, представлені на сайті, належать їхнім законним власникам. Просимо Вас не зберігати копії інформації.