Зона кода

А давайте немного попрограммируем...

Об одной глупой ошибке и её неожиданных последствиях

C99Полезное

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

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

Оказалось, что ответить на этот вопрос не очень-то и просто. Я провёл небольшое расследование и кое-что стало понятно, однако, не могу сказать, что сумел расставить все точки над i.

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

Но сначала — небольшая разминка.

Разминка

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

    uint y3 = y12 - 1 + round((x2 - x1 + 1) * 0,86602540378);

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

А теперь вопрос к читателю: какую ошибку содержит эта строка?

Догадались? Если нет, то даю подсказку. Сообщение компилятора об ошибке выглядит так: "too many arguments to function 'round'" ("слишком много аргументов передано функции 'round'").

Сам я, даже прочитав это сообщение, обнаружил саму ошибку, к сожалению, далеко не сразу. А заключается она вот в чём: запятую, призванную отделить целую часть числа от дробной, компилятор воспринимает как разделитель аргументов. Таким образом, он считает, что первый фактический аргумент, переданный функции round(), — это значение выражения (x2 - x1 + 1) * 0, а второй — число 86602540378. Но функция round() принимает один аргумент, а не два, поэтому компилятор и "недоволен".

Разумеется, я хотел передать функции round() лишь одно значение, но по ошибке для отделения дробной части от целой использовал запятую вместо точки. Я, просто напросто, скопировал число из стандартного калькулятора Windows в буфер обмена, после чего вставил его в редактор кода. А в калькуляторе в качестве разделителя используется запятая.

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

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

А теперь — к основной теме статьи

Проблемная программа

Я убрал из программы, содержащей ошибку, которую мы собираемся обсуждать, все те функции и действия, которые никакого отношения к ошибке не имели. В итоге, сильно урезанная программа, как мы увидим, стала достаточно бессмысленной, хотя её код синтаксически безупречен. Мы не будем отвлекаться и пояснять, чем именно занималась исходная программа; желающие это узнать, могут обратиться к соответствующей статье (для тех, кто будет читать ту статью, поясним: речь идёт о второй версии программы).

Программа состоит из файла main.c и файлов графической библиотеки pgraph. Читателю необязательно быть знакомым с этой библиотекой, чтобы понимать код, содержащийся в main.c: этот код я подробно прокомментирую.

Содержимое файла main.с приведено ниже.

 1.#include "pgraph.h"
 2.
 3.#define W 720
 4.#define H 720
 5.
 6.image *img;
 7.
 8.void save()
 9.{
10.    static int count = 0;
11.    char filename[16];
12.    sprintf(filename, "results\\%04d.bmp", ++count);
13.    save_to_file(img, filename);
14.}
15.
16.int main()
17.{
18.    img = create_image(W, H);
19.    for (int i = 0; i < 24; i++)
20.        save();
21.    free(img);
22.    return 0;
23.}

К нашему файлу подключается графическая библиотека pgraph (стр. 1). Объявляется глобальная переменная img — указатель на объект типа image, описанного в этой библиотеке. Этот объект представляет собой холст для рисования, с которым можно, посредством функций, определённых в pgraph, выполнять различные действия: например, рисовать на нём, а также сохранять объект в графическом файле формата BMP. Объекты типа image вполне уместно называть также изображениями.

Рассмотрим функцию save(). Она сохраняет объект, адресуемый img, в графическом файле, помещаемом в каталог results, который, в свою очередь, находится в директории исполняемого файла. Имя создаваемого графического файла зависит от того, каким по счёту является текущий вызов функции save(). При первом вызове файл называется 0001.bmp, при втором — 0002.bmp и т. д.

Статическая переменная count (стр. 10) — это счётчик вызовов функции save(). Его значение является частью имени создаваемого файла. Для хранения пути к этому файлу относительно директории исполняемого файла создаётся массив filename (стр. 11). Сам путь формируется и записывается в этот массив в строке 12. В 13-й строке с помощью функции save_to_file(), определённой в библиотеке pgraph, изображение, адресуемое img, сохраняется в файле, путь к которому "прописан" в массиве filename.

Переходим к функции main(). Создаём динамически изображение с помощью функции create_image(), определённой в pgraph (стр. 18). Такое только что созданное изображение будет представлять собой белый квадрат со стороной 720 пикселей. А затем с помощью цикла сохраняем это изображение в 24-х одинаковых файлах с именами 0001.bmp, 0002.bmp, …, 0024.bmp (см. стр. 19-20). Наконец, удаляем уже ненужный объект, адресуемый img (стр. 20).

Напоминаю, что нет смысла искать в этой программе смысл улыбка.

