Menu

Частые ошибки в C: на чём спотыкаются новички и как это исправить

Ошибки, которые хотя бы раз делает каждый программист на C: пропущенная точка с запятой, = вместо ==, неявное объявление функции, scanf без &, сравнение строк через ==, целочисленное деление, неинициализированные переменные и смещение на единицу — каждая как симптом, причина и решение.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

Каждый пункт ниже — это симптом, стоящая за ним причина и решение.

Пропущенная точка с запятой и лавина ошибок

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

program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'

Причина: в C инструкции завершаются ;. Пропустите одну — и компилятор склеит следующую строку с текущей, а затем сообщит о путанице там, где объединённый текст перестаёт иметь смысл, то есть обычно на следующей строке.

int main(void) {
    int x = 5      /* пропущена точка с запятой */
    printf("%d\n", x);
    return 0;
}

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

Такая же лавина возникает из-за незакрытой фигурной скобки или незавершённого комментария /*, причём ошибки могут оказаться на десятки строк дальше.

Одно дополнение: точка с запятой не ставится после заголовка if, for, while и после закрывающей скобки определения функции. if (x > 0); прекрасно компилируется и не делает ничего — ; и есть всё тело.

Присваивание вместо сравнения

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

int x = 5;
if (x = 10) {              /* присваивает 10, затем проверяет 10 -> истина */
    printf("x равен десяти\n");  /* печатается всегда; x теперь 10 */
}

Причина: = присваивает, == сравнивает. Значение присваивания — это присвоенное значение, поэтому if (x = 10) проверяет 10, которое ненулевое, а значит истинное. Компилятор это принимает, потому что иногда именно это и имеют в виду.

Решение: используйте == в каждом условии и включите -Wall, чтобы компилятор предупреждал:

warning: suggest parentheses around assignment used as truth value

Если вы действительно имеете в виду присваивание внутри условия — обычное дело для while ((c = getchar()) != EOF), — дополнительные скобки говорят об этом прямо и убирают предупреждение.

Неявное объявление функции

Симптом:

warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'

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

Причина: компилятор встретил вызов функции, для которой у него нет объявления. В диалектах до C99 он предполагал, что функция возвращает int, и шёл дальше; на 64-битных системах это предположение обрезает возвращаемый указатель до 32 бит — вот как забытый <stdlib.h> превращает malloc в падение.

Решение: подключите нужный заголовочный файл.

ФункцияЗаголовочный файл
printf, scanf, fopen<stdio.h>
malloc, free, atoi, rand, exit<stdlib.h>
strlen, strcpy, strcmp<string.h>
sqrt, pow, fabs<math.h>
isdigit, toupper<ctype.h>
time<time.h>

Для ваших собственных функций то же предупреждение означает, что вы вызвали функцию до её определения. Поставьте прототип над main:

scanf без &

Симптом: программа падает на вводе или ничего не читает и оставляет переменную нетронутой.

int age;
scanf("%d", age);     /* пропущен & : передаётся значение, а не адрес */

Причина: scanf пишет в вашу переменную, поэтому ему нужен её адрес. Передавая age, вы отдаёте тот мусор, что в ней лежал, и scanf воспринимает его как место для записи, а это обычно segfault.

Решение: & перед переменной — для любого типа, кроме массива, который уже и так является адресом:

Две соседние ловушки из того же семейства: используйте %lf для double в scanf (%f там означает float, и запись байтов размером с float в double оставит его неверным) и всегда ограничивайте %s шириной — %49s для буфера в 50 байт, — иначе длинное слово его переполнит.

Ещё лучше: читайте строку целиком через fgets и разбирайте её — так переполнение невозможно и во входном буфере не остаётся хвостов. Подробнее об обоих — в scanf.

Сравнение строк через ==

Симптом: две очевидно совпадающие строки сравниваются как разные.

char a[] = "hello";
char b[] = "hello";

if (a == b) {              /* сравнение двух адресов: ложь */
    printf("одинаковые\n");
}

Причина: строка в C — это не значение, а указатель на первый символ. == сравнивает указатели. Два массива с одинаковым текстом живут по разным адресам, поэтому проверка ложна. (Что сбивает с толку, сравнение двух одинаковых литералов иногда даёт истину, потому что компилятор может хранить одну копию, — и из-за этого баг становится плавающим.)

Решение: strcmp, и помните, что он возвращает 0 при равенстве:

Перевёрнутое прочтение тоже многих подводит: if (strcmp(a, b)) истинно, когда строки различаются, поскольку ненулевой результат означает «не равны». Всегда пишите == 0 явно.

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

Симптом: среднее, равное 0, процент, который всегда 0 или 100, отношение, потерявшее дробную часть.

int correct = 7, total = 10;
double score = correct / total;      /* 0.0, а не 0.7 */

