Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

DevDoc - статьи для разработчика ПО под Windows Отладка приложений на C++. Часть 1


DevDoc home page
 
   
 

Отладка приложений на C++. Часть 1.

Введение

Эта статья начинает цикл об отладке приложений, написанных на C++ в среде Microsoft Visual Studio .NET. Существует множество методик, которые позволяют уменьшить количество багов в программе. В этой статье я хочу описать методики, которые я использовал в течение ряда лет при разработке программ и которые хорошо себя зарекомендовали.

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

Стоимость ошибки

Мы не будем рассматривать стоимость последствий использования программ с ошибками. Есть существенные различия между системой управления атомным реактором и программой для рисования заставки на Вашем рабочем столе.

В данном разделе я хочу затронуть тему стоимость исправления ошибки. Ошибка может появиться на разных этапах разработки:

  • Проектирование архитектуры
  • Реализация ядра системы
  • Написание функционала
  • Внедрение

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

Самые «дорогие» ошибки – это ошибки обнаруженные на этапе внедрения. С одной стороны уже не остается времени на их исправления, т.к. есть жестко поставленные сроки. С другой стороны на этом этапе заказчик может самостоятельно столкнуться с ошибками. В лучшем случае у него сложиться негативное впечатление от Вашей работы.

Исправлять или нет?

Что более важно уметь исправлять ошибки или не допускать их появления? Ответ очевиден. Если код сразу написан правильно, то программист может сэкономить массу времени и нервной энергии, а не гоняться за необъяснимыми глюками. За все время работы я не разу не видел программу, которая бы не содержала ошибок за исключением поделок из нескольких строчек кода. Тем не менее после сбора небольшой статистики в нашей компании выяснилось, что количество ошибок которые допускает программист на одну строку кода более или менее постоянно для каждого разработчика.

Условно все ошибки можно разделить на два больших класса. Это ошибки/опечатки при наборе программы и ошибки в алгоритмах. И те и другие приводят к неправильной работе программы. Для исправления первых, надо выработать стиль программирования, который позволяет минимизировать количество опечаток. Назовем этот метод пассивным исправлением ошибок. Несмотря на кажущуюся простоту он позволяет избавиться от 1-10% всех ошибок. Это уже немало и стоит тех небольших усилий, которых вы потратите. Помимо устранения опечаток, хороший стиль программирования позволит быстрее находить ошибки других типов. Пассивное исправление ошибок позволяет минимизировать количество ошибок на начальном этапе разработки и таким образом сокращает затраты на их исправление в будущем.

Пассивное исправление ошибок

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

Используйте возможности компилятора

Разработчики компилятора C++ очень хорошо знают особенности языка, подводные камни при использовании тех или иных конструкций. Например, обоснованно считается, что использование указателей в C++ потенциально опасно, т.к. при неаккуратном применении может привести к порче памяти.

Многие программисты даже и не подозревают, что компилятор может найти огромное количество ошибок в их коде. Для поиска множества ошибок в вашем коде можно использовать опцию Warning level 4 компилятора. По умолчанию установлен уровень 3, который пропускает часть потенциально опасных мест. Я рекомендую, ставить эту опцию в отладочной сборке. Как руководитель группы программистов я требую от подчиненных, чтобы при таких установках компилятора не возникало предупреждений. Предупреждение это не обязательно ошибка – это просто указание программисту, что он возможно не правильно использует конструкцию языка.

К сожалению стандартные библиотеки (или код сторонних разработчиков) содержит много потенциально опасных мест и при использовании опции Warning Level 4 компилятор выдает большое количество предупреждений. Поэтому я рекомендую использовать этот прием время от времени, чтобы контролировать что в Вашем коде все в порядке.

Что с чем сравнивать?

Рассмотрим кусочек кода:

if(iValue = 0)
{}

В первой же строке содержится ошибка. Программист просто опечатался и вместо операции сравнения написал операцию присваивания. Компилятор, не видит критических ошибок в таком коде. В лучшем случае вы получите предупреждение. При выполнении такого кода переменное присвоиться ноль и тело оператора if никогда не выполниться. Кроме того такое «неожиданное» изменения переменной может повлиять и на другой код.

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

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

if(0 = iValue)
{
  ….
}

Компилятор выдаст не просто предупреждение, а ошибку, т.к. присвоить значение константе невозможно. Если взять за правило в условных операторах писать на первом месте константу, программист может избавить от подобных опечаток.

Верить или нет…

Рассмотрим функцию, которая рассчитывает средне арифметическое значение:

double calcAverage(double *p, int size)
{
  double fSum = 0;
   for(int i = 0; i < size; i++)
   {
    fSum += p[i];
   }
   return fSum/size;
}

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

double *pArray;
double fRes = calcAverage(pArray, 10);

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

double calcAverage(double *p, int size)
{
  ASSERT(size > 0);
   ASSERT(_CrtIsValidPointer(p, size * sizeof(double), TRUE);
   double fSum = 0;
   for(int i = 0; i < size; i++)
   {
    fSum += p[i];
   }
   return fSum/size;
}

Макрос ASSERT проверяет, если его аргумент принимает значение FALSE, то выдается предупреждение на этапе выполнения. Функция _CrtIsValidPointer проверяет что указатель корректный и его можно использовать для операций чтения и записи.

Посмотрите также документацию на функции: _CrtCheckMemory, _CrtIsValidHeapPointer, CrtIsMemoryBlock. Они также позволяют проверять указатели на корректность.

Внимание!!! Макрос ASSERT работает только в отладочной версии программы. В релизе он полностью удаляется из текста программы, поэтому надо быть очень внимательным и не использовать этот макрос для проверки результатов вычисления значимых выражений. Например, следующий код содержит ошибку:

HANDLE hFile;
ASSERT(INVALID_HANDLE_VALUE != (hFile = CreateFile(….)));

В отладочной версии программы все будет работать замечательно, а в релизе аргумент макроса ASSERT будет удален из программы. Поэтому для обработки ошибок настоятельно рекомендуется использовать другие методы. Если же все таки надо проверять значения именно таким образом, то обратите внимание на макрос VERIFY, он работает аналогично, в т.ч. и в релизе.

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

Copyright (C) Kudinov Alexander, 2006-2007

Перепечатка материалов с данного сайта запрещена без писменного разрешения автора. При перепечатке обязательно указывать ссылку на оригинал.


В избранное