Лекция 11

 

4.9. Принципы разработки программ

 

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

В первом из подразделов даны рекомендации, по­лезные при написании любых, в том числе самых простых, программ. Они касаются в основном записи алгоритмов на языке C++ и документирования программ. Технология проектирования и тестирования более крупных систем рассмотрена во втором подразделе. Третий посвящен динамическим структурам, которые широко используются для эффективной об­работки данных.

 

4.9.1. Кодирование и документирование

 

В программировании поставленная задача обычно имеет множество допустимых решений (алгоритмов, способов их записи, стилей программирования). Возникает вопрос, как лучше написать программу? Нужно ли все усилия направить на достижение эффективности (повышение быстродействия, уменьшение требуемой памяти) или есть и какие-то другие критерии оценки качества программы? С опытом у каждого программиста вырабатываются индивидуальные правила и стиль, однако учиться лишь на собственных ошибках – не самый эффективный путь. Кроме того, современное программирование все более становится коллективным, и необходимо учитывать особенности совместной работы коллектива программистов над общей задачей. Конечно, невозможно получить советы на все случаи жизни. Недаром программирование, особенно в ранней стадии развития, считалось искус­ством. Тем не менее, разумное следование общеизвестным рекомендациям поможет избежать многих рас­пространенных ошибок.

Главная цель «правильного» программирования – получение легко читаемой и возможно более простой про­граммы, иногда даже в ущерб эффективности. Многие современные технологии программирования, в конечном счете, направлены на достижение этого результата, поскольку именно он позволяет добиться надежности и простоты модификации программы. Уже на этапе отладки и тестирования проявляется тот факт, что в простой программе намного легче устранить ошибку. Также нужно исходить из того, что в дальнейшем во время ее эксплуатации программа будет подвергаться регулярным изменениям, причем другими программистами. Сопровождение программы обычно составляет гораздо более продолжительный срок, чем ее разработка. Пусть даже есть полная уверенность в том, что с некоторой программой будет иметь дело единственный разработчик. Все равно нет никакой гарантии, что через несколько лет он сам так же легко сможет ее усовершенствовать или устранить выявленный недостаток. Отсюда правило: пиши программу так, чтобы другой (возможно, менее квалифицированный) программист без труда смог понять ее текст и с минимальными усилиями модифицировать. Ни в коем случае не следует демонстрировать свою «эрудицию» («вот как я умею!»), используя самые фантастические конструкции вместо простых. Гибкий язык C++ как никакой другой позволяет так делать, но это касается и других языков. Гордиться можно умением не запутать, а упростить программу, причем так, чтобы при этом как можно меньше пострадала ее эффективность.

Исключения здесь недопустимы. С аналогичных позиций следует подходить и к учебным примерам, решаемым на лабораторных занятиях. Нет возможности в рамках учебного курса заняться серьезным крупномасштабным проектом, да и начинать учиться с таких проектов нецелесообразно. Поэтому маленькие учебные задачи даются с целью в будущем перейти к созданию промышленных систем. Совершенно неверен и даже вреден дифференцированный подход вроде «это маленькая программа, которую и так любой поймет, зачем утруждать себя написанием в ней каких-то комментариев?». Во-первых, маленькая программа со временем может стать больше. Во-вторых, делая себе поблажки, вы препятствуете развитию у себя хорошего стиля программирования. Профессионалу уже не требуется заставлять себя хорошо работать, так как это является частью его стиля, выработанного годами и закрепленного в подсознании. Он даже минимальную программу напишет по правилам, потому что другого в принципе не делает.

Первый шаг при создании программы – записать алгоритм на некотором языке, близком к естественному. Такое описание полезно по нескольким при­чинам: оно помогает в деталях продумать алгоритм, найти на самой ранней ста­дии некоторые ошибки, разбить будущую программу на логическую последовательность блоков, а также обеспечить комментарии к ней.

Если алгоритм можно разбить на последовательность законченных действий (к этому надо стремиться), каждое из них оформляется в виде функции. Любая функция должна решать единственную задачу (не следует объеди­нять два коротких независимых фрагмента). Размер функции может варьироваться в широких пределах. Он зависит от объема выделяемого в функцию завершенного фрагмента кода. Желательно, чтобы тело функции помещалось на 1–2 экранах: одинаково сложно разбираться в програм­ме, содержащей несколько необъятных функций, и в россыпи из сотен подпро­грамм по нескольку строк каждая.

Один из важнейших принципов «хорошего» программирования – не допускать дублирования кода или данных. Дублирование ведет к ненадежности, так как при модификации необходимо изменить все копии. Упустив хотя бы одну, получим ошибку. Поэтому, если некоторые действия встречаются в программе хотя бы дважды, их рекомендуется оформить как функцию. Однотипные действия оформляются в виде перегруженных функций или функций с параметрами. При этом могут также помочь шаблоны. Короткие функции имеет смысл объявить со спецификатором inline.

Необходимо тщательно выбирать имена переменных. Правильно подобранные имена могут сделать программу в определенной степени самодокументированной. Неудачные имена, наоборот, служат источником проблем. Не следует увлекаться сокра­щениями, так как они ухудшают читаемость, и часто можно забыть, как именно было со­кращено то или иное слово. Общая тенденция состоит в том, что чем больше об­ласть видимости переменной, тем более длинное у нее имя. Перед таким именем часто ставится префикс типа (одна или несколько букв, по которым можно опре­делить тип переменной). Для счетчиков коротких циклов, напротив, достаточно обой­тись однобуквенными именами типа i, j или k. Имена макросов предпочтитель­но записывать заглавными буквами, чтобы отличать их от других элементов программы. Не рекомендуется использовать имена, начинающиеся с символа подчеркивания, имена типов, оканчивающиеся на _t, а также идентификаторы, совпадающие с именами ресурсов стандартной библиотеки C++.

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

Рекомендуется минимизировать область видимости любой переменной. Локальные переменные предпочтительнее глобальных. Если глобальная перемен­ная все же необходима, лучше объявить ее статической, что ограничит область ее действия одним исходным файлом.

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

Входные параметры функции, которые не должны в ней изменяться, следует передавать как константные ссылки, а не по значению. Кроме улучшения читае­мости программы и уменьшения возможности случайных ошибок, этот способ гораздо более эффективен, особенно в случае передачи сложных объектов. Ис­ключение составляют параметры, размер которых меньше размера указателя – их эффективнее передавать по значению (не забывая указывать const).

Выходные параметры функции предпочтительнее передавать по адресу, а не по ссылке. Тогда из семантики вызова функции легко понять, что внутри нее параметр изменяется.

Нельзя возвращать из функции ссылку на локальную переменную, потому что она (если не статическая) автоматически уничтожается при выходе из функции, содержащей ее обла­сть действия. Не рекомендуется возвращать ссылку на переменную, созданную внутри функции в динамической памяти с помощью функции malloc или операции new, так как это приводит к трудно контролируемым утечкам памяти.

Следует избегать использования в программе констант в явном виде. Они должны иметь осмысленные имена, заданные с помощью const или enum (последнее предпочтительнее, так как специальная память под перечисление не выделяется). Символиче­ское имя делает программу более понятной. Кроме того, при необходимости из­менить именованную константу, это делается лишь в одном месте про­граммы (см. также выше о дублировании кода).

Для записи каждого фрагмента алгоритма необходимо использовать наиболее подходящие средства языка. Теоретически можно любой цикл реализовать с по­мощью операторов if и goto, но это неприемлемо, поскольку с помощью опе­раторов цикла те же действия легче читаются, а компилятор генерирует более эффективный код. Ветвление на несколько направлений предпочтительнее реализовывать с помощью оператора switch, а не нескольких if. Массив указателей на функции поможет организовать эффективный способ вызова некоторой функции из их набора.

Следует избегать лишних вызовов функций. Например, вместо операторов

if ( strstr (a, b) > 0 ) { ... }

else if ( strstr (a, b) < 0 ) { ... }

else if ( strstr (a, b) == 0 ) { ... }

лучше написать

int is_equal = strstr (a, b);

if ( is_equal > 0 ) { ... }

else if ( is_equal < 0 ) { ... }

else { ... } // Здесь также ликвидирована лишняя проверка условия is_equal == 0

Если первая ветвь оператора if содержит передачу управления, нет необходимости использовать else:

if ( is_equal > 0 ) { ... break; }

if ( is_equal < 0 ) { ... return;}

{ ... } // здесь is_equal == 0

Бессмысленно и неэстетично применять проверку на неравенство нулю или, что еще хуже, на равенство true или false:

bool is_busy;

if ( is_busy == true ) { ... } // Плохо! Лучше if ( is_busy )

if ( is_busy == false ) { ... } // Плохо! Лучше if ( !is_busy )

char s[80];

while ( fgets (s) != NULL ) { ... } // Плохо! Лучше while ( fgets (s) )

