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