Функции - прв дел

Функциите се градбени единки на програмите. Тие овозможуваат произволно структурирање на кодот и поголема модуларност. Кај сите програми кои ги разгледавме досега, наредбите кои требаше да се извршат ги пишувавме во една функција - main(). Тоа е основниот дел кој го содржат сите програми - main() e првата функција која се повикува при стартот на една програма напишана во програмскиот јазик C++.

Практично гледано, функциите претставуваат множества наредби кои се извршуваат при секој нивен повик. Повик на одредена функција претставува наредба до процесорот за запирање на тековното извршување на наредби, запишување на моментната локација во програмата и повикување на соодветната функција. По завршување на повиканата функција, програмата продолжува со извршување од местото од каде е функцијата повикана.

Основната синтакса за креирање на функции е следната:

tip ime(parametar1, parametar2, parametar3, parametar4, ...)
{
    naredba1;
    naredba2;
    naredba3;
    .....
    naredbaN;
}

Притоа, tip е типот на податок кој се враќа како резултат од функцијата (или void, ако функцијата не враќа резултат), parametar1, parametar2, ..., итн, се параметрите со кои се повикува функцијата (тип и име), одделени со запирка, додека naredba1, naredba2, ..., naredbaN, ги претставуваат наредбите кои се извршуваат при секој повик на функцијата. Листата на параметри е незадолжителна и можно е повикување на функција без параметри - на пример, main().

Кога пишувате програми, обидете се да пишувате по една функција за секоја задача која програмата треба да ја реши (подредување на елементи во низа, пресметување на некој сложен израз, итн). Функциите овозможуваат делење на програмата на помали парчиња - кои, соодветно, се поедноставни за решавање. На овој начин, користејќи структурирано програмирање, ја намалуваме сложеноста на проблемот кој сакаме да го решиме.

Да разгледаме една едноставна програма составена од 2 функции:

Програма 14.1

#include <iostream>
using namespace std;

int factorial(int n)
{
      int res = 1;
          
      for (int i=2; i<=n; i++)
            res *= i;
      
      return res;
}

int main()
{
      int n = 4;
      int f = factorial(n);       //f = 24
      
      cout << f << endl;          //pechati '24' (1*2*3*4)
      
      return 0;
}

Прво, забележете дека функцијата factorial(int n) е декларирана пред функцијата main(). Во C++, тоа е задолжително - не може да се повика функција која не е претходно декларирана.

Во функцијата main(), со наредбата од линија 17 декларираме променлива f од тип int (цел број) и на истата и доделуваме вредност factorial(4). Очигледно, "factorial(4)" не е цел број, како што би се очекувало при доделување на вредност на целобројна променлива - тоа всушност е повик до функцијата factorial(int n). Во овој момент, процесорот запишува до каде стигнал со извршување на наредби, и потоа ја повикува функцијата factorial(int n). Повикувањето се врши на начин што системот ги копира вредностите со кои е повикана функцијата (4) во соодветните параметри (n). Понатаму, се извршуваат сите наредби во функцијата factorial(int n). Оваа функција декларира нова променлива res, пресметува одреден израз и го враќа резултатот од пресметката. Бидејќи е декларирана локално, променливата res може да се користи само во оваа функција/блок.

(Потсетување) C++ овозможува декларирање на променливи на два начини: глобално (надвор од која било функција) и локално (во одредена функција или блок). Променливите кои се декларирани глобално може да се користат на која било локација во програмата (во која било функција или блок). Од друга страна, променливите кои се декларирани локално може да се користат само во функцијата/блокот во кој се декларирани (и тоа, откако ќе бидат декларирани).

Забележете дека функцијата која ја повикавме е дефинирана како "int factorial(n)", што означува дека таа треба да врати резултат цел број. Тоа го правиме со наредбата од линија 11 - функцијата враќа резултат res=24. Овој резултат се запишува во променливата f (дефинирана во main()). По ова, програмата го продолжува извршувањето од линија 19 - каде што обележавме дека треба да се вратиме по повикот на функцијата factorial(int n). Запамтете дека резултатот од извршувањето на една функција се запишува (како директно да сме внеле вредност) на местото од каде се повикала таа функција. Во овој случај, 24, резултатот од извршувањето на factorial(4), директно се внесува во променливата f ("int f = 24;").

