Указатели, адреса и nullptr
В прошлом уроке ссылка была второй табличкой на двери дома. Табличку прибивают один раз, и без дома её не бывает.
У каждого дома на улице памяти есть номер — его адрес. Адрес можно записать на бумажку. По бумажке вы придёте к дому, заглянете внутрь или что-то в нём поменяете. А потом бумажку можно стереть и написать на ней адрес другого дома. Можно и оставить её пустой, если идти пока некуда.
Такая бумажка в C++ называется указателем. В этой программе один указатель p по очереди меняет две переменные:
#include <iostream>
int main() {
int a = 1;
int b = 2;
int* p = &a;
*p = 10;
p = &b;
*p = 20;
std::cout << a << ' ' << b << '\n'; // 10 20
}Давайте разберём пример по строкам:
int a = 1;иint b = 2;строят два дома.int* p = &a;записывает на бумажкуpадрес домаa.*p = 10;идёт по адресу с бумажки и кладёт10в домa.p = &b;стирает бумажку и пишет на ней адрес домаb.*p = 20;идёт уже к домуbи кладёт туда20.- Программа печатает
10 20: через один указатель изменились обе переменные.
Адрес и разыменование
Указатель — переменная, которая хранит адрес другой переменной, то есть номер места этой другой переменной в памяти. Тип int* читается как «указатель на int». Выражение &a даёт адрес a, поэтому int* p = &a; кладёт в p адрес a.
Выражение *p обозначает саму переменную, адрес которой хранится в p. Такой доступ называют разыменованием: *p = 10; меняет a. Строка p = &b; меняет сам указатель, то есть переписывает бумажку, и следующее *p = 20; меняет уже b.
Сравните это со ссылкой: ref = b; не перевешивает табличку, а копирует значение. Пройдите программу по шагам и следите, к какому дому ведут ref и p после каждой строки:
1–2#include <iostream>3int main() {4 int a = 1;5 int b = 2;6 int& ref = a;7 int* p = &a;8 ref = b;9 std::cout << a << " " << b << "\n";10 p = &b;11 *p = 7;12 std::cout << a << " " << b << "\n";13}
Дом a, внутри 1
Символы & и * значат разное в объявлении и в выражении:
- в объявлении
int&— ссылка; - в объявлении
int*— указатель; - в выражении
&a— адресa; - в выражении
*p— переменная, на которую указываетp.
Нулевой указатель
Указателю, которому пока не на что указывать, присваивают значение nullptr, и такой указатель называют нулевым указателем. Это пустая бумажка. Например, именно так функция поиска сообщает, что ничего не нашла. Перед разыменованием такой указатель проверяют:
int* found = nullptr;
if (found != nullptr) {
std::cout << *found << '\n';
}Строка if (found != nullptr) спрашивает: есть ли на бумажке адрес? Только если есть, программа идёт по нему и печатает значение.
За nullptr нет переменной, и читать через него нечего. Разыменование нулевого указателя — неопределённое поведение. Компилятор такую программу собирает, а стандарт не говорит, что она сделает. Не гарантировано даже падение программы.
Здесь аналогия с бумажкой заканчивается: с пустой бумажкой человек никуда не пойдёт, а программа с *found на нулевом указателе может сделать что угодно.
Частая ошибка
Указатель объявили без начального значения и сразу разыменовали:
int main() {
int* p;
*p = 5; // ✗
}warning: variable 'p' is uninitialized when used here [-Wuninitialized]Компилятор только предупреждает. Значение p не задано: на бумажке случайные каракули, и запись по такому адресу — неопределённое поведение. Если адреса ещё нет, инициализируйте указатель значением nullptr: тогда проверка p != nullptr покажет, что он пуст.
Главное
- Указатель хранит адрес переменной:
&aберёт адрес, а*p— сама переменная по этому адресу. - Указатель можно перенаправить:
p = &b;переписывает адрес, а*p = 20;меняет переменную. nullptr— пустой указатель; его проверяют перед разыменованием, иначе неопределённое поведение.- Указатель без начального значения разыменовывать нельзя — инициализируйте его хотя бы
nullptr.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.