Компилятор читает сверху вниз
Компилятор C обрабатывает исходный файл за один проход, от первой строки до последней. Дойдя до вызова функции, он должен уже знать три вещи: что функция возвращает, сколько аргументов принимает и какого они типа. Без этого он не может ни сгенерировать корректный код, ни проверить вызов.
Поэтому вот это — проблема:
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add ещё не встречалась */
return 0;
}
int add(int a, int b) {
return a + b;
}
Решение — прототип: сигнатура функции, написанная до вызова, с точкой с запятой там, где было бы тело.
Теперь main может идти первой, и именно так организовано большинство файлов на C: прототипы наверху говорят, что файл предлагает, main читается как план, а детали следуют ниже.
Объявление против определения
Два слова, которые легко смешать, но которые стоит различать:
- Объявление говорит, что функция существует, и задаёт её тип. Оно заканчивается точкой с запятой и не имеет тела. Писать его можно сколько угодно раз.
- Определение даёт тело. Оно должно встречаться ровно один раз во всей программе — два определения одной функции дают ошибку компоновщика («multiple definition of»).
int add(int a, int b); /* объявление (прототип) */
int add(int a, int b) { return a + b; } /* определение - тоже объявление */
Имена параметров необязательны в прототипе; компилятору важны только типы:
int add(int, int); /* законно и эквивалентно */
int add(int a, int b); /* лучше: имена документируют порядок */
Используйте имена. void drawRect(int, int, int, int); не говорит читателю ничего, а void drawRect(int x, int y, int width, int height); — всё.
Что даёт прототип
Не только возможность переставлять определения — именно он заставляет компилятор проверять ваши вызовы.
Когда прототип в области видимости, 3 и 2 преобразуются в double перед вызовом. Без него они были бы переданы как int, а scale прочитала бы их как double — мусор, причём без всякой диагностики в старых диалектах C.
Прототип также ловит неверное число аргументов и несовместимые типы на этапе компиляции:
scale(3.0); /* ошибка: слишком мало аргументов */
scale(3.0, "two"); /* ошибка: передаётся char * там, где ожидается double */
Это как раз те ошибки, которые вам нужны — громкие, во время компиляции, с указанием строки.
Ошибки неявного объявления
Вызовите функцию, о которой компилятор никогда не слышал, и получите одно из самых частых сообщений в C:
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
В C89 компилятор строил догадку: считал, что функция возвращает int, и принимал любые переданные аргументы. Эта догадка чаще неверна, чем верна, и когда настоящая функция возвращает double или указатель, результат — бессмыслица. В C99 неявные объявления убрали из языка, поэтому в C99 и позже это ошибка — современные GCC и clang в свежих версиях отвергают её по умолчанию.
Две причины, два решения:
Ваша собственная функция, ещё не объявленная. Добавьте прототип над вызовом или перенесите определение выше.
Библиотечная функция, чей заголовок вы забыли. Прототипы библиотеки живут в её заголовке, поэтому нужен #include:
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
Родственное сообщение, conflicting types for 'add', означает, что прототип и определение расходятся — отличается тип параметра или тип возвращаемого значения. Исправьте то, что неверно; они должны совпадать в точности.
Взаимной рекурсии нужен прототип
Иногда перестановкой не обойтись: две функции, вызывающие друг друга, не могут обе идти первыми.
Один прототип разрывает круг. Это тот же механизм, на который опирается обычная рекурсивная функция: собственное имя функции видно внутри её тела, поэтому прямой рекурсии прототип не нужен, а взаимной — нужен.
Прототипы в заголовочных файлах
Как только программа занимает несколько файлов .c, прототипы переезжают в заголовок, чтобы все файлы видели одни и те же объявления из одного места.
mathutils.h — интерфейс:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c — реализация:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c — потребитель:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
Затем:
gcc main.c mathutils.c -o program
Здесь стоит перенять две привычки. Обёртка #ifndef / #define / #endif — это страж включения: он не даёт обработать объявления дважды, если заголовок подключится двумя путями. А mathutils.c подключает собственный заголовок — это выглядит избыточным, но таковым не является, потому что заставляет компилятор сверять каждое определение с объявлением, которым будут пользоваться другие файлы. Если они когда-нибудь разойдутся, вы узнаете об этом сразу, а не на этапе компоновки. Об остальном — Заголовочные файлы.
Угловые скобки (<stdio.h>) ищут по системным путям включения; кавычки ("mathutils.h") сначала ищут в каталоге вашего проекта. Для своих заголовков используйте кавычки.
f(void) против f()
Вот это по-настоящему неожиданно, и именно поэтому каждый пример в этой документации пишет int main(void).
void ping(void); /* НЕ принимает аргументов - вызовы с аргументами отвергаются */
void pong(); /* НИЧЕГО не говорит о параметрах */
void ping(void); — это прототип: он объявляет, что функция не принимает параметров, поэтому ping(1, 2, 3) — ошибка компиляции.
void pong(); — объявление старого стиля, унаследованное из достандартного C. Оно объявляет тип возвращаемого значения и ровно ничего о параметрах, поэтому компилятор не может проверять вызовы — pong(1, 2, 3) тихо компилируется и делает нечто неопределённое.
int main(void) { /* правильно: main не принимает аргументов */
int main() { /* законно, но проверка аргументов выключена */
Всегда пишите (void) для функции без параметров. В C23 () стало означать то же, что и (void), что наконец убирает эту ловушку, — но масса кода и масса компиляторов до этого ещё не дошли, а (void) корректно в любом стандарте C.
То же различие относится и к определению. void ping(void) { } — определение в стиле прототипа; void ping() { } — нет, и оно не включает проверку вызовов, расположенных выше него.
Распространённые ошибки
- Точка с запятой на определении.
int add(int a, int b); { return a + b; }объявляетadd, а затем оставляет бесхозный блок. Сообщение об ошибке сбивает с толку; причина — один символ. - Пропущенная точка с запятой в прототипе. Компилятор читает дальше всё, что следует за ней, и сообщает нечто невразумительное несколькими строками ниже.
- Расхождение прототипа и определения.
conflicting types for .... Сделайте их идентичными — а лучше подключите заголовок в файле реализации, чтобы проверка выполнялась автоматически. - Объявление функции внутри другой функции. Законно (
int add(int, int);внутриmain), но тогда объявление видно только в этой функции. Размещайте прототипы на уровне файла. - Определение функции в заголовке. Подключите его из двух файлов
.c, и компоновщик сообщит о дублирующемся определении. В заголовках — объявления; определения идут в файл.c. - Расчёт на проверку через
(). Она не проверяет. Пишите(void).
Часто задаваемые вопросы
Что такое прототип функции в C?
Это объявление сигнатуры функции — типа возвращаемого значения, имени и типов параметров, — заканчивающееся точкой с запятой вместо тела: int add(int a, int b);. Он сообщает компилятору, как функция вызывается, чтобы вызовы можно было проверить, но ничего не говорит о том, что она делает.
В чём разница между объявлением и определением в C?
Объявление вводит имя и тип (int add(int, int);) и может встречаться много раз. Определение даёт тело (int add(int a, int b) { return a + b; }) и должно встречаться ровно один раз во всей программе. Каждое определение одновременно является объявлением.
Как исправить «implicit declaration of function» в C?
Добавьте прототип перед вызовом. Для своих функций поместите returnType name(paramTypes); в начало файла или в заголовок; для библиотечных функций подключите нужный заголовок — #include <stdio.h> для printf, <stdlib.h> для malloc, <string.h> для strlen, <math.h> для sqrt.
В чём разница между f(void) и f() в C?
void f(void); объявляет функцию, которая не принимает аргументов, и компилятор отвергнет любой вызов с аргументами. void f(); — объявление старого стиля, которое не говорит о параметрах ничего, поэтому проверка аргументов отключена. Всегда пишите (void); в C23 обе формы наконец стали значить одно и то же, но старый код и старые компиляторы до сих пор различаются.