Погледнете ја следната програма составена од 4 функции:

Програма 14.2

#include <iostream>
using namespace std;

int soberi(int a, int b)
{
      cout << "2 ";
      int r = (a+b);
      return r;
}

//(x+x)*(y+y)
int mnozenje(int x, int y)
{
      cout << "4 ";
      
      int p = soberi(x, x);
      int v = soberi(y, y);
      
      return (p*v);
}

void ispechatiKraj()
{
      cout << "Kraj na programata." << endl;
}

int main()
{
      int a=4, b=4;
      cout << "1 ";
      int rezultat1 = soberi(3, 5);
      cout << "3 ";
      
      //presmetaj (a+a)*(b+b) + 1
      int rezultat2 = mnozenje(a, b) + 1;
      cout << "5 " << endl;
      
      //dosega, izlezot od programata e '1 2 3 4 2 2 5'
      
      cout << rezultat1 << endl;                         //pechati '8'
      cout << rezultat2 << endl;                         //pechati '65'
      
      cout << soberi(3, soberi(1, 2) + 1) << endl;       //pechati '7'
      
      //ispechati 'Kraj na programata.'
      ispechatiKraj();
      
      return 0;
}

Оваа програма е малку покомплицирана, па ќе се обидеме да ја објасниме дел по дел. Нормално, како и претходно, првата функција која се повикува е функцијата main(). Наредбата од линија 29 служи за иницијализација на две променливи a и b со вредности a=4 и b=4. Потоа, програмата печати "1 " со вметнување на текст во соодветниот излезен поток.

Следната наредба декларира променлива rezultat1 од тип int (цел број) и на неа и доделува вредност soberi(3, 5). Повторно, "soberi(3, 5)" е функција чија вредност треба да се пресмета пред да продолжиме со извршување на програмата, па процесорот запамтува до каде стигнал со извршувањето на наредби и ја повикува функцијата soberi(int a, int b) така што ги копира вредностите со кои е повикана функцијата (3, 5) во соодветните параметри (а, b). Притоа, нормално, првата вредност (3) оди кај првиот параметар (а), додека втората вредност (5) оди кај вториот параметар (b).

Понатаму, се извршуваат наредбите од функцијата soberi(int a, int b) - линиите 6, 7 и 8, и функцијата враќа резултат r=8. Овој резултат се запишува во променливата rezultat1 (дефинирана во main()) и програмата продолжува со извршување.

Пред да продолжиме понатаму, важно е да дефинираме два термини:

  • параметар - тоа е променлива дефинирана во декларацијата на функцијата - на пример, параметри на функцијата soberi(int a, int b) се променливите a и b. Параметрите на одредена функција може да се користат само во таа функција (локално).
  • аргумент - тоа е вредноста со која се повикува одредена функција, односно вредноста која се копира во некој од параметрите на функцијата (на линија 31, како аргументи ги користиме вредностите 3 и 5, на линија 35 тоа се вредностите на променливите а и b, односно 4 и 4).

Да продолжиме со дискутирање на програмата. На линија 35 постои декларација на нова променлива rezultat2, повикување на функција mnozenje(int x, int y) со аргументи a и b (чии вредности, 4 и 4, се копираат во параметрите x и y, соодветно) и доделување на вредност 65 (резултатот од функцијата за множење [64] + 1) на променливата rezultat2. Забележете дека имињата на параметрите (x и y) не мора да одговараат на имињата на променливите (a и b) кои се користат како аргументи. Исто така, забележете дека една функција, во случајов mnozenje(int x, int y), може да повикува други функции soberi(int a, int b) произволно - на кои било локации во кодот. Ова не е ништо поразлично од повикувањето на функции од страна на функцијата main() - во двата случаи имаме повик на една функција од страна на друга.

