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

6.1. Тестируйте при написании кода

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

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

Приводимый фрагмент кода, моделирующий f gets, считывает симво­лы, пока не найдет символ перевода строки или не заполнит буфер:

? int i;

? char s[MAX];

?

? for (i = 0; (s[i] = getchar()) != '\n' && i < MAX-1; ++i)

? ;

? s[--i] = ‘\0';

Представьте себе, что вы только что написали этот цикл. Теперь мысtнно выполните за него обработку строки. Первое граничное условие, которое надо проверить, очевидно: пустая строка. Если представить строку, содержащую единственный символ перевода строки, то нетрудно убедиться, что цикл остановится на первой итерации со значением i, 1 равным 0, так что в последней строке i будет уменьшено до -1 и, следовательно, запишет нулевой байт в элемент

s[-1 ], который находится вне границ массива. Итак, проверка первого же граничного условия обнаружила ошибку.

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

? for (i = 0; i < MAX-1; i++)

? if ((s[i] = getchar()) == '\n')

? break;

? s[i] = ‘0\’;

Повторив в уме первый тест, мы удостоверимся, что теперь строка, содержащая только символ перевода строки, обрабатывается корректно: i равно 0, первый же введенный символ прерывает работу цикла, а '\0' сохраняется в s[0]. Проверив схожим образом варианты с вводом одного и двух символов, замыкаемых символом перевода строки, мы убедимся, что цикл работает корректно вблизи нижней границы ввода.

Теперь надо проверить и другие граничные условия. Ситуации, когда во вводе содержится очень длинная строка или не содержится символов перевода строки, предусмотрены кодом — на этот случай существует ограничение i значением МАХ-1. Однако что будет, если ввод абсолютно пуст (в нем нет вообще ни одного символа) и первый

же вызов getchar возвратит значение EOF? Надо добавить проверку

и для такого случая:

? for (i = 0; i < MAX-1; i++)

? if ((s[i] = getchar()) == ‘\0’ | | s[i] == EOF)

? break;

? s[i] = ‘\0’;

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

Следующим шагом будет проверка ввода около другой границы, ког­да массив почти заполнен, полностью заполнен и наконец переполнен, особенно если как раз в этот момент и встречается символ перевода строки. Мы не будем расписывать здесь все детали этих тестов; выполните их самостоятельно, — это очень хорошее упражнение. Задумавшись о все­возможных граничных условиях, нам придется решить, что делать в слу­чае, если буфер заполнится до того, как во вводе встретится ' \n'; этот пробел в спецификации должен быть ликвидирован на ранней стадии написания программы.

Тестирование граничных условий особенно эффективно для поиска ошибок выхода за границы массива на 1 (off-by-one errors). Попракти­ковавшись, вы сделаете такую проверку своей второй натурой, и мно­жество тривиальных ошибок будет устранено в самый момент возник­новения.

Тестируйте пред- и постусловия. Еще один способ предотвратить воз­никновение проблем — удостовериться в том, что ожидаемые или необходимые условия удовлетворяются до (предусловие) или после (постусловие) выполнения некоторого блока кода. Проверяя вводимые значения на соответствие допустимому диапазону, мы встретились как раз с примером тестирования предусловий. Ниже приведена функция для вычисления среднего из п элементов массива. При значениях п, меньших или равных 0, функция работает некорректно:

? double avg(double a[], int n)

? { /* average - среднее */

? int i;

? double sum;

?

? sum = 0.0;

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

? sum += a[i];

? return sum / n;

? }

Что будет делать эта функция, если п будет равно 0? Массив, не со­держащий элементов, — вполне осмысленный элемент программы, а вот среднее значение его элементов не имеет никакого смысла. Долж­на ли функция позволять системе отлавливать деление на 0? Прерывать функцию? Сообщать об ошибке? Без предупреждения возвращать какое-нибудь нейтральное значение? А что если п вообще отрицательно, что абсолютно бессмысленно, но не невозможно? Как мы уже говорили в главе 4, нам. представляется правильным в случае, если n меньше либо равно нулю, возвращать 0 в качестве среднего значения:

return n <= 0 ? 0.0 : sum/n;

но однозначно правильного ответа здесь не существует.

Имеется, правда, гарантированно неверное мнение— игнорировать проблему. В ноябре 1998 в журнале Scientific American был описан инцидент, произошедший на борту американского ракетного крейсера Yorktown. Член команды по ошибке вместо значимого числа ввел 0, что привело к ошибке деления на нуль; ошибка разрослась з конце концов силовая установка корабля оказалась выведена из строя. Несколько часов Yorktown дрейфовал по воле волн — а все из-за того, что в программе не была осуществлена проверка диапазона вводимых значений.

