Невоспроизводимые ошибки
С нестабильными ошибками сложнее всего иметь дело, и обычно проблема не столь очевидна, как неисправное "железо". Однако сам факт, что проблема недетерминирована, содержит в себе информацию. Это означает, что ошибка, скорее всего, не в вашем алгоритме, а в том, как ваш код использует информацию, которая изменяется при каждом выполнении программы.
Проверьте, что все переменные инициализированы. Может быть, вы просто используете случайное значение, оставшееся в повторно используемой ячейке памяти. При написании программ на С и C++ в этом чаще всего виновны локальные переменные функций и выделяемая память. Установите все переменные в заранее известное значение; напрмер, если стартовое значение генератора случайных чисел обычно вычисляется исходя из времени суток, то присвойте ему нулевое значение.
Если ошибка изменяет свое поведение или вообще исчезает при добавлении отладочного кода, то это может быть связано с выделением памяти: где-то вы пишете за пределы выделенной памяти, а добавление отладочного кода изменяет расположение данных в памяти, так что ошибка начинает проявляться по-другому. Большинство функций вывода, от printf до диалоговых окон, захватывают для себя память сами, еще больше "мутя воду".
Если место ошибки, казалось бы, находится далеко от любого места, в котором она могла бы появиться, значит, происходит запись за пределы доступной памяти, причем затирается значение, которое используется лишь гораздо позже. Иногда случается проблема "висящих указателей", когда указатель на локальную переменную случайно передается за пределы функции, а затем используется. Возврат адреса локальной переменной – лучший способ создания ошибки замедленного действия:
К тому моменту, когда указатель, возвращаемый функцией msg, используется, он уже не указывает на осмысленное место. Память нужно выделять с помощью функции malloc, использовать массив, объявленный как static, или требовать предоставления памяти вызывающей программой.
Использование динамически выделяемого значения после того, как оно было освобождено, имеет подобные симптомы. Мы уже упоминали об этом во второй главе, когда написали функцию freeall. Вот этот код – неверен:
?for (р = listp; р!= NULL; р = p › next) ? free(p);
После того как память была освобождена, она не должна использоваться, потому что ее содержимое могло измениться и нет гарантии, что p › next все еще указывает на правильное значение.
В некоторых реализациях malloc и free повторное освобождение участка памяти портит внутренние структуры, но не вызывает никаких проблем до тех пор, пока гораздо позже какая-нибудь другая операция выделения памяти не поскользнется на испорченном участке памяти. Некоторые реализации динамического выделения памяти имеют отладочные возможности для проверки корректности области динамической памяти при каждом вызове. Включите такую отладку, если вы столкнулись с недетерминированной ошибкой.
В крайнем случае вы можете написать собственную реализацию динамической памяти, которая проверяет каждую операцию или просто заносит эти операции в журнал для дальнейшего изучения. Если ситуация удручает, достаточно написать простую, не очень скоростную реализацию. Есть также великолепные коммерческие продукты, проверяющие работу с памятью и отлавливающие ошибки и утечки; если у вас таких нет, собственная версия malloc и fгее может в какой-то мере их заменить.
Когда программа работает у одного пользователя и не работает у другого, значит, дело во внешней среде, в которой выполняется программа. Несоответствие может оказаться в файлах, которые читает программа, в правах доступа к файлам, в переменных окружения, в путях поиска команд, в значениях по умолчанию и в стартовых файлах. Сложно сказать что-либо в такой ситуации, потому что вам придется постараться полностью сдублировать среду выполнения неверной программы, практически вжиться в шкуру ее пользователя.
Упражнение 5.1
Напишите версии malloc и free, которыми можно пользоваться для отладки проблем с выделением памяти. Одним из возможных подходов является проверка всего используемого пространства при каждом вызове malloc и free; другой подход – записывать журнальную информацию, которую можно обрабатывать специальной программой. В любом случае добавьте в начало и конец выделяемой памяти специальные маркеры, чтобы отследить запись, выходящую за ее пределы.