Управляющие конструкции языка Си. Представление программ в виде функций. Работа с памятью. Структуры
Представление программы в виде функций
Прототипы функций
Перед использованием или реализацией функции необходимо описать ее прототип. Прототип функции сообщает информацию об имени функции, типе возвращаемого значения, количестве и типах ее аргументов. Пример:
int gcd(int x, int y);
Описан прототип функции gcd, возвращающей целое значение, с двумя целыми аргументами. Имена аргументов x и y здесь являются лишь комментариями, не несущими никакой информации для компилятора. Их можно опускать, например, описание
int gcd(int, int);
является вполне допустимым.
Описания прототипов функций обычно выносятся в заголовочные файлы, см. раздел 3.1. Для коротких программ, которые помещаются в одном файле, описания прототипов располагают в начале программы. Рассмотрим пример такой короткой программы.
Пример: вычисление наибольшего общего делителя
Программа вводит с клавиатуры терминала два целых числа, затем вычисляет и печатает их наибольший общий делитель. Непосредственно вычисление наибольшего общего делителя реализовано в виде отдельной функции
int gcd(int x, int y);
( gcd - от слов greatest common divisor ). Основная функция main лишь вводит исходные данные, вызывает функцию gcd и печатает ответ. Описание прототипа функции gcd располагается в начале текста программы, затем следует функция main и в конце - реализация функции gcd. Приведем полный текст программы:
#include <stdio.h> // Описания стандартного ввода-вывода
int gcd(int x, int y); // Описание прототипа функции
int main() {
    int x, y, d;
    printf("Введите два числа:\n");
    scanf("%d%d", &x, &y);
    d = gcd(x, y);
    printf("НОД = %d\n", d);
    return 0;
}
int gcd(int x, int y) { // Реализация функции gcd
    while (y != 0) {
        // Инвариант: НОД(x, y) не меняется
        int r = x % y;  // Заменяем пару (x, y) на
        x = y;          // пару (y, r), где r --
        y = r;          // остаток от деления x на y
    }
    // Утверждение: y == 0
    return x;   // НОД(x, 0) = x
}Стоит отметить, что реализация функции gcd располагается в конце текста программы. Можно было бы расположить реализацию функции в начале текста и при этом сэкономить на описании прототипа. Это, однако, дурной стиль! Лучше всегда, не задумываясь, описывать прототипы всех функций в начале текста, ведь функции могут вызывать друг друга, и правильно упорядочить их (чтобы вызываемая функция была реализована раньше вызывающей) во многих случаях невозможно. К тому же предпочтительнее, чтобы основная функция main, с которой начинается выполнение программы, была бы реализована раньше функций, которые из нее вызываются. Это соответствует технологии "сверху вниз" разработки программы: основная задача решается сразу на первом шаге путем сведения ее к одной или нескольким вспомогательным задачам, которые решаются на следующих шагах.
Передача параметров функциям
В языке Си функциям передаются значения фактических параметров. При вызове функции значения параметров копируются в аппаратный стек, см. раздел 2.3. Следует четко понимать, что изменение формальных параметров в теле функции не приводит к изменению переменных вызывающей программы, передаваемых функции при ее вызове, - ведь функция работает не с самими этими переменными, а с копиями их значений! Рассмотрим, например, следующий фрагмент программы:
void f(int x);  // Описание прототипа функции
int main() {
    . . .
    int x = 5;
    f(x);
    // Значение x по-прежнему равно 5
    . . .
}
void f(int x) {
    . . .
    x = 0;  // Изменение формального параметра
    . . .   // не приводит к изменению фактического
            // параметра в вызывающей программе
}Здесь в функции main вызывается функция f, которой передается значение переменной x, равное пяти. Несмотря на то, что в теле функции f формальному параметру x присваивается значение 0, значение переменной x в функции main не меняется.
Если необходимо, чтобы функция могла изменить значения переменных вызывающей программы, надо передавать ей указатели на эти переменные. Тогда функция может записать любую информацию по переданным адресам. В Си таким образом реализуются выходные и входно-выходные параметры функций. Подробно этот прием уже рассматривался в разделе 3.5.4, где был дан короткий обзор функций printf и scanf из стандартной библиотеки ввода-вывода языка Си. Напомним, что функции ввода scanf надо передавать адреса вводимых переменных, а не их значения.
Пример: расширенный алгоритм Евклида
Вернемся к примеру с расширенным алгоритмом Евклида, подробно рассмотренному в разделе 1.5.2. Напомним, что наибольший общий делитель двух целых чисел выражается в виде их линейной комбинации с целыми коэффициентами. Пусть x и y - два целых числа, хотя бы одно из которых не равно нулю. Тогда их наибольший общий делитель d = НОД(x,y) выражается в виде
d = ux+vy,
где u и v - некоторые целые числа. Алгоритм вычисления чисел d, u, v по заданным x и y называется расширенным алгоритмом Евклида. Мы уже выписывали его на псевдокоде, используя схему построения цикла с помощью инварианта.
Оформим расширенный алгоритм Евклида в виде функции на Си. Назовем ее extGCD (от англ. Extended Greatest Common Divizor ). У этой функции два входных аргумента x, y и три выходных аргумента d, u, v. В случае выходных аргументов надо передавать функции указатели на переменные. Итак, функция имеет следующий прототип:
void extGCD(int x, int y, int *d, int *u, int *v);
При вызове функция вычисляет наибольший общий делитель от двух переданных целых значений x и y и коэффициенты его представления через x и y. Ответ записывается по переданным адресам d, u, v.
Приведем полный текст программы. Функция main вводит исходные данные (числа x и y ), вызывает функцию extGCD и печатает ответ. Функция extGCD использует схему построения цикла с помощью инварианта для реализации расширенного алгоритма Евклида.
#include <stdio.h> // Описания стандартного ввода-вывода
// Прототип функции extGCD (расш. алгоритм Евклида)
void extGCD(int x, int y, int *d, int *u, int *v);
int main() {
    int x, y, d, u, v;
    printf("Введите два числа:\n");
    scanf("%d%d", &x, &y);
    if (x == 0 && y == 0) {
        printf("Должно быть хотя бы одно ненулевое.\n");
        return 1; // Вернуть код некорректного завершения
    }
    // Вызываем раширенный алгоритм Евклида
    extGCD(x, y, &d, &u, &v);
    // Печатаем ответ
    printf("НОД = %d, u = %d, v = %d\n", d, u, v);
    return 0;   // Вернуть код успешного завершения
}
void extGCD(int x, int y, int *d, int *u, int *v) {
    int a, b, q, r, u1, v1, u2, v2;
    int t; // вспомогательная переменная
    // инициализация
    a = x; b = y;
    u1 = 1; v1 = 0;
    u2 = 0; v2 = 1;
    // утверждение: НОД(a, b) == НОД(x, y)  &&
    //              a == u1 * x + v1 * y    &&
    //              b == u2 * x + v2 * y;
    while (b != 0) {
        // инвариант: НОД(a, b) == НОД(x, y)  &&
        //            a == u1 * x + v1 * y    &&
        //            b == u2 * x + v2 * y;
        q = a / b; // целая часть частного a / b
        r = a % b; // остаток от деления a на b
        a = b; b = r; // заменяем пару (a, b) на (b, r)
        // Вычисляем новые значения переменных u1, u2
        t = u2;         // запоминаем старое значение u2
        u2 = u1 - q * u2; // вычисляем новое значение u2
        u1 = t;           // новое u1 := старое u2
        // Аналогично вычисляем новые значения v1, v2
        t = v2;
        v2 = v1 - q * v2;
        v1 = t;
    }
    // утверждение: b == 0                 &&
    //              НОД(a, b) == НОД(m, n) &&
    //              a == u1 * m + v1 * n;
    // Выдаем ответ
    *d = a;
    *u = u1; *v = v1;
}Пример работы программы:
Введите два числа: 187 51 НОД = 17, u = -1, v = 4
Здесь первая и третья строка напечатаны компьютером, вторая введена человеком.
 
                             
