Каждый программист на 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.