Указатель в C++. Простой подход.

В следующей статье мы рассмотрим основную концепцию указателей в C+. Откровенно говоря, непонимание основ принципа рисования часто приводит к отвращению к языку C++. Более того, отсутствие ощущения того, что мы делаем, как передаются данные, удлиняет время разработки, так как приходится включать дополнительное время на отладку.

Интуиция

Прежде чем мы начнем копаться в принципе. Мы можем подумать о следующем простом анекдоте и лучше понять, что такое указатель.

Предположим, вы живете в большом городе и вместо смартфона или мобильного устройства у вас есть «старомодная» книга со всеми автобусными маршрутами в вашем городе. Вы прекрасно знакомы с этой книгой и точно знаете, как найти автобус, который доставит вас из определенного места города и доставит до места назначения. Каждая страница этой книги пронумерована и содержит полную информацию о расписании движения на определенной остановке. Ваш друг попросил вас сообщить ему точное время движения автобуса (например, номер: bus_29), который доставит его в вечернее время из пункта А и доставит в пункт Б. Вы знаете, как указать правильные данные, но это занимает некоторое время. Через 5 мин вы приходите с ответом и говорите четырём друзьям: У вас автобус в 19.29. Вы закрыли книгу и ждете ответа. Ваш друг ответил: «Хорошо», однако снова спросил: «А сколько времени в следующий раз?» Чтобы ответить на этот вопрос, вам нужно повторить тот же процесс еще раз. Также требуется 5 мин. Вы приходите с ответом: Следующий автобус в 20.03. Однако на этот раз вы немного умнее и не закрываете книгу «полностью». Вы оставляете палец на странице, которую почти проверили. Ваш друг удовлетворен ответом, но он не исчерпал вопрос. У нас есть автобус с 6 до 7 вечера? На этот раз ответ будет для вас достаточно быстрым, так как вы держите палец на странице, содержащей искомую информацию. Вы приходите с ответом: у нас есть только один автобус между 6 и 7 вечера. Он покидает пункт А в 18:16. Ваш друг теперь счастлив и говорит: Отлично, спасибо, у меня больше нет вопросов. Вы убираете палец, закрываете книгу и кладете ее в сумку.

Теперь мы можем провести некоторые аналогии. Ваш палец, который вы оставили в книге на определенной странице, является указателем. Номер страницы — это адрес памяти, Информации (расписание автобуса), которые включены в страницу — это значения по определенному адресу (в нашем примере — номер страницы книги).

Как видите, указатель (ваш палец) ускоряет процесс получения искомых данных.

Уточнение и простой пример

В информатике мы говорим, что указатель — это переменная (наш палец), которая содержит адрес памяти (страницу книги). Вот и все.
Чтобы быть более точным, можно сказать, что основное различие между переменной и указателем заключается в том, что переменная содержит числовое значение, а указатель содержит адрес (шестнадцатеричное значение, которое понимается компилятором как адрес). предоставление» компилятору специального оператора: (*) — оператор разыменования, компилятор понимает, что сохраненное шестнадцатеричное значение является адресом). Адрес указывает на имя переменной, определенное во время инициализации или позднее обычным присвоением адреса переменной.

Теперь мы подходим к правильному определению. Автор книги, которую вы проверили для своего друга, следовал определенной политике и написал расписание (значение или данные) для автобуса, который ваш друг запросил на странице 1000 (адрес данных). Вы помните, что оставили свой палец (указатель) на этой странице — страница 1000 (адрес). Теперь мы можем написать «псевдокод»:

bus_29 = time_table (… 6.16PM, 7.29PM, 9.03PM…)
your_finger = page 1000 (which contains the time table for bus29)

Теперь мы можем перевести наш псевдокод на C++ (здесь я применил некоторое упрощение и использовал только одно время 7:29 вечера, которое я выразил как 729. Обычно мы могли бы определить таблицу времени как массив или вектор и т. д.)

int bus_29 = 729;
int *ptr_bus_29 = &bus_29;

Просто чтобы дать обзор того, что мы сделали.

int bus_29 = 729; 

Мы определяем новую переменную с именем bus_29. Тип этой переменной int. Значение значения равно 729 (что в нашем случае соответствует 19:29).

