Rambler's Top100 Service калинин.ru / программирование / c и c++ /  << 25.09.00 >>

Шаманство, или ошибки работы с памятью

Когда программа становится внушительной по своему содержанию (то есть, не по количеству строчек, а по непонятности внутренних связей), то ее поведение становится похожим на поведение настоящего живого существа. Такое же непредсказуемое... впрочем, кое что все-таки предсказать можно: работать оно не будет. Во всяком случае, сразу.

Программирование на C и C++ дает возможность допускать такие ошибки, поиск которых озадачил бы самого Шерлока Холмса. Вообще говоря, чем загадочнее ведет себя программа, тем проще в ней допущена ошибка. А искать простые ошибки сложнее всего, как это ни странно; все потому, что сложная ошибка обычно приводит к каким-то принципиальным неточностям в работе программы, а ошибка простая либо превращает всю работу в бред пьяного программиста, либо всегда приводит к одному и тому же: segmentation fault.

И зря говорят, что если ваша программа выдала фразу core dumped, то ошибку найти очень просто: это, мол, всего лишь обращение по неверному указателю, например, нулевому. Обращение-то, конечно же, есть, но вот почему в указателе появилось неверное значение? Откуда оно взялось? Зачастую на этот вопрос не так просто ответить.

В Java исключены указатели именно потому, что работа с ними является основным источником ошибок программистов. При этом отсутствие инициализации является одним из самых простых и легко отлавливаемых вариантов ошибок.

Самые трудные ошибки пояляются, по-моему, тогда, когда в программе постоянно идут процессы выделения и удаления памяти. То есть, в короткие промежутки времени появляются объекты и уничтожаются. В этом случае, если где-нибудь что-нибудь некорректно "указать", то "core dumped", вполне вероятно, появится не сразу, а лишь через некоторое время. Все дело в том, что ошибки с указателями проявляются обычно в двух случаях: работа с несуществующим указателем и выход за пределы массива (тоже в конечном итоге сводится к несуществующему указателю, но несколько чаще встречается).

Я уже писал о том, что загадки, возникающие при удалении незанятой памяти, одни из самых трудных. Выход за границы массива, пожалуй, еще сложнее.

Представьте себе: вы выделили некоторый буфер и в него что-то записываете, какие-то промежуточные данные. Это критическое по времени место, поэтому тут быть не может никаких проверок и, ко всему прочему, вы уверены в том, что исходного размера буфера хватит на все, что в него будут писать. Лично я бы не хотел торопиться с подобными утвержденияями: а почему, собественно, вы так в этом уверены? И вообще, а вы уверены в том, что правильно вычиcлили этот самый размер буфера?

Ответы на эти вопросы должны у вас быть. Мало того, они должны находиться в комментариях рядом с вычислением размера буфера и его заполнением, что бы потом не гадать, чем руководствовался автор, когда написал

char buf[100];

Что он хотел сказать? Откуда взялось число 100? Совершенно непонятно.

Теперь о том, почему важно не ошибиться с размерами. Представьте себе, что вы вышли за пределы массива. Там может "ничего не быть", т.е. этот адрес не принадлежит программе и тогда в нормальной операционной системе вы получите соответствующее "матерное" выражение. А если там что-то было?

Самый простой случай --- если там были просто данные. Например, какое-нибудь число. Тогда ошибка, по крайней мере, будет видна почти сразу... а если там находился другой указатель? Тогда у вас получается наведенная ошибка очень высокой сложности. Потому что вы будете очень долго искать то место, где вы забыли нужным образом проинициализировать этот указатель...

Мало того, подобные "наведенные" ошибки вполне могут вести себя по-разному не только на разных тестах, но и на одинаковых.

А если еще программа "кормится" данными, которые поступают непрерывно... и еще она сделана таким образом, что реагирует на события, которые каким-то образом распределяются циклом обработки событий... тогда все будет совсем плохо. Отлаживать подобные программы очень сложно, тем более что, зачастую, для того, что бы получить замеченную ошибку повторно, может потребоваться несколько часов выполнения программы. И что делать в этих случаях?

Поиск таких ошибок более всего напоминает шаманские пляски с бубном около костра, не зря этот образ появился в программистком жаргоне. Потому что программист, измученный бдениями, начинает просто случайным образом "удалять" (закомментировав некоторую область, или набрав #if 0 ... #endif) блоки своей программы, что бы посмотреть, в каком случае оно будет работать, а в каком --- нет.

Это действительно напоминает шаманство, потому что иногда программист уже не верит в то, что, например, "от перестановки мест сумма слагаемых не меняется" и запросто может попытаться переставить и проверить результат... авось?

А вот теперь я подобрался к тому, о чем хотел сказать. В шаманстве тоже можно выделить систему. Для этого достаточно осознать, что большинсто загадочных ошибок происходят именно из-за манипуляций с указателями. Поэтому, вместо того что бы переставлять местами строчки программы, можно просто попытаться для начала закомментировать в некоторых особенно опасных местах удаление выделенной памяти и посмотреть что получится.

Кстати сказать, отладка таких моментов требует (именно требует) наличия отладочной информации во всех используемых библиотеках, так будет легче работать. Так что, если есть возможность скомпилировать библиотеку с отладочной информацией, то так и надо делать --- от лишнего можно будет избавиться потом.

Если загадки остались, то надо двинуться дальше и проверить индексацию массивов на корректность. В идеале, перед каждым обращением к массиву должна находиться проверка инварианта относительно того, что индекс находиться в допустимых пределах. Такие проверки надо делать отключаемыми при помощи макросов DEBUG/RELEASE с тем, что бы в окончательной версии эти дополнительные проверки не мешались бы (этим, в конце-концов, C отличается от Java: хотим --- проверяем, не хотим --- не проверяем). В этом случае вы значительно быстрее сможете найти глупую ошибку (а ошибки вообще не бывают умными; но найденные --- глупее оставшихся ;) ).

На самом деле, в C++ очень удобно использовать для подобных проверок шаблонные типы данных. То есть, сделать тип "массив", в котром переопределить необходимые операции, снабдив каждую из них нужными проверками. Операции реализовать как inline, это позволит не потерять эффективность работы программы. В то же самое время, очень легко будет удалить все отладочные проверки или вставить новые. В общем, реализация своего собственного типа данных Buffer является очень полезной.

Кстати, раз уж зашла об этом речь, то абзац выше является еще одним свидетельством того, что C++ надо использовать "полностью" и никогда не писать на нем как на "усовершенствованном C". Если вы предпочитаете писать на C, то именно его и надо использовать. При помощи C++ те же задачи решаются совсем по другому.

Резюме

Ошибки допускают все и бессонные ночи бывают у каждого программиста. Самое страшное заключается в том, что когда ошибка найдена, то всегда появляется ощущение зря потерянного времени... вообще говоря, любой опыт, если он не прошел даром, положителен. То есть, это значит, что в следующий раз, возможно, подобную ошибку вы будете искать не так долго.

Хотя, конечно же, лучше всего ошибок не допускать вообще. А вот как это сделать?


Версия для печати


  Ссылки по теме:
Бъерн Страуструп
   Язык программирования C++, 3 издание.
/comment/books/16_08_00.shtml
   C++: библиотека программиста.
/comment/books/27_08_00.html
   C и C++. Правила программирования.
  Рядом в разделе:
ploticus (16.10.00)
   Есть такая программа, предназначенная для создания графиков различных видов из командной строки, называется ploticus. Программа сама по себе достаточно удобная ---...   >>>>
Библиотека консорциума W3, libwww (20.09.00)
   Популярный нынче термин "веб-программирование" обычно подразумевает под собой программирование, в лучшем случае, на perl, в худшем --- на PHP, в совсем...   >>>>
  Рядом по дате:
free.rambler.ru, свободный доступ в интернет (26.09.00)
   На начали наконец-то мелькать баннеры нового Рамблеровского проекта под названием Rambler-FreeNet. Суть его достаточно проста: Рамблер предоставляет относительно свободный доступ в...   >>>>
Свобода (24.09.00)
   Когда говорят о свободе человека, всегда подразумевается свобода относительная, во всяком случае в нашей стране. Имеется в виду свобода относительно несвободы...   >>>>
  Содержание:
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
   C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Просто так
Студенческое
Туризм
  Байки
Фотографии
Комментарии
   Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Благодарности
Форум
Хронология
 
  В этом разделе:
Простой, но полезный аллокатор памяти (18.02.03)
   Эта заметка --- продолжение "Postfix изнутри" в том смысле, что в качестве примера опять берется postfix. Но если в прошлый раз...   >>>>
C или C++? (09.07.01)
   Существуют два диаметрально противоположенных, но одинаково распространенных мнения, которые можно выразить как "C++ это C с классами" и "C++ и C...   >>>>
Религия и goto (14.04.01)
   Начнем несколько издалека. В программировании существует тенденция к алгоритмизации самого процесса программирования. То есть, выведение некоторых универсальных правил, использование которых в...   >>>>
ploticus (16.10.00)
   Есть такая программа, предназначенная для создания графиков различных видов из командной строки, называется ploticus. Программа сама по себе достаточно удобная ---...   >>>>
Шаманство, или ошибки работы с памятью (25.09.00)
   Когда программа становится внушительной по своему содержанию (то есть, не по количеству строчек, а по непонятности внутренних связей), то ее поведение...   >>>>
Библиотека консорциума W3, libwww (20.09.00)
   Популярный нынче термин "веб-программирование" обычно подразумевает под собой программирование, в лучшем случае, на perl, в худшем --- на PHP, в совсем...   >>>>
Инварианты внутри программы (18.09.00)
   Вы когда-нибудь задумывались, над тем, как вы пишите программы? Если нет, то, я думаю, сегодняшняя заметка будет вам полезна. Итак, как...   >>>>
Содержание раздела полностью...
   Примерно в тоже время
free.rambler.ru, свободный доступ в интернет (26.09.00)
   На начали наконец-то мелькать баннеры нового Рамблеровского проекта под названием Rambler-FreeNet. Суть его достаточно проста: Рамблер предоставляет относительно свободный доступ в...   >>>>
Свобода (24.09.00)
   Когда говорят о свободе человека, всегда подразумевается свобода относительная, во всяком случае в нашей стране. Имеется в виду свобода относительно несвободы...   >>>>
Хронология полностью...
   Содержание
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
  C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Туризм
  Байки
Фотографии
Комментарии
  Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Студенческое
Просто так
Благодарности
Форум
Хронология
© 2000-2008, Andrey L. Kalinin
mailto:andrey@kalinin.ru
Rambler's Top100