Голуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с. Руководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета
Скачать 1.36 Mb.
|
?: ) просто потому, что она им кажется непонятной. Тем не менее, это условное выражение может существенно упростить ваш код и, следственно, сделать его лучше читаемым. Я думаю, что: Правила программирования на Си и Си++ 68 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 { Правила программирования на Си и Си++ 70 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 ( некое_условие ) { // ... ♣ Решение о переводе некоторых из идентификаторов, по меньшей мере, спорное. Однако, если вы не знаете английского, то будете лишены возможности оценить юмор автора, которым он оживил большую часть своих примеров. — Ред. Правила программирования на Си и Си++ 72 возвращаемое_значение = НЕЧТО; goto выход; } else { // ... возвращаемое_значение = НЕЧТО_ЕЩЕ; goto выход; } выход: return возвращаемое_значение; } Этот метод не срабатывает в Си++, потому что функции конструктора вызываются неявно в качестве части объявления; объявление часто скрывает вызов функции. Если вы пропускаете объявление, то вы пропускаете и вызов конструктора. Например, в следующей программе деструктор для x вызовется, а конструктор — нет: foo() { if ( некое_условие ) goto выход; некий_класс x; // Конструктор не вызывается. (Оператор // goto перескакивает через него.) // ... выход: // Здесь вызывается деструктор для x // при выходе x из области видимости. } Вследствие этой проблемы лучше всего совсем избегать переходов goto в программах на Си++. 54.1. Всегда предусматривайте возврат значения из блока внешнего уровня Иногда, когда подпрограммы короткие, не стоит стараться обеспечить единственную точку выхода. (По моему мнению, правило "избегай запутанности" перекрывает любое другое правило, с которыми оно входит в конфликт). В этой ситуации всегда старайтесь убедиться, что из подпрограммы нет таких путей, которые не проходят через оператор return . Не так: if ( a ) { // ... return делай_что_нужно(); } Правила обычного программирования 73 else { // ... 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 { } Правила программирования на Си и Си++ 74 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 ); Правила программирования на Си и Си++ 76 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(); Правила обычного программирования 77 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(); Правила программирования на Си и Си++ 78 // ... } destroy_window() { // ... if ( --num_windows == 0 ) // только что уничтожено shutdown_video_system(); // последнее окно } В Си++ вы можете для этой цели использовать статический член класса. 56.4. Если оператор if завершается оператором return , то не используйте else Вместо: if ( условие ) |