Пред да се повика една функција, програмата мора да ги знае вредностите на аргументите со кои се повикува таа функција - за точно да знае кои вредности да им ги додели на параметрите. На линија 43, потребно е да се пресметаат двата аргументи со кои се повикува првата функција soberi(int a, int b). Вредноста на првиот аргумент (3) e веднаш јасна, но за да ја дознаеме вредноста на вториот аргумент, мора да повикаме функција soberi(int a, int b) со аргументи 1 и 2 и да додадеме 1 на излезот (резултатот) од таа функција. Кога ќе се повика првобитната функција soberi(int a, int b), таа ќе се повика со аргументи 3 и 4, и ќе врати резултат 7 = 3+4.

Останува уште да го разгледаме повикот на функцијата ispechatiKraj(). Иако оваа функција не прима аргументи - како што не прима аргументи ниту функцијата main(), во C++ е задолжително, при дефинирање на една функција и при секое нејзино повикување да ги запишеме двете загради "(" и ")". Тоа е она што ги разликува функциите од променливите и што му кажува на компајлерот дека одреден израз е повик на функција. Дополнително, а за разлика од main(), ispechatiKraj() не ни враќа вредност. Во C++, кога сакаме да наведеме дека една функција не враќа вредност, го користиме клучниот збор void.

Вметнати функции

Вметната функција претставува функција за која програмерот побарал истата да биде вметната (вклучувајќи ја тука целата нејзина содржина) на сите места во програмата каде таа се споменува. Со ова се намалува времето потребно за повикување на функција (процесорот не мора да ја памети тековната состојба, да запише до која наредба стигнал со извршување, да ја зачува вредноста на сите регистри, да ги пополни параметрите со вредностите на аргументите и да скокне до соодветната функција), и ваквиот начин на дефинирање најчесто се користи за функции кои се повикуваат многу често. За сложени функции и функции кои се повикуваат ретко (само неколку пати), времето изгубено за нивно повикување е незначително.

Во C++, вметнати функции се дефинираат со клучниот збор inline, кој се запишува пред типот на податок кој го враќа функцијата.

Програма 14.3

#include <iostream>
using namespace std;

inline int max(int a, int b)
{
      int n = a;
      if (b > a)
            n = b;
      
      return n;
}

int main()
{
      cout << max(3, 4) << endl;       //pechati '4'
      cout << max(2, 1) << endl;       //pechati '2'
      
      int n = 3, p = 2;
      cout << max(n, p) << endl;       //pechati '3'
      cout << max(p, n) << endl;       //pechati '3'
      
      return 0;
}

Во програмата дадена погоре, намерно е креирана променлива n во функцијата main() и променлива n во функцијата max(int a, int b). Забележете дека, иако сме специфицирале дека содржината на функцијата max(int a, int b) треба да се вметне на местото од каде истата се повикува и постои променлива n и во двете функции, програмата работи како што треба - компајлерот самиот знае како да се справи со проблемот. Ова е многу важно правило: постапката на вметнување на ниеден начин не го менува однесувањето (резултатот) на функцијата.

Со клучниот збор inline програмерот испраќа само препорака до компајлерот - кој понатаму има слобода да го игнорира барањето за вметнување на функција - на пример, доколку функцијата е премногу сложена, премногу долга, итн. Од друга страна, дел од денешните компајлери автоматски ги вметнуваат едноставните функции кога ќе забележат дека тоа би имало позитивен ефект на перформансите на програмата.

Прототипови

Веќе кажавме дека, во C++, е задолжително да се декларираат сите функции пред истите да може да се повикуваат. На пример, во програмата спомената погоре, функцијата max(int a, int b) мора, во програмскиот код, да е дефинирана пред функцијата main(). Во спротивно, компајлерот ќе пријави дека не знае што е тоа max(int a, int b) и нема да успее да ја претвори нашата програма во извршна датотека.

Понекогаш, како програмери, сакаме да го направиме обратното - прво да го запишеме поважниот дел од програмата, па потоа да ги наведеме подзадачите (функциите) кои служат како помошно средство при решавање на главниот проблем. Или пак, можеби е потребно да креираме две функции F1() и F2() кои се повикуваат една со друга - F1() ја повикува F2() и F2() ја повикува F1(). Во тој случај, не постои начин на кој F1() би ја запишале пред F2() и обратно - F2() би ја запишале пред F1(). Во вакви ситуации, C++ ни овозможува креирање на, т.н., прототипови на функции. Прототип претставува декларација на функција без пишување на нејзиниот код.

