Найти тему
IT. Как это работает?

От транзистора до фреймворка. Часть 15. Указатели

Оглавление

Видео: YouTube

Мы продолжаем копаться в недрах объектно-ориентированного программирования. В прошлый раз были рассмотрены структуры. Это синтаксическая конструкция позволяющая объединить под одним именем данные разных типов. На этот раз поговорим об указателях.

Просто об указателях.

Для того чтобы понять мощь этого средства рассмотрим простой и наглядный пример. На складе лежат три кирпичных стопки.

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

-2

Потом выкладываем эти три кирпича в порядке погрузки стопок.

-3

Сейчас не перемещались тяжелые стопки кирпичей. Перемещались только записки о том, в каком порядке отправлять стопки на погрузку. Как вы понимаете, масса стопок и кирпичей это отсылка к количеству данных, которые при перемещении должны будут пройти через регистры процессора. Стопки кирпичей это большие структуры. Отдельные кирпичи — записки это переменные, указывающие на структуры.

Операция взятия адреса переменной.

Немного вспомним устройство простейшего процессора архитектуры фон-Неймана. В общей памяти хранятся программы и данные. Выборка машинных инструкций и данных происходит по очереди. При выборке данных из памяти большую роль играет указатель стека. Так называемый stack pointer (SP).

Роль указателя стека в формировании адреса данных
Роль указателя стека в формировании адреса данных

При организации вызова функции, в специальную область памяти помещаются параметры и локальные переменные. Содержимое регистра SP при этом указывает на самый нижний элемент в стековом фрейме функции. Подробнее об этом можно узнать в выпуске про функции. Теперь про сами указатели. Первым делом это такая же переменная, как и другие. Тонкость в том, что в ней хранится адрес какой-либо ячейки памяти.
На простой программе посмотрим как это происходит.

-5

В функции main определены переменная равная 10, массив из пяти элементов. Кроме того, еще два указателя на переменные типа char. Синтаксически записывается все просто. Главное не забыть звездочку между типом и именем указателя. Подготовка функции к работе включает в себя в первую очередь размещение указателя стека на какую либо из ячеек.

Формирование стекового фрейма функции main
Формирование стекового фрейма функции main

В этом примере в содержимое регистра SP заносится 255. Начиная с этой вершины в глубину стека помещается число 10. Потом размещается массив, начиная с последнего элемента, нулевой элемент при этом окажется по меньшему адресу. При размещении в стеке переменных, хранящих адреса в программе, нет конкретного указания о содержимом этих переменных. Можно просто уменьшить содержимое SP на 2. На то количество байт, содержимое которых заранее неизвестно. На этом подготовка к запуску функции закончена.
Первым действием в указатель
p1 заносится адрес переменной variable. Синтаксически это почти как присвоение, только нужно не забыть про знак & (амперсанд) впереди имени переменной. Технически здесь происходит примерно вот что.

Расчет адреса переменной относительно SP
Расчет адреса переменной относительно SP

В регистре R1 будет вычислен адрес переменной variable. Первым делом очищается его содержимое командой загрузки числа в регистр. В регистр R2 заносится смещение переменной variable относительно указателя стека. Смещение равно 7. Третья инструкция добавляет в R1 содержимое указателя стека. К этому потом добавляется смещение, что в совокупности даст полный адрес ячейки с переменной. Этот адрес необходимо поместить в ячейку со смещением 1 от указателя стека. Поэтому в R2 заносится 1. Далее команда сохранения содержимого R1 в нужной ячейке памяти.

Указатель содержит адрес переменной
Указатель содержит адрес переменной

Теперь указатель p1 содержит адрес переменной variable. Чтобы в указателе p2 разместить адрес нулевого элемента массива проделываются те же действия. Обнуляется R1. Потом в R2 смещение, в R1 указатель стека (SP). Складываем, получая полный адрес, потом размещаем его в нужной ячейке.