Используйте утверждения. В С и C++ существует возможность ис­пользования специального механизма утверждений (assertions) (в <assert.h>), который позволяет включать в программу проверку н пред- и постусловий. Поскольку невыполненное утверждение прерывает работу программы, используют их, как правило, в ситуациях, когда сбой на самом деле не ожидается, а при его возникновении нет возможности продолжить работу нормально. Только что рассмотренный ример можно было бы дополнить утверждением, вставленным перед началом цикла:

assert (n > 0 );

Если утверждение не выполнено, программа прерывается; сопровождается это выдачей стандартного сообщения:

Assertion failed: n > 0, file avgtest.c, line 7

Abort (crash)

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

Используйте подход защитного программирования. Полезно встав­лять некоторый код для обработки (хотя бы просто предупреждения пользователю) случаев, которых "не может быть никогда", то есть ситуа­ций, которые теоретически не должны случиться, но все же имеют место (например, из-за сбоя где-то в другом участке программы). Хороший пример — добавление проверки на нулевой или отрицательный размер массива в функцию avg. Еще одним примером может стать программа, выставляющая оценки по американской системе; очевидно, что отрица­тельных или очень больших значений появиться в ней не может, но луч­ше все же это проверить:

if (grade < 0 | | grade > 100) /* этого не может быть */

letter = '?';

else if (grade >= 90)

letter = 'A';

else

……

Это пример защитного программирования (defensive programming), при котором вы убеждаетесь в том, что программа защищена от непра­вильного использования или некорректных данных. Пустые указате­ли, индексы вне диапазона, деление на ноль и другие ошибки можно обнаружить на ранних стадиях жизни программы или нейтрализовать. Если бы все программисты применяли принципы защитного програм­мирования, с Yorktown ничего бы не произошло, что бы там ни вводил оператор.

Проверяйте коды возврата функций. Одним из приемов защиты, кото­рым программисты почему-то незаслуженно пренебрегают, является проверка возвращаемого значения библиотечных функций и системных вызовов. Значения, возвращаемые функциями, обслуживающими ввод, такими как fread и fscanf, надо всегда проверять. Также обяза­тельно надо проверять и возвращаемые значения вызовов открытий файлов типа fopen. Если чтение или открытие файла по каким-то причи­нам не выполняется, не может быть и речи о нормальном продолжении работы программы.

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

fp = fopen(outfile, "w");

while (...) /* вывод в outfile */

fprintf(fp, ...);

if (fclose(fp) == EOF) { /* нет ли ошибок? */

/* произошла какая-то ошибка вывода */

}

Последствия ошибок вывода могут быть серьезными. Если записываемый файл является обновленной версией уже существующего файла, такая проверка спасет вас от удаления старой версии при отсутствии cформировавшейся новой.

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

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

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

1. Этот код должен вычислять факториалы:

? int factorial(int n)

? {

? int fac;

? fac = 1;

? while (n--)

? fac *= n;

? return fac;

? }

2. Этот отрывок должен распечатывать символы строки, каждый в от­дельной строке:

? i = 0;

? do {

? putchar(s[i++]);

? putchar('\n');

? } while (s[i] != ‘\0’);

3. Предполагается, что эта функция будет копировать строку из одного места в другое (из источника src в приемник dest):

? void strcpy(char *dest, char *src)

? {

? int i;

?

? for (i = 0; src[i] != ‘\0’; i++)

? dest[i] = src[i];

? }

4. Еще один пример копирования строк — на этот раз копируется n сим­волов из s в t:

? void strncpy(char *t, char *s, int n)

? {

? while (n > 0 && *s != ‘\0’) {

? *t = *s;

? t++;

? s++;

? n--;

? }

? }

5. Сравнение чисел:

? if ( i > j)

? printf("%d больше %d.\n", i, j);

? else

? printf ("%d меньше %d.\n", i, j);

6. Проверка класса символа:

? if (с >= 'A’ && с <= Z) {

? if (с <= 'L')

? cout « " первая половина алфавита ";

? else

? cout « " вторая половина алфавита ";

? }

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

Мы пишем эту книгу в конце 1998 года, поэтому призрак пробле­мы 2000 года неотступно стоит перед нами как самая глобальная ошибка граничных условий.

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

2. Как вы будете тестировать стандартную функцию ctime, которая возвращает строковое представление даты в такой форме:

Fri Dec 31 23:58:27 EST 1999\n\0

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

3. Опишите, как вы будете тестировать программу-календарь, которая генерирует вывод в таком виде:

January 2000

S M Tu W Th F S

1

2 3 4 5 6 7 8

9 10 11 12 13 14 15

16 17 18 19 20 21 22

23 24 25 26 27 28 29

30 31

4. Какие еще граничные условия в отношении времени и дат существу­ют в используемой вами системе? Как бы вы оттестировали их?

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