Базовая отладка | Кодементор
Отладка — это навык, который вам понадобится. Программы редко работают с первого раза, и нужно уметь разбираться, что идет не так.
Отладку можно выполнить несколькими способами: два из них — с помощью регистрации или запуска отладчика (иногда и того, и другого). В любом случае цель состоит в том, чтобы наблюдать за работой программы и определять, где что-то идет не так.
По сути, нас интересуют две вещи:
- проверка того, что переменные обновляются (устанавливаются), как и ожидалось, и,
- проверка потока управления работает, как ожидалось.
Как только эти две вещи работают, вообще говоря, большинство программ будут работать или, по крайней мере, давать более ожидаемые результаты.
Чтобы хорошо отлаживать программы, очень помогает написать «простой код»! . «Простой код» использует больше коротких строк вместо меньшего количества длинных. Более короткие строки связаны друг с другом через локальные переменные.
Сегодня в большинстве языков, особенно в C, C++, Java и C#, использование локальных переменных практически бесплатно, поскольку они размещаются либо в стеке (очень недорого), либо непосредственно в машинных регистрах (совершенно бесплатно) — в дальнейшем, если используется локальная переменная. всего один раз — как это часто бывает при программировании в таком простом стиле — тогда компилятор (при оптимизации) может полностью исключить локальную переменную.
Кроме того, давайте также отметим, что этот подход «простого кода» применим к исправлению ошибок во время компиляции, а также к отладке во время выполнения. Если компилятор на что-то жалуется, во многих языках сообщение об ошибке будет гораздо более информативным при использовании простого кода, где в каждой строке кода выполняется только одна основная операция. Если программа дает сбой во время выполнения (например, ошибка сегментации или исключение нулевого указателя), использование простого кода поможет точно определить операцию, вызвавшую ошибку, поскольку с помощью простого кода мы уменьшаем количество операций в строке.
Можно сделать много простого кода, но на самом деле единственным недостатком является многословие. Итак, когда вы закончите отладку, вы можете собрать некоторые вещи в более крупные операторы, если хотите. Однако, с другой стороны, мы можем применить простой код в крайнем случае как подход к отладке вещей в области кода, где мы не понимаем, что не так: в самом крайнем случае мы будем выполнять только одну операцию (например, добавление , вызов, разыменование) на строку, используя столько переменных, сколько необходимо для их соединения.
Давайте рассмотрим несколько примеров написания простого кода:
if ( ButtonEvents () & BUTTON_1_DOWN ) {
...
} else {
...
}
Поскольку проверяемое условие включает вызов функции, вместо этого мы записываем возвращаемое значение в локальную переменную следующим образом:
int be = ButtonCheckEvents ();
if ( be & BUTTON_1_DOWN ) {
...
} else {
...
}
Это упрощение разделяет большее и более сложное условие if на две строки, соединенные переменной (be
). Здесь результат вызова функции записывается в переменную, и благодаря этому мы можем гораздо проще проверить возвращаемое значение, будь то с помощью отладчика или ведения журнала с помощью printf.
Затем мы хотим проверить переменные и поток управления. Поскольку мы отделили первичное выражение от исходного оператора if, уместно сначала увидеть значение новой переменной.
int be = ButtonEvents ();
printf ("be = %d\n", be ); // new printf added for debugging (check variable value)
if ( be & BUTTON_1_DOWN ) {
...
} else {
...
}
*Мы могли бы пойти еще дальше, даже захватив* be & BUTTON_1_DOWN
в локальную переменную, уменьшая условие if до проверки только одной переменной.
Также нам нужно проверить поток управления:
int be = ButtonEvents ();
printf ("be = %d\n", be );
if ( be & BUTTON_1_DOWN ) {
printf ( "Button 1 down...\n" ); // new debugging printf, flow of control
...
} else {
printf ( "Button 1 not down...\n" ); // new debugging printf, flow
...
}
С потоком управления мы смотрим, работает ли весь поток в целом, независимо от того, пропускаем ли мы часть else, проверяем неправильное условие, пропускаем break;
оператор в операторе switch, ошибка в цикле управления и т. д.
Вы можете видеть, что некоторые операторы printf добавлены только для того, чтобы указать поток управления, происходящий в программе. Другие используются для отображения значений переменных после их присвоения.
При использовании отладчика идея состоит в том, чтобы получить ту же информацию: назначение переменной, чтобы увидеть, вычисляются ли разумные значения, и поток управления, работает ли он или нет.
Как упоминалось ранее, если поток управления работает правильно и присваивание переменных работает, большинство программ будут работать!
Иногда printfs становятся слишком шумными, и чтобы увидеть, в чем проблема, нам нужно уменьшить шум. Когда мы выполняем отладку printf, мы можем использовать простые операторы if, чтобы ограничить количество выводимых printf:
int be = ButtonEvents ();
if ( count == 100 ) // if condition guard to reduce printfs
printf ("be = %d\n", be );
if ( be & BUTTON_1_DOWN ) {
printf ( "Button 1 down...\n" );
...
} else {
printf ( "Button 1 not down...\n" );
...
}
В качестве if-условий, которые защищают/уменьшают отладочные printf, мы можем либо тестировать существующее состояние программы (переменные), либо вводить новое состояние по мере необходимости (оно идет не так на 100-м счете: у нас уже есть счетчик для этого или мы должны создать счетчик? ).
Когда мы используем отладчик, мы можем иногда вводить условную точку останова, если отладчик поддерживает эту концепцию. Тем не менее, они могут работать медленнее, чем обычная точка останова, тогда как размещение тестового состояния if-statement может быть лучше:
int be = ButtonEvents ();
if ( count == 100 ) {
int a = 1; // good line to put break point in debugger
}
if ( be & BUTTON_1_DOWN ) {
printf ( "Button 1 down...\n" );
...
} else {
printf ( "Button 1 not down...\n" );
...
}
Эта конструкция имеет два эффекта: она обеспечивает хорошее место для установки безусловной точки останова (при этом прерывание происходит только при интересующем условии), а также (в большинстве языков) обеспечивает предупреждение компилятора о том, что «a» не используется: это хорошо чтобы помочь нам не забыть удалить такие дополнения после того, как код заработает.
Операторы присваивания также могут быть упрощены в том смысле, что если выполняется слишком много работы, мы можем разбить логику на два оператора, также связанных локальными переменными.
Таким образом, напишите «простой» код.