Опубликован: 16.09.2005 | Уровень: для всех | Доступ: платный | ВУЗ: Московский государственный университет имени М.В.Ломоносова
Лекция 9:

Управляющие конструкции языка Си. Представление программ в виде функций. Работа с памятью. Структуры

Представление программы в виде функций

Прототипы функций

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

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

Здесь первая и третья строка напечатаны компьютером, вторая введена человеком.

Кирилл Юлаев
Кирилл Юлаев
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Денис Шестериков
Денис Шестериков
Россия
Сергей Мамойленко
Сергей Мамойленко
Россия, Московский государственный институт стали и сплавов (Технологический университет), 2000