Визначення і виклик функцій

Функція - це сукупність оголошень і операторів, звичайно призначена для вирішення певної задачі. Кожна функція повинна мати ім’я, яке використовується для її оголошення, визначення і виклику. У будь-якій програмі на Сі повинна бути функція з ім’ям main (головна функція), саме з цієї функції, в якому б місці програми вона не знаходилася, починається виконання програми.

При виклику функції їй за допомогою аргументів (формальних параметрів) можуть бути передані деякі значення (фактичні параметри), використовувані під час виконання функції. Функція може повертати деяке (одне !) значення. Це значення, що повертається, і є результат виконання функції, який при виконанні програми підставляється в точку виклику функції, де б цей виклик ні зустрівся. Допускається також використовувати функції що не мають аргументів і функції не повертаючі ніяких значень. Дія таких функцій може полягати, наприклад, в зміні значень деяких змінних, висновку на друк деяких текстів і т.п..

З використанням функцій в мові Сі зв’язані три поняття - визначення функції (опис дій, виконуваних функцією), оголошення функції (завдання форми звернення до функції) і виклик функції.

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

Приклад:

1int test (unsigned char r)
2{
3   if (r>='А' && c<=' ')
4      return 1;
5   else
6      return 0;
7}

У даному прикладі визначена функція з ім’ям test, що має один параметр з ім’ям r і типом unsigned char. Функція повертає ціле значення, рівне 1, якщо параметр функції є буквою російського алфавіту, або 0 інакше.

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

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

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

int test (unsigned char r); або test (unsigned char);

У програмах на мові Сі широко використовуються, так звані, бібліотечні функції, тобто функції заздалегідь розроблені і записані в бібліотеки. Прототипи бібліотечних функцій знаходяться в спеціальних заголовних файлах, що поставляються разом з бібліотеками у складі систем програмування, і включаються в програму за допомогою директиви #include.

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

Оголошення параметрів функції при її визначенні може бути виконане в так званому “старому стилі”, при якому в дужках після імені функції слідують тільки імена параметрів, а після дужок оголошення типів параметрів. Наприклад, функція test з попереднього прикладу може бути визначена таким чином:

1int test (r)
2unsigned char r;
3{
4   /* ...  */
5   /* тіло функції  */
6   /* ...  */
7}

Відповідно до синтаксису мови Сі визначення функції має наступну форму:

[специфікатор-класу-пам'яті] [специфікатор-типу] ім'я-функції ([список-формальних-параметрів]) { тіло-функції }

Необов’язковий специфікатор-класу-пам’яті задає клас пам’яті функції, який може бути static або extern. Детально класи пам’яті будуть розглянуті в наступному розділі.

Специфікатор-типу функції задає тип значення, що повертається, і може задавати будь-який тип. Якщо специфікатор-типа не заданий, то передбачається, що функція повертає значення типу int.

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

Функція повертає значення якщо її виконання закінчується оператором return, що містить деякий вираз. Вказаний вираз обчислюється, перетвориться, якщо необхідно, до типу значення, що повертається, і повертається в точку виклику функції як результат. Якщо оператор return не містить виразу або виконання функції завершується після виконання останнього її оператора (без виконання оператора return), то значення, що повертається, не визначено. Для функцій, що не використовують значення, що повертається, повинен бути використаний тип void, вказуючий на відсутність значення, що повертається. Якщо функція визначена як функція, що повертає деяке значення, а в операторі return при виході з неї відсутній вираз, то поведінка викликаючої функції після передачі їй управління може бути непередбачуваним.

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

Якщо функція не використовує параметрів, то наявність круглих дужок обов’язкова, а замість списку параметрів рекомендується вказати слово void.

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

Для формального параметра можна задавати клас пам’яті register, при цьому для величин типу int специфікатор типу можна опустити.

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

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

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

Тіло функції - це складовий оператор, що містить оператори, що визначають дію функції.

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

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

Приклад:

1/*  Неправильне використання параметрів  */
2void change (int x, int у)
3{
4   int k=x;
5   x=y;
6   y=k;
7}

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

Приклад:

1/* Правильне використання параметрів  */
2void change (int *x, int *y)
3{
4   int k=*x;
5   x=*y;
6   y=k;
7}

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

1change (&a,&b);

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

[специфікатор_класу_пам'яті] [специфікатор_типу] ім'я_функції ([список_формальних_параметрів]) [,список_імен_функцій];

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

Прототип - це явне оголошення функції, яке передує визначенню функції. Тип значення, що повертається, при оголошенні функції повинен відповідати типу значення, що повертається, у визначенні функції.

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

Таким чином, прототип функції необхідно задавати в наступних випадках:

  1. Функція повертає значення типу, відмінного від int.
  2. Потрібно проініціалізувати деякий покажчик на функцію до того, як ця функція буде визначена.

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

У прототипі можна вказати, що число параметрів функції змінне, або що функція не має параметрів.

Якщо прототип заданий з класом пам’яті static, то і визначення функції повинне мати клас пам’яті static. Якщо специфікатор класу пам’яті не вказаний, то мається на увазі клас пам’яті extern.

Виклик функції має наступний формат: адресний-вираз ([список-виразів])

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

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

Фактичний параметр може бути величиной будь-якого основного типа, структурою, об’єднанням, переліком або покажчиком на об’єкт будь-якого типу. Масив і функція не можуть бути використані як фактичні параметри, але можна використовувати покажчики на ці об’єкти.

Виконання виклику функції відбувається таким чином:

  1. Обчислюються вирази в списку виразів і піддаються звичним арифметичним перетворенням. Потім, якщо відомий прототип функції, тип одержаного фактичного аргументу порівнюється з типом відповідного формального параметра. Якщо вони не співпадають, то або виробляється перетворення типів, або формується повідомлення про помилку. Число виразів в списку виразів повинне співпадати з числом формальних параметрів, якщо тільки функція не має змінного числа параметрів. У останньому випадку перевірці підлягають тільки обов’язкові параметри. Якщо в прототипі функції вказано, що їй не потрібні параметри, а при виклику вони вказані, формується повідомлення про помилку.
  2. Відбувається присвоєння значень фактичних параметрів відповідним формальним параметрам.
  3. Управління передається на першого оператора функції.
  4. Виконання оператора return в тілі функції повертає управління і можливе, значення у викликаючу функцію. За відсутності оператора return управління повертається після виконання останнього оператора тіла функції, а значення, що повертається, не визначено.

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

Приклад:

1int (*fun)(int x, int *y);

Тут оголошена змінна fun як покажчик на функцію з двома параметрами: типу int і покажчиком на int. Сама функція повинна повертати значення типу int. Круглі дужки, що містять ім’я покажчика fun і ознаку покажчика *, обов’язкові, інакше запис

1int *fun (int x,int *y);

інтерпретуватиметься як оголошення функції fun повертаючою покажчик на int.

Виклик функції можливий тільки після ініціалізації значення покажчика fun і має вигляд:

1(*fun)(i,&j);

У цьому виразі для отримання адреси функції, на яку посилається покажчик fun використовується операція розадресації * .

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

Приклад:

1double (*fun1)(int x, int у);
2double fun2(int до, int l);
3
4fun1=fun2;     /* ініціалізація покажчика на функцію */
5(*fun1)(2,7);  /* звернення до функції        */

У розглянутому прикладі покажчик на функцію fun1 описаний як покажчик на функцію з двома параметрами, повертаючу значення типу double, і також описана функція fun2. Інакше, тобто коли покажчику на функцію привласнюється функція описана інакше, ніж покажчик, відбудеться помилка.

Розглянемо приклад використання покажчика на функцію як параметр функції, що обчислює похідну від функції cos(x).

Приклад:

 1double derivate(double x, double dx, double (*f)(double x) );
 2double fun(double z);
 3
 4int main()
 5{
 6   double x;   /* точка обчислення похідної */
 7   double dx;  /* приріст          */
 8   double z;   /* значення похідної     */
 9   scanf("%f,%f",&x,&dx); /* введення значень x і dx     */
10   z=derivate(x,dx,fun);   /* виклик функції        */
11   printf("%f",z);     /* друк значення похідної */
12   return 0;
13}
14
15double derivate(double x,double dx, double (*f)(double z) )
16{
17   /* функція що обчислює похідну похідну */
18   double xk,xk1,pr;
19   xk=fun(x);
20   xk1=fun(x+dx);
21   pr=(xk1/xk-1e0)*xk/dx;
22   return pr;
23}
24
25double fun(double z)
26{
27   /* функція від якої обчислюється похідна */
28   return (cos(z));
29}

Для обчислення похідної від якої-небудь іншої функції можна змінити тіло функції fun або використати при виклику функції derivate ім’я іншої функції. Зокрема, для обчислення похідної від функції cos(x) можна викликати функцію derivate у формі

1z=derivate(x,dx,cos);

а для обчислення похідної від функції sin(x) у формі

1z=derivate(x,dx,sin);

Будь-яка функція в програмі на мові Сі може бути викликана рекурсивно, тобто вона може викликати саму себе. Компілятор допускає будь-яке число рекурсивних викликів. При кожному виклику для формальних параметрів і змінних з класом пам’яті auto і register виділяється нова область пам’яті, так що їх значення з попередніх викликів не втрачаються, але в кожен момент часу доступні тільки значення поточного виклику.

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

Класичний приклад рекурсії - це математичне визначення факторіалу n! :

1   n! = 1 при n=0;
2   n*(n-1)! при  n>1 .

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

1long factorial(int n)
2{
3   return ( (n==1) ? 1 : n*factorial(n-1) );
4}

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

Виклик функції із змінним числом параметрів

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

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

Прикладами функцій із змінним числом параметрів є функції з бібліотеки функцій мови СІ, здійснюючі операції введення-висновку інформації (printf,scanf і т.п.). Детально ці функції розглянуті в третій частині книги.

Програміст може розробляти свої функції із змінним числом параметрів. Для забезпечення зручного способу доступу до аргументів функції із змінним числом параметрів є три макроозначення (макроси) va_start, va_arg, va_end, що знаходяться в заголовному файлі stdarg.h. Ці макроси указують на те, що функція, розроблена користувачем, має деяке число обов’язкових аргументів, за якими слідує змінне число необов’язкових аргументів. Обов’язкові аргументи доступні через свої імена як при виклику звичної функції. Для витягання необов’язкових аргументів використовуються макроси va_start, va_arg, va_end в наступному порядку.

Макрос va_start призначений для установки аргументу arg_ptr на початок списку необов’язкових параметрів і має вид функції з двома параметрами:

1void va_start(arg_ptr, prav_param);

Параметр prav_param повинен бути останнім обов’язковим параметром функції, що викликається, а покажчик arg_ptr повинен бути оголошений з визначенням в списку змінних типу va_list у вигляді: va_list arg_ptr;

Макрос va_start повинен бути використаний до першого використання макросу va_arg.

Макрокоманда va_arg забезпечує доступ до поточного параметра функції, що викликається, і теж має вид функції з двома параметрами type_arg va_arg(arg_ptr,type);

Ця макрокоманда витягує значення типу type за адресою, заданою покажчиком arg_ptr, збільшує значення покажчика arg_ptr на довжину використаного параметра (довжина type) і таким чином параметр arg_ptr указуватиме на наступний параметр функції, що викликається. Макрокоманда va_arg використовується стільки раз, скільки необхідно для витягання всіх параметрів функції, що викликається.

Макрос va_end використовується після закінчення обробки всіх параметрів функції і встановлює покажчик списку необов’язкових параметрів на нуль (NULL).

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

Приклад:

 1#include <stdarg.h>
 2
 3int main()
 4{
 5   int n;
 6   int average(int, ...);
 7   n=average(2,3,4, -1);
 8   /* виклик з чотирма параметрами */
 9   printf("n=%d",n);
10   n=average(5,6,7,8,9, -1);
11   /* виклик з шістьма параметрами  */
12   printf("n=%d",n);
13   return 0;
14}
15
16int average(int x,...);
17{
18   int i=0, j=0, sum=0;
19   va_list ptr;
20   va_start(ptr,x);  /* установка покажчика ptr на */
21                        /* перший необов'язковий параметр */
22   if (x!=-1)
23      sum=x;    /* перевірка на порожній список   */
24   else
25      return 0;
26   j++;
27   while ( (i=va_arg(ptr,int))!=-1)
28   /* вибірка чергового  */
29   {
30                     /* параметра і перевірка */
31      sum+=i;        /* на кінець списку   */
32      j++;
33   }
34   va_end(ptr);   /* закриття списку параметрів  */
35   return (sum/j);
36}

Передача параметрів функції main

Функція main, з якою починається виконання C-програми, може бути визначена з параметрами, які передаються із зовнішнього оточення, наприклад, з командного рядка. У зовнішньому оточенні діють свої правила представлення даних, а точніше, всі дані представляються у вигляді рядків символів. Для передачі цих рядків у функцію main використовуються два параметри, перший параметр служить для передачі числа передаваних рядків, другий для передачі самих рядків. Загальноприйняті (але не обов’язкові) імена цих параметрів argc і argv. Параметр argc має тип int, його значення формується з аналізу командного рядка і рівне кількості слів в командному рядку, включаючи і ім’я програми, що викликається (під словом розуміється будь-який текст пропуск, що не містить символу). Параметр argv це масив покажчиків на рядки, кожна з яких містить одне слово з командного рядка. Якщо слово повинне містити символ пропуск, то при записі його в командний рядок воно повинне бути укладене в лапки.

Функція main може мати і третій параметр, який прийнято називати envp, і який служить для передачі у функцію main параметрів операційної системи (середовища) в якій виконується C-програма.

Заголовок функції main має вигляд:

1int main (int argc, char *argv[], char *envp[])

Якщо, наприклад, командний рядок C-програми має вигляд:

1C:\>cprog working 'С program' 1

то аргументи argc, argv, envp представляються в пам’яті як показано в схемі на рис.1.

 1argc [ 4 ]
 2argv [  ]--> [  ]-->  [A:\cprog.exe\0]
 3     [  ]-->  [working\0]
 4     [  ]-->  [C program\0]
 5     [  ]-->  [1\0]
 6     [NULL]
 7
 8envp  [  ]--> [ ]-->  [path=A:\;C:\0]
 9      [  ]-->  [lib=D:\LIB\0]
10      [  ]-->  [include=D:\INCLUDE\0]
11      [  ]-->  [conspec=C:\COMMAND.COM\0]
12      [NULL]

Рис.1. Схема розміщення параметрів командного рядка

Операційна система підтримує передачу значень для параметрів argc, argv, envp, а на користувачі лежить відповідальність за передачу і використання фактичних аргументів функції main.

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

Приклад:

 1int main ( int argc, char *argv[], char *envp[])
 2{
 3   int i=0;
 4   printf ("\n Ім'я програми %s", argv[0]);
 5   for  (i=1; i>=argc; i++)
 6      printf ("\n аргумент %d рівний %s", argv[i]);
 7
 8   printf ("\n Параметри операційної системи:");
 9   while (*envp)
10   {
11      printf ("\n %s",*envp);
12      envp++;
13   }
14   return 0;
15}

Доступ до параметрів операційної системи можна також дістати за допомогою бібліотечної функції getenv, її прототип має наступний вигляд:

1char *getenv (const char *varname);

Аргумент цієї функції задає ім’я параметра середовища, покажчик на значення якої видасть функція getenv. Якщо вказаний параметр не визначений в середовищі в даний момент, то значення NULL, що повертається.

Використовуючи покажчик, одержаний функцією getenv, можна тільки прочитати значення параметра операційної системи, але не можна його змінити. Для зміни значення параметра системи призначена функція puteuv.

Компілятор мови Сі будує C-програму таким чином, що спочатку роботи програми виконується деяка ініціалізація, що включає, крім всього іншого, обробку аргументів, передаваних функції main, і передачу їй значень параметрів середовища. Ці дії виконуються бібліотечними функціями _setargv і _setenv, які завжди поміщаються компілятором перед функцією main.

Якщо C-програма не використовує передачу аргументів і значень параметрів операційної системи, то доцільно заборонити використання бібліотечних функцій _setargv і _setenv помістивши в C-програму перед функцією main функції з такими ж іменами, але не виконуючі ніяких дій (заглушки). Початок програми в цьому випадку матиме вигляд:

 1_setargv()
 2{
 3   return ;  /* порожня функція */
 4}
 5
 6_setenv()
 7{
 8   return ;  /* порожня функція */
 9}
10
11int main()
12{
13   /* головна функція без аргументів */
14   /* ... */
15   /* ... */
16   return 0;
17}

У приведеній програмі при виклику бібліотечних функцій _setargv і _setenv будуть використані функції поміщені в програму користувачем і не виконуючі ніяких дій. Це помітно понизить розмір одержуваного exe-файлу.