Пета вежба¶
Функције
Неке белешке...¶
До сада програмски код који смо желели да извршимо смо писали унутар функције main
, а по неке готове математичке функције из стандардне библиотеке користили како не би морали сами да имплементирамо поједина израчунавања, као и употребљавали фунцкије за унос података са стандардног улаза и штампање података на стандардни излаз.
У програмским језицима се често јављају појмови попут процедура, потпрограма, функција и сл., који омогућавају бољу организацију програмског кода као и могућност вишеструког коришћења истог програмског кода за различите вредност параметара, тј. аргумената. У савременим програмским језицима најчешће ћемо се сусретати са функцијама, које је програмски језик C популаризовао.
Процедуре и потпрограми нпр. су чести називи за скупину програмског кода груписану под неким заједничким називом. Употребом тог назива могуће је тај програмски код више пута извршавати из главног програма или пак других процедура, тј. потпрограма.
Функције у математичком смислу нпр. су изрази који трансформишу улаз у неки одговарајући излаз.
У програмском језику C, функције су суштински процедуре (тј. потпрограми) који се могу понашати налик математичким функцијама, тј. да за неке улазна параметре дају одређену излазну (тј. повратну) вредност.
Функције се дефинишу на следећи начин:
Није обавезно навести повратни тип и у том случају компајлер ће функцију имплицитно сматрати као да је повратног типа int
. Наравно, увек је боље експлицитно назначити повратни тип функције.
Могуће је навести произвољан број параметра функције, међусобно одвојени карактером запета (,
).
Параметре није обавезно навести, функција може бити без параметара. У случају да желимо да дефинишемо функцију без параметара коректније је по компајлеру да се напише void
уместо да се остави празнина.
Функције се позивају на следећи начин:
Уколико функција има повратну вредност, на место позива функције биће враћена та вредност у изразу.
За аргумент редудантно је навести да може бити константна вредност, променљива, позив функције, итд. јер су све то заправо изрази. При извршењу програмског кода, у тренутку када се дође до нпр. променљиве или позива функције, доћиће до њихове евалуације и они ће суштински бити замењени својом вредношћу, било то вредност коју променљива чува или вредност коју функција враћа. Управо зато су и изрази, јер је могуће њихову вредност евалуирати (израчунати у току извршења програма) као и користити аритметичке и логичке операције над њима како би формирали комплексније математичке или логичке изразе који потом све скупа када се евалуирају дају неку вредност целокупног тог израза.
При позиву фунцкије потребно је навести тачан број аргумента који одговарају параметрима те функције, као и да тип података за сваки од аргумента одговара типу параметара.
Неки примери дефинисања функција и њихова употреба у главном програму
Шта ће бити излаз овог програма?
Функција stampajBroj
је повратног типа void
, што значи да нема повратну вредност и има само један параметар.
Функција min
је повратног типа int
и има два параметра. Њена повратна вредност биће вредност мањег параметра од два прослеђена.
Функција inkrement
наизглед нема дефинисану повратну вредност, међутим њена повратна вредност је заправо int
(када се повратна вредност функције не напише, подразумева се int
). Ова функција нема параметре, што се означава тако што се наведе void
уместо листе параметара.
static
модификатор при декларацији променљиве налаже да ће променљива задржати своју вредност по завршетку програмског блока (scope-a) у коме је декларисана. На тај начин, променљива static int i
у функцији inkrement
по завршењу функције ће заджати тренутну вредност која ће се пренети у наредно извршење функције.
Функција без параметара
У програмском језику C дефинисање функције без параметара је могуће тако што се уместо листе параметара наведе кључна реч void
.
Уколико се листа параметра остави празна, таква функција нема параметре, али могуће је навести произвољан број аргумената при позиву функције, што може довести да забуне у начину на који је фунцкија дефинисана и на начину на који је могуће ту функцију потом користити јер неће проузроковати у компајлерској грешци. Једна од ситних и чудних ствари програмског језика C...
За потребе овог курса, ово и није баш битно - користите шта год Вам је лакше.
return
наредбу је неопходно познавати за рад са функцијама. Чим се у току извршења програма наиђе на наредбу return
, функција која се тренутно извршава бива прекинута и ток извршења програма се враћа назад на место где је функција била позвана. Уколико функција има повратни тип и return
наредби се наведе аргумент у виду вредности која одговара повратном типу функције, та вредност биће враћена у изразу на место позива функције.
Разлике употребе функција на овом курсу
У програмском језику C могуће је употребити кључну реч return
на више места и било где у функцији или да је нема. Када повратни тип фунцкије није void
, return
наредби је могуће додати аргумент који ће бити вредност коју ће функција вратити. Уколико у функцији нема return
наредбе или постоји return
без аргумента, повратну вредност функције није могуће знати и може бити било која заостала насумична вредност у меморији коју је компајлер предоделио за чување повратне вредности.
На писменом делу испита овог курса обавезно је да све Ваше функције имају само један return
и то на самом крају фунцкије као завршна наредба! Додатно, уколико фунцкија није void
повратног типа, потребно је уз return
додати аргумент који ће бити повратна вредност функције и мора одговарати повратном типу функције.
Чест је случај да се тражи да функција примени неки рад на променљивама које су пренете преко параметра функције, тј. пренете преко референце или показивача. Некада се тражи да функција враћа више вредност, што технички није могуће услед лимитација програмског језика C на начин на који функције раде, али суштински је могуће употребити излазне вредности пренете преко параметара функције, тј. референце или показивача, да емулирамо тако нешто.
Показивачи су променљиве чија је вредност меморијска локација. Често се каже да показивач показује на неку другу променљиву, тј. показује на меморијску локацију неке друге променљиве. Слично као што променљива која чува низ је заправо показивач који указује на први елемент низа, тј. променљива матрице је показивач који указује на прву врсту (ред) матрице.
Како употребом показивача добијамо приступ меморијској локацији на коју нека променљива из главног програма чува вредност, онда је могуће да на исту ту меморијску локацију из функције упишемо неку вредност, која ће потом бити приступачна из главног програма такође. На тај начин могуће је да функција „врати више вредности” преко параметара.
Пошто су променљиве низова и матрица показивачи (на први елемент низа, тј. на прву врсту/ред матрице), када се низ (односно матрица) проследи као аргумент функције и потом функција изврши неке промене над елементима, те промене биће видљиве и у главном програму. Уколико је потребно да елемети низа (тј. матрице) остану непромењени у главном програму, а функција врши промене над њима, функцији је потребно или проследити копију низа (матрице) или копирати елементе у нови низ (матрицу) декларисан унутар функције и вршити рад над том копијом.
Ако је променљива a
показивач (int *a
), онда у изразу a
представљаће меморијску локацију, а *a
вредност која је ускладиштена на тој меморијској локацији (употребљавамо унарни оператор дереференцирања *
над показивачем). Вредност сачувану на тој меморијској локацији је могуће модификовати као да сматрамо да је *a
било која обична променљива, тј. могуће је доделити нову вредност као *a = 10
, инкрементовати (*a)++
, декрементовати вредност (*a)--
итд.
Показивач је могуће декларисати као int *a
, int* a
, int * a
, али и као int*a
. Није битно да ли постоји бланко знак са леве или десне стране *
. Најчешће се виђа облик int *a
због тога што је очигледније да је такво a
заправо показивач, лакше је потом запамтити синтаксу *a
за придобијање вредности на коју тај показивач указује. Такође уколико на истој линији желимо да декларишемо више показивача потребно је то чинити на следећи начин int *a, *b, *c
(опет, није битно да ли је *
одвојена бланко знаком од назива, како год је Вама лепше). Уколико би написали int* a, b, c
нпр. онда је овде само a
показивач (int*
), док су b
и c
променљиве типа int
!
Користите шта год је Вама лакше, али такође будите обазриви када читате програмске кодове да Вам не промакне *
била слепљена уз назив или тип податка. Наравно, показивач не мора да указује на тип int
, можемо имати показивач на било који тип, као и показивач на показивач, на показивач, ... (double *b
, char *c
, float **d
).
Да би показивач био користан, његова вредност треба бити постављена на вредност меморијске локације. Меморијску локацију било које променљиве (чак иако је та променљива показивач и она је ускладиштена на некој меморијској локацији па је могуће чак имати и показивач показивача показивача...) можемо добити употребом унарног оператора „адреса од” &
. Овај оператор смо често употребљавали при позиву функције scanf()
управо како би њој проследили као аргументе меморијске локације променљивих да би било могуће у њих уписати вредност коју би потом могли да користимо у главном прогрмау.
Пример употребе показивача
c
је показивач на променљиву b
. *c += 3
ће повећати за 3 вредност на коју показивач c
указује, тј. вредност променљиве b
.
Функција func()
је позвана са аргументима адреса од a
и вредност од b
, самим тим иако функција мења вредност оба параметра и a
и b
, само је вредност променљиве a
у главном програму промењена како је она једино пренета преко показивача (тј. њене меморијске локације).
Задатак 1.¶
Нацртати структурни дијаграм тока алгоритма и написати функцију int prost(int A)
на програмском језику C која одређује да ли је број \(A\) прост број. У главном програму испитати и приказати колико бројева, од укупно \(N\) бројева које задаје корисник, је просто.
Териотисање¶
Испитивање да ли је број прост је већ приказано у 1. Задатку Друге лабораторијске вежбе.
Потребно је још то написати у функцији коју ћемо потом позивати у петљи како би одређивали да ли је унети број прост и уколико јесте инкрементовати неки бројач. На крају, штампамо резултат тог бројача.
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 2.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију за замену места вредностима две целобројне променљиве \(a\) и \(b\), пренете преко параметара функције. У главном програму унети три променљиве и коришћењем формиране функције циклично их промерити за једну позицију улево. Приказати бројеве након уноса и након померања.
Териотисање¶
Како би било могуће заменити вредности променљивих функцијом, потребно је пренети променљиве путем показивача у параметру функције. То чинимо тако што дефинишемо параметре функције да су типа показивача за одговарајући тип података који нам је потребан. За овај задатак како је потребно заменити места вредностима целобројног (int
) типа, параметри ће бити int *
типа (показивач на вредност типа int
).
Пошто су показивачи меморијске локације, да би приступили вредностима на тим меморијским локацијама и извршили њихову замену, потребно је употребити унарни оператор *
(дереференцирање).
При позиву функције, потребно је проследити меморијске локације променљивих чије вредности је потребно заменити, што чинимо употребом унарног оператора &
(адреса од).
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 3.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију која одређује и приказује декадну вредност бинарног броја \(B\) прослеђеног као низ бинарних цифара, тако да је цифра најмање тежине на почетку низа. У главном програму унети бинарни број у виду низа цифара и коришћењем формиране функције приказати његов декадни еквивалент.
Териотисање¶
Како је бинарни бројни систем позициони систем и знамо да има основу \(2\), лако можемо претворити бинарне цифре запамћене у низу у декадни број.
Да би претворилин неки произбољан број неког позиционог бројног система у декадни број, то чинимо тако што би издвајали цифре тог броја редом (нпр. дељењем броја са основом бројног система све док број не постане нула) и затим сумирати производ свих цифрара и основе бројног система на степен који одговара тежини издвојене цифре. Цифра најмање тежине (\(0\)), била би цифра скроз десно, док цифра највеће тежине (један број мањи од броја цифара броја) би била цифра скроз лево.
Како већ имамо издвојене цифре запамћене у низу у поретку од цифре најмање до највеће тежине, цифре бинарног броја који је потребно претворити у декадни број су у обрнутом редоследу. Можемо се нпр. кретати од почетка низа.
У почетном тренутку можемо одредити да је резултујући декадни број \(0\) (неутрални елемент за сабирање), а множицал цифара једнак \(1\) (за \(2^0 = 1\)). За сваку цифру редом у низу помножити цифру са множиоцем и додати производ суми резултујућег декадног броја, затим множилац помножити са \(2\) како би припремили множилац за наредну цифру. На крају функције вратити добијен декадни број.
У главном програму потребно је унети број цифара, затим унети бинарне цифре броја у обрнутом редоследу (од најмање ка највеће тежине цифара) и запамтити их у низу. Можемо користити формат за унос цифара %1d
како би ограничили да је могуће унети само једноцифрене целе бројеве, самим тим није обавезно при уносу одвајати цифре бланко знацима или новим редом.
Позвати функцију са аргументима низ бинарних цифара и број бинарних цифара, потом штампати њен резултат.
Дијаграм тока алгоритма¶
Изворни код програма¶
Модификација задатка / II начин
Ово решење је могуће применити и када су цифре у низу запамћене од најмање до највеће тежине, само је потребо тада петљу кореговати тако да се креће од \(N - 1\) до \(0\).
Шта уколико је низ \(B\) такав да се на првој позицији налази цифра највеће тежине, притом да низ обилазимо исто од његовог почетка?
Уколико резултујући декадни број множимо са \(2\), потом само додамо тренутну цифру суми, можемо срачунати декадни еквивалент бинарног броја и када се крећемо цифрама броја од највеће до најмање тежине. Притом, сваки пут када се резултујући број помножи са \(2\), цифра претходно додата цуми биће одгурнута улево (посматрајте као да радите са декадним цифрама и множите са \(10\)), самим тим све цифре на самом крају дођу на своја места и дају адекватан резултат, као да смо их одмах множили са \(2\) на степен тежине те цифре.
Задатак 4.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију minmax
која одређује и враћа индексе највеће и најмање вредности у низу \(X\) са \(N\) елемената. У главном програму унети број елемената и елементе низа, и коришћењем формиране функције одредити и приказати најмањи и највећи елемент низа.
Териотисање¶
Како је потребно да функција враћа две вредност, потребно је да вредности преносимо преко параметара функције употребом показивача. Параметри за индекс највећег и најмање вредности нека су maxI
и minI
респективно, а њихов тип података биће int *
(показивач на вредност типа int
). Како су они показивачи, да би приступили вредности на коју они указују и како би је модификовали, потребно је употребити унарни оператор *
(дереференцирање). У почетном тренутку можемо да претпоставимо да је елемент са индексом \(0\) и најмањи и највећи елемент, потом у петљи од елемента са индексом \(1\) до крајњег елемента са индексом \(N-1\) проваравати да ли је тренутни мањи од претходно одређеног минималног, ако јесте његов индекс постаје нови најмањи индекс, као и проверавати да ли је тренутни елемент већи од претходно одређеног максималног, ако јесте његов индекс постаје нови највећи индекс.
У главном програму потребно је декларисати променљиве које ће нам служити да чувају повратне вредности функције које ћемо пренети преко параметара. При позиву функције меморијске локације тих променљивих је потребно проследити као аргументе употребом унарног оператором &
(адреса од). Штампати најмањи и највећи елемент.
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 5.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију delioci
која враћа низ са свим делиоцима природног броја пренетог преко параметара функције. У главном програму унети природан број \(B\) и коришћењем формиране функције одредити и приказати све његове делиоце.
Териотисање¶
Функцију можемо дефинисати са параметрима: број чије делиоце је потребно одредити и низ у којем ћемо сачувати делиоце тог броја. Повратна вредност функције може бити int
, самим тим функција би враћала број одређених делиоца као повратну вредност. Могуће је да је функција типа void
, а потом да је величина низа, тј. број одређених делиоца такође прослеђен преко параметра функције, али као показивач (int *n
) да би у функцији било могуће ту вредност из главног програма модификовати.
У функцији, петљом проласком кроз бројеве од 1 до самог броја чије делиоце тражимо, и употребом модуло оператора (остатка при дељењу), одређујемо делилац, сачувамо га у низу и повећавамо број одређених делиоца (тј. величину низа).
На крају функције вратити број одређених делилаца.
У главном програму позвати функцију са аргументима унетог природног броја и низа у коме ће се његови делиоци бити смештени, а повратну вредност функције доделити променљивој која ће представљати величину низа. Бројачком петљом потом штампати елементе низа.
Дијаграм тока алгоритма¶
Изворни код програма¶
Потенцијална оптимизација
Уместо да се проверавају делиоци неког броја \(a\) до самог тог броја, трагање се може ограничити до \(\sqrt{a}\), како су делиоци спарени (исти разлог иза оптимизације код провере да ли је број прост). Делиоци броја \(100\) биће \(1, 2, 4, 5, 10, 20, 25, 50, 100\) и они су на неки начин спарени - \(1\) и \(100\), \(2\) и \(50\), \(4\) и \(25\), \(5\) и \(20\) и \(10\) спарен сам са собом. Производ ових парова даће дељник, у овом примеру број \(100\), стога трагање можемо ограничити до половине делилаца једнако корену дељеника, јер се делиоци спарују први и задњи, други и предзадњи, ... С тим, пар било ког делиоца можемо одредити дељењем дељеника и делиоца. Применом овог знања можемо на тај начин смањити време извршења програма. Једини trade-off овог приступа је што редослед делиоца неће бити уређен, већ ће у низу бити упамћени редом као први, задњи, други, предзадњи, трећи, ...
Задатак 6.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију sortiranje
која врши уређивање елемената низа пренетог преко параметара функције у нерастући или неопадајући редослед, у зависности од вредности целобројног параметара smer
. У главном програму унети број елемената и елементе низа, и коришћењем функције уредити елементе низа у нерастући, а затим у неопадајући редослед. Приказати низ пре и после сваког уређења.
Териотисање¶
У функцији можемо имплементовати алгоритам за сортирање елемената низа као по обичају што смо то чинили до сада. Једина разлика биће услов, како желимо да зависно од параметра smer
применимо један од услова, употребом мало математичке логике можемо бирати ако је \(smer = 1 \land a(i) < a(j)\), онда заменити елементе \(a(i)\) и \(a(j)\), или ако је \(smer = -1 \land a(i) > a(j)\) онда такође заменити места елементима. Дакле ове услове можемо спојити дисјункцијом и проблем решен.
Из главног програма унети низ, позвати функцију са смером \(1\), штампати низ, потом позвати функцију још једном са смером \(-1\), поново штампати низ.
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 7.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију која приказује елементе матрице пренете преко параметара функције. Приликом приказа, коришћењем карактера |
формирати матрицу као у примеру. У главном програму унети матрицу \(A_{N \times N}\) и приказати је употребом формиране функције. Пример:
Териотисање¶
За овај задатак штампање матрице је потребно написати у фунцкцији, с тим да пре првог елемента врсте и након задњег елемента врсте буде одштампан карактер |
, додатно можемо користити форматирано штампање бројева како би сви били поравнани подједнако као у датом примеру. Вероватно није битно са колико бланко знака слева поравњате елемент при штампању, у овом решењу је то чињено са 5 бланко знака употребом форматираног штампања %5d
.
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 8.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију која одређује и враћа суму вредности елемената испод споредне дијагонале целобројне матрице \(A_{N \times N}\), прослеђене преко параметара функције. У главном програму унети две целобројне матрице \(B\) и \(C\) и приказати ону чија је сума елемената испод споредне дијагонале већа.
Териотисање¶
Да би одредили суму елеменаа испод споредне дијагонале, поставићемо неку променљиву сума на \(0\) (неутрални елемент за сабирање). Како би заобишли проверу да ли се елемент налази испод споредне дијагонале можемо угњеждене петље за пролазак кроз елементе матрице модификовати тако да је спољашња петља врста \(i = 1, N - 1\) (у врсти са индексом \(0\) нема елемената испод споредне дијагонале), а унутрашња петља колона \(j = N - i, N - 1\). Елементе на позицијама \((i, j)\) додати суми. На крају функције вратити суму.
Додатно можемо написати функције са унос и штампање матрице како би скратили код главног програма (main
функцију).
Функцију можемо употребити у услову поређења како би поредили суму елемената испод споредне дијагонале једне и друге матрице и на основу тога штампати једну или другу.
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 9.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију exp(x, E)
која одређује вредност суме:
Израчунавање прекинути када релативна вредност прираштаја суме постане мања од задате врености \(E\). У главном програму учитати вредности параметара \(x\) и \(E\) и коришћењем формиране функције одредити и приказати вредности дате суме.
Териотисање¶
Рачунање суме можемо извршити употребом repeat-until петље, тј. do-while
петље у програмском језику C која ће имати обрнут услов у односу на repeat-until
петљу у алгоритму. Пошто треба рачунање прекинути када је прираштај суме мањи од вредности \(E\) потребно је да чувамо преходну вредност суме. Притом први корак у петљи јесте постављање нове претходне вредности суме једнаку тренутној вредности суме, затим суми додати \(\frac{x}{n}\), инкрементовати \(n\) за један. Петља се извршава све док апсолутна вредност разлике тренутне и претходне вредности суме је већа од вредности \(E\) (тј. петља се завршава у тренутку када апсолутна вредност разлике тренутне и претходне вредност постане мања или једнака вредност \(E\)).
Функција за рачунање апсолутне вредност је у овом решењу такође имплементована, али није грешка искористити fabsl()
функцију из стандардне библиотеке дефинисану у math.h
заглављу.
Дијаграм тока алгоритма¶
Изворни код програма¶
Задатак 10.¶
Нацртати структурни дијаграм тока алгоритма и на програмском језику C написати функцију која транспонује матрицу \(A_{N \times N}\), прослеђену преко параметара функције. У главном програму унети целобројну матрицу и приказати унету и транспоновану матрицу употребом формиране функције.
Териотисање¶
Матрицу је могуће транспоновати тако што се крећемо елементима изнад (или испод) главне дијагонале и затим заменимо места елементима \(a(i,j)\) и \(a(j,i)\). Елементи на главној дијагонали остаће на свом месту и не обухватамо их у петљи, а не пролазимо кроз целу матрицу како би заобишли да елементи двапут замене места и самим тим матрица би остала непромењена.
Додатно можемо написати функцију за штампање матрице како би скратили код главног програма (main
функцију).