Menu

Прототипы функций в C: объявление против определения

Почему C должен увидеть форму функции до её вызова — написание прототипов, исправление ошибки implicit declaration, прототипы в заголовочных файлах и настоящая разница между f(void) и f().

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

Компилятор читает сверху вниз

Компилятор 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 обе формы наконец стали значить одно и то же, но старый код и старые компиляторы до сих пор различаются.

Coddy programming languages illustration

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

НАЧАТЬ