Стиль программирования также индивидуален, как и предпочтения в одежде. К сожалению, также как многие программисты не могут победить в конкурсе моды, так и код имеет плохой стиль. Этот код обычно просто обнаружить по тому, насколько трудно его понять.
Хороший стиль программирования может дать почувствовать разницу между программами, которые просты в отладке и модификации, и теми, от которых просто хочется бежать подальше.
Есть несколько аспектов стиля программирования. Нет совершенного стиля, который бы удовлетворял всех. Каждый программист должен найти свой стиль, комфортный для него или её. Его целью должно быть написание программы понятной для чтения и понимания, а не попытка привести в заблуждение человека, которому придётся исправлять ошибки в коде.
Кроме того, хороший стиль программирования приводит к уменьшению времени необходимого на написание программы и, конечно же, к уменьшению времени затраченного на её отладку и модификацию.
Следующие разделы обсуждают различные аспекты стиля программирования. Они отражают личные пристрастия автора, но основаны на годах, потраченных им на понимание кода, чаще хорошего, а иногда плохого, и в основном — своего собственного!
Возможно, важнейшей стороной стиля является однообразие. Пытайтесь, насколько это возможно, использовать одинаковые правила на протяжении всей программы. Смешивая разные стили в одной программе, можно сбить с толку даже лучших программистов, пытающихся разобраться в коде.
Если в проекте участвуют несколько программистов, то до того, как будет написана первая строка кода, будет полезно обсудить основные правила стиля. Некоторые правила важнее, чем другие. Убедитесь, что все понимают эти правила и согласны им следовать.
При просмотре участка кода часто трудно определить, где находится объект. Один требует просмотра всех деклараций объектов внутри функции, другие декларированы вообще вне какой-либо функции, а декларации третьих включены из другого исходного файла. Если не следовать строгим правилам именования объектов, то каждое место в программе потребует утомительных поисков.
Использование смешанного регистра в именах с точными правилами может сделать работу более простой. Не важно, какие правила установлены, если эти правила применяются на протяжении всей программы.
Рассмотрим следующий примерный набор правил, которые используются в этой книге:
int x, counter, limit; float save_global; struct s * sptr;
static int TotalCount; extern float GlobalAverage; static struct s SepStruct;
extern int TrimLength( char * ptr, int len );
static field * CreateField( char * name );
#define FIELD_LIMIT 500 #define BUFSIZE 32 enum { INVALID, HELP, ADD, DELETE, REPLACE };
typedef
полностью набираются буквами верхнего
регистра,
typedef struct { float real; float imaginary; } COMPLEX;
Таким образом, можно определить продолжительность хранения и область видимости каждого идентификатора, не обращаясь к контексту. Рассмотрим фрагмент программы:
chr = ReadChar(); if( chr != EOF ) { GlbChr = chr; }
Используя вышеприведённые правила,
ReadChar
— функция,
chr
— объект с автоматической продолжительностью
хранения, определённый внутри текущей функции,
EOF
— константа,
GlbChr
— объект со статической продолжительностью
хранения.
Заметьте, что библиотечные не используют в именах буквы смешанного регистра.
Функция main
также не начинается с прописной буквы
M
. При использовании приведённого выше стиля, библиотечные
функции будут отличаться от других регистром букв.
Именование объектов может быть решающим в упрощении поиска ошибок или
внесении изменений. Использование имён объектов таких, как
linecount
, columns
и rownumber
сделает
программу более читабельной. Конечно, короткие формы имён прокрадутся в код
(немногие программисты любят набирать больше, чем это нужно на самом деле),
но их нужно использовать рассудительно.
Однообразие в именовании также помогает сделать код более читаемым. Если
структура используется по всюду в программе, то имена объектов, которые
указывают на неё, должны быть одинаковыми. Используя пример таблицы символов,
объект по имени symptr
может быть использован везде для
обозначения «указателя на символьную структуру». Программист, видя этот
объект, автоматически будет знать для чего он декларирован.
Точно также необходимы подходящие имена функций. Имена типа
DoIt
, оберегая пишущего программиста от необходимости придумать
хорошее имя, создадут другому трудности в понимании того, что тут должно
произойти.
Ниже приведена верная функция:
static void BubbleSort( int list[], int n ) /**********************************/ { int index1 = 0; int index2; int temp; if( n < 2 )return; do { index2 = index1 + 1; do { if( list[ index1 ] > list[ index2 ] ) { temp = list[ index1 ]; list[ index1 ] = list[ index2 ]; list[ index2 ] = temp; } } while( ++index2 < n ); } while( ++index1 < n-1 ); }
(Компилятор поймёт, что здесь всё верно, но программист найдёт это очень трудным для проверки.) Ниже приведена та же функция, но с использованием отступов, чтобы долее чётко проиллюстрировать структуру функции:
static void BubbleSort( int list[], int n ) /*****************************************/ { int index1 = 0; int index2; int temp; if( n < 2 )return; do { index2 = index1 + 1; do { if( list[ index1 ] > list[ index2 ] ) { temp = list[ index1 ]; list[ index1 ] = list[ index2 ]; list[ index2 ] = temp; } } while( ++index2 < n ); } while( ++index1 < n-1 ); }
В общем, делать отступ для каждого уровня вложенности кода на равное
расстояние, например, 4 пробела является хорошей практикой. Так, субъект
команды if
всегда отступает на 4 пробела внутрь if
.
При этом способе все циклы и команды выбора будут выступать, облегчая поиск
конца команды.
Ниже приведены некоторые рекомендуемые образцы организации отступов. Эти образцы использованы в этой книге.
int Fn( void ) /************/ { /* отступ на 4 */ }
if( условие ) { /* отступ на 4 */ } else { /* отступ на 4 */ }
if( условие ) { /* отступ на 4 */ } else if( условие ) { /* отступ на 4 от первого if */ if( условие ) { /* отступ на 4 от ближайшего if */ } } else { /* отступ на 4 от первого if */ }
switch( условие ) { case VALUE: /* отступ на 4 от switch */ case VALUE: default: }
do { /* отступ на 4 */ while( условие ); while( условие ) { /* отступ на 4 */ }
for( a; b; c ) { /* отступ на 4 */ }
Два других популярных стиля отступов:
if( условие ) { команды }
и
if( условие ) { команды }
Какой конкретно используется стиль, не важно. Главное, чтобы он был одинаков везде.
Длинные списки деклараций объектов могут быть трудными для чтения, если об этом не позаботиться. Рассмотрим декларации:
struct flentry *flptr; struct fldsym *sptr; char *bufptr,*wsbuff; int length;
А теперь рассмотрим те же декларации, но с некоторым визуальным выравниванием:
struct flentry * flptr; struct fldsym * sptr; char * bufptr; char * wsbuff; int length;
Проще просматривать список объектов, когда все их имена начинаются с одинаковой позиции.
Функция длиной в несколько сот строк может оказаться трудной для восприятия, особенно, если её просматривать на терминале, который имеет 25 строк. Кроме того, большие функции склонны иметь много вложенных программных структур, что затрудняет понимание их логики.
Функцию, которая полностью умещается на экране, проще изучить и понять. Программные конструкции не выглядят запутанными. Большие функции часто можно разбить на маленькие, которые будет проще поддерживать.
static
для большинства функций
Большинство функций модуля не требуется вызывать из других модулей. Но, если
в декларации функции не используется ключевое слово static
, то
функция автоматически получает внешнюю связь. Это может привести к
быстрому увеличению количества внешних символов, которое может вызвать
конфликты имён. К тому же некоторые линкеры могут накладывать ограничения на
это число.
Только те функции, которые должны иметь внешнюю связь, должны быть внешними.
Все другие определения функций должны начинаться с ключевого слова
static
.
Хорошей идеей будет также начинать определения внешних функций с ключевого
слова extern
, не смотря на то, что это и так принято по
умолчанию.
Статические объекты, которые декларированы вне определения какой-либо функции, и используются повсюду в модуле, как правило должны быть декларированы вместе, например до определения первой функции. Если они будут располагаться близко к началу модуля, то их будет проще найти.
Если объект со статической продолжительностью хранения присутствует в одном модуле и имеет внутреннюю связь, то в другом модуле нельзя декларировать объект с таким же именем. Программиста это может привести в заблуждение.
Более того, если объект существует в внешней связью, то в модуле нельзя декларировать другой объект с тем же именем и с внутренней связью. Этот второй объект внутри модуля будет перекрывать первый, что может, скорее всего, запутать того, кто будет анализировать код.
Для организации структур данных и связанной с ними информации можно использовать включаемые файлы. Это нужно делать, когда одна и та же структура используется в разных модулях, и даже, когда она только в одном месте.
В общем, каждый включаемый файл должен содержать структуры и относящуюся к ним информацию для некоторого аспекта программы. Например, файл, описывающий таблицу символов, может содержать структуры или другие необходимые типы вместе с полезными самообъявляющимися константами.
Прототипы функций очень полезны для исключения ошибок при вызове функций. Если у каждой функции в программе есть прототип (и этот прототип используется), то будет труднее передать ей неправильное число аргументов или аргументы неправильного типа, или неверно использовать результат.
В примере с таблицей символов, включаемый файл, который описывает структуру таблицы символов и другие связанные с ней глобальные объекты или константы, также должен содержать прототипы функций, которые будут использоваться для доступа к таблице. Другим подходом будет иметь отдельный файл содержащий прототипы функций, возможно с другими соглашениями об именовании. Например,
#include "symbols.h" #include "symbols.fn"
включит структуры и необходимые значения из файла symbols.h
, а
прототипы функций из symbols.fh
.
Так же как большая функция, в которой делается очень много, может запутать и слишком длинная команда. Раньше, для получения лучшего кода программисту приходилось комбинировать много операций в одной команде. С современными компиляторами разбиение команды на две или более приведёт к генерации одинакового кода, а программа при этом будет проще для понимания.
Распространённым примером команды, которую можно разбить является
if( (c = getchar()) != EOF ) {
Раньше эта команда позволяла компилятору не сохранять значение переменной
c
, а затем не перезагружать её для сравнения с EOF
.
Однако эквивалентное
c = getchar(); if( c != EOF ) {
читается проще, а большинство компиляторов произведут такой же код.
goto
слишком часто
Команда goto
является очень мощным инструментом, но ей очень
легко злоупотребить. Вот несколько общих правил использования
goto
:
Если этого недостаточно, то вот ещё:
goto
для перехода на метку, которая
находится выше. Это первый шаг для создания запутанного кода («спагетти–код»).
Для этого можно использовать циклы.
goto
для входа внутрь блока (составной
команды). В блок нужно входить, проходя через его открывающую скобку.
goto
для выхода из вложенных блоков, когда
недостаточно команды break
.
И кроме этого, используйте goto
как можно реже.
Комментарии являются одним из самых решающих моментов в стиле программирования. Независимо от того, насколько хорошо написана программа, часть кода может быть трудной для понимания. Комментарии могут дать разъяснения по поводу того, что собирается сделать программа.
Каждое определение функции должно начинаться с краткого комментария, описывающего то, что делает функция.
Каждый модуль должен начинаться комментарием, описывающим его предназначение. Неплохо также обозначить, кто написал его и когда, кто его изменял, почему и когда. Последний набор информации часто называется контрольным следом, так как позволяет программисту отследить развитие модуля и одновременно то, кто его изменял.
Ниже приведён «контрольный след» из некоторого реального модуля:
/* Изменено: Кем: Почему: * ======== ==== ====== * 84/04/23 Dave McClurkin Начальная реализация * 84/11/08 Jim Graham Реализован некомбинируемый TOTAL; * добавлены MAXIMUM,MINIMUM,AVERAGE * 84/12/12 Steve McDowell Добавлен вызов CheckBreak * 85/01/12 ... Исправлена ошибка переполнения * 85/01/29 Alex Kachura Сохраняет значение поля TYP_ * 86/01/31 Steve McDowell Перешли к использ-ю численного аккумулятора * 86/12/10 ... Удалена часть закомментированного кода * 87/02/24 ... Все команды сделаны комбинируемыми */