Что такое указатель и адрес

Урок вводит главное понятие C — указатель: что такое адрес переменной, как взять его оператором &, как объявить указатель и прочитать значение по адресу через разыменование *.
Указатель — это переменная, которая хранит не значение, а адрес другого значения в памяти. Освоить указатели — значит по-настоящему понять C.

Каждая переменная живёт по какому-то адресу в памяти — это просто номер ячейки. Оператор & («взять адрес») возвращает адрес переменной. Указатель — это переменная, которая хранит такой адрес. Объявляется со звёздочкой:

int x = 42;
int *p = &x;     // p хранит АДРЕС переменной x

printf("Значение x: %d\n", x);       // 42
printf("Адрес x:    %p\n", (void*)&x); // напр. 0x7ffe...
printf("p хранит:   %p\n", (void*)p);   // тот же адрес
printf("По адресу p лежит: %d\n", *p);  // 42 — разыменование

Здесь работают два оператора-«антипода». &x берёт адрес переменной. А *p делает обратное — «иди по этому адресу и возьми значение». Это называется разыменование. Через *p можно не только читать, но и писать: *p = 100; изменит саму x.

Как работает под капотом

Представьте память как ряд пронумерованных ячеек. Переменная x лежит в одной из них, а указатель p — в другой, и хранит номер ячейки x:

Адрес      Содержимое      Имя
--------   ------------    ----
0x1000     [   42   ]      x        <-- значение
0x1008     [ 0x1000 ]      p        <-- p хранит АДРЕС x

&x   =  0x1000      (адрес x)
p    =  0x1000      (p указывает на x)
*p   =  42          (значение по адресу, что хранит p)

Графически:   p ----> x
              [0x1000] [42]

Стрелка p -> x читается «p указывает на x». Когда вы пишете *p, вы идёте по стрелке и попадаете в x. Когда пишете *p = 100, вы изменяете x, не упоминая её по имени. Именно так функции из прошлого раздела меняли переменные вызывающего.

Частые ошибки

  • Путаница объявления и разыменования. В int *p звёздочка означает «p — указатель». В *p = 5 та же звёздочка — «запиши по адресу». Контекст разный.
  • Неинициализированный указатель. int *p; без присваивания указывает «в никуда»; разыменование такого указателя — крах.
  • Разыменование NULL. Указатель со значением NULL намеренно «ничей»; *p при p == NULL вызывает падение программы.
  • Печать указателя как %d. Адрес выводят через %p, а не %d.

Best practices

  • Инициализируйте указатели сразу — адресом реальной переменной или NULL, если адреса пока нет.
  • Перед разыменованием проверяйте указатель на NULL: if (p != NULL) { ... *p ... }.
  • Читайте объявления справа налево: int *p — «p есть указатель на int». Это помогает в сложных типах.

В Python нет явных указателей, но идею «отдельная таблица: имя -> адрес -> значение» можно смоделировать словарём-памятью. Так становится виден механизм разыменования.

# Моделируем память как словарь адрес -> значение
memory = {0x1000: 42}     # по адресу 0x1000 лежит значение 42

x_addr = 0x1000           # "адрес x" (как &x в C)
p = x_addr                # указатель p хранит адрес

print("p хранит адрес:", hex(p))
print("*p (значение по адресу):", memory[p])   # разыменование

memory[p] = 100           # *p = 100 — пишем по адресу
print("После *p = 100, значение x:", memory[x_addr])  # 100

Та же логика на Python ▶ — словарь играет роль памяти, ключ — адрес, доступ memory[p] — это разыменование *p. В C всё то же, только адреса настоящие.

Типизация указателей

У указателя есть тип, и это не формальность. int * и char * — разные типы, хотя оба хранят адрес. Тип говорит компилятору две вещи: сколько байтов читать при разыменовании и на сколько сдвигаться при арифметике (об этом — отдельный урок). Поэтому нельзя бездумно присвоить указатель одного типа другому. Есть особый «универсальный» указатель void * — он хранит адрес, но не помнит тип, поэтому разыменовать его напрямую нельзя, нужно сначала привести к конкретному типу. Именно void * возвращает malloc, ведь функция не знает, подо что вы выделяете память. А ещё указатели бывают на указатели (int **): такая «двойная стрелка» нужна, когда функция должна изменить сам указатель вызывающего, а не только то, на что он смотрит.

Итоги

Указатель — переменная, хранящая адрес другого значения. Оператор & берёт адрес, оператор * разыменовывает (читает или пишет значение по адресу). Объявление int *p и операция *p используют одну звёздочку в разных ролях. Главные опасности — неинициализированный указатель и разыменование NULL; защита — инициализация и проверка перед использованием.

Проверьте себя
1. Что хранит указатель int *p = &x;?
AКопию значения x
BАдрес переменной x в памяти
CИмя переменной x
DТип переменной x
2. Что делает выражение *p = 100, если p указывает на переменную x?
AСоздаёт новую переменную
BЗаписывает 100 в саму переменную x — через её адрес
CМеняет адрес, хранящийся в p
DПечатает 100 на экран