Глава 10. Функции

В настоящата глава ще се запознаем с функциите и ще научим какво представляват те, както и кои са базовите концепции при работа с тях. Ще научим защо е добра практика да ги използваме, как да ги дефинираме и извикваме. Ще се запознаем с параметър и връщана стойност от функция, както и как да използваме тази връщана стойност. Накрая на главата, ще разгледаме утвърдените практики при използването на функции.

Какво е "функция"?

До момента установихме, че при писане на програма, която решава даден проблем, ни улеснява това, че разделяме задачата на части. Всяка част отговаря за дадено действие и по този начин не само ни е по-лесно да решим задачата, но и значително се подобрява както четимостта на кода, така и проследяването за грешки.

В контекста на програмирането, функция (метод) се нарича именувана група от инструкции, които изпълняват дадена операция (функционалност). Тази група от инструкции е логически отделена и именувана, така че изпълнението на инструкциите в групата може да бъде стартирано чрез извикване на нейното име в хода на изпълнението на програмата. Стартирането на изпълнението на инструкциите във функцията се нарича извикване на функцията (на английски function call или invoking a function).

Една функция може да бъде извикана толкова пъти, колкото ние преценим, че ни е нужно за решаване на даден проблем. Това ни спестява повторението на един и същи код няколко пъти, както и намалява възможността да пропуснем грешка при евентуална корекция на въпросния код.

Ще разгледаме два типа функции - "прости" (без параметри) и "сложни" (с параметри).

Прости функции

Простите функции отговарят за изпълнението на дадено действие, което спомага за решаване на определен проблем. Такива действия могат да бъдат например разпечатване на даден низ в конзолата, извършване на някаква проверка, изпълнение на цикъл и други.

Нека разгледаме следния пример за проста функция:

Функцията обаче има още една част, независима от горната:

За разликите между двете - структурата, употребата и значението, им ще коментираме подробно в главата.

Тази функция има задачата да отпечата заглавие, което представлява поредица от символа -. Поради тази причина името й е printHeader. Кръглите скоби ( и ) винаги следват името, независимо как сме именували функцията. По-късно в тази глава ще разгледаме утвърдени практики за именуване на функции, а за момента ще отбележим само, че е важно името да описва действието, което тя извършва. Ще поговорим и за ключовата дума void.

Тялото на функцията, което се намира между къдравите скоби { и }, съдържа програмния код (инструкциите), който решава проблема, описан от името ѝ. Тялото на функцията се изписва по-навътре, обикновено 4 интервала (една табулация), които го обособяват като отделен блок инструкции, прилежащи към функцията.

Защо да използваме функции?

Дотук установихме, че функциите спомагат за разделянето на обемна задача на по-малки части, което води до по-лесно решаване на въпросното задание. Това прави програмата ни не само по-добре структурирана и лесно четима, но и по-разбираема.

Чрез функциите избягваме повторението на програмен код. Повтарящият се код е лоша практика, тъй като силно затруднява поддръжката на програмата и води до грешки. Ако дадена част от кода ни присъства в програмата няколко пъти и се наложи да променим нещо, то промените трябва да бъдат направени във всяко едно повторение на въпросния код. Вероятността да пропуснем място, на което трябва да нанесем корекция, е много голяма, което би довело до некоректно поведение на програмата. Това е причината, поради която е добра практика, ако използваме даден фрагмент код повече от веднъж в програмата си, да го дефинираме като отделна функция.

Функциите ни предоставят възможността да използваме даден код няколко пъти. С решаването на все повече и повече задачи ще установите, че използването на вече съществуващи функции спестява много време и усилия. Нещо повече - всеки път, когато пишем програма на C++, използваме главната функция main().

Деклариране и дефиниция на функции

В езика за програмиране C++ съществува разлика между понятията декларация и дефиниция на функция:

  • Декларацията на функция информира компилатора или интерпретатора, че функцията със съответното име и параметри съществува, без да съдържа имплементация (тялото на функцията).
  • Дефиницията на функция съдържа нейната имплементация (тялото ѝ).

Декларация

Нека разгледаме следния пример за деклариране на функция, намираща лицето на квадрат по зададена страна num:

Следната декларация е еквивалентна на горната, като това ще коментираме по-надолу в главата:

Нека обърнем внимание на следното - даденият програмен фрагмент само съобщава на компилатора или интерпретатора, че в програмата ще бъде дефинирана и използвана функцията getSquare().

Декларирането на една функция се осъществява след using namespace std; и преди главната функция main(). В примера тук са показани декларациите на функциите getSquare(), multiply() и printHeader():

Задължителните елементи при деклариране на функция:

  • Тип на връщаната стойност. В случая типът е double, което заявява, че функцията getSquare(…) ще върне резултат, който е от тип double. Връщаната стойност може да бъде както int, double, char и т.н., така и void. Ако типът е void, то това означава, че функцията не връща резултат, а само изпълнява дадена операция.
  • Име на функцията. Името на функцията е определено от нас, като не забравяме, че трябва да описва операцията, която е изпълнявана от кода в тялото ѝ. В примера името е getSquare, което ни указва, че задачата на тази функция е да изчисли лицето на квадрат.
  • Списък с параметри. Декларира се между скобите ( и ), които изписваме след името на функцията. Тук изброяваме поредицата от параметри, които тя ще използва, като се подразбира, че могат да бъдат от различен тип (int, double, char и т.н.). Интересното тук е, че може (дори обикновено така се прави) да се запише само типът на използваните параметри (т.е. да не се идентифицират). Това е допустимо само при декларирането на функция. Може да присъства само един параметър, няколко такива или да е празен списък. Ако няма параметри, то ще запишем единствено скобите (). В конкретния пример декларираме параметъра double num.
  • Накрая поставяме точка и запетая ;.
