Обычно с такими строками используется переменная, представляющая определенное состояние. Довольно просто, и это рекомендуется многими книгами по программированию на С, определять с помощью
#define
для таких состояний именованные константы. Например:
/* Различные состояния, в которых можно
находиться при поиске конца записи. */
#define NOSTATE 1 /* сканирование еще не началось (все) */
На уровне исходного кода это выглядит замечательно. Но опять-таки, есть проблема, когда вы пытаетесь просмотреть код из GDB:
(gdb) print state
$1 = 2
Здесь вы также вынуждены возвращаться обратно и смотреть в заголовочный файл, чтобы выяснить, что означает 2. Какова же альтернатива?
Рекомендация: Для определения именованных констант используйте вместо макросов перечисления (enum). Использование исходного кода такое же, а значения enum может выводить также и отладчик.
Пример, тоже из
io.c
в
gawk
:
typedef enum scanstate {
NOSTATE, /* сканирование еще не начато (все) */
INLEADER, /* пропуск начальных данных (RS = "") */
Теперь при просмотре state из GDB мы видим что-то полезное:
(gdb) print state
$1 = NOSTATE
15.4.1.3. При необходимости переставляйте код
Довольно часто условие в
if
или
while
состоит из нескольких проверок, разделенных
&&
или
||
. Если эти проверки являются вызовами функций (или даже не являются ими), невозможно осуществить пошаговое прохождение каждой отдельной части условия. Команды GDB
step
и
next
работают на основе операторов (statements), а не выражений (expressions). (Разнесение их по нескольким строкам все равно не помогает).
Рекомендация: перепишите исходный код, явно используя временные переменные, в которых сохраняются значения или условные результаты, так что вы можете проверить их в отладчике. Первоначальный код должен быть сохранен в комментарии, чтобы вы (или программист
после вас) могли сказать, что происходит.
Вот конкретный пример: функция
do_input
из файла
io.c gawk
:
1 /* do_input --- главный цикл обработки ввода */
2
3 void
4 do_input
5 {
6 IOBUF *iop;
7 extern int exiting;
8 int rval1, rval2, rval3;
9
10 (void)setjmp(filebuf); /* for 'nextfile' */
11
12 while ((iop = nextfile(FALSE)) != NULL) {
13 /*
14 * Здесь было:
15 if (inrec(iop) == 0)
16 while (interpret(expression_value) && inrec(iop) == 0)
17 continue;
18 * Теперь развернуто для простоты отладки.
19 */
20 rvall = inrec(iop);
21 if (rvall == 0) {
22 for (;;) {
23 rval2 = rval3 = -1; /* для отладки */
24 rval2 = interpret(expression_value);
25 if (rval2 != 0)
26 rval3 = inrec(iop);
27 if (rval2 == 0 || rval3 != 0)
28 break;
29 }
30 }
31 if (exiting)
32 break;
33 }
34 }
(Номера строк приведены относительно начала этой процедуры, а не файла.) Эта функция является основой главного цикла обработки
gawk
. Внешний цикл (строки 12 и 33) проходит через файлы данных командной строки. Комментарий в строках 13–19 показывает оригинальный код, который читает из текущего файла каждую запись и обрабатывает ее
Возвращаемое
inrec
значение 0 означает, что все в порядке, тогда как ненулевое возвращаемое значение
interpret
означает, что все в порядке. Когда мы попытались пройти через этот цикл, проверяя процесс чтения записей, возникла необходимость выполнить каждый шаг отдельно.
Строки 20–30 представляют переписанный код, который вызывает каждую функцию отдельно, сохраняя возвращаемые значения в локальных переменных, чтобы их можно было напечатать из отладчика. Обратите внимание, как в строке 23 этим переменным каждый раз присваиваются известные, ошибочные значения: в противном случае они могли бы сохранить свои значения от предыдущих итераций цикла. Строка 27 является тестом завершения, поскольку код изменился, превратившись в бесконечный цикл (сравните строку 22 со строкой 16), тест завершения цикла является противоположным первоначальному.