Правила программирования на С и С++. Главы 1-6 - Не портьте область глобальных имен

ОГЛАВЛЕНИЕ

 

56. Не портьте область глобальных имен.

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

  • Локальная переменная всегда более предпочтительна, чем член класса.
  • Член класса всегда более предпочтителен, чем статическая глобальная переменная.
  • Статическая глобальная переменная всегда более предпочтительна, чем настоящая глобальная переменная.
Статический глобальный идентификатор не экспортируется из файла .c, поэтому он невидим из других модулей. Применяйте модификатор static к как можно большему числу глобальных идентификаторов (переменным и функциям). Ключ доступа private в определении класса еще лучше. Идентификатор, определенный локально внутри подпрограммы, лучше всех, потому что он изолирован от всех других функций в программе.

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

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;