JavaRush /Курсы /C++ SELF /Переменные в памяти: адреса, ссылки и указатели

Переменные в памяти: адреса, ссылки и указатели

C++ SELF
6 уровень , 3 лекция
Открыта

1. Переменная как объект в памяти: значение и адрес

Представьте память компьютера как огромную линейку из маленьких ячеек по 1 байту:

flowchart TD
    subgraph Row1["Адреса"]
        direction LR
        R1000["1000"]
        R1001["1001"]
        R1002["1002"]
        R1003["1003"]
        R1004["1004"]
        R1005["1005"]
    end

    subgraph Row2["Данные (по 1 байту)"]
        direction LR
        B1000["[байт]"]
        B1001["[байт]"]
        B1002["[байт]"]
        B1003["[байт]"]
        B1004["[байт]"]
        B1005["[байт]"]
    end

Любая переменная занимает несколько байтов подряд:

  • char обычно 1 байт,
  • int часто 4 байта,
  • double часто 8 байт,
  • std::string внутри устроен сложно, но для нас важно одно: это тоже объект в памяти.

У каждого объекта в памяти есть адрес — “координата”, где объект начинается.

Адрес — это НЕ значение

Допустим у вас есть такой код:

int balance = 42;

У balance есть:

  • значение: 42
  • адрес: что-то вроде 0x7ffeefbff5ac (точное число не важно)

И это две разные сущности. Очень разные.

2. Оператор взятия адреса &

C++ позволяет получить адрес переменной: номер ячейки памяти где она хранится. Для этого используется оператор & и читается он так:

&x — “адрес переменной x

Возьмём переменную из нашего мини‑приложения “Копилка” (баланс) и посмотрим на неё глазами памяти:

#include <iostream>

int main() {
    int balance = 42;

    std::cout << "balance  = " << balance << '\n';
    std::cout << "&balance = " << &balance << '\n';
}

Адрес обычно печатается как 0x... (шестнадцатеричное число). И да: адрес может быть другим при следующем запуске программы. Это нормально: операционная система любит переставлять вещи (в том числе ради безопасности).

Какой тип у &balance?

Если balance имеет тип int, то &balance имеет тип int*. То есть “адрес int” — это значение типа “указатель на int”.

3. Размер объекта: sizeof

Раз тип — это ещё и “сколько памяти нужно”, логично уметь это проверять. Для этого есть оператор sizeof.

#include <iostream>

int main() {
    int x = 0;
    double y = 0.0;

    std::cout << "sizeof(x) = " << sizeof(x) << '\n';
    std::cout << "sizeof(y) = " << sizeof(y) << '\n';
}

Чаще всего вы увидите что-то вроде:

sizeof(x) = 4
sizeof(y) = 8

Это не “магия компилятора”, это просто факт: тип задаёт, сколько байт выделяется под объект.

И маленькая любопытная деталь: размер указателя часто одинаковый (например, 8 байт на 64‑битных системах), даже если он “указывает” то на int, то на double. Указатель хранит адрес, а адреса в рамках одной платформы имеют одну “длину”.

#include <iostream>

int main() {
    int x = 1;
    int* px = &x;

    std::cout << "sizeof(int)  = " << sizeof(int) << '\n';
    std::cout << "sizeof(px)   = " << sizeof(px) << '\n';
}

4. Что можно адресовать, а что нельзя

Адрес можно взять у того, что реально существует как отдельный объект.

У переменной адрес получить можно:

int x = 10;
std::cout << &x << '\n';

А вот у “промежуточного результата вычисления” адрес взять нельзя:

int x = 10;
std::cout << &(x + 1) << '\n'; // не компилируется

На текущем уровне достаточно правила: Адрес берём у “настоящей переменной/объекта”, а не у “результата формулы”.

5. Ссылка T&: “второе имя” объекта

Так же с помощью & можно делать переменные-ссылки. Ссылка — это alias. Псевдоним. Второе имя уже существующего объекта.

#include <iostream>

int main() {
    int balance = 100;
    int& refBalance = balance;   // refBalance — второе имя balance

    refBalance = refBalance + 50; // меняем через ссылку
    std::cout << balance << '\n'; // 150
}

Переменная refBalance указывает на тот же адрес памяти что и balance.

Важно понять суть: мы не создавали копию. Объект один, имён два.

Проверяем, что объект один: адреса совпадают

Чтобы убедиться, что две переменные указывают на одну область памяти, достаточно воспользоваться оператором &, с которым вы уже знакомы. Пример:

#include <iostream>

int main() {
    int balance = 100;
    int& refBalance = balance;

    std::cout << &balance << '\n';
    std::cout << &refBalance << '\n'; // тот же адрес
}

