Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

Упражнение 1 -6

Перечислите все возможные варианты, которые выдаст приведенное выражение в зависимости от порядка вычислений:

? n = 1;

? printf("%d %d\n", n++, n++);

Попробуйте пропустить это выражение через разные компиляторы смотрите, что получается на практике.

1.3. Стилевое единство и идиомы

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

Будьте последовательны в применении отступов и фигурных скобок.

Отступы помогают воспринять структуру кода, но какой стиль располо­жения лучше? Следует располагать открывающую фигурную скобку, на той же строке, что if, или на следующей? Программисты ведут нескон­чаемые споры о наилучшем расположении текста кода, однако выбор конкретного стиля гораздо менее важен, чем логичность и последова­тельность его применения во всем приложении. Выберите себе раз и на­всегда один стиль — лучше всего, конечно, наш — и используйте его все­гда, не тратьте времени на споры.

Стоит ли вставлять фигурные скобки, когда необходимости в них нет? Как и обычные скобки, дополнительные фигурные скобки могут разре­шить неясные моменты и облегчить понимание структуры кода. Многие опытные программисты во имя все того же постоянства всегда заключают в фигурные скобки тела циклов и условных операторов. Однако если тело состоит лишь из одной строки, то скобки явно не нужны, и мы бы посове­товали их опустить. Впрочем, не забудьте вставить их в тех случаях, когда они помогут разрешить неясности с "висящим else", — подобная неопре­деленность хорошо иллюстрируется следующим примером:

? if (month == FEB) {

? if (year%4 == 0)

? if (day > 29)

? legal = FALSE; else

? else

? if (day > 28)

? legal = FALSE;

? }

В данном фрагменте выравнивание выполнено неправильно, поскольку |на самом деле else относится к строке

? if (day > 29)

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

? if (month == FEB) {

? if (year%4 ==0) {

? if (day > 29)

? legal = FALSE;

? } else {

? if (day > 28)

? legal = FALSE;

? }

? }

Применение средств редактирования с поддержкой синтаксиса умень-|шает вероятность возникновения подобных ошибок.

