Перейти к содержанию
Educora
Средний20 мин6 / 10

Указатели, ссылки и память

Разберись с адресами памяти, операторами `&` и `*`, `nullptr`, динамической памятью (`new`/`delete`) и `std::unique_ptr`, который управляет ею безопасно.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять, что такое указатель, получать адрес через & и разыменовывать через *
  • Сравнивать указатели со ссылками и проверять nullptr
  • Объяснять, как new и delete выделяют динамическую память и чем они опасны
  • Применять принцип RAII и std::unique_ptr

Ты хочешь дать другу книгу, но она очень тяжёлая. Вместо того чтобы нести её, ты пишешь на листке адрес: «улица Низами, 12, третья полка». По адресу друг сам найдёт книгу. В C++ указатель — как раз такой листок: он хранит не само значение, а его адрес в памяти. Указатели дают C++ скорость и гибкость, но невнимательность может дорого обойтись.

Адреса и указатели

Память компьютера состоит из пронумерованных ячеек, и каждая переменная находится по некоторому адресу. Оператор & возвращает адрес переменной. Адрес обычно выводится в шестнадцатеричной системе и может меняться при каждом запуске программы:

C++
#include <iostream>

int main() {
    int coins = 50;
    std::cout << "Value: " << coins << '\n';
    std::cout << "Address: " << &coins << '\n';
    return 0;
}
Во второй строке появится адрес вроде 0x7ffc5a2b1c4c — на каждом компьютере и при каждом запуске свой.
Определение
Указатель

Переменная, которая хранит адрес другой переменной в памяти. Запись int* p означает «p — указатель на значение типа int». Выражение *p обращается к значению по этому адресу — это называется разыменованием.

C++
#include <iostream>

int main() {
    int coins = 50;
    int* ptr = &coins;

    std::cout << "coins = " << coins << '\n';
    std::cout << "*ptr = " << *ptr << '\n';

    *ptr = 75;
    std::cout << "coins = " << coins << '\n';
    std::cout << std::boolalpha << (ptr == &coins) << '\n';
    return 0;
}
Ожидаемый результат
coins = 50
*ptr = 50
coins = 75
true
*ptr = 75 меняет значение по адресу указателя, то есть саму переменную coins.
ЗаписьЗначение
&xадрес переменной x
int* pобъявление указателя на int
*pзначение по адресу из p
nullptrуказатель, который никуда не указывает
p->nameкраткая запись (*p).name

Ссылки и nullptr

В прошлых уроках ты познакомился со ссылками (int& ref = a;) — ссылка это второе имя переменной. В отличие от указателя, ссылка обязательно привязывается к переменной при объявлении и потом не может быть перепривязана к другой. Указатель же в любой момент можно направить на другой адрес, а может и вовсе никуда не указывать.

C++
#include <iostream>

int main() {
    int a = 10;
    int b = 20;
    int& ref = a;
    int* ptr = &a;

    ref += 5;
    std::cout << "a = " << a << '\n';

    ptr = &b;
    *ptr += 5;
    std::cout << "b = " << b << '\n';
    std::cout << "a = " << a << '\n';
    return 0;
}
Ожидаемый результат
a = 15
b = 25
a = 15

Указателю, который никуда не указывает, присваивают nullptr. Разыменование nullptr (*p) — неопределённое поведение, и обычно программа аварийно завершается. Поэтому, если указатель может быть пустым, проверяй его перед использованием:

C++
#include <iostream>

void printValue(const int* p) {
    if (p == nullptr) {
        std::cout << "No value\n";
        return;
    }
    std::cout << "Value: " << *p << '\n';
}

int main() {
    int level = 3;
    printValue(&level);
    printValue(nullptr);
    return 0;
}
Ожидаемый результат
Value: 3
No value

Динамическая память: new и delete

Обычные локальные переменные создаются в стеке и автоматически удаляются, когда функция завершается. Но иногда память нужно выделить во время работы программы и нужного размера. Оператор new выделяет место в куче (heap) и возвращает его адрес. Такая память сама не освобождается: её нужно вручную вернуть через delete (а массив — через delete[]).

C++
#include <iostream>

int main() {
    int* p = new int(42);
    std::cout << *p << '\n';
    delete p;
    p = nullptr;

    int size = 4;
    int* squares = new int[size];
    for (int i = 0; i < size; i++) {
        squares[i] = i * i;
    }
    std::cout << squares[3] << '\n';
    delete[] squares;
    return 0;
}
Ожидаемый результат
42
9

RAII и умные указатели

Современный C++ решает эти проблемы с помощью принципа RAII (Resource Acquisition Is Initialization — «получение ресурса есть инициализация»): объект берёт ресурс во владение и автоматически освобождает его при своём уничтожении. Именно так работают std::vector и std::string. Для одиночного объекта есть умный указатель std::unique_ptr из <memory>: он единственный владелец памяти и сам удаляет её, выходя из области видимости.

C++
#include <iostream>
#include <memory>
#include <utility>

int main() {
    std::unique_ptr<int> score = std::make_unique<int>(100);
    *score += 50;
    std::cout << "Score: " << *score << '\n';

    std::unique_ptr<int> other = std::move(score);
    std::cout << std::boolalpha << (score == nullptr) << '\n';
    std::cout << "Other: " << *other << '\n';
    return 0;
}   // other deletes the memory automatically
Ожидаемый результат
Score: 150
true
Other: 150
unique_ptr нельзя скопировать, владение можно только передать через std::move; после этого score пуст.

Главное

  • Указатель хранит адрес другой переменной: int* p = &x;.
  • & даёт адрес, а * обращается к значению по адресу.
  • Ссылка всегда привязана к одной переменной; указатель можно перенаправить, и он может быть nullptr.
  • Память из new нужно освобождать через delete (для массивов delete[]), иначе будет утечка.
  • RAII: std::vector, std::string и std::unique_ptr освобождают память автоматически — отдавай им предпочтение.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что даёт выражение &x?