Если адрес одинаковый, то и объект один и тот же.

Правила ссылок (коротко, но строго)

Переменную-ссылку нужно инициализировать сразу. То есть так нельзя:

int& r; // ошибка: ссылка не может быть “пустой”

Ссылка не “переназначается” на другой объект. И вот здесь ломается мозг у всех новичков (вы не исключение, расслабьтесь):

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int& r = a; // r — это a
    r = b;      // это НЕ “перепривязать r”
                // это “присвоить a значение b”

    std::cout << a << '\n'; // 2
}

То есть r = b; — обычное присваивание, потому что r ведёт себя как “то же самое, что a”.

На нашем уровне это полезно запомнить как простую мысль: ссылка всегда ссылается на реальный объект.

Не путайте & в типе и & как оператор

Это два разных смысла одного символа:

  • int& ref = balance;& часть типа (ссылка)
  • &balanceоперация “взять адрес”

C++ тут как тот друг, который один и тот же мем использует в разных ситуациях: “ну вы же поняли по контексту”. Да, по контексту.

6. Указатель T*: переменная, которая хранит адрес

Указатель — это не ссылка, это отдельная переменная. Её значение — адрес другого объекта.

#include <iostream>

int main() {
    int balance = 100;
    int* pBalance = &balance; // pBalance хранит адрес balance

    std::cout << "pBalance  = " << pBalance << '\n';  // адрес
    std::cout << "*pBalance = " << *pBalance << '\n'; // значение по адресу
}

Тут важно разделить в голове три сущности:

  • balance — объект с числом
  • &balance — адрес объекта
  • pBalance — объект, который хранит адрес (то есть pBalance == &balance)

Разыменование *p

Оператор * в выражении означает “взять значение по адресу”.

#include <iostream>

int main() {
    int balance = 100;
    int* pBalance = &balance;

    *pBalance = *pBalance + 50;   // меняем balance через указатель
    std::cout << balance << '\n'; // 150
}

Указатель тоже переменная (у него тоже есть адрес)

Это иногда помогает “приземлить” картину мира:

#include <iostream>

int main() {
    int balance = 100;
    int* pBalance = &balance;

    std::cout << "&balance  = " << &balance << '\n';
    std::cout << "pBalance  = " << pBalance << '\n';
    std::cout << "&pBalance = " << &pBalance << '\n';
}

&pBalance — это адрес самого указателя в памяти.

nullptr: указатель “в никуда”

Указатель может иметь специальное значение nullptr — “ни на что не указывает”.

#include <iostream>

int main() {
    int* p = nullptr;

    if (p != nullptr) {
        std::cout << *p << '\n'; // безопасно только если p валиден
    } else {
        std::cout << "Pointer is null\n";
    }
}

На текущем уровне запомните правило безопасности, как правило дорожного движения: Разыменовывать *p можно только если p != nullptr и указывает на живой объект.

Указатель можно переназначать

В отличие от ссылки, указатель можно “перенаправить” на другой объект:

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int* p = &a;
    std::cout << *p << '\n'; // 1

    p = &b;                  // теперь p указывает на b
    std::cout << *p << '\n'; // 2
}

7. Полезные нюансы

Бонус: -> для указателей на объекты

Если у вас указатель на объект, и вы хотите вызвать метод, есть два равных варианта:

  • (*p).method() — длинно, но честно
  • p->method() — коротко и удобно
#include <iostream>
#include <string>

int main() {
    std::string owner = "Alex";
    std::string* pOwner = &owner;

    std::cout << (*pOwner).size() << '\n';
    std::cout << pOwner->size() << '\n'; // то же самое
}

Переменная vs ссылка vs указатель

Сущность Что это Может быть “пустой”? Можно перенаправить? Как получить значение
int x
сам объект
x
int& r = x
второе имя объекта
x
нет нет
r
int* p = &x
отдельный объект, хранит адрес
x
да (
nullptr
)
да
*p

Если хочется коротко “в одно предложение”:

Ссылка выглядит как переменная и ведёт к одному и тому же объекту, а указатель — это отдельная переменная‑адрес, которую нужно проверять и разыменовывать.

Область видимости и “висячие” адреса: когда адрес становится опасным

Вы уже знаете, что переменная живёт в пределах блока { ... }. Теперь добавим к этому важное следствие: Адрес корректен только пока жив объект, на который он указывает.

Самый простой пример “висячего указателя”:

#include <iostream>

int main() {
    int* p = nullptr;

    { 
        int temp = 123;
        p = &temp;
        std::cout << *p << '\n'; // 123 — тут всё хорошо
    }

    // temp уже не существует, а p хранит старый адрес.
    std::cout << *p << '\n'; // ОПАСНО (не делайте так)
}

Почему это так коварно? Потому что программа иногда “как будто работает”, а иногда падает, а иногда начинает печатать ерунду. Это одна из причин, почему указатели считают “опасными”: они дают много свободы, а свобода требует дисциплины.

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

8. Встраиваем тему в наше мини‑приложение “Копилка”: режим инспектора

Мы начали “Копилку” в прошлых лекциях: читаем пополнение и расход, обновляем баланс. Сегодня добавим режим “инспектора”: покажем, что баланс — это не абстрактное число, а объект с адресом, и что его можно менять разными способами.

Баланс и его адрес

#include <iostream>

int main() {
    int balance = 0;

    std::cout << "balance  = " << balance << '\n';
    std::cout << "&balance = " << &balance << '\n';
}

Ссылка на баланс: меняем “как будто напрямую”

#include <iostream>

int main() {
    int balance = 0;
    int& refBalance = balance;

    refBalance = refBalance + 10; // пополнили через ссылку
    std::cout << balance << '\n'; // 10
}

Указатель на баланс: меняем через *

#include <iostream>

int main() {
    int balance = 0;
    int* pBalance = &balance;

    *pBalance = *pBalance + 25;   // пополнили через указатель
    std::cout << balance << '\n'; // 25
}

Мини-сценарий: пополнение, расход и “один и тот же объект”

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

#include <iostream>

int main() {
    int balance = 0;
    int& refBalance = balance;
    int* pBalance = &balance;

    int deposit = 0, spend = 0;
    std::cin >> deposit >> spend;

    refBalance = refBalance + deposit; // через ссылку
    *pBalance = *pBalance - spend;     // через указатель

    std::cout << "balance = " << balance << '\n';
}

Если вы сейчас подумали “это странно, зачем так делать, можно же просто balance = balance + deposit;” — отлично! Значит вы понимаете, что это учебный эксперимент, а не стиль для реального проекта.

Смысл упражнения в другом: вы учитесь видеть, что refBalance и *pBalance работают с тем же самым объектом.

9. Типичные ошибки

Ошибка №1: неинициализированный указатель.
Если написать int* p; и не дать ему значение, то внутри будет “мусорный адрес”. Иногда программа сразу упадёт при *p, иногда начнёт портить память, а иногда (самый опасный вариант) “почти работает”. На уровне привычки лечится просто: любой указатель либо сразу получает валидный адрес, либо nullptr.

Ошибка №2: разыменование nullptr.
nullptr — это не “нулевой объект”, это “вообще нет объекта”. Разыменование *p при p == nullptr — плохая идея, которая обычно заканчивается аварийно. Поэтому проверка if (p != nullptr) перед разыменованием — не паранойя, а базовая гигиена.

Ошибка №3: путаница * и & в объявлении и в выражении.
int* p — звёздочка часть типа (“указатель”).
*p — звёздочка действие (“взять значение по адресу”).
Точно так же int& r — амперсанд часть типа (“ссылка”), а &x — операция “взять адрес”. Один и тот же символ, два смысла — классика C++: язык экономит символы, а вы потом тратите нервные клетки. Со временем это начинает читаться автоматически.

Ошибка №4: ожидание, что ссылку можно “перепривязать”.
Если у вас int& r = a;, то r навсегда про a. Строка r = b; не меняет “куда смотрит ссылка”, она меняет значение a. Если вам нужна именно возможность переключаться между объектами — это уже сценарий указателя.

Ошибка №5: висячий указатель после выхода из блока.
Если вы взяли адрес переменной, которая живёт внутри { ... }, то после закрывающей } объект уничтожен, а адрес становится “висячим”. Это не “редкая теория”, это реальная причина странных багов. Хорошая привычка: не хранить адреса на “внутренние” переменные дольше, чем живёт их блок, и особенно не использовать такие адреса после выхода из блока.

1
Задача
C++ SELF, 6 уровень, 3 лекция
Недоступна
Сила ссылки
Сила ссылки
1
Задача
C++ SELF, 6 уровень, 3 лекция
Недоступна
Выбор указателя
Выбор указателя
1
Задача
C++ SELF, 6 уровень, 3 лекция
Недоступна
Указательный обмен
Указательный обмен
1
Задача
C++ SELF, 6 уровень, 3 лекция
Недоступна
Команды на память
Команды на память
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #2923722 Уровень 17
8 мая 2026
А в чем разница между nullptr, NULL, null, 0? Почему не использовать например 0 во всех случаях?