[БЕЗ_ЗВУКА] В самом
начале нашего курса мы вам показывали,
что в C++ можно создавать свои типы данных и встраивать их в уже существующие.
Мы это показывали вам на примере структуры Person, которая представляла человека.
Дайте вспомним ее.
Итак, у нас была структура Person,
которая представляла человека,
у которого было имя, фамилия и возраст.
И мы говорили, что мы можем завести вектор типа Person,
называли его staff и добавляли туда
Ивана Иванова,
которому было 25 лет.
Вот таким образом мы пользовались этой структурой.
А теперь давайте представим,
что мы провели перепись населения города Москвы,
и у нас есть функция, которая возвращает всех людей, которые живут в Москве.
Она возвращает вектор Person,
и мы ее назовем GetMoscowPopulation.
Я специально не привожу тело этой функции, потому что оно нам сейчас неважно.
Важно, что у нас просто есть какая-то функция,
которая возвращает список людей, которые живут в Москве.
И давайте мы эту функцию вызовем.
Скомпилируем нашу программу.
Она у нас компилируется.
Хорошо.
Теперь мы хотим сделать следующее: мы хотим красиво,
форматированно вывести, сколько людей живет в Москве.
И для этого мы напишем функцию.
Ну будет функция, которая ничего не возвращает,
— PrintPopulationSize.
И она принимает вектор людей,
назовем его просто p, и выводит красивое сообщение.
"There are" << p.size
() << [ЗВУК]
"people in Moscow",
и перевод строки.
Давайте ее вызовем и передадим
туда наш вектор people.
Скомпилируем нашу программу,
она компилируется, и давайте ее запустим.
Вот наша программа вывела,
что в Москве 12,5 млн людей.
Вы, возможно, могли заметить,
что это сообщение в консоли появилось не мгновенно, не сразу.
Давайте я еще раз запущу нашу программу, вы увидите какую-то временную задержку.
Вот, сообщение появляется не сразу.
И давайте сделаем такую вещь: давайте замеряем,
сколько времени выполняется функция GetMoscowPopulation,
а сколько работает функция PrintPopulationSize.
Сделаем мы это так.
Мы подключим библиотеку chrono.
Это специальная библиотека в C++ для работы с промежутками времени.
После using namespace std мы напишем using namespace
std: :chrono, а дальше сделаем вот такие вещи.
Мы запомним момент времени сразу
непосредственно перед функцией GetMoscowPopulation.
Это делается так: мы пишем auto start =
steady_clock: :now().
Мы с вами уже говорили, что auto — это способ сказать компилятору:
«Я не хочу печатать руками тип переменной, выведи его сам».
И вот здесь мы именно просим компилятор самому догадаться,
какой тип возвращает вот этот вызов.
И такой же вызов сделаем
после вызова функции GetMoscowPopulation.
И давайте выведем, сколько работала эта функция,
сколько миллисекунд работала эта функция.
Выведем имя функции и выведем
вот таким образом,
[ЗВУК] сколько
работала наша функция в миллисекундах.
И сделаем то же самое для функции PrintPopulationSize.
Скопируем, только auto нам теперь не нужен,
потому что переменная start у нас объявлена, вставим сюда,
здесь то же самое, и поменяем названием функции на PrintPopulationSize.
Итак, давайте сначала скомпилируем нашу программу.
Она у нас компилируется, отлично!
Значит, еще раз перед тем, как мы ее запустим: в переменную start мы записали
момент времени до запуска функции, вызвали функцию,
запомнили момент времени после вызова функции.
И вывели в консоль, сколько миллисекунд прошло,
пока выполнялась функция GetMoscowPopulation.
И то же самое сделали для функции PrintPopulationSize.
Запустим нашу программу.
Она отработала, и мы видим, что мы забыли перевод строки,
и у нас плохо отформатировался текст.
Давайте снова выполним нашу программу.
И что мы видим?
Мы видим, что...
Да, тут еще нужен пробел.
Мы видим, что функция GetMoscowPopulation
работала 609 мс, а PrintPopulationSize — 1034 мс.
Представляете, что у нас происходит?
У нас функция, которая возвращает векторы с 12 млн элементов (причем,
она его еще откуда-то берет!), она работает быстрее, чем функция
PrintPopulationSize, которая, вроде бы, всего лишь печатает размер вектора.
Она не делает больше ничего.
Вот она.
Но при этом она работает дольше.
Почему так?
Ну, потому что, мы с вами говорили,
что в C++ параметры в функцию передаются по значению.
То есть вот эта вот переменная p — это полная глубокая копия
вектора из 12,5 млн элементов, которые вернула функция GetMoscowPopulation.
И, соответственно, чтобы всего лишь распечатать его размер,
мы тратим целую секунду на то, чтобы выполнить копирование.
Конечно же, в реальных программах такое недопустимо,
и с этим надо как-то бороться.
И, собственно, что мы можем сделать?
Мы с вами говорили, что в C++ есть способ
менять фактические аргументы функции с помощью передачи их по ссылке.
Давайте это сделаем.
Давайте в функцию PrintPopulationSize вектор передадим по ссылке.
Я добавлю амперсенд в тип параметра функции PrintPopulationSize.
Давайте снова скомпилируем нашу программу и запустим ее.
Посмотрим, что она вывела.
Она сказала, что теперь PrintPopulationSize отработал за 0 мс.
Мы добились того, чего хотели.
Теперь при вызове PrintPopulationSize не происходит копирования.
В нее, в эту функцию, передается ссылка на тот вектор,
который мы получили из функции GetMoscowPopulation, — собственно,
на вектор people, — и мы просто печатаем размер.
Теперь все у нас хорошо.
Однако у этого решения есть ряд недостатков.
Недостаток первый: давайте посмотрим на функцию PrintPopulationSize.
Из ее названия следует, что она просто печатает размер вектора.
Если мы посмотрим только на ее объявления, не заглядывая в тело, что мы увидим?
Ага! Эта функция что-то печатает и она
принимает вектор.
Наверное, она просто берет, узнает его размер и выводит это на экран.
Но мы видим, что она принимает вектор по ссылке.
А мы говорили, что передача по ссылке — это способ
изменить фактический параметр функции, то есть тот объект, который в нее передан.
И, глядя на это объявление функции, мы можем решить, что функция,
которая называется «распечатать размер», еще и меняет вектор.
И это очень странно и вводит в заблуждение.
Это — первый недостаток нашего решения.
Второй недостаток следующий.
Вот смотрите, мы берем и передаем в нашу функцию объект people.
При этом больше мы этот объект никак не используем.
И мы можем захотеть не создавать промежуточную переменную,
а сразу же передать в функцию PrintPopulationSize результат вызова
функции GetMoscowPopulation.
Давайте попробуем это сделать, но только я не буду удалять первую часть кода,
я просто вместо переменной people передам в PrintPopulationSize
вызов GetMoscowPopulation.
И попробую скомпилировать нашу программу.
Запускаем компиляцию и видим: программа не компилируется.
Я делаю двойной клик на сообщении об ошибке и вижу,
что программа не компилируется как раз в том месте,
где мы вызываем комбинацию двух наших функций.
Дело в том, что по правилам С++ результат
вызова функции не может быть передан по ссылке в другую функцию.
Мы будем далее с вами рассматривать и разбирать, почему так происходит.
Сейчас мы в эти детали углубляться не будем.
Но просто факт остается фактом: мы не можем в C++ передать результат
вызова одной функции в другую функцию, если она принимает его просто по ссылке.
Итак, у нас есть две проблемы с нашим текущим решением.
Неужели мы никак не сможем найти идеальное решение,
которое не будет обладать указанными недостатками, но при этом не заставит
нас выполнять глубокое копирование при каждом вызове функции печати размера.
И выход есть.
Это — принимать параметр функции в PrintPopulationSize по константной ссылке.
Чтобы это сделать, я напишу ключевое слово const слева от типа параметра.
Смотрите, теперь у нас p — это константная ссылка на вектор Person.
Ну, потому что это все еще ссылка, у нас есть знак &, но есть еще слово const.
И давайте посмотрим, что у нас...
что мы получили.
Во-первых, давайте посмотрим на наш вызов, на нашу комбинацию вызовов двух функций.
Скомпилируем нашу программу.
Она компилируется.
Да!
По константной ссылке в C++ можно принимать результат вызова других функций.
Теперь вектор, который возвращается из GetMoscowPopulation попадает
в нашу функцию PrintPopulationSize и захватывается по константной ссылке.
Это работает.
Дальше давайте вернем, как у нас здесь было, передадим сюда вектор people,
снова скомпилируем нашу программу, запустим ее и увидим,
что PrintPopulationSize выполняется за 0 милисекунд.
То есть при передаче по константной ссылке у нас не произошло
лишнего копирования, ну потому что константная ссылка — это все еще ссылка,
и поэтому копирования не происходит.
Таким образом что мы получили?
За счет передачи по константной ссылке мы избавились от лишнего копирования,
мы теперь не вводим в заблуждение пользователя нашей функции,
потому что он видит, что параметр принимается по константной ссылке,
то есть параметр не будет изменен.
И мы имеем возможность передавать в функцию
PrintPopulationSize вектора, которые возвращаются из других функций.
Таким образом константная ссылка — это способ
сэкономить на ненужном, лишнем копировании при передаче параметров функций,.
функции.
Ну и кроме этого, она защищает нас от случайного
изменения фактических параметров функций.
Вот смотрите, допустим, мы в нашей функции,
которая принимает все население Москвы,
вдруг случайно захотели добавить туда какого-нибудь жителя Санкт-Петербурга.
Допустим, это будет какой-нибудь Владимир Петров, Петров.
И пусть ему будет 40 лет.
Вот мы писали функцию вывода размера населения,
ну и как-то так случайно мы решили добавить туда еще одного жителя.
И если наша ссылка будет не константной — вот я убрал
ключевое слово const — то наша программа скомпилируется,
она разрешит нам добавить еще одного жителя в Москву.
Но если же мы будем принимать параметр по константной ссылке,
то программа не компилируется,
потому что мы не можем изменять фактические параметры,
которые мы получаем, принимаем по константной ссылке.
Таким образом давайте подведем итог.
Константная ссылка в C++ — это способ избежать
копирования аргументов при вызове функции.
Кроме того, константная ссылка защищает нас от
случайного изменения фактических аргументов функций.
Ну и теперь вы знаете, что чтобы объявить константную ссылку,
нужно приписать ключевое слово const слева от имени типа.