Функции: параметры, аргументы, return
Ты пользуешься функцией с самого первого урока — main() и есть функция, просто с особой ролью точки входа. Пора разобраться, как устроены функции вообще: как объявить свою, что такое параметры и аргументы, и что делает return. В конце урока этим же приёмом мы разберём код программы-персонажа на отдельные функции — они ещё пригодятся в одном из ближайших уроков.
01Что ты изучишь
- Как объявить свою функцию: тип возврата, имя, параметры, тело
- Чем параметр отличается от аргумента
- Что делает
returnи почему тип возвращаемого значения должен совпадать с объявленным void— функции, которые ничего не возвращают
02Зачем функции, если уже есть циклы
Цикл повторяет один и тот же код много раз подряд, в одном месте. Но что, если один и тот же расчёт (например, «площадь квадрата со стороной N») нужен в разных, не обязательно соседних местах программы? Копировать формулу в каждое такое место — то же самое дублирование, что решают циклы, только для «одинакового кода в разных местах», а не «одинакового кода много раз подряд». Функция — это как раз именованный, переиспользуемый кусок кода: написал один раз, используешь по имени сколько угодно раз.
03Объявление функции
Ты уже писал одну функцию с самого первого урока, просто не думал о ней в этих терминах, — main(). У неё есть тип возврата (int), имя (main), пустые скобки параметров и тело { }. Своя функция строится по тому же самому шаблону — просто с именем, параметрами и телом, которые выбираешь сам:
int square(int n)
{
return n * n;
}
int main()
{
int result = square(5);
std::cout << result; // 25
return 0;
}
int перед square — тип значения, которое функция вернёт (как и у main(), которая тоже объявлена как int main() — просто её возвращаемое значение операционная система использует иначе, чем обычные функции). square — имя, по тем же правилам идентификаторов, что и у переменных. (int n) — параметр: значение, которое функция принимает при вызове. Тело — то, что выполняется при каждом вызове.
04Параметр и аргумент — разные слова для разных ролей
Эти два термина путают чаще всего, хотя разница простая:
- Параметр — имя в объявлении функции (
nвint square(int n)). Это «переменная-заглушка», которая существует только внутри функции. - Аргумент — конкретное значение, переданное при вызове (
5вsquare(5)). У одной и той же функции с одним параметром может быть сколько угодно разных аргументов в разных вызовах.
std::cout << square(5); // аргумент 5 → параметр n получает 5 → результат 25
std::cout << square(10); // аргумент 10 → параметр n получает 10 → результат 100
При каждом вызове параметр n — это, по сути, новая переменная, которая существует только на время выполнения функции и получает значение переданного аргумента. Изменения n внутри функции не затрагивают переменную, которая, возможно, использовалась как аргумент снаружи — параметр живёт своей отдельной жизнью. Почему это так и что вообще определяет, где переменная «видна», а где нет, — отдельная большая тема, разберём её подробно в ближайшем следующем уроке.
05return — что возвращает функция
return уже встречался в main() — return 0;. У функции с непустым типом возврата (как int square(int n)) return обязателен на каждом пути выполнения, и тип возвращаемого значения должен совпадать с объявленным типом функции (или как минимум приводиться к нему): int square(...) не может вернуть std::string. return ещё и немедленно завершает функцию — код после него в этом же пути выполнения не выполнится, аналогично тому, как break завершает цикл.
06void — функция без возвращаемого значения
Не каждая функция должна что-то возвращать — иногда единственная цель функции просто выполнить действие, например напечатать приветствие. Писать int и придумывать бессмысленный return 0; только ради формы было бы нечестно: тип возврата должен отражать, что функция реально делает. Для этого случая есть отдельный тип — void («пусто»), означающий именно «эта функция ничего не возвращает»:
void printGreeting(std::string name)
{
std::cout << "Hello, " << name << "!\n";
}
int main()
{
printGreeting("Ada");
printGreeting("Alan");
return 0;
}
В void-функции return; без значения допустим (чтобы досрочно завершить функцию, как break для цикла), но return со значением — нет, это ошибка компиляции: значение возвращать некуда, тип возврата явно объявлен как «ничего».
07Не только int: float, double и char в роли типа функции
Всё, что было выше, работало с int и void — но параметром и типом возврата может быть любой тип, который умеет хранить переменная (тема про основные типы данных). Дробные вычисления — самый частый повод завести функцию с double-параметрами и double-возвратом:
double average(double a, double b)
{
return (a + b) / 2.0;
}
int main()
{
double result = average(3.0, 4.0);
std::cout << result; // 3.5
return 0;
}
Правило то же самое, что и для square: тип перед именем — это тип значения, которое функция вернёт, а тип перед именем параметра — тип значения, которое она примет. Здесь оба параметра и возврат — double, потому что среднее двух чисел почти никогда не целое. float устроен так же (тоже дробный тип, просто с меньшей точностью) — float half(float x) { return x / 2.0f; } объявляется и вызывается по тому же шаблону, менять нужно только слово float вместо double у параметра и возврата.
Тип возврата и тип параметра не обязаны совпадать друг с другом — только с тем, что реально написано в return, и с тем, что реально передаётся при вызове. Функция gradeFor дальше в этом уроке вернёт char, приняв int — это совершенно нормальная и частая комбинация: параметр и возврат выбираются независимо, каждый под свою роль.
08Части функции — таблица
| Часть | Пример | Роль |
|---|---|---|
| Тип возврата | int, void, double… | что функция вернёт (или ничего, если void) |
| Имя | square | идентификатор, по которому функцию вызывают |
| Параметр | int n в объявлении | «заглушка» для значения внутри функции |
| Аргумент | 5 в square(5) | конкретное значение при конкретном вызове |
return | return n * n; | значение, отдаваемое наружу; сразу завершает функцию |
09Хуже → лучше: от копипасты к функции
Задача — вернуться к программе-персонажу и вывести буквенную оценку по hp для трёх участников отряда: 95, 75 и 50 очков здоровья (90 и выше — 'A', 70–89 — 'B', меньше 70 — 'F'). Логика одна и та же — но записана она может быть по-разному.
int hp1 = 95;
if (hp1 >= 90) { std::cout << "A" << "\n"; }
else if (hp1 >= 70) { std::cout << "B" << "\n"; }
else { std::cout << "F" << "\n"; }
int hp2 = 75;
if (hp2 >= 90) { std::cout << "A" << "\n"; }
else if (hp2 >= 70) { std::cout << "B" << "\n"; }
else { std::cout << "F" << "\n"; }
int hp3 = 50;
if (hp3 >= 90) { std::cout << "A" << "\n"; }
else if (hp3 >= 70) { std::cout << "B" << "\n"; }
else { std::cout << "F" << "\n"; }
Работает правильно, но если границы оценок понадобится изменить (например, поднять порог для 'A' до 95), придётся находить и чинить три одинаковых блока — и легко забыть один из них.
char gradeFor(int hp)
{
if (hp >= 90) { return 'A'; }
else if (hp >= 70) { return 'B'; }
else { return 'F'; }
}
int main()
{
std::cout << gradeFor(95) << "\n";
std::cout << gradeFor(75) << "\n";
std::cout << gradeFor(50) << "\n";
return 0;
}
Тот же результат, но правило теперь в одном месте — изменить границу нужно будет только внутри gradeFor, и это исправление сразу подействует на все вызовы. Обрати внимание: gradeFor возвращает char, а не выводит его сама — вычисление и вывод специально разделены, вывод остаётся снаружи, в main().
char gradeFor(int hp)
{
if (hp >= 90) { return 'A'; }
else if (hp >= 70) { return 'B'; }
else { return 'F'; }
}
void printReport(int hp)
{
std::cout << "HP: " << hp << ", Grade: " << gradeFor(hp) << "\n";
}
int main()
{
printReport(95);
printReport(75);
printReport(50);
return 0;
}
printReport — void-функция, которая внутри себя вызывает другую функцию, gradeFor. Функции могут вызывать друг друга без ограничений — это совершенно обычная практика: каждая функция отвечает ровно за одну вещь (посчитать оценку — отдельно, вывести отчёт — отдельно), а main() становится короче и читается как список шагов. gradeFor в этом виде ещё пригодится — программа-персонаж скоро соберётся в цельную консольную программу, и эта функция станет одним из её кирпичиков.
10Попробуй изменить
- Добавь второй параметр в
square:int multiply(int a, int b) { return a * b; }— вызовиmultiply(3, 4). - В
printGreetingдобавь третий вызов с другим именем — тело функции менять не нужно, только новый вызов с новым аргументом. - Попробуй убрать
return n * n;изsquare— компилятор предупредит (или откажется собирать), что функция с непустым типом возврата не всегда что-то возвращает. - Напиши свою функцию с
doubleпо образцуaverage— например,double square2(double x) { return x * x; }— и сравни вызовsquare2(2.5)с целочисленнымsquare(2): одна и та же идея (умножить на себя), два разных типа.
11Живая отладка: типичные ошибки
Точный текст ошибки зависит от компилятора и его версии — ниже характерный вид в стиле g++.
int square(int n) { return n * n; } int main() { return 0; }
Скомпилируется без ошибок и без предупреждений — объявление функции просто описывает, что она делает, но само по себе ничего не выполняет. Код внутри square выполнится только тогда, когда её действительно вызовут по имени: square(5).
void printGreeting(std::string name)
{
return name; // void ничего не возвращает — return со значением здесь ошибка
}
Либо изменить тип возврата на подходящий (std::string), либо убрать значение из return.
int sign(int x)
{
if (x > 0)
{
return 1;
}
// нет return для случая x <= 0 — а тип возврата int обещает значение всегда
}
Вызов такой функции при x <= 0 — неопределённое поведение сродни чтению неинициализированной переменной: вернётся какой-то мусор. На многих реальных проектах такие предупреждения намеренно превращают в ошибки сборки (флаг -Werror). Правильно — добавить else с собственным return.
12Проверь себя
Три вопроса: разница параметра и аргумента на конкретном примере, диагностика ошибки компиляции, и предсказание того, что случится без вызова.
int triple(int x) { return x * 3; }
std::cout << triple(7) << triple(2);Показать объяснение
x — параметр, один-единственный, часть объявления функции. 7 и 2 — два разных аргумента в двух разных вызовах одной и той же функции с одним параметром (§04).
void logMessage(std::string text)
{
return text;
}Показать объяснение
void означает «функция ничего не возвращает», а return text; пытается вернуть значение — противоречие, которое компилятор не пропустит (см. §06 и §11). Исправление: либо сменить тип возврата на std::string, либо убрать значение из return (оставить просто return; или вообще убрать).
int square(int n) { return n * n; }
int main() { return 0; }Показать объяснение
Ничего не выведет. square объявлена, но нигде не вызвана — объявление функции лишь описывает, что она умеет делать, само по себе оно ничего не выполняет (§11). Код внутри square выполнился бы только при реальном вызове вида square(5).
13Практика
★★ Напиши функцию int square(int n), возвращающую n * n, вызови её хотя бы один раз и выведи результат через std::cout.
Шаги: 1) реши тип возврата и тип параметра — оба целые числа, значит int; 2) напиши сигнатуру int square(int n) и тело с одним return; 3) в main() вызови square с любым числом-аргументом и передай результат в std::cout.
Сигнатура строится по образцу из раздела «Объявление функции»: тип возврата совпадает с тем, что стоит после return внутри тела, а параметр — с тем, что передаётся при вызове.
Решение: int square(int n) { return n * n; } — затем в main(): std::cout << square(5);
★★ Напиши void-функцию printStars(int count), которая циклом выводит count звёздочек * подряд, затем перевод строки. Вызови её дважды с разными аргументами.
Шаги: 1) функция ничего не возвращает — значит тип возврата void; 2) параметр count — целое число, int; 3) внутри — цикл, который повторяется count раз и каждый раз выводит один символ *; 4) после цикла — отдельный вывод "\n"; 5) вызови функцию дважды с двумя разными числами, чтобы разница была видна.
Цикл внутри функции должен реально использовать count в своём условии (например, for (int i = 0; i < count; i++)) — иначе число звёздочек не будет меняться вместе с аргументом.
Решение: void printStars(int count) { for (int i = 0; i < count; i++) { std::cout << "*"; } std::cout << "\n"; } — вызови дважды: printStars(3); printStars(5);
★★ Напиши функцию double average(double a, double b), возвращающую среднее арифметическое двух чисел, вызови её и выведи результат через std::cout.
Шаги: 1) результат — дробное число (среднее двух чисел почти никогда не целое), значит и параметры, и тип возврата — double; 2) тело — одна строка: сложи оба параметра и раздели на 2.0 (не на 2 — так надёжнее получить именно дробное деление); 3) вызови average с двумя числами и выведи результат.
Возврат должен реально складывать и делить оба параметра функции — не подставленные числа, а именно a и b, чтобы результат менялся вместе с аргументами вызова.
Решение: double average(double a, double b) { return (a + b) / 2.0; } — затем в main(): std::cout << average(3.0, 4.0);
14Термины
| Термин | Значит |
|---|---|
| Функция (function) | именованный переиспользуемый блок кода: тип возврата, имя, параметры, тело |
| Параметр (parameter) | имя-заглушка в объявлении функции, существующее только внутри неё |
| Аргумент (argument) | конкретное значение, переданное функции при вызове |
return | инструкция, отдающая значение наружу и немедленно завершающая функцию |
void | тип возврата для функций, которые ничего не возвращают |
15Что ты теперь умеешь
До этого урока весь код жил внутри одного main(). Теперь его можно разложить на именованные, переиспользуемые куски:
- Ты можешь объявить свою функцию с нужным типом возврата, параметрами и телом — не только по образцу
int, но и сvoid,double,charпод конкретную задачу. - Ты можешь объяснить разницу между параметром и аргументом на любом конкретном вызове, а не только заучить определение.
- Ты можешь заметить, когда один и тот же кусок кода стоит вынести в функцию, вместо того чтобы копировать его снова.
- Ты можешь предсказать, что случится, если функция объявлена, но ни разу не вызвана — и почему компилятор не считает это ошибкой.
- Ты можешь найти в чужом коде функцию, которая обещает возвращаемое значение, но реально его игнорирует (или наоборот — void, который пытается что-то вернуть).
16Кратко
- Функция = тип возврата + имя + параметры в скобках + тело
{ }. - Параметр — имя в объявлении; аргумент — конкретное значение при конкретном вызове.
return значение;— отдаёт результат и сразу завершает функцию; тип значения должен совпадать с объявленным типом возврата.void— функция без возвращаемого значения;return;без значения допустим,return значение;— нет.- Тип возврата и тип параметра выбираются независимо друг от друга и могут быть любыми —
int,double,char,boolи так далее — под конкретную задачу, а не толькоint. - Объявление функции ничего не выполняет само по себе — нужен явный вызов по имени.
Функции разложили код на именованные куски — но пока каждая переменная всё ещё видна отовсюду, где написана, без явных границ. Следующий урок — область видимости: где именно переменная существует, и что происходит с параметром после того, как функция завершилась.
Самая частая ошибка новичка с функциями — писать их «постфактум», уже утонув в повторяющемся коде. Правильная привычка обратная: если чувствуешь, что вот-вот скопируешь кусок кода второй раз, — это и есть сигнал остановиться и вынести его в функцию прямо сейчас, до копирования, а не после третьего повтора. И ещё: хорошее имя функции — это глагол, описывающий действие (printStars, gradeFor), а не существительное. Если имя функции не складывается в осмысленный глагол — возможно, она делает не одну вещь, а сразу несколько, и её стоит разбить на две.
Функции-члены класса в Unreal C++ (методы Actor'ов и компонентов) устроены по тем же правилам — тип возврата, имя, параметры, return. Разница появится позже: пометив функцию макросом UFUNCTION(BlueprintCallable), её можно будет вызывать ещё и из Blueprint — но сама функция внутри остаётся обычной C++ функцией, ничем принципиально не отличающейся от square из этого урока.