Жанр: Учеба
Правила программирования на Си и Си плюс плюс.
...имя state заставило нас ввести два ненужных
Формат ирование и документ ация 63
идентификатора: IN_WORD и BETWEEN_WORDS. Теперь взгляните на этот
вариант:
int nwords2(const char *str)
{
int word_count = 0;
int in_word = 0;
for
(; *str ; ++str )
{
if
( isspace(*str) )
in_word = 0;
else
if
( !in_word )
{
++word_count;
in_word = 1;
}
}
return word_count;
}
Переименование нечетко названной переменной state во что-нибудь,
что действительно описывает назначение переменной, позволило мне
исключить булевые именованные константы IN_WORD
и
BETWEEN_WORDS
. Получившаяся подпрограмма меньше и легче читается.
Вот другой пример. Следующая программа:
enum child_type { I_AM_A_LEFT_CHILD, I_AM_A_RIGHT_CHILD };
struct tnode
{
child_type position;
struct tnode *left,
*right;
} t;
//...
t.position = I_AM_LEFT_CHILD;
if
( t.position == I_AM_LEFT_CHILD )
//...
может быть упрощена подобным образом:
struct tnode
{
unsigned is_left_child ;
struct tnode *left,
*right;
} t;
t.is_left_child = 1;
if
( t.is_left_child )
В стандарте ISO/IEC 14882 существует тип bool'. Имеет смысл заменить тип
переменной is_left_child на bool. - Ред.
Правила программирования на Си и Си++
64
//...
тем самым исключая два ненужных идентификатора. И вот последний
пример:
enum { SOME_BEHAVIOR, SOME_OTHER_BEHAVIOR, SOME_THIRD_BEHAVIOR };
f( SOME_BEHAVIOR, x);
f( SOME_OTHER_BEHAVIOR, x);
f( SOME_THIRD_BEHAVIOR, x);
требующий четырех идентификаторов (три именованные константы и имя
функции). Лучше, хотя это не всегда возможно, исключить селекторную
константу в пользу дополнительных функций:
some_behavior(x);
some_other_behavior(x);
some_third_behavior(x);
Обратной стороной этой монеты является вызов функции. Рассмотрим
следующий прототип:
create_window( int has_border, int is_scrollable,
int is_maximized );
Я снова выбрал рациональные имена для исключения необходимости в
именованных константах. К сожалению, вызов этой функции плохо
читаем:
create_window( TRUE, FALSE, TRUE );
Просто взглянув на такой вызов, я не получу никакого представления о
том, как будет выглядеть это окно. Несколько именованных констант
проясняют обстоятельства в этом вызове:
enum { UNBORDERED =0; BORDERED =1}; // Нужно показать значения,
enum { UNSCROLLABLE=0; SCROLLABLE =1}; // или create_window()
enum { NORMAL_SIZE =0; MAXIMIZED =1}; // не будет работать.
//...
create_window( BORDERED, UNSCROLLABLE, MAXIMIZED );
но теперь у меня другая проблема. Я не хочу использовать именованные
константы внутри самой create_window(). Они здесь только для того,
чтобы сделать ее вызов более читаемым, и я не хочу загромождать эту
функцию таким кодом, как:
if
( has_border == BORDERED )
//...
сравнивая его с более простым:
if
( has_border )
//...
Формат ирование и документ ация 65
Первый вариант уродлив и многословен. К сожалению, если кто-то
изменит значение именованной константы BORDERED, второй оператор
if
не будет работать. Я обычно соглашаюсь с мнением, что программист,
занимающийся сопровождением, не должен менять значения
идентификаторов, как я это проделал в предыдущем примере.
Часть
Правила обычного
программирования
Эта часть содержит правила, относящиеся к написанию собственно
исходного текста программы, в отличие от предыдущей части, в которой
рассматривалась разработка программы в целом. Эти правила не слишком
зависят от выбора языка программирования.
Правила обычного программирования 67
50. Не путайте привычность с читаемостью
(Или синдром "настоящего программиста, который может
программировать на любом языке как на ФОРТРАНе"). Многие люди
пытаются злоупотреблять препроцессором для того, чтобы придать Си
большее сходство с каким-нибудь другим языком программирования.
Например:
#define begin {
#define end }
while ( ... )
begin
// ...
end
Эта практика ничего не дает, кроме того, что ваш код становится
нечитаемым для кого-нибудь, кто не знает того языка, который вы
стараетесь имитировать. Для программиста на Си код станет менее
читаемым, не более того.
Родственная проблема связана с использованием макросов
препроцессора для скрытия синтаксиса объявлений Си. Например, не
делайте следующего:
typedef const char *LPCSTR;
LPCSTR str;
Подобные вещи вызывают проблемы с сопровождением, потому что ктото,
не знакомый с вашими соглашениями, будет должен просматривать
typedef
, чтобы разобраться, что происходит на самом деле.
Дополнительная путаница возникает в Си++, потому что читатель может
интерпретировать происходящее, как определение объекта Си++ из
класса LPCSTR. Большинству программистов на Си++ не придет в голову,
что LPCSTR является указателем. Объявления Си очень легко читаются
программистами на Си. (Кстати, не путайте вышеупомянутое с разумной
практикой определения типа word в виде 16-битового числа со знаком
для преодоления проблемы переносимости, присущей типу int, размер
которого не определен ни в ANSI Си, ни в ANSI Си++).
К тому же, многие программисты избегают условной операции (?:)
просто потому, что она им кажется непонятной. Тем не менее, это
условное выражение может существенно упростить ваш код и,
следственно, сделать его лучше читаемым. Я думаю, что:
Правила программирования на Си и Си++
printf("%s", str ? str : ""
пусто"");
гораздо элегантнее, чем:
if ( str == NULL )
printf( ""
пусто"" );
else
printf( "%s", str );
Вы к тому же экономите на двух вызовах printf(). Мне также часто
приходится видеть неправильное использование операций ++ и --. Весь
смысл автоинкремента или автодекремента заключается в соединении
этих операций с другими. Вместо:
while ( *p )
{
putchar ( *p );
++p;
}
или:
for
( ; *p ; ++p )
putchar ( *p );
используйте:
while
( *p )
putchar ( *p++ );
Этот код вполне читаем для компетентного программиста на языке Си,
даже если ему нет эквивалентной операции в ФОРТРАНе или Паскале.
Вы также никогда не должны прятать операторы в макросах из-за того,
что вам просто не нравится, как они выглядят. Я однажды видел
следующее в реальной программе:
struct tree_node
{
struct tree_node *lftchld;
};
#define left_child(x) ((x)-"lftchld)
//...
traverse( tree_node *root )
{
if
( left_child(root) )
traverse( left_child( root ) );
// ...
}
Программист намеренно сделал определение структуры труднее
читаемым для того, чтобы избежать конфликта имен между полем и
совершенно необходимым макросом, и все из-за того, что ему не нравился
Правила обычного программирования 69
внешний вид оператора -". Для него было бы гораздо лучшим выходом
просто назвать поле left_child и совсем избавиться от макроса.
Если вы действительно думаете, что программа должна внешне
выглядеть как на Паскале, чтобы быть читаемой, то вы должны
программировать на Паскале, а не на Си или Си++.
51. Функция должна делать только одно дело
Это обычно не очень удачная мысль - записывать то, что должна делать
функция, через ее аргументы. Это должно делать имя функции.
Например:
UpdateAllViews( CView *sender, long lhint, CObject *phint )
{
// sender lhint phint
// NULL xx xx Начальное обновление, вызываемое из
// обрамляющего окна
// Cview* 0 Crect* Вызывается, когда встроенный объект
// становится действительным. phint
// указывает на прямоугольник документа,
// сохраняя положение недействительного
// объекта
// Cview* 1 Crect* Сообщение, посылаемое объектом CView*
// ("sender" - передатчик). phint сохраняет
// для CView* обрамляющее окно его клиента.
}
Вам нужны вместо этого три функции: initial_update(),
update_embedded_object()
и update_view(). Верным ключом для
понимания того, что здесь что-то не так, является туманная природа имен
аргументов. Функциям не должны передаваться "намеки". Им должны
даваться указания.
52. Иметь слишком много уровней абстракции или
инкапсуляции так же плохо, как и слишком мало
Основной смысл использования таких абстракций, как функции или
символьные константы (или инкапсуляций, подобных определениям
struct
или class), заключается в улучшении читаемости программ. Не
пользуйтесь ими просто потому, что вы можете делать это. Например,
вложенные структуры в данном фрагменте не служат какой-либо
полезной цели:
struct tree_node;
struct child_ptr
{
unsigned is_thread;
Правила программирования на Си и Си++
struct tree_node *child;
};
struct tree_node
{
struct child_ptr left,
right;
};
tree_node *p;
if
( !p-"left.am_a_thread )
p = p-"left.child;
Следующий код лучше читается, потому что в нем меньше точек, и легче
сопровождается, так как в нем нужно отлаживать на одно определение
меньше:
struct tree_node
{
struct tree_node *left_child;
unsigned left_is_thread : 1;
struct tree_node *right_child;
unsigned right_is_thread : 1;
};
if
( !p-"left_is_thread )
p = p-"left_child;
53. Функция должна вызываться более одного раза,
но!
Кроме того, если функция должным образом связана (т.е. если она
выполняет единственную операцию и весь код функции работает на ее
результат), то нет причины извлекать кусок кода в другие функции, если
только вы не желаете использовать эту часть кода где-то еще. Мой опыт
говорит, что когда функция становится слишком большой, то часто
возможно выделить куски, которые обладают достаточной общностью,
чтобы быть использованными где-либо еще в программе, так что это
правило на самом деле не противоречит правилу "маленькое -
прекрасно". Если вы не выделяете этот код, то блочный комментарий,
описывающий назначение этого блока программы, который вы могли бы
выделить, служит той же самой цели, что и имя функции -
документированию.
Тем не менее, иногда выделение кода в функции меньшего размера
существенно улучшает читаемость этого кода по причине устранения
беспорядка. Однако эта практика - создание абстракции для части кода в
виде имени функции - добавляет идентификаторы в область глобальных
имен, и эта возросшая общая сложность является определенным минусом.
Правила обычного программирования 71
Если я использую функцию этим способом - как абстракцию -, то
обычно объявляю ее одновременно статической, чтобы к ней нельзя было
получить доступ снаружи текущего файла, и встроенной, чтобы ее вызов
не приводил к накладным расходам. Не доводите процесс
функционального абстрагирования до крайности. Мне доводилось видеть
отличные программы, доведенные абстрагированием до полностью
нечитаемого состояния такой степени, что нет ни одной функции длиннее,
чем 5 или 6 строк. Получающаяся программа работает также значительно
медленнее, чем необходимо, и ее исходный текст в 5 раз длиннее, чем
нужно.
53.1. Код, используемый более одного раза, должен
быть помещен в функцию
Это правило является обратной стороной предыдущего. Если вы
обнаруживаете почти идентичный код появляющимся более чем в одном
месте своей программы, то этот код должен быть выделен в
подпрограмму, которая вызывается из нескольких мест. Выгода состоит в
меньшем размере программы и лучшей сопровождаемости вследствие
упрощения программы и того, что вы должны теперь сопровождать лишь
одну функцию; если вы находите ошибку, то вам нужно исправить ее
только в одном месте. Как было упомянуто ранее, имя функции также
дает хорошую абстракцию. Вызовы функции с хорошо выбранным
именем являются обычно самодокументирующимися, устраняя
необходимость в комментариях.
54. Функция должна иметь лишь одну точку выхода
Это правило применимо лишь к программам на Си. Вообще, множество
переходов goto к одной точке выхода лучше, чем много операторов
return
. Этим способом вы можете поместить точку прерывания у
единственной точки выхода, вместо того, чтобы возиться с несколькими
прерываниями. Например:
f()
{
int возвращаемое_значение = ОШИБКА;
if
( некое_условие )
{
// ...
возвращаемое_значение = НЕЧТО;
Решение о переводе некоторых из идентификаторов, по меньшей мере, спорное.
Однако, если вы не знаете английского, то будете лишены возможности оценить
юмор автора, которым он оживил большую часть своих примеров. - Ред.
Правила программирования на Си и Си++
goto выход;
}
else
{
// ...
возвращаемое_значение = НЕЧТО_ЕЩЕ;
goto выход;
}
выход:
return возвращаемое_значение;
}
Этот метод не срабатывает в Си++, потому что функции конструктора
вызываются неявно в качестве части объявления; объявление часто
скрывает вызов функции. Если вы пропускаете объявление, то вы
пропускаете и вызов конструктора. Например, в следующей программе
деструктор для x вызовется, а конструктор - нет:
foo()
{
if
( некое_условие )
goto выход;
некий_класс x; // Конструктор не вызывается. (Оператор
// goto перескакивает через него.)
// ...
выход:
// Здесь вызывается деструктор для x
// при выходе x из области видимости.
}
Вследствие этой проблемы лучше всего совсем избегать переходов goto
в программах на Си++.
54.1. Всегда предусматривайте возврат значения из
блока внешнего уровня
Иногда, когда подпрограммы короткие, не стоит стараться обеспечить
единственную точку выхода. (По моему мнению, правило "избегай
запутанности" перекрывает любое другое правило, с которыми оно
входит в конфликт). В этой ситуации всегда старайтесь убедиться, что из
подпрограммы нет таких путей, которые не проходят через оператор
return
. Не так:
if
( a )
{
// ...
return делай_что_нужно();
}
else
Правила обычного программирования 73
{
// ...
return ОШИБКА;
}
а так:
if
( a )
{
// ...
return делай_что_нужно();
}
// ...
return ОШИБКА;
В идеале, выход по ошибке организуется из внешнего уровня блока так,
чтобы вы правильно обработали неожиданный аварийный выход на
внешний уровень.
55. Избегайте дублирования усилий
Следующий фрагмент демонстрирует эту проблему:
if
( strcmp(a, b) " 0 )
{
}
else if
( strcmp(a, b) " 0 )
{
}
else if
( strcmp(a, b) == 0 )
{
}
Вызов strcmp() в Си связан с немалыми накладными расходами (как в
Паскале и других языках программирования), значительно лучше сделать
так:
int cmp = strcmp(a, b);
if
( cmp " 0 )
{
}
else if
( cmp " 0 )
{
}
else // остается случай cmp == 0
{
}
Правила программирования на Си и Си++
56. Не захламляйте область глобальных имен
Беспорядок в области глобальных имен является характерной проблемой
для среды групповой разработки. Вам не очень понравится спрашивать
разрешение от каждого участника группы каждый раз, когда вы вводите
новый идентификатор. Поэтому:
+ Локальная переменная всегда более предпочтительна, чем член класса.
+ Член класса всегда более предпочтителен, чем статическая глобальная
переменная.
+ Статическая глобальная переменная всегда более предпочтительна,
чем настоящая глобальная переменная.
Статический глобальный идентификатор не экспортируется из файла .c,
поэтому он невидим из других модулей. Применяйте модификатор
static
к как можно большему числу глобальных идентификаторов
(переменным и функциям). Ключ доступа private в определении класса
еще лучше. Идентификатор, определенный локально внутри
подпрограммы, лучше всех, потому что он изолирован от всех других
функций в программе.
Вывод: избегайте препроцессора. Так как у макроса такая большая
область видимости, то он, по сути, то же самое, что и глобальная
переменная.
56.1. Избегайте глобальных идентификаторов
Раскрывая немного предыдущее правило, положим, что две функции
связаны посредством глобальной переменной, если одна из них
устанавливает эту переменную, а вторая ее использует. (Если бы
глобальная переменная не использовалась совместно, то не было бы
причины иметь ее глобальной; она могла бы быть статической
локальной). Отношения связи с участием глобальных переменных
вызывают особенно неприятные проблемы при сопровождении, потому
что эти отношения тяжело отслеживать. Когда глобальная переменная
меняется во время выполнения программы, то очень трудно разобраться,
что ее изменило. Аналогично, если вы должны изменить поведение
глобального объекта, то очень трудно разобраться, где он используется.
По этой причине лучше всего вообще избегать глобальных переменных.
Конечно, большинству программ реального мира необходимо
незначительное количество глобальных переменных, но, как правило, я
начинаю сильно нервничать, если их становится больше 10.
Вы зачастую можете ограничить область видимости глобальной
Правила обычного программирования 75
переменной одним файлом, объявив ее статической, что, по меньшей
мере, ограничит ущерб одним файлом. По крайней мере, вы знаете, что
все отношения связи сосредоточены в текущем файле. Также имейте в
виду, что все, что я говорил о глобальных переменных, относится и к
макросам, функциям и так далее. Ограничивайте доступ к функциям,
делая их любой ценой статическими.
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; )
// ...
Правила обычного программирования 77
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(); // последнее окно
}
В Си++ вы можете для этой цели использовать статический член класса.
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()
{
// ...
Правила обычного программирования 79
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. Если большой блок следует первым, то
вероятность того, что вычисляться
...Поділитися у соцмережах