1. Основы памяти: введение в указатели, адресацию и операторы разыменования
Основы памяти: введение в указатели, адресацию и операторы разыменования
Добро пожаловать на курс «Мастерство работы с указателями в C++». Если вы здесь, значит, вы готовы заглянуть «под капот» программирования и понять, как на самом деле работает ваш компьютер. Указатели часто называют самой сложной темой для новичков, но на самом деле это просто карта, указывающая, где лежит сокровище.
В этой вводной статье мы разберем фундаментальные понятия: что такое оперативная память, как устроен адрес переменной и как мы можем управлять этими адресами с помощью указателей.
Как устроена память компьютера
Чтобы понять указатели, нужно сначала представить, как выглядит оперативная память (RAM) вашего компьютера. Представьте себе бесконечно длинную улицу, на которой стоят дома. У каждого дома есть свой уникальный номер — это адрес.
В мире компьютера:
* Дом — это ячейка памяти размером в 1 байт.
* Жилец дома — это данные (число, символ или часть большого объекта).
* Номер дома — это адрес ячейки (обычно записывается в шестнадцатеричном формате, например, 0x7ffee4).
Когда вы создаете переменную в C++, вы, по сути, бронируете один или несколько таких «домов» подряд, чтобы поселить туда свои данные.
!Схематичное изображение оперативной памяти как последовательности пронумерованных ячеек.
Переменные и их адреса
Рассмотрим простой пример:
Что происходит, когда выполняется эта строка?
int обычно занимает 4 байта, система резервирует 4 ячейки подряд.score становится псевдонимом для этого адреса.Оператор взятия адреса (&)
Мы знаем, что переменная score хранит значение 100. Но как узнать, где именно в памяти она находится? Для этого в C++ существует оператор взятия адреса, который обозначается символом амперсанда — &.
Если вы поставите & перед именем переменной, программа вернет не значение переменной, а её адрес в памяти.
Адрес 0x61ff0c — это шестнадцатеричное число, которое говорит процессору, в какой именно ячейке начинается наша переменная.
Что такое указатель?
Теперь мы подошли к главному. Указатель — это переменная, значением которой является адрес другой переменной.
Прочитайте это определение еще раз. Обычная переменная хранит данные (число, символ). Указатель хранит координаты этих данных.
Если обычная переменная — это дом, то указатель — это записка, на которой написан адрес этого дома.
Объявление указателя
Чтобы объявить указатель, мы используем символ звездочки * между типом данных и именем переменной.
Здесь ptr — это имя нашей переменной-указателя. Тип int* означает «указатель на целое число». Это важно: указатель всегда должен знать, на какой тип данных он указывает (мы обсудим это ниже).
Инициализация указателя
Указатель бесполезен (и даже опасен), если он никуда не указывает. Давайте запишем в него адрес нашей переменной score.
Теперь у нас есть связь:
* score == 100
* &score == 0x61ff0c (адрес)
* ptr == 0x61ff0c (тот же самый адрес)
!Визуализация связи указателя и переменной: указатель хранит адрес, который ссылается на переменную.
Оператор разыменования (*)
Иметь адрес — это хорошо, но как получить доступ к данным, которые находятся по этому адресу? Для этого используется оператор разыменования, который тоже обозначается звездочкой *.
Внимание! Символ * используется в двух разных контекстах, и это часто путает новичков:
int* p): означает «создать переменную типа указатель».*p): означает «перейти по адресу и взять значение».Пример использования:
Когда мы пишем *ptr, мы говорим компьютеру: «Возьми адрес, который записан в ptr, иди в ту ячейку памяти и работай с тем, что там лежит».
Зачем указателям типы?
Почему мы не можем просто создать универсальный тип pointer для всего? Почему мы обязаны писать int, double, char*?
Дело в том, что память — это сплошной поток байтов. Адрес указывает только на начальный байт. Компьютеру нужно знать, сколько байтов нужно прочитать, начиная с этого адреса, и как их интерпретировать.
Рассмотрим простую математическую зависимость размера памяти от типа данных:
Где — общий размер занимаемой памяти в байтах, — количество переменных (в случае массива или одной переменной, где ), а — размер одного элемента данного типа в байтах.
Если у нас есть указатель int* p, компилятор знает, что:
Если бы это был char* p, компилятор прочитал бы только 1 байт и интерпретировал его как символ.
> «Указатель без типа подобен письму без индекса: почтальон найдет дом, но не будет знать, в какую квартиру звонить и посылку какого размера вручать.»
Нулевой указатель (nullptr)
Работа с памятью требует дисциплины. Если вы объявили указатель, но не присвоили ему адрес, он будет указывать на случайное место в памяти (мусор). Попытка записать что-то по этому случайному адресу может привести к краху программы.
Чтобы избежать этого, в современном C++ используется ключевое слово nullptr.
Это гарантирует, что указатель «пуст» и никуда не указывает. Это стандарт безопасности. Перед использованием указателя хорошим тоном считается проверка:
Итоги
Сегодня мы заложили фундамент для всего курса. Давайте закрепим основные тезисы:
&.*.В следующей статье мы разберем арифметику указателей и узнаем, как они связаны с массивами. Готовьтесь, будет интересно!