Продолжим начатую тему адресации. Чтобы быть в курсе обсуждения, обязательно прочитайте первую часть:
Краткий пересказ. Запись для массива вида
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. Что-то типа масла масленого, но тем не менее так работает. Давайте проверим:
Шестнадцатеричное число 7ffd9d99f92c это адрес элемента с индексом 3. Если предположения насчёт array верны, то мы можем написать и так:
И действительно, так тоже работает. Но получились разные адреса. Среда исполнения не гарантирует, что они будут одинаковые от запуска к запуску. Поэтому напишем несколько вариантов внутри одной программы:
Теперь всё ровно. Обратим внимание на разницу между array и array+3: 45С - 450 = С, т.е. 12. Мы сдвинулись от начала массива 3 раза, но адрес сдвинулся на 12 байт.
Дело в том, что массив в данном случае состоит из элементов типа int, и каждый элемент занимает 4 байта. Сдвиг на 3 элемента даёт сдвиг адреса на 12 байт.
Это небольшая поблажка со стороны языка C. Чтобы не приходилось рассчитывать смещения адресов самостоятельно, можно указать лишь количество элементов, а C сам умножит это количество на размер элемента.
Хммм, смотрите: это не просто адрес. Это такой адрес, который знает, что его содержимое занимает сколько-то байт.
И таким образом мы подходим к понятию указателя.
Зачем нужны указатели?
А для чего нам вообще обращаться не к содержимому, а к значениям адресов в программе? Ведь мы спокойно пишем всякое типа foo = 5, и это работа с содержимым. А какая польза в работе с адресами?
Самый простой пример – выделение памяти. Начинали мы с того, что вся память принадлежит нашей программе, и мы можем делать с ней что хотим. Но в реальности это не так.
Когда понадобится сохранить в памяти, допустим, очень большой объём данных, мы должны попросить эту память у системы. Система может выделить адрес для нас, но где мы будем его держать?
Адреса переменных, определённых в нашей программе, рассчитываются компилятором и поэтому заранее известны. Это константы, которые нигде отдельно не хранятся и записаны прямо в машинном коде. Но компилятор не имеет понятия, какой адрес вернёт система. Это внешняя информация, которая станет доступна только в процессе выполнения программы.
Значит, этот адрес нельзя прошить прямо в код. Получив, его нужно хранить, и для хранения заведём обычную переменную:
foo = malloc(1024)
Функция malloc() выделит необходимый объём памяти и вернёт адрес. Этот адрес мы сохраним в переменной foo.
Теперь, когда нам будет нужен адрес выделенной памяти, мы будем брать его из foo.
Обратите внимание: foo это переменная, у которой есть собственный, рассчитанный компилятором адрес. А в содержимом этого адреса хранится другой адрес – тот, который выделила система.
Такая переменная и называется указателем. Её содержимое указывает на другой адрес. Но то, что в ней хранится адрес, не делает её какой-то необычной. Ведь адрес это просто число, и значит любую переменную можно считать указателем.
Например, если написать
foo = 5
То мы можем считать, что там хранится просто какое-то число 5, либо адрес 5.
Как именно трактовать, зависит от логики программы, но у нас есть возможность превратить любую переменную в указатель, и наоборот.
Посмотрим пример:
Здесь в переменную bar помещён адрес переменной foo. И несмотря на предупреждения компилятора (он думает, что мы творим дичь), адрес получен. Теперь можно считать, что bar содержит число 140726928180584, и тогда это просто какие-то данные. А если считать, что 140726928180584 это адрес, тогда bar это уже указатель. А с адресом мы можем делать всё, что делали выше. Например, прибавить к нему 3.
bar += 3
Что получится в результате? Адрес, хранимый в bar, изменится, и bar станет указывать в другое место.
Хмм, постойте. Но как работать с этим адресом? Адрес нужен, чтобы записывать или читать какие-то данные в нём. Но если адрес хранится в переменной, расположенной по своему собственному адресу, как быть? Написав так:
bar = 5
Мы поменяем содержимое bar, но не содержимое адреса, который хранится в bar.
И значит, нужно изобретать новое соглашение:
*bar = 5
Символ * обозначает, что нам нужно не содержимое bar, а содержимое того адреса, который хранится в bar.
К сожалению, тут есть один нюанс. Хотя логически мы могли бы приписать * к любой переменной и получить содержимое того адреса-неадреса, который в ней хранится, практически язык C ограничивает такое применение только специальными типами. Мы объявляем их так:
int* foo
char* bar
long* baz
То есть, берём обычный тип, вроде int, добавляем к нему *, и получаем тип "указатель на int".
Теперь можно переписать код и избавиться от предупреждений:
Результат: 5. Это содержимое переменной foo, адрес которой хранится в указателе bar. То, что bar это "указатель на int" – важно. Таким образом компилятор понимает, что чтение и запись по адресу, хранимому в bar, делается порциями размером в int.
А может, это массив?
Да, теперь можно взять содержимое bar и используя его как адрес, взять содержимое этого адреса как у массива :)
Как видим, изменив содержимое адреса, хранимого в bar, мы изменили и содержимое переменной foo, потому что это её адрес.
Хотя bar указывает только на foo, мы можем написать к примеру bar[10] или *(bar + 10) и указать куда-то в другое место. Для чтения это достаточно безопасно, но вот если туда что-то записать, последствия будут непредсказуемые.
И всё же...
Мы можем превратить переменную с обычным типом в указатель.
Мы поясняем компилятору, что вот в этом месте переменную bar нужно использовать, будто бы у неё тип int*:
*( (int*) bar )
И хотя он ворчит, но делает как нам надо. Главное, чтобы оригинальный тип переменной был не меньшего размера, чем размер указателя (unsigned long).
Читайте дальше: