- Код, който има определен семантично завършен смисъл
- Позволява многократната употреба и извикване на кода
- Повишава значително качеството на кода, заради въвеждането на абстракция
- Може да връща даден резултат
[<return type | void>] <identifier>([[<param type> <param identifier>]...])
{
<code>
return [<return value>];
}
return
прекратява изпълнението на функцията и връща указания резултатvoid
означава, че функцията не връща стойност. Тогава може да ползвамеreturn
без да подаваме стойност. Може да не извикамеreturn
нито веднъж- Параметрите са незадължителни
Note
Когато изтървем типа на връщане, се подразбира тип int
. Това никога и по никакъв повод не го правим!
Caution
Когато функцията има тип на връщане, трябва задължително ВСЕКИ КЛОН на програмата да връща стойност
Important
СИГНАТУРА на функция е последователността от тип на връщане, име на функцията и броя и типа на подадените параметри. Имената на параметрите НЕ СА част от сигнатурата на функцията.
- Параметрите при декларация на функция се наричат формални
- Това са променливите, с които се изпълнява функцията
- Те са само нови имена
- Реалните стойности идват при извикването на функцията
- Фактическите параметри живеят в новия function scope
- Може дадена функция да няма параметри
- Параметрите с които извикваме функцията
- Реалните стойности, които се подават на формалните параметри
- Възможно е да се случи имплицитно преобразуване на типовете, реално при извикване се случва присвояване на фактическия параметър на формалния
// int x = 42.0 - implicit casting on invocation
int func(int x)
{
return x + 1;
}
int main()
{
std::cout << func(42.0);
}
- Подредбата на параметрите е важна, те са позиционни
- При предаване на параметър на функция, се създава копие на оригиналния параметър, не се правят промени по оригиналните променливи
- Добре е даден параметър да е константа, ако няма да се променя
Important
Фактическият и формалният параметър са две отделни променливи, които имат еднакви стойности в началото
- Forward declaration, прототип, декларация - изписване на сигнатурата на функцията
- Не можем в кода да ползваме дадена функция, ако преди това не е декларирана или дефинирана на по-горен ред
- Дефиницията може да е на по-късен етап
- Може да има най-много една дефиниция
- Ако ползваме функция, която е единствено декларирана, без да е дефинирана - грешка
- Може да имаме повече от една функция с дадено име. Тези функции обаче трябва да са с различни сигнатури, изразяващи се в различни параметри
- Не бива да правим функции, различаващи се само по типа на връщане - грешки по двусмислие
int f() {return 0;}
int f(int x) {return x;}
int f(int x, int y) {return x + y;}
- Параметрите може да имат стойност по подразбиране
- Само последните параметри може да имат стойност по подразбиране
- Това е така, понеже параметрите са позиционни за функцията и няма нужда да подаване параметър, който има стойност по подразбиране, ако тя ни устройва
// void printLetter(bool capitalize = true, char c) - wrong
void printLetter(char c, bool capitalize = true)
{
std::cout << (capitalize ? c + 'A' - 'a' : c);
}
int main()
{
printLetter('a');
printLetter('a', false);
printLetter('a', true);
}
- Чиста функция е такава, която не променя външното състояние на програмата, а просто връща резултат от някакви действия над входа си
- Четенето от и писането на конзолата не са чисти операции
- Трябва да се стремим да пишем чисти функции
- Това очевидно не е възможно винаги, но трябва да отделяме логиката така, че да разграничим чистите операции от тези, мутиращи състоянието
Caution
Една функция трябва да отговаря за едно нещо и в никакъв случай да не смесваме чисти операции с мутиращи състоянието
Important
Всяка функция трябва да извършва точно едно нещо
// pure function
bool isCapitalLetter(char c)
{
return 'A' <= c && c <= 'Z';
}
// separate function for writing (or reading)
bool printLetter(char c)
{
std::cout << c;
}
// incorrect use of non-pure function with composite logic
bool printCapitalLetter()
{
char c;
std::cin >> a;
std::cout << c + 'A' - 'a';
}
- В големи проекти отделяме дефинициите от декларациите
- Декларациите се правят в хедърни файлове (
.hpp
) - Тези хедърни файлове се
include
-ват при нужда от употреба на функцията - За всеки хедърен файл съществува един
.cpp
файл, в който са имплементациите - дефинициите
Note
Това води освен до подобряване на качеството на кода, така и до оптимизация на процеса на компенсация
// Never use if-else to return true or false value of function
bool isCapitalLetter(char c)
{
if('A' <= c && c <= 'Z')
return true;
else
return false;
// return 'A' <= c && c <= 'Z';
}
// Never use branching when you can skip
bool isPrime(int x)
{
if(x <= 0)
{
return 0;
}
else
{
for (int i = 2; i < a; i++)
{
if (x % i == 0) return false;
}
return true;
}
}
// Missing return value of the last branch
int getValue(char c, int x)
{
if('A' <= c && c <= 'Z')
return c - 'A';
else if(0 <= x && x <= 10)
return x + 50;
// return 0;
}
bool isValidData(int a)
{
if (a >= 1000)
{
return true;
}
if (a % 2 != 0)
{
return false;
}
}
- Употреба на guard clause
bool IsPrime(const int a)
{
if (a < 2) return false;
if (a == 2) return true;
for (int i = 3; i * i < a; i += 2)
{
if (a % i == 0) return false;
}
return true;
}
int increment(int x)
{
x++;
}
int main()
{
int x = 7;
increment(x);
std::cout << x; // 7
}
-
Когато предаваме параметър по показаните начини досега, ние предаваме копие на стойността на параметъра
-
Може да предаваме и действителната променлива, като така всяка промяна ще се отразява и на нея
-
Ползване на референция - след типа на променливата слагаме
&
int increment(int& x)
{
x++;
}
int main()
{
int x = 7;
increment(x);
std::cout << x; // 8
}
- Трябва да се инициализират при декларация
- След като веднъж е била инициализирана, повече не може да се променя
// Променливите x и y сочат на едно и също място в паметта
// Aналогично е когато се ползва и във функция
// - формалният параметър сочи същата памет като фактическия
int x = 42;
int& y = x;
y++;
std::cout << x << " " << y; //43 43
// Тук за променливата b се заделя нова памет и се копира стойността на a
int a = 42;
int b = a;
- Указателят е променлива, която пази адрес от паметта - шестнадесетично число, пазещо поредния номер в паметта на първата клетка от променливата
- Създаваме указател от даден тип като след типа сложим
*
int* pointer = 0x123;
- Префиксен оператор
&
, приложен на променлива - връща нейния адрес - Префиксен оператор
*
, приложен на променлива-указател - връща стойността, намираща се на този адрес, като взима предвид типа на указателя(колко байта да прочете) - операцита се нарича дереференсиране на указател
int x = 42;
int* xAddress = &x; //0x...
int xValue = *xAddress; // 42
// Променливата y има за стойност някакво число - адрес
// Стойността на y е адресът на променливата x
int x = 42;
int* y = &x;
(*y)++;
std::cout << x << " " << (*y); //43 43
int increment(int* x)
{
(*x)++;
}
int main()
{
int x = 7;
increment(&x);
std::cout << x; // 8
}
Important
Указателите са изключително важни, защото ни предоставят възможност за "пряк достъп" до паметта! Оттук нататък те ще са основа при работата с масиви и при въвеждането на обектно-ориентираното програмиране.
Caution
Указателите позволяват много гъвкавост. Когато може да не ползваме указател, няма нужда да го правим. В случаите, в които е неизбежна употребата, трябва да се прави с много внимание и разбиране. От указателите не може да се избяга. Трябва да се разбират!
- Извикването на функции става веднага при достигането на инструкцията
- Инструкциите след функцията продължават след извикването на функцията
- Може да викаме функции във функции
- Стековата рамка запазва последователните извиквания при влизане във функцията и когато функция свърши я маха от стековата рамка (последното извикване се маха)
- Стекът е структура от данни (Повече подробности?)
- Извикване на функция от функция (Рекурсия?)