[БЕЗ_ЗВУКА] Сейчас от нашего вектора нет никакой пользы, потому что мы только можем задать количество объектов в нем, создать их и удалить. Мы даже не можем никак обратиться по индексу к этим элементам. Вот давайте мы, собственно, это и сделаем: давайте добавим в наш вектор возможность обращаться к отдельным элементам. Для этого напишем в нем оператор квадратные скобки, который принимает индекс. И зададимся вопросом, а как же нам этот оператор реализовать? Потому что у нас есть указатель data, вот он, это указатель на первый элемент нашего вектора. Вот давайте рассмотрим это на слайде. У нас есть указатель data, он указывает на первый элемент вектора. А мы хотим уметь обращаться не только к первому элементу, но и к произвольному: ко второму, к третьему и так далее. Как же нам сделать такую вещь: как нам отступить от этого элемента «один элемент» так, чтобы получить указатель вот на этот? Или, допустим, отступить от этого элемента 2, чтобы получить указатель вот на этот элемент? Это делается очень просто — прибавлением числа к указателю. То есть если у нас есть указатель data, который указывает на первый элемент вектора, то если мы прибавим к data единицу, мы получим указатель на следующий — на второй элемент вектора. И аналогично для других элементов: третий элемент вектора — это data + 2, а последний элемент — это data + size − 1. Таким образом, наш оператор доступа по индексу реализуется очень просто. Мы пишем return * (data + index). Давайте мы это скомпилируем. Это компилируется, и проверим, что это работает. Давайте пока заменим string на int и допустим напишем вот так: for (int i = 0, i < 5) sv [i] = 5 − i. А потом пройдемся по нашему вектору и выведем значение нашего вектора. Это компилируется, запускаем, и у нас вывелись числа 5, 4, 3, 2, 1. То есть мы как раз записали убывающие числа, начиная с пятерки, и действительно они у нас записались. Таким образом, вот такая вот реализация для оператора квадратные скобки — она прекрасно работает, и мы действительно выполняем, прибавляя к указателю число, мы просто смещаемся к нужному нам элементу вектора. На что здесь стоит обратить внимание? Во-первых, смотрите, мы вот здесь вот, вот этой командой во сути выделили в куче пять целых чисел. Если мы вот здесь попросим вывести, например, 12-й элемент нашего вектора, при том что у нас их всего пять, то наша программа скомпилируется и даже может отработать. Вот, в данном случае у нас вывелось какое-то непонятное число. То есть в процессе работы программы нет никакого... Сам язык C++ никак не контролирует доступ к данным, которые мы осуществляем через указатель. Вот мы сейчас смогли спокойно прочитать число, которое лежит за границами нашего вектора. Однако это не значит, что мы это можем делать всегда. Давайте, опять же, заменим int на string. Скомпилируем нашу программу, запустим и увидим, как она благополучно упала. Так что несмотря на то, что сам C++ не контролирует доступ, есть еще операционная система, и вот она иногда реагирует на то, когда ваша программа лезет не туда. Собственно, почему со строкой у нас так не получилось? Потому что string — это класс, у него есть какая-то своя внутренняя структура, какие-то внутренние инварианты. И естественно, когда мы пытаемся выводить строку, мы обращаемся к какой-то случайной памяти, в которой, естественно, для нас мусор. И там никакие инварианты класса string не выполняются, и программа, естественно, падает. Вот таким образом мы с вами немного познакомились с арифметикой указателей. Давайте обратим внимание еще на такую вещь. К указателям можно не только прибавлять целые числа, но и вычитать их. Собственно, семантика здесь та же самая: мы точно так же отступаем в памяти на заданное количество объектов, просто в другую сторону. Вот на слайде приведен пример: у нас есть указатель p, который указывает на третий элемент в наборе из пяти элементов типа T. И тогда указатели p + 1 и p +2 указывают в одну сторону на эти элементы, а p − 1 и p − 2 указывают в другую сторону на эти элементы. Хорошо. Давайте рассмотрим еще один момент. Мы с вами говорили, что локальные переменные функций хранятся на стеке. Стек — это тоже область оперативной памяти. Поэтому у этих переменных тоже должен быть адрес — должен быть адрес в оперативной памяти, где они размещены. И этот адрес, его достаточно легко получить с помощью оператора & (амперсанд). Давайте рассмотрим пример. У нас есть функция main, в ней есть переменная x типа int = 5. И есть переменная y, которая является указателем на int, и она хранит в себе адрес переменной x. И значит, вы можете видеть, как это выглядит схематично. То есть переменная y является указателем на переменную x. Она указывает на ту область в памяти, где переменная x хранится. Давайте посмотрим на это в ide. Пока что перестанем пользоваться нашим вектором. Объявим переменную x типа int и объявим переменную y типа указатель на int и присвоим ей адрес переменной x. Дальше напишем: *y = 7. То есть присвоим объекту, на который указывает переменная y, значение 7. И выведем переменную x. Как вы думаете, что у нас сейчас появится на экране? Конечно же, семерка. Потому что переменная y указывает на переменную x. Мы туда, в ту область памяти, где размещена переменная x, записали семерку. И естественно, когда мы выводим переменную x, мы читаем из этой области памяти и получаем семерку. Хорошо. Давайте еще немного поиграемся с указателями на локальные переменные, а также с арифметикой указателей. Смотрите, давайте мы объявим три переменные: переменную a, которая пусть будет равна 43, переменную b, которая будет равна, например, 71, и переменную c, которая будет равна 89. А дальше мы сделаем вот что. Мы выведем на экран значение, которое лежит по такому указателю: мы берем указатель на переменную b и вычитаем из него единицу. И берем, опять же, указатель на переменную b и прибавляем к нему единицу. Компилируем, запускаем. 43, 89. Ведь правда, это 43 очень похоже на вот это 43? А это 89 очень похоже на вот это 89? Этот пример как раз таки еще раз демонстрирует, как работает арифметика указателей. Мы взяли указатель на int — вот он, адрес b, b — это переменная типа int, поэтому &b — это указатель на int. Вычли из него единицу, то есть мы переместились на один int, скажем так, влево. И попали в переменную a, которая равна 43. Здесь мы взяли адрес ближе к переменной b, сместились на один int в другую сторону и попали в переменную c, в которой лежит значение 89. Отлично. Продолжаем развлекаться с указателями на переменные и давайте посмотрим еще вот какой пример. Мы с вами говорили в самом начале этого блока про то, что локальные переменные функций размещаются в стеке, в стековых фреймах. Давайте заведем функцию f, объявим ей... а пусть вот перенесем в нее вот эти вот наши переменные, a и b. Здесь оставим переменную c, а вот здесь напишем вот такой цикл. Это будет цикл по i от 0 до 20. А здесь мы будем делать вот что. Мы будем вызывать функцию f, а потом будем выводить... вернее, даже не выводить, а будем запоминать в переменной x значения по указателю адрес c − i. И будем выводить << i << << x <<. Компилируем — компилируется, запускаем, и вот у нас вывелись какие-то числа. Давайте обратим внимание на 14-ю и 15-ю строки. Здесь записаны цифры 43 и 71. 43, 71. Давайте поменяем эти числа и снова запустим нашу программу. В 14-й и 15-й строке мы увидим 434 и 711. Что мы с вами только что сделали? Мы функцией main запустили функцию f. Она запустилась, выделила на стеке фрейм, разместила в этом фрейме свои переменные a и b, а дальше мы не знаем точно, где на стеке эти переменные лежат, но где-то они лежат. Поэтому мы пробуем разные смещения относительно своей переменной c и пытаемся найти стековый фрейм функции f. И мы это сделали. Вот. На 14 int-ов, скажем так, влево от переменной c лежит переменная a. И на 15 int-ов лежит переменная b. То есть мы смогли из функции c прямо вот залезть в стековый фрейм функции f после того, как она отработала. Мы в самом начале смотрели на то, что, когда функция отработала, то с ее стековым фреймом ничего не происходит, он не затирается никак. Мы просто переносим вершину стека. Вот мы сейчас в этом убедились. Ну и здесь мог возникнуть вопрос, почему мы на каждую итерацию цикла вызываем функцию f. Ну, дело в том, что когда мы выполняем вывод в поток вывода, то, как вы уже знаете, вот эти вот строки разворачиваются в вызов перегрузок операторов, то есть в вызов других функций, и эти функции, естественно, затирают стековый фрейм функции f. Поэтому мы перед каждым смещением пробуем, ну эту функцию еще раз запускаем, чтобы входить именно в ее стековый фрейм, а не во фрейм вывода в поток. Прекрасно. И у нас остался еще один важный пример, касающийся арифметики указателей. Мы говорили, что указатель — это адрес в памяти, и даже видели его целочисленное значение. Давайте объявим переменную, назовем ее d, которая равна адресу переменной c, и объявим переменную e = d + 1 и выведем эти переменные в консоль. Заодно вы увидите, что указатели можно выводить в консоль. Компилируем, запускаем и видим два шестнадцатиричных числа, которые вывелись в консоль. Если мы вычтем одно из другого, прямо вот я даже открою калькулятор, если мы вычтем из 40 16-ричный 3C, у нас получится 4. Теперь возьмем и, допустим, прибавим 3. Компилируем, запускаем. Снова из 48 16-ричных вычитаем 3C, получилось C, или в десятичной системе 12. Что этим примером хочется показать? Что когда мы прибавляем к указателю число, это разворачивается в то, что реально вот это значение целочисленное, которое хранится в указателе, оно изменяется на вот это вот число, умноженное на размер типа, на который указывает наш указатель. Вот здесь вот не зря было 4, потому что это int. У int на 64-битных платформах — размер 4. Даже вот, например, мы можем поменять все на uint64_t, запустить нашу программу, снова пойти в калькулятор и теперь вычесть из 50 38. Получается 24, то есть 3 * 8, 8 байт — это размер типа uint64_t. Таким образом, вы теперь понимаете, что, собственно, лежит под арифметикой указателей, и не только с логической точки зрения, что значит прибавление числа к указателю, но и как оно отражается на на конкретном значении, конкретном адресе, который хранится в указателе. Это все, что касается арифметики указателей, за одним исключением. Давайте вернемся, мы вообще в этом видео писали operator [ ] для нашего вектора. Давайте мы в него вернемся и скажем, что вот такой вот синтаксис, он достаточно громоздкий, тут нужно скобки ставить, звездочку ставить, это неудобно. Ему, этому синтаксису, есть эквивалент. Мы можем просто к указателю добавить в квадратных скобках тот индекс, и это развернется в обращение. Это вернет нам ссылку на объект, который на заданный индекс отстоит от нашего указателя. Давайте мы это скомпилируем. Правда, чтобы это прямо вот проверить, нужно шаблон-то наш применить. Вот оно компилируется, отлично. Давайте подведем итоги этого большого видео. Мы узнали, что прибавление числа X к указателю T изменяет его адрес на X * sizeof (T). При этом мы получаем возможность обратиться к объекту типа T, отстоящему на заданном расстоянии от нашего указателя. Кроме того, мы узнали, что оператор &, который пишется слева от имени переменной, позволяет получить адрес этой локальной переменной. Мы прекрасно посмотрели на то, что что при обращении к указателям не происходит никакого контроля доступа со стороны именно C++, ну и наконец, в финале мы увидели, что запись * (p + 5), как на слайде, эквивалентна просто p [5].