Память и указатели
Мы подошли к самой мощной и самой тонкой части C. На этом уроке мы узнаем, где переменные находятся в памяти, как получить их адрес и как управлять ими напрямую через указатель (pointer). Каждую идею мы рассмотрим на живой диаграмме памяти — с адресованными ячейками и метками указателей.
Что вы узнаете на этом уроке
6.1 Что такое память?
Под памятью компьютера (memory или RAM) понимается последовательность пронумерованных по порядку ячеек, в которых хранятся данные. У каждой ячейки есть свой порядковый номер, и этот номер называется адресом (address) — точно как номера домов на улице.
Когда вы создаёте в программе переменную, например int yosh = 19;, C даёт одной из этих ячеек (или нескольким) имя yosh и записывает туда 19. В прошлые недели мы работали только со значением (19). Теперь же мы увидим и то, где это значение находится, то есть его адрес.
Адреса обычно записываются в шестнадцатеричном (hex) виде, например 0x100. Точный номер для нас не важен, важно другое: каждая переменная живёт в памяти в строго определённом месте.
6.2 Оператор адреса (&)
Чтобы получить адрес переменной, мы ставим перед ней знак & (ампересанд). Например &yosh означает: по какому адресу находится переменная yosh. Ниже три переменные в памяти. Нажмите на ячейку, чтобы увидеть её адрес:
scanf("%d", &yosh) был ровно тот же &. Причина в том, что scanf нужно передать не значение, а куда записывать, то есть адрес.6.3 Что такое указатель?
Под указателем (pointer) понимается переменная, которая хранит адрес другой переменной. То есть внутри указателя лежит не значение, а адрес. Чтобы его создать, мы ставим перед типом звёздочку: int *p означает, что p — это указатель на int.
Ниже выберите, на какую переменную указывает указатель. Зелёная метка p обозначает указатель, а ячейка, на которой он стоит, — переменную, на которую он указывает:
6.4 Dereferencing (*p)
Теперь самое интересное. Чтобы прочитать или изменить значение, на которое указывает указатель, мы снова используем звёздочку, но теперь она означает dereferencing (переход по указателю). *p означает: значение в том месте, на которое указывает p.
Если мы напишем *p = 25;, это значит «запиши 25 в ячейку, на которую указывает p». Поскольку p указывает на yosh, сама переменная yosh станет равной 25. Попробуйте:
p — это адрес, а *p — значение по этому адресу. Это две разные вещи. Именно поэтому указатели так мощны: передав указатель в функцию, она может изменить переменную, находящуюся в другом месте.6.5 Массив и указатель
Теперь ответ на загадку из 4-й недели. В C имя массива на самом деле является указателем: оно обозначает адрес первого элемента массива. То есть arr и &arr[0] — это одно и то же. Поэтому можно написать int *p = arr;, и p теперь указывает на начало массива.
Ниже массив в памяти. Зелёная метка p указывает на начало массива. Нажмите на ячейку, чтобы увидеть адрес каждого элемента:
6.6 Арифметика указателей
Поскольку имя массива — это указатель, над указателем можно выполнять и арифметику. p + 1 означает «перейти к следующему элементу». Но есть тонкость: указатель сдвигается не на 1 байт, а на один элемент (для int — 4 байта). Значит *(p + 1) — это то же самое, что arr[1].
Ниже проведите указатель по массиву. Понаблюдайте, как сдвигается зелёная метка p и как меняются адрес и значение *p:
Сделайте прогноз
Что эта программа выведет в терминал?
6.7 Строки и указатели
Теперь раскрывается и правда о строках. В 4-й неделе мы видели, что строка — это массив char. А массив — это указатель. Значит, строка на самом деле — это char-указатель (char *), который указывает на первую букву.
Здесь ism указывает на первую букву A. *ism даёт нам A (dereferencing), а ism + 1 переходит к следующей букве l. Именно поэтому строки можно перебирать и указателем — вплоть до \0 в конце.
printf("%s", ism) в printf передаётся не вся строка, а только начальный адрес. А printf, начиная с этого адреса, читает буквы, пока не дойдёт до \0. Всё построено на указателях.6.8 Динамическая память (malloc)
До сих пор размер переменных был известен заранее. Но иногда программа узнаёт, сколько памяти нужно, только во время работы, например сколько чисел введёт пользователь. В этом случае мы используем динамическую память: функция malloc выделяет память нужного размера и возвращает её адрес (указатель).
Ниже выберите размер, запросите память через malloc, а закончив, освободите её через free:
free. Иначе она останется занятой. Эта проблема называется утечкой памяти (memory leak).6.9 Stack и heap
Память программы делится на две основные части. Ваши обычные переменные живут в одном месте, а память, полученная через malloc, — в другом. Эти части называются stack и heap:
- Здесь обычные, локальные переменные
- Автоматически: когда функция завершается, очищается сама
- Быстро, но размер ограничен
- Например:
int yosh = 19;
- Динамическая память (malloc) берётся отсюда
- Управляется вручную: вы сами освобождаете её через free
- Большой и гибкий, но требует осторожности
- Например:
malloc(...)
Основное отличие: stack очищает себя сам, а heap — на вашей ответственности. Именно поэтому каждому malloc должен соответствовать один free.
6.10 Глубже advanced
Указатели мощны, но требуют осторожности. Вот самые частые понятия и ошибки.
NULL-указатель
NULL — это специальное значение, означающее «не указывает никуда». Когда указатель ещё не готов к использованию, ему присваивают NULL. Разыменование (*p) указателя, указывающего на NULL, обрушивает программу, поэтому сначала его нужно проверить.
Висячий указатель и утечка памяти
Есть две распространённые ошибки. Висячий указатель (dangling pointer): указатель, который указывает на освобождённую память, использовать его опасно. Утечка памяти (memory leak): вызвать malloc и забыть вызвать free — память впустую остаётся занятой.
p = NULL;. Тогда он не останется висячим, и позже его будет легко проверить.Словарь терминов
6.11 Тест знаний
16 вопросов. Чтобы завершить неделю, ответьте правильно как минимум на 11 из них.
Поздравляем! Неделя 6 завершена
Теперь вы понимаете самую тонкую тему C — память и указатели: адрес, указатель, dereferencing, арифметику указателей и динамическую память. Это место, где многие останавливаются, а вы прошли его.
Следующая неделя: Структуры данных (struct, связный список, стек и очередь).
Перейти к следующему модулю