Неделя 6 · Foundation

Память и указатели

Мы подошли к самой мощной и самой тонкой части C. На этом уроке мы узнаем, где переменные находятся в памяти, как получить их адрес и как управлять ими напрямую через указатель (pointer). Каждую идею мы рассмотрим на живой диаграмме памяти — с адресованными ячейками и метками указателей.

Что вы узнаете на этом уроке

Поймёте, что память — это последовательность адресов
Получите адрес переменной с помощью оператора &
Создадите указатель и направите его на другую переменную
Прочитаете и измените значение через *p
Увидите связь массива и указателя, а также арифметику указателей
Поработаете с динамической памятью через malloc и free

6.1 Что такое память?

Под памятью компьютера (memory или RAM) понимается последовательность пронумерованных по порядку ячеек, в которых хранятся данные. У каждой ячейки есть свой порядковый номер, и этот номер называется адресом (address) — точно как номера домов на улице.

Когда вы создаёте в программе переменную, например int yosh = 19;, C даёт одной из этих ячеек (или нескольким) имя yosh и записывает туда 19. В прошлые недели мы работали только со значением (19). Теперь же мы увидим и то, где это значение находится, то есть его адрес.

Адреса обычно записываются в шестнадцатеричном (hex) виде, например 0x100. Точный номер для нас не важен, важно другое: каждая переменная живёт в памяти в строго определённом месте.

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

6.2 Оператор адреса (&)

Чтобы получить адрес переменной, мы ставим перед ней знак & (ампересанд). Например &yosh означает: по какому адресу находится переменная yosh. Ниже три переменные в памяти. Нажмите на ячейку, чтобы увидеть её адрес:

Просмотр адреса нажмите на ячейку
main.c
1int yosh = 19;
2int narx = 500;
3int soni = 3;
Нажмите на одну из ячеек выше: вы увидите её адрес (&).
Вспомните: на прошлой неделе в scanf("%d", &yosh) был ровно тот же &. Причина в том, что scanf нужно передать не значение, а куда записывать, то есть адрес.

6.3 Что такое указатель?

Под указателем (pointer) понимается переменная, которая хранит адрес другой переменной. То есть внутри указателя лежит не значение, а адрес. Чтобы его создать, мы ставим перед типом звёздочку: int *p означает, что p — это указатель на int.

pointer.c
1int yosh = 19;
2int *p = &yosh; // p указывает на yosh

Ниже выберите, на какую переменную указывает указатель. Зелёная метка p обозначает указатель, а ячейка, на которой он стоит, — переменную, на которую он указывает:

Диаграмма указателя выберите указатель
p =
Указатель и сам находится в памяти и имеет свой адрес. Но его значение — это адрес другой переменной. Именно поэтому мы говорим, что указатель «указывает», — он ссылается на какое-то место.

6.4 Dereferencing (*p)

Теперь самое интересное. Чтобы прочитать или изменить значение, на которое указывает указатель, мы снова используем звёздочку, но теперь она означает dereferencing (переход по указателю). *p означает: значение в том месте, на которое указывает p.

Если мы напишем *p = 25;, это значит «запиши 25 в ячейку, на которую указывает p». Поскольку p указывает на yosh, сама переменная yosh станет равной 25. Попробуйте:

Поле dereferencing введите значение
*p =
Обратите внимание: p — это адрес, а *p — значение по этому адресу. Это две разные вещи. Именно поэтому указатели так мощны: передав указатель в функцию, она может изменить переменную, находящуюся в другом месте.

6.5 Массив и указатель

Теперь ответ на загадку из 4-й недели. В C имя массива на самом деле является указателем: оно обозначает адрес первого элемента массива. То есть arr и &arr[0] — это одно и то же. Поэтому можно написать int *p = arr;, и p теперь указывает на начало массива.

massiv.c
1int arr[4] = {10, 20, 30, 40};
2int *p = arr; // p = &arr[0]

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

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

6.6 Арифметика указателей

