Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Posibnyk_C_sum.doc
Скачиваний:
9
Добавлен:
29.08.2019
Размер:
1.63 Mб
Скачать

9 Процедури

Процедура являє собою підпорядковану програму. Вона має такі області застосування:

  • структурування програми. Як правило, велика програма розв’язує декілька підзадач. Якщо кожну з них оформити у вигляді підпрограм, то їх легше буде читати та налагоджувати. Це може бути вигідним і при програмуванні однієї задачі декількома програмістами;

  • уніфікація обчислювального процесу. Часто підзадачі бувають однотипними, наприклад, коли треба розв’язати декілька різних рівнянь одним і тим же методом у різних місцях програми. Звичайно, що з метою недопущення повторення програмного коду таку задачу слід оформити у вигляді підпрограми, до якої звертатися з різними значеннями коефіцієнтів рівняння в різних місцях програми;

  • створення бібліотеки програм для їх повторного використання.

Підпрограма характерна наявністю вхідних (тих, які вона одержує з головної програми) та вихідних (тих, які вона повертає в головну програму) параметрів. Вхідні параметри називаються дійсними на рівні оператора виклику підпрограми у головній програмі та формальними в переліку параметрів на стороні підпрограми. Під формальні параметри пам’ять дублюється і вони локалізовані в підпрограмі; після закінчення виконання підпрограми зайнята ними пам’ять звільняється і може бути використана іншими підпрограмами. Але, це не стосується даних класу пам’яті static, які можна прочитати повторно при наступному звернення до процедури.

В залежності від кількості вихідних параметрів розрізняють два типи підпрограм: процедура-підпрограма і процедура-функція. Функція так же, як підпрограма, може мати багато вхідних параметрів, але, на відміну від неї, видає тільки один результат.

Функцiя. Мова Сi дозволяє використовувати лише функцiю, причому її параметрами не можуть бути масиви. Але це її не збіднює, за рахунок використання вказівників вдається обробляти необмежену кількість будь-яких даних. Дані, передані вказівниками на них, стають глобальними – вони не дублюються в процедурі і займають одну й ту ж область пам’яті, задану в головній програмі.

Головна програма мовою С – теж функція. Вона може бути запущеною на виконання іншою функцією і приймати вхідні параметри, та повертати значення. Якщо це значення дорівнює 0, то воно означає, що програма закінчилася успішно, інакше – одиницю. Зауважимо, що ці ж значення (0 або 1) повертає функція exit() при примусовому завершенні програми.

Є три види роботи з функціями: оголошення, визначення і звернення (використання). Програма мовою С належить до багатофайлових програм, тобто всі або частина її функцій можуть знаходитися в різних файлах, тому функції повиннi бути оголошенi в головнiй або пiдпорядкованих програмах. Незалежно від місця оголошення кожна функція є глобальною, тобто доступною всім. Якщо вся програма разом з підпорядкованими функціями знаходиться в одному й тому ж файлі, то їх спеціально можна й не оголошувати. Тоді визначення, тобто сам текст функції, поміщають за межами головної програми на її початку.

Оголошення процедури-функцiї називають ще прототипом функції, воно має такий вигляд:

ТФ IФ(СТ);

де ТФ – тип функції або тип значення, яке вона повертає;

IФ – iм’я процедури-функцiї, iдентифiкатор;

СТ – список типiв параметрiв.

Визначення функцiї виглядає так:

ТФ IФ(СПТ){ОВЗ; ТілоФ;}

де СПТ – список формальних параметрiв i їх типiв;

ОВЗ – оголошення внутрiшнiх (власних) змiнних функцiї;

ТілоФ – тiло функції, оператори та вирази.

Звернення до процедури-функції відбувається так же, як до стандартних функцій – шляхом вказання у потрібному місці програми такого виразу:

IФ(СД),

де СД – список дійсних параметрів.

Тiло процедури-функцiї може мiстити один або декiлька операторiв return(вираз); Його виконання спричиняє завершення процедури i повернення в програму на те ж місце, з якого функцiя викликалася. Якщо цей оператор має вираз, то його значення присвоюється iменi процедури i є результатом її виконання. Якщо фукція не повертає в головну програму ніяких значень (наприклад, видає на екран малюнок), то вона може й не мати цього оператора. Але це не вважається хорошим тоном програмування, при читанні такої програми не видно чітко де закінчується текст функції. У такому випадку наприкінці функції варто вживати оператор return без виразу (просто return;).

Формальнi параметри функцiї, якщо вони є, локалiзованi в її тiлi, пам’ять пiд них дублюється. Локалiзованими є також внутрiшнi змiннi функцiї, їхні значення втрачаються пiсля виходу з неї, якщо вони не мають клас пам’ятi static. Це особливо важливо для програм, процедури-функції яких утворюють динамічні масиви. Якщо не застосовуються засоби звільнення (наприклад, за допомогою функції free(), розгляненої в розділі 6) такої пам’яті, то вказівник на неї пропадає, тоді використати її повторно буде неможливо. При багаторазовому зверненні до такої функції можна розтранжирити всю оперативну пам’ять.

При звернення до функцiї її параметри необхідно перечисляти в тому порядку, в якому вони оголошені.

Покажемо порядок використання функції на прикладі обчислення значення проходки h (9.1) в метрах за час буріння tb = 4 год [3].

де k = 0,098 – інтенсивність зносу долота, год-1;

v0 = 1,328 – початкова швидкість буріння, м/год.

Означений інтеграл обчислимо за формулою трапецій (9.2).

де n=100 – кількість елементарних відрізків;

h=(b-a)/n – крок інтегрування;

t0=a, ti+1=ti+h.

Аналізуючи формулу (9.2), бачимо, що вона містить 3-разове звернення до функції f(t) з різними значеннями аргумента. Звичайно, що для її обчислення доцільно використати процедуру.

Алгоритм головної програми обчислення проходки показаний на рисунку 9.1, він являє собою цикл типу арифметичної прогресії з накопиченням суми. В його лінійній частині присвоюються значення змінним a=0, b=4, n=100, обчислюється крок інтегрування h=(b-a)/n (блок 2) та початкове значення суми s=(f(a)+f(b))/2 (блок 3). Блок 3 має вигляд прямокутника з подвоєними бiчними сторонами, він символізує звернення до функції f(t).

У блоці 4 відображено заголовок циклу, параметр якого i змінюється від 1 до n-1. В тілі циклу (блок 5) накопичується сума s, цей блок теж має звернення до функції, тому його бічні сторони теж подвоєні.

Наприкінці алгоритму обчислюється остаточне значення s*=h, яке виводиться блоком 6.

Підпорядкований алгоритм лінійного типу показаний на рисунку 9.2, він призначений для обчислення підінтегральної функції і має всього-на-всього один вираз: f(t)=v0 /(1+kt).

Програма, яка розв’зує цю задачу, показана в прикладі 9.1, вона складається з двох частин: головна програма main() і підпрограма f(). Крім відомої вже директиви #include<stdio.h>, вона має ще дві директиви #define, які служать для ініціалізації відповідними константами змінних: v0=1.328 – початкової швидкості буріння та k=0.098 – інтенсивності зносу долота.

Блок головної програми починається з оголошення підпорядкованої функці float f(float). Це оголошення повідомляє компілятор про те, що функція повинна видати результат типу float і при зверненні до неї їй буде надано один вхідний параметр типу float. Під час компіляції програми компілятор використовує ці відомості, він перевіряє їх на відповідність до поданих у визначенні функції та зверненні до неї.

Приклад 9.1 – Використання процедури-функції

#include<stdio.h> /* Головна програма */

#define v0 1.328

#define k 0.098

int main(void)

{ float f(float);

int i, n=100;

float a=0, b=4, t, h, s;

h=(b-a)/n;

s=(f(a)+f(b))/2;

for(i=1, t=a+h; i<n; i++, t+=h)s+=f(t);

printf("Проходка=%6.3f метрів\n",s*=h);

return 0;

}

float f(float t) /* Процедура-функція */

{

/* return 3.0; цей оператор був використаний для перевірки програми */

return v0/(1+k*t);

}

З метою перевірки правильності ця програма спочатку була запущена на виконання з виразом return 3.0; у підпрограмі. Оскільки табличний інтеграл функції f(3.0) дорівнює 12, то й було одержано це правильне число. За другим разом був використаний заданий у постановці задачі підінтегральний вираз, а результат мав такий вигляд:

Проходка = 4.482 метрів

Вхідний параметр підпорядкованої функції змінна t є локальним. Отже, під цю змінну заново відводиться пам’ять при кожному зверненні до підпрограми, а при виході з неї пам’ять звільняється. Можна підрахувати, що в програмі прикладу 9.1 це відбудеться 101 раз: 2 рази перед циклом і 99 – у циклі. Це говорить про суттєве зростання затрат машинного часу, та й пам’яті, на використання функцій, особливо в циклах.

Змінні v0 та k можна було й оголосити, наприклад, так: float v0=1.328, k=0.098. Оскільки вони використовуються лише в підпрограмі, то цілком логічно помістити й це оголошення в її тіло. Але вони тоді подібно до параметра t стають локальними, тому спричинять зайві затрати машинного часу, бо при кожному зверненні до функції прийдеться відводити для них пам’ять. Якщо це оголошення помістити в головну програму, то становище лише погіршиться. Їх прийдеться передавати в підпрограму як параметри, звернення до неї може виглядати, наприклад, так: f(a, v0, k). Через це збільшиться ще й кількість переданих параметрів, які знову ж таки стають локальними. Дещо краще виглядатиме оголошення змінних v0 та k перед функцією main(), тоді вони стають глобальними, тобто доступними підпрограмі.

Застосування адреси в якості параметра функції покажемо на прикладі розв’язування диференціального рівняння, яке описує перехідний процес системи автоматичного регулювання температури крекінгу нафти при одиничному збуренні. Задача Коші має такий вигляд:

a3y'''+a2y''+а1y'+a0y=b0, (9.3)

y=y'=y''=0 при t=0,

де a0=6,4; a1=0,27; a2=0,03; a3=0,001; b0=5,4. Час встановлення технологічного параметра tвст=2 с.

Розв’яжемо рівняння (9.3) методом Ейлера-Коші за формулою

yiп = yi + hf(ti, yi);

yi+1 = yi + h[f(ti+1, yiп) + f(ti, yi)]/2.

Для цього відрізок [0, tвст] розіб’ємо на n=100 елементарних відрізків довжиною h=tвст/n, тоді t0=0, ti+1=ti+h (i=0, n). Результатом обчислень буде функція y=f(t) у вигляді таблиці.

Перетворимо дифрiвняння (9.3) в систему рiвнянь першого порядку, застосуємо таку пiдстановку:

y=y3;

y'=y3'=y2;

y''=y3''=y2'=y1.

Тоді система дифрiвнянь прийме такий вигляд:

y1'=(b0-a0*x3-a1*x2-a2*x1)/a3;

y2'=y1;

y3'=y2,

а початковi умови такий: y1=y2=y3=0 при t=0.

Відповідно до методу Ейлера-Коші при переході в кожну наступну точку враховується кут нахилу дотичної до кривої точного розв’язку (її перша похідна) в двох крайніх точках (тобто в точках ti i ti+1) кожного елементарного відрізка.

Програма, яка реалізовує цей метод показана в прикладі 9.2.

Приклад 9.2 – Використання адреси в якості параметра функції

# include <stdio.h>

# include <bios.h>

float f[3];

float a0=6.4, a1=0.27, a2=0.03, a3=0.001, b0=5.4;

void systema(float *);

void systema(float *yy)

{

f[0]=(b0-a0*(*(yy+2))-a1*(*(yy+1))-a2*(*yy))/a3;

f[1]=*yy;

f[2]=*(yy+1);

return;

}

main()

{

float y[3], z[3], a[3], b[3],c[3], d[3];

float twst=2.0, h, t=0;

int i, j, m=3, n=100;

clrscr(); h=twst/n;

y[0]=y[1]=y[2]=0;

for(i=1;i<=n;i++)

{

for(j=0; j<m; j++)z[j]=y[j];

systema(y);

for(j=0; j<m; j++){a[j]=f[j]; y[j]=z[j]+a[j]*h;}

systema(y);

for(j=0; j<m; j++){y[j]=z[j]+(a[j]+f[j])*h/2;}

printf("i=%3d y=%4.2f t=%4.2f\n", i , y[2], t+=h);

if(!(i % 20)){bioskey(0); printf("\n");}

}

}

Тут масив f[] – глобальний, він доступний обом програмам: головній підпорядкованій. Головна програма містить два звернення до функції systema() із вказівником y, який оголошено як масив y[3]. Нагадаємо, що ім’я масиву і ім’я вказівника на нього можуть бути взаємозамінними. Це ж стосується і параметрів функції, ім’я масиву y при зверненні до функції systema(y) в головній програмі прикладу 9.2 вживається як вказівник, а нижче у варіанті функції systema() вказівник yy служить як ім’я масиву:

void systema(float *yy)

{

f[0]=(b0-a0*yy[2]-a1*yy[1]-a2*yy[0])/a3;

f[1]=yy[0];

f[2]=yy[1];

return;

}

Функція може й повертати адресу, це показано в прикладі 9.3.

Приклад 9.3 – Повернення функцією адреси

# include <stdio.h>

main()

{

int *p=5555, *f(int *);

clrscr();

printf("rez=%d\n", f(p));

getch();

}

int *f(int *c){return c;}

rez=5555

Макрос із параметрами вигідно використовувати замість функції в тих випадках, коли вона обчислює лише один вираз.

Але він має й свої недоліки, з яких деякі демонструє програма прикладу 9.4. Нижче під нею показані два результати її виконання.

Приклад 9.4 – Макрос із параметрами

#include<stdio.h> /* Макрос із параметрами */

#define mmm(x) x*x

int main(void)

{int z=2, k=3;

printf("\n");

printf("mmm=%d mzk=%d mpz=%d\n", mmm(z), mmm(z+k));

getch();

return(0);

}

mmm=4 mzk=11

Ця програма містить макрос mmm, дві змінні цілого типу: z=2 і k=3 та два звернення до макроса: mmm(z), mmm(z+k). Перший результат зрозумілий – два помножити на два завжди дорівнювало чотири. Ставлення до другого результату може виявитися неоднозначним. Якщо ми хотіли спочатку помножити, а потім додати числа, то він вірний: 2+3*2+3=11. Але, якщо ми надіялися на число 25, оскільки (2+3)*(2+3)=25, то макрос слід було написати так: mmm(x) (x)*(x).

Звернення до цього макроса з параметром ++z, тобто при mmm(++z), у різних програмах може дати як очікувані, так непередбачені результати, тому подібний параметр використовувати не варто.

Рекурсія функції являє собою звернення функції до самої себе. Вона може застосовуватися в тих випадках, де є якась рекурентна формула. Це, наприклад, відома задача про Ханойську башню, обчислення факторіалу числа, знаходження популярного серед математиків ряду чисел Фібоначчі та ін.

Покажемо рекурсію функції на прикладі задачі розділу 6 про накопичення суми членів безмежного ряду. Її реалізовує програма прикладу 9.5, яка складається з головної програми і функції rekurs(). Головна програма має лінійний тип, вона містить оголошення та початкові значення змінних i=1, a=1 та s=0, а також звернення до підпрограми-функції з цими ж параметрами.

Оголошення та визначення функції rekurs() знаходиться на початку програмного блока. Вона має три вище вже перечислені параметри, які й змінюються в ході її виконання. Блок тіла функції починається з оголошення та ініціалізації двох локальних змінних: x=1.2, eps=1e-5. Всередині блока міститься звернення до самої себе, власне рекурсія з нарощеними на один крок параметрами. Рекурсивний процес безмежний, тому з метою попередження зациклення застосовується оператор if, який містить умову припинення рекурсії і видачі результату. Закінчується функція оператором return.

Крім цих складників, у підпрограму спеціально внесено ще два оператори виклику функції printf() (перед та після оператора if) для видачі проміжних значень параметрів. Вони виконують допоміжну роботу і служать лише для демонстрації ходу рекурсії.

Під текстом програми показано результати її виконання. Аналізуючи їх, зробимо деякі висновки. Рекурсивний процес є циклічним, отже, його можна застосовувати замість оператора циклу. Проте, тут є не один, а два цикли: один лобовий або носовий, а другий прикінцевий або хвостовий – маємо ніби прямий і зворотній ходи. Лобовий цикл зрозумілий, він зумовлений зміною параметрів процедури. Однак, після звернення до функції в її тілі немає ніяких операторів, які б змінювали параметри. Тим не менше, функція виводу, встановлена після цього звернення, видає не однакові, а змінені значення, причому вони йдуть у зворотньому порядку до одержаних у лобовому циклі. Це говорить про те, що маємо ще й хвостовий цикл.

Приклад 9.5 – Рекурсія функцій

#include<stdio.h> /* Рекурсія функцій */

#include<math.h>

void rekurs(float, float, int);

void rekurs(float a, float s, int i)

{

float x=1.2, eps=1e-5;

printf("Носова рекурсія, s=%9.6f a=%9.6f i=%2d\n", s, a, i);

if(fabs(a)>eps)rekurs(a*-x/i, s+a, i+1);

else printf("Результат= %9.6f\n", s);

printf("Хвостова рекурсія, s=%9.6f a=%9.6f i=%2d\n", s, a, i);

return;

}

int main(void)

{int i=1;

float a=1, s=0;

rekurs(a, s, i);

return(0);

}

Носова рекурсія, s= 0.000000 a= 1.000000 i= 1

Носова рекурсія, s= 1.000000 a=-1.200000 i= 2

Носова рекурсія, s=-0.200000 a= 0.720000 i= 3

Носова рекурсія, s= 0.520000 a=-0.288000 i= 4

Носова рекурсія, s= 0.232000 a= 0.086400 i= 5

Носова рекурсія, s= 0.318400 a=-0.020736 i= 6

Носова рекурсія, s= 0.297664 a= 0.004147 i= 7

Носова рекурсія, s= 0.301811 a=-0.000711 i= 8

Носова рекурсія, s= 0.301100 a= 0.000107 i= 9

Носова рекурсія, s= 0.301207 a=-0.000014 i=10

Носова рекурсія, s= 0.301193 a= 0.000002 i=11

Результат= 0.301193

Хвостова рекурсія, s= 0.301193 a= 0.000002 i=11

Хвостова рекурсія, s= 0.301207 a=-0.000014 i=10

Хвостова рекурсія, s= 0.301100 a= 0.000107 i= 9

Хвостова рекурсія, s= 0.301811 a=-0.000711 i= 8

Хвостова рекурсія, s= 0.297664 a= 0.004147 i= 7

Хвостова рекурсія, s= 0.318400 a=-0.020736 i= 6

Хвостова рекурсія, s= 0.232000 a= 0.086400 i= 5

Хвостова рекурсія, s= 0.520000 a=-0.288000 i= 4

Хвостова рекурсія, s=-0.200000 a= 0.720000 i= 3

Хвостова рекурсія, s= 1.000000 a=-1.200000 i= 2

Хвостова рекурсія, s= 0.000000 a= 1.000000 i= 1

Таке могло стастися тільки в одному випадку: при кожному зверненні до процедури значення параметрів були запам’ятовані і створився такий собі ланцюжок цих значень, а при кожному виході з процедури вони були прочитані й видані. Тобто кожний вихід з процедури відбувався в саму себе, де знаходилися залишені нею значення даних, і лише останній – у головну програму.

Порівняно з циклом (наприклад, із циклом прикладу 6.3) рекурсія функцій є вкрай неефективною. По-перше, як бачимо, вона транжирить пам’ять. Та не тільки за рахунок зберігання поточних значень формальних параметрів, але й через багаторазове копіювання всіх її локальних змінних, адже при кожному зверненні до процедури вони заново оголошуються. У програмі прикладу 9.5 це змінні x=1.2 і eps=1e-5. По-друге, за ступенем складності програми та характеру її виконання будь-яка функція набагато перевершує цикл.

Але в ході виконання циклу не запам’ятовуються ніякі поточні значення змінних та ще й у зворотньому порядку. Отож, переваги рекурсії функцій полягають хіба що в наявності хвостової рекурсії, яка дозволяє використати всі проміжні результати обчислень після того, як вони були одержані.

Слід, однак, зауважити, що існують задачі, зокрема сортування методом Хоара, який розглядається в розділі 11, коли застосування рекурсії функцій дозволяє спростити їх програмування.

Аналізуючи приклад 9.5, бачимо, що рекурсію функції можна застосувати замість циклу. Причому це цикл з передумовою, тобто з перевіркою на досягнення кінця циклу перед наступним його виконанням. Він не може бути циклом з постумовою. Це показано в програмі прикладу 9.6 для визначення кореня рівняння Редліха-Квонга за методом Ньютона, реалізованого в програмі прикладу 4.3.4.

Оскільки програма прикладу 9.6 має цикл з передумовою, то на її початку змінна z приймає формальне значення 10 – більше від p=1, ніж на величину eps, для забезпечення початку ітераційного процесу, адже умова fabs(p-z)>eps перевіряється до того, як змінна z буде ініціалізована.

Слід наголосити на тому, що і в цій, і в попередній (приклад 9.5), і в інших програмах, де рекурсія функцій застосовується замість циклу, звернення до підпрограми відбувається з нарощеними на один крок значеннями параметрів.

Приклад 9.6 – Розв’язання рівняння із застосуванням рекурсії

#include<stdio.h> /* Розв’язання рівняння Редліха-Квонга */

#include<math.h>

main()

{

float z=10, p=1, f(float, float);

p=f(z, p);

}

float f(float z, float p)

{

float k=3.2, c=1.4, eps=1e-5;

if(fabs(p-z)>eps){z=p; p=f(z, z-(z*z*z-z*z+k*z-c)/(3*z*z-2*z+k));}

else printf("z=%f\n", p);

return p;

}

У прикладі 9.7 показано застосування рекурсивної функції reku_for() замість циклу типу for, реалізованого в прикладі 4.3.4. В цій програмі параметрами функції служать нарощені на один крок змінні z, та i, які змінюються в ході виконання циклу.

Приклад 9.7 – Рекурсія функцій замість циклу типу for

#include<stdio.h> /* Рекурсія функцій замість циклу типу for */

void reku_for(float z, int i)

{ float k=3.2;

printf("y=%5.2f z=%4.1f\n", 3*z*z-2*z+k, z);

if(i<11)reku_for(z+0.2, i+1); else return;

}

int main(void)

{

float z=0; int n=1;

reku_for(z, n);

return(0);

}

Запитання для самоперевiрки

  1. Дайте визначення головної прогрaми та пiдпрограми.

  2. Перечисліть області застосування підпрограм.

  3. Назвіть характерні особливості процедури-функції. Чим вона відрізняється від процедури-підпрограми?

  4. Якi види пiдпрограм дозволяє застосовувати мова С?

  5. Що таке прототип функції та для чого він служить?

  6. Чим вiдрiзняються дiйснi параметри вiд формальних. Чи можна застосовувати вирази в їх якостi?

  7. Що таке локальні та глобальні параметри?

  8. Якi типи даних дозволено передавати в процедуру мовою С?

  9. Чи повиннi спiвпадати кiлькiсть i тип формальних та дiйсних параметрiв у головній та підпорядкованій програмах мовою С?

  10. Запропонуйте варiант програми прикладу 7.1 із застосуванням рекурсії функції.

  11. В яких випадках оператор виходу з функції return необов’язковий?

  12. Якi є способи передачi масиву в процедуру мовою С?

  13. Що таке рекурсія функції?

  14. Що таке носовий і хвостовий цикли рекурсії функцій?

  15. Як звільняється пам’ять, відведена під параметри та локальні змінні після виходу з функції при рекурсії?

  16. Чи можна застосувати рекурсію функцій замість будь-якого циклу, чи є тут якісь обмеження? В яких випадках рекурсія функцій може бути вигіднішою за цикл?

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]