Родитель
							
								
									9f922afaa9
								
							
						
					
					
						Сommit
						6c14501bc6
					
				| @ -0,0 +1,554 @@ | ||||
| # Лекция 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(); | ||||
| } | ||||
| ``` | ||||
| Объявления и определения важны при делении программы на файлы. | ||||
					Загрузка…
					
					
				
		Ссылка в новой задаче