Объявление и вызов функций

Урок объясняет, как устроены функции в C: определение с параметрами и типом возврата, разница между объявлением и определением, зачем нужны прототипы.
Функция в C — это именованный блок кода с чётким контрактом: какие типы принимает и какой тип возвращает. Компилятор обязан знать этот контракт ДО первого вызова, иначе будет ошибка.

Функция позволяет дать имя куску логики и переиспользовать его. Определение состоит из типа возврата, имени, списка параметров и тела:

// тип   имя    параметры
int sum(int a, int b) {
    return a + b;     // возвращаем результат
}

int main(void) {
    int result = sum(3, 4);   // вызов
    printf("%d\n", result);   // 7
    return 0;
}

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

В C важен порядок: функцию нужно объявить до её использования. Если main идёт первым, а sum — после, компилятор, дойдя до вызова, ещё не знает о sum. Решение — прототип: объявление функции без тела в начале файла.

int sum(int a, int b);   // прототип: контракт известен заранее

int main(void) {
    printf("%d\n", sum(3, 4));
    return 0;
}

int sum(int a, int b) {   // определение ниже — уже не проблема
    return a + b;
}

Как работает под капотом

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

main()                         sum(a, b)
  |                              |
  | result = sum(3, 4)          |
  |   копируем 3 -> a           |
  |   копируем 4 -> b   ------>  a=3, b=4
  |   (запоминаем, куда         |
  |    вернуться)               | return a + b  (=7)
  |                    <------   |
  | result = 7                  |
  v                             (функция завершилась)
продолжаем после вызова

Прототип нужен именно для того, чтобы компилятор в точке вызова знал: sum принимает два int и возвращает int. Тогда он сможет проверить корректность вызова и правильно подготовить аргументы.

Частые ошибки

  • Вызов функции до объявления без прототипа. Старые компиляторы «угадывали» тип, новые выдают ошибку. Всегда объявляйте заранее.
  • Несовпадение типа возврата и return. Функция int, а вы забыли return — поведение не определено.
  • Пустые скобки вместо void. int f() в старом C значит «параметры не проверяются», а int f(void) — «параметров нет». Пишите void.
  • Слишком длинные функции. Функция на 200 строк нечитаема и плохо тестируется.

Best practices

  • Выносите прототипы функций в заголовочный файл (.h), а определения — в .c. Это основа модульности в C.
  • Одна функция — одна задача. Если не можете описать функцию одним предложением, её стоит разбить.
  • Давайте функциям глаголы в именах: calculate_average, print_table — код читается как фразы.

Идея «функция — это контракт вход/выход» одинакова во всех языках, и логику легко проверить на Python.

# Та же функция sum и её использование
def c_sum(a, b):
    return a + b

result = c_sum(3, 4)
print("sum(3, 4) =", result)

# Композиция функций — типично и для C
def average(a, b, c):
    return c_sum(c_sum(a, b), c) / 3

print("Среднее (10,20,30):", average(10, 20, 30))

Та же логика на Python ▶ — разница в том, что в Python нет прототипов: интерпретатор узнаёт о функции в момент вызова. В C компилятор должен знать контракт заранее.

Заголовочные файлы и модульность

В настоящих проектах функции не живут в одном файле. Их прототипы выносят в заголовочный файл (.h), а определения — в файл реализации (.c). Другие модули подключают заголовок через #include \"mymodule.h\" и получают доступ к функциям, не зная их внутренностей. Это и есть модульность C: заголовок — публичный контракт, .c-файл — скрытая реализация. Чтобы один и тот же заголовок, подключённый дважды, не вызвал ошибку повторного объявления, его оборачивают в защиту от повторного включения — конструкцию #ifndef / #define / #endif или прагму #pragma once. Понимание этого механизма — порог между «пишу один файл» и «работаю над реальным проектом из многих файлов».

Итоги

Функция в C — это контракт: тип возврата, имя, типы параметров. Компилятор должен знать этот контракт до первого вызова, поэтому функции либо определяют выше места использования, либо объявляют прототипом. Параметры — локальные переменные, получающие копии аргументов. Хорошая практика — прототипы в .h, определения в .c, и одна задача на функцию.

Проверьте себя
1. Зачем нужен прототип функции в C?
AЧтобы функция работала быстрее
BЧтобы компилятор знал контракт функции (типы параметров и возврата) до её первого вызова
CЧтобы автоматически документировать код
DПрототипы в C не нужны
2. Что означает int f(void) в отличие от int f()?
AНичего, это одно и то же
Bf(void) явно говорит, что функция не принимает параметров, а f() в старом C означает «параметры не проверяются»
Cf(void) возвращает void
Df(void) — это прототип, а f() — определение