В рассматриваемом примере даже после исправления отмеченной ^ошибки код трудно понять. В нем будет проще разобраться, если мы за-(.ведем переменную для хранения количества дней в феврале:

? if (month == FEB) {

? int nday;

?

? nday =28;

? if (year%4 == 0)

? nday = 29;

? if (day > nday)

? legal = FALSE;

? }

Код все еще неверен: 2000 год является високосным, а 1900-й и 2100-й — не високосные.Тем не менее такая структура уже значительно проще до­водится до безупречной.

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

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

Одной из наиболее типичных идиом является форма написания цик­ла. Рассмотрим код на С, C++ или Java для просмотра n элементов мас­сива, например для их инициализации. Можно написать этот код так:

? i = 0;

? while (i <= n-1)

? array[i++] = 1.0;

или так:

? for (i = 0; i < n; )

? array[i++] = 1.0;

или даже так:

? for (i = n; --i >= 0; )

? array[i] = 1.0;

Все три способа, в принципе, правильны, однако устоявшаяся, идиома­тическая форма выглядит так:

for (i = 0; i < n; i++)

array[i] = 1.0;

Выбор именно этой формы не случаен. При такой записи обходятся все n элементов массива, пронумерованные от 0 до n-1. Все управление циклом находится непосредственно в его заголовке; обход происходит в возрастающем порядке; для обновления переменной счетчика цикла используется типичная операция инкремента (++). После выхода индекс цикла имеет известное нам значение как раз за последним элементом массива. Те, для кого этот язык как родной, все понимают без пояснений и воспроизводят конструкцию не задумываясь.

В C++ и Java стандартный вариант включает еще и объявление пере­менной цикла:

for (int i = 0; i < n; i++)

array[i] = 1.0;

Вот как выглядит стандартный цикл для обхода списка в С:

for (р = list; р != NULL; р = p->next)

И опять все управляющие элементы цикла находятся непосредственно в выражении for.

Для бесконечного цикла мы предпочитаем конструкцию

for (;;)

однако популярна и конструкция

while (1)

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

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

? for (

? ар = агг;

? ар < агг + 128;

? *ар++ = 0

? )

? {

? ;

? }

Стандартную форму записи цикла воспринять гораздо проще:

for (ар = агг; ар < агг+128; ар++)

*ар = 0;

Небрежно выбранное расположение отступов растянет код на несколь­ко экранов или страниц, что также не улучшает его восприятие.

Еще один стандартный прием — вставлять присваивание внутрь ус­ловия цикла:

while ((с = getchar()) != EOF)

putchar(c);

Выражение do-while используется гораздо реже, чем for и while, по­скольку при его применении тело цикла исполняется как минимум один раз вне зависимости от условий, которые проверяются не в начале цик­ла, а в конце. Во многих случаях это становится причиной появления ошибки, ждущей своего часа, как, например, в следующем варианте цик­ла для getchar:

? do {

? с = getchar();

? putchar(c);

? } while (с != EOF);

В этом случае в переменную будет записано мусорное значение, по­скольку проверка производится уже после вызова putchar. Цикл do-while стоит применять только в тех случаях, когда тело цикла обязатель­но должно быть выполнено хотя бы один раз; некоторые примеры подобных ситуаций мы рассмотрим несколько позже.

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

? int i, *iArray, nmemb;

? iArray = malloc(nmemb * sizeof(int));

? for (i = 0; i <= nmemb; i++ )

? iArray[i] = i;

Место в памяти выделяется для элементов в количестве nmemb — от iArray[0] до iArray[nmemb-1 ], но, поскольку в условии цикла проверка производится на "меньше или равно" (<=), цикл вылезет за границу мас­сива и испортит новой записью то, что хранится за пределами выделен­ной памяти. К сожалению, ошибки подобного типа нередко можно обна­ружить только после того, как данные уже разрушены.

В С и C++ есть также идиомы для выделения места под строки и для работы с ними, а код, который их не использует, зачастую таит в себе ошибку:

? char *p, buf[256];

?

? gets(buf);

? р = malloc(strlen(buf));

? strcpy(p, buf);

Никогда не следует употреблять gets, поскольку не существует способа ограничить размер вводимой информации. Это ведет к проблемам, свя­занным с безопасностью, к которым мы вернемся в главе 6; там мы уви­дим, что всегда лучше использовать fgets. Однако существует и еще одна проблема: функция strlen вычисляет размер строки, не учитывая завершающего строку символа ' \0', а функция st rcpy его копирует. Та­ким образом, выделяется недостаточно места, и st rcpy пишет за предела­ми выделенной памяти. Стандартной формой является следующая:

р = malloc(strlen(buf)+1);

strcpy(p, buf);

или

р = new char[strlen(buf)+1];

strcpy(p, buf);

в C++. Таким образом, если вы не видите +1, то стоит проверить все еще раз.

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

В большинстве сред С и C++ предусмотрена библиотечная функ­ция strdup, которая создает копию строки, используя для этого malloc и st rcpy, что гарантирует от обсуждавшейся чуть выше ошибки. К сожа­лению, strdup не входит в стандарт ANSI С.

Кстати говоря, ни в первой, ни в окончательной версии не проверяет­ся значение, возвращаемое функцией malloc. Мы опустили эту проверку для того, чтобы сфокусировать ваше внимание на теме данного раздела, однако в настоящей программе значения, возвращаемые функциями malloc, realloc, strdup, а также другими функциями, выделяющими па­мять, должны в обязательном порядке проверяться.

Используйте цепочки else-if для многовариантных ветвлений. Стан­дартной формой записи многовариантного ветвления является последо­вательность операторов if. . .else if. . .else:

if (условие 1)

выражение 1

else if (условие 2)

выражение 2

.......

else if (условие n)

выражение n

else

выражение по умолчанию

Условия читаются сверху вниз; по достижении первого условия, которое оказалось верным, выполняется следующее за ним выражение, после чего оставшаяся часть конструкции пропускается. Под выражением мы, естественно, понимаем здесь либо одно выражение, либо группу выра­жений, заключенную в фигурные скобки. Последнее else обрабатывает ситуацию "по умолчанию" — когда ни одна из перечисленных альтернатив не была выбрана. Это замыкающее else может быть опущено, если не пре­дусматривается никаких действий "по умолчанию", однако и в таком случае полезно оставить эту часть с сообщением об ошибке, чтобы отловить усло­вия, которых "не может быть никогда".

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

Последовательность вложенных выражений if — предвестник труд­но читаемого кода, если не заведомых ошибок:

? if (argc == 3)

? if ((fin = fopen(argv[1], "r")) != NULL)

? if ((fout = fopen(argv[2], "w")) != NULL) {

? while ((c = getc(fin)) != EOF)

? putc(c, fout);

? fclose(fin); fclose(fout);

? } else

? printf("He открыть выходной файл %s\n", argv[2]);

? else

? printf("He открыть входной файл %s\n", argv[1]);

? else

? printf("Использование: ср входной_файл выходной_файл \п");

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

if (argc != 3)

printf("Использование: ср входной_файл выходной_файл \п");

else if ((fin = fopen(argv[1], "r")) == NULL)

printf("He открыть входной файл %s\n", argv[1]);

else if ((fout = fopen(argv[2], "w")) == NULL) {

printf("He открыть выходной файл %s\n", argv[2]);

fclose(fin);

} else {

while ((c = getc(fin)) != EOF)

putc(c, fout);

fclose(fin);

fclose(fout);

}

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

? switch (с) {

? case ' - ': sign = -1;

? case ' + ': с = getchar();

? case ‘.’ : break;

? default: if (lisdigit(c))

? return 0;

? }

Здесь используется замысловатая последовательность перескоков в вы-j ражении-переключателе, а все для того, чтобы избежать дублирований одной строки кода! В стандартной, идиоматической форме каждый ва­риант должен оканчиваться выходом (break), с редкими и обязательно откомментированными исключениями. Традиционную структуру про­ще воспринимать, хотя она и получается несколько длиннее:

? switch (с) {

? case '-':

? sign = -1;

? /* перескок вниз */

? case '+':

? с = getchar();

? break;

? case '.':

? break;

? default:

? if (lisdigit(c))

? return 0;

? break;

? }

Некоторое удлинение кода с лихвой окупается увеличением ясности. В данной конкретной ситуации, однако, применение последователь­ности выражений else-if будет даже более понятным:

if (с == ‘-‘) {

sign = -1;

с = getchar();

} else if (с =='+'){

с = getchar();

} else if (с != '. ' && ! isdigit(c)) {

return 0;

}

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

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

case '0':

case '1':

case '2':

.........

break;

При такой записи комментарии не нужны.

Упражнение 1-7

Перепишите выражения C/C++ более понятным образом:

? if (istty(stdin)) ;

? else if (istty(stdout)) ;

? else if (istty(stderr)) ;

? else return(0);

? if (retval != SUCCESS)

? {

? return (retval);

? }

? /* Все идет хорошо! */

? return SUCCESS;

? for (k = 0; k++ < 5; x += dx)

? scanf("%lf", &dx);

Упражнение 1-8

Найдите ошибки в этом фрагменте кода на языке Java и исправьте их, переписав цикл в стандартной форме:

? int count = 0;

? while (count < total) {

? count++;

? if (this.getName(count) == nametable.userName()) {

? return (true);

? }

? }

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]