Найти в Дзене
ZDG

Соседка вставляла указатель не туда, не вытерпел и показал свой

Оглавление

Продолжим начатую тему адресации. Чтобы быть в курсе обсуждения, обязательно прочитайте первую часть:

Краткий пересказ. Запись для массива вида

array[3] = 5

Состоит из следующих частей: array это символическое название для адреса памяти, допустим 2048, далее 3 это тоже адрес памяти, но относительный (относительно адреса array), а квадратные скобки это содержимое адреса. Итого получаем доступ к содержимому адреса 2051 и пишем туда число 5:

[2048 + 3] = 5

В том, что array это именно адрес, можно убедиться, попытавшись написать

array = 5

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

А вот обычной переменной (не массиву) foo можно присвоить значение:

foo = 5

Но foo это тоже адрес. Ошибки не возникает, потому что мы ранее договорились, что это условность, которая сокращает foo[0] или [foo] до просто foo.

Так что возникает неоднозначность. Мы должны помнить, с чем имеем дело: с массивом или скалярной переменной.

Эта неоднозначность развивается дальше, когда нам нужно не содержимое адреса, а сам адрес. Для массива это опять же работает. Можно написать выражение типа

foo = array

и ошибки не будет. Мы поместим в foo именно адрес, обозначенный как array. (Это довольно абстрактное утверждение, но дальше посмотрим.)

Но eсли есть две переменные foo и bar, то поместить адрес bar в foo уже невозможно. Ведь у нас есть соглашение, что foo = bar это на самом деле [foo] = [bar] и значит, нужен какой-то другой способ для получения адреса.

И тогда мы вводим новое соглашение: если перед именем написать &, то это будет обозначать адрес:

foo = &bar

Произошёл обмен шила на мыло. Изначально отказавшись от квадратных скобок ради красоты и удобства, мы ввели что-то взамен. Вместо

[foo] = bar

пишем

foo = &bar

Как уже нетрудно догадаться, в данных примерах я "придумываю" синтаксис языка C.

Для массивов правила действуют как всегда: выражение &array смысла не имеет, так как array это уже и так адрес, а вот выражение &array[3] вернёт адрес третьего элемента в массиве. Чтобы было понятней, напишем его с соблюдением порядка действий:

&( array[3] )

Cначала ищется содержимое элемента по адресу array+3, а потом уже адрес этого содержимого, который мы в принципе и так знаем, потому что он и есть array+3. Что-то типа масла масленого, но тем не менее так работает. Давайте проверим:

-2

Шестнадцатеричное число 7ffd9d99f92c это адрес элемента с индексом 3. Если предположения насчёт array верны, то мы можем написать и так:

-3

И действительно, так тоже работает. Но получились разные адреса. Среда исполнения не гарантирует, что они будут одинаковые от запуска к запуску. Поэтому напишем несколько вариантов внутри одной программы:

-4

Теперь всё ровно. Обратим внимание на разницу между array и array+3: 45С - 450 = С, т.е. 12. Мы сдвинулись от начала массива 3 раза, но адрес сдвинулся на 12 байт.

-5

Дело в том, что массив в данном случае состоит из элементов типа int, и каждый элемент занимает 4 байта. Сдвиг на 3 элемента даёт сдвиг адреса на 12 байт.

Это небольшая поблажка со стороны языка C. Чтобы не приходилось рассчитывать смещения адресов самостоятельно, можно указать лишь количество элементов, а C сам умножит это количество на размер элемента.

Хммм, смотрите: это не просто адрес. Это такой адрес, который знает, что его содержимое занимает сколько-то байт.

И таким образом мы подходим к понятию указателя.

Зачем нужны указатели?

А для чего нам вообще обращаться не к содержимому, а к значениям адресов в программе? Ведь мы спокойно пишем всякое типа foo = 5, и это работа с содержимым. А какая польза в работе с адресами?

Самый простой пример – выделение памяти. Начинали мы с того, что вся память принадлежит нашей программе, и мы можем делать с ней что хотим. Но в реальности это не так.

