CHAP5 (1018805), страница 2
Текст из файла (страница 2)
56.1. Избегайте глобальных идентификаторов.
Раскрывая немного предыдущее правило, положим, что две функции связаны посредством глобальной переменной, если одна из них устанавливает эту переменную, а вторая ее использует. (Если бы глобальная переменная не использовалась совместно, то не было бы причины иметь ее глобальной; она могла бы быть статической локальной). Отношения связи с участием глобальных переменных вызывают особенно неприятные проблемы при сопровождении, потому что эти отношения тяжело отслеживать. Когда глобальная переменная меняется во время выполнения программы, то очень трудно разобраться, что ее изменило. Аналогично, если вы должны изменить поведение глобального объекта, то очень трудно разобраться, где он используется. По этой причине лучше всего вообще избегать глобальных переменных. Конечно, большинству программ реального мира необходимо незначительное количество глобальных переменных, но, как правило, я начинаю сильно нервничать, если их становится больше 10.
Вы зачастую можете ограничить область видимости глобальной переменной одним файлом, объявив ее статической, что по меньшей мере ограничит ущерб одним файлом. По крайней мере вы знаете, что все отношения связи сосредоточены в текущем файле. Также имейте ввиду, что все, что я говорил о глобальных переменных, относится и к макросам, функциям и так далее. Ограничивайте доступ к функциям, делая их любой ценой статическими.
56.2. Никогда не требуйте инициализации глобальной переменной при вызове функции.
Вот одна ситуация, где оправданы статические глобальные переменные: если у вас применяется система рекурсивных функций. (Вы можете использовать статические глобальные переменные для сокращения потребного объема стековой памяти, применяя их для передачи значений между подпрограммами. Вам никогда не нужно использовать статическую глобальную переменную для передачи информации из одной подпрограммы в другую, являющуюся рекурсивным экземпляром той же самой подпрограммы. Верным выбором в этой ситуации будет использование локальной статической переменной. Используйте статические глобальные переменные в ситуациях, где вызывается более одной подпрограммы: A() вызывает B(), которая вызывает A(), вызывающую в свою очередь B(), и так далее).
Так как глобальная переменная, используемая нашей рекурсивной функцией, сделана статической для минимизации связывания, то как вам ее инициализировать? Далее показано как не нужно этого делать. Вот файл 1:
static int glob;
get_glob( x )
{
return glob;
}
set_glob( x )
{
glob = x;
}
void recursive_function( void )
{
int y = glob
// ...
recursive_function();
}
а вот файл 2:
set_glob( 10 );
recursive_function();
x = get_glob();
Вы при этом немногого достигли с точки зрения связывания; на самом деле, с простой глобальной переменной было бы проще управляться. Кроме того, вы подготовили себе потенциальную проблему: возможность забыть вызвать set_glob(). Вот как сделать это правильно:
static int glob;
static void recursive_function( void )
{
int y = glob;
// ...
recursive_function();
}
int do_recursive( int init_val )
{
glob = init_val;
recursive_function();
return glob;
}
Ни к глобальной переменной, ни к рекурсивной функции нельзя получить доступ прямо снаружи модуля из-за статического выделения памяти. Вы должны получить доступ к рекурсивной функции посредством функции доступа do_recursive(), которая гарантирует, что все инициализировано правильно перед тем, как выполнить вызов рекурсивной функции.
56.2.1. Делайте локальные переменные статическими в рекурсивных функциях, если их значения не участвуют в рекурсивном вызове.
Так как мы занялись темой рекурсии, то вот правило, которое используется для того, чтобы еще сильнее сократить использование стека. Локальная переменная может быть объявлена статической (тем самым она удаляется из стека), если ее значение не должно сохраняться после рекурсивного вызова. Вот один пример:
f()
{
static int i;
// ...
for ( i = 10; --i >= 0; )
// ...
f();
for ( i = 10; --i >= 0; ) // переменная i вновь инициализируется после
// ... // рекурсивного вызова, поэтому она может
} // быть статической.
Вот другой:
int f()
{
static int depth = 0;
static int depth_max = 0;
++depth; depth_max = max( depth, depth_max );
if ( depth > 10 )
return -1; // уровень рекурсии слишком глубок.
f();
--depth;
return depth_max;
}
В этом последнем случае переменная depth используется для передачи информации - глубины рекурсии - от одного экземпляра подпрограммы другому, рекурсивному экземпляру этой же самой подпрограммы. Переменная depth_max хранит след достигнутой максимальной глубины рекурсии. depth вовсе не будет работать, если она должна будет сохранять свое значение после вызовов - весь смысл в том, что каждый рекурсивный вызов модифицирует эту переменную.
56.3. Используйте счетчик экземпляров объектов вместо инициализирующих функций .
Инициализирующие функции, с очевидным исключением в виде конструкторов С++, не должны использоваться просто потому, что слишком просто забыть их вызвать. Многие системы с оконным интерфейсом, например, требуют, чтобы вы вызывали функцию инициализации окна перед его созданием (и другую - закрытия - после удаления последнего окна). Это плохая идея. Уладьте эту проблему при помощи счетчика экземпляров, который обычно в С должен быть глобальной переменной (объявленной статической для ограничения области ее видимости). Сделайте это так:
static int num_windows = 0; // ограничьте доступ к текущему модулю
create_window()
{
if ( ++num_windows == 1 ) // только что создано первое окно
initialize_video_system();
// ...
}
destroy_window()
{
// ...
if ( --num_windows == 0 ) // только что уничтожено первое окно
shutdown_video_system();
}
static int число_окон = 0; // ограничьте доступ к текущему модулю
создать_окно()
{
if ( ++число_окон == 1 ) // только что создано первое окно инициализировать_видео_систему();
// ...
}
уничтожить_окно()
{
// ...
if ( --число_окон == 0 ) // только что уничтожено первое окно
закрыть_видео_систему();
}
В С++ вы можете для этой цели использовать статический член класса.
56.4. Если оператор if завершается оператором return, то не используйте else.
Вместо:
if ( условие )
return xxx;
else
{
делать_массу_вещей();
}
обычно лучше записать:
if ( условие )
return xxx;
делать_массу_вещей();
Лучше сделать так, чтобы последним оператором return был аварийный возврат по ошибке, так чтобы вы получили сообщение об ошибке, если нечаянно заблудились.
Условный оператор также может решать эту проблему в простых ситуациях и делать код более читаемым для новичка. Вместо:
f()
{
// ...
if ( x )
return 123;
else if ( y )
return 456;
else
return ERROR;
}
используйте
f()
{
// ...
return x ? 123 :
y ? 456 :
ERROR ;
}
Заметьте, насколько форматирование улучшает читаемость предыдущего кода.
Одна распространенная ситуация, в которой у вас имеется множество точек возврата, выглядит следующим образом:
if ( A )
return FAIL;
else if ( B )
return SUCCESS;
else
{
// Масса кода
return SUCCESS; // Подозрительны два одинаковых возвращаемых значения.
}
Вы можете устранить это следующим образом. Во-первых, избавьтесь от повтора возвращаемых значений, переместив их во внешний уровень вот так:
if ( A )
return FAIL;
else if ( B )
;
else
{
// Масса кода
}
return SUCCESS;
Затем освободитесь от предложения if, связанного с пустым оператором:
if ( A )
return FAIL;
else if ( B )
{
// Масса кода
}
return SUCCESS;
57. Помещайте более короткий блок условного оператора if/else первым.
Часто бывает, что у оператора if/else одно предложение (или внутренний блок) короткое (обычно оператор для обработки ошибки), а другое, выполняющее собственно работу, - большое:
if ( некая_ошибка() )
error( "ААААхххх!!!!" );
else
{
// Здесь следуют 30 строк кода
}
Всегда помещайте короткое предложение в начале. То есть, не делайте так:
if ( !некая_ошибка() )
{
// Здесь следуют 30 строк кода
}
else
error( "ААААхххх!!!!" );
Проблема заключается в том, что проверка в операторе if управляет else в той же степени, что и if. Если большой блок следует первым, то вероятность того, что вычисляться будет предложение else, расположенное на следующем экране или странице, довольно велика. И если я допустил в нем ошибку, то мне придется поломать голову над тем, как добраться до else. Если в это время if в поле зрения, то я знаю, как туда попасть.
58. Старайтесь сдвинуть ошибки с этапа выполнения на этап компиляции.
Неинициализированные переменные - по сути ошибки, ждущие своего часа. Вы всегда должны инициализировать переменную при ее объявлении. В С++ инициализация во время объявления возможна всегда, потому что объявление может проводиться везде, где можно поместить оператор; просто откладывайте объявление до тех пор, пока у вас не будет достаточно информации для объявления в произвольном месте с инициализацией переменной. Таким образом, если вы попытаетесь использовать эту переменную преждевременно, то получите ошибку на этапе компиляции ("переменная не найдена") вместо ошибки во время выполнения.
В С вы можете объявлять переменную после любой открытой фигурной скобки, поэтому вы можете часто откладывать объявление на некоторое время, но при этом у вас в распоряжении нет гибкости С++. В самом крайнем случае инициализируйте переменную таким значением, которое в ответ на него понятным образом вызовет в подпрограмме аварию; не присваивайте переменной произвольное значение - оно может быть принято в программе за имеющее смысл. Например, указатель, инициализированный значением NULL, более надежен, чем имеющий произвольное значение, которое может оказаться существующим адресом.
С другой стороны, хорошо иметь все объявления переменных в одном месте в начале блоке так, чтобы вы могли их легко найти. Если ваша подпрограмма достаточно мала, то вы обычно можете сделать и то, и другое. Например, вы можете разделить подпрограмму на части для того, чтобы переместить объявления в начало блока, чтобы вам их было легче найти. Подпрограмма, подобная следующей:
f()
{
// код, который не использует переменную i
int i = init_val;
// код, который использует переменную i
}
может быть разделена следующим образом:
f()
{
// код, который не использует переменную i
g( init_val );
}
g( int init_val )
{
int i = init_val;
// код, который использует переменную i
}
Переменная-счетчик общего назначения, которая инициализируется в начале цикла for, является очевидным исключением из этого правила. Иногда использование слишком большого количества подпрограмм может вызвать больше проблем, чем решить их, и в этом случае лучше выбрать внутренние объявления. Используйте свою голову.
59. Применяйте указатели на функции С в качестве селекторов.
Это правило строго для программистов на С. (Программирующие на С++ должны использовать виртуальные функции). В С заменяйте подобный код:
typedef enum shape_type { CIRCLE, LINE, TEXT };
typedef struct
{
shape_type type;
union shape_data
{ // здесь данные для различных форм.
} data;
} shape;
extern void print_circle( shape *p );
extern void print_line ( shape *p );
extern void print_text ( shape *p );
shape a_circle = { CIRCLE, ... };
print_shape( shape *p )
{
switch( p->type )















