Вы не можете выбрать более 25 тем
Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
555 строки
24 KiB
Markdown
555 строки
24 KiB
Markdown
# Лекция 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<double> xs) {
|
|
double total{};
|
|
for (double x : xs) {
|
|
total += x;
|
|
}
|
|
return total;
|
|
}
|
|
```
|
|
|
|
Вспомним, что при вызове функции значения аргументов *копируются*
|
|
в переменные-формальные параметры, то есть в `xs` будет помещена копия вектора,
|
|
который передан функции.
|
|
Если этот вектор большой, будет потрачено много лишней памяти,
|
|
кроме того, это копирование бесполезно — функция не меняет `xs` даже внутри.
|
|
|
|
Можно передавать `xs` по ссылке, чтобы не копировать вектор,
|
|
а работать с той переменной, которую передали функции, напрямую:
|
|
|
|
```cpp
|
|
double sum(vector<double>& xs) { ... }
|
|
...
|
|
vector<double> xs;
|
|
double s = sum(xs);
|
|
```
|
|
|
|
Однако есть две проблемы:
|
|
|
|
* Нельзя вызвать `sum({1, 2, 3})`,
|
|
потому что `{1, 2, 3}` — выражение, а не переменная.
|
|
Это запрещено, потому что с помощью ссылки возможно поменять то,
|
|
на что она ссылается, однако выражение поменять нельзя в принципе.
|
|
(Можно изменить значение переменной, содержащей `5 = 3 + 2`,
|
|
но нельзя поменять саму `5`, «пятерку как таковую».)
|
|
|
|
* При чтении кода непонятно, не меняет ли `sum()` свой аргумент,
|
|
и нет гарантий, что она этого не начнет делать в будущем.
|
|
|
|
Итак, нужно сослаться на аргумент, но сделать этот так,
|
|
чтобы с точки зрения `sum()` эта переменная была неизменяемой,
|
|
даже если в месте вызова менять ее можно.
|
|
Это делается с помощью константной ссылки:
|
|
|
|
```cpp
|
|
double sum(const vector<double>& xs) { ... }
|
|
```
|
|
|
|
При передаче параметров нетривиального типа (не `int`, `double` и т. п.),
|
|
в том числе при передаче `std::vector<T>` и `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<double> 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<double> numbers;
|
|
size_t bin_count{}; // или size_t bin_count = 0;
|
|
};
|
|
```
|
|
|
|
Как известно, переменные типа `vector<T>` по умолчанию содержат пустой вектор,
|
|
поэтому для поля `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<double>& 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();
|
|
}
|
|
```
|
|
Объявления и определения важны при делении программы на файлы.
|