LEARNING · STAGE 00 · PROGRAMMING BASICS

Функции: параметры, аргументы, return

Ты пользуешься функцией с самого первого урока — main() и есть функция, просто с особой ролью точки входа. Пора разобраться, как устроены функции вообще: как объявить свою, что такое параметры и аргументы, и что делает return. В конце урока этим же приёмом мы разберём код программы-персонажа на отдельные функции — они ещё пригодятся в одном из ближайших уроков.

01Что ты изучишь

  • Как объявить свою функцию: тип возврата, имя, параметры, тело
  • Чем параметр отличается от аргумента
  • Что делает return и почему тип возвращаемого значения должен совпадать с объявленным
  • void — функции, которые ничего не возвращают

02Зачем функции, если уже есть циклы

Цикл повторяет один и тот же код много раз подряд, в одном месте. Но что, если один и тот же расчёт (например, «площадь квадрата со стороной N») нужен в разных, не обязательно соседних местах программы? Копировать формулу в каждое такое место — то же самое дублирование, что решают циклы, только для «одинакового кода в разных местах», а не «одинакового кода много раз подряд». Функция — это как раз именованный, переиспользуемый кусок кода: написал один раз, используешь по имени сколько угодно раз.

03Объявление функции

Ты уже писал одну функцию с самого первого урока, просто не думал о ней в этих терминах, — main(). У неё есть тип возврата (int), имя (main), пустые скобки параметров и тело { }. Своя функция строится по тому же самому шаблону — просто с именем, параметрами и телом, которые выбираешь сам:

main.cppC++
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)). У одной и той же функции с одним параметром может быть сколько угодно разных аргументов в разных вызовах.
Один параметр, разные аргументыC++
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 («пусто»), означающий именно «эта функция ничего не возвращает»:

main.cppC++
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-возвратом:

main.cppC++
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)конкретное значение при конкретном вызове
returnreturn n * n;значение, отдаваемое наружу; сразу завершает функцию

09Хуже → лучше: от копипасты к функции

Задача — вернуться к программе-персонажу и вывести буквенную оценку по hp для трёх участников отряда: 95, 75 и 50 очков здоровья (90 и выше — 'A', 70–89 — 'B', меньше 70 — 'F'). Логика одна и та же — но записана она может быть по-разному.

Хуже — одна и та же цепочка if/else скопирована триждыC++
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), придётся находить и чинить три одинаковых блока — и легко забыть один из них.

Лучше — логика написана один раз, в функцииC++
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().

Лучше всего — вывод тоже вынесен в свою функциюC++
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-функция пытается вернуть значение
void printGreeting(std::string name)
{
    return name;   // void ничего не возвращает — return со значением здесь ошибка
}
консоль (иллюстрация)
g++ main.cpp -o main main.cpp: In function 'void printGreeting(std::string)': main.cpp:4:12: error: return-statement with a value, in function returning 'void' 4 | return name; | ^~~~ Компиляция не удалась.

Либо изменить тип возврата на подходящий (std::string), либо убрать значение из return.

Не все пути выполнения возвращают значение
int sign(int x)
{
    if (x > 0)
    {
        return 1;
    }
    // нет return для случая x <= 0 — а тип возврата int обещает значение всегда
}
консоль (иллюстрация)
g++ main.cpp -o main main.cpp: In function 'int sign(int)': main.cpp:7:1: warning: control reaches end of non-void function [-Wreturn-type] 7 | } | ^ Предупреждение, не ошибка — но результат непредсказуем.

Вызов такой функции при 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.

★★ Напиши void-функцию printStars(int count), которая циклом выводит count звёздочек * подряд, затем перевод строки. Вызови её дважды с разными аргументами.

Шаги: 1) функция ничего не возвращает — значит тип возврата void; 2) параметр count — целое число, int; 3) внутри — цикл, который повторяется count раз и каждый раз выводит один символ *; 4) после цикла — отдельный вывод "\n"; 5) вызови функцию дважды с двумя разными числами, чтобы разница была видна.

★★ Напиши функцию double average(double a, double b), возвращающую среднее арифметическое двух чисел, вызови её и выведи результат через std::cout.

Шаги: 1) результат — дробное число (среднее двух чисел почти никогда не целое), значит и параметры, и тип возврата — double; 2) тело — одна строка: сложи оба параметра и раздели на 2.0 (не на 2 — так надёжнее получить именно дробное деление); 3) вызови average с двумя числами и выведи результат.

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 Engine?

Функции-члены класса в Unreal C++ (методы Actor'ов и компонентов) устроены по тем же правилам — тип возврата, имя, параметры, return. Разница появится позже: пометив функцию макросом UFUNCTION(BlueprintCallable), её можно будет вызывать ещё и из Blueprint — но сама функция внутри остаётся обычной C++ функцией, ничем принципиально не отличающейся от square из этого урока.