# Лекция 3. Функции, указатели, ссылки, структуры ## Функции (повторение) Функция — это именованный блок кода с формальными параметрами и возвращаемым значением. Пример: ```cpp double multiply(double x, int y) { return x * y; } ``` Здесь `double` — тип возвращаемого значения (результата), `multiply` — имя функции, `(double x, int y)` — список формальных параметров, а в фигурных скобках — тело функции. Если вместо типа возвращаемого значения указать `void`, это значит, что функция значения не возвращает. Тип возвращаемого значения, имя функции и список формальных параметров могут быть разнесены по строкам в соответствии со стилем и читаемостью. Например, мы в лабораторных работах предпочитаем тип возвращаемого значения писать на отдельной строке. С точки зрения C++ это безразлично. Типы ее формальных параметров составляют *сигнатуру* функции. Отметим, что тип возвращаемого значения, имя функции и имена параметров в сигнатуру не входят. Например, сигнатура `multiply`: `(double, int)`. Оператор `return` определяет возвращаемое значение и немедленно завершает функцию. В `void`-функциях тоже можно использовать `return` без указания результата, чтобы немедленно выйти из функции. **Внимание.** Выполнение не-`void` функции всегда должно заканчиваться `return`, хотя C++ не отслеживает это жестко (но может выдать предупреждение). Внутри тела функции её формальные параметры являются локальными переменными. Они независимы от переменных в месте вызова функции, даже если у них одинаковые имена: ```cpp void func(int x) { x = 66; } ... int x = 42; func(x); // x == 42 ``` При вызове функции значения, переданные ей в качестве аргументов, *копируются* в её переменные-параметры. ## Указатели (повторение) **Примечание.** В этой лекции не рассматривается динамическое выделение памяти, а только сами указатели как тип данных и их применение в связи с функциями. Всю память компьютера можно представить как массив байтов. Тогда индекс в этом массиве, то есть номер ячейки памяти, называется *адресом,* а переменная, содержащая адрес, называется *указателем.* При объявлении указателей перед именем переменной ставится звездочка. Например, так объявляется указатель на действительное число: ```cpp double* r1; ``` Часто звездочку прижимают к имени типа, а не переменной, как в примере. Есть известная «ловушка»: ```cpp double* x, y; ``` Здесь только `x` является указателем (имеет тип `double*`), а `y` является обычной переменной (имеет тип `double`). Надо либо ставить звездочку перед каждой переменной-указателем, либо объявлять каждую переменную отдельно (это почти всегда нагляднее). В указатель записывается не значение переменной, а ее адрес. Адрес берется *оператором взятия адреса* в виде амперсанда (`&`): ```cpp double x = 3.14; double* p = &x; ``` Вот как расположены при этом данные в памяти: ``` адреса: 0 1 8 9 10 11 12 42 43 44 45 46 +---+- -+---+---+---+---+- -+---+---+---+---+- ячейки: | | ..... | 3.14 | ..... | 8 | ... +---+- -+---+---+---+---+- -+---+-.'+---+---+- ↑\_____________/ \__.'_________/ | x .' p | .' &x = 8 = ...............' ``` Чтобы, имея указатель, обратиться к тем данным, адрес которых он хранит, используется оператор *разыменования* в виде звездочки: ```cpp *p = 2.71; // x = 2.71 ``` Есть специальное значение указателя — нулевой: `NULL`, `0` или `nullptr`. Указатель, хранящий такой адрес, запрещено разыменовывать. Начальное значение указателя, если оно не присвоено явно, не определено, как и для любых других переменных встроенных типов. Таким образом, переменной-указателем нельзя корректно пользоваться, пока ей что-нибудь не присвоено. ### Висячие указатели (dangling pointers) К сожалению, C++ не отслеживает, что значение указателя всегда корректно. Рассмотрим пример: ```cpp int* p = nullptr; if (...) { int x; p = &x; ... } cout << *p; ``` В последней строке `p` указывает на переменную, которая объявлена внутри `if` и уже не существует после выхода из фигурных скобок. Поэтому, хотя указатель и хранит не `nullptr`, разыменовывать его нельзя. Такие указатели на данные, которых уже нет, называются *висячими (dangling).* Другой пример: ```cpp int* func() { int x = 42; return &x; } ... auto p = func(); cout << *p; ``` Здесь функция возвращает адрес локальной переменной. Однако локальные время жизни локальных переменных ограничено функцией, поэтому пользоваться таким возвращаемым значением нельзя. При работе с указателями надо всегда думать о том, чтобы время жизни указателя не превышало время жизни данных, адрес которых указатели хранят. ## Ссылки Ссылка (reference) — это новое имя для существующего объекта. Объект может быть переменной или её частью, такой как элемент массива. Ссылки объявляются с использованием амперсанда: ```cpp int var = 42; int& ref = var; ``` Не следует путать амперсанд при объявлении ссылок с амперсандом-оператором взятия адреса! Обращение к ссылке эквивалентно обращению к тому, на что она ссылается: ```cpp cout << ref; // 42 ref = 66; cout << var; // 66 ``` В частности, так как ссылка не является самостоятельной переменной, её адрес — это адрес того, на что она ссылается, а явное разыменование не нужно (если это не ссылка на указатель, конечно): ```cpp if (&var == &ref) { ... } // истинно *ref = 66; // ОШИБКА: обращение к ref — обращение к var, // а var не указатель, разыменовать её нельзя. ``` Поскольку ссылка — новое имя для *существующего* объекта, у нее всегда должно быть начальное значение: ```cpp int& ref; // ОШИБКА: новое имя для чего? ``` Не бывает «нулевой ссылки», подобной нулевому указателю. Даже вне связи с функциями ссылки могут применяться, чтобы дать более короткие или понятные имена в коде: ```cpp double& first = some_vector[0]; // ... fisrt = 0; ``` ### Передача входных параметров функций по ссылкам Рассмотрим функцию, суммирующую элементы вектора: ```cpp double sum(vector xs) { double total{}; for (double x : xs) { total += x; } return total; } ``` Вспомним, что при вызове функции значения аргументов *копируются* в переменные-формальные параметры, то есть в `xs` будет помещена копия вектора, который передан функции. Если этот вектор большой, будет потрачено много лишней памяти, кроме того, это копирование бесполезно — функция не меняет `xs` даже внутри. Можно передавать `xs` по ссылке, чтобы не копировать вектор, а работать с той переменной, которую передали функции, напрямую: ```cpp double sum(vector& xs) { ... } ... vector xs; double s = sum(xs); ``` Однако есть две проблемы: * Нельзя вызвать `sum({1, 2, 3})`, потому что `{1, 2, 3}` — выражение, а не переменная. Это запрещено, потому что с помощью ссылки возможно поменять то, на что она ссылается, однако выражение поменять нельзя в принципе. (Можно изменить значение переменной, содержащей `5 = 3 + 2`, но нельзя поменять саму `5`, «пятерку как таковую».) * При чтении кода непонятно, не меняет ли `sum()` свой аргумент, и нет гарантий, что она этого не начнет делать в будущем. Итак, нужно сослаться на аргумент, но сделать этот так, чтобы с точки зрения `sum()` эта переменная была неизменяемой, даже если в месте вызова менять ее можно. Это делается с помощью константной ссылки: ```cpp double sum(const vector& xs) { ... } ``` При передаче параметров нетривиального типа (не `int`, `double` и т. п.), в том числе при передаче `std::vector` и `std::string`, рекомендуется по умолчанию использовать константную ссылку. ## Выходные параметры функций через указатели и ссылки Составим функцию для решения квадратного уравнения в действительных числах. Очевидно, что она принимает коэффициенты уравнения. Возвращает она три значения: * признак, что действительные решения есть; * корень `x1`; * корень `x2`. Однако у функции возвращаемое значение только одно, допустим, признак. Как вернуть корни? Можно сделать это через ссылки: ```cpp bool solve(double a, double b, double c, double& x1, double& x2) { auto d = b*b - 4*a*c; if (d < 0) { return false; } x1 = (-b + sqrt(d)) / 2*a; x2 = (-b - sqrt(d)) / 2*a; return true; } ``` Вызов функции будет выглядеть так: ```cpp double x1, x2; if (solve(3, 2, 1, x1, x2)) { cout << "x1 = " << x1 << "\n" << "x2 = " << x2 << "\n"; } else { cout << "Нет действительных корней.\n"; } ``` Можно было бы использовать указатели: ```cpp bool solve(double a, double b, double c, double* x1, double* x2) { auto d = b*b - 4*a*c; if (d < 0) { return false; } *x1 = (-b + sqrt(d)) / 2*a; *x2 = (-b - sqrt(d)) / 2*a; return true; } ``` Вызов функции будет выглядеть так: ```cpp double x1, x2; if (solve(3, 2, 1, &x1, &x2)) { cout << "x1 = " << x1 << "\n" << "x2 = " << x2 << "\n"; } else { cout << "Нет действительных корней.\n"; } ``` Какой вариант лучше и почему? В случае с указателями в функцию мог бы быть передан нулевой указатель: ```cpp solve(3, 2, 1, nullptr, &x2); ``` Программа успешно компилировалась бы, но при запуске аварийно завершилась, поскольку в функции `solve()` был бы разыменован нулевой указатель `x1`. Может показаться, что из-за этого вариант с указателями хуже: функция должна проверять, что ей не передали нулевой указатель, а со ссылками этого не потребовалось бы — ведь «нулевых ссылок» нет. Однако наличие особого значения у указателя — не только проблема, но и возможность связать с этим значением особую логику. Например, функция могла бы быть реализована так: ```cpp bool solve(double a, double b, double c, double* x1, double* x2) { auto d = b*b - 4*a*c; if (d < 0) { return false; } if (x1) { *x1 = (-b + sqrt(d)) / 2*a; } if (x2) { *x2 = (-b - sqrt(d)) / 2*a; } return true; } ``` Теперь, если передать в функцию `nullptr` в качестве `x1` или `x2`, она не будет вычислять соответствующий корень. Таким образом программа может сэкономить вычисления, если оба корня ей заведомо не нужны. Можно даже передать `nullptr` в качестве и `x1`, и `x2`, тогда функция просто проверит, есть ли действительные решения — возможно, конкретной программе, которая использует `solve()`, только это и нужно. Вывод: использовать для передачи выходных параметров указатели или ссылки зависит от того, нужна ли дополнительная гибкость логики, которую дает наличие особого значения — нулевого указателя. Заметим, что некоторые проекты предпочитают всегда использовать указатели, даже если предполагается, что они обязаны не быть `nullptr` никогда. Причина в том, что в случае ссылок по вызову `solve(a, b, c, x1, x2)` невозможно определить, какие из переменных после этой строки могут поменяться. Вызов же `solve(a, b, c, &x1, &x2)` ясно показывает, что `solve()` может поменять `x1` и `x2`. ## Структуры Структура — это пользовательский тип данных, представляющий собой совокупность именованных полей различных типов. Структуры удобны для того, чтобы сгруппировать несколько переменных, которые используются в программе совместно. Например, в задаче ЛР № 1 можно было бы объединить входные данные в структуру: ```cpp struct Input { std::vector numbers; size_t bin_count; }; ``` Здесь `Input` — имя структуры, а `numbers` и `bin_count` — её поля. Важно понять, что определение выше описывает тип данных (аналог `std::vector` или `std::string`), а не переменную, то есть код выше не описывает переменные `numbers` и `bin_count`, куда можно сохранить значения. Он описывает, что каждая переменная типа `Input` содержит поля `numbers` и `bin_count`, в которые уже можно сохранить конкретные данные. Переменные типа структур объявляются так же, как переменные других типов; именем типа выступает имя структуры: ```cpp Input x; ``` Говорят, что переменная `x` — экземпляр структуры. К полям структуры обращаются через точку: ```cpp cout << x.numbers.size(); // 0 cin >> x.bin_count; if (x.bin_count == 0) { ... } ``` Можно объявить несколько переменных типа структуры: ```cpp Input y; Input z; y.bin_count = 3; z.bin_count = 4; ``` Значения, хранимые в `x`, `y` и `z` будут независимы друг от друга. ### Инициализация полей Вернемся к объявлению `x`, чему было равно `x.bin_count` до её ввода? Так как в определении структуры для поля `bin_count` не было указано значения по умолчанию, то и конкретное начальное значение `x.bin_count` не определено. Удобно, чтобы у всех полей были начальные значения. Например, так можно инициализировать нулем `bin_count` любой новой переменной типа `Input`: ```cpp struct Input { vector numbers; size_t bin_count{}; // или size_t bin_count = 0; }; ``` Как известно, переменные типа `vector` по умолчанию содержат пустой вектор, поэтому для поля `numbers` о начальном значении заботиться не нужно. ### Оператор «стрелка» (`->`) На переменные типа структуры могут быть указатели: ```cpp Input* p = new Input; ``` Чтобы использовать оператор `.` для доступа к полям структуры, на которую указывает `p`, нужно сначала разыменовать `p`. Оператор `.` имеет наивысший приоритет, поэтому нужны скобки: `(*p).bin_count`. Это громоздко, поэтому в C++ введено оператор «стрелки» `->`, чтобы записать то же самое проще: `p->bin_count`. ## Перегрузка функций Можно объявить набор функций с одинаковыми именами, но разными сигнатурами: ```cpp // print(3.14) -> 3.14 void print(double x) { cout << x; } // print({1, 2, 3}) -> {1, 2, 3}; void print(const std::vector& xs) { cout << "{"; bool need_comma = false; for (auto x : xs) { if (need_comma) { cout << ", "; } else { need_comma = true; } print(x); // вызов print(double), а не рекурсия } cout << "}"; } ``` Говорят, что функция `print()` *перегружена (overloaded),* а каждая из конкретных функций называется её *перегрузкой.* Когда компилятор встречает вызов `print()`, он анализирует типы параметров и вызывает соответствующую перегрузку. Перегрузки бывают полезны в обобщенном коде, то есть таком, который готов работать с различными типами данных. Об это будет рассказано на последующих лекциях. ## Объявление и определение функции Будет ли компилироваться программа такого вида? ```cpp void foo() { bar(); } void bar() { foo(); } ``` Функция `foo()` вызывает функцию `bar()` раньше, чем она описана, что вызовет ошибку. Однако перенести описание `bar()` выше описания `foo()` нельзя: `bar()` содержит вызов `foo()`, который тогда окажется выше, чем описана `foo()`. Рассуждая логически, для того, чтобы скомпилировать `foo()`, компилятору не нужно тело функции `bar()` — достаточно знать, что такая функция есть, и какие у нее параметры (нет параметров). Сообщить компилятору о сигнатуре функции можно с помощью *объявления функции (declaration)*. В отличие от уже знакомого *определения функции (definition),* объявление не содержит тела, а кончается точкой с запятой: ```cpp void bar(); // объявление bar() void foo() { bar(); // вызов bar() } void bar() { // определение bar() foo(); } ``` Объявления и определения важны при делении программы на файлы.