Поскольку имя массива — это указатель, над указателем можно выполнять и арифметику. p + 1 означает «перейти к следующему элементу». Но есть тонкость: указатель сдвигается не на 1 байт, а на один элемент (для int — 4 байта). Значит *(p + 1) — это то же самое, что arr[1].

Ниже проведите указатель по массиву. Понаблюдайте, как сдвигается зелёная метка p и как меняются адрес и значение *p:

Арифметика указателей сдвигайте p

Сделайте прогноз

Что эта программа выведет в терминал?

main.c
1int arr[] = {5, 10, 15};
2int *p = arr;
3printf("%d", *(p + 2));
10
15
5

6.7 Строки и указатели

Теперь раскрывается и правда о строках. В 4-й неделе мы видели, что строка — это массив char. А массив — это указатель. Значит, строка на самом деле — это char-указатель (char *), который указывает на первую букву.

satr.c
1char *ism = "Ali";
2printf("%c\n", *ism); // A (первая буква)
3printf("%s\n", ism); // Ali (вся строка)

Здесь ism указывает на первую букву A. *ism даёт нам A (dereferencing), а ism + 1 переходит к следующей букве l. Именно поэтому строки можно перебирать и указателем — вплоть до \0 в конце.

Значит, в printf("%s", ism) в printf передаётся не вся строка, а только начальный адрес. А printf, начиная с этого адреса, читает буквы, пока не дойдёт до \0. Всё построено на указателях.

6.8 Динамическая память (malloc)

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

dinamik.c
1int *p = malloc(3 * sizeof(int));
2p[0] = 10; // используем как массив
3free(p); // закончив, освобождаем

Ниже выберите размер, запросите память через malloc, а закончив, освободите её через free:

malloc и free запросите память
размер:
Самое важное правило: каждую память, полученную через malloc, обязательно нужно освободить через free. Иначе она останется занятой. Эта проблема называется утечкой памяти (memory leak).

6.9 Stack и heap

Память программы делится на две основные части. Ваши обычные переменные живут в одном месте, а память, полученная через malloc, — в другом. Эти части называются stack и heap:

Stack
  • Здесь обычные, локальные переменные
  • Автоматически: когда функция завершается, очищается сама
  • Быстро, но размер ограничен
  • Например: int yosh = 19;
Heap
  • Динамическая память (malloc) берётся отсюда
  • Управляется вручную: вы сами освобождаете её через free
  • Большой и гибкий, но требует осторожности
  • Например: malloc(...)

Основное отличие: stack очищает себя сам, а heap — на вашей ответственности. Именно поэтому каждому malloc должен соответствовать один free.

6.10 Глубже advanced

Указатели мощны, но требуют осторожности. Вот самые частые понятия и ошибки.

NULL-указатель

NULL — это специальное значение, означающее «не указывает никуда». Когда указатель ещё не готов к использованию, ему присваивают NULL. Разыменование (*p) указателя, указывающего на NULL, обрушивает программу, поэтому сначала его нужно проверить.

Висячий указатель и утечка памяти

Есть две распространённые ошибки. Висячий указатель (dangling pointer): указатель, который указывает на освобождённую память, использовать его опасно. Утечка памяти (memory leak): вызвать malloc и забыть вызвать free — память впустую остаётся занятой.

Хорошая практика: сразу после free присвойте указателю p = NULL;. Тогда он не останется висячим, и позже его будет легко проверить.

Словарь терминов

адресместо переменной в памяти (address), берётся через &.
указательпеременная, хранящая адрес другой переменной.
*pdereferencing: значение в месте, на которое указывает указатель.
mallocво время работы выделяет динамическую память из heap.
freeосвобождает память, полученную через malloc.
NULLспециальное значение указателя, не указывающее никуда.

6.11 Тест знаний

16 вопросов. Чтобы завершить неделю, ответьте правильно как минимум на 11 из них.

Поздравляем! Неделя 6 завершена

Теперь вы понимаете самую тонкую тему C — память и указатели: адрес, указатель, dereferencing, арифметику указателей и динамическую память. Это место, где многие останавливаются, а вы прошли его.

Следующая неделя: Структуры данных (struct, связный список, стек и очередь).

Перейти к следующему модулю