Как дать имя алгоритму, вызывать его сколько угодно раз и собирать программу из небольших проверяемых частей
В практических работах раздела 6 одни и те же десять строк — «запросить число, выбросить остаток строки, проверить EOF и диапазон» — повторялись в каждой программе, а в одной программе по несколько раз. Повторённый код плох тремя вещами: его дольше читать, ошибку надо исправлять во всех копиях, а копии со временем расходятся.
Функция — именованный фрагмент программы с параметрами и результатом. Её пишут один раз, а вызывают сколько угодно. В блок-схеме вызов функции — символ «предопределённый процесс» (раздел 2, пункт 04). Вы уже вызывали функции: printf, scanf, sqrt. Теперь — свои.
тип_результата имя(тип параметр1, тип параметр2, …)
{
тело
return значение;
}
/* НОД функцией: определение до main, вызов из main */
#include <stdio.h>
int gcd(int a, int b)
{
while (b != 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
int main(void)
{
printf("%d\n", gcd(48, 18)); /* 6 */
int x = 84, y = 36;
int g = gcd(x, y); /* 12 */
printf("%d/%d = %d/%d\n", x, y, x / g, y / g); /* 84/36 = 7/3 */
printf("%d\n", gcd(gcd(12, 18), 8)); /* НОД трёх чисел: 2 */
return 0;
}
int gcd(int a, int b) — заголовок: функция возвращает int и принимает два int. a и b — параметры: переменные функции, которые получают значения при вызове.gcd(48, 18) — вызов; 48 и 18 — аргументы. Аргументом может быть любое выражение подходящего типа, в том числе вызов другой функции.return a; — завершить функцию и вернуть значение. Вызов gcd(…) в выражении заменяется этим значением.void; return; без значения в ней можно не писать. Функция без параметров — (void) в скобках, как у main./* Процедура: печать линии из n символов c */
void print_line(int n, char c)
{
for (int i = 0; i < n; i++) {
putchar(c); /* вывести один символ */
}
putchar('\n');
}
/* print_line(10, '-'); -> ---------- */
В Pascal функцию без результата называют процедурой. В C это та же функция, только с типом void.
Компилятор читает файл сверху вниз. Встретив вызов, он должен уже знать заголовок функции — чтобы проверить число и типы аргументов. Если функция определена ниже вызова, её заранее объявляют прототипом — заголовком с точкой с запятой:
#include <stdio.h>
int gcd(int a, int b); /* прототип: объявление без тела */
int main(void)
{
printf("%d\n", gcd(48, 18));
return 0;
}
int gcd(int a, int b) /* определение — ниже */
{
…
}
Так main стоит в начале файла, и программу удобно читать сверху: сначала что делается, потом как. Заголовочный файл <stdio.h> — это и есть набор прототипов printf, scanf и других (раздел 1, пункт 06).
| Ошибка | Сообщение gcc |
|---|---|
| вызов до объявления | error: implicit declaration of function 'gcd' (с gcc 14 — ошибка, раньше — предупреждение) |
| аргументов меньше, чем параметров | error: too few arguments to function 'gcd' |
| аргументов больше | error: too many arguments to function 'gcd' |
| прототип и определение не совпадают | error: conflicting types for 'gcd' |
| прототип есть, определения нет | ошибка компоновщика: undefined reference to 'gcd' |
При вызове значение каждого аргумента копируется в параметр. Параметр — отдельная переменная функции; изменение параметра не меняет аргумент. В gcd параметры a и b меняются в цикле, а x и y в main остаются 84 и 36 — поэтому их можно вывести после вызова.
Обратная сторона: функция не может изменить переменную вызывающего. Попытка обменять значения двух переменных функцией:
/* Обмен не работает: функция меняет свои копии */
#include <stdio.h>
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
printf("inside: a = %d, b = %d\n", a, b);
}
int main(void)
{
int x = 1, y = 2;
swap(x, y);
printf("outside: x = %d, y = %d\n", x, y);
return 0;
}
inside: a = 2, b = 1
outside: x = 1, y = 2
Чтобы функция могла изменить переменную вызывающего, ей передают не значение, а адрес переменной — так работает scanf(…, &n). Это указатели, раздел 10; там swap будет исправлен. До тех пор функция возвращает один результат через return.
return и функция-условиеreturn может стоять в нескольких местах: функция заканчивается на первом выполненном. Так удобно писать функции-проверки, которые возвращают bool:
#include <stdbool.h>
bool is_prime(int n)
{
if (n < 2) {
return false;
}
for (int d = 2; d <= n / d; d++) {
if (n % d == 0) {
return false; /* нашли делитель — дальше искать незачем */
}
}
return true;
}
bool is_leap(int year)
{
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
Вызов читается как фраза: if (is_prime(n)), days = is_leap(year) ? 29 : 28;. Цикл поиска делителя из раздела 6 стал короче: return из середины цикла заменяет и break, и проверку «почему цикл закончился».
return. Если функция с типом int может дойти до закрывающей скобки без return, вызывающий получит мусор. gcc предупреждает: control reaches end of non-void function [-Wreturn-type]. Типичный случай — if (…) return 1; else if (…) return 0; без последнего else.Переменные, объявленные внутри функции, и её параметры — локальные: они видны только в этой функции и существуют, пока функция выполняется. У main и у gcd могут быть переменные с одинаковыми именами — это разные переменные.
Где они хранятся: при каждом вызове в памяти выделяется кадр — место под параметры и локальные переменные функции и адрес, куда вернуться. Кадры складываются в стек вызовов: вызов кладёт кадр сверху, return его снимает. Для printf("%d", gcd(48, 18)):
при вызове gcd внутри gcd после return
┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ │ │ gcd: a, b, r │ │ │
│ main: x, y │ → │ main: x, y │ → │ main: x, y │
└──────────────┘ └──────────────────────┘ └──────────────┘
Поэтому значения локальных переменных между вызовами не сохраняются: следующий вызов получает новый кадр. Отладчик (раздел 1, пункт 08) показывает стек командой backtrace (bt): какие функции вызваны и из каких строк. Подробно о времени жизни, static и глобальных переменных — раздел 15.
Повтор запроса до верного ввода из раздела 6 — первая функция, которая пригодится во всех следующих программах:
/* Прочитать целое из [min; max]; при неверном вводе — повторить запрос.
При конце ввода — сообщение и завершение программы с кодом 1. */
int read_int(const char *prompt, int min, int max)
{
while (1) {
printf("%s", prompt);
int value = 0;
int read = scanf("%d", &value);
if (read == EOF) {
printf("\nError: no input\n");
exit(1); /* из <stdlib.h>: завершить программу */
}
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
}
if (read == 1 && value >= min && value <= max) {
return value;
}
printf("Enter an integer %d..%d\n", min, max);
}
}
const char *prompt — параметр-строка: текст подсказки, например "Year: ". Что значат * и const, разбирается в разделах 10 и 11; сейчас достаточно, что так в функцию передаётся строка в кавычках.exit(1) завершает всю программу с кодом 1 из любого места — не только из main.int year = read_int("Year: ", 1, 9999);Задача: вывести простые числа отрезка [a; b] и их количество. Разбиваем на части, каждая — функция с понятным именем:
/* Простые числа отрезка — программа из функций */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
int read_int(const char *prompt, int min, int max);
bool is_prime(int n);
int print_primes(int a, int b);
int main(void)
{
int a = read_int("a: ", 1, 100000);
int b = read_int("b: ", a, 100000); /* граница зависит от a */
int count = print_primes(a, b);
printf("total %d\n", count);
return 0;
}
/* Вывести простые из [a; b] в строку, вернуть их количество */
int print_primes(int a, int b)
{
int count = 0;
for (int i = a; i <= b; i++) {
if (is_prime(i)) {
printf("%d ", i);
count++;
}
}
printf("\n");
return count;
}
/* is_prime и read_int — как в пунктах 05 и 07 */
a: 1
b: 20
2 3 5 7 11 13 17 19
total 8
main теперь читается как план решения в три строки. Каждую функцию можно проверить отдельно: is_prime — на 1, 2, 9, 97; read_int — на буквах и границах.
| Правило курса | Почему |
|---|---|
Одна функция — одна задача, и её можно назвать одним глаголом: read_int, is_prime, print_primes | если в названии нужно «и» — это две функции |
Функция, которая вычисляет, не выводит; выводит отдельная функция или main | вычисление можно использовать там, где выводить не нужно, и проверить без экрана |
| Функция помещается на экран — до 30–40 строк | длинную функцию нельзя проверить целиком глазами |
Данные функция получает через параметры, результат отдаёт через return; глобальных переменных нет | по заголовку видно всё, от чего зависит функция |
Прототипы — перед main, определения — после | main читается первым, как оглавление |
void — нет результата или нет параметровreturn завершает функцию; в функции с результатом он есть на каждом путиswap; раздела 15 — область видимости, static, указатель на функцию; раздела 16 — рекурсия; раздела 18 — функции в отдельных файлах.