JavaRush /Курсы /C++ SELF /Сигнатура функции: параметры и тип

Сигнатура функции: параметры и тип

C++ SELF
14 уровень , 1 лекция
Открыта

1. Анатомия сигнатуры: из каких частей она состоит

Когда вы впервые слышите слово «сигнатура», мозг может попытаться вспомнить подпись в паспорте или автограф. И это, в общем-то, близко: сигнатура функции — это её «официальная подпись» в коде. Она объясняет компилятору и людям, как эту функцию можно вызывать, что ей нужно на вход и что она обещает вернуть. Если сигнатура непонятная или «врёт», дальше ломается всё: вызов, чтение кода, поиск ошибок.

В C++ сигнатура — это не просто формальность. Компилятор проверяет вызовы функций именно по сигнатуре: совпадает ли количество аргументов, подходят ли типы, можно ли использовать результат как значение. А ещё сигнатура — это часть «контракта» между вашим main() и остальными кусками программы: договорились, что функция возвращает bool — значит, её можно честно поставить в if.

Если смотреть на определение функции как на «шапку + тело», то сигнатура — это почти вся «шапка». Она описывает, что это за функция и как с ней общаться. Важно научиться видеть в сигнатуре структуру, а не набор символов. Тогда вы перестанете бояться длинных строк и начнёте читать их как обычное предложение: «эта функция берёт то-то и возвращает то-то».

Сигнатуру можно представить как три основные части: тип возврата, имя функции, список параметров (в круглых скобках). Тело { ... } — это уже реализация, но про неё компилятор «узнаёт» позже; сначала он должен понять именно подпись.

Общая форма

Пока мы работаем без продвинутых спецэффектов, держим в голове простую форму:

тип_возврата имя_функции(параметры) {
    // тело функции
}

Например:

int sum(int a, int b) {
    return a + b;
}

Табличка-подсказка: как читать сигнатуру

Чтобы не путаться, удобно один раз зафиксировать роли частей сигнатуры.

Часть Пример Смысл
Тип возврата
int
Какой тип значения функция возвращает (или
void
, если не возвращает)
Имя
sum
Как мы будем вызывать функцию
Параметры
(int a, int b)
Какие значения нужно передать внутрь

2. Параметры: что это такое и как выбирать их правильно

Параметры — это «входные переменные» функции. Они создаются каждый раз при вызове и живут только внутри функции. С практической точки зрения параметры — это способ сделать функцию универсальной: вместо «посчитать сумму 3 и 4» мы пишем «посчитать сумму любых двух чисел». И чем лучше вы подбираете параметры, тем меньше у вас соблазна лезть во внешние переменные и устраивать в программе телепатию.

При этом параметры — не просто «переменные в скобках». Они формируют правила вызова: порядок важен, тип важен, количество важно. Компилятор не угадывает, что вы «имели в виду». Он верит сигнатуре как налоговой декларации: написали — отвечайте.

Параметр = тип + имя

Минимально понятный параметр выглядит так: int count, char ch, std::string text.

#include <string>

int count_digits(std::string text) {
    int cnt = 0;
    for (char c : text) {
        if (c >= '0' && c <= '9') cnt += 1;
    }
    return cnt;
}

Здесь text — параметр. Он появился только внутри count_digits(), снаружи его как будто не существует.

Пока мы не углубились в тему «передача по ссылке», будем честны: std::string здесь передаётся по значению (копируется). Для учебных примеров — нормально. Оптимизацию и «как не копировать лишнего» мы разберём позже.

Порядок параметров — часть договора

Очень частая ошибка новичка: «ну я же передал два числа, какая разница в каком порядке?». Разница огромная — потому что параметры в сигнатуре стоят в конкретном порядке.

int rectangle_area(int width, int height) {
    return width * height;
}

Если вы вызываете rectangle_area(2, 10), то width == 2, height == 10. Компилятор не может догадаться, что вы «вообще-то имели в виду наоборот».

Сколько параметров делать

Есть практичное правило «на чувство»: если в функцию нужно передать 6–7 параметров, вы почти наверняка пытаетесь засунуть в неё слишком много ответственности. Но прямо сейчас мы ещё не обсуждаем проектирование больших моделей данных, поэтому действуем проще: параметров должно быть ровно столько, сколько нужно для работы функции, без магии и глобального контекста.

4. Аргументы: чем они отличаются от параметров

Если параметры — это то, что написано в объявлении/определении функции, то аргументы — это то, что вы реально передаёте при вызове. Казалось бы, мелочь в терминах, но на практике путаница «параметр/аргумент» приводит к странным объяснениям: «я поменял аргумент внутри функции, но снаружи не поменялось». На самом деле вы меняли параметр, то есть локальную копию — и всё логично.

