1.5. Функции - StriderAJR/StudentCpp GitHub Wiki

Когда лень писать одно и то же

Функция

Что такое функция

Функция - это именованный блок кода, который можно вызывать для исполнения. Может принимать параметры, может возвращать значение. Когда лень писать одно и то же несколько раз

Пример простейшей ф-ции

    int add(int a, int b) // <-- Заголовок ф-ции
    { // <-- Начало тела ф-ции

        // Тут может быть любой код, который нужно выполнить

        return a + b; // Возвращаем результат работы функции
    } // <-- Конец тела ф-ции

Заголовок ф-ции обязательно должен состоять из следующий элементов:

Тип возвращаемого значения (ТВЗ)

Это тип данных результата работы ф-ции. Именно результат останется в коде в месте вызова вашей ф-ции. Если ф-ция не будет иметь результат (просто что-то делает и все), тогда тип возвращаемого значения будет void - отсутствие типа

Имя функции всегда должно содержать глаголы и по имени ф-ции должно быть легко понятно, что она делает. Хорошие примеры имен ф-ций: readFromFile, writeToFile, getVectorLength, multiplyMatrix, add Примеры плохих имен функций: func1, foo, boo, getResult, do, blablabla

Список параметров

Параметры перечисляются в ф-ции через запятую. Параметр - это локальная переменная для функции, а значит вы объявляете переменные. Синтаксис будет соответствующий. int param1, double param2 и т.д.

Достаточно часто возникает ситуация, когда нужно в коде совершать какие-то однотипные действия. Есть выбор:

  1. Копировать код каждый раз, когда нужно вновь выполнить этот алгоритм
  2. Создать функцию и вызывать каждый раз ее

Допустим, нам нужно написать программу, которая запрашивает у пользователя 3 числа, а затем выводит произведение этих чисел. Вроде бы ничего сложного. Только вот код:

#include <iostream>
using namespace std;

int main()
{
    int a, b, c;

    cin >> a >> b >> c;
    cout << "a * b * c = " << a*b*c << endl;

    return 0;
}

Работать, конечно, будет, но на "честном слове". На честном слове пользователя, что он введет именно числа, а не попытается ввести какие-то другие символы. А по-хорошему считается, что пользователь - это... Ну как бы повежливее сказать. Обезьяна с гранатой - сделать может что угодно, когда угодно и с чем угодно. Один человек сказал мне вот что:

Программа должна работать даже если я сяду на клавиатуру задницей.

Чтобы это высказывание работало для такой, казалось бы, простейшей программы, нужно ввести "защиту от дурака" (подробнее об этой теме в отдельной теме), т.е. удостоверится, что пользователь действительно введет числа. Если что-то он делает не так, нужно "ударить его по рукам" и подсказать, как сделать правильно.

Например, как-то так (допустим, функции использовать не будем):

#include <iostream>
using namespace std;

int main()
{
    int a, b, c;

    do
    {
        int buffLen = 20;
        char* buff = new char[buffLen];

        cout << "Input number: ";
        cin.getline(buff, buffLen);

        bool isOk = true;
        for (int i = 0; i < strlen(buff); i++)
            if (buff[i] < '0' || buff[i] > '9') {
                isOk = false;
                break;
            }

        if (!isOk)
            cout << "Not number. Try again!" << endl;
        else {
            a = atoi(buff);
            break;
        }
    } while (true);

    do
    {
        int buffLen = 20;
        char* buff = new char[buffLen];

        cout << "Input number: ";
        cin.getline(buff, buffLen);

        bool isOk = true;
        for (int i = 0; i < strlen(buff); i++)
            if (buff[i] < '0' || buff[i] > '9') {
                isOk = false;
                break;
            }

        if (!isOk)
            cout << "Not number. Try again!" << endl;
        else {
            b = atoi(buff);
            break;
        }
    } while (true);

    do
    {
        int buffLen = 20;
        char* buff = new char[buffLen];

        cout << "Input number: ";
        cin.getline(buff, buffLen);

        bool isOk = true;
        for (int i = 0; i < strlen(buff); i++)
            if (buff[i] < '0' || buff[i] > '9') {
                isOk = false;
                break;
            }

        if (!isOk)
            cout << "Not number. Try again!" << endl;
        else {
            c = atoi(buff);
            break;
        }
    } while (true);

    cout << endl << "a * b * c = " << a*b*c << endl;

    return 0;
}

Какой замечательный код, не правда ли? Такая простая программа превратилась в жирного и неповоротливого монстра. А теперь представьте, что нам нужно изменить текст ошибки. Править это придется 3 раза. А если мы в программе в разных местах должны считывать числа? Несколько десяткой раз. Представили? Да, морозец по коже, соглашусь.

Поэтому лучше будет все-таки не боятся использовать функции:

int readNumber()
{

    do
    {
        int buffLen = 20;
        char* buff = new char[buffLen];

        cout << "Input number: ";
        cin.getline(buff, buffLen);

        bool isOk = true;
        for (int i = 0; i < strlen(buff); i++)
            if (buff[i] < '0' || buff[i] > '9') {
                isOk = false;
                break;
            }

        if (!isOk)
            cout << "Not number. Try again!" << endl;
        else 
            return atoi(buff);
    } while (true);
}

int main()
{
    int a = readNumber(), b = readNumber(), c = readNumber();

    cout << endl << "a * b * c = " << a*b*c << endl;

    return 0;
}

Так-то лучше!

Как понять, что нужно создавать функцию На самом деле это достаточно творческий вопрос. Каждый разработчик сам решает, когда "пора". Но основное правило такое:

Если в коде есть 2 и более повторяющиеся участка кода, значит, их нужно выносить в отдельную функцию.

Некоторые IDE сами отслеживают повторяющиеся участки кода и начинают назойливо предлагать вынести их в функцию.

Также тревожным сигналом является, если вы копируете больше пары строк для вставки где-то в вашем алгоритме. Это уже "повторяющийся код", которого нужно избегать.

Но опять же все эти советы относительны. В первую очередь нужно оперировать здравым смыслом.

Синтаксис заголовка функции

тип_возвращаемого_значения имя_функции(список_параметров);
// Или более кратко
ТВЗ имя_функции(параметры);

Параметры и возвращаемое значение

Тип возвращаемого значения (ТВЗ) Это тип данных результата работы ф-ции. Именно результат останется в коде в месте вызова вашей ф-ции. Если ф-ция не будет иметь результат (просто что-то делает и все), тогда тип возвращаемого значения будет void - отсутствие типа

 void writeToFile(char* str)
    {
        // ...
    }

    double devide(int a, double b, float c)
    {
        return a / b / c;

        cout << "^_^"; // Этот код уже не будет выполнен
                       // После команды return ф-ция возвращает результат своей работы и завершает выполнение сразу
    }

    void consoleRead(char* buff, int num)
    {
        cin.getline(buff, num);
    }
    
    int main() 
    {
        double a = devide(10, 3.7, 1.2);      
        while (a > 0)
        {
            cout << a << endl;
            int p1; double p2; float p3;
            cin >> p1 >> p2 >> p3;
            a = devide(p1, p2, p3);
        }

        return 0;
    }

Что будет происходить:

  1. Вызовется ф-ция, параметры (локальные переменные ф-ции) проинициализируются соответствующими значениями (a = 10, b = 3.7, c = 1.2) Обратите внимание, что переменная a в ф-ции main и переменная a - в ф-ции devide - разные локальные переменные! ВНИМАНИЕ!!! Каждая существует только внутри своего блока кода. И они никак не перемекаются, это разные ячейки в памяти.
int main() 
    {
        double a = devide(10, 3.7, 1.2);      
        while (a > 0)
        {
  1. Ф-ция devide делает свое дело (причем, что она делает нас в ф-ции main вооще не волнует - это личное дело самой ф-ции)
double devide(int a, double b, float c)
   {
        return a / b / c;

        cout << "^_^"; 
    }
  1. Как ф-ция выполнится, вместе всей конструкции "devide(10, 3.7, 1.2)" подставится итоговый результат работы ф-ции. Будет что-то вроде double a = результат;

И вызывать функцию мы можем до посинения:

  while (a > 0)
        {
            cout << a << endl;
            int p1; double p2; float p3;
            cin >> p1 >> p2 >> p3;
            a = devide(p1, p2, p3);
        }

        return 0;
    }

А теперь рассмотрим, когда функции каскадно вызывают друг друга

    void ChildFunction()
    {
        cout << "ChildFunction called" << endl;
    }

    void ParentFunction()
    {
        cout << "ParentFunction called" << endl;
`LastFunction()`; <-- Попробуйте раскомментировать эту строчку

У вас будет ошибка, смысл которой сводится к тому, что компилятор не знает такую ф-цию LastFunction. Кажется, что это бред, потому что вон же она чуть ниже, однако это не бред: компилятор С++ работает сверху вниз - все, что используется должно быть объявлено выше того момента, когда вы это используете. Поэтому компилятор действительно на момент вызова ф-ции LastFunction ее еще не встретил и не знает, что это такое и как себя с ней вести.

Думаете проблема решается просто? Нужно просто перенести LastFunction повыше? Ну попробуйте и увидите, что тут специально использовано несколько перекрестных ф-ций, которые используются друг другом. В лучшем случае вам придется долго играть в пятнашки, чтобы правильно разместить перекрестные ф-ции, и чтобы они "видели" друг друга.

Но программисты люди ленивые - играть в пятнашки слишком долго и неэффективно

Пробелма решается с использованием "прототипов" функций (см. ниже)

}

 void SemiFunction()
 {
     cout << "SemiFunction called" << endl;

     ParentFunction();
 }

 void LastFunction()
 {
     cout << "LastFunction called" << endl;

     SemiFunction();
 }

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

Использование прототипов

void AnotherChildFunction(); Это называется прототипов ф-ции. Прототип - синтаксическая запись заголовка ф-ции без тела

void AnotherParentFunction(); После заголовка функции сразу же идет ";" - блок кода ф-ции не начинается, его просто нет

void AnotherSemiFunction(); Прототип ф-ции дает понять компилятору, что где-то там когда-нибудь обязательно встретиться

void AnotherLastFunction(); ф-ция в своем нормальном виде, но вот прямо сейчас всю ф-цию мы тебе не покажем, но имей в виде, что она есть.

После такого "диалога" компилятор успокаивается, он нам верит до поры до времени и если встретит использования ф-ции, описанной с помощью прототипа, он это проглотит и уже не будет пугаться "Ой, а чёй-то такое?!?! Но имейте в виду, если есть прототип ф-ции, но до конца модуля самой ф-ции не встретится, то компилятор все равно поднимет панику:

"Ты мне обещал! Обещал, что покажешь всю ф-цию, а ее нет!!!! :'''((("

    void AnotherChildFunction()
    {
        cout << "ChildFunction called" << endl;
    }

    void AnotherParentFunction()
    {
        cout << "ParentFunction called" << endl;
        AnotherLastFunction();
    }

    void AnotherSemiFunction()
    {
        cout << "SemiFunction called" << endl;

        ParentFunction();
    }

    void AnotherLastFunction()
    {
        cout << "LastFunction called" << endl;

        SemiFunction();
    }
}
⚠️ **GitHub.com Fallback** ⚠️