- •1. Стиль 10
- •3. Проектирование и реализация 63
- •4. Интерфейсы 85
- •5. Отладка 115
- •6. Тестирование 134
- •7. Производительность 157
- •8. Переносимость 180
- •9. Нотация 203
- •Введение
- •Брайан в. Керниган
- •1.1. Имена
- •1.2. Выражения
- •Упражнение 1 -6
- •1.3. Стилевое единство и идиомы
- •1.4. Макрофункции
- •1.5. Загадочные числа
- •1.6. Комментарии
- •1.7. Стоит ли так беспокоиться?
- •Дополнительная литература
- •2.1. Поиск
- •2.2. Сортировка
- •2.3. Библиотеки
- •2.4. Быстрая сортировка на языке Java
- •2.5. "О большое"
- •2.6. Динамически расширяемые массивы
- •2.7. Списки
- •Упражнение 2-8
- •2.8. Деревья
- •Упражнение 2-15
- •2.10. Заключение
- •Дополнительная литература
- •Проектирование и реализация
- •3.1. Алгоритм цепей Маркова
- •3.2. Варианты структуры данных
- •3.3. Создание структуры данных в языке с
- •3.4. Генерация вывода
- •3.5.Java
- •Into the air. When water goes into the air it
- •3.7. Awk и Perl
- •3.8. Производительность
- •3.9. Уроки
- •Дополнительная литература
- •4. Интерфейсы
- •4.1. Значения, разделенные запятой
- •4.2. Прототип библиотеки
- •4.3. Библиотека для распространения
- •Упражнение 4-4
- •4.5 Принципы интерфейса
- •4.6. Управление ресурсами
- •4.7. Abort, Retry, Fail?
- •4.8. Пользовательские интерфейсы
- •Дополнительная литература
- •5. Отладка
- •5.1. Отладчики
- •5.2. Хорошие подсказки, простые ошибки
- •5.3, Трудные ошибки, нет зацепок
- •5.4. Последняя надежда
- •5.5. Невоспроизводимые ошибки
- •5.6. Средства отладки
- •5.7. Чужие ошибки
- •5.8. Заключение
- •Дополнительная литература
- •6. Тестирование
- •6.1. Тестируйте при написании кода
- •6.2. Систематическое тестирование
- •6.3. Автоматизация тестирования
- •6.4. Тестовые оснастки
- •6.5. Стрессовое тестирование
- •6.6. Полезные советы
- •6.7. Кто осуществляет тестирование?
- •6.8. Тестирование программы markov
- •6.9. Заключение
- •Дополнительная литература
- •7.Производительность
- •7.1. Узкое место
- •7.2. Замеры времени и профилирование
- •7.3. Стратегии ускорения
- •7.4. Настройка кода
- •7.5. Эффективное использование памяти
- •7.6. Предварительная оценка
- •7.7. Заключение
- •Дополнительная литература
- •8. Переносимость
- •8.1. Язык
- •8.2. Заголовочные файлы и библиотеки
- •8.3. Организация программы
- •8.4. Изоляция
- •8.5. Обмен данными
- •8.6. Порядок байтов
- •8.7. Переносимость и внесение усовершенствований
- •8.8. Интернационализация
- •8.9. Заключение
- •Дополнительная литература
- •9.1. Форматирование данных
- •9.2. Регулярные выражения
- •Упражнение 9-12
- •9.3. Программируемые инструменты
- •9.4. Интерпретаторы, компиляторы и виртуальные машины
- •9.5. Программы, которые пишут программы
- •9.6. Использование макросов для генерации кода
- •9.7. Компиляция "налету"
- •Дополнительная литература
- •Интерфейсы
- •Отладка
- •Тестирование
- •Производительность
- •Переносимость
Упражнение 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);
? }
? }
