array[4] = {neil, 4}
В еще одной системе (запускающей другое ядро Linux) мы получили следующий вывод:
Segmentation fault
В вашей системе Linux вы увидите один из приведенных результатов или совсем другой. Мы рассчитывали получить приведенный далее вывод:
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Ясно, что в данном программном коде есть серьезная ошибка. Он не выполняет сортировку корректно, если вообще работает, а если он завершается с ошибкой сегментации, то операционная система посылает сигнал программе, сообщая о том, что обнаружен несанкционированный доступ к памяти, и преждевременно завершает программу, чтобы не испортить данные в оперативной памяти.
Способность операционной системы обнаружить несанкционированный доступ к памяти зависит от настройки оборудования и некоторых тонкостей реализации системы управления памятью. В большинстве систем объем памяти, выделяемый программе операционной системой, больше реально используемого. Если несанкционированный доступ осуществляется к этому участку памяти, оборудование может не выявить несанкционированный доступ. Вот почему не все версии Linux и UNIX сгенерируют сигнал о нарушении сегментации.
Примечание
Некоторые библиотечные функции, такие как printf, в определенных обстоятельствах также будут препятствовать некорректному доступу, например при использовании указателя null.
Когда вы исследуете проблемы доступа к элементам массива, часто полезно увеличить размер этих элементов, поскольку это увеличит размер ошибки. Если вы читаете единственный байт за пределами массива байтов, это может вам сойти с рук, т.к. память, выделенная программе, будет округляться до величины, зависящей от операционной системы, возможно, равной 8 Кбайт.
Если вы увеличите размер элемента массива, заменив элемент типа item массивом из 4096 символов, любое обращение к несуществующему элементу массива, возможно, окажется за пределами выделенной памяти. Каждый элемент массива равен 4 Кбайт, поэтому некорректно используемый участок памяти будет находиться за концом массива на расстоянии от 0 до 4 Кбайт.
Если мы внесем эту поправку, назвав результат debug3.c, то получим ошибку сегментации в версиях Linux обоих авторов.
/* 2 */ char data[4096];
$ <b>сс -о debug3 debug3.с</b>
$ <b>./debug3</b>
Segmentation fault
Возможно, что какие-то варианты систем Linux или UNIX все еще не будут выдавать сообщение об ошибке сегментации. Когда стандарт ANSI С утверждает, что поведение не определено, на самом деле он разрешает программе делать все, что угодно. Это выглядит так, как будто мы написали не удовлетворяющую стандартам программу на языке С, и она может демонстрировать очень странное поведение! Как видите, изъян в программе переводит ее в категорию программ с непредсказуемым поведением.
Анализ кода
Как мы упоминали ранее, часто, если программа не работает, как ожидалось, неплохо перечитать ее. Предположим, что мы просмотрели программный код примера этой главы и исправили в нем все очевидные ошибки.
Примечание
Анализ кода — это термин, применяемый для обозначения более формального процесса, в ходе которого группа разработчиков тщательно просматривает несколько сотен строк программного кода, но масштаб не имеет значения, это все равно анализ кода, и он остается очень полезным методом поиска ошибок.
Существуют средства, которые могут помочь в анализе кода, одно из самых очевидных — компилятор. Он сообщит вам о любых имеющихся в вашей программе синтаксических ошибках.
Примечание
У некоторых компиляторов есть опции, формирующие предупреждения в сомнительных случаях, таких как отсутствие инициализации переменных или применение присваиваний в условиях. Например, компилятор GNU можно запускать со следующими опциями:
gcc -Wall -pedantic -ansi
Они порождают много предупреждений и дополнительных проверок на соответствие стандартам языка С. Рекомендуем взять за правило использование этих опций, особенно Wall. Она генерирует полезную информацию при обнаружении ошибок в программе.
Чуть позже мы кратко обсудим и другие средства, lint и splint. Как и компилятор, они анализируют код и сообщают о фрагментах кода, которые могут быть некорректными.
Оснащение средствами контроля
Оснащение средствами контроля — это вставка в программу кода для сбора дополнительной информации о поведении программы во время ее выполнения. Очень популярна вставка вызовов функции printf для вывода значений переменных на разных стадиях выполнения программы. Вы можете с пользой для себя добавить несколько вызовов printf, но должны знать о том, что этот процесс повлечет за собой дополнительные редактирование и компиляцию при любом изменении программы и, конечно, вам придется удалить код, когда ошибки будут исправлены.
Здесь могут помочь два метода оснащения средствами контроля. Первый использует препроцессор языка С для выборочного включения кода средств контроля так, что вам нужно только перекомпилировать программу для вставки или удаления отладочного кода. Сделать это можно очень просто, с помощью конструкций, подобных приведенным далее:
#ifdef DEBUG
printf("variable x has value = %dn", x);
#endif
Вы можете компилировать программу с флагом компилятора -DDEBUG для определения символического имени DEBUG и включения дополнительного кода и без этого флага — для удаления отладочного кода. Можно создать и более сложный вариант использования пронумерованных отладочных макросов:
#define BASIC_DEBUG 1
#define EXTRA_DEBUG 2
#define SUPER_DEBUG 4
#if (DEBUG & EXTRA_DEBUG)
printf...
#endif
В этом случае вы всегда должны определять макрос DEBUG, но можете настраивать объем отладочной информации или уровень детализации. Флаг компилятора -DDEBUG=5 в нашем примере активизирует макросы BASIC_DEBUG и SUPER_DEBUG, но не EXTRA_DEBUG. Флаг DDEBUG=0 отключит всю отладочную информацию. С другой стороны, вставка следующих строк устранит необходимость задания в командной строке DEBUG, если отладки не требуется.