Функції
Функція - Це самостійна одиниця програми, яка спроектована для реалізації конкретного підзавдання. Функція може бути багаторазово викликана з іншої ділянки програми, це дозволяє виключити повтор тих самих дій. Функції також допомагають логічно вибудувати програму.
Функція визначається таким чином:
1. Тип значення, що повертається;
2. Ім'я функції;
3. Інформація про формальні аргументи;
4. Тіло функції.
Формальний аргумент — це змінна у функції, що викликається.
Загальна форма запису функції:
(Тип1 agr1, тип2 agr2, . ) Тіло функції;
>
Оператором повернення з функції до точки її виклику є оператор return.
Давайте подивися приклад функції:
int SUM(int a, int b) { int sum = a + b; return sum; }
Функція SUM приймає два значення типу int і повертає їхню суму. a, b - формальні аргументи, а sum - Локальна змінна.
Щоб викликати функцію, потрібно вказати її ім'я і перерахувати в дужках фактичні аргументи, що передаються.
Фактичний аргумент - це величина, яка надається формальному аргументу при виклику функції.
Приклад виклику функції SUM:
У змінну s буде записано значення, яке поверне функція SUM.
Між формальними та фактичними параметрами при виклику функції повинні дотримуватися правил відповідності за послідовністю та за типами.
#include #include int
SUM
(int a, int b)
{
int sum = a + b;
return sum;
}
int main()
{
int x, y; scanf_s("%i%i", &x, &y);
int s =
SUM(x, y);
printf("%i", s);
return
0;
}
При передачі аргументів відбувається їхнє копіювання. Це означає, що будь-які зміни, які функція здійснює над змінними, мають місце лише всередині функції. Щоб змінити фактичні значення, необхідно передати вказівник на цей елемент.
Наприклад, функція sqr повертає квадрат числа та змінює фактичну змінну x на 5:
#include #include int
sqr(int
*a)
{
int b =
*a;
*a =
*a +
5;
// !
return b * b;
}
int main()
{
int x;
int
*p =
&x; scanf_s("%i", &x);
int ans =
sqr(p);
printf("ans = %i\nnew x = %i", ans, x);
return
0;
}
Функції можуть не повертати значення, а просто виконувати деякі обчислення. У цьому випадку вказується порожній тип значення void, що повертається, а оператор return може або відсутній, або не повертати ніякого значення (return;):
#include #include void
sqr(int x)
{
printf("%i\n", x * x);
}
int main()
{
int x; scanf_s("%i", &x);
sqr(x); x +=
10;
sqr(x);
return
0;
}
Передача масиву у функцію
Оскільки ім'я масиву - це покажчик, тому передача масиву на функцію дорівнює передачі покажчика.
Приклад передачі масиву у функцію:
#include #include void
F(int
*mas, int n)
{
// або for
(int i =
0; i n; i++) mas[i]
=
100;
}
int main()
{
int n, a[10]; scanf_s("%i", &n);
for
(int i =
0; i n; i++) scanf_s("%i", &a[i]);
F(a, n);
return
0;
}
Оскільки ми передали покажчик, всі зміни функції будуть поширюватися і поза нею. Аналогічно і з двовимірним масивом
Щоб передати функції двовимірний масив, необхідно чітко вказати розміри масиву:
#include #include void
print(int mas[][10], int n, int m)
{
for
(int i =
0; i n; i++)
{
for
(int j =
0; j m; j++)
{
printf("%i", mas[i][j]);
}
printf("\n");
}
}
int main()
{
int n, m, a[10][10]; scanf_s("%i%i", &n, &m);
for
(int i =
0; i n; i++)
{
for
(int j =
0; j m; j++)
{ scanf_s("%i", &a[i][j]);
}
}
print(a, n, m);
return
0;
}
Якщо двовимірним масив був виділений динамічно, то в функцію потрібно передати покажчик на покажчик:
void F(int **mas, int n, int m) { . }
Передача структури на функцію
Передати структуру в функцію можна як за значенням (тоді відбудеться копіювання структури функції), так і за адресою. Передавати адресу в функцію набагато вигідніше по пам'яті. Справа в тому, що розмір покажчика може бути набагато меншим за розмір структурної змінної.
#include #include struct
point
{
int x;
int y;
};
void
F(struct
point* a)
{ a->x =
100; a->y =
22;
}
int main()
{
struct
point P; P.x
=
10; P.y
=
4;
F(&P);
printf("%i %i", P.x, P.y);
return
0;
}
Якщо хочемо передати структуру за значенням, то змінюємо рядок 17 на F(P), а функцію так:
void F(struct point a) { a.x = 100; a.y = 22; }
Передача масиву структур повністю збігається з передачею звичайного масиву:
#include #include struct
point
{
int x;
int y;
};
void
print(struct
point* a, int n)
{
for
(int i =
0; i n; i++)
{
printf("%i %i\n", a[i].x, a[i].y);
}
}
int main()
{
struct
point P[10];
int n; scanf_s("%i", &n);
for
(int i =
0; i n; i++)
{ scanf_s("%i%i", &P[i].x, &P[i].y);
}
print(P, n);
return
0;
}
При динамічному виділення пам'яті під структуру зміни не відбуваються.
Рекурсивні функції
Функція, що викликає сама себе, називається рекурсивною функцією.
Для рекурсивної функції обов'язково потрібно вказати умова виходу з рекурсії, інакше функція буде нескінченно викликати себе. Усі рішення зводиться до знаходження умови виходу.
При кожному виклику для формальних аргументів та локальних змінних виділяється нова область пам'яті, так що їх значення з попередніх викликів не втрачаються, але в кожний момент часу доступні лише значення поточного виклику (крім покажчиків). Число рекурсивних дзвінків обмежується лише ресурсом пам'яті комп'ютера і при надто великій кількості рекурсивних дзвінків може статися переповнення стека.
Коли виконується умова виходу, функція перестає викликати себе і повертає якесь значення, зокрема і порожнє. Після повернення ми потрапляємо на попередній рівень рекурсії замість останнього виклику функції.
Завдання: дано натуральне число n > 1. Виведіть усі прості множники цього числа в порядку незменшення з урахуванням кратності.
Код на Сі:
#include #include void F(int n, int i)
{
if
(n ==
1)
{
return;
}
if
(n % i ==
0)
{
printf("%i", i); F(n / i, i);
}
else
{ F(n, i +
1);
}
return;
}
int main()
{
int n; scanf_s("%i", &n); F(n, 2);
return
0;
}
Щоб детальніше розібратися з дією рекурсії, можна переглянути, як використовується рекурсія в DFS - обхід графа в глибину.
Code.C © Copyright Павло Калашніков 2021
зворотний зв'язок [email protected]
Функції
Теґи: Функції в сі, прототип, опис, визначення, виклик. Формальні параметри та фактичні параметри. Аргументи функції, передача за значенням, передача за вказівником. Повернення значення.
Вступ
Чим далі ми вивчаємо це, тим більше стають програми. Ми збираємо всі дії в одну функцію main і по кілька разів копіюємо ті самі дії, створюємо десятки змінних з унікальними іменами. Наші програми розпухають і стають все менш і менш зрозумілими, розгалуження стають все довшими та гіллястішими.
Але із ситуації, що склалася, є вихід! Тепер ми навчимося створювати функції на сі. Функції, по-перше, допоможуть виділити в окремі підпрограми код, що дублюється, по-друге, допоможуть логічно розбити програму на частини, по-третє, з функціями вси пов'язано багато особливостей, які дозволять використовувати нові підходи до структурування додатків.
Функція – це іменована частина програми, що може бути багаторазово викликана з іншого ділянки програми (у якому ця функція видно). Функція може приймати фіксоване чи змінне число аргументів, і може мати аргументів. Функція може як повертати значення, і бути порожньою (void) і нічого не повертати.
Ми вже знайомі з багатьма функціями та знаємо, як їх викликати – це функції бібліотек stdio, stdlib, string, conio та ін. Більше того, main – це також функція. Вона відрізняється від інших лише тим, що є точкою входу під час запуску програми.
Функція всі визначається глобальному контексті. Синтаксис функції:
Найпростіший приклад – функція, яка приймає число типу float та повертає квадрат цього числа
#include #include float sqr(float x) <float tmp = x*x; return tmp; >void main()Усередині функції sqr ми створили локальну змінну, якій надали значення аргументу. Як аргумент функції передали число 9,3.Службове слово return повертає значення змінної tmp. Можна переписати функцію так:
У цьому випадку спочатку буде виконано множення, а потім повернення значення. У тому випадку, якщо функція нічого не повертає, типом значення, що повертається, буде void. Наприклад, функція, яка друкує квадрат числа:
void printSqr(float x)в даному випадку return означає вихід із функції. Якщо функція нічого не повертає, return можна не писати. Тоді функція доопрацює до кінця і відбудеться повернення управління функції, що викликає.
void printSqr(float x)Якщо функція не набирає аргументів, то дужки залишають порожніми. Також можна написати слово void:
void printHelloWorld() void printHelloWorld(void)Формальні та фактичні параметри
При оголошенні функції вказуються формальні параметри, які потім використовуються всередині самої функції. Під час виклику ми використовуємо фактичні параметри. Фактичними параметрами можуть бути змінні будь-якого відповідного типу чи константи.
Наприклад, нехай є функція, яка повертає квадрат числа та функція, яка підсумовує два числа.
#include #include //Формальні параметри мають імена a та b //за ними ми звертаємося до переданих аргументів усередині функції int sum(int a, int b) < return a+b; >float square(float x) < return x*x; >void main() < //Фактичні параметри можуть мати будь-яке ім'я, у тому числі й не мати імені int one = 1; float two = 2.0; //Передаємо змінні, друга змінна наводиться до потрібного типу printf("%d\n", sum(one, two)); //Передаємо числові константи printf("%d\n", sum(10, 20)); //Передаємо числові константи невірного типу, вони автоматично наводиться до потрібного printf("%d\n", sum(10, 20.f)); //Змінна цілого типу наводиться до типу з плаваючою точкою printf("%.3f\n", square(one)); //Як аргумент може виступати і виклик функції, яка повертає потрібне значення printf("%.3f\n", square(sum(2 + 4, 3))); getch(); >
Звертаю увагу, що приведення типів відбувається неявно і тільки тоді, коли це можливо. Якщо функція отримує число як аргумент, то не можна їй передати змінний рядок, наприклад "20" і т.д. Взагалі краще завжди використовувати вірний тип або явно приводити тип до потрібного.
Якщо функція повертає значення, воно не обов'язково має бути збережено. Наприклад, ми користуємося функцією getch, яка зчитує символ та повертає його.
#include #include void main() < char c; do < //Зберігаємо повернене значення змінну c = getch(); printf("%c", c); >while(c != 'q'); //Повернене значення не зберігається getch(); >
Передача аргументів
При передачі аргументів відбувається їхнє копіювання. Це означає, що будь-які зміни, які функція здійснює над змінними, мають місце лише всередині функції. Наприклад
#include #include void change(int a) < a = 100; printf("%d\n", a); >void main()Програми виведе
200
100
200
Зрозуміло, чому. Усередині функції ми працюємо зі змінною x, яка є копією змінної d. Ми змінюємо локальну копію, але при цьому змінна d не змінюється. Після виходу з функції локальну змінну буде знищено. Змінна d при цьому не зміниться.
Як тоді можна змінити змінну? Для цього потрібно передати адресу цієї змінної. Перепишемо функцію, щоб вона приймала покажчик типу int
Ось тепер програма виводить
200
100
100
Тут також була створена локальна змінна, але оскільки передана була адреса, ми змінили значення змінної d, використовуючи її адресу в оперативній пам'яті.
У програмуванні перший спосіб передачі параметрів називають передачею за значенням, другий - передачею за вказівником. Запам'ятайте просте правило: якщо ви хочете змінити змінну, необхідно передавати функції покажчик на цю змінну. Отже, щоб змінити покажчик, необхідно передавати покажчик на покажчик і т.д.Наприклад, напишемо функцію, яка прийматиме розмір масиву типу int та створюватиме його. З першого погляду, функція має виглядати якось так:
#include #include #include void init(int *a, unsigned size) < a = (int*) malloc(size * sizeof(int)); >void main() < int *a = NULL; init(a, 100); if (a == NULL) < printf("ERROR"); >else < printf("OKAY. "); free(a); > getch (); >
Але ця функція виведе ERROR. Ми передали адресу змінної. Всередині функції init було створено локальну змінну a, яка зберігає адресу масиву. Після виходу з функції цю локальну змінну було знищено. Крім того, що ми не змогли досягти потрібного результату, у нас виявився витік пам'яті: було виділено пам'ять на купі, але вже не існує змінної, яка б зберігала адресу цієї ділянки.
Для зміни об'єкта необхідно передавати покажчик на нього, у разі – покажчик на покажчик.
#include #include #include void init(int **a, unsigned size) < *a = (int*) malloc(size * sizeof(int)); >void main() < int *a = NULL; init(&a, 100); if (a == NULL) < printf("ERROR"); >else < printf("OKAY. "); free(a); > getch (); >
Ось тепер все працює як слід.
Ще подібний приклад. Напишемо функцію, яка приймає як аргумент рядок і повертає покажчик на область пам'яті, в яку скопійовано цей рядок.
У цьому прикладі витоку пам'яті не відбувається. Ми виділили пам'ять за допомогою функції malloc, скопіювали туди рядок, а потім повернули покажчик. Локальні змінні були видалені, але змінна test зберігає адресу ділянки пам'яті на купі, тому його можна видалити за допомогою функції free.
Оголошення функції та визначення функції. Створення власної бібліотеки
У си можна оголосити функцію до її визначення. Оголошення функції, її прототип, складається з значення, імені функції і типу аргументів, що повертається.Імена аргументів не можна писати. Наприклад
#include #include //Прототипи функцій. Імена аргументів можна писати int odd(int); int even(int); void main() < printf("if %d odd? %d\n", 11, odd(11)); printf("if %d odd? %d\n", 10, odd(10)); getch(); >//Визначення функцій int even(int a) < if (a) < odd(--a); >else < return 1; >> int odd(int a) < if (a) < even(--a); >else < return 0; >>
Це змішана рекурсія – функція odd повертає 1, якщо число непарне та 0, якщо парне.
Зазвичай оголошення функції поміщають окремо в .h файл, а визначення функцій в .c файл. Таким чином, заголовний файл є інтерфейсом бібліотеки і показує, як з нею працювати, не вдаючись у вміст коду.
Давайте створимо просту бібліотеку. Для цього потрібно буде створити два файли - один з розширенням .h і помістити туди прототипи функцій, а інший з розширенням .c і помістити туди визначення цих функцій. Якщо ви працюєте з IDE, то .h файл необхідно створювати в папці Файли заголовків, а файли коду в папці Файли вихідного коду. Нехай файли називаються File1.h та File1.c
Перепишемо попередній код. Ось так буде виглядати заголовний файл File1.h
#ifndef _FILE1_H_ #define _FILE1_H_ int odd(int); int even(int); #endif
Вміст вихідного коду файлу File1.c
#include "File1.h" int even(int a) < if (a) < odd(--a); >else < return 1; >> int odd(int a) < if (a) < even(--a); >else < return 0; >>#include #include #include "File1.h" void main()
Розглянемо особливості кожного файлу. Наш файл, який містить функцію main, підключає необхідні бібліотеки, а також заголовковий файл File1.h. Тепер компілятор відомі прототипи функцій, тобто він знає повертається тип, кількість і тип аргументів і імена функцій.
Заголовний файл, як і було зазначено раніше, містить прототип функцій. Також тут можуть бути підключені використовувані бібліотеки. Макрозахист #define _FILE1_H_ і т.д.використовується для запобігання повторному копіюванню коду бібліотеки під час компіляції. Ці рядки можна замінити однією
#pragma once int odd(int); int even(int);
Файл File1.c вихідного коду підключає свій заголовний файл. Все як завжди логічно і просто. У заголовних файлах прийнято крім прототипів функцій виносити константи, макропідстановки та визначати нові типи даних. Крім того, саме в файлах заголовків можна широко коментувати код і писати приклади його використання.
Передача масиву як аргумент
Як уже говорилося раніше, ім'я масиву підмінюється на покажчик, тому передача одновимірного масиву еквівалентна передачі покажчика. Приклад: функція отримує масив та його розмір і виводить на друк:
#include #include void printArray(int *arr, unsigned size) < unsigned i; for (i = 0; i < size; i++) < printf("%d ", arr[i]); >> void main() < int x[10] = ; printArray(x, 10); getch(); >
У цьому прикладі функція може мати такий вигляд
void printArray(int arr[], unsigned size) < unsigned i; for (i = 0; i < size; i++) < printf("%d ", arr[i]); >>
Також нагадаю, що правило підміни масиву на покажчик не є рекурсивним. Це означає, що необхідно вказувати розмірність двовимірного масиву під час передачі
#include #include void printArray(int arr[][5], unsigned size) < unsigned i, j; for (i = 0; i < size; i++) < for (j = 0; j < 5; j++) < printf("%d", arr[i][j]); >printf("\n"); > > void main() < int x[][5] = < < 1, 2, 3, 4, 5>, < 6, 7, 8, 9, 10>>; printArray(x, 2); getch(); >
#include #include void printArray(int (*arr)[5], unsigned size) < unsigned i, j; for (i = 0; i < size; i++) < for (j = 0; j < 5; j++) < printf("%d", arr[i][j]); >printf("\n"); > > void main() < int x[][5] = < < 1, 2, 3, 4, 5>, < 6, 7, 8, 9, 10>>; printArray(x, 2); getch(); >
Якщо двовимірний масив створений динамічно, можна передавати покажчик на покажчик. Наприклад, функція, яка отримує масив слів і повертає масив цілих, рівних довжині кожного слова:
#include #include #include #include #define SIZE 10 unsigned* getLengths(const char **words, unsigned size) < unsigned *lengths = NULL; unsigned i; lengths = (unsigned*) malloc(size * sizeof(unsigned)); for (i = 0; i < size; i++) < lengths[i] = strlen(words[i]); >return lengths; > void main() < char **words = NULL; char buffer [128]; unsigned i; unsigned * len = NULL; words = (char**) malloc(SIZE * sizeof(char*)); for (i = 0; i < SIZE; i++) < printf("%d. ", i); scanf("%127s", buffer); words[i] = (char *) malloc (128); strcpy(words[i], buffer); > len = getLengths (words, SIZE); for (i = 0; i < SIZE; i++) < printf("%d ", len[i]); free(words[i]); > free (words); free(len); getch(); >
Можна замість повертати покажчик на масив, передавати масив, який необхідно заповнити
#include #include #include #include #define SIZE 10 void getLengths(const char **words, unsigned size, unsigned *out) < unsigned i; for (i = 0; i < size; i++) < out[i] = strlen(words[i]); >> void main() < char **words = NULL; char buffer [128]; unsigned i; unsigned * len = NULL; words = (char**) malloc(SIZE * sizeof(char*)); for (i = 0; i < SIZE; i++) < printf("%d. ", i); scanf("%127s", buffer); words[i] = (char *) malloc (128); strcpy(words[i], buffer); >len = (unsigned*) malloc(SIZE * sizeof(unsigned)); getLengths(words, SIZE, len); for (i = 0; i < SIZE; i++) < printf("%d ", len[i]); free(words[i]); > free (words); free(len); getch(); >
На цьому перше знайомство із функціями закінчується: тема дуже велика і розбита на кілька статей.
