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

7 Процедури

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

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

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

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

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

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

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

Головна програма мовою С – теж функція. Вона може бути запущеною на виконання іншою функцією, найчастіше – операційною системою, і приймати вхідні параметри, та повертати значення. Якщо це значення дорівнює 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. Це особливо важливо для програм, процедури-функції яких утворюють динамічні масиви. Якщо не застосовуються засоби звільнення (вищерозглянена в розділі 6 функція free()) такої пам’яті, то вказівник на неї пропадає, тоді використати її повторно буде неможливо. При багаторазовому зверненні до такої функції можна розтранжирити всю пам’ять.

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

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

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

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

Означений інтеграл обчислимо за формулою Сімпсона, яка має такий вигляд:

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

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

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

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

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

Початкове s:

Сума s:

Вивід s:

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

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

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

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

#include<stdio.h> /* funkcia */

#define v0 1.328

#define k 0.098

float f(float);

int main(void)

{int i, n=100;

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

h=(b-a)/n;

s=f(a)+f(b)+4*f(b-h);

for(i=1; i<n/2; i++)s+=4*f(a+(2*i-1)*h)+2*f(a+2*i*h);

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

return 0;

}

float f(float t)

{

return v0/(1+k*t);

}

Результати виконання програми funkcia мають такий вигляд:

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

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

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

Вхідний параметр підпорядкованої функції змінна t є локальним. Отже, під цю змінну заново відводиться пам’ять при кожному зверненні до підпрограми, а при виході з неї пам’ять звільняється і повертається в загальний пул. Можна підрахувати, що в програмі funkcia це відбудеться 101 раз: 3 рази перед циклом і 98 – у циклі (100/2-1)*2=98). Це говорить про суттєве зростання затрат машинного часу, та й пам’яті, на використання функцій, особливо в циклах.

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

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

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

#include<stdio.h> /* makros */

#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), mmm(++z));

getch();

return(0);

}

mmm=4 mzk=11

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

Оскільки показана вище функція f(t) програми funkcia теж складається з одного виразу, то замість неї можна було б застосувати макрос, наприклад, такого вигляду:

#define f(t) v0/(1+ki*(t))

Звернемо увагу на те, що його змінна t взята в дужки, натомість вираз оператора return v0/(1+ki*t); їх не має. Подібно до вже розгляненого вище макроса mmm(x) звернення до цього макроса з головної програми містить суму (наприклад, f(b-h)), тому відсутність дужок спричинить помилку, програма видасть невірний результат.

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

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

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

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

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

#include<stdio.h> /* rekursia */

#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

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

Аналізуючи програму rekursia, бачимо, що рекурсію функції можна застосувати замість циклу. Причому це цикл з передумовою, тобто з перевіркою на досягнення кінця циклу перед наступним його виконанням. Він не може бути циклом з постумовою, який реалізований у програмі Njuton, відомої з розділу 4.3. Наприклад, програма для визначення кореня рівняння Редліха-Квонга за методом Ньютона буде мати такий вигляд (програма function):

#include<stdio.h> /* function */

#include<math.h>

main()

{

float z=10, p=1;

float f(float, loat);

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;

}

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

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

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

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

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

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

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

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

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

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

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

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

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

  10. Запропонуйте варiант програми function для даного тут прикладу, в якiй буде вiдсутня директива #include<math.h>. Які для цього треба внести зміни в програму?

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

  12. Якi є способи передачi масиву в процедуру? Запропонуйте варiант головної програми та процедури, у яких обробляється масив, але буде вiдсутнiй вказівник на масив.

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

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

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

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

  17. В яких випадках рекурсія функцій може бути вигіднішою за цикл?

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