Управляющие конструкции языка Си. Представление программ в виде функций. Работа с памятью. Структуры
Представление программы в виде функций
Прототипы функций
Перед использованием или реализацией функции необходимо описать ее прототип. Прототип функции сообщает информацию об имени функции, типе возвращаемого значения, количестве и типах ее аргументов. Пример:
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
Здесь первая и третья строка напечатаны компьютером, вторая введена человеком.