|
Программирование >> Арифметические и логические операции
связей), то ее поведение становится похожим на поведение настоящего живого существа. Такое же непредсказуемое... впрочем, кое что все-таки предсказать можно: работать оно не будет. Во всяком случае, сразу. Программирование на С и C++ дает возможность допускать такие ошибки, поиск которых озадачил бы самого Шерлока Холмса. Вообще говоря, чем загадочнее ведет себя программа, тем проще в ней допущена ошибка. А искать простые ошибки сложнее всего, как это ни странно; все потому, что сложная ошибка обычно приводит к каким-то принципиальным неточностям в работе программы, а ошибка простая либо превращает всю работу в бред пьяного программиста , либо всегда приводит к одному и тому же: segmentation fault. И зря говорят, что если ваша программа выдала фразу core dumped, то ошибку найти очень просто: это, мол, всего лишь обращение по неверному указателю, например, нулевому. Обращение-то, конечно же, есть, но вот почему в указателе появилось неверное значение? Откуда оно взялось? Зачастую на этот вопрос не так просто ответить. В Java исключены указатели именно потому, что работа с ними является основным источником ошибок программистов. При этом отсутствие инициализации является одним из самых простых и легко отлавливаемых вариантов ошибок. Самые трудные ошибки появляются, как правило, тогда, когда в программе постоянно идут процессы выделения и удаления памяти. То есть, в короткие промежутки времени появляются объекты и уничтожаются. В этом случае, если где-нибудь что-нибудь некорректно указать , то core dumped , вполне вероятно, появится не сразу, а лишь через некоторое время. Все дело в том, что ошибки с указателями проявляются обычно в двух случаях: работа с несуществующим указателем и выход за пределы массива (тоже в конечном итоге сводится к несуществующему указателю, но несколько чаще встречается). Загадки, возникающие при удалении незанятой памяти, одни из самых трудных. Выход за границы массива, пожалуй, еще сложнее. Представьте себе: вы выделили некоторый буфер и в него что-то записываете, какие-то промежуточные данные. Это критическое по времени место, поэтому тут быть не может никаких проверок и, ко всему прочему, вы уверены в том, что исходного размера буфера хватит на все, что в него будут писать. Не хотелось бы торопиться с подобными утверждениями: а почему, собственно, вы так в этом уверены? И вообще, а вы уверены в том, что правильно вычислили этот самый размер буфера? Ответы на эти вопросы должны у вас быть. Мало того, они должны находиться в комментариях рядом с вычислением размера буфера и его заполнением, чтобы потом не гадать, чем руководствовался автор, когда написал: char buf[100]; Что он хотел сказать? Откуда взялось число 100? Совершенно непонятно. Теперь о том, почему важно не ошибиться с размерами. Представьте себе, что вы вышли за пределы массива. Там может ничего не быть , т.е. этот адрес не принадлежит программе и тогда в нормальной операционной системе вы получите соответствующее матерное выражение. А если там что-то было? Самый простой случай - если там были просто данные. Например, какое-нибудь число. Тогда ошибка, по крайней мере, будет видна почти сразу... а если там находился другой указатель? Тогда у вас получается наведенная ошибка очень высокой сложности. Потому что вы будете очень долго искать то место, где вы забыли нужным образом проини-циализировать этот указатель... Мало того, подобные наведенные ошибки вполне могут вести себя по-разному не только на разных тестах, но и на одинаковых. А если еще программа кормится данными, которые поступают непрерывно... и еще она сделана таким образом, что реагирует на события, которые каким-то образом распределяются циклом обработки событий... тогда все будет совсем плохо. Отлаживать подобные программы очень сложно, тем более что зачастую, для того, чтобы получить замеченную ошибку повторно, может потребоваться несколько часов выполнения программы. И что делать в этих случаях? Поиск таких ошибок более всего напоминает шаманские пляски с бубном около костра, не зря этот образ появился в программистском жаргоне. Потому что программист, измученный бдениями, начинает просто случайным образом удалять (закомментировав некоторую область, или набрав #if 0 ... #endif) блоки своей программы, чтобы посмотреть, в каком случае оно будет работать, а в каком - нет. Это действительно напоминает шаманство, потому что иногда программист уже не верит в то, что, например, от перестановки мест сумма слагаемых не меняется и запросто может попытаться переставить и проверить результат... авось? А вот теперь мы подошли к тому, что в шаманстве тоже можно выделить систему. Для этого достаточно осознать, что большинство зага- дочных ошибок происходят именно из-за манипуляций с указателями. Поэтому, вместо того чтобы переставлять местами строчки программы, можно просто попытаться для начала закомментировать в некоторых особенно опасных местах удаление выделенной памяти и посмотреть что получится. Кстати сказать, отладка таких моментов требует (именно требует) наличия отладочной информации во всех используемых библиотеках, так будет легче работать. Так что, если есть возможность скомпилировать библиотеку с отладочной информацией, то так и надо делать - от лишнего можно будет избавиться потом. Если загадки остались, то надо двинуться дальше и проверить индексацию массивов на корректность. В идеале, перед каждым обращением к массиву должна находиться проверка инварианта относительно того, что индекс находиться в допустимых пределах. Такие проверки надо делать отключаемыми при помощи макросов DEBUG/RELEASE с тем, чтобы в окончательной версии эти дополнительные проверки не мешались бы (этим, в конце-концов, С отличается от Java: хотим - проверяем, не хотим - не проверяем). В этом случае вы значительно быстрее сможете найти глупую ошибку (а ошибки вообще не бывают умными; но найденные - глупее оставшихся)). На самом деле, в C++ очень удобно использовать для подобных проверок шаблонные типы данных. То есть, сделать тип массив , в котором переопределить необходимые операции, снабдив каждую из них нужными проверками. Операции необходимо реализовать как inline, это позволит не потерять эффективность работы программы. В то же самое время, очень легко будет удалить все отладочные проверки или вставить новые. В общем, реализация своего собственного типа данных Buffer является очень полезной. Кстати, раз уж зашла об этом речь, то абзац выше является еще одним свидетельством того, что C+ + надо использовать полностью и никогда не писать на нем как на усовершенствованном Си . Если вы предпочитаете писать на Си, то именно его и надо использовать. При помощи C++ те же задачи решаются совсем по другому. Глава 17. Создание графиков с помощью ploticus Есть такая программа, предназначенная для создания графиков различных видов из командной строки, называется ploticus. Программа сама по себе достаточно удобная - потому что иногда очень полезно ав- томатизировать генерацию различных графических отчетов, а тут без командной строки и вызова программ из скриптов не обойтись. Нет, таких программ великое множество, но ploticus отличается от них очень удобным преимуществом: он глупый . То есть, его можно, например, заставить разместить надпись на рисунке с точностью до пиксела... иногда это нужно. Но разговор не об удобстве этой программы. Просто иногда требуется использовать ploticus, но при этом немного доработанный напильником. Сначала немного лирики. Ploticus, для того, чтобы построить график, читает некоторый файл, в котором находится определение этого самого графика (скрипт, так сказать). Этот файл обладает очень простой грамматикой. Мало того, ploticus умеет организовывать программный канал (хотя, кто этого не умеет?) и читать данные оттуда, как результат выполнения другой программы. Так вот о чтении этого файла мы и хотим немного рассказать. Итак, подпрограммы чтения данных в ploticus разбиты на некоторые логические блоки, исходя из структуры самого файла с данными. Ну это понятно и логично. Не особенно понятно другое: каждая подпрограмма (парсер) на вход воспринимает название файла, в котором находится содержимое. В итоге, основная подпрограмма сначала разбивает файл на блоки, содержимое этих файлов копирует (!) во временные файлы (!!), которые подсовывает на вход другим подпрограммам. Это, конечно, уже достаточно оригинально, хотя задумка автора ясна - он хотел сделать так, чтобы в этих местах на вход подпрограммам чтения данных можно было бы подсунуть имя программы, которая эти данные бы сгенерировала. Тем не менее, можно было бы сделать значительно красивее, чем создавать кучу временных текстовых файлов. Эти подпарсеры реализованы... аналогичным образом. Т.е., автор не смущаясь разбивает подсунутые файлы еще на кусочки и записывает их в другие временные файлы, которые потом читает. В принципе, все эти места как раз и требовали вмешательства напильника, потому что хотелось иметь программный интерфейс ко всему этому хозяйству и, желательно, чтобы данные не покидали оперативной памяти. Все написанное выше уже смешно. Но кусочек кода, который приведен, может довести программиста до истерического смеха. Вот он (с купюрами): else if( stricmp( attr, data )==0 ) { FILE *tfp; sprintf( datafile, %s D , Tmpname ); getmultiline( data , lineval, fp, MAXBIGBUF, Bigbuf ); tfp = fopen( datafile, w ); if( tfp == NULL ) return( Eerr( 294, Cannot open tmp data file , datafile )); fprintf( tfp, %s , Bigbuf ); fclose( tfp ); } Это как раз и есть выделение секции с данными и запись ее во временный файл. Вообще, использование fprintf с шаблоном %s уже смотрится очень оригинально, но то, что идет 80 строками ниже еще более необычно: if( standardinput strcmp( datafile, - ) ==0 ) { /* a file of - means read from stdin */ dfp = stdin; goto PT1; if( strlen( datafile ) > 0 ) sprintf( command, cat %s , datafile if( strlen( command ) > 0 ) { dfp = popen( command, r ); if( dfp == NULL ) { Skipout = 1; return( Eerr( 401, Cannot open , command ) ); PT1: Обратите внимание на строчку: if( strlen( datafile ) > 0 ) sprintf( command, cat %s , datafile и следующую за ней: dfp = popen( command, r ); Честно говоря, это впечатляет. Очень впечатляет... при этом, совершено не понятно что мешало использовать обычный fopen() для этого (раз уж так хочется), раз уж есть строки вида: dfp = stdin; goto PT1; В общем, дикость. Если кто-то не понял, то объясним то, что происходит, на пальцах: читается секция data и ее содержимое записывается в файл, название которого содержится в datafile. Потом, проверяется название этого файла, если оно равно - , то это значит, что данные ожидаются со стандартного файла ввода, stdin. Если же нет, то проверяется длина строки, на которую указывает datafile. Если она ненулевая, то считается, что команда, результаты работы которой будут считаться за входные данные, это cat datafile . Если же ненулевая длина у другого параметра, command, то его значение принимается за выполняемую команду. После всех этих манипуляций, открывается программный канал (pipe) при помощи popen(), результатом которой является обычный указатель на структуру FILE (при его помощи можно использовать обычные средства ввода-вывода). Вам это не смешно? Глава 18. Автоматизация и моторизация приложения Программирование давно стало сплавом творчества и строительства, оставляя в прошлом сугубо научно-шаманскую окраску ремесла. И если такой переход уже сделан, то сейчас можно обозначить новый виток - ломание барьеров API и переход к более обобщенному подходу в проектировании, выход на новый уровень абстракции. Немало этому способствовал Интернет и его грандиозное творение - XML. Сегодня ключ к успеху приложения сплавляется из способности его создателей обеспечить максимальную совместимость со стандартами и в то же время масштабируемость. Придумано такое количество различных технологий для связи приложений и повторного использования кода, что сегодня прикладные программы не могут жить без такой поддержки . Под термином автоматизация понимается настоящее оживление приложений,
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |