Ошибки при программировании на языке С. 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Ошибки при программировании на языке С.



Лекция 1.

Ошибки при программировании на языке С.

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

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

Введение

Определение. Баг (bug) в программе – та часть программного кода, которая приводит к     ошибке, дефекту, аварийному завершению или работе программы со сбоями.

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

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

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

По степени серьезности можно выделить:

·   Катастрофические баги (эффект очень серьезен, система может перестать функционировать, может быть нарушена система безопасности и т.д.)

· Крупные баги (могут вызвать серьезные последствия для системы, такие как, например, потеря важных данных)

· Мелкие баги (могут вызвать небольшие или незначительные последствия для системы, например, отображение данных в неправильном формате, отличном от запроектированного)

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

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

…4 июня 1996 года меньше чем через минуту после своего первого старта французская ракета Ariane 5 (Ариан 5) самоликвидировалась. Результат многолетней работы европейских ученых, гордость стран Евросоюза, новая ракета-носитель взорвалась через 40 секунд после своего первого старта. Только научное оборудование на борту ракеты стоило около 500 миллионов долларов, не говоря о множестве побочных финансовых последствий. Система автоподрыва ракеты сработала после остановки обоих процессоров в результате цепочки ошибок. Началом этой цепочки послужило переполнение буфера, поскольку система навигации подала недопустимо большое значение параметра горизонтальной скорости. Дело в том, что система управления Ariane 5 переделывалась из Ariane 4, а там такого большого значения не могло быть теоретически.  Внутреннее исключительное состояние при выполнении вычислений произошло во время выполнения преобразования данных из 64-битового числа с плавающей точкой в 16-битовое целое число со знаком. Значение числа с плавающей точкой было больше, чем то, что могло быть представлено 16-битовым знаковым целым. В результате возникла ошибка операнда. Эти команды преобразования данных в тексте программы на языке Ада не были защищены от ошибок операнда, в целях снижения нагрузки на рабочий компьютер инженеры сняли защиту – и просчитались. Хотя другие преобразования подобных переменных в том же самом месте в тексте программы были защищены.

Баги

Указатели и память

Утечки памяти

(баг частый, катастрофический)

Утечка памяти часто возникает в языках, в которых не предусмотрена автоматическая «сборка мусора», в том числе в С и С++.

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

Пример подобной ошибки:

# define MAX_SIZE 20

int foo (char *x)

{

//выделяем буфер из динамической памяти, называемой также кучей (heap)

char *p=malloc(MAX_SIZE);

//если строка x не помещается в буфер, то возврат из функции

If (strlen (x) >= MAX_SIZE)

{

  printf (“ String too long.\n”);

  return 1;

}

strcpy (p, x); //копируем строку в буфер

// делаем с ней что-нибудь полезное

...

...

// нормальный выход из программы

free (p); // освобождаем выделенную память

return 0; //возвращение без ошибок

}

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

Учитывая, что серверные приложения не перезапускаются месяцами (и даже годами), становится ясно: утечки представляют собой едва ли не основную проблему, и потеря даже одного байта в долговременной перспективе выливается в сотни мегабайт «осадочной» памяти.

Разыменование NULL

(баг частый, катастрофический)

К этому багу ведет некорректная инициализация или ее отсутствие. Чтобы разыменовать область памяти, нужно ее до этого выделить.

float *arr;

int n;

printf (“ n = “);

scanf (“%i”, &n);

if (n > 0)

arr  = (float *) malloc (n * sizeof (float));

*arr = 1.0; // если n <= 0, то возникнет ошибка

Также данная ошибка может возникнуть из-за использования псевдонимов (две переменные ссылаются на один и тот же объект; после того, как память освобождена через одну переменную, делается попытка использовать другую переменную).

int *ptr1;

int *ptr2;

ptr1 = (int *) malloc (sizeof (int));

ptr2 = ptr1;

...

free (ptr1);

*ptr2 = 666;

Неинициализированная память

(баг редкий, крупный)

Пример 1. Поиск максимума в массиве.

int imax, max, arr [ 10 ];

...

max = arr[0];

for (int i = 1; i < 10; ++i)

   if (arr [ i ] > max)

    {

            max = arr [ i ];       

             imax = i;

     }

printf (“ max = a [ %i ] = %i\n”, imax, max);

Если максимальным является элемент arr [ 0 ], то переменная imax останется неинициализированной, и на экране вполне может появиться что-нибудь вроде

max = arr [ -13267 ] = 11

Пример 2.

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

int   x;

switch (getch ())

{

case ‘0’: x = 0; break;

case ‘1’: x = 1; break;

}

return x;

Простейшее решение проблемы – использовать объявление с инициализацией:

  int x = -1;

и проверять возвращенное функцией значение { 0, 1, -1 }.

Более сложная, но лучшая стратегия – рассмотреть все возможные сценарии и пути выполнения в операторе switch.

Else

  return 0;

Если значение суммы x + y выходит за пределы диапазона значений типа int, возникает переполнение, и бит знака устанавливается в 1. Условие оказывается истинным, и вместо нуля возвращается единица.

Пример 2.

unsigned char a = 100; //01100100 двоичное

unsigned char   b = 200; //11001000

unsigned char  c = a + b; // 100101100 -> 00101100: переполнение

В итоге c = 100 + 200 будет равно 44.

Пример 3.

int a = 1000;

char b = a; /* переполнение при преобразовании int к char. В итоге b будет содержать лишь 8 младших битов a */

printf (“ b = %i\n”, b); // будет выведено b = -24

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

       double > float > int > char.

 Прилагательные long, long long, unsigned повышают ранг типа. Так что unsigned int > int. Если из небольшого значения а типа unsigned int вычесть какое – нибудь число, большее а, то отрицательный результат будет интерпретирован как очень большое положительное число. Такого рода ошибки часто возникают при работе с функциями, возвращающими тип size_t, например, length().

Пример.

/*Поиск вхождения строки “qwert” в строку   s. Функция возвращает позицию самого левого вхождения или -1 */

int find (char *s) {

for (int i =0; i < strlen(s) – 5; ++i)

  if (s[ i ] ==’q’ && s[i + 1] == ‘w’ && s[i + 2] == ‘e’ && s[i + 3] == ‘r’ && s[i + 4] == ‘t’)

                               return  i;

return -1;

}

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

Возможные верные варианты цикла:

for (int i =0; i < (int) strlen(s) – 5; ++i)

или

for (int i =0; i < (int)(strlen(s) – 5); ++i)

Можно предусмотреть случаи, когда s имеет длину < 5:

if (strlen(s) < 5) return -1;      

   

 

Переполнение буфера

(баг частый, катастрофический)

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

int main ()

{

   char ch [ 10 ];

    gets (ch); // может вызвать переполнение буфера

   ...

}

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

- «нападение на стек» - изменение адреса возврата с целью выполнения вредоносного кода

- перезапись указателя с целью получить определенные данные

- переполнение кучи.

 3. 4. Арифметические исключения

(баги частые, крупные / мелкие)

Арифметические исключения – класс ошибок. Некоторые примеры:

· Деление на 0

· Извлечение квадратного корня из отрицательного числа

· Исключения при работе с вещественными числами в формате с плавающей точкой

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

Пример. Вычисление площади треугольника по трем его сторонам (формула Герона).

#include < stdio.h >

#include < conio.h >

#include < math.h >

float   area (float   a, float   b, float c)

{

float   P = a + b + c;

float   p = P/2;

float   s = sqrt (p * (p – a) * (p – b) * (p – c));

return  s;

}