Когда понадобится сохранить в памяти, допустим, очень большой объём данных, мы должны попросить эту память у системы. Система может выделить адрес для нас, но где мы будем его держать?

Адреса переменных, определённых в нашей программе, рассчитываются компилятором и поэтому заранее известны. Это константы, которые нигде отдельно не хранятся и записаны прямо в машинном коде. Но компилятор не имеет понятия, какой адрес вернёт система. Это внешняя информация, которая станет доступна только в процессе выполнения программы.

Значит, этот адрес нельзя прошить прямо в код. Получив, его нужно хранить, и для хранения заведём обычную переменную:

foo = malloc(1024)

Функция malloc() выделит необходимый объём памяти и вернёт адрес. Этот адрес мы сохраним в переменной foo.

-6

Теперь, когда нам будет нужен адрес выделенной памяти, мы будем брать его из foo.

Обратите внимание: foo это переменная, у которой есть собственный, рассчитанный компилятором адрес. А в содержимом этого адреса хранится другой адрес – тот, который выделила система.

Такая переменная и называется указателем. Её содержимое указывает на другой адрес. Но то, что в ней хранится адрес, не делает её какой-то необычной. Ведь адрес это просто число, и значит любую переменную можно считать указателем.

Например, если написать

foo = 5

То мы можем считать, что там хранится просто какое-то число 5, либо адрес 5.

Как именно трактовать, зависит от логики программы, но у нас есть возможность превратить любую переменную в указатель, и наоборот.

Посмотрим пример:

-7

Здесь в переменную bar помещён адрес переменной foo. И несмотря на предупреждения компилятора (он думает, что мы творим дичь), адрес получен. Теперь можно считать, что bar содержит число 140726928180584, и тогда это просто какие-то данные. А если считать, что 140726928180584 это адрес, тогда bar это уже указатель. А с адресом мы можем делать всё, что делали выше. Например, прибавить к нему 3.

bar += 3

Что получится в результате? Адрес, хранимый в bar, изменится, и bar станет указывать в другое место.

Хмм, постойте. Но как работать с этим адресом? Адрес нужен, чтобы записывать или читать какие-то данные в нём. Но если адрес хранится в переменной, расположенной по своему собственному адресу, как быть? Написав так:

bar = 5

Мы поменяем содержимое bar, но не содержимое адреса, который хранится в bar.

И значит, нужно изобретать новое соглашение:

*bar = 5

Символ * обозначает, что нам нужно не содержимое bar, а содержимое того адреса, который хранится в bar.

-8

К сожалению, тут есть один нюанс. Хотя логически мы могли бы приписать * к любой переменной и получить содержимое того адреса-неадреса, который в ней хранится, практически язык C ограничивает такое применение только специальными типами. Мы объявляем их так:

int* foo
char* bar
long* baz

То есть, берём обычный тип, вроде int, добавляем к нему *, и получаем тип "указатель на int".

Теперь можно переписать код и избавиться от предупреждений:

-9

Результат: 5. Это содержимое переменной foo, адрес которой хранится в указателе bar. То, что bar это "указатель на int" – важно. Таким образом компилятор понимает, что чтение и запись по адресу, хранимому в bar, делается порциями размером в int.

А может, это массив?

Да, теперь можно взять содержимое bar и используя его как адрес, взять содержимое этого адреса как у массива :)

-10

Как видим, изменив содержимое адреса, хранимого в bar, мы изменили и содержимое переменной foo, потому что это её адрес.

Хотя bar указывает только на foo, мы можем написать к примеру bar[10] или *(bar + 10) и указать куда-то в другое место. Для чтения это достаточно безопасно, но вот если туда что-то записать, последствия будут непредсказуемые.

-11

И всё же...

Мы можем превратить переменную с обычным типом в указатель.

-12

Мы поясняем компилятору, что вот в этом месте переменную bar нужно использовать, будто бы у неё тип int*:

*( (int*) bar )

И хотя он ворчит, но делает как нам надо. Главное, чтобы оригинальный тип переменной был не меньшего размера, чем размер указателя (unsigned long).

Читайте дальше: