
Основы программирования. Борисенко
.pdf3.6.2. Пример: вычисление наибольшего общего делителя |
161 |
Описания прототипов функций обычно выносятся в заголовочные файлы, см. раздел 3.1. Д л я коротких программ, которые помещаются в одном файле, описания прототипов располагают в начале програм¬ мы. Рассмотрим пример такой короткой программы.
3.6.2.Пример: вычисление наибольшего общего делителя
Программа вводит с клавиатуры терминала два целых числа, за¬ тем вычисляет и печатает их наибольший общий делитель. Непо средственно вычисление наибольшего общего делителя реализовано в виде отдельной функции
int gcd(int x, i n t y);
(gcd — от слов greatest common divizor). Основная функция main лишь вводит исходные данные, вызывает функцию gcd и печатает ответ. Описание прототипа функции gcd располагается в начале тек¬ ста программы, затем следует функция main и конце — реализация функции gcd. Приведем полный текст программы:
#include <stdio.h> // Описания стандартного ввода-вывода
int |
gcd(int |
x, |
i n t y); // Описание |
прототипа |
функции |
|
int |
main() |
{ |
d; |
|
|
|
|
int x, |
y, |
числа:^"); |
|
|
|
|
printf("Введите два |
|
|
|||
|
scanf('7„dy„d", &x, &y); |
|
|
|||
|
d = gcd(x, y); |
|
|
|
||
|
printf("№^ = °/„d\n", d); |
|
|
|||
} |
return |
0; |
|
|
|
|
|
|
|
|
|
|
|
int |
gcd(int |
x, |
i n t y) { // Реализация функции gcd |
|||
|
while (y != 0) { |
|
меняется |
|
||
|
// Инвариант: Н О Д ^ , y) не |
|
||||
|
int |
r |
= x % y; |
// Заменяем |
пару (x, y) на |
|
|
x = y; |
|
// пару (y, r ) , где |
r |
162 |
|
3.6. Представление программы в виде функций |
|
y = r; |
// остаток |
от деления x на y |
|
} |
|
|
|
// Утверждение: y == 0 |
|
||
return |
x; |
// Н О Д ^ , 0) = x |
|
} |
|
|
|
Стоит отметить, что реализация функции gcd располагается в |
|||
конце текста |
программы. М о ж н о было |
бы расположить реализацию |
функции в начале текста и при этом сэкономить на описании про тотипа. Это, однако, дурной стиль! Лучше всегда, не задумываясь, описывать прототипы всех функций в начале текста, ведь функции могут вызывать друг друга, и правильно упорядочить их (чтобы вы¬ зываемая функция была реализована раньше вызывающей) во мно¬ гих случаях невозможно. К тому ж е предпочтительнее, чтобы основ¬ ная функция main, с которой начинается выполнение программы, была бы реализована раньше функций, которые из нее вызываются. Это соответствует технологии «сверху вниз» разработки программы: основная задача решается сразу на первом шаге путем сведения ее к одной или нескольким вспомогательным задачам, которые решаются на следующих шагах.
3.6.3.Передача параметров функциям
В языке Си функциям передаются значения фактических пара метров. При вызове функции значения параметров копируются в аппаратный стек, см. раздел 2.3. Следует четко понимать, что изме¬ нение формальных параметров в теле функции не приводит к изме¬ нению переменных вызывающей программы, передаваемых функции при ее вызове, — ведь функция работает не с самими этими перемен¬ ными, а с копиями их значений! Рассмотрим, например, следующий фрагмент программы:
void f ( i n t x); // Описание прототипа функции
int main() {
int x = 5; f(x);
// Значение x по-прежнему равно 5
3.6.4. Пример: расширенный алгоритм Евклида |
163 |
|||
} |
|
|
|
|
void |
f ( i n t |
x) { |
|
|
|
x = 0; |
// Изменение |
формального параметра |
|
|
. . . |
/ / н е приводит к изменению |
фактического |
|
} |
|
// параметра |
в вызывающей |
программе |
|
|
|
|
|
Здесь в функции main вызывается функция f, |
которой передается |
|||
значение |
переменной x, равное |
пяти. Несмотря |
на то, что в теле |
функции f формальному параметру x присваивается значение 0, зна¬ чение переменной x в функции ma n не меняется.
Если необходимо, чтобы функция могла изменить значения пе¬ ременных вызывающей программы, надо передавать ей указатели на эти переменные. Тогда функция может записать любую информацию
по переданным адресам. В Си таким образом реализуются |
выходные |
и входно-выходные параметры функций. Подробно этот |
прием у ж е |
рассматривался в разделе 3.5.4, где был да н короткий обзор функ ций printf и scanf из стандартной библиотеки ввода-вывода языка Си. Напомним, что функции ввода scanf надо передавать адреса вводи¬ мых переменных, а не их значения.
3.6.4.Пример: расширенный алгоритм Евклида
Вернемся к примеру с расширенным алгоритмом Евклида, по¬ дробно рассмотренному в разделе 1.5.2. Напомним, что наибольший общий делитель двух целых чисел выражается в виде их линейной комбинации с целыми коэффициентами. Пусть x и у — два целых числа, хотя бы одно из которых не равно нулю. Тогда их наибольший общий делитель d = Н О Д ( х , у ) выражается в виде
d = ux + vy,
где u и v — некоторые целые числа. Алгоритм вычисления чисел d, u, v по заданным x и y называется расширенным алгоритмом Евклида. М ы у ж е выписывали его на псевдокоде, используя схему построения цикла с помощью инварианта.
164 3.6. Представление программы в виде функций
Оформим расширенный алгоритм Евклида в виде функции на Си. Назовем ее e x t G C D (от англ. Extended Greatest Common Divizor). У
этой функции два входных аргумента x, y и три выходных |
аргумен¬ |
та d, u, v. В случае выходных аргументов надо передавать |
функции |
указатели на переменные. Итак, функция имеет следующий прото тип:
void extGCD(int x, i n t y, i n t *d, i n t *u, i n t *v);
При вызове функция вычисляет наибольший общий делитель от двух переданных целых значений x и y и коэффициенты его представле¬ ния через x и y. Ответ записывается по переданным адресам d, u, v.
Приведем полный текст программы. Ф у н к ц и я main вводит исход¬
ные данные (числа x и |
y), вызывает функцию |
e x t G C D и |
печатает |
|
ответ. Ф у н к ц и я |
e x t G C D |
использует схему построения цикла с помо¬ |
||
щью инварианта |
для реализации расширенного |
алгоритма |
Евклида. |
#include <stdio.h> // Описания стандартного ввода-вывода
// Прототип функции extGCD (расш. алгоритм Евклида) void extGCD(int x, i n t y, i n t *d, i n t *u, i n t *v);
int main() {
int x, y, d, u, v; printf("Введите два числа:\n");
scani('7„dy„d", |
&x, |
&y); |
|
i f (x == 0 |
&& |
y == |
0) { |
р ^ ^ Ю ' Д о л ж н о |
быть хотя бы одно ненулевое.^"); |
||
return |
1; |
// Вернуть код некорректного завершения |
|
} |
|
|
|
//Вызываем раширенный алгоритм Евклида extGCD(x, y, &d, &u, &v);
//Печатаем ответ
printf("НОД = °/„d, u = /d, v = /d\n", d, u, v);
return 0; // Вернуть код успешного завершения
3.6.4. Пример: расширенный алгоритм Евклида |
165 |
|||
} |
|
|
|
|
void extGCD(int |
x, i n t y, i n t *d, i n t *u, i n t *v) |
{ |
||
int |
a, b, |
q, |
r, u i , v i , u2, v2; |
|
int |
t ; // |
вспомогательная переменная |
|
// инициализация a = x; b = y;
u1 = 1; v1 = 0;
u2 = 0; v2 = 1;
// утверждение: |
Н О Д ^ , |
b) == |
+ |
Н О Д ^ , y) |
&& |
||||
// |
a |
== |
u1 |
* x |
v1 |
* |
y |
&& |
|
// |
b |
== |
u2 |
* x |
+ |
v2 |
* |
y; |
|
while |
(b |
!= 0) |
{ |
|
b) == |
|
Н О Д ^ , |
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 |
b) == |
|
&& |
|
|
||
// |
Н О Д ^ , |
НОД(ш, n) && |
|
|
|||
// |
a |
== u1 * m |
+ v1 |
* n; |
|
|
166 |
3.7. Работа с памятью |
// Выдаем ответ *d = a;
*u = u1; *v = v1;
}
Пример работы программы:
Введите два числа: 187 51
НОД = 17, u = -1, v = 4
Здесь первая и третья строка напечатаны компьютером, вторая вве дена человеком.
3.7.Работа с памятью
Втрадиционных языках программирования, таких как Си, Фор
тран, Паскаль, существуют три вида памяти: статическая, стековая и динамическая. Конечно, с физической точки зрения никаких раз
личных видов памяти нет: оперативная память — это массив |
байтов, |
к а ж д ы й байт имеет адрес, начиная с нуля. Когда говорится |
о видах |
памяти, имеются в виду способы организации работы с ней, включая выделение и освобождение памяти, а также методы доступа.
3.7.1.Статическая память
Статическая память выделяется еще до начала работы програм¬ мы, на стадии компиляции и сборки. Статические переменные имеют
фиксированный адрес, известный до запуска программы |
и не изме¬ |
няющийся в процессе ее работы. Статические переменные |
создаются |
и инициализируются до входа в функцию main, с которой |
начинается |
выполнение программы. |
|
Существует два типа статических переменных: |
|
глобальные переменные — это переменные, определенные вне функций, в описании которых отсутствует слово static. Обычно описания глобальных переменных, включающие слово extern,
3.7.1. Статическая память |
167 |
выносятся в заголовочные файлы (h-файлы). Слово extern озна¬ чает, что переменная описывается, но не создается в данной точке программы. Определения глобальных переменных, т.е. описания без слова extern, помещаются в файлы реализации (c- файлы или cpp-файлы). Пример: глобальная переменная maxind описывается дважды:
— в h-файле с помощью строки
extern i n t maxind;
это описание сообщает о наличии такой переменной, но не создает эту переменную!
— в cpp-файле с помощью строки
int maxind = 1000;
это |
описание |
создает |
переменную maxind и присваивает |
|
ей |
начальное |
значение |
1000. Заметим, |
что стандарт язы¬ |
ка |
не требует |
обязательного присвоения |
начальных значе¬ |
ний глобальным переменным, но, тем не менее, это луч¬ ше делать всегда, иначе в переменной будет содержаться непредсказуемое значение (мусор, ка к говорят программи¬ сты). Инициализация всех глобальных переменных при их определении — это правило хорошего стиля.
Глобальные переменные называются так потому, что они до¬ ступны в любой точке программы во всех ее файлах. Поэтому имена глобальных переменных должны быть достаточно длин¬ ными, чтобы избежать случайного совпадения имен двух раз¬ ных переменных. Например, имена x ил и n дл я глобальной переменной не подходят;
статические переменные — это переменные, в описании которых присутствует слово static. Как правило, статические перемен¬ ные описываются вне функций. Такие статические переменные во всем подобны глобальным, с одним исключением: область видимости статической переменной ограничена одним файлом, внутри которого она определена, — и, более того, ее можно ис¬ пользовать только после ее описания, т.е. ниже по тексту. По
168 |
3 7 Работа с памятью |
этой причине описания статических переменных обычно выно¬ сятся в начало файла. В отличие от глобальных переменных, статические переменные никогда не описываются в h - файлах (модификаторы extern и static конфликтуют между собой).
Совет: используйте статические переменные, если нужно, что¬ бы они были доступны только для функций, описанных внутри
одного |
и того |
же файла. |
|
По возможности не применяйте в та¬ |
||||
ких ситуациях |
глобальные переменные, это позволит избежать |
|||||||
конфликтов |
имен при реализации |
больших проектов, состоя¬ |
||||||
щих из сотен |
файлов. |
|
|
|
|
|
||
С т а т и ч е с к ую переменную можно |
описать и внутри ф у н к ц и и , хо |
|||||||
тя |
обычно |
та к никто |
не делает. Переменная |
размещается не в |
||||
стеке, а в статической |
памяти, т.е. ее нельзя |
использовать при |
||||||
рекурсии, |
а ее значение |
сохраняется м е ж д у р а з л и ч н ы м и входа¬ |
||||||
ми в ф у н к ц и ю . Область |
видимости такой переменной ограниче |
|||||||
на |
телом |
ф у н к ц и и , |
в которой она определена. В остальном она |
|||||
подобна |
статической |
или глобальной переменной. |
||||||
|
З а м е т и м , что ключевое слово |
static в языке Си используется |
||||||
для двух |
р а з л и ч н ы х |
целей: |
|
|
—ка к у к а з а н и е типа памяти: переменная располагается в ста¬ тической памяти, а не в стеке;
—ка к способ ограничить область видимости переменной рам¬
ками одного ф а й л а ( в случае описания переменной вне ф у н к ц и и ) .
Слово static может присутствовать и в заголовке функции. При этом оно используется только дл я того, чтобы ограничить об¬ ласть видимости имени фунции рамками одного файла. При¬ мер:
s t a t ic |
i n t gcd(int |
x, i n t y); // Прототип ф-ции |
static |
i n t gcd(int |
x, i n t y) { // Реализация |
} |
|
|
Совет: используйте модификатор static в заголовке функции, если известно, что функция будет вызываться лишь внутри од¬ ного файла. Слово static должно присутствовать как в описании
3.7.2. Стековая, или локальная, память |
169 |
прототипа функции, так и в заголовке функции при ее реали¬ зации.
3.7.2.Стековая, или локальная, память
Локальные, или стековые, переменные — это переменные, опи¬ санные внутри функции. Память для таких переменных выделяется в аппаратном стеке, см. раздел 2.3.2. Память выделяется в момент входа в функцию или блок и освобождается в момент выхода из функции или блока. При этом захват и освобождение памяти про¬ исходят практически мгновенно, т.к. компьютер только изменяет ре¬ гистр, содержащий адрес вершины стека.
Локальные переменные можно использовать при рекурсии, по¬ скольку при повторном входе в функцию в стеке создается новый набор локальных переменных, а предыдущий набор не разрушается. По этой ж е причине локальные переменные безопасны при исполь¬
зовании нитей в параллельном программировании |
(см. раздел 2.6.2). |
Программисты называют такое свойство функции |
реентерабельно |
стью, от англ. re-enter able — возможность повторного входа. Это очень важное качество с точки зрения надежности и безопасности программы! Программа, работающая со статическими переменными, этим свойством не обладает, поэтому для защиты статических пе¬ ременных приходится использовать механизмы синхронизации (см. 2.6.2), а логика программы резко усложняется. Всегда следует из¬ бегать использования глобальных и статических переменных, если можно обойтись локальными.
Недостатки локальных переменных являются продожением их до¬ стоинств. Локальные переменные создаются при входе в функцию и исчезают после выхода из нее, поэтому их нельзя использовать в ка¬ честве данных, разделяемых между несколькими функциями . К тому же, размер аппаратного стека не бесконечен, стек может в один пре¬ красный момент переполниться (например, при глубокой рекурсии), что приведет к катастрофическому завершению программы. Поэтому локальные переменные не должны иметь большого размера. В част¬ ности, нельзя использовать большие массивы в качестве локальных переменных.
170 |
3.7. Работа с памятью |
3.7.3.Динамическая память, или куча
Помимо статической и стековой памяти, существует еще прак тически неограниченный ресурс памяти, которая называется дина мическая, или куча (heap). Программа может захватывать участки динамической памяти нужного размера. После использования ранее захваченный участок динамической памяти следует освободить.
Под динамическую память отводится пространство виртуальной памяти процесса между статической памятью и стеком. (Механизм виртуальной памяти был рассмотрен в разделе 2.6.) Обычно стек рас полагается в старших адресах виртуальной памяти и растет в сторо ну уменьшения адресов (см. раздел 2.3). Программа и константные данные размещаются в младших адресах, выше располагаются ста¬
тические |
переменные. Пространство |
выше статических переменных |
||
и ниже стека занимает динамическая |
память: |
|||
адрес |
содержимое |
памяти |
||
0 |
код программы |
и данные, |
||
4 |
защищенные |
от изменения |
||
8 |
|
|
|
|
|
статические |
переменные |
||
|
программы |
|
|
|
|
динамическая |
|
|
|
|
память |
|
|
|
макс. |
стек | |
|
|
|
адрес |
|
|
|
|
( 2 3 2 - |
4) |
|
|
|
Структура динамической памяти автоматически поддерживается исполняющей системой языка Си или C + + . Динамическая память состоит из захваченных и свободных сегментов, каждому из кото¬ рых предшествует описатель сегмента. При выполнении запроса на захват памяти исполняющая система производит поиск свободного сегмента достаточного размера и захватывает в нем отрезок требуе¬ мой длины. При освобождении сегмента памяти он помечается как свободный, при необходимости несколько подряд идущих свободных сегментов объединяются.