Как найти логическую ошибку в коде

Статьи о проверке проектов с открытым исходным кодом — вещь полезная. Кто-то, в том числе и разработчики, узнает об ошибках, содержащихся в проекте, кто-то узнает о методологии статического анализа и начнет применять её для повышения качества своего кода. Для нас же это прекрасный способ популяризации анализатора PVS-Studio, а заодно возможность его дополнительного тестирования. На этот раз я проверил платформу Accord.Net и нашёл в коде много интересных фрагментов.

О проекте и анализаторе

Accord.Net — платформа машинного обучения на базе .Net, написанная на языке C#. Платформа состоит из нескольких библиотек, охватывающих широкий диапазон задач, таких как статическая обработка данных, машинное обучение, распознавание образов и пр. Исходный код проекта доступен в репозитории на GitHub.

Для проверки проекта использовался статический анализатор кода PVS-Studio, доступный для загрузки по ссылке. Кроме того, вы можете ознакомиться с другими статьями о проверке открытых проектов или с «базой ошибок», в которой мы собираем найденные нами баги.

Немного о предупреждениях

Анализатор выдал 91 предупреждение первого уровня и 141 предупреждение второго уровня достоверности. 109 из этих предупреждений были описаны или упомянуты в статье. Бегло просмотрев остальные предупреждения, ещё 23 я бы отнёс к ошибкам, но они не приведены в статье, так как показались недостаточно интересными или были схожи с уже описанными. Об остальных предупреждениях анализатора судить уже сложнее — нужно тщательнее разбираться и анализировать код. В итоге, минимум 132 из 232 предупреждений я бы отнёс к ошибкам. Это говорит о том, что число ложных срабатываний анализатора составляет примерно 46%. А, нет, погодите… Это говорит нам о том, что каждое второе предупреждение анализатора — ошибка в коде! По-моему, это достаточно весомый аргумент необходимости использования инструментов статического анализа. О том, как правильно и неправильно использовать статические анализаторы, написано в конце статьи. Пока же предлагаю посмотреть, что же интересного удалось обнаружить в исходном коде.

Найденные ошибки

Одинаковые подвыражения

Допустить ошибку, отлавливаемую диагностическим сообщением V3001 достаточно легко, особенно если использовать copy-paste, или если названия переменных, используемых в выражении, схожи. Эти ошибки — одни из самых распространённых, и в этом проекте без них не обошлось. Попробуйте найти ошибку в фрагменте ниже, не подсматривая в предупреждение анализатора.

public Blob[] GetObjects(UnmanagedImage image, 
                         bool extractInOriginalSize)
{
  ....
  if ((image.PixelFormat != PixelFormat.Format24bppRgb)    &&
      (image.PixelFormat != PixelFormat.Format8bppIndexed) &&
      (image.PixelFormat != PixelFormat.Format32bppRgb)    &&
      (image.PixelFormat != PixelFormat.Format32bppArgb)   &&
      (image.PixelFormat != PixelFormat.Format32bppRgb)    &&
      (image.PixelFormat != PixelFormat.Format32bppPArgb)
      )
  ....
}

Предупреждение PVS-Studio: V3001 There are identical sub-expressions ‘image.PixelFormat != PixelFormat.Format32bppRgb’ to the left and to the right of the ‘&&’ operator. Accord.Imaging BlobCounterBase.cs 670

Здесь два раза повторяется подвыражение image.PixelFormat != PixelFormat.Format32bppRgb. С такими названиями элементов перечисления ошибиться было достаточно просто, что и произошло. Стоит отметить, что, просматривая такой код, очень легко пропустить лишнее подвыражение. А вот является ли одно из сравнений лишним, или подразумевалось иное значение перечисления — вопрос интересный. В одном случае будет просто избыточный код, а в другом — логическая ошибка.

Этот код встретился ещё раз в этом же файле в строке 833. Copy-paste… Copy-paste никогда не меняется.

Ошибочное условие выхода из цикла

Все мы привыкли называть счётчики циклов именами вроде i, j, k и т.п. Как правило, это удобно и является обыденной практикой, но иногда может встать боком. Как в примере ниже.

public static void Convert(float[][] from, short[][] to)
{
  for (int i = 0; i < from.Length; i++)
    for (int j = 0; i < from[0].Length; j++)
      to[i][j] = (short)(from[i][j] * (32767f));
}

Предупреждение PVS-Studio: V3015 It is likely that a wrong variable is being compared inside the ‘for’ operator. Consider reviewing ‘i’ Accord.Audio SampleConverter.cs 611

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

Разная логика для одинаковых условий

Встретился код, когда для двух одинаковых операторов if была предусмотрена разная логика.

public void Fit(double[][] observations, 
                double[] weights, 
                MultivariateEmpiricalOptions options)
{
  if (weights != null)
    throw new ArgumentException("This distribution does not support  
                                 weighted  samples.", "weights");
  ....
  if (weights != null)
      weights = inPlace ? weights : (double[])weights.Clone();
  ....
}

Предупреждение PVS-Studio: V3021 There are two ‘if’ statements with identical conditional expressions. The first ‘if’ statement contains method return. This means that the second ‘if’ statement is senseless Accord.Statistics MultivariateEmpiricalDistribution.cs 653

Странный код, правда? Особенно если учесть тот факт, что переменная weights является параметром метода и никак не используется между этими условиями. Таким образом, второй оператор if никогда не будет выполнен, так как если выражение weights != null истинно, будет сгенерировано исключение типа ArgumentException.

Этот код встретился ещё раз в этом же файле в строке 687.

Условия, значения которых всегда ложны

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

private static void dscal(int n, double da, double[] dx, 
                          int _dx_offset, int incx)
{
  ....
  if (((n <= 0) || (incx <= 0)))
  {
    return;
  }
  ....
  int _i_inc = incx;
  for (i = 1; (_i_inc < 0) ? i >= nincx : i <= nincx; i += _i_inc)
  ....
}

Предупреждение PVS-Studio: V3022 Expression ‘(_i_inc < 0)’ is always false. Accord.Math BoundedBroydenFletcherGoldfarbShanno.FORTRAN.cs 5222

Безусловно, сейчас ошибку найти легче, так как из метода специально были вырезаны фрагменты, не интересные нам. И всё же сходу трудно сказать, где же в этом коде проблемное место. А дело в том (как вы могли догадаться, прочитав сообщение анализатора), что выражение (_i_inc < 0) всегда ложно. Здесь следует обратить внимание на то, что переменная _i_inc инициализируется значением переменной incx. В то же время значение переменной incx на момент инициализации _i_inc положительно, так как иначе был бы осуществлён выход из тела метода выше по коду. Следовательно, _i_inc может иметь только положительное значение, результат сравнения _i_inc < 0 всегда имеет значение false, и условием выхода из цикла всегда будет выражение i <= nincx.

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

private void hqr2()
{
  ....
  int low = 0;
  ....
  for (int i = 0; i < nn; i++)
  {
      if (i < low | i > high)
        ....
  }
  ....
}

Предупреждение PVS-Studio: V3063 A part of conditional expression is always false: i < low. Accord.Math JaggedEigenvalueDecompositionF.cs 571

Подвыражение i < low всегда будет иметь значение false, так как минимальное значение, принимаемое переменной i — 0, а значение low на момент сравнения также всегда будет равно 0. Следовательно, подвыражение i < low будет вычисляться «вхолостую».

Таких мест встретилось много. Все предупреждения приводить не буду, но несколько перечислю:

  • V3063 A part of conditional expression is always false: i < low. Accord.Math JaggedEigenvalueDecompositionF.cs 972
  • V3063 A part of conditional expression is always false: i < low. Accord.Math JaggedEigenvalueDecomposition.cs 571
  • V3063 A part of conditional expression is always false: i < low. Accord.Math JaggedEigenvalueDecomposition.cs 972
  • V3063 A part of conditional expression is always false: i < low. Accord.Math EigenvalueDecomposition.cs 567

Целочисленное деление с приведением к вещественному типу

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

public static double GetSpectralResolution(int samplingRate, 
                                           int samples)
{
  return samplingRate / samples;
}

Предупреждение PVS-Studio: V3041 The expression was implicitly cast from ‘int’ type to ‘double’ type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. Accord.Audio Tools.cs 158

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

А вот следующий код выглядит ещё более странно:

public static int GreatestCommonDivisor(int a, int b)
{
  int x = a - b * (int)Math.Floor((double)(a / b));
  while (x != 0)
  {
    a = b;
    b = x;
    x = a - b * (int)Math.Floor((double)(a / b));
  }
  return b;    
}

Предупреждения PVS-Studio:

  • V3041 The expression was implicitly cast from ‘int’ type to ‘double’ type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. Accord.Math Tools.cs 137
  • V3041 The expression was implicitly cast from ‘int’ type to ‘double’ type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. Accord.Math Tools.cs 142

Анализатору показалось подозрительным выражение (double)(a / b). Метод Floor возвращает наибольшее целое число, меньшее или равное заданному числу двойной точности с плавающей запятой (MSDN. Math.Floor). Но переменные a и b имеют тип int, следовательно — будет выполнено целочисленное деление, результатом которого будет целочисленное значение. Выходит, что нет никакого смысла в явном приведении этого значения к типу double и вызову метода Floor.

Для корректного выполнения операции было нужно привести к типу double один из операндов. Тогда было бы выполнено вещественное деление, и вызов метода Floor был бы оправдан:

Math.Floor((double)a / b)

Параметр метода, значение которого всегда перезаписывается

Продолжим. Ошибки следующего типа встречаются не часто, но всё же попадаются.

private static double WeightedMode(double[] observations, 
                                   double[] weights, 
                                   double mode, 
                                   int imax, 
                                   int imin)
{
  ....
  var bestValue = currentValue;
  ....
  mode = bestValue;
  return mode;
}

Предупреждение PVS-Studio: V3061 Parameter ‘mode’ is always rewritten in method body before being used. Accord.Statistics TriangularDistribution.cs 646

Один из параметров метода — mode — перезаписывается и возвращается. При этом в теле метода mode никак не используется (не считая перезаписи). Не возьмусь утверждать, что это ошибка (среди подозрительных мест, найденных этим диагностическим правилом в других проектах, явно было видно ошибочные), но выглядит этот код странно.

Вообще есть интересная особенность: как правило, ошибки в этом проекте не встречаются поодиночке. Точно такой же код можно найти в нескольких местах. Действительно, copy-paste не меняется…

  • V3061 Parameter ‘mode’ is always rewritten in method body before being used. Accord.Statistics TriangularDistribution.cs 678
  • V3061 Parameter ‘mode’ is always rewritten in method body before being used. Accord.Statistics TriangularDistribution.cs 706
  • V3061 Parameter ‘mode’ is always rewritten in method body before being used. Accord.Statistics TriangularDistribution.cs 735

Разыменовывание нулевой ссылки

public override string ToString(string format, 
                                IFormatProvider formatProvider)
{
  ....
  var fmt = components[i] as IFormattable;
  if (fmt != null)
    sb.AppendFormat(fmt.ToString(format, formatProvider));
  else
    sb.AppendFormat(fmt.ToString());
  ....
}

Предупреждение PVS-Studio: V3080 Possible null dereference. Consider inspecting ‘fmt’. Accord.Statistics MultivariateMixture’1.cs 697

Обычно ошибки, приводящие к возникновению исключений, отлавливаются в процессе разработки, если код тестируется достаточно тщательно. Но не всегда, и пример выше — тому доказательство. В случае, если выражение fmt != null ложно, у объекта fmt вызывается экземплярный метод ToString. Результат? Исключение типа NullReferenceException.

И, как вы уже могли догадаться, такая же ошибка встретилась ещё раз: MultivariateMixture’1.cs 697

Взаимное присвоение ссылок

public class MetropolisHasting<T> : IRandomNumberGenerator<T[]>
{
  ....        
  T[] current;
  T[] next;
  ....
  public bool TryGenerate()
  {
    ....
    var aux = current;
    current = next;
    next = current;
   ....
  }
  ....
}

Предупреждение PVS-Studio: V3037 An odd sequence of assignments of this kind: A = B; B = A;. Check lines: 290, 289. Accord.Statistics MetropolisHasting.cs 290

Очевидно, что в приведённом фрагменте метода TryGenerate хотели поменять местами ссылки на массивы next и current (переменная aux больше нигде не используется). Но из-за ошибки получилось, что и current, и next теперь хранят ссылки на один и тот же массив — тот, ссылка на который содержалась в переменной next.

Правильный код должен был выглядеть так:

var aux = current;
current = next;
next = aux;

Потенциальное деление на 0

При анализе обнаружилось несколько ошибок потенциального деления на 0. Мельком взглянем на них:

public BlackmanWindow(double alpha, int length) 
    : base(length)
{
    double a0 = (1.0 - alpha) / 2.0;
    double a1 = 0.5;
    double a2 = alpha / 2.0;

    for (int i = 0; i < length; i++)
        this[i] = (float)(a0 - 
          a1 * Math.Cos((2.0 * System.Math.PI * i) / (length - 1)) +
          a2 * Math.Cos((4.0 * System.Math.PI * i) / (length - 1)));
}

Предупреждения PVS-Studio:

  • V3064 Potential division by zero. Consider inspecting denominator ‘(length — 1)’. Accord.Audio BlackmanWindow.cs 64
  • V3064 Potential division by zero. Consider inspecting denominator ‘(length — 1)’. Accord.Audio BlackmanWindow.cs 65

Анализатор счёл подозрительным следующий код:

(2.0 * System.Math.PI * i) / (length - 1)

Возможно, что в реальности ошибки здесь и не будет (length — длина какого-то окна, и для возникновения ошибки она должна иметь значение 1), но как знать? Лучше перестраховаться, иначе велик шанс получить очень неприятную ошибку, которую к тому же будет тяжело обнаружить.

Встретился ещё один интересный фрагмент кода с потенциальным делением на 0.

public static double[,] Centering(int size)
{
  if (size < 0)
  {
      throw new ArgumentOutOfRangeException("size", size,
          "The size of the centering matrix must 
           be a positive integer.");
  }

  double[,] C = Matrix.Square(size, -1.0 / size);

  ....
}

Предупреждение PVS-Studio: V3064 Potential division by zero. Consider inspecting denominator ‘size’. Accord.Math Matrix.Construction.cs 794

Как по мне — весьма интересная и забавная ошибка. Анализатор отметил, что в выражении -1.0 / size возможно деление на 0. А теперь обратите внимание на проверку выше. Если size < 0, будет сгенерировано исключение, однако если size == 0, исключения не будет, а вот деление на 0 — будет. При этом в литерале, передаваемом конструктору исключения, написано, что размер матрицы должен быть положительным числом, но проверка при этом осуществляется на неотрицательные значения. А положительное и неотрицательное — всё же разные понятия. Полагаю, для исправления ошибки достаточно просто подправить проверку:

if (size <= 0)

Использование битового оператора вместо логического

Порой приходится сталкиваться с тем, что не все программисты знают разницу между битовыми и логическими операторами (‘|’ и ‘||’, ‘&’ и ‘&&’). Это может приводить к совершенно разным последствиям — от лишних вычислений до падений. В данном проекте встретилось несколько подозрительных мест с использованием битовых операций:

public JaggedSingularValueDecompositionF(
         Single[][] value,
         bool computeLeftSingularVectors, 
         bool computeRightSingularVectors, 
         bool autoTranspose, 
         bool inPlace)
{
  ....
  if ((k < nct) & (s[k] != 0.0))
  ....
}

Предупреждение PVS-Studio: V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math JaggedSingularValueDecompositionF.cs 461

Тело оператора if будет выполнено, если оба подвыражения (k < nct и s[k] != 0.0) имеют значение true. Но при этом, даже если значение первого подвыражения (k < nct) имеет значение false, второе подвыражение всё равно будет вычисляться, чего не было бы при использовании оператора &&. Итак, если программист проверял значение k, чтобы не выйти за границу массива, то у него ничего из этого не вышло.

Схожие предупреждения:

  • V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math JaggedSingularValueDecompositionF.cs 510
  • V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math JaggedSingularValueDecompositionF.cs 595
  • V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math JaggedSingularValueDecomposition.cs 461
  • V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math JaggedSingularValueDecomposition.cs 510
  • V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math JaggedSingularValueDecomposition.cs 595
  • V3093 The ‘&’ operator evaluates both operands. Perhaps a short-circuit ‘&&’ operator should be used instead. Accord.Math Gamma.cs 296

Проверка в цикле одного и того же элемента

Встретилась ошибка, найденная диагностическим правилом из последнего релиза анализатора.

public override int[] Compute(double[][] data, double[] weights)
{
  ....
  int cols = data[0].Length;
  for (int i = 0; i < data.Length; i++)
    if (data[0].Length != cols)
      throw new DimensionMismatchException("data", 
                  "The points matrix should be rectangular. 
                   The vector at position {} has a different
                   length than previous ones.");
  ....
}

Предупреждение PVS-Studio: V3102 Suspicious access to element of ‘data’ object by a constant index inside a loop. Accord.MachineLearning BinarySplit.cs 121

Интересная ошибка. Программист хотел проверить, что зубчатый массив data на самом деле является двумерным (т.е. является матрицей). Но ошибся в выражении data[0].Length != cols, использовав в качестве значения индекса не счётчик цикла i, а целочисленный литерал — 0. В итоге выражение data[0].Length != cols всегда будет ложно, так как оно эквивалентно выражению data[0].Length != data[0].Length. Если бы параметр data был двумерным массивом (Double[,]), этой ошибки, равно как и всей проверки, можно было бы избежать. Но возможно, что использование зубчатого массива обусловлено какими-то особенностями архитектуры приложения.

Передача методу в качестве аргумента вызывающего объекта

Следующий фрагмент кода также выглядит подозрительно.

public static double WeightedMean(this double[] values, 
                                       double[] weights)
{
  ....
}

public override void Fit(double[] observations, 
                         double[] weights, 
                         IFittingOptions options)
{
  ....
  mean = observations.WeightedMean(observations);
  ....
}

Предупреждение PVS-Studio: V3062 An object ‘observations’ is used as an argument to its own method. Consider checking the first actual argument of the ‘WeightedMean’ method. Accord.Statistics InverseGaussianDistribution.cs 325

Анализатор счёл подозрительным факт, что метод WeightedMean в качестве аргумента принимает тот же объект, у которого он вызывается. Это становится вдвойне странным, если учесть, что WeightedMean — метод-расширения. Я провёл дополнительное исследование, посмотрев, как используется данный метод в других местах. Во всех местах его использования в качестве второго аргумента выступал массив weights (обратите внимание, что такой массив есть и в рассматриваемом методе Fit), так что скорее всего это ошибка и правильный код должен выглядеть так:

mean = observations.WeightedMean(weights);

Потенциальная ошибка сериализации

Обнаружилась потенциальная проблема сериализации одного из классов.

public class DenavitHartenbergNodeCollection :  
  Collection<DenavitHartenbergNode>
{ .... }

[Serializable]
public class DenavitHartenbergNode
{
  ....
  public DenavitHartenbergNodeCollection Children 
  { 
    get; 
    private set; 
  }
  ....
}

Предупреждение PVS-Studio: V3097 Possible exception: the ‘DenavitHartenbergNode’ type marked by [Serializable] contains non-serializable members not marked by [NonSerialized]. Accord.Math DenavitHartenbergNode.cs 77

При сериализации экземпляра класса DenavitHartenbergNode возможно возникновение исключения типа SerializationException. Всё зависит от выбранного типа сериализатора. Если в качестве сериализатора выступает, например, экземпляр типа BinaryFormatter, будет сгенерировано исключение, так как требуется, чтобы все сериализуемые члены (а это свойство тоже сериализуемо) были декорированы атрибутом [Serializable].

Некоторые способы решения:

  • реализовать это свойство через поле, декорированное атрибутом [NonSerialized]. Тогда поле (а следовательно — и ассоциированное с ним свойство) не будут сериализованы;
  • реализовать интерфейс ISerializable, и в методе GetObjecData проигнорировать сериализацию этого свойства;
  • декорировать тип DenavitHartenbergNodeCollection атрибутом [Serializable].

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

Если же экземпляры данного типа сериализуются с использованием сериализаторов, не требующих, чтобы все сериализуемые члены были декорированы атрибутом [Serializable], можно не волноваться на счёт этого кода.

Анализатором было обнаружено очень много небезопасных фрагментов вызовов событий. Насколько много? 75 предупреждений V3083! Предлагаю рассмотреть один фрагмент кода, так как остальные, в принципе, схожи с ним.

private void timeUp_Elapsed(object sender, ElapsedEventArgs e)
{
  ....
  if (TempoDetected != null)
    TempoDetected(this, EventArgs.Empty);
}

Предупреждение PVS-Studio: V3083 Unsafe invocation of event ‘TempoDetected’, NullReferenceException is possible. Consider assigning event to a local variable before invoking it. Accord.Audio Metronome.cs 223

В данном фрагменте кода проверяется, есть ли подписчики у события TempoDetected, и, если они есть, вызывается событие. Предполагалось, что, если на момент проверки у TempoDetected не будет подписчиков, это позволит избежать возникновения исключения. Однако есть вероятность, что в момент между проверкой TempoDetected на неравенство null и вызовом события, у него не останется подписчиков (например, в других потоках произойдёт отписка от этого события). В итоге, если на момент вызова события у TempoDetected не будет подписчиков, будет сгенерировано исключение типа NullReferenceException. Избежать подобных проблем можно, например, использую null-условный оператор ‘?.’, добавленный в C# 6.0. Более подробно прочитать о проблеме и других способах её решения можно в документации к этому диагностическому правилу.

Как нужно и как не нужно использовать статический анализатор

Перед тем, как закончить, я бы хотел немного рассказать о том, как же всё-таки необходимо использовать инструменты статического анализа. Часто приходится сталкиваться с таким подходом: «Мы проверили перед релизом наш проект. Что-то ничего особо интересного не нашлось.» Нет, нет, нет! Это самый неправильный способ использования. В качестве параллели можно привести такой пример — откажитесь от разработки программ в IDE, пишите всё в простом блокноте. И перед самым релизом начинайте работать в IDE. Странно? Ещё бы! Толку от этой IDE, если большую часть времени, когда от неё был бы толк, она пылилась у вас на SSD/HDD? Такая же ситуация и со статическими анализаторами. Применять их нужно регулярно, а не разово.

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

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

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

Таким образом, именно регулярное использование инструментов статического анализа позволит получить от них наибольшую пользу.

Итоги

В очередной раз удалось проверить интересный проект, найти не менее интересные ошибки и рассказать о них. Возможно, кто-то что-то возьмёт на заметку, узнает что-то новое или просто будет аккуратнее при написании кода. Тем не менее все мы люди, а человеку свойственно ошибаться. Помочь исправить ошибки, допущенные в программном коде, поможет PVS-Studio. Регулярное применение статического анализатора позволит уменьшить головную боль, связанную с поиском ошибок и их исправлением.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Accord.Net: Looking for a Bug that Could Help Machines Conquer Humankind.

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

 

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

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

  1. Синтаксические — ошибки, которые допускаются в результате невнимательности программистов: неправильно выбран оператор, пропущена буква или символ, лишние буква, символ, оператор и т. п.

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

Синтаксические ошибки

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

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

Вопрос: «Как найти ошибки кода HTML?» решается за пять минут. В сети очень много валидаторов кода HTML — найдите себе подходящий и используйте. К примеру, самым известным из них является «Валидатор от W3C». Данный ресурс предоставит список ошибок HTML с инструкциями и описанием. Вам останется только их исправитьи все будет хорошо.

Таким же образом проверяются синтаксические ошибки CSS и JavaScript:

  • находите нужный валидатор, 

  • находите при помощи него синтаксические ошибки,

  • исправляете ошибки.

Логические ошибки

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

Советы:

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

  2. «Ок, Google!». Если вы нашли ошибку, то есть шанс, что она не уникальна и кто-то с ней уже сталкивался. А это значит, что вполне вероятно, что у кого-то уже есть решение этой проблемы. Поэтому попробуйте найти ее решение в сети.

  3. Ищите строку! Если поиск в сети не дал результатов, то запустите программу в отладчике и попробуйте найти строку кода, где возникает ошибка. Это, скорее всего, не решит проблему, но даст вам хоть какое-то представление о ней и позволит продолжить дальнейшие поиски.

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

  5. Исключайте. Может так случиться, что сразу найти нужную строку кода не получится. В таком случае нужно выявить «проблемный» блок кода. Для этого нужно постепенно отключать компоненты программы, пока не будет выявлен «проблемный» компонент.

  6. Нужно исключить проблему в аппаратном обеспечении. Редкий случай, но бывает так, что проблема с «железом» вызывает ошибки с исследуемой программой. Можно обновить операционную систему, среду разработки, заменить оперативную память и т. д.

  7. Ищите совпадения. Когда возникают ошибки в программе? В одно и то же время? В одном и том же месте? Что общего у пользователей, у которых возникают ошибки? Задавайте подобные вопросы и ищите взаимосвязь. Это может натолкнуть вас на поиск самой проблемы.

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

Заключение

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

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

Искать ошибки в программах — непростая задача. Здесь нет никаких готовых методик или рецептов успеха. Можно даже сказать, что это — искусство. Тем не менее есть общие советы, которые помогут вам при поиске. В статье описаны основные шаги, которые стоит предпринять, если ваша программа работает некорректно.

Шаг 1: Занесите ошибку в трекер

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

  1. Вы забыли какую-то важную деталь об ошибке, например, в чем она заключалась.
  2. Вы могли делегировать ее кому-то более опытному.

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

Вы должны записать в трекер следующую информацию:

  1. Что делал пользователь.
  2. Что он ожидал увидеть.
  3. Что случилось на самом деле.

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

Шаг 2: Поищите сообщение об ошибке в сети

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

Шаг 3: Найдите строку, в которой проявляется ошибка

Если ошибка вызывает падение программы, попробуйте запустить её в IDE под отладчиком и посмотрите, на какой строчке кода она остановится. Совершенно необязательно, что ошибка будет именно в этой строке (см. следующий шаг), но, по крайней мере, это может дать вам информацию о природе бага.

Шаг 4: Найдите точную строку, в которой появилась ошибка

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

Если вы отслеживаете выполнение программы в отладчике, то вы можете пройтись назад по стектрейсу, чтобы найти ошибку. Если вы находитесь внутри функции, вызванной внутри другой функции, вызванной внутри другой функции, то стектрейс покажет список функций до самой точки входа в программу (функции main()). Если ошибка случилась где-то в подключаемой библиотеке, предположите, что ошибка все-таки в вашей программе — это случается гораздо чаще. Найдите по стектрейсу, откуда в вашем коде вызывается библиотечная функция, и продолжайте искать.

Шаг 5: Выясните природу ошибки

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

  1. Ошибка на единицу
    Вы начали цикл for с единицы вместо нуля или наоборот. Или, например, подумали, что метод .count() или .length() вернул индекс последнего элемента. Проверьте документацию к языку, чтобы убедиться, что нумерация массивов начинается с нуля или с единицы. Эта ошибка иногда проявляется в виде исключения Index out of range.
  2. Состояние гонки
    Ваш процесс или поток пытается использовать результат выполнения дочернего до того, как тот завершил свою работу. Ищите использование sleep() в коде. Возможно, на мощной машине дочерний поток выполняется за миллисекунду, а на менее производительной системе происходят задержки. Используйте правильные способы синхронизации многопоточного кода: мьютексы, семафоры, события и т. д.
  3. Неправильные настройки или константы
    Проверьте ваши конфигурационные файлы и константы. Я однажды потратил ужасные 16 часов, пытаясь понять, почему корзина на сайте с покупками виснет на стадии отправки заказа. Причина оказалась в неправильном значении в /etc/hosts, которое не позволяло приложению найти ip-адрес почтового сервера, что вызывало бесконечный цикл в попытке отправить счет заказчику.
  4. Неожиданный null
    Бьюсь об заклад, вы не раз получали ошибку с неинициализированной переменной. Убедитесь, что вы проверяете ссылки на null, особенно при обращении к свойствам по цепочке. Также проверьте случаи, когда возвращаемое из базы данных значение NULL представлено особым типом.
  5. Некорректные входные данные
    Вы проверяете вводимые данные? Вы точно не пытаетесь провести арифметические операции с введенными пользователем строками?
  6. Присваивание вместо сравнения
    Убедитесь, что вы не написали = вместо ==, особенно в C-подобных языках.
  7. Ошибка округления
    Это случается, когда вы используете целое вместо Decimal, или float для денежных сумм, или слишком короткое целое (например, пытаетесь записать число большее, чем 2147483647, в 32-битное целое). Кроме того, может случиться так, что ошибка округления проявляется не сразу, а накапливается со временем (т. н. Эффект бабочки).
  8. Переполнение буфера и выход за пределы массива
    Проблема номер один в компьютерной безопасности. Вы выделяете память меньшего объема, чем записываемые туда данные. Или пытаетесь обратиться к элементу за пределами массива.
  9. Программисты не умеют считать
    Вы используете некорректную формулу. Проверьте, что вы не используете целочисленное деление вместо взятия остатка, или знаете, как перевести рациональную дробь в десятичную и т. д.
  10. Конкатенация строки и числа
    Вы ожидаете конкатенации двух строк, но одно из значений — число, и компилятор пытается произвести арифметические вычисления. Попробуйте явно приводить каждое значение к строке.
  11. 33 символа в varchar(32)
    Проверяйте данные, передаваемые в INSERT, на совпадение типов. Некоторые БД выбрасывают исключения (как и должны делать), некоторые просто обрезают строку (как MySQL). Недавно я столкнулся с такой ошибкой: программист забыл убрать кавычки из строки перед вставкой в базу данных, и длина строки превысила допустимую как раз на два символа. На поиск бага ушло много времени, потому что заметить две маленькие кавычки было сложно.
  12. Некорректное состояние
    Вы пытаетесь выполнить запрос при закрытом соединении или пытаетесь вставить запись в таблицу прежде, чем обновили таблицы, от которых она зависит.
  13. Особенности вашей системы, которых нет у пользователя
    Например: в тестовой БД между ID заказа и адресом отношение 1:1, и вы программировали, исходя из этого предположения. Но в работе выясняется, что заказы могут отправляться на один и тот же адрес, и, таким образом, у вас отношение 1:многим.

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

Шаг 6: Метод исключения

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

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

Шаг 7: Логгируйте все подряд и анализируйте журнал

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

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

Шаг 8: Исключите влияние железа или платформы

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

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

Ради интереса, переключите кабель питания в другую розетку или к другому ИБП. Безумно? Почему бы не попробовать?

Если у вас возникает одна и та же ошибка вне зависимости от среды, то она в вашем коде.

Шаг 9: Обратите внимание на совпадения

  1. Ошибка появляется всегда в одно и то же время? Проверьте задачи, выполняющиеся по расписанию.
  2. Ошибка всегда проявляется вместе с чем-то еще, насколько абсурдной ни была бы эта связь? Обращайте внимание на каждую деталь. На каждую. Например, проявляется ли ошибка, когда включен кондиционер? Возможно, из-за этого падает напряжение в сети, что вызывает странные эффекты в железе.
  3. Есть ли что-то общее у пользователей программы, даже не связанное с ПО? Например, географическое положение (так был найден легендарный баг с письмом за 500 миль).
  4. Ошибка проявляется, когда другой процесс забирает достаточно большое количество памяти или ресурсов процессора? (Я однажды нашел в этом причину раздражающей проблемы «no trusted connection» с SQL-сервером).

Шаг 10: Обратитесь в техподдержку

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

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

Полезные советы (когда ничего не помогает)

  1. Позовите кого-нибудь еще.
    Попросите коллегу поискать ошибку вместе с вами. Возможно, он заметит что-то, что вы упустили. Это можно сделать на любом этапе.
  2. Внимательно просмотрите код.
    Я часто нахожу ошибку, просто спокойно просматривая код с начала и прокручивая его в голове.
  3. Рассмотрите случаи, когда код работает, и сравните их с неработающими.
    Недавно я обнаружил ошибку, заключавшуюся в том, что когда вводимые данные в XML-формате содержали строку xsi:type='xs:string', все ломалось, но если этой строки не было, все работало корректно. Оказалось, что дополнительный атрибут ломал механизм десериализации.
  4. Идите спать.
    Не бойтесь идти домой до того, как исправите ошибку. Ваши способности обратно пропорциональны вашей усталости. Вы просто потратите время и измотаете себя.
  5. Сделайте творческий перерыв.
    Творческий перерыв — это когда вы отвлекаетесь от задачи и переключаете внимание на другие вещи. Вы, возможно, замечали, что лучшие идеи приходят в голову в душе или по пути домой. Смена контекста иногда помогает. Сходите пообедать, посмотрите фильм, полистайте интернет или займитесь другой проблемой.
  6. Закройте глаза на некоторые симптомы и сообщения и попробуйте сначала.
    Некоторые баги могут влиять друг на друга. Драйвер для dial-up соединения в Windows 95 мог сообщать, что канал занят, при том что вы могли отчетливо слышать звук соединяющегося модема. Если вам приходится держать в голове слишком много симптомов, попробуйте сконцентрироваться только на одном. Исправьте или найдите его причину и переходите к следующему.
  7. Поиграйте в доктора Хауса (только без Викодина).
    Соберите всех коллег, ходите по кабинету с тростью, пишите симптомы на доске и бросайте язвительные комментарии. Раз это работает в сериалах, почему бы не попробовать?

Что вам точно не поможет

  1. Паника
    Не надо сразу палить из пушки по воробьям. Некоторые менеджеры начинают паниковать и сразу откатываться, перезагружать сервера и т. п. в надежде, что что-нибудь из этого исправит проблему. Это никогда не работает. Кроме того, это создает еще больше хаоса и увеличивает время, необходимое для поиска ошибки. Делайте только один шаг за раз. Изучите результат. Обдумайте его, а затем переходите к следующей гипотезе.
  2. «Хелп, плиииз!»
    Когда вы обращаетесь на форум за советом, вы как минимум должны уже выполнить шаг 3. Никто не захочет или не сможет вам помочь, если вы не предоставите подробное описание проблемы, включая информацию об ОС, железе и участок проблемного кода. Создавайте тему только тогда, когда можете все подробно описать, и придумайте информативное название для нее.
  3. Переход на личности
    Если вы думаете, что в ошибке виноват кто-то другой, постарайтесь по крайней мере говорить с ним вежливо. Оскорбления, крики и паника не помогут человеку решить проблему. Даже если у вас в команде не в почете демократия, крики и применение грубой силы не заставят исправления магическим образом появиться.

Ошибка, которую я недавно исправил

Это была загадочная проблема с дублирующимися именами генерируемых файлов. Дальнейшая проверка показала, что у файлов различное содержание. Это было странно, поскольку имена файлов включали дату и время создания в формате yyMMddhhmmss. Шаг 9, совпадения: первый файл был создан в полпятого утра, дубликат генерировался в полпятого вечера того же дня. Совпадение? Нет, поскольку hh в строке формата — это 12-часовой формат времени. Вот оно что! Поменял формат на yyMMddHHmmss, и ошибка исчезла.

Перевод статьи «How to fix bugs, step by step»

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

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

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

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

Начало
сеанса отладки

Первый шаг отладки
приложения – это выбор команды Start
Debugging
(F5) на стандартной
панели инструментов или в меню Debug,
после чего приложение запускается в
режиме отладки.

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

Установка
точек останова

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

Установить точку
останова на какой-либо строке кода можно
при помощи щелчка по полю индикаторов
данной строки (рис. 16). Либо можно
установить курсор на нужной строке и
нажать клавишу F9.

Рисунок
16.
Установка
точки останова

Просмотр
данных в отладчике

Когда выполнение
программы в сеансе отладки приостановлено
(например,
при помощи точки
останова), можно
изучить состояние и содержимое ее
переменных и объектов.
Для этого в VS
можно использовать
три вида окон:
Local
(Локальные)
,
Autos
(Видимые)
и Watch
(Контрольные).

Доступ к окнам можно
получить нажав
Debug->Windows->выбрать
нужное окно(Рис. 17)

Рисунок 17. Доступ к
окнам

Окно Local
показывает
все переменные и их значения для текущей
области видимости отладчика.
Это дает вам
представление обо всем,
что имеется в
текущей выполняющейся функции.
Переменные в
этом окне организованы в список и
автоматически настраиваются отладчиком.
На рис.
18 показан пример
окна Local.
С его помощью
можно увидеть приложение нашего примера,
которое
приостановлено до обнуления соответствующих
элементов массива.
Обратите внимание,
что объект
(массив)
a
развернут для
того, чтобы
показать значения его элементов в момент
остановки выполнения программы.
По мере установки
значений результаты будут отображаться
в столбце Value.

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

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

показано окно Autos
для той же
самой строки кода, которая показана на
рис. 18. Обратите внимание на разницу.

Окна Watch
в VS позволяют
настраивать собственный список переменных
и выражений, за которыми нужно наблюдать
(рис. 20). Окна Watch
выглядят и
ведут себя точно так же, как и окна Local
и Autos.
Кроме того, те элементы, которые вы
размещаете в окнах Watch,
сохраняются между сеансами отладки.

Рисунок
18.
Окно
Local

Рисунок
19.
Окно
Autos

Вы получаете доступ
к окнам Watch
из меню или панели инструментов Debug
(рис. 17).
Четыре окна Watch
(которые называются Watch
1
, Watch
2
, Watch
3
и Watch
4
) позволяют
настроить четыре списка элементов, за
которыми необходимо наблюдать. Эта
возможность может быть особенно полезна
в том случае, когда каждый список
относится к отдельной области видимости
вашего приложения.

Переменную или
выражение в окно Watch
1
можно добавить
из редактора кода. Для этого в редакторе
кода выделите переменную (или выражение),
щелкните по ней правой кнопкой мыши и
выберите пункт Add
Watch.
При этом выделенная переменная (или
выражение) будет помещена в окно Watch
1
. Вы можете
также перетащить выделенный элемент в
это окно.

Рисунок
20.
Окно
Watch
1

Пошаговое
прохождение для поиска ошибки

После того как в
нашем примере отладчик,
встретив точку
останова, прервал
выполнение программы,
далее можно
выполнять код по шагам (режим
трассировки
).
Для этого можно
выбрать команду Step
into
на панели
инструментов Debug
или нажать
функциональную клавишу F11(Рис.
21). Это приведет
к последовательному выполнению кода
по одной строке,
что позволит
вам видеть одновременно и ход выполнения
приложения, и
состояние объектов программы по мере
выполнения кода.
Команда Step
into
(F11) позволяет
продвигаться по коду по одной строке.
Вызов этой
команды выполнит текущую строку кода
и поместит курсор на следующую выполняемую
строку. Важное
различие между Step
into
и другими
похожими командами состоит в

том,
как Step
into
обрабатывает
строки кода, в
которых содержатся вызовы функций.
Если вы находитесь
на строке кода,
которая вызывает
другую функцию программы,
то выполнение
команды Step
into
перенесет вас
на первую строку этой функции.

Если сделать так в
нашем примере,
то вы увидите
ошибку: обнуление
элементов массива должно начинаться
не с элемента с индексом i1,
а со следующего
элемента i1+1.

Команда Step
out
(F10) позволяет
вам сохранять фокус в текущей функции
(не заходя в вызываемые ею подпрограммы),
т. е. вызов Run
out
приведет к
выполнению строки за строкой, но не
заведет вас в вызовы функций и при этом
следующей выполняемой

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

Рис 21. Команда Step
Into

Одной из более
удобных (и
часто упускаемых)
функциональных
возможностей набора инструментов
отладки является функция Run
to
cursor
( Выполнить до текущей позиции)
.
Она работает в
полном соответствии со своим названием.
Вы устанавливаете
курсор на некий код и вызываете эту
команду. Приложение
компилируется и выполняется до тех пор,
пока не доходит
до той строки,
где находится
курсор. В
этой точке отладчик прерывает приложение
и выдает вам эту строку кода для пошагового
прохождения.
Рис. 22.

Рисунок 22. Вызов
команды Run
To
Cursor

Продолжить отладку
после точки останова можно повторным
нажатием на кнопку F5 (Start
Debugging).

Рисунок 23. Результат
работы программы после исправления
ошибки

Рассмотрим пошаговое
выполнение программы с использованием
окна Watch
на простейшем примере.

Пример.

#include
«stdafx.h»

#include
«conio.h»

int
_tmain(int argc, _TCHAR* argv[])

{

int
a,S,i,b;

a=0;

b=0;

i=0;

S=1;

do

{

a=a+1;

b=b+2;

S=(S*a)+(S/b);

i++;

}while(i<5);

printf(«%d»,S);

getch();

return
0;

}

Запускаем
трассировку(Step
Into
)
нажатием
F11 либо
Debug>Step
Into
.
Открываем
окно Watch(Debug>Windows>Watch>Watch1).
Переход на
следующий шаг осуществляется нажатием
кнопки F11.

Рисунок 24. Окно
редактора кода в начале трассировки.

Рисунок 25. Значение
переменных перед первым прохождением
цикла

Рисунок 26. Окно
редактора кода перед первым прохождением
цикла

Рисунок
27. Значения переменных после выполнения
операции а=а+1

Рисунок
28. Значения переменных после выполнения
операции b=b+2

Рисунок
29. Значения переменных после выполнения
операции S=(S*a)+(S/b)

Рисунок
30. Значения переменных после выполнения
операции i++

Рисунок
31. Значения переменных после прохождения
цикла

Рисунок 32. Вывод
итогового значения на экран

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

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

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

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

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

Синтаксические ошибки

Данный вид ошибок, также иногда очень критично влияют на работу программы и
тоже может привести к тому, что программа не сможет запуститься. Но такие ошибки
найти и исправить довольно просто. Для этого используют специальные сервисы или
программы — валидаторы.
Очень часто такой вид ошибок связан с кодом HTML. Потому что очень важно, чтобы
код HTML вашего ресурса был валидным, так как от этого напрямую зависит качество его индексации. Об этом неоднократно заявляли представители компании Гугл.
Вопрос: «Как найти ошибки кода HTML?» решается за пять минут. В сети очень много
валидаторов кода HTML — найдите себе подходящий и используйте. К примеру,
самым известным из них является «Валидатор от W3C». Данный ресурс
предоставить список ошибок HTML с инструкциями и описанием. Вам останется
только их исправить и все будет хорошо.
Таким же образом проверяются синтаксические ошибки CSS и JavaScript:

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

Логические ошибки

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

  1. Всегда записывайте ошибку в блокнот или трекер. Как только заметили
    логическую ошибку в программе, нужно записать ее в трекер. Потому что вы
    не знаете сколько уйдет времени и сил на поиск такой ошибки. А в процессе
    поиска может произойти все что угодно и вы просто можете забыть какие-то
    важные детали о самой ошибке.
  2. «Ок, Google!». Если вы нашли ошибку, то есть шанс, что она не уникальна и
    кто-то с ней уже сталкивался. А это значит, что вполне вероятно, что у кого-то
    уже есть решение этой проблемы. Поэтому попробуйте найти ее решение в
    сети.
  3. Ищите строку! Если поиск в сети не дал результатов, то запустите программу в
    отладчике и попробуйте найти строку кода, где возникает ошибка. Это скорее
    всего не решит проблему, но даст вам хоть какое-то представление о ней и
    позволит продолжить дальнейшие поиски.
  4. Найдите точную строку! Отладчик вам выдаст строку с багом, но скорее всего
    причина будет не в этой строке. Чаще всего причина возникает в данных,
    которые получила эта строка с багом. Поэтому нужно провести более
    тщательный анализ и найти причину и природу возникновения ошибки.
    Ошибки могут происходить по разным причинам, поэтому это процесс будет
    не самым простым и легким.
  5. Исключайте. Может так случиться, что сразу найти нужную строку кода не
    получится. В таком случае, нужно выявить «проблемный» блок кода. Для этого
    нужно отключать постепенно компоненты программы, пока не будет выявлен
    «проблемный» компонент.
  6. Нужно исключить проблему в аппаратном обеспечении. Редкий случай, но
    бывает проблема с «железом», вызывает ошибки с исследуемой программой.
    Можно, обновить операционную систему, среду разработку, заменить
    оперативную память и т. д.
  7. Ищите совпадения. Когда возникают ошибки в программе? В одно и то же
    время? В одном и том же месте? Что есть общее у пользователей, у которых
    возникает ошибки? Задавайте подобные вопросы и ищите взаимосвязь. Это
    поможет вас натолкнуть на поиск самой проблемы.
  8. Обратитесь за помощью. Не стесняйтесь спрашивать у более опытных коллег,
    что может быть с вашей программой? Как найти ошибки в кое и как их исправить? Ведь проблема может быть в чем угодно, а поиски решения могут
    занять долгое время. А с вами рядом всегда могут оказаться те, что сможет
    вам помочь.

Возможно вам будет интересно почитать статью “Как стать работником компании Google?”

Заключение

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

Text.ru - 100.00%

Поделись статьей с друзьями!

Понравилась статья? Поделить с друзьями:
  • Как найти логические ошибки в программе
  • Как найти логические и стилистические ошибки
  • Как найти лог ошибки при зависании windows 10
  • Как найти лог ошибки и исправить
  • Как найти лог ошибки в сталкере