Мы можем «спросить» компилятор, где в памяти находится переменная. Для этого нам нужно применить оператор (&) — адресный оператор, который возвращает адрес переменной.

std::cout << "address for bus_29 is : " << &bus_29 << std::endl;
//in our example we assume that this address equals 1000

После этого мы должны объявить наш указатель. Обратите внимание, что тип указателя должен совпадать с типом значения, на которое он указывает. В нашем случае также (int).
Мы используем (*) — оператор разыменования, который возвращает значение, хранящееся в адресе, хранящемся в переменной-указателе.

int *ptr_bus_29;
// declaration of a pointer variable called ptr_bus_29

Чтобы использовать указатель, помимо объявления нам нужно инициализировать (присвоить адрес). Мы можем совместить объявление и инициализацию (присваивание).

int *ptr_bus_29 = &bus_29;

Пожалуйста, рассмотрите следующий пример.

int bus_29 = 729;
int *ptr_bus_29 = &bus_29; //declaration and assignment
std::cout << bus_29 << std::endl; 
//prints (729)
std::cout << &bus_29 << std::endl; 
//prints the address of variable bus_29. In our case (1000)
std::cout << *ptr_bus_29 << std::endl; 
//prints value pointed by the ptr_bus_29, which in our case is (729)
std::cout << ptr_bus_29 << std::endl;
//prints the content of variable (pointer) ptr_bus_29, which in our case is (1000)

Простая арифметика указателя.

Очень важно увидеть, как мы можем использовать указатель для обхода контейнера (здесь мы используем массив. Массив — это последовательность элементов одного типа, размещенных в смежных ячейках памяти). В следующих примерах мы используем два массива, один массив содержит четыре типа значений целого числа, а другой массив содержит значение строкового типа. Пожалуйста, посмотрите, как мы можем пройти. Для обхода массивов (в приведенных ниже примерах) мы назначаем указатель на адрес первого элемента массива. Инкрементное значение адреса указателя, сдвигает позицию указанного значения в массиве. Как известно, целое число занимает 4 байта памяти, а строка — 8 байт. Компилятор достаточно умен и «понимает», что добавление (1) означает просто переход к следующему элементу (в данном случае) массива. Посмотрите на адрес, который содержит указатель. Когда указатель имеет целочисленный тип, размер увеличивается (после приращения в цикле for) на 4 байта, а для размера строки адрес увеличивается на 8 байтов.

Указатель функции. Перезвони

В следующем параграфе мы обсудим указатель, в котором хранится адрес функции. Этот метод очень полезен, когда мы используем обратный вызов.

Давайте взглянем на рисунок ниже, на котором изображена простая машинная память. Адреса на этот раз начинаются с 3000. Обратите внимание, что это место в памяти не такое же, как для хранения переменных. Этот «тип» памяти предназначен только для кода вашей программы. В любом случае, можно предположить, что (в примере ниже) основная функция занимает 12 байт (3 инструкции), а callBackFunction, callBack_1, callBack_2 занимают по 8 байт (2 инструкции). Функция представляет собой набор инструкций (определение функции => что делает функция), которые хранятся в непрерывном блоке памяти.
Когда мы вызываем функцию, мы вызываем первую инструкцию определенной функции (точка входа). Имя функции связано с адресом этой точки входа (который также является адресом нашей функции).
Указатель функции, аналогичный указателю переменной, хранит адрес. Однако на этот раз это адрес не переменной, а точки входа в функцию (адрес первой инструкции вызываемой функции).
Обычно программа выполняется последовательно, инструкция за инструкцией. Однако мы можем изменить эту процедуру (программа может перейти к другой функции), например, если мы вызовем функцию из другой функции.
В нашем случае, когда мы вызываем функцию под названиемFunction (из main), мы можем вызвать в дальнейшем другую функцию (callBack_1 или callBack_2). Это делается путем отправки (в качестве параметра) указателя на функцию. callBackFunction, по полученному адресу обратного вызова вызывает callBack_1 или callBack_2.

В этой статье я показал некоторые основные сведения об указателях в C++. Обратите внимание, что приведенные примеры не исчерпывают область указателей. В следующих статьях я сосредоточусь на динамическом распределении памяти и умных указателях.

Спасибо за уделенное время.