Синтаксата за креирање на прототипови е идентична со синтаксата што ја користевме за креирање на функции - само без наведување на содржината на функцијата (наредбите кои таа ги содржи). Наместо содржина го запишуваме знакот ';' (кој е задолжителен!). Притоа, бидејќи за креирање на прототипот не се потребни вистинските имиња на параметрите (туку само нивните типови) - нив можеме да ги прескокнеме.

Програма 14.4

#include <iostream>
using namespace std;

int max(int a, int b);                 //prototip (so iminja na parametri)
int min(int , int);                    //prototip (bez iminja na parametri)

int main()
{
      cout << max(3, 4) << endl;       //pechati '4'
      cout << min(2, 1) << endl;       //pechati '1'
      
      return 0;
}

//definicija na max(int a, int b)
int max(int a, int b)
{
      if (a > b)
            return a;
      
      return b;
}

//definicija na min(int a, int b)
int min(int a, int b)
{
      if (a < b)
            return a;
      
      return b;
}

Доколку наведеме прототип (декларација) на една функција и потоа не ја дефинираме истата - компајлерот ќе пријави грешка само ако таа функција се повикува некаде во програмата. Доколку функцијата не се повикува од ниту една друга функција, програмата ќе се преведе успешно.

Преддефинирани вредности

Во декларацијата на една функција може да наведеме и преддефинирани вредности за последните неколку параметри. Овие вредности нема да имаат никаков ефект доколку програмерот наведе аргументи (вредности) за тие параметри - преддефинираната вредност ќе се игнорира. Но, доколку при повикот на функцијата програмерот не наведе некој од аргументите, тогаш како вредност на тој параметар ќе се искористи преддефинираната вредност.

Во C++, преддефинираните вредности ги пишуваме во декларацијата на функцијата, по името на параметарот и операторот '=':

Програма 14.5

#include <iostream>
using namespace std;

void prva(int a=7, int b=3, int c=5)
{
      cout << "a=" << a << ",b=" << b << ",c=" << c << endl;
}

int vtora(int a, int b, int c=3)
{
      return a+b+c;
}

int main()
{
      prva(1, 2, 3);                        //pechati 'a=1,b=2,c=3'
      prva(4, 5, 6);                        //pechati 'a=4,b=5,c=6'
      
      prva(0, 2);                           //pechati 'a=0,b=2,c=5'
      prva(3, 8);                           //pechati 'a=3,b=8,c=5'
      
      prva(0);                              //pechati 'a=0,b=3,c=5'
      prva(5);                              //pechati 'a=5,b=3,c=5'
      
      prva();                               //pechati 'a=7,b=3,c=5'
      
      cout << vtora(1, 2) << endl;          //pechati '6'
      cout << vtora(1, 2, 1) << endl;       //pechati '4'
      
      return 0;
}

Важно е да се каже дека преддефинирани вредности може да се наведат само за најдесните параметри, т.е., доколку наведеме преддефинирана вредност за еден параметар, тогаш мора да се наведе вредност и за сите параметри кои се наоѓаат десно од тој параметар (ако постојат такви). Само така компајлерот ќе знае кои аргументи не се наведени (се изоставени) при повикот на функцијата. На пример, во функцијата vtora(int a, int b, int c), не може да наведеме преддефинирана вредност за параметарот a, а да не наведеме преддефинирана вредност за параметрите b и c. Слично, програмата не може да повика функција со наредбата vtora(, 2, 3) - да изостави некој од првите аргументи, а да ги наведе останатите.

При декларирањето на вакви функции, параметрите на функциите кои се менуваат најчесто треба да се наоѓаат лево. Оние параметри пак, за кои програмерот ретко ќе наведува аргументи, треба да се наоѓаат од десната страна (бидејќи за нив е дозволено да се наведе преддефинирана вредност).

Преоптоварени функции

Понекогаш сакаме да креираме две или повеќе функции кои враќаат еден ист резултат (на пример, збир на броеви), но се извршуваат врз различни типови на податоци (цели броеви, реални броеви, еден цел и еден реален број, итн). C++ нуди механизам (т.н. преоптоварување на функции) преку кој неколку функции може да имаат едно исто име. За да се реализира ова, нормално, потребно е овие функции да се разликуваат по бројот на параметрите и/или по нивниот тип.