int main() {

  float test[4][3] = { { 3, 4, 5 }, { 5, 12, 13 }, { -3, -4, -5 }, { 3, 4, 8 } };

  for (int i = 0; i < 4; ++i)

          printf (“ area (%f, %f, %f) = %f\n”, test [ i ] [ 0 ], test [ i ] [ 1 ], test [ i ] [ 2 ],

                area (test [ i ] [ 0 ], test [ i ] [ 1 ], test [ i ] [ 2 ]))

getch ();

}

Вывод программы:

 area (3.000000, 4.000000, 5.000000) = 6.000000

area (5.000000, 12.000000, 13.000000) = 30.000000

area (-3.000000, -4.000000,-5.000000) = 6.000000

area (3.000000, 4.000000, 8.000000) = -1.#IND00

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

Решение в данном примере состоит в том, чтобы сделать защиту путем контроля вводимых данных: a > 0, b > 0, c > 0, a + b > c, a + c > b, b + c > a.

Off - by – one

(баг частый, крупный)

Это одна из наиболее общих ошибок. Примеры:

· Начальное значение переменной цикла for равно 1 вместо 0 или наоборот

· Написание <= N вместо < N или наоборот

· Цикл по элементам массива 1.. n вместо 0.. n-1

и т.д.

Приоритет операторов

(баг частый, мелкий)

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

Пример: if ((x==0) && (y == 1))

                   z=1;

              else z=2;

Ошибки обработки строк

Во многих случаях функции обработки строк strcpy, sprint, gets и другие используются неверно.

Например, одним из фактических параметров может оказаться NULL.

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

В функции копирования strcpy () строка source  может быть большего размера, чем строка dest. В результате может произойти переполнение буфера.

Размер массива

Индекс массива в С  всегда начинается с 0. К последнему элементу массива a из n элементовможно обратиться через a [ n – 1].

int  a[10];// индексы элементов массива 0.. 9

for (int  i = 0; i < 10; ++i)

a[ i] = i * i;

Целочисленное деление

В отличие от Pascal, С использует операцию / и для вещественного, и для целочисленного деления. Если оба операнда целочисленные, то используется целочисленное деление, в противном случае – вещественное.

double halfe = 1 / 2; // halfe = 0!

double halfe = 1.0 / 2; // теперь halfe = 0.5

Если оба операнда целые, но требуется вещественное деление, один из операндов нужно привести к вещественному типу:

int x = 5, y = 2;

double d = x / y; // d=2

double d1 = (double) x/ y; // d1=2.5

Sizeof ()

Во всех случаях, когда важно количество памяти, занимаемое переменной того или иного типа, следует использовать sizeof () для определения этого количества памяти.

Примеры.

int  *arr = (int *)malloc (20 * sizeof (int));

/* выделяется память под 20 значений типа int */

double x = 1;

char *ptr = (char *) &x;

for (int i = sizeof (x) – 1; i >= 0; --i) {

printf (“ “);

for (int j = 7; j >= 0; --j)

             printf (“%i”, (*(ptr+i) & (1 << j))? 1: 0);

}

/* Вывод внутреннего представления переменной   x типа double. Здесь sizeof() используется для определения количества байт, занимаемых переменной x.

На экран в данном примере будет выведено

00111111 11110000 00000000 00000000 00000000 00000000 00000000 00000000

*/

Void main()

Вообще говоря, следующие два варианта объявления функции main () являются рабочими:

· int main(){

      ....

      return 0;

}

· void main(){

      ....

      return 0;

}

Тем не менее,следует пользоваться только первым вариантом. Это связано с тем, что число, которое возвращает функция main(), является кодом возврата в операционную систему. В случае объявления void main() стандарт С89 не предусматривает определенного кода возврата. Иными словами, в этом случае программа вернет в операционную систему произвольное число в зависимости от компилятора.

 

