Базовый CS с нуля
Значение и адрес
Посмотри на два предложения:
- «Значение по адресу 4 равно 100».
- «Значение по адресу 4 — это адрес, а именно адрес 100».
Оба совершенно корректны. Оба описывают реальные вещи, происходящие в программах. Но одно из них — второе — это идея, которая сбивает с толку каждого новичка и которую каждый опытный инженер использует не задумываясь.
Значение — это просто биты в ячейке. Ничто не мешает этим битам быть интерпретированными как адрес. Когда значение является адресом, мы имеем дело с указателем: ячейкой, содержимое которой говорит, куда смотреть дальше. Этот урок проводит максимально чёткую границу между тем, что такое значение и что такое адрес, чтобы идея указателя воспринималась ясно.
После этого урока ты сможешь точно разграничить адрес и значение, описать, как значение само может быть адресом (концепция указателя), и объяснить, что значит «прочитать значение по адресу X» — операция, называемая разыменованием.
Напоминание: два свойства ячейки памяти. У каждой ячейки памяти есть ровно два свойства. Первое — её адрес: фиксированный порядковый номер ячейки в ряду, начиная с 0. Второе — её значение: биты, хранящиеся в ячейке в данный момент, которые могут меняться в любое время.
Эти два свойства всегда разделены. Адрес — это то, как ты находишь ячейку. Значение — то, что ты находишь, оказавшись там. Ячейка по адресу 12 всегда будет по адресу 12, но сегодня может хранить 255, а завтра — 0.
Адрес — число. Значение тоже число. Вот где живёт ключевая тонкость. На 32-битной машине адреса — это 32-битные числа (от 0 до примерно 4 миллиардов). Значения, хранящиеся в ячейке, тоже числа — последовательности битов, которые можно интерпретировать как число.
Поскольку и то, и другое просто числа, значение в ячейке может оказаться в диапазоне допустимых адресов. Рассмотрим ячейку по адресу 5, хранящую значение 12. Ничто в числе 12 не говорит «я адрес» или «я просто данные». Биты одинаковы. Интерпретация зависит от того, как программа решает использовать это значение.
Если использовать 12 как число в арифметике — это данные. Если использовать 12 как адрес — то есть сказать «иди посмотри на ячейку с номером 12» — то 12 трактуется как адрес. Биты в ячейке не изменились; изменилось только использование.
Указатель: значение, которое является адресом. Когда ячейка содержит значение, которое намеренно используется как адрес — когда программа хранит адрес X в ячейке, то есть «значение ячейки A — это адрес другой ячейки B» — эта ячейка A называется указателем (или в высокоуровневых языках — ссылкой).
На низком уровне:
- Ячейка A по адресу 5 хранит значение 12.
- Программа трактует это 12 как адрес.
- Затем она читает ячейку B по адресу 12, чтобы получить реальные данные.
Ячейка A указывает на ячейку B. A — указатель. B — ячейка, на которую он указывает.
Идея указателя не вводит новое железо. Указатели — просто договорённость: хранить в ячейке число, имеющее смысл адреса, и помнить, что собираешься использовать его как адрес. Аппаратура не знает понятия «указатель» — она просто загружает биты из ячейки.
Разыменование: «прочитать значение по адресу X». Операция «использовать значение как адрес для нахождения другого значения» называется разыменованием (или «следованием по указателю»). В примере выше: ячейка A хранит 12 (указатель). Разыменовать A — значит перейти по адресу 12 и прочитать там хранящееся значение. Ты «прошёл по указателю» на один шаг.
В большинстве языков программирования для этого есть явный синтаксис. В C пишешь *p
(где p хранит адрес), чтобы сказать «значение по адресу, хранящемуся в p». В Python
и JavaScript адрес никогда не виден напрямую, потому что язык разыменовывает ссылки на
объекты автоматически — но аппаратура делает то же самое.
Двухшаговый процесс:
- Читаем ячейку A → получаем значение 12 (адрес).
- Читаем ячейку по адресу 12 → получаем реальные данные.
Без первого шага реальные данные недостижимы.
Почему это работает
Зачем вообще хранить адрес в ячейке? Почему бы не положить данные прямо туда, куда нужно?
Две причины. Первая — косвенность позволяет совместное использование: несколько ячеек могут хранить один и тот же адрес, то есть несколько частей программы могут ссылаться на одни и те же данные без копирования. Вторая — косвенность позволяет обновления: если ячейка A хранит адрес данных и данные перемещаются на новое место, обновляешь только значение ячейки A (до нового адреса) — и все указатели через A автоматически следуют на новое место. Без косвенности пришлось бы обновлять каждую копию.
Эти два преимущества — совместное использование без копирования и обновление в одном месте — объясняют, почему указатели и ссылки присутствуют практически в каждой программе.
Трассировка двухшагового чтения по указателю.
Состояние памяти:
- Адрес 0: значение
3← эта ячейка хранит адрес нужных нам данных - Адрес 1: значение
0 - Адрес 2: значение
0 - Адрес 3: значение
71← это реальные данные
Программа выполняет: «прочитать значение по адресу, хранящемуся по адресу 0».
Шаг 1 — Чтение адреса 0. CPU читает ячейку по адресу 0 и получает 3.
Это значение указателя — адрес.
Шаг 2 — Чтение адреса 3 (только что найденного адреса). CPU читает ячейку по
адресу 3 и получает 71. Это реальные данные, которые нужны программе.
Программа получила 71. Потребовалось два чтения памяти: одно для нахождения адреса,
второе для чтения данных по нему.
Если бы вместо этого 71 хранилось прямо по адресу 0, хватило бы одного чтения.
Косвенность стоит одного дополнительного чтения. Этот лишний шаг — цена гибкости,
которую дают указатели.
Что если изменить значение по адресу 0 на 2? Тогда та же двухшаговая операция
вернёт значение по адресу 2 (равное 0 в нашей таблице). Указатель изменился, и данные,
на которые он ссылается, тоже изменились.
Частая ошибка
Самая распространённая ошибка при разграничении значения и адреса — путать «значение 12» с «адресом 12». Если ячейка по адресу 5 хранит значение 12, это не значит, что адрес 12 существует (его может не быть, если память меньше 13 ячеек). Это также не значит, что ячейка 5 чем-то особенна по сравнению с ячейкой, хранящей значение 7. Значение 12 становится адресом только тогда, когда программа явно использует его как таковой. Голые биты не имеют присущего им смысла — смысл возникает из того, как программа их использует.
Ячейка по адресу 7 хранит значение 3. Программа читает адрес 7, а затем читает только что полученный адрес. Какой адрес она читает на втором шаге?
Ячейка по адресу 2 хранит значение 2. Ячейка по адресу 3 хранит значение 99. Программа читает адрес 2, затем использует его как адрес. Какое значение она получит после второго чтения?
Указатель — ячейка, значение которой используется как адрес. Есть ли у ячейки-указателя собственный адрес? Введи 1 (да) или 0 (нет).
Разыменование указателя требует скольких чтений памяти для получения итогового значения данных: 1 или 2?
Память имеет 10 ячеек (адреса 0–9). Ячейка по адресу 4 хранит значение 15. Можно ли разыменовать этот указатель? Введи 1 (да, безопасно) или 0 (нет, за границей).
Ячейка по адресу 8 хранит значение 20. Ячейка по адресу 20 хранит значение 7. Программа разыменовывает адрес 8. Какое значение она получает?
Адрес называет ячейку — это фиксированный порядковый номер. Значение — данные, хранящиеся в ячейке в данный момент; оно может меняться. Поскольку и адреса, и значения — просто числа, значение может быть допустимым адресом. Когда программа намеренно хранит адрес в ячейке и использует содержимое этой ячейки как адрес для следующего чтения памяти, такая ячейка называется указателем (или ссылкой). Использование указателя для получения итоговых данных называется разыменованием: читаем ячейку- указатель, чтобы получить адрес, затем читаем этот адрес, чтобы получить реальные данные — два чтения памяти вместо одного. Косвенность через указатели позволяет нескольким частям программы совместно использовать данные без копирования и позволяет одному обновлению указателя перенаправить все ссылки сразу.