Родитель
							
								
									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(); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | Объявления и определения важны при делении программы на файлы. | ||||||
					Загрузка…
					
					
				
		Ссылка в новой задаче