Перейти к содержимому

Указатели

  • Адрес памяти
  • Указатель
  • Обозначение типа указателя
  • Получение адреса переменной
  • Вписывание по адресу и считывание по адресу, используя dereference (разыменовывание)
  • Размер указателя vs размер того, на что он указывает
  • Почемы указатели с разным размером элемента несовместимы

Объясните, что произойдет в следующих отрезках кода. Также, запустите данный код, чтобы убедиться в правильности вашей логики.

Там, где main не был расписан, код помещается в типичный основной файл:

#include <iostream>
#include <cstdint>
int main()
{
// сюда
}
int a{ 5 };
int* b{ &a }; // int* b = &a;
std::cout << b;
std::cout << std::endl;
Ответ:

int* b{ &a } сохраняет в b адрес переменной a. Далее печатается адрес, сохраненный в b. Не значение по адресу из b (записали бы как *b), а адрес в b.

Какой тип у переменной b?

Синтаксис определения переменных состоит из:

  1. типа;
  2. имеми переменной;
  3. необязательно, инициализации.

int* b{ &a }; — здесь:

  1. int* — это тип;
  2. b — это имя переменной;
  3. { &a } — это инициализация.

У b тип не int, а int*!

Какой тип у выражения &a?

& — это оператор, получающий адрес переменной. & применяется не на значение a, а на саму переменную a.

Тип &a — это int*. Этот тип хранит во время компиляции информацию о том, что получаемый адрес является именно адресом переменной типа int, а не какого-то другого типа.

& способен дать адрес любого объекта, но об этом в 3-ей лабе.

2. Адрес неинициализированной переменной.

Заголовок раздела «2. Адрес неинициализированной переменной.»

Допустимо ли подобное?

int a;
int* b{ &a };
std::cout << b;
std::cout << std::endl;
Ответ:

Можно брать адрес неинициализированной переменной. Напечатается как обычный адрес.

Недопустимо было бы считывать по этому адресу.

int a { 1 };
int* b{ &a };
*b = 2;
std::cout << a;
std::cout << std::endl;
Ответ:

В b был сохранен адрес переменной a.

На строчке *b = 2, *b позволяет сослатся на переменную, находящуюся по адресу из b, то есть на a.

*b = 2 -> a = 2 вписывает 2 в a.

Допустимо ли подобное?

int* b{ 32 };
std::cout << *b;
std::cout << std::endl;
Ответ:

Нет. Адрес невозможно задать так напрямую, его необходимо получать используя оператор & на что-то с чем связан адрес (например, переменную).

int a{ 5 };
int b{ *(&a) };
std::cout << b;
std::cout << std::endl;
Правильный ответ:

Вычисление выражения *(&a):

  • &a переходит в адрес переменной a (скажем, 32).
  • *32 переходит по адресу, позволяя обращатся к переменной a.
  • ссылка на a в качестве выражения дает значение переменной a, 5.

*(&a) -> *32 -> a -> 5

int a{ 5 };
int* b{ &a };
std::cout << (*b) + 7;
std::cout << std::endl;
Правильный ответ:

(*b) + 7 это выражение. Вычисляется по частям:

  • (*b) означает переход по адресу из b и восприятие этого как переменной a.
  • (*b) + 7 -> a + 7 - a заменяется на значение из a, потому что используется в качестве выражения.
  • 5 + 7 -> 12.
uint8_t a{ 5 };
int* b{ &a };
Правильный ответ:

Ошибка компиляции (см. видео про указатели)

int a = 5;
uint8_t* b = &a;
Правильный ответ:

Ошибка компиляции (см. видео про указатели)

Будет ли в b и в c тот же адрес?

int a = 5;
int* b = &a;
a = 6;
int* c = &a;
Правильный ответ:

Будет тот же адрес.

Переменные никогда не меняют свой адрес. a = 6 впишет 6 в уже имеющуюся ячейку памяти. Это не перенаправляет a в другую ячейку.

&a берет адрес ячейки a, а не значения в ней. Он всегда будет давать тот же адрес, вне зависимости от того, какое значение сохранили в a.