Что получаем в итоге?

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

После запуска программа упорно завершаться не хотела. А когда я заглянул в директорию results, то обнаружил в ней уже несколько сотен файлов, и они продолжали появляться! Программу пришлось принудительно завершить комбинацией клавиш Ctrl+C.

А теперь предлагаю читателю внимательно посмотреть на код, находящийся в файле main.c, приведённый выше, и ответить на вопрос: где мной была допущена ошибка?

Как я обнаружил ошибку

Разумеется, я понял, что цикл, по неясным мне причинам, никак не хочет завершаться. А что происходит со значениями переменной цикла i?

Чтобы ответить на это вопрос, я немного изменил код функции main(): в самое начало тела цикла вставил вывод на консоль содержимого i:

int main()
{
    img = create_image(W, H);
    for (int i = 0; i < 24; i++)
    {
        printf("%d\n", i);
        save();
    }
    free(img);
    return 0;
}

В итоге на консоль было выведен ноль, за которым стали выводиться сплошные единицы:

0
1
1
1
1

С первым нулём и второй единицей всё нормально: они и должны выводиться. Но почему дальше выводятся сплошные единицы? Единственный ответ, который мне пришёл в голову: значения i каждый раз каким-то образом обнуляет функция save(). Но каким?

Начал тщательно изучать код функции save(). И ошибка обнаружилась в 11-й строке: слишком маленький размер я отвёл массиву filename! Действительно, имя директории results состоит из 7 символов, ещё один символ — обратный слеш, наконец полное имя файла — это ещё 8 символов. Итого, путь к графическому файлу относительно директории исполняемого файла состоит из 16 символов. Но ведь для хранения C-строки длинною 16 символов требуется 17 односимвольных ячеек памяти, поскольку последняя ячейка должна содержать завершающий нулевой символ!

Таким образом, массив для хранения пути к файлу должен иметь размер 17, а не 16! Ошибка, которую я допустил, прямо скажем, детская!

После того, как я заменил 11-ю строку строкой

    char filename[17];

программа заработала нормально.

Но почему?

Хоть причина ошибки и была обнаружена, хотелось также установить причинно-следственную связь между ошибкой и её последствиями, т. е. ответить на вопрос: почему указание неверного размера массива приводит к обнулению переменной i?

Совершенно понятно, что функция sprintf(), вызывающаяся из save(), записывает нулевой завершающий символ формируемой ею C-строки в байт, который для этого не предназначен. Я выдвинул следующее предположение: участок памяти для хранения переменной i расположен сразу после участка, выделенного для хранения массива filename. В результате функция sprintf() записывает нулевой символ в первый байт, выделенный для i.

Версия весьма правдоподобна. Действительно, если i содержит число от 0 до 255 включительно, то только первый байт может быть отличен от 0, а оставшиеся 3 — нулевые (компилятор MinGW64, который мы используем, отводит под переменные типа int по 4 байта). Так что если первый байт обнулить, то все 4 байта будут содержать нули, т. е. i приобретёт значение 0. В нашем случае как раз и происходят одно обнуление исходного значения, т. е. нуля и многократные последующие обнуления единиц.

На всякий случай, поясню, что в компьютерах, использующих intel-совместимые процессоры, байты, содержащие значение типа int, располагаются в памяти в порядке возрастания их старшинства. Например, если в память записано значение типа int, равное 0xabcdef9, то отведённые под это значение 4 байта будут, в порядке увеличения адресов, содержать значения 0xf9, 0xde, 0xbc, 0xa. Так что адрес переменной типа int всегда совпадает с адресом её младшего байта. Например, адрес переменной со значением 0xabcdef9 совпадёт с адресом байта, содержащего xf9.

Всё, сказанное в предыдущем абзаце, относится, разумеется, ко всем целочисленным переменным.

Но вернёмся к моей версии. Чтобы проверить её, я вставил в код функций save() и main() вызовы функций, выводящие на печать адреса массива filename и переменной i соответственно. Вот как теперь стал выглядеть код этих функций:

 1.void save()
 2.{
 3.    static int count = 0;
 4.    char filename[16];
 5.    printf("адрес filename: %p\n", filename);
 6.    sprintf(filename, "results\\%04d.bmp", ++count);
 7.    save_to_file(img, filename);
 8.}
 9.
10.int main()
11.{
12.    img = create_image(W, H);
13.    for (int i = 0; i < 24; i++)
14.    {
15.        printf("%d\n", i);
16.        save();
17.        printf("адрес i: %p\n", &i);
18.    }
19.    free(img);
20.    return 0;
21.}

Новые строки имеют номера 5 и 17.

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

1.0
2.адрес filename: 000000000062FE00
3.адрес i: 000000000062FDFC
4.1
5.адрес filename: 000000000062FE00
6.адрес i: 000000000062FDFC
7.1
8.адрес filename: 000000000062FE00
9.адрес i: 000000000062FDFC

И что мы видим? Адрес массива превышает адрес переменной, причём разность адресов равна 4-ём, т. е. размеру этой переменной. Действительно, массив и переменная располагаются в памяти "вплотную", но только за переменной следует массив, а не наоборот, как я предполагал!

Итак, я оказался неправ. Нулевой завершающий символ C-строки записывается в область памяти, не пресекающейся с её участком, отведённым под переменную i.

Продолжаем исследования

Я выдвинул новую версию. Предположил, что нулевой символ "портит" ту область памяти, в которой содержится какой-либо адрес. Далее происходит запись по неверному адресу, которая как-раз таки изменяет содержимое i. Такая запись, подумал я, может выполняться одной из функций, вызываемых из save(): sprintf() или save_to_file(). Эта версия — единственная, пришедшая в мою голову.

Как её проверить? Я решил, что уберу из программы вызовы этих двух функций и посмотрю, что изменится. Да и вообще, упрощу программу по максимуму. А нулевой завершающий символ в байт, следующий за массивом filename, я запишу "сам" (функция sprintf(), в силу своего отсутствия, сделать этого уже не сможет). Вот какая программа у меня получилась:

 1.#include <stdio.h>
 2.
 3.void save()
 4.{
 5.    char filename[16];
 6.    filename[16] = 0;
 7.}
 8.
 9.int main()
10.{
11.    int i = 0xabcdef9;
12.    printf("i до вызова save(): %x\n", i);
13.    save();
14.    printf("i после вызова save(): %x", i);
15.    return 0;
16.}

Я убрал все типы и функции, определённые в библиотеке pgraph, поэтому необходимость в её отпала. Как мы видим, размер тела функции save() крайне мал. Мы лишь объявляем массив (стр. 5) и записываем ноль в байт, следующий за ним (стр. 6).

Из функции main() я убрал цикл и присвоил переменной i такое значение, что обнуление любого из 4-х её байтов скажется на этом значении (стр. 11). Это я сделал для того, чтобы отслеживать те байты i, которые будут обнулены (если они будут обнулены). Значение i я вывожу 2 раза: до вызова save() (стр. 12) и после (стр. 14).

Компилирую и запускаю программу. Получаю следующий вывод:

i до вызова save(): abcdef9
i после вызова save(): 0

Ну что ж, значение i по-прежнему изменяется. Значит, функции sprintf() и save_to_file(), скорее всего, ни при чём. А вот и сюрприз: обнуляется не один байт переменной i, а вся переменная целиком. Как такое может быть? В save() изменяется лишь 1 байт, а в итоге, изменяются 4! Какой код осуществляет полное обнуление i?

Над этим вопросом я глубоко задумался. И тут мне пришло в голову вывести на печать адреса переменной i до вызова save() и после. Вот каким образом я изменил функцию main():

int main()
{
    int i = 0xabcdef9;
    printf("i до вызова save(): %x\n", i);
    printf("адрес i до вызова save(): %p\n", &i);
    save();
    printf("i после вызова save(): %x\n", i);
    printf("адрес i после вызова save(): %p", &i);
    return 0;
}

И вот какой результат выполнения программы я получил:

i до вызова save(): abcdef9
адрес i до вызова save(): 000000000062FE4C
i после вызова save(): 0
адрес i после вызова save(): 000000000062FDFC

Вот это да! Изменился адрес локальной переменной! Но ведь такого быть не должно никогда! Это аксиома! Ведь мы знаем, что ни при каких обстоятельствах адрес переменной измениться не может! Но, оказывается, что таким хитрым приёмом изменить адрес переменной можно, всё же, ухитриться.

Заметим, что "новый" адрес i меньше "старого" на 80.

А теперь давайте выясним, что располагается по "старому" адресу. Для этого сразу после директивы #include объявим глобальную переменную p для хранения "старого" адреса i:

int *p;

Я объявил p именно глобальной переменной, чтобы не "затрагивать" участок памяти, выделенный для хранения локальных переменных функции main(). Теперь добавим в main() сохранение в p "старого" адреса i и вывод на печать значение, расположенного по "старому" адресу, после вызова save():

int main()
{
    int i = 0xabcdef9;
    printf("i до вызова save(): %x\n", i);
    printf("адрес i до вызова save(): %p\n", p = &i);
    save();
    printf("i после вызова save(): %x\n", i);
    printf("адрес i после вызова save(): %p\n", &i);
    printf("значение, расположенное по \"старому\" адресу: %x", *p);
    return 0;
}

И вот результат выполнения:

i до вызова save(): abcdef9
адрес i до вызова save(): 000000000062FE4C
i после вызова save(): 0
адрес i после вызова save(): 000000000062FDFC
значение, расположенное по "старому" адресу: abcdef9

Таким образом, значение, располагающееся по "старому" адресу переменной i, не изменилось!

Выходит, что я, полагая, что запись нуля в байт, расположенный сразу после массива filename, приводит к повреждению значения переменной i, заблуждался. Значение остаётся в целости и сохранности, а вот адрес переменной i портится. И "новое" значение переменной i (т. е. ноль) — это просто значение, расположенное по "новому" адресу i.

Следующий эксперимент: давайте выясним, будет ли повторный вызов функции save() снова изменять адрес i. Вот соответствующий код функции main():

 1.int main()
 2.{
 3.    int i = 0xabcdef9;
 4.    printf("i до вызова save(): %x\n", i);
 5.    printf("адрес i до вызова save(): %p\n", p = &i);
 6.    save();
 7.    printf("i после вызова save(): %x\n", i);
 8.    printf("адрес i после вызова save(): %p\n", &i);
 9.    i = 0xabcdef9;
10.    save();
11.    printf("i после второго вызова save(): %x\n", i);
12.    printf("адрес i после второго вызова save(): %p\n", &i);
13.    return 0;
14.}

Я, как можно заметить, перед вторым вызовом save(), снова присваиваю i значение 0xabcdef9 (стр. 9). Теперь присвоение идёт уже по "новому" адресу. После компиляции запускаем программу на выполнение и получаем вот что:

1.i до вызова save(): abcdef9
2.адрес i до вызова save(): 000000000062FE4C
3.i после вызова save(): 0
4.адрес i после вызова save(): 000000000062FDFC
5.i после второго вызова save(): abcdef9
6.адрес i после второго вызова save(): 000000000062FDFC

Что ж, второй вызов save() никак не повлиял ни на адрес переменной i, ни на её значение.

А давайте теперь выясним, что именно содержится в байте, следующим за массивом filename, до того, как мы записываем туда 0 в функции save(). Выведем содержимое этого байта перед его изменением на печать:

void save()
{
    char filename[16];
    printf("Содержимое байта: %x\n", filename[16]);
    filename[16] = 0;
}

Вместе с этой версией save() будем использовать третью с конца версию функции main(). Вот какой вывод мы получаем:

i до вызова save(): abcdef9
адрес i до вызова save(): 000000000062FE4C
Содержимое байта: 50
i после вызова save(): 0
адрес i после вызова save(): 000000000062FDFC

Итак, в интересующем нас байте до его обнуления хранилось число 50 в шестнадцатеричной системе исчисления, т. е. 80 в десятичной. Но ведь "новый" адрес переменной i "отстаёт" от "старого" именно на 80! Нет никаких сомнений в том, что это совпадение неслучайно!

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

void save()
{
    char filename[16];
    printf("Содержимое четырёх байтов: %x\n", *(int *) &filename[16]);
    filename[16] = 0;
}

Функцию main() оставим без изменений.

Вот какой консольный вывод получаем в итоге:

i до вызова save(): abcdef9
адрес i до вызова save(): 000000000062FE4C
Содержимое четырёх байтов: 62fe50
i после вызова save(): 0
адрес i после вызова save(): 000000000062FDFC

И что же мы видим? Сразу же за массивом filename хранится некоторый адрес, превышающий "старый" адрес переменной i на 4, т. е. на размер значения типа int.

К каким же выводам можно придти? Как мы знаем, память, резервируемая для хранения локальных переменных функции, организована в виде стека. И, вероятно, сразу за массивом filename располагается область памяти, содержащая адрес дна стека, предназначенного для хранения локальных переменных функции main(). Судя по всему, стек растёт в направлении уменьшения адресов, т. к. первоначальный адрес i меньше адреса предполагаемого дна стека.

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

Адреса локальных переменных, надо полагать, вычисляются с использованием адреса дна стека, из которого вычитаются смещения адресов локальных переменных. В нашем случае смещение, соответствующее переменной i, равно 4 (как раз размер значения типа int).

Первоначально адрес i, скорей всего, вычислялся так: 0x62fe50 − 0x4 = 0x62fe4c. А после того, как мы обнулили предположительно последний байт адреса дна стека, стал вычисляться, видимо, так: 0x62fe00 − 0x4 = 0x62fdfc. Если это верно, то, действительно, обнаруженное нами совпадение первоначального содержимого обнулённого байта с разностью "старого" и "нового" адресов переменной i неслучайно.

Ну а по "новому" адресу i располагались 4 нулевые байта, идущие подряд. Именно поэтому создалась иллюзия обнуления значения i.

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

А почему, всё-таки, цикл бесконечный?

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

Вопрос вполне резонный. Давайте вспомним, что значения счётчика цикла, выводимые нами на консоль перед вызовом save(), всегда, за исключением первого нулевого значения, равны 1. Это означает, что после вызова save() значение i всегда нулевое. Но ведь изменение адреса i происходит только после первого вызова save(), а в ходе следующих вызовов адрес не изменяется. Почему же тогда значение i обнуляется после каждого вызова save()?

Единственное объяснение, которое мне приходит в голову, заключается в том, что обнулением i занимаются функции, вызывающиеся из save() (или одна из этих функций). Дело может быть в следующем. Какая-то функция, вызывающаяся из save(), задействует область памяти, в которой теперь хранится значение i (или часть этой области), а после использования — освобождает её. Причём, делает это на вполне "законных" основаниях, ведь эта область памяти никогда не резервировалась под i, а доступ к ней мы получили "незаконно".

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

Для того, чтобы проверить эту гипотезу, я убрал из первоначального варианта save() вызов одной из функций, а именно, функции sprintf(). Разумеется, вставил оперетор присваивания, обнуляющий байт, следующий за массивом filename. Вызов save_to_file() оставил, но немного изменил его, задав постоянное имя файла (формированием переменного имени занималась, как раз-таки, sprintf()). Также я использовал тот вариант main(), в котором на консоль выводится i. Ниже привожу содержимое файла main.c полностью.

#include "pgraph.h"

#define W 720
#define H 720

image *img;

void save()
{
    static int count = 0;
    char filename[16];
    filename[16] = 0;
    save_to_file(img, "filename.bmp");
}

int main()
{
    img = create_image(W, H);
    for (int i = 0; i < 24; i++)
    {
        printf("%d\n", i);
        save();
    }
    free(img);
    return 0;
}

Вот какой консольный вывод я получил в результате выполнения программы:

0 
1 
2 
...
...
...
22
23

Многоточия, разумеется, вставил я сам, заменив ими числа от 3 до 21.

Итак, цикл выполнился 24 раза, что подтверждает мою гипотезу о записи одной из функций нулей в область памяти, содержащую счётчик цикла (после изменения его адреса). И "виноватой" в этом оказалась функция sprintf(). Напомню, что эта же функция в исходно программе ответственна и за обнуление байта, следущего за массивом filename.

А ещё мне стало интересно, полностью функция sprintf() затирает значение i (расположенное по "новому" адресу) или частично. Из уже проведённых экспериментов этого не понять: при вызове sprintf() значение i всегда было настолько мало, что 3 старших байта всегда были нулевые.

Для удовлетворения своего интереса я написал следующую программу:

#include <stdio.h>

void save()
{
    static int count = 0;
    char filename[16];
    sprintf(filename, "results\\%04d.bmp", ++count);
}

int main()
{
    int i = 0xabcdef9;
    printf("i до первого вызова save(): %x\n", i);
    save();
    printf("i после первого вызова save(): %x\n", i);
    i = 0xabcdef9;
    printf("i до второго вызова save(): %x\n", i);
    save();
    printf("i после второго вызова save(): %x\n", i);
    return 0;
}

Вот что я получил в результате её выполнения:

i до первого вызова save(): abcdef9
i после первого вызова save(): 0
i до второго вызова save(): abcdef9
i после второго вызова save(): 0

Вывод: затирает полностью.

Заключение

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

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

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

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

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

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

Но и высококлассные программисты допускают ошибки, с которыми мы встречаемся уже как пользователи. Взять тот же самый Dev-C++ 5.11, которым я постоянно пользуюсь. У этой программы случаются внезапные зависания, пусть и очень редкие. А ещё весьма странным образом осуществляется копирование кода в буфер обмена в случае, если код содержит кириллицу. Иногда такое копирование выполняется нормально. А иногда в место русских букв в буфер обмена вставляются нечитаемые символы.

Напоследок отмечу, что ошибки, связанные с выходом за границы массивов, могут проявляться по разному и в случае, если один и тот же ошибочный код компилируется разными компиляторами. Так что поясню, что весь код, приведённый в этой статье, я компилировал компилятором MinGW64 версии 4.9.2, работающем под управлением 64-битной версии операционной системы Windows 10.

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

У меня всё!