На пример, следнава програма креира неколку функции со име zgolemi, секоја со различна комбинација на параметри:

Програма 14.6

#include <iostream>
using namespace std;

int zgolemi(int a, int b)
{
      return a+b;
}

int zgolemi(int a)
{
      return a+1;
}

double zgolemi(double a, double b)
{
      return a+b;
}

double zgolemi(double a)
{
      return a+1;
}

int main()
{
      cout << zgolemi(2, 3) << endl;        //se povikuva zgolemi(int a, int b)
      cout << zgolemi(5, 9) << endl;        //se povikuva zgolemi(int a, int b)
      cout << zgolemi(3) << endl;           //se povikuva zgolemi(int a)
      
      cout << zgolemi(3.0, 0.5) << endl;    //se povikuva zgolemi(double a, double b)
      cout << zgolemi(4.1, 2.9) << endl;    //se povikuva zgolemi(double a, double b)
      cout << zgolemi(2.0) << endl;         //se povikuva zgolemi(double a)
      
      return 0;
}

Доколку добро сте го прочитале делот од предавањето за преддефинирани вредности на функциите, веројатно ви е јасно дека четирите функции дадени погоре можеме едноставно да ги собереме во две функции: една која прима параметри цели броеви (ќе поставиме преддефинирана вредност за b=1) и една која прима параметри реални броеви (слично, со b=1.0). Сепак, целта на овој дел од предавањето е да покаже дека неколку функции со исто име може да примаат различен број на параметри. Затоа и примерот е малку посложен отколку што е тоа навистина потребно.

Гледајќи го кодот на програмата, забележуваме дека таа е составена од четири функции со име zgolemi. Притоа, сите функции примаат различна комбинација од параметри (еден цел број, два цели броеви, еден реален број или два реални броеви). Притоа, зависно од типот на аргумент/и кои се предаваат, компајлерот знае точно која функција да ја повика.

Изборот која од неколку преоптоварени функции ќе се повика се сведува на проверка на исполнувањето на некои од следните услови:

  1. Доколку кај една функција постои целосно совпаѓање на типовите на аргументите со типовите на параметрите, тогаш таа е функцијата која ќе се повика. На пример, како аргументи на функцијата zgolemi(int a, int b) испраќаме две вредности од тип int.
  2. Доколку не постои функција која го задоволува условот 1, се проверува дали постои одредена функција чија листа на параметри може да се совпадне со аргументите со едноставно проширување на нивниот опсег. На пример, како аргумент на функцијата zgolemi(int a, int b) испраќаме две вредности од тип short. Бидејќи int има поголем опсег од short, а и двата типа се однесуваат на исто множество на податоци (цели броеви), C++ ќе го претвори short во int и ќе ја повика таа функција.
  3. Доколку не постои функција која ги задоволува условите 1 или 2, се проверува дали постои функција чија листа на параметри може да се совпадне со аргументите преку претворање на нивниот тип. На пример, како аргумент на функцијата zgolemi(double a, double b) испраќаме две вредности од тип int (во програмата дадена погоре, можеме да ја избришеме првата функција за да го забележиме овој ефект). Тука може да се појави мал проблем - т.н. амбициозно совпаѓање. На пример, што да се направи доколку постојат две функции - zgolemi(double a, double b) и zgolemi(float a, float b), и ние испратиме повик со аргументи од тип int. Во C++, бидејќи сите претворања (од еден тип на податок во друг) се сметаат за еднакви, ваквото повикување на функција е недозволено. Овој проблем може да го решиме пред самиот повик на функцијата, преку експлицитно претопување на податоците во соодветниот (посакуван) тип: zgolemi((double) x, (double) y)). Слично, може да напишеме уште една функција со истото име, но со параметри од соодветниот тип: во случајов zgolemi(int a, int b).

Функциите може да се преоптоваруваат само според параметрите, а не и според типот на податок кој се враќа како резултат од функцијата. Не е дозволено креирање на две функции со исто име, иста комбинација од параметри и различен тип на податок кој се враќа како резултат. Едноставно, во овој случај, компајлерот нема да знае која функција да ја повика.

Математички функции

Голем број на математички операции и трансформации се веќе имплементирани во програмскиот јазик C++ - во вид на функции кои нашите програми може да ги повикуваат. За да може да ги користиме овие функции, потребно е вклучување на соодветната датотека од библиотеката на C++ која ги содржи нивните дефиниции (со користење на директивата "#include <cmath>").

функција значење
sin(x) Синус од аголот X (претставен во радијани)
cos(x) Косинус од аголот X (претставен во радијани)
tan(x) Тангенс од аголот X (претставен во радијани)
asin(x) Инверзна операција од sin(x). Резултатот е вредност (агол) изразен во радијани.
acos(x) Инверзна операција од cos(x). Резултатот е вредност (агол) изразен во радијани.
atan(x) Инверзна операција од tan(x). Резултатот е вредност (агол) изразен во радијани.
atan2(y, x) Аркус тангенс од y/x. Резултатот е вредност (агол) изразен во радијани.
Функцијата го користи знакот на двата аргументи (x и y) за одредување на квадрантот.
exp(x) Вредноста на изразот ex, каде e=2.718281828 (основа на природен логаритам)
log(x) Природен логаритам (со основа e) од x.
log10(x) Логаритам (со основа 10) од x.
pow(x, y) Вредноста на изразот xy (x на степен y).
sqrt(x) Квадратен корен од x.
ceil(x) Најмалиот цел број што не е помал од x - ceil(4.3) = 5
floor(x) Најголемиот цел број што не е поголем од x - floor(4.8) = 4
fabs(x) Апсолутна вредност од x

Доколку не разбирате што значат првите неколку функции, не паничете - веројатно сеуште не сте ги изучувале овие стандардни тригонометриски поими. Кога ќе стигнете до таа област од математиката, ќе знаете точно што значи секоја од овие функции, каков резултат таа враќа и како таа вредност може да се искористи за решавање на одредени геометриски проблеми.

Следнава програма го илустрира ефектот од користењето на дел од гореспоменатите математички функции:

Програма 14.7

#include <iostream>
#include <cmath>
using namespace std;

int main()
{
      double x = 3.0, y = 2.7;
      
      cout << pow(2.0, 3.0) << endl;       //pechati '8'
      cout << sqrt(4) << endl;             //pechati '2'
      
      cout << fabs(-7.123) << endl;        //pechati '7.123'
      
      cout << log(x) << endl;              //pechati '1.09861'
      cout << log10(x) << endl;            //pechati '0.477121'
      
      cout << floor(y) << endl;            //pechati '2'
      cout << ceil(y) << endl;             //pechati '3'
      
      return 0;
}

Функции за работа со знаци

Покрај математички операции, во програмскиот јазик C++ однапред се имплементирани и неколку функции за работа со текстуални знаци. За да може да ги користиме овие функции, потребно е вклучување на соодветната датотека која ги содржи нивните дефиниции (со користење на директивата "#include <cctype>").

функција значење
isalnum(c) Проверува дали c е алфанумерички податок (буква или цифра)
isalpha(c) Проверува дали c е буква
isdigit(c) Проверува дали c е цифра
islower(c) Проверува дали c е мала буква
isupper(c) Проверува дали c е голема буква
tolower(c) Доколку c е голема буква, се враќа соодветната мала буква.
Во спротивно, се враќа аргументот (истиот знак).
toupper(c) Доколку c е мала буква, се враќа соодветната голема буква.
Во спротивно, се враќа аргументот (истиот знак).

Програма 14.8

#include <iostream>
#include <cctype>
using namespace std;

int main()
{
      if (isalpha('x'))                          //true
            cout << "x e bukva." << endl;
      
      if (isdigit('3'))                          //true
            cout << "3 e cifra." << endl;
      
      char ch1 = toupper('x');                   //ch1 = 'X'
      char ch2 = tolower('3');                   //ch2 = '3'
      
      cout << ch1 << endl;                       //pechati 'X'
      cout << ch2 << endl;                       //pechati '3'
      
      return 0;
}

Дозвола за користење: CC BY-NC 2.5 ©