Глиф Учебники

Урок 7. Функции и ссылки

Курс «C++» · урок 7 из 10 · 4 минуты чтения

Когда одно и то же действие нужно в нескольких местах программы, его выносят в функцию — именованный кусок кода, который можно вызвать по имени. main — тоже функция, а sqrt или max — функции из стандартной библиотеки. Теперь пора писать свои.

Как устроена функция

C++
#include <iostream>
using namespace std;

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

void printLine(int length) {
    for (int i = 0; i < length; i++) {
        cout << "-";
    }
    cout << "\n";
}

int main() {
    cout << square(7) << "\n";
    int a = 3;
    cout << square(a + 1) << "\n";
    printLine(10);
    return 0;
}
Вывод
49
16
----------

Из чего состоит функция:

Значения, с которыми функцию вызывают (7, a + 1), называются аргументами. При вызове square(a + 1) параметр x получает значение 4.

в main int a = 3; cout << square( a + 1 ) << "\n"; вывод 16 аргумент 4 результат 16 функция тип имя параметр int square ( int x ) { return x * x; } x 4 4 * 4 = 16
При вызове square(a + 1) аргумент 4 попадает в параметр x, функция считает x * x, и return отдаёт 16 обратно в место вызова — это число и выводится на экран.

В функции, которая что-то возвращает, return должен сработать на любом пути выполнения. Если для какого-то случая его нет, результат непредсказуем; -Wall предупредит: control reaches end of non-void function.

Функция, которая отвечает «да» или «нет»

Функция может возвращать bool. Проверка числа на простоту — классика:

C++
#include <iostream>
using namespace std;

bool isPrime(int n) {
    if (n < 2) {
        return false;
    }
    for (int d = 2; d * d <= n; d++) {
        if (n % d == 0) {
            return false;
        }
    }
    return true;
}

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        if (isPrime(i)) {
            cout << i << " ";
        }
    }
    cout << "\n";
    return 0;
}
Ввод
30
Вывод
2 3 5 7 11 13 17 19 23 29

return false внутри цикла сразу выходит из функции — дальше проверять делители не нужно. Делители ищутся только до корня из n (d * d <= n): если у числа есть делитель больше корня, то есть и парный меньше корня.

Сначала объявить, потом вызвать

Компилятор читает файл сверху вниз. Функцию, которую вызывают в main, надо написать выше main — иначе ошибка was not declared in this scope. Если хочется держать main первой, выше ставят только объявление (прототип) — первую строку функции с точкой с запятой:

C++
#include <iostream>
using namespace std;

int cube(int x);   // объявление

int main() {
    cout << cube(3) << "\n";
    return 0;
}

int cube(int x) {  // определение
    return x * x * x;
}
Вывод
27

Объявление без определения соберётся, но на последнем шаге сборки даст ошибку undefined reference — об этом справка Ошибка undefined reference.

Передача по значению

Параметр — копия аргумента. Что бы функция ни делала с параметром, переменная снаружи не меняется:

C++
#include <iostream>
using namespace std;

void tryChange(int x) {
    x = 100;
}

int main() {
    int a = 5;
    tryChange(a);
    cout << a << "\n";
    return 0;
}
Вывод
5

Ссылки

Ссылка — второе имя для уже существующей переменной. Объявляется со знаком & после типа:

C++
#include <iostream>
using namespace std;

void swapValues(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}

int main() {
    int x = 1, y = 2;
    int& r = x;     // r — другое имя для x
    r = 10;
    cout << x << "\n";
    swapValues(x, y);
    cout << x << " " << y << "\n";
    return 0;
}
Вывод
10
2 10

Параметр int& a — не копия, а сама переменная вызывающего. Поэтому swapValues меняет x и y местами. Так функция может вернуть несколько результатов: через параметры-ссылки. В стандартной библиотеке такая функция уже есть — swap(x, y).

По значению — копия void tryChange(int x) main a 5 копия tryChange x 100 x = 100; в a осталось 5 По ссылке — второе имя int& r = x; x r 10 r = 10; одно значение: x тоже стал 10
При передаче по значению tryChange получает копию a и меняет только её, а ссылка int& r — второе имя той же переменной x, поэтому r = 10 меняет и x.

Аргументом параметра-ссылки может быть только переменная: swapValues(1, 2) не соберётся — числу 1 нельзя присвоить новое значение.

Ссылка на const — для больших данных

Передача вектора или строки по значению копирует их целиком. Для вектора из миллиона чисел это долго, а если функция вызывается много раз — очень долго. Решение — const и &:

C++
#include <iostream>
#include <vector>
using namespace std;

long long total(const vector<int>& v) {
    long long s = 0;
    for (int x : v) {
        s += x;
    }
    return s;
}

int main() {
    vector<int> data = {4, 8, 15, 16, 23, 42};
    cout << total(data) << "\n";
    return 0;
}
Вывод
108

const vector<int>& v — функция работает с самим вектором без копирования, а const запрещает его менять. Правило: простые типы (int, double, char) передавайте по значению, а vector, string и другие большие объекты — по const-ссылке, если функция их не меняет, и по обычной ссылке, если меняет.

Перегрузка

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

C++
#include <iostream>
#include <string>
using namespace std;

void show(int x) {
    cout << "int " << x << "\n";
}

void show(double x) {
    cout << "double " << x << "\n";
}

void show(const string& s) {
    cout << "string " << s << "\n";
}

int main() {
    show(3);
    show(2.5);
    show(string("hi"));
    return 0;
}
Вывод
int 3
double 2.5
string hi

Рекурсия

Функция может вызывать сама себя — это рекурсия. Факториал: n! = n · (n − 1)!, а 0! = 1.

C++
#include <iostream>
using namespace std;

long long factorial(int n) {
    if (n == 0) {
        return 1;              // база рекурсии
    }
    return n * factorial(n - 1);
}

int main() {
    cout << factorial(5) << "\n";
    cout << factorial(20) << "\n";
    return 0;
}
Вывод
120
2432902008176640000

У рекурсии обязательно должна быть база — случай, когда функция больше не вызывает себя. Без неё вызовы идут, пока не кончится память для них (стек), и программа падает. То же бывает и при правильной рекурсии, но очень глубокой — в сотни тысяч вызовов и больше.

вызовы идут вниз, до базы результаты — обратно вверх factorial(5) 5 * 24 = 120 factorial(4) 4 * 6 = 24 factorial(3) 3 * 2 = 6 factorial(2) 2 * 1 = 2 factorial(1) 1 * 1 = 1 factorial(0) 1 база: return 1
factorial(5) вызывает factorial(4) и так далее до базы factorial(0), которая сразу возвращает 1; затем результаты идут обратно вверх: 1, 1, 2, 6, 24 и наконец 120.

Локальные и глобальные переменные

Переменные, объявленные внутри функции или блока { }, — локальные: они существуют только там. У каждой функции свои локальные переменные, и одинаковые имена в разных функциях не мешают друг другу. Переменные вне всех функций — глобальные, они видны везде. На олимпиадах глобальными часто делают большие массивы, но в остальном лучше передавать данные через параметры: так понятнее, кто что меняет.

Коротко

Проверьте себя

1. Что выведет программа?
C++
#include <iostream>
using namespace std;

void addOne(int x) {
    x = x + 1;
}

int main() {
    int a = 5;
    addOne(a);
    cout << a << "\n";
    return 0;
}
2. Как передать большой вектор в функцию, которая его только читает?
3. Что значит void перед именем функции?
4. Что обязательно должно быть у рекурсивной функции?