Skip to content

Latest commit

 

History

History

Week 04

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Функции

Същност

  • Код, който има определен семантично завършен смисъл
  • Позволява многократната употреба и извикване на кода
  • Повишава значително качеството на кода, заради въвеждането на абстракция
  • Може да връща даден резултат

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

[<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, прототип, декларация - изписване на сигнатурата на функцията
  • Не можем в кода да ползваме дадена функция, ако преди това не е декларирана или дефинирана на по-горен ред
  • Дефиницията може да е на по-късен етап
  • Може да има най-много една дефиниция
  • Ако ползваме функция, която е единствено декларирана, без да е дефинирана - грешка

Overloading

  • Може да имаме повече от една функция с дадено име. Тези функции обаче трябва да са с различни сигнатури, изразяващи се в различни параметри
  • Не бива да правим функции, различаващи се само по типа на връщане - грешки по двусмислие
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);
}

Pure functions

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

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

Указателите позволяват много гъвкавост. Когато може да не ползваме указател, няма нужда да го правим. В случаите, в които е неизбежна употребата, трябва да се прави с много внимание и разбиране. От указателите не може да се избяга. Трябва да се разбират!

Стекова рамка

  • Извикването на функции става веднага при достигането на инструкцията
  • Инструкциите след функцията продължават след извикването на функцията
  • Може да викаме функции във функции
  • Стековата рамка запазва последователните извиквания при влизане във функцията и когато функция свърши я маха от стековата рамка (последното извикване се маха)
  • Стекът е структура от данни (Повече подробности?)
  • Извикване на функция от функция (Рекурсия?)