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
и т.д.
Достаточно часто возникает ситуация, когда нужно в коде совершать какие-то однотипные действия. Есть выбор:
- Копировать код каждый раз, когда нужно вновь выполнить этот алгоритм
- Создать функцию и вызывать каждый раз ее
Допустим, нам нужно написать программу, которая запрашивает у пользователя 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;
}
Что будет происходить:
- Вызовется ф-ция, параметры (локальные переменные ф-ции) проинициализируются соответствующими значениями (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)
{
- Ф-ция devide делает свое дело (причем, что она делает нас в ф-ции main вооще не волнует - это личное дело самой ф-ции)
double devide(int a, double b, float c)
{
return a / b / c;
cout << "^_^";
}
- Как ф-ция выполнится, вместе всей конструкции "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();
}
}