5.1.2.2.1. Запуск программы. Функция, вызывающая запуск программы, называется main() (главная). Реализация не объявляет прототип для этой функции. Он должен быть определен путем возврата целого типа int без параметров:            int main (void) {...} либо с двумя параметрами (здесь они называются argc и argv, хотя можно использовать любые имена в том порядке, как они размещены в функции, в которой объявлены):   int main(int argc, char *argv[]){...} либо некоторым другим определенным в реализации способом. 5.1.2.2.3. Завершение программы. Если возвращаемый главной функцией тип является типом, совместимым с int, то возврат в главную функцию эквивалентен вызову функции выхода со значением, возвращенным главной функцией в качестве ее аргумента; при достижении скобки }, которая завершает главную функцию, возвращается значение 0. Если возвращаемый тип несовместим с типом int, состояние завершения, возвращаемое в хост – среду, является неспецифицированным.  

P.S.

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

 

 

   

 

 

 

 

 

 

 

Лекция 1.

Ошибки при программировании на языке С.

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

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

Введение

Определение. Баг (bug) в программе – та часть программного кода, которая приводит к     ошибке, дефекту, аварийному завершению или работе программы со сбоями.

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

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

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

По степени серьезности можно выделить:

·   Катастрофические баги (эффект очень серьезен, система может перестать функционировать, может быть нарушена система безопасности и т.д.)

· Крупные баги (могут вызвать серьезные последствия для системы, такие как, например, потеря важных данных)

· Мелкие баги (могут вызвать небольшие или незначительные последствия для системы, например, отображение данных в неправильном формате, отличном от запроектированного)

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

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

…4 июня 1996 года меньше чем через минуту после своего первого старта французская ракета Ariane 5 (Ариан 5) самоликвидировалась. Результат многолетней работы европейских ученых, гордость стран Евросоюза, новая ракета-носитель взорвалась через 40 секунд после своего первого старта. Только научное оборудование на борту ракеты стоило около 500 миллионов долларов, не говоря о множестве побочных финансовых последствий. Система автоподрыва ракеты сработала после остановки обоих процессоров в результате цепочки ошибок. Началом этой цепочки послужило переполнение буфера, поскольку система навигации подала недопустимо большое значение параметра горизонтальной скорости. Дело в том, что система управления Ariane 5 переделывалась из Ariane 4, а там такого большого значения не могло быть теоретически.  Внутреннее исключительное состояние при выполнении вычислений произошло во время выполнения преобразования данных из 64-битового числа с плавающей точкой в 16-битовое целое число со знаком. Значение числа с плавающей точкой было больше, чем то, что могло быть представлено 16-битовым знаковым целым. В результате возникла ошибка операнда. Эти команды преобразования данных в тексте программы на языке Ада не были защищены от ошибок операнда, в целях снижения нагрузки на рабочий компьютер инженеры сняли защиту – и просчитались. Хотя другие преобразования подобных переменных в том же самом месте в тексте программы были защищены.

Баги

Указатели и память

Утечки памяти

(баг частый, катастрофический)

Утечка памяти часто возникает в языках, в которых не предусмотрена автоматическая «сборка мусора», в том числе в С и С++.

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

Пример подобной ошибки:

# define MAX_SIZE 20

int foo (char *x)

{

//выделяем буфер из динамической памяти, называемой также кучей (heap)

char *p=malloc(MAX_SIZE);

//если строка x не помещается в буфер, то возврат из функции

If (strlen (x) >= MAX_SIZE)

{

  printf (“ String too long.\n”);

  return 1;

}

strcpy (p, x); //копируем строку в буфер

// делаем с ней что-нибудь полезное

...

...

// нормальный выход из программы

free (p); // освобождаем выделенную память

return 0; //возвращение без ошибок

}

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

Учитывая, что серверные приложения не перезапускаются месяцами (и даже годами), становится ясно: утечки представляют собой едва ли не основную проблему, и потеря даже одного байта в долговременной перспективе выливается в сотни мегабайт «осадочной» памяти.



Поделиться:


Последнее изменение этой страницы: 2021-03-09; просмотров: 106; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.227.102.124 (0.109 с.)