Причина: оба операнда — int, поэтому C выполняет целочисленное деление и отбрасывает дробь до того, как результат присваивается double. 7 / 10 — это 0; преобразование 0 в double даёт 0.0.

Решение: сделайте один операнд числом с плавающей точкой до деления:

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

Та же ловушка прячется в выражениях вроде (a + b) / 2 для середины отрезка и 1 / 2 * x, которое всегда равно 0 независимо от x.

Пропущенный или неверный return

Симптом: функция возвращает правдоподобное на вид, но неверное число, причём разное при каждом запуске или каждой сборке.

int add(int a, int b) {
    int sum = a + b;
    /* нет инструкции return */
}

Причина: достижение конца функции, не являющейся void, без возврата даёт неопределённое значение — на практике то, что случайно оказалось в регистре возврата. Если вызывающая сторона его использует, это неопределённое поведение.

Более коварная форма — возврат на одних путях и отсутствие возврата на других:

int classify(int n) {
    if (n > 0) return 1;
    if (n < 0) return -1;
    /* при n == 0 управление доходит до конца */
}

Решение: возвращайте значение на каждом пути и компилируйте с -Wall — сообщение GCC «control reaches end of non-void function» ловит оба варианта.

Неинициализированные переменные

Симптом: мусор в выводе или результаты, которые меняются от запуска к запуску и от уровня оптимизации к уровню оптимизации.

int total;                       /* содержит то, что лежало на стеке */
for (int i = 1; i <= 5; i++) {
    total += i;                  /* прибавляем к мусору */
}
printf("%d\n", total);           /* какое-то огромное число */

Причина: локальные переменные не обнуляются. Глобальная или static переменная автоматически получает ноль; локальная начинается с тех байтов, что уже лежали по этому адресу на стеке.

Решение: инициализируйте в момент объявления. Это не стоит ничего и убирает целый класс багов:

-Wall -Wextra предупреждает о многих таких случаях («may be used uninitialized»), а -fsanitize=memory или valgrind ловят остальные. Эта ошибка особенно противна, потому что неинициализированный указатель ведёт прямиком к ошибке сегментации.

Смещение на единицу

Симптом: последний элемент пропущен или тронут один лишний элемент, и программа сбоит позже.

int arr[5];
for (int i = 0; i <= 5; i++) {   /* трогает arr[5], которого не существует */
    arr[i] = i;
}

Причина: у массива из n элементов индексы от 0 до n - 1. <= делает один лишний проход.

Решение: шаблон i < n, причём n вычисляется из массива, а не пишется числом дважды:

Строковый вариант — забыть место под '\0' — это та же ошибка в другой шляпе, а char word[5] с "hello" внутри — это переполнение буфера.

Две мелочи, о которых стоит знать

Точка с запятой после заголовка цикла. for (int i = 0; i < 10; i++);, за которым идёт блок в фигурных скобках, десять раз прокручивает пустой цикл, а затем один раз выполняет блок. И компилируется без единого замечания.

sizeof на указателе. Внутри функции параметр-массив — это указатель, поэтому sizeof(arr) даёт размер указателя (8 байт), а не массива. Передавайте длину отдельным аргументом:

Привычка, которая предотвращает почти всё это

Компилируйте с включёнными предупреждениями, начиная с самой первой программы:

gcc -Wall -Wextra -g program.c -o program

-Wall -Wextra ловят присваивание в условии, отсутствующий return, чтение неинициализированной переменной, неиспользуемую переменную и спецификатор printf, не соответствующий аргументу. Добавленный на время разработки -fsanitize=address,undefined ловит почти всё остальное в момент, когда это происходит.

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

Часто задаваемые вопросы

Что означает «implicit declaration of function» в C?

Компилятор встретил вызов функции, объявления которой он никогда не видел. Почти всегда это забытый #include: printf требует <stdio.h>, malloc<stdlib.h>, strlen<string.h>. Это также может означать, что вы вызвали свою собственную функцию до её определения, — лечится прототипом над main.

Почему моя программа на C выдаёт ошибку в строке, которая выглядит нормально?

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

Почему в C нельзя сравнивать строки через ==?

Потому что строка в C — это char *, то есть указатель, поэтому == сравнивает два адреса, а не символы, на которые они указывают. Две одинаковые строки, лежащие в разных местах, дадут false. Используйте strcmp(a, b) == 0 из <string.h>: функция возвращает 0, когда содержимое совпадает.

Почему 5 / 2 даёт 2 в C?

Потому что оба операнда целые, поэтому C выполняет целочисленное деление и отбрасывает дробную часть. Сделайте одну из сторон числом с плавающей точкой, чтобы получить 2.5: 5.0 / 2 или (double)a / b, когда работаете с переменными. Приводить результат уже поздно: (double)(5 / 2) — это 2.0.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