Указатели
Концепты
Заголовок раздела «Концепты»- Адрес памяти
- Указатель
- Обозначение типа указателя
- Получение адреса переменной
- Вписывание по адресу и считывание по адресу, используя dereference (разыменовывание)
- Размер указателя vs размер того, на что он указывает
- Почемы указатели с разным размером элемента несовместимы
Примеры на понимание
Заголовок раздела «Примеры на понимание»Объясните, что произойдет в следующих отрезках кода. Также, запустите данный код, чтобы убедиться в правильности вашей логики.
Там, где
mainне был расписан, код помещается в типичный основной файл:#include <iostream>#include <cstdint>int main(){// сюда}
1. Адрес переменной
Заголовок раздела «1. Адрес переменной»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?
Синтаксис определения переменных состоит из:
- типа;
- имеми переменной;
- необязательно, инициализации.
int* b{ &a }; — здесь:
int*— это тип;b— это имя переменной;{ &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;Ответ:
Можно брать адрес неинициализированной переменной. Напечатается как обычный адрес.
Недопустимо было бы считывать по этому адресу.
3. Оператор dereference (на запись)
Заголовок раздела «3. Оператор dereference (на запись)»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.
4. Число как адрес
Заголовок раздела «4. Число как адрес»Допустимо ли подобное?
int* b{ 32 };std::cout << *b;std::cout << std::endl;Ответ:
Нет. Адрес невозможно задать так напрямую, его необходимо получать используя
оператор & на что-то с чем связан адрес (например, переменную).
5. Оператор dereference (на чтение)
Заголовок раздела «5. Оператор dereference (на чтение)»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
6. Печать сложного выражения
Заголовок раздела «6. Печать сложного выражения»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.
7. Указатель на больший тип данных
Заголовок раздела «7. Указатель на больший тип данных»uint8_t a{ 5 };int* b{ &a };Правильный ответ:
Ошибка компиляции (см. видео про указатели)
8. Указатель на меньший тип данных
Заголовок раздела «8. Указатель на меньший тип данных»int a = 5;uint8_t* b = &a;Правильный ответ:
Ошибка компиляции (см. видео про указатели)
9. Зависимость адреса от значения
Заголовок раздела «9. Зависимость адреса от значения»Будет ли в b и в c тот же адрес?
int a = 5;int* b = &a;a = 6;int* c = &a;Правильный ответ:
Будет тот же адрес.
Переменные никогда не меняют свой адрес.
a = 6 впишет 6 в уже имеющуюся ячейку памяти.
Это не перенаправляет a в другую ячейку.
&a берет адрес ячейки a, а не значения в ней.
Он всегда будет давать тот же адрес, вне зависимости от того,
какое значение сохранили в a.
10. Это та же память!
Заголовок раздела «10. Это та же память!»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 напрямую.
11. Инициализация через указатель
Заголовок раздела «11. Инициализация через указатель»Допустимо ли подобное?
int a;int* b = &a;*b = 5;std::cout << a;std::cout << std::endl;Ответ:
Переменную можно инициализировать через указатель. Подобное допустимо.
12. Перезапись указателя
Заголовок раздела «12. Перезапись указателя»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.
13. Двойной указатель
Заголовок раздела «13. Двойной указатель»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; // скажем, адрес = 32int b = 6; // скажем, адрес = 36int* p = &a; // адрес p = 40, адрес в p = 32int** 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;14. Размеры указателей
Заголовок раздела «14. Размеры указателей»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-битной).
15. Размер указателя и переменной
Заголовок раздела «15. Размер указателя и переменной»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.
16. Указатель на самого себя
Заголовок раздела «16. Указатель на самого себя»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).