-9
Расчет адреса нулевого элемента массива
Расчет адреса нулевого элемента массива

Как можно заметить, не так уж мгновенно все происходит, хотя и операций на все уходит немного.

Копирование указателя. Адресная арифметика.

Вот что происходит очень быстро, так это копирование указателя.

Копирование указателя
Копирование указателя

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

-12
Расчет адреса элемента массива
Расчет адреса элемента массива

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

Доступ к элементам структуры по указателю.

Теперь вернемся к игровым юнитам и посмотрим как синтаксически и технически происходит доступ к элементам структуры.
В функции
main происходит инициализация свойств персонажа. При этом уже становится привычным, что в стековый фрейм функции заносятся все свойства, начиная с последних. Также в функции определен указатель на структуру King. Под него также выделяется место в стеке.

Расчет адреса структуры
Расчет адреса структуры

В этой строке кода в указатель pKing заносится адрес первого элемента структуры. Синтаксически доступ к свойствам по указателю осуществляется при помощи стрелочки.

Передача параметра по значению.

Сейчас в исходный код добавлена функция damage() уменьшения здоровья (health) на единицу.

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

Размещение структуры в стеке функции main
Размещение структуры в стеке функции main

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

в стек будет помещена не сама структура, а ее копия,

что раздует стек и будет сопровождаться большим количеством машинных инструкций.

Передача в стек копии структуры
Передача в стек копии структуры

Внутри функции операция будет совершена над копией.

Операции с копией структуры
Операции с копией структуры

А при возврате структуры обратно через стек, функции main прийдется копировать в свою собственную структуру все поля копии.

Возврат копии структуры через стек
Возврат копии структуры через стек

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

Передача параметра по указателю.

Улучшим исходный код. Теперь функция принимает не саму структуру, а указатель на нее.

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

Передача в стек указателя на структуру
Передача в стек указателя на структуру

Но теперь во фрейм функции damage уходит указатель, что привело к уменьшению бесполезной работы процессора.

Модификация структуры по указателю
Модификация структуры по указателю

Внутри функции произошло изменение поля структуры, которая находится непонятно где с точки зрения функции damage. Главное это то, что есть указатель, а это значит, что со структурой можно производить манипуляции. Также необходимо отметить, что возврат результата через стек теперь не нужен, что еще больше ускорило программу.

Возврат из функции
Возврат из функции

Указатель на функцию.

Мы подошли к вишенке на торте. Это указатель на функцию. Обратим внимание на описание структуры.

Последним полем у нее появилась загадочная запись void (*hit)(King * k);. Так выглядит синтаксис указателя на функцию. Чисто технически это такая же переменная как и все остальные, но в ней хранится адрес первой инструкции функции. При вызове этой функции произойдет передача содержимого этой переменной в регистр указателя инструкции PC.
Теперь про синтаксис. Возвращает функция пустоту (
void). Название этого указателя hit. Параметром функции должен быть указатель на структуру King. Обратим внимание также на то, что при инициализации указателя на функцию нужно присвоить ему конкретное значение. У нас это будет адрес функции damage. С этого самого момента структура кроме свойств обрела еще и метод работы с одним из своих свойств. А что это как не зачаток объектно-ориентированного подхода?
Конечно, мы при этом не окунаемся в условности вроде
инкапсуляции, наследования, но у нас все впереди. Выполнение функции начинается после вызова метода нашего объекта через точку и имя метода hit.

Инициализация указателя на функцию
Инициализация указателя на функцию

Выполнение программы пойдет по машинным инструкциям функции damage и это все уже привычно.

Модификация объекта по указателю
Модификация объекта по указателю

Одно неудобство. Параметром метода объекта является указатель на сам объект k.hit(&k) . С точки зрения высокоуровневых языков выглядит чем-то чужеродным. Но стоит присмотреться, то указатель на сам объект передается в методы, явно или завуалированно. Указателем на объект является this в языках C++ и Java, также им является self в языке Python.

Наука
7 млн интересуются