Задължително се поставя знакът "точка и запетая" ; в края на декларирането на дадена фукция.

При декларацията на функции е важно да спазваме последователността на основните ѝ елементи - първо изписваме тип на връщаната стойност, след това име на функцията и списък от параметри, ограден с кръгли скоби (), и знакът ; в края.

Дефиниция

След като сме декларирали функцията, следва нейната дефиниция. Дефиницията на функция представлява описването на нейното тяло (нейната имплементация). Тялото съдържа кода (програмен блок), който реализира логиката на функцията. В показания пример изчисляваме лицето на квадрат, а именно num * num. Това е дефиницията на декларираната по-горе функция getSquare(…):

Дефинициите на функции се осъществяват след главната main() функция. След малко ще разгледаме цялостен пример.

Задължителните елементи при дефиниране на функция:

  • Тип на връщаната стойност. Типът трябва да бъде съобразен с типа на връщаната стойност при декларацията на функцията, т.е. той трябва да е същият. Ако при декларацията е записано, че функцията ще връща стойност от тип int, то и при дефиницията ще трябва типът да е int.
  • Име на функция. Името трябва да е същото като при декларацията на функцията.
  • Списък с параметри. Декларира се между скобите ( и ). Отново се съобразяваме със записаното при декларацията на функцията. Ако има повече от един параметър, то ги записваме в същия ред, както при декларацията на функцията. Например, ако сме декларирали функция function(double, int, double, char), то при нейната дефиниция ще подредим параметрите в същата последователност - double, int, double, char. Освен това тук задължително трябва да се идентифицират въпросните параметри (за разлика от декларацията, където това можеше да се пропусне). Следователно примерна идентификация на параметрите би изглеждала по следния начин: (double var1, int var2, double var3, char var4).
  • Имплементация (тяло). Записва се в областта, отделена от къдравите скоби { и }. Тази област се създава след списъка с параметри. В тялото на функцията описваме алгоритъма (инструкциите), по който тя решава даден проблем, т.е. тялото съдържа кода, който реализира логиката на функцията. Ако функцията връща стойност, то в края на имплементацията ще добавим оператор return, а след него това, което ще връщаме, и знак ;. За връщащи и невръщащи стойности ще говорим по-нататък.
  • Тук не се поставя ; в края, както при декларирането.

Видове променливи

Когато декларираме дадена променлива в тялото на една функция, я наричаме локална променлива за функцията. Областта, в която съществува и може да бъде използвана тази променлива, започва от реда, на който сме я декларирали и стига до затварящата къдрава скоба } на тялото на функцията. Тази област се нарича област на видимост на променливата (variable scope). Когато декларираме дадена променлива външно от всички функции в дадена програма (включително и от main() функцията), я наричаме глобална променлива за програмата. Тя може да бъде достъпвана във всяка част на програмния код.

Параметри на функции

Това са данните (променливи или дори константи), които се подават на функцията по време на нейното извикване, за да ги обработи, връщайки стойност или изпълнявайки някакво действие с тях. Трябва да се внимава параметрите да бъдат подредени в една и съща последователност при декларирането и дефинирането на функцията.

Извикване на функции

Извикването на функция представлява стартирането на изпълнението на кода, който се намира в тялото на функцията. Това става като изпишем името ѝ, последвано от кръглите скоби () и знака ; за край на реда. Ако функцията ни изисква входни данни, то те се подават в скобите (), като последователността на фактическите параметри трябва да съвпада с последователността на подадените при декларирането на функцията. Ето един пример:

Дадена функция може да бъде извикана от няколко места в нашата програма. Единият начин е да бъде извикана от главната функция:

Функция може да бъде извикана и от тялото на произволна друга функция:

Важно: Разгледаните тук функции не връщат стойност (резултат). Тях ще ги разгледаме малко по-късно, в секция Връщане на резултат от функция.

Съществува вариант функцията да бъде извикана от собственото си тяло. Това се нарича рекурсия и можете да намерите повече информация за нея в Wikipedia или да потърсите сами в Интернет.

Обобщение

Дотук подробно разгледахме елементите на една функция в езика C++, както и разпределението им в кода на програмата. Оказва се, методиката за работа с функции, която разгледахме, е стандартна, но не е единствената. Ще разгледаме два различни подхода, като единия го разгледахме подробно.

Нека напишем програма, използваща функция (printSentence()) за извеждане на изречението I am learning functions in C++.. Използваме познатия ни вече алгоритъм:

  • Декларираме функцията printSentence() преди main() функцията .
  • Дефинираме функцията printSentence() след main() функцията.
  • Извикваме функцията printSentence() в главната main().
// First approach
#include <iostream>    
using namespace std;

// Declaration
void printSentence();  

int main() {
    // Function call
    printSentence();   
    return 0;
}

// Definition
void printSentence() {              
    cout << "I am learning functions in C++.";
}

Може да тествате примера онлайн: https://repl.it/@vncpetrov/printSentence1.

Какъв е другият подход? Нека разгледаме следната програма, решаваща същата задача:

// Second approach
#include <iostream>    
using namespace std;

// Declaration and definition at same time
void printSentence() {                     
    cout << "I am learning functions.";    
}  

int main() {
    // Function call
    printSentence();
    return 0;
}

Може да тествате примера онлайн: https://repl.it/@vncpetrov/printSentence2-1.

Оказва се, че резултатът от двете програми е еднакъв. Разликата е само, че при втория подход декларирането и дефинирането на функцията са слeти в едно, като това се случва преди главната функция main(). Факт е, че вторият подход изисква по-малко писане на код, но в практиката често се използва първият подход, тъй като дава по-добра четимост на програмата, както и поради други причини.

