Содержание

Часто встречающиеся ошибки

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

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

Некоторые компиляторы, включая Watcom C/16 и C/32, имеют встроенную проверку наличия таких ошибок и предупреждают, когда для них возникают условия. Наверное лучше исправить код, который вызывает предупреждения, чем отключить эту проверку.

В последующих разделах демонстрируются некоторые распространённые ошибки, и обсуждается как их избежать.

Присвоение вместо сравнения

Фрагмент кода

chr = getc();
if( chr = 'a' ) {
    printf( "буква 'a'\n" );
} else {
    printf( "буква не 'a'\n" );
}

никогда не напечатает сообщение буква не 'a', каким бы ни было значение chr.

Проблема находится во второй строке примера. Команда

if( chr = 'a' ) {

присваивает объекту chr символьную константу 'a'. Если значение chr не равно нулю, то исполняется команда, идущая после if.

Значение константы 'a' никогда не равно нулю, поэтому всегда будет исполняться первая часть if. Вторую часть можно было и не писать!

Конечно же, правильным способом написать вторую строку является

if( chr == 'a' ) {

изменив = на ==. Эта команда говорит, что нужно сравнить значение chr с константой 'a' и выполнить команду после if, если они равны.

Использование одного знака равенства (присвоение) вместо двух (проверка на равенство) — частая ошибка, которую делают программисты, особенно те, кто хорошо знаком с такими языками как Pascal, в котором одиночный знак = означает проверку на равенство.

Неожиданный приоритет операторов

Фрагмент кода

if( chr = getc() != EOF ) {
    printf( "Значением chr является %d\n", chr );
}

всегда будет печатать 1, пока getc не встретит конец файла. Здесь сделана попытка присвоить значение возвращаемое getc переменной chr, а затем сравнить её значение с EOF.

Проблема находится в первой строке, где делается вызов библиотечной функции getc. Значение, которое она возвращает (целое, которое представляет символ, или EOF, если встретится конец файла) сравнивается с EOF, и, если они не равны (т.е. не конец файла), то объекту chr присваивается 1. Иначе, если они равны, присваивается 0. Потому chr всегда будет равно либо 0, либо 1.

Правильным способом написать этот код будет

if( (chr = getc()) != EOF ) {
    printf( "Значением chr является %d\n", chr );
}

Дополнительные скобки заставляют сделать сначала присвоение, а потом — сравнение.

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

Скрытая ошибка во включаемом файле

Предположим, что файл mytypes.h содержит строку

typedef int COUNTER

а основной файл компилируется, начиная с

#include "mytypes.h"

extern int main( void )
/*********************/
{
    COUNTER x;
/* ... */
}

Попытка компилировать основной файл приведёт к сообщению об ошибке, такому как

Error! Expecting ';' but found 'extern' on line 3 (Ошибка! В строке 3 ожидается ';', а встречено 'extern')

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

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

Лишние точки с запятой в определении макро

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

#define MAXVAL 10;

/* ... */

if( value >= MAXVAL ) break;

Компилятор выдаст сообщение ошибке похожее на

Error! Expecting ')' but found ';' on line 372 (Ошибка! В строке 372 ожидается ')', а встречено ';')

Проблему просто локализовать, когда будет сделана макроподстановка в строке 372. С использованием определения MAXVAL, строка 372 после подстановки будет выглядеть так:

if( value >= 10; ) break;

Точка с запятой (;) в определении не трактуется в качестве конца команды как это ожидалось, а была включена в определение макро (самообъявляющейся константы) MAXVAL. Подстановка привела к тому, что точка с запятой оказалась посреди управляющего выражения, а отсюда — сообщение об ошибке.

Висячий else

Предположим, что во фрагменте кода

if( value1 > 0 )
    if( value2 > 0 )
        printf( "Оба значения больше нуля\n" );
else
    printf( "Значение value1 не больше нуля\n" );

value1 имело значение 3, а value2 — значение -7. Будет выведено сообщение

value1 не больше нуля

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

if( value1 > 0 )
    if( value2 > 0 )
        printf( "Оба значения больше нуля\n" );
    else
        printf( "Значение value1 не больше нуля\n" );

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

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

if( value1 > 0 ) {
    if( value2 > 0 ) {
        printf( "Оба значения больше нуля\n" );
    }
} else {
    printf( "Значение value1 не больше нуля\n" );
}

Забытый break в команде switch

Во фрагменте кода

switch( value ) {
  case 1:
    printf( "value равно 1\n" );
  default:
    printf( "value не равно 1\n" );
}

если value будет равно 1, то выведется следующее:

value равно 1
value не равно 1

Это неожиданное поведение происходит, потому что, когда value равно 1, switch передаст управление на метку case 1:, где произойдёт первый вызов printf. Затем будет встречена метка default. Во время исполнения метки игнорируются, поэтому следующей командой, которая будет исполнена, будет второй printf.

Этот пример следует исправить так:

switch( value ) {
  case 1:
    printf( "value равно 1\n" );
    break;
  default:
    printf( "value не равно 1\n" );
}

Команда break приводит к передаче управления команде следующей за закрывающей фигурной скобкой команды switch.

Побочные эффекты в макро

Во фрагменте кода

#define endof( ptr ) ptr + strlen( ptr )
/* ... */
endptr = endof( ptr++ );

команда расширяется в

endptr = ptr++ + strlen( ptr++ );

Параметр ptr увеличивается дважды, а не раз, как ожидалось.

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

getc       putc
getchar    putchar

Стандарт ANSI требует, чтобы в документации указывалось, какие библиотечные функции вычисляют свои аргументы более одного раза.