Давайте зафиксируем на очень коротком примере и больше не путаться.

#include <iostream>

int add(int a, int b) {      // a и b — параметры
    return a + b;
}

int main() {
    std::cout << add(10, 20) << '\n';  // 10 и 20 — аргументы
}

Аргументом может быть выражение

Аргумент — это не обязательно «голое число». Это может быть переменная, выражение, результат другой функции — что угодно, что вычисляется в значение подходящего типа.

#include <iostream>

int square(int x) {
    return x * x;
}

int main() {
    int a = 3;
    std::cout << square(a + 1) << '\n'; // 16
}

Обратите внимание: a + 1 — это выражение, но оно вычисляется в int, поэтому подходит параметру int x.

5. Тип возврата и использование результата

Тип возврата — это часть сигнатуры, которую новичок часто воспринимает как «ну надо что-то написать слева». На самом деле это ключевая часть контракта: тип возврата определяет, можно ли использовать функцию как значение, можно ли положить её результат в переменную, можно ли поставить её в условие if, и вообще что именно мы получаем «на выходе».

При этом сегодня мы не углубляемся в тонкости return и «все пути должны вернуть значение» — это будет отдельно. Сейчас нам важно научиться выбирать тип результата и понимать последствия.

Функция возвращает значение: int, double, bool, std::string

Например, функция-проверка обычно возвращает bool, потому что её удобно читать в if:

#include <string>

bool is_short(std::string text) {
    return text.size() < 5;
}

Теперь это читается естественно:

if (is_short(name)) { /* ... */ }

А функция-вычисление часто возвращает число:

double average(int a, int b) {
    return (a + b) / 2.0;
}

Функция ничего не возвращает: void

Иногда функция делает действие: печатает, рисует рамку, показывает меню. В таком случае возвращать значение не обязательно — используем void.

#include <iostream>

void print_separator() {
    std::cout << "----------\n"; // ----------
}

И вот тут важная вещь: void-функцию нельзя «подставить как значение». То есть такое писать не надо:

std::cout << print_separator(); // ошибка: print_separator ничего не возвращает

Вызов функции как часть выражения

Один из самых приятных эффектов функций с возвращаемым значением — их можно использовать как строительные блоки в выражениях. Вы уже умеете складывать числа и сравнивать их. Вызов функции, которая возвращает int, ведёт себя почти как int: его можно сложить, сравнить, вывести, присвоить.

Это похоже на «вызвал функцию — получил значение», и это очень полезная модель мышления для новичка. Особенно если вы хотите, чтобы main() был похож на сценарий: «получить данные → вычислить → вывести».

#include <iostream>

int double_value(int x) {
    return x * 2;
}

int main() {
    std::cout << double_value(21) << '\n'; // 42
}
#include <iostream>

int triple(int x) {
    return x * 3;
}

int main() {
    int v = triple(7);
    std::cout << v << '\n'; // 21
}

6. Правила объявления: что компилятор должен знать о функции

Сейчас будет момент, который в реальной жизни вызывает примерно 30% всех «почему оно не компилируется?!». Компилятор читает файл сверху вниз. Это значит: когда он встречает вызов foo(...), он должен уже понимать, что такое foo, сколько у неё параметров и какого они типа. То есть функция должна быть объявлена до места использования.

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

«Объявлена» vs «реализована» на бытовом уровне

Пока упростим терминологию: когда компилятор видит строку вида int sum(int a, int b), он уже знает сигнатуру. Если после неё идёт { ... }, значит мы сразу дали и объявление, и реализацию в одном месте.

А вот «прототипы» (объявление без тела) и «порядок кода в файле» — это следующий шаг, чтобы можно было держать main() наверху и не превращать начало файла в простыню из функций. Но туда мы пока не забегаем.

Мини-схема: как компилятор «видит» файл

flowchart TD
    A[Компилятор читает файл сверху вниз] --> B{Встретил вызов функции}
    B -->|Функция уже объявлена выше| C[Проверяет сигнатуру и компилирует вызов]
    B -->|Функция ещё не объявлена| D[Ошибка: функция не объявлена / not declared]

7. Практический мини-проект: «TextLab» — консольный анализатор строк

Сейчас соберём маленькое приложение, которое будет с нами ещё некоторое время: меню, выбор действия, обработка строки. Наша цель не в том, чтобы написать «идеальный продукт», а в том, чтобы на примере увидеть, как сигнатуры делают код читаемым. Мы будем добавлять функции так, чтобы main() становился сценарием, а детали жили в отдельных блоках.

Пока сделаем три функции: печать меню (void), подсчёт цифр (int), проверка «есть ли пробел» (bool). И всё это аккуратно подключим в main().

Печать меню: void, без параметров

#include <iostream>

void print_menu() {
    std::cout << "1) Count digits\n";
    std::cout << "2) Contains space?\n";
    std::cout << "0) Exit\n";
}

Заметьте: параметров нет, потому что меню всегда одинаковое. Тип возврата void, потому что функция ничего «не отдаёт» — она просто печатает.

Подсчёт цифр: int, один параметр

#include <string>

int count_digits(std::string text) {
    int cnt = 0;
    for (char c : text) {
        if (c >= '0' && c <= '9') cnt += 1;
    }
    return cnt;
}

Сигнатура говорит сама за себя: «дай мне строку — я верну количество цифр».

Проверка условия: bool, один параметр

#include <string>

bool contains_space(std::string text) {
    for (char c : text) {
        if (c == ' ') return true;
    }
    return false;
}

Опять же: функция возвращает bool, поэтому её удобно использовать в if.

Собираем main() как сценарий

#include <iostream>
#include <string>

void print_menu() {
    std::cout << "1) Count digits\n";
    std::cout << "2) Contains space?\n";
    std::cout << "0) Exit\n";
}

int count_digits(std::string text) {
    int cnt = 0;
    for (char c : text) {
        if (c >= '0' && c <= '9') cnt += 1;
    }
    return cnt;
}

bool contains_space(std::string text) {
    for (char c : text) {
        if (c == ' ') return true;
    }
    return false;
}

int main() {
    std::string text;
    std::getline(std::cin, text);

    print_menu();

    int choice = 0;
    std::cin >> choice;

    if (choice == 1) {
        std::cout << count_digits(text) << '\n'; // например: 3
    } else if (choice == 2) {
        std::cout << contains_space(text) << '\n'; // 0 или 1
    }
}

Да, это приложение ещё «сыроватое» (например, мы пока не повторяем меню в цикле, не обрабатываем все варианты, и смешали getline с >>). Но для сегодняшней темы оно идеально: по нему видно, как сигнатуры определяют, что можно сделать с результатом функции.

8. Типичные ошибки

Ошибка №1: путаница между параметрами и аргументами.
Часто начинающий программист говорит: «я изменил аргумент внутри функции». На самом деле внутри функции вы меняете параметр — локальную переменную, созданную при вызове. Аргумент существует в месте вызова и может быть хоть a + 1, его нельзя «потрогать» изнутри функции напрямую.

Ошибка №2: неправильный выбор типа возврата (особенно int вместо bool, или наоборот).
Иногда пишут функцию is_valid(...), но возвращают int и начинают кодировать смысл числами: 0 — плохо, 1 — хорошо. Формально работает, но читается хуже и провоцирует ошибки. Если смысл результата «да/нет», то bool делает код честнее.

Ошибка №3: попытка использовать void-функцию как значение.
Конструкция вида std::cout << print_menu(); выглядит логично, если думать «всё можно вывести». Но void означает «нет значения», поэтому такой код не компилируется. Если нужно вывести что-то — пусть функция возвращает строку, либо пусть печатает сама, но тогда её вызов должен быть отдельной инструкцией.

Ошибка №4: вызов функции до того, как компилятор увидел её сигнатуру.
Новички часто пишут main() наверху, а функции — ниже, и получают ошибку «функция не объявлена». На текущем этапе лечения два: либо поднимать определения функций выше main(), либо (чуть позже) использовать прототипы. Сегодня мы придерживаемся первого, самого прямого варианта.

Ошибка №5: “параметры без смысла” — имена вроде a, b, x1, x2 там, где важна читаемость.
В математической задаче a и b ещё терпимо, но в прикладном коде лучше писать width, height, text, count. Сигнатура — это часть документации к вашей функции. Чем яснее она читается, тем меньше вы будете «дебажить глазами» собственный код через неделю.

1
Задача
C++ SELF, 14 уровень, 1 лекция
Недоступна
Два итога
Два итога
1
Задача
C++ SELF, 14 уровень, 1 лекция
Недоступна
Печать баннера
Печать баннера
1
Задача
C++ SELF, 14 уровень, 1 лекция
Недоступна
Анализ строки
Анализ строки
1
Задача
C++ SELF, 14 уровень, 1 лекция
Недоступна
Мини отчёт
Мини отчёт
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