int a = 5;
int* ap = &a;
*ap = 6;
std::cout << a;
std::cout << std::endl;
a = 7;
std::cout << *ap;
std::cout << std::endl;
Ответ:

Запись и чтение всегда происходит по тому же адресу. ap содержит адрес переменной a. Запись в или чтение из *ap эквивалентно работе с a напрямую.

Допустимо ли подобное?

int a;
int* b = &a;
*b = 5;
std::cout << a;
std::cout << std::endl;
Ответ:

Переменную можно инициализировать через указатель. Подобное допустимо.

int a = 5;
int* p = &a;
*p = 6;
int b = 7;
p = &b;
*p = 8;
std::cout << a;
std::cout << std::endl;
std::cout << b;
std::cout << std::endl;
Ответ:

На строчке p = &b перезаписывается сам адрес из p на адрес другой переменной (b).

*p = 8 впишет 8 уже в b.

int a = 5;
int b = 6;
int* p = &a;
int** pp = &p;
**pp = 7;
*pp = &b;
**pp = 8;
std::cout << a;
std::cout << std::endl;
std::cout << b;
std::cout << std::endl;
Ответ
int a = 5; // скажем, адрес = 32
int b = 6; // скажем, адрес = 36
int* p = &a; // адрес p = 40, адрес в p = 32
int** pp = &p; // адрес в pp = 40
**pp = 7; // *(*pp) --> *(40) --> *(p) --> *32 --> a
// то есть a = 7
*pp = &b; // адрес в p = 36
**pp = 8; // *(*pp) --> *(40) --> *(p) --> *36 --> b
// то есть b = 8
std::cout << a;
std::cout << std::endl;
std::cout << b;
std::cout << std::endl;
int a = 7;
int* pa = &a;
void* voidp = pa;
uint8_t c = 9;
uint8_t* pc = &c;
std::cout << sizeof(pa);
std::cout << std::endl;
std::cout << sizeof(voidp);
std::cout << std::endl;
std::cout << sizeof(pc);
std::cout << std::endl;
Ответ

Размер указателя любого типа тот же, потому что это лишь адрес памяти.

На 64-битных системах размер любого указателя 64 бита (скорее всего вы на 64-битной).

int a = 7;
int* ap = &a;
std::cout << sizeof(a);
std::cout << std::endl;
std::cout << sizeof(ap);
std::cout << std::endl;
std::cout << sizeof(*ap);
std::cout << std::endl;
Ответ

sizeof(a) это то же, что и sizeof(int), 4.

sizeof(ap) это то же, что и sizeof(int*), 8.

sizeof(*ap) не вычисляет выражение *ap, а лишь смотрит, какой тип у результата был бы. Чтение из *ap дало бы int, поэтому считается sizeof(int), 4.

void* p = nullptr;
p = static_cast<void*>(&p);
std::cout << p;
std::cout << std::endl;
std::cout << &p;
std::cout << std::endl;
Ответ

&p берет адрес самой переменной p. Тип выражения &p — это void** (адрес переменной типа void*).

static_cast<void*>(...) явно преобразует этот void** в void*, то есть в тип самой переменной p. Поэтому присваивание p = static_cast<void*>(&p) сохраняет в p адрес самой p — теперь p указывает на саму себя.

Напечатаются два одинаковых адреса: значение в p и адрес &p совпадают.

Если static_cast<void*> тут опустить, он все равно произойдет сам по себе

17. Копирование переменной и указателя с похожими именами

Заголовок раздела «17. Копирование переменной и указателя с похожими именами»
int x{ 0 };
int* px{ &x };
int y{ x };
int* py{ px };
*px = 1;
std::cout << x << std::endl;
std::cout << y << std::endl;
std::cout << *px << std::endl;
std::cout << *py << std::endl;
Ответ:

int y{ x } копирует значение из x — дальше y независима от x, поэтому запись через указатель ее не меняет.

int* py{ px } копирует адрес из px — оба указателя смотрят на одну и ту же переменную x.

*px = 1 вписывает 1 в x, поэтому напечатается:

  • x — 1,
  • y — 0 (значение скопировано до записи),
  • *px — 1,
  • *py — 1 (указывает туда же, куда и px).