while ( a == 0 ) { ... } // Можно while ( !a )

Если одна из ветвей условного оператора гораздо короче, чем другая, более ко­роткую ветвь if лучше поместить сверху для улучшения читаемости. Обе альтернативы (if и else) лучше писать в разных строках на одном уровне. Лишь короткий оператор if можно целиком записать в одной строке.

В некоторых случаях условная операция лучше условного оператора:

if ( z ) i = j; else i = k; // Лучше так: i = z ? j : k;

При использовании циклов рекомендуется объединять инициализацию, проверку условия выхода и приращение в одном месте. Необходимо выбирать наиболее подходящий тип оператора цикла.

Необходимо предусматривать выдачу сообщений об ошибках. Такое сообщение должно быть информативными и подсказывать пользовате­лю, как ее исправить. Например, при вводе неверного значения в сообщении дол­жен быть указан допустимый диапазон.

Операции выделения и освобождения динамической памяти следует помещать в одну и ту же функцию. Утечки памяти (когда ее выделили, а освободить забы­ли) создают большие проблемы в программах, продолжительность работы кото­рых не ограничена: на серверах баз данных, в операционных системах и так далее.

Полезно дать почитать свою программу кому-нибудь другому. В тех местах, которые не будут ему понятны, внести в текст комментарии. Комментарии имеют весьма важное значение для читаемости программы.

Приведем рекомендации относительно комментариев и форматирования текста програм­мы. В силу лаконичности и гибкости языка C++ программы на нем плохо читаемы (например, хуже Pascal). Поэтому в них особенно важно придерживаться хорошего стиля при форматировании и документировании. Основная часть документации должна нахо­диться в самом тексте программы. Хорошие комментарии написать почти так же слож­но, как и хорошую программу. Для этого нужно иметь определенные литературные способности.

Комментарии должны представлять собой грамматически правильные предложения без сокраще­ний и со знаками препинания. Они предназначены для облегчения понимания текста программы, а не наоборот. Они также не должны подтверждать очевидное. Например, бессмысленны фразы типа «вы­зов функции f» или «описание переменных». Зато полезно указать, зачем вызывается функция или каково предназначение описываемых переменных. Если комментарий к фрагменту про­граммы занимает несколько строк, его лучше разместить до фрагмента, а не справа от него. Левый отступ комментария дол­жен соответствовать отступу комментируемого блока.

// Комментарий, описывающий,

// что происходит в следующем ниже блоке программы.

{

/* Блок программы */

}

Функции и другие логически завершенные фрагменты кода рекомендуется разделять пустыми строками и комментариями.

Программу необходимо записывать структурировано, начиная равнозначные конструкции с одного вертикального уровня, и сдвигая вложенные блоки на 2–4 символа вправо. Желательно, чтобы закрывающая фигурная скобка находилась строго под соответствующей ей открывающей. Рекомендуется также формати­ровать текст по столбцам там, где это возможно.

#include <string.h>

#include <stdlib.h>

#include <stdio.h>

 

const N_MAX = 10;

int main()

{

double m[N_MAX]; // комментарий

const char s[] = "2, 38, 5, 70, 0, 0, 1"; // комментарий

char *p = (char *)s; // комментарий

int i = 0; // комментарий

 

// Преобразование строки s в массив чисел m

do

{

m[i++] = atof(p);

if (i > N_MAX - 1) break; // Чтобы не выйти за границу массива

} while (p = strchr(p, ','), p++);

 

// Выдача полученного массива на экран

for (int k = 0; k < i; k++) printf ("%5.2f ", m[k]);

 

return 0;

}

 

Для улучшения ясности можно изменить величину отступов:

if ( is_best ) best ( );

else if ( is_bad ) worse ( );

else if ( is_vovochka ) worst ( );

Рекомендуется помечать конец длинного составного оператора:

while ( gets(s) )

{

for (d = 0; i < N; i++ )

{

for (j = 0; j < N; j++)

{

// … две страницы кода

} // for (j = 0; j < N; j++)

} // for (d = 0; i < N; i++)

} // while ( gets(s) )

Не следует размещать в одной строке много операторов. Как и в русском языке, после знаков препинания должны использоваться пробелы:

f=a+b; // плохо! Лучше f = а + b;

Изложенные выше рекомендации не охватывают все ситуации, возникающие при кодировании программ, причем иногда вступают в противоречие друг с другом. Это лишь некоторая стартовая информация, которая поможет начинающему программисту выработать индивидуальный стиль хорошего программирования.