В настоящата книга се използва първият подход (т.е. с отделена декларация и дефиниция). Това е и препоръчителният подход.

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

Нека да поясним още нещо - вместо деклариране на функция (както дотук използвахме за яснота), ще казваме прототип на функция. Самото наименование ни подсказва изведеното по-горе, а именно че преди функция main() съобщаваме за дадена функция (правим й прототип), създаваме я (пишем нейното тяло) след функция main(), а я използваме (извикваме) или в main(), или в друга функция. Единствената функция, на която не правим прототип, е главната main().

Пример: празна касова бележка

Да се напише функция, който печата празна касова бележка. Функцията трябва да извиква други три функции: една за принтиране на заглавието, една за основната част на бележката и една за долната част.

Част от касовата бележка Текст
Горна част CASH RECEIPT
------------------------------
Средна част Charged to____________________
Received by___________________
Долна част ------------------------------
(c) SoftUni

Примерен вход и изход

Вход Изход
(няма) CASH RECEIPT
------------------------------
Charged to____________________
Received by___________________
------------------------------
(c) SoftUni

Насоки и подсказки

Първата ни стъпка е да създадем void функция за принтиране на заглавната част от касовата бележка (header). Под създаване на функция трябва да разбираме писане на прототип и дефиниция отделно. Нека й дадем смислено име, което описва кратко и ясно задачата ѝ, например printReceiptHeader(). Ето как ще изглежда нейната дефиниция:

Оттук нататък даваните примери в главата ще съдържат само дефиниции на функции. Оставяме читателя сам да създава техните прототипи.

Съвсем аналогично създаваме още две функции - една за разпечатване на средната част на бележката (тяло) printReceiptBody() и една за разпечатване на долната част на бележката (footer) printReceiptFooter(). След това създаваме и още една функция (printReceipt()), която ще извиква една след друга трите функции, които написахме до момента:

Накрая ще извикаме функцията printReceipt() от тялото на главната main() функция за нашата програма:

Броят на съответните символи в изхода трябва да е както в показания по-горе пример!

Тестване в Judge системата

Програмата с общо пет функции, които се извикват една от друга, е готова и можем да я изпълним и тестваме, след което да я пратим за проверка в Judge системата: https://judge.softuni.org/Contests/Practice/Index/1374#0.

Функции с параметри

Много често в практиката, за да бъде решен даден проблем, функцията, с чиято помощ постигаме това, се нуждае от допълнителна информация, която зависи от задачата ѝ. Именно тази информация представляват параметрите на функцията и нейното поведение зависи от тях.

Използване на параметри във функциите

Както отбелязахме по-горе, параметрите освен нула на брой, могат също така да са един или няколко. При декларацията им ги разделяме със запетая. Те могат да бъдат от различен тип данни (int, string и т.н.), а по-долу е показан пример как точно ще бъдат използвани от функцията.

Създаваме прототип на функцията, като най-малкото, което упоменаваме в списъка с параметри, е типът данни на параметрите, т.е. не е задължително те да бъдат именувани. При дефиницията обаче това е задължително. Там е и мястото, където се записва тялото на функцията.

След това извикваме функцията като ѝ подаваме конкретни стойности:

При декларирането на параметри можем да използваме различни типове променливи, като трябва да внимаваме всеки един параметър да има типиме при дефиницията). Важно е да отбележим, че при последващото извикване на функцията, трябва да подаваме стойности за параметрите в реда, в който са декларирани. Ако имаме функция с параметри int и char в тази последователност, при извикването ѝ не може да подадем първо стойност за char и след това за int. Единствено може да разменяме местата на подадените параметри, ако изрично изпишем преди това името на параметъра, както ще забележим малко по-нататък в един от примерите. Това като цяло не е добра практика!

Нека разгледаме пример за декларация на функция, която има няколко параметъра от различен тип:

Пример: знак на цяло число

Да се създаде функция, която печата знака на цяло число n.

Примерен вход и изход

Вход Изход
2 The number 2 is positive.
-5 The number -5 is negative.
0 The number 0 is zero.

Насоки и подсказки

Първата ни стъпка е създаването на функция и даването ѝ на описателно име, например printSign(…). Тази функция ще има само един параметър (от тип int) - числото, чийто знак искаме да проверим. Следващата ни стъпка е имплементирането на логиката, чрез която програмата ни ще проверява какъв точно е знакът на числото. От примерите виждаме, че има три случая - числото е по-голямо от нула, равно на нула или по-малко от нула, което означава, че ще направим три проверки в тялото на функцията:

Следващата ни стъпка е да прочетем входното число и да извикаме новата функция от тялото на main() функцията:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#1.

Може да ви направи впечатление, че променливата от main(), която подаваме като параметър на printSign(…) функцията, е с име n, а във printSign(…) е именувана a. Можехме във въпросната функция да използваме параметър, който носи същото име като променливата в main(), т.е. n.

В C++ е задължително при извикване на функции списъкът с параметри да се запълва, тъй както са декларирани самите функции - същият брой параметри, същите типове данни и подредба.

Пример: принтиране на триъгълник

Да се създаде функция, която принтира триъгълник, както е показано в примерите.

Примерен вход и изход

Вход Изход Вход Изход
3 1
1 2
1 2 3
1 2
1
4 1
1 2
1 2 3
1 2 3 4
1 2 3
1 2
1

Насоки и подсказки

Преди да създадем функция за принтиране на един ред с дадени начало и край, прочитаме входното число от конзолата. След това избираме смислено име за функцията, което описва целта й, например printLine(…), и я имплементираме:

От задачите за рисуване на конзолата си спомняме, че е добра практика да разделяме фигурата на няколко части. За наше улеснение ще разделим триъгълника на три части - горна, средна и долна част.

Следващата ни стъпка е с цикъл да разпечатаме горната половина от триъгълника:

След това разпечатваме средната линия:

Накрая разпечатваме долната част от триъгълника, като този път стъпката на цикъла намалява.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#2.

Пример: рисуване на запълнен квадрат

Да се нарисува на конзолата запълнен квадрат със страна n, както е показно в примерите.

Примерен вход и изход

Вход Изход
4 --------
-\/\/\/-
-\/\/\/-
--------
Вход Изход
5 ----------
-\/\/\/\/-
-\/\/\/\/-
-\/\/\/\/-
----------

Насоки и подсказки

Първата ни стъпка е да прочетем входа от конзолата. След това трябва да създадем функция, която ще принтира първия и последен ред, тъй като те са еднакви. Нека не забравяме, че трябва да ѝ дадем описателно име и да ѝ зададем като параметър дължината на страната:

Следващата ни стъпка е да създадем функция, която ще рисува на конзолата средните редове. Отново задаваме описателно име, например printMiddleRow(…):

Нека обърнем внимание, че за да се отпечата символът "\", трябва да се въведе в програмата "\\" (т.е трябва да го екранираме). Накрая извикваме създадените функции в главния main() функция на програмата, за да нарисуваме целия квадрат:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#3.

Връщане на резултат от функция

До момента разгледахме функции, които извършват дадено действие, например отпечатване на даден текст, число или фигура на конзолата. Освен този тип функции, съществуват и такива, които могат да връщат някакъв резултат от своето изпълнение - например резултатът от умножението на две числа. Именно тези функции ще разгледаме в следващите редове.

Тип на връщаната от функцията стойност

До сега разглеждахме примери, в които при декларация на функции използвахме ключовата дума void, която указва, че функцията не връща резултат, а изпълнява определено действие:

Ако заменим ключовата дума void с някакъв тип данни, то това ще укаже на програмата, че функцията трябва да върне някаква стойност от указания тип. Тази върната стойност може да бъде от всякакъв тип – int, char, double и т.н.

За да върне една функция резултат, е нужно да напишем очаквания тип на резултата при декларацията на функцията на мястото на void.

Важно е да отбележим, че резултатът, който се връща от функцията, може да е от тип, съвместим с типа на връщаната стойност на функцията. Например, ако декларираният тип на връщаната стойност е double, то може да върнем резултат от тип int.

Оператор return

За да върнем резултат от функция, на помощ идва операторът return. Той трябва да бъде използван в тялото на функцията и указва на програмата да спре изпълнението си и да върне на извиквача на функцията определена стойност, която се определя от израза след въпросния оператор return.

Операторът return може да бъде използван и във функции, които не връщат резултат (void функции). След оператора не трябва да има израз, който да бъде върнат. Тогава самата функция ще спре изпълнението си, без да връща стойност. В този случай употребата на return е единствено за излизане от функцията. Възможно е и операторът return да бъде използван на повече от едно място в тялото на функцията.

В примера по-долу имаме функция, която сравнява две числа и връща резултат съответно -1, 0 или 1 според това дали първият аргумент е по-малък, равен или по-голям от втория аргумент, подаден на функцията. Функцията използва ключовата дума return на три различни места, за да върне три различни стойности според логиката на сравненията на числата:

Кодът след return е недостъпен

След оператора return, в текущия блок, не трябва да има други редове код, тъй като изпълнението на функцията се прекратява и програмата продължава от мястото, откъдето е извикана функцията. Ако след оператора return има други инструкции, то те няма да бъдат изпълнени. Някои редактори, в това число и Visual Studio, ще покажат предупреждение, съобщавайки ни, че е засечен код, който не може да бъде достъпен:

В програмирането не може да има два пъти оператор return един след друг, защото изпълнението на първия няма да позволи да се изпълни вторият.

Понякога програмистите се шегуват с фразата “пиши return; return; и да си ходим”, за да обяснят, че логиката на програмата е объркана.

Употреба на връщаната от функцията стойност

След като дадена функция е изпълнена и върне стойност, то тази стойност може да се използва по няколко начина. Първият е да присвоим резултата като стойност на променлива от съвместим тип:

Вторият е резултатът да бъде използван в израз:

Третият е да подадем резултата от работата на функцията към друга функция:

Пример: пресмятане на лицето на триъгълник

Да се напише функция, която изчислява лицето на триъгълник по дадени основа и височина и връща стойността му.

Примерен вход и изход

Вход Изход
3
4
6

Насоки и подсказки

Първо създаваме функция, която да изчислява лицето на базата на две променливи - дължината на страната length и височината height. Този път внимаваме при декларацията да подадем коректен тип данни, който искаме функцията да върне, а именно float (може да бъде и double):

Следващата ни стъпка е да прочетем входните данни и да извикаме новата функция с тях. Резултатът записваме в подходяща променлива и извеждаме на екрана:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#4.

Пример: степен на число

Да се напише функция, която изчислява и връща резултата от повдигането на число на дадена степен.

Примерен вход и изход

Вход Изход Вход Изход
2
8
256 3
4
81

Насоки и подсказки

Първата ни стъпка отново ще е да прочетем входните данни от конзолата. Следващата стъпка е да създадем функция, която ще приема два параметъра (числото и степента) и ще връща като резултат число от тип double:

Задачата може да се реши, като се използва функцията pow(…) от библиотеката cmath. Ако тя се използва, препоръчително e идентификаторът pow на параметъра да бъде променен, за да не се получи объркване в програмата. След като сме направили нужните изчисления, ни остава да извикаме дефинираната функция и да отпечатаме резултата.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#5.

Функции, връщащи няколко стойности

Понякога се налага една функция да връща повече от една стойност. Разбира се, очевидно е, че такава функция би била сложна и може да бъде разделена на отделни функции. Но все пак, ако трябва една функция да връща няколко стойности, то се нуждаем от друг подход. Той може да бъде свързан с използването на масиви, вектори и т.н., но те не са обект на изучаване в настоящата книга. Нека обаче да разгледаме поне един универсален метод за групиране на данни - ще въведем понятието структури (или структурен тип данни).

Структури

Ако декларираме една структура, то това означава, че създаваме съвкупност от променливи, свързани логически, разбира се, които също се декларират. Ето един пример за структура:

Променливите name, age, sex и grade са елементи на структурата student. Декларацията на student се прави преди главната main функция. Тялото на структурата се намира между къдравите скобите { и }, като след затварящата скоба } задължително се поставя точка и запетая ;. А как се ползва в програмата?

Това, което правим в main(), е деклариране на променлива pupil от структурен тип данни student, който пък, от своя страна, вече е деклариран преди main(). Въвежданите след това стойности всъщност са елементите на pupil. До тях достигаме, като изпишем името на декларираната от нас променлива от структурен тип данни (в случая pupil), след това поставим точка . и накрая изпишем желания от нас елемент (например name). Така, за да въведем името на дадения ученик, използваме pupil.name.

Важно е да отбележим, че действия могат да бъдат извършвани само с елементите на структурите, но не и с имената на самите структури.

Структури и функции

Ето как ще изглежда прототипът на една функция, използваща променлива от структурен тип данни:

А дефиницията:

В случая това е функция, която ще върне променлива от структурен тип данни student. По този начин функцията връща четири стойности (за името, възрастта, пола и класа на ученика).

Нека читателят направи самостоятелно цялата програма, използваща функция, която въвежда четирите компонента от характеристиката на един ученик, а след това да допише програмата така, че функцията да се изпълнява за всеки ученик от едно училище с n на брой ученици.

Варианти на функции

В много езици за програмиране една и съща функция може да е декларирана в няколко варианта с еднакво име и различни параметри. Това е известно с термина "function overloading". Сега нека разгледаме как се пишат подобни функции (overloaded functions).

Сигнатура на функцията

В програмирането начинът, по който се идентифицира една функция, е чрез двойката елементи от декларацията ѝ – име на функцията и списък от неговите параметри. Тези два елемента определят нейната спецификация, т. нар. сигнатура на функцията:

В този пример сигнатурата на функцията е нейното име print, както и нейният параметър char symbol.

Ако в програмата ни има функции с еднакви имена, но с различни сигнатури, то казваме, че имаме варианти на функции (function overloading).

Варианти на функции

Както споменахме, ако използваме едно и също име за няколко функции с различни сигнатури, то това означава, че имаме варианти на функция. Кодът по-долу показва как три различни функции могат да са с едно и също име, но да имат различни сигнатури и да изпълняват различни действия:

Сигнатура и тип на връщаната стойност

Важно е да отбележим, че връщаният тип като резултат на функцията не е част от сигнатурата му. Ако връщаната стойност беше част от сигнатурата на функцията, то няма как компилаторът да знае коя функция точно да извика.

Нека разгледаме следния пример - имаме две функции с различен тип на връщаната стойност. Въпреки това Visual Studio ни показва, че има грешка, защото сигнатурите и на двете са еднакви. Съответно при опит за извикване на функция с име print(…), компилаторът не би могъл да прецени коя от двете функции да изпълни:

Пример: по-голямата от две стойности

Като входни данни са дадени две стойности от един и същ тип. Стойностите могат да са от тип int, char или string. Да се създаде функция getMax(…), която връща като резултат по-голямата от двете стойности.

Примерен вход и изход

Вход Изход Вход Изход Вход Изход
int
2
16
16 char
a
z
z string
Ivan
Todor
Todor

Насоки и подсказки

За да създадем тази функция, първо трябва да създадем три други функции с едно и също име и различни сигнатури. Първо създаваме функция, която ще сравнява цели числа:

Следвайки логиката от предходната функция, създаваме такава със същото име, която обаче ще сравнява символи:

Следващата функция, която трябва да създадем, ще сравнява стрингове. Тук логиката ще е малко по-различна, тъй като стойностите от тип string не позволяват да бъдат сравнявани чрез операторите < и >. Ще използваме функцията compare(…), която връща числова стойност: по-голяма от 0 (сравняваният обект е по-голям), по-малка от 0 (сравняваният обект е по-малък) и 0 (при два еднакви обекта). За да използваме функцията, трябва да бъде декларирана библиотеката string:

Последната стъпка е да прочетем входните данни, да използваме подходящи променливи и да извикаме функцията getMax(…) от тялото на функцията main():

Важно е да се отбележи, че един стринг може да бъде съставен от няколко думи, между които има интервал (space). Например низът "Software University" е валиден стринг. За да можем, обаче да прочетем такъв низ в C++ е необходимо да използваме различен от досегашния подход - чрез функцията getline(…):

string str;
getline(cin, str);

Може да тествате примера онлайн: https://repl.it/@vncpetrov/ReadingStringWithGetline.

За да използваме функцията getline(…), трябва предварително да сме реферирали библиотеката string (в зависимост от средата за програмиране, може да бъде срещната и като cstring или string.h): #include <string>.

Заради известни съображения, на които няма да се спираме сега, ще въведем употребата на още една функция, част от подхода за прочитане на стринг, съдържащ интервали: cin.ignore(). Тази функция е необходимо да бъде извикана преди getline(…), ако преди това сме прочели стойности от друг тип данни (int, char и т.н.). Ако последователно се четат стрингове, съдържащи интервали, то между тях не е нужно да се извиква споменатата функция. Нека разгледаме фрагмент от програма, която чете и печата различни стойности, сред които стринг, съдържащ интервали:

int number;
char symbol;
string word;
string sentence;

cout << "Enter a number: ";
cin >> number;

cout << "Enter a symbol: ";
cin >> symbol;

cout << "Enter a single word: ";
cin >> word;

cin.ignore();
cout << "Enter a sentence: ";
getline(cin, sentence);

cout << number << endl;
cout << symbol << endl;
cout << word << endl;
cout << sentence << endl;

Може да тествате примера онлайн: https://repl.it/@vncpetrov/ReadingAndPrintingMultipleValues.

При закоментиране на реда cin.ignore();, програмата няма да работи както очакваме и прочитането на последната стойност ще се пропусне.

Низовете, в които има интервали, както и низовете, в които няма интервали, се сравняват по един и същ начин от функцията compare(…) - лексикографски.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#6.

Вложени функции (локални функции)

В някои езици за програмиране може една функция да бъде декларирана и дефинирана в тялото на друга функция, т.е тя е локална. В езика C++ това е недопустимо!

В езика за програмиране C++ не съществува понятието локална функция!

Именуване на функции. Добри практики при работа с функции

В тази част ще се запознаем с някои утвърдени практики при работа с функции, свързани с именуването, подредбата на кода и неговата структура.

Именуване на функции

Когато именуваме дадена функция е препоръчително да използваме смислени имена. Тъй като всяка функция отговаря за някаква част от нашия проблем, то при именуването ѝ трябва да вземем предвид действието, което тя извършва, т.е. добра практика е името да описва нейната цел.

Името започва с малка буква и трябва да е съставено от глагол или от двойка: глагол + съществително име. Форматирането на името става, спазвайки lowerCamelCase конвенцията, т.е. първата дума започва с малка буква, а всяка следваща с главна. Кръглите скоби ( и ) винаги следват името на функцията.

Всяка функция трябва да изпълнява самостоятелна задача, а името на функцията трябва да описва каква е операцията, която извършва.

Няколко примера за коректно именуване на функции:

  • findStudent
  • loadReport
  • sine

Няколко примера за лошо именуване на функции:

  • function1
  • doSomething
  • handleStuff
  • simpleFunction
  • dirtyHack

Ако не можем да измислим подходящо име, то най-вероятно функцията решава повече от една задача или няма ясно дефинирана цел. В такива случай е добре да помислим как да я разделим на няколко отделни функции.

Именуване на параметрите на функциите

При именуването на параметрите на функцията важат почти същите правила, както и при самите функции. Разликата тук е, че за имената на параметрите е добре да използваме съществително име или двойка от прилагателно и съществително име. При именуването на параметрите пак се спазва lowerCamelCase конвенцията. Трябва да отбележим, че е добра практика името на параметъра да указва каква е мерната единица, която се използва при работа с него.

Няколко примера за коректно именуване на параметри на функции:

  • firstName
  • report
  • speedKmH
  • usersList
  • fontSizeInPixels
  • font

Няколко примера за некоректно именуване на параметри на функции:

  • p
  • p1
  • p2
  • populate
  • LastName
  • last_name

Добри практики при работа с функции

Нека отново припомним, че една функция трябва да изпълнява само една точно определена задача. Ако това не може да бъде постигнато, то тогава трябва да помислим как да разделим функцията на няколко отделни такива. Както казахме, името на функцията трябва точно и ясно да описва нейната цел. Друга добра практика в програмирането е да избягваме функции, по-дълги от екрана ни (приблизително). Ако все пак кода стане много обемен, то е препоръчително функцията да се раздели на няколко по-кратки, както в следния пример:

Структура и форматиране на кода

При писането на функции трябва да внимаваме да спазваме коректна индентация (отместване навътре с една табулация или 4 интервала). В C++ неправилната индентация не би довела до некоректна работа на програмата, но прави кода по-трудно четим. Пример за правилно форматиран C++ код:

Пример за некоректно форматиран C++ код:

Когато заглавният ред на функцията е твърде дълъг, се препоръчва той да се раздели на няколко реда, като всеки ред след първия се отмества с две табулации надясно (за по-добра четимост):

Друга добра практика при писане на код е да оставяме празен ред между функциите, след циклите и условните конструкции. Също така, опитвайте да избягвате да пишете дълги редове и сложни изрази. С времето ще установите, че това подобрява четимостта на кода и спестява време.

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

Какво научихме от тази глава?

В тази глава се запознахме с базовите концепции при работа с функции:

  • Научихме, че целта на функциите е да разделят големи програми с много редове код на по-малки и ясно обособени задачи.
  • Запознахме се със структурата на функциите - как да ги декларираме, дефинираме и извикваме по тяхното име.
  • Разгледахме примери за функции с параметри и как да ги използваме в нашата програма.
  • Научихме какво представляват сигнатурата и връщаната стойност на функцията, както и каква е ролята на оператора return в функциите.
  • Запознахме се с добрите практики при работа с функции - как да именуваме тях и техните параметри, как да форматираме кода и други.

Упражнения

За да затвърдим работата с функции, ще решим няколко задачи. В тях се изисква да напишете функция с определена функционалност и след това да я извикате като ѝ подадете данни, прочетени от конзолата, точно както е показано в примерния вход и изход.

Задача: "Hello, Име!"

Да се напише функция, който получава като параметър име и принтира на конзолата "Hello, \!".

Примерен вход и изход

Вход Изход
Peter Hello, Peter!

Насоки и подсказки

Дефинираме функция printName(string name) и я имплементираме, след което в главната програма трябва да прочетем от конзолата име на човек и да извикаме функцията, като ѝ подаваме прочетеното име.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#7.

Задача: по-малко число

Да се създаде функция getMin(int a, int b), която връща по-малкото от две числа. Да се напише програма, която чете като входни данни от конзолата три числа и печата най-малкото от тях. Да се използва функцията getMin(…), която е вече създадена.

Примерен вход и изход

Вход Изход Вход Изход
1
2
3
1 -100
-101
-102
-102

Насоки и подсказки

Дефинираме функция getMin(int a, int b) и я имплементираме, след което я извикваме от главната програма, както е показано по-долу. За да намерите минимума на три числа, намерете първо минимума на първите две от тях и след това минимума на резултата и третото число:

int min = getMin(getMin(num1, num2), num3);

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#8.

Задача: повтаряне на низ

Да се напише функция repeatString(str, count), която получава като параметри променлива от тип string и цяло число n и отпечатва низа n пъти.

Примерен вход и изход

Вход Изход Вход Изход
str
2
strstr roki
6
rokirokirokirokirokiroki

Насоки и подсказки

Задачата може да бъде решена по два начина - единият е чрез void функция, другият е чрез функция, връщаща стойност от тип string. Във втория случай това означава, че ще трябва да съединяваме отделните низове. Това може да стане чрез обикновено събиране (конкатенация), както в примера долу:

string str1, str2;
cin >> str1;
cin >> str2;

str1 += str2;
cout << str1;

Може да тествате примера онлайн: https://repl.it/@vncpetrov/workWithTypeString.

В по-нататъшното си обучение в областта на C++ ще научим и други подходи при работа с низове.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#9.

Задача: n-та цифра

Да се напише функция findNthDigit(number, index), която получава число и индекс N като параметри и печата N-тата цифра на числото (като се брои отдясно наляво, започвайки от 1). След това, резултатът да се отпечата на конзолата.

Примерен вход и изход

Вход Изход Вход Изход Вход Изход
83746
2
4 93847837
6
8 2435
4
2

Насоки и подсказки

За да изпълним алгоритъма, ще използваме while цикъл, докато дадено число не стане 0. На всяка итерация от while цикъла ще проверяваме дали настоящият индекс на цифрата не отговаря на индекса, който търсим. Ако отговаря, ще върнем като резултат цифрата на индекса (number % 10). Ако не отговаря, ще премахнем последната цифра на числото (number = number / 10). Трябва да следим коя цифра проверяваме по индекс (от дясно на ляво, започвайки от 1). Когато намерим цифрата, ще върнем индекса.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#10.

Задача: число към бройна система

Да се напише функция integerToBase(number, toBase), която получава като параметри цяло число и основа на бройна система и връща входното число, конвертирано към посочената бройна система. След това, резултатът да се отпечата на конзолата. Входното число винаги ще е в бройна система 10, а параметърът за основа ще е между 2 и 10.

Примерен вход и изход

Вход Изход Вход Изход Вход Изход
3
2
11 4
4
10 9
7
12

Насоки и подсказки

За да решим задачата, ще декларираме стрингова променлива, в която ще пазим крайния резултат. След това трябва да изпълним следните изчисления, нужни за конвертиране на числото:

  • Изчисляваме остатъка от числото, разделено на основата.
  • Вмъкваме остатъка от числото в началото на низа, представящ резултата.
  • Разделяме числото на основата.
  • Повтаряме алгоритъма, докато входното число не стане 0.

При решаването на тази задача се достига до това, че трябва променлива то тип данни int да се превърне в string. Това може да стане по начина, описан по-долу. Този метод ще използваме не само за да конвертираме int, но и други типове данни (char, double и други) в string.

int number;
char symbol;
cin >> number;
cin >> symbol;

string numberAsString = to_string(number);
string symbolAsString = to_string(symbol);

cout << numberAsString << endl;
cout << symbolAsString << endl;

Може да тествате примера онлайн: https://repl.it/@vncpetrov/IntToStringConversion.

Кодът на самата задача може да бъде представен по следния начин:

string integerToBase(int, int);

int main() {
  // TODO: Implement the missing conversion logic

  return 0;
}

string integerToBase(int number, int toBase) {
  string result = "";
  while (number != 0) {
    // TODO: Implement the missing conversion logic
  }

  return result;
}

Може да тествате примера онлайн: https://repl.it/@vncpetrov/IntegerToBase.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#11.

Задача: известия

Да се напише програма, която прочита цяло число N и на следващите редове въвежда N съобщения (като за всяко съобщение се прочитат по няколко реда). За всяко съобщение може да се получат различен брой параметри. Всяко съобщение започва с message_type: success, warning или error:

  • Когато messageType е success да се четат operation + message (всяко на отделен ред, в тази последователност).
  • Когато messageType е warning да се чете само message.
  • Когато messageType е error да се четат operation + message + errorCode (всяко на отделен ред, в тази последователност).

На конзолата да се отпечата всяко прочетено съобщение, форматирано в зависимост от неговия messageType. Като след заглавния ред за всяко съобщение да се отпечатат толкова на брой символа =, колкото е дълъг съответният заглавен ред и да се сложи по един празен ред след всяко съобщение (за по-детайлно разбиране погледнете примерите).

Задачата да се реши с дефиниране на четири функции: showSuccessMessage(), showWarningMessage(), showErrorMessage() и readAndProcessMessage(), като само последната функция да се извиква от главната main() функция:

Примерен вход и изход

Вход Изход
4
error
credit card purchase
Invalid customer address
500
warning
Email not confirmed
success
user registration
User registered successfully
warning
Customer has not email assigned
Error: Failed to execute credit card purchase.
==============================================
Reason: Invalid customer address.
Error code: 500.

Warning: Email not confirmed.
=============================

Successfully executed user registration.
========================================
User registered successfully.

Warning: Customer has not email assigned.
=========================================

Насоки и подсказки

Дефинираме и имплементираме посочените четири функции в условието.

В readAndProcessMessage() прочитаме типа съобщение от конзолата и според прочетения тип прочитаме останалите данни (които може да са още един, два или три реда). След това извикваме съответната функция за печатане на съответния тип съобщение.

За да се изведе ред от символи, дълъг колкото даден низ, е необходимо първо да намерим дължината на съответния низ. Това може да се направи по следния начин:

string str = "Software University";
int length = str.size();

cout << string(length, '=') << endl;

Може да тествате примера онлайн: https://repl.it/@vncpetrov/GetStringLength.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#12.

Задача: числа към думи

Да се напише функция letterize(number), която прочита цяло число и го разпечатва с думи на английски език според условията по-долу:

  • Да се отпечатат с думи стотиците, десетиците и единиците (и евентуални минус) според правилата на английския език.
  • Ако числото е по-голямо от 999, трябва да се принтира "too large".
  • Ако числото е по-малко от -999, трябва да се принтира "too small".
  • Ако числото е отрицателно, трябва да се принтира "minus" преди него.
  • Ако числото не е съставено от три цифри, не трябва да се принтира.

Примерен вход и изход

Вход Изход Вход Изход
3
999
-420
1020
nine-hundred and ninety nine
minus four-hundred and twenty
too large
2
15
350
three-hundred and fifty
Вход Изход Вход Изход
4
311
418
509
-9945
three-hundred and eleven
four-hundred and eighteen
five-hundred and nine
too small
3
500
123
9
five-hundred
one-hundred and twenty three

Насоки и подсказки

Можем първо да отпечатаме стотиците като текст - числото / 100, след тях десетиците - (числото / 10) % 10 и накрая единиците - (числото % 10).

Първият специален случай е когато числото е точно закръглено на 100 (напр. 100, 200, 300 и т.н.). В този случай отпечатваме "one-hundred", "two-hundred", "three-hundred" и т.н.

Вторият специален случай е когато числото, формирано от последните две цифри на входното число, е по-малко от 10 (напр. 101, 305, 609 и т.н.). В този случай отпечатваме "one-hundred and one", "three-hundred and five", "six-hundred and nine" и т.н.

Третият специален случай е когато числото, формирано от последните две цифри на входното число, е по-голямо от 10 и по-малко от 20 (напр. 111, 814, 919 и т.н.). В този случай отпечатваме "one-hundred and eleven", "eight-hundred and fourteen", "nine-hundred and nineteen" и т.н.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#13.

Задача: криптиране на низ

Да се напише функция еncrypt(char letter), която криптира дадена буква по следния начин:

  • Вземат се първата и последна цифра от ASCII кода на буквата и се залепят една за друга в низ, който ще представя резултата.
  • Към началото на стойността на низа, който представя резултата, се залепя символът, който отговаря на следното условие:
    • ASCII кода на буквата + последната цифра от ASCII кода на буквата.
  • След това към края на стойността на низа, който представя резултата, се залепя символът, който отговаря на следното условие:
    • ASCII кода на буквата - първата цифра от ASCII кода на буквата.
  • функцията трябва да върне като резултат криптирания низ.

Пример:

  • j → p16i
    • ASCII кодът на j e 106 → Първа цифра - 1, последна цифра - 6.
    • Залепяме първата и последната цифра → 16.
    • Към началото на стойността на низа, който представя резултата, залепяме символа, който се получава от сбора на ASCII кода + последната цифра → 106 + 6 → 112 → p.
    • Към края на стойността на низа, който представя резултата, залепяме символът, който се получава от разликата на ASCII кода - първата цифра → 106 - 1 → 105 → i.

Използвайки функцията, описана по-горе, да се напише програма, която чете поредица от символи, криптира ги и отпечатва резултата на един ред.

Приемаме, че входните данни винаги ще бъдат валидни. От конзолата трябва да се прочетат входните данни, подадени от потребителя – цяло число N, следвани от по един символ на всеки от следващите N реда.

Да се криптират символите и да се добавят към криптирания низ. Накрая като резултат трябва да се отпечата криптиран низ от символи като в следващия пример:

  • S, o, f, t, U, n, i → V83Kp11nh12ez16sZ85Mn10mn15h

Примерен вход и изход

Вход Изход
7
S
o
f
t
U
n
i
V83Kp11nh12ez16sZ85Mn10mn15h
Вход Изход
7
B
i
r
a
H
a
x
H66<n15hv14qh97XJ72Ah97xx10w

Насоки и подсказки

Всички операции ще извършим, като всеки път запазваме резултатите в променливи от тип string. След това ще "залепим" всички тези променливи както при задача "Повтаряне на низ". Така създаваме крайната променливата result от тип string и я извеждаме. Трябва да се завърти цикъл n пъти, като на всяка итерация към променливата result ще прибавяме криптирания символ.

За да намерим първата и последната цифри от ASCII кода, ще използваме алгоритъма, който използвахме за решаване на задача "n-та цифра", а за да създадем низа, ще процедираме както в задача "Число към бройна система".

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.org/Contests/Practice/Index/1374#14.

results matching ""

    No results matching ""