Несмотря на то, что программу на 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 требует, чтобы в документации указывалось, какие библиотечные функции вычисляют свои аргументы более одного раза.