На уроке №10 мы узнали, что переменная — это название кусочка памяти, который содержит значение.
Оператор адреса &
При выполнении инициализации переменной, ей автоматически присваивается свободный адрес памяти, и, любое значение, которое мы присваиваем переменной, сохраняется по этому адресу в памяти. Например:
1 |
int b = 8; |
При выполнении этого стейтмента процессором, выделяется часть оперативной памяти. В качестве примера предположим, что переменной b
присваивается ячейка памяти под номером 150. Всякий раз, когда программа встречает переменную b
в выражении или в стейтменте, она понимает, что для того, чтобы получить значение — ей нужно заглянуть в ячейку памяти под номером 150.
Хорошая новость — нам не нужно беспокоиться о том, какие конкретно адреса памяти выделены для определенных переменных. Мы просто ссылаемся на переменную через присвоенный ей идентификатор, а компилятор конвертирует это имя в соответствующий адрес памяти. Однако этот подход имеет некоторые ограничения, которые мы обсудим на этом и следующих уроках.
Оператор адреса &
позволяет узнать, какой адрес памяти присвоен определенной переменной. Всё довольно просто:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int a = 7; std::cout << a << '\n'; // выводим значение переменной a std::cout << &a << '\n'; // выводим адрес памяти переменной a return 0; } |
Результат на моем компьютере:
7
0046FCF0
Примечание: Хотя оператор адреса выглядит так же, как оператор побитового И, отличить их можно по тому, что оператор адреса является унарным оператором, а оператор побитового И — бинарным оператором.
Оператор разыменования *
Оператор разыменования *
позволяет получить значение по указанному адресу:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { int a = 7; std::cout << a << '\n'; // выводим значение переменной a std::cout << &a << '\n'; // выводим адрес переменной a std::cout << *&a << '\n'; /// выводим значение ячейки памяти переменной a return 0; } |
Результат на моем компьютере:
7
0046FCF0
7
Примечание: Хотя оператор разыменования выглядит так же, как и оператор умножения, отличить их можно по тому, что оператор разыменования — унарный, а оператор умножения — бинарный.
Указатели
Теперь, когда мы уже знаем об операторах адреса и разыменования, мы можем поговорить об указателях.
Указатель — это переменная, значением которой является адрес ячейки памяти. Указатели объявляются точно так же, как и обычные переменные, только со звёздочкой между типом данных и идентификатором:
1 2 3 4 5 6 7 |
int *iPtr; // указатель на значение типа int double *dPtr; // указатель на значение типа double int* iPtr3; // корректный синтаксис (допустимый, но не желательный) int * iPtr4; // корректный синтаксис (не делайте так) int *iPtr5, *iPtr6; // объявляем два указателя для переменных типа int |
Синтаксически язык C++ принимает объявление указателя, когда звёздочка находится рядом с типом данных, с идентификатором или даже посередине. Обратите внимание, эта звёздочка не является оператором разыменования. Это всего лишь часть синтаксиса объявления указателя.
Однако, при объявлении нескольких указателей, звёздочка должна находиться возле каждого идентификатора. Это легко забыть, если вы привыкли указывать звёздочку возле типа данных, а не возле имени переменной. Например:
1 |
int* iPtr3, iPtr4; // iPtr3 - это указатель на значение типа int, а iPtr4 - это обычная переменная типа int! |
По этой причине, при объявлении указателя, рекомендуется указывать звёздочку возле имени переменной. Как и обычные переменные, указатели не инициализируются при объявлении. Содержимым неинициализированного указателя является обычный мусор.
Присваивание значений указателю
Поскольку указатели содержат только адреса, то при присваивании указателю значения — это значение должно быть адресом. Для получения адреса переменной используется оператор адреса:
1 2 |
int value = 5; int *ptr = &value; // инициализируем ptr адресом значения переменной |
Приведенное выше можно проиллюстрировать следующим образом:
Вот почему указатели имеют такое имя: ptr
содержит адрес значения переменной value
, и, можно сказать, ptr
указывает на это значение.
Еще очень часто можно увидеть следующее:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int main() { int value = 5; int *ptr = &value; // инициализируем ptr адресом значения переменной std::cout << &value << '\n'; // выводим адрес значения переменной value std::cout << ptr << '\n'; // выводим адрес, который хранит ptr return 0; } |
Результат на моем компьютере:
003AFCD4
003AFCD4
Тип указателя должен соответствовать типу переменной, на которую он указывает:
1 2 3 4 5 6 7 |
int iValue = 7; double dValue = 9.0; int *iPtr = &iValue; // ок double *dPtr = &dValue; // ок iPtr = &dValue; // неправильно: указатель типа int не может указывать на адрес переменной типа double dPtr = &iValue; // неправильно: указатель типа double не может указывать на адрес переменной типа int |
Следующее не является допустимым:
1 |
int *ptr = 7; |
Это связано с тем, что указатели могут содержать только адреса, а целочисленный литерал 7
не имеет адреса памяти. Если вы все же сделаете это, то компилятор сообщит вам, что он не может преобразовать целочисленное значение в целочисленный указатель.
Язык C++ также не позволит вам напрямую присваивать адреса памяти указателю:
1 |
double *dPtr = 0x0012FF7C; // не ок: рассматривается как присваивание целочисленного литерала |
Оператор адреса возвращает указатель
Стоит отметить, что оператор адреса &
не возвращает адрес своего операнда в качестве литерала. Вместо этого он возвращает указатель, содержащий адрес операнда, тип которого получен из аргумента (например, адрес переменной типа int передается как адрес указателя на значение типа int):
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> #include <typeinfo> int main() { int x(4); std::cout << typeid(&x).name(); return 0; } |
Результат выполнения программы:
Разыменование указателей
Как только у нас есть указатель, указывающий на что-либо, мы можем его разыменовать, чтобы получить значение, на которое он указывает. Разыменованный указатель — это содержимое ячейки памяти, на которую он указывает:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int value = 5; std::cout << &value << std::endl; // выводим адрес value std::cout << value << std::endl; // выводим содержимое value int *ptr = &value; // ptr указывает на value std::cout << ptr << std::endl; // выводим адрес, который хранится в ptr, т.е. &value std::cout << *ptr << std::endl; // разыменовываем ptr (получаем значение на которое указывает ptr) return 0; } |
Результат:
0034FD90
5
0034FD90
5
Вот почему указатели должны иметь тип данных. Без типа указатель не знал бы, как интерпретировать содержимое, на которое он указывает (при разыменовании). Также, поэтому и должны совпадать тип указателя с типом переменной. Если они не совпадают, то указатель при разыменовании может неправильно интерпретировать биты (например, вместо типа double использовать тип int).
Одному указателю можно присваивать разные значения:
1 2 3 4 5 6 7 8 9 10 |
int value1 = 5; int value2 = 7; int *ptr; ptr = &value1; // ptr указывает на value1 std::cout << *ptr; // выведется 5 ptr = &value2; // ptr теперь указывает на value2 std::cout << *ptr; // выведется 7 |
Когда адрес значения переменной присвоен указателю, то выполняется следующее:
ptr
— это то же самое, что и &value
;
*ptr
обрабатывается так же, как и value
.
Поскольку *ptr
обрабатывается так же, как и value
, то мы можем присваивать ему значения так, как если бы это была обычная переменная. Например:
1 2 3 4 5 |
int value = 5; int *ptr = &value; // ptr указывает на value *ptr = 7; // *ptr - это то же самое, что и value, которому мы присвоили значение 7 std::cout << value; // выведется 7 |
Разыменование некорректных указателей
Указатели в языке C++ по своей природе являются небезопасными, а их неправильное использование — один из лучших способов получить сбой программы.
При разыменовании указателя, программа пытается перейти в ячейку памяти, которая хранится в указателе и извлечь содержимое этой ячейки. По соображениям безопасности современные операционные системы (ОС) запускают программы в песочнице для предотвращения их неправильного взаимодействия с другими программами и для защиты стабильности самой операционной системы. Если программа попытается получить доступ к ячейке памяти, не выделенной для нее операционной системой, то ОС сразу завершит выполнение этой программы.
Следующая программа хорошо иллюстрирует вышесказанное. При запуске вы получите сбой (попробуйте, ничего страшного с вашим компьютером не произойдет):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> void foo(int *&p) { } int main() { int *p; // создаем неинициализированный указатель (содержимым которого является мусор) foo(p); // вводим компилятор в заблуждение, будто бы собираемся присвоить указателю корректное значение std::cout << *p; // разыменовываем указатель с мусором return 0; } |
Размер указателей
Размер указателя зависит от архитектуры, на которой скомпилирован исполняемый файл: 32-битный исполняемый файл использует 32-битные адреса памяти. Следовательно, указатель на 32-битном устройстве занимает 32 бита (4 байта). С 64-битным исполняемым файлом указатель будет занимать 64 бита (8 байт). И это вне зависимости от того, на что указывает указатель:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
char *chPtr; // тип char занимает 1 байт int *iPtr; // тип int занимает 4 байта struct Something { int nX, nY, nZ; }; Something *somethingPtr; std::cout << sizeof(chPtr) << '\n'; // выведется 4 std::cout << sizeof(iPtr) << '\n'; // выведется 4 std::cout << sizeof(somethingPtr) << '\n'; // выведется 4 |
Как вы можете видеть, размер указателя всегда один и тот же. Это связано с тем, что указатель — это всего лишь адрес памяти, а количество бит, необходимое для доступа к адресу памяти на определенном устройстве, — всегда постоянное.
В чём польза указателей?
Сейчас вы можете подумать, что указатели являются непрактичными и вообще ненужными. Зачем использовать указатель, если мы можем использовать исходную переменную?
Однако, оказывается, указатели полезны в следующих случаях:
Случай №1: Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву.
Случай №2: Они являются единственным способом динамического выделения памяти в C++. Это, безусловно, самый распространенный вариант использования указателей.
Случай №3: Они могут использоваться для передачи большого количества данных в функцию без копирования этих данных.
Случай №4: Они могут использоваться для передачи одной функции в качестве параметра другой функции.
Случай №5: Они используются для достижения полиморфизма при работе с наследованием.
Случай №6: Они могут использоваться для представления одной структуры/класса в другой структуре/классе, формируя, таким образом, целые цепочки.
Указатели применяются во многих случаях. Не волнуйтесь, если вы многого не понимаете из вышесказанного. Теперь, когда мы разобрались с указателями на базовом уровне, мы можем начать углубляться в отдельные случаи, в которых они полезны, что мы и сделаем на последующих уроках.
Заключение
Указатели — это переменные, которые содержат адреса памяти. Их можно разыменовать с помощью оператора разыменования *
для извлечения значений, хранимых по адресу памяти. Разыменование указателя, значением которого является мусор, приведет к сбою в вашей программе.
Совет: При объявлении указателя указывайте звёздочку возле имени переменной.
Тест
Задание №1
Какие значения мы получим в результате выполнения следующей программы (предположим, что это 32-битное устройство, и тип short занимает 2 байта):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
short value = 7; // &value = 0012FF60 short otherValue = 3; // &otherValue = 0012FF54 short *ptr = &value; std::cout << &value << '\n'; std::cout << value << '\n'; std::cout << ptr << '\n'; std::cout << *ptr << '\n'; std::cout << '\n'; *ptr = 9; std::cout << &value << '\n'; std::cout << value << '\n'; std::cout << ptr << '\n'; std::cout << *ptr << '\n'; std::cout << '\n'; ptr = &otherValue; std::cout << &otherValue << '\n'; std::cout << otherValue << '\n'; std::cout << ptr << '\n'; std::cout << *ptr << '\n'; std::cout << '\n'; std::cout << sizeof(ptr) << '\n'; std::cout << sizeof(*ptr) << '\n'; |
Ответ №1
Значения:
0012FF60
7
0012FF60
7
0012FF60
9
0012FF60
9
0012FF54
3
0012FF54
3
4
2
Краткое объяснение по поводу последней пары: 4
и 2
. 32-битное устройство означает, что размер указателя составляет 32 бита, но оператор sizeof всегда выводит размер в байтах: 32 бита = 4 байта. Таким образом, sizeof(ptr)
равен 4
. Поскольку ptr
является указателем на значение типа short, то *ptr
является типа short. Размер short в этом примере составляет 2 байта. Таким образом, sizeof(*ptr)
равен 2
.
Задание №2
Что не так со следующим фрагментом кода:
1 2 3 |
int value = 45; int *ptr = &value; // объявляем указатель и инициализируем его адресом переменной value *ptr = &value; // присваиваем адрес value для ptr |
Ответ №2
Последняя строка не скомпилируется. Рассмотрим эту программу детально.
В первой строке находится стандартное определение переменной вместе с инициализируемым значением. Здесь ничего особенного.
Во второй строке мы определяем новый указатель с именем ptr
и присваиваем ему адрес переменной value
. Помним, что в этом контексте звёздочка является частью синтаксиса объявления указателя, а не оператором разыменования. Так что и в этой строке всё нормально.
В третьей строке звёздочка уже является оператором разыменования, и используется для вытаскивания значения, на которое указывает указатель. Таким образом, эта строка говорит: «Вытаскиваем значение, на которое указывает ptr
(целочисленное значение), и переписываем его на адрес этого же значения». А это уже какая-то чепуха — вы не можете присвоить адрес целочисленному значению!
Третья строка должна быть:
1 |
ptr = &value; |
В вышеприведенной строке мы корректно присваиваем указателю адрес значения переменной.
Подскажите пожалуйста, какой тип переменной в параметре функции foo, int *&p, что то не встречал такого определения?
Спасибо
Это ссылка. Тема, связанная с ссылками будет на следующих уроках, поэтому ее синтаксис может быть вам незнаком.
Для создания ссылки используется символ амперсанда &.
Ссылку можно создавать на множество различных объектов, переменные, указатели и так далее.
Ссылка — это разыменованный указатель.
В нашем случае я полагаю, что это указатель, который содержит адрес другого указателя, а затем неявно разыменовывается.
Эта тема будет на последующих уроках. Там будут все эти нюансы.
Например, создаем переменную и ссылку на нее:
ref — будет работать как псевдоним для переменной x: любое использование ref будет равносильно использованию переменной x, которая была привязана к ссылке.
Например:
x будет равен 5.
То есть ref — это указатель, содержащий адрес ячейки x, который затем разыменовывается.
Аналог ссылке можно сделать так:
*ptr — это как ссылка, только здесь она явно разыменовывается с помощью оператора разыменования *.
"Размер указателя зависит от архитектуры, на которой скомпилирован исполняемый файл:…"
Странно: архитектура моего устройства(ноутбука) x64 разрядная, а размер указателя выводится в 32 бита или 4 байта. Значит эта информация не актуальна?
Прошу прощения за флуд. Вроде бы разобрался в чем причина: в конфигурации сборки компилятора.
Спасибо, информация хорошо разжевана. Впервые разобрался с указателями.
Для любителей поскрипеть мозгами и залезть глубже:
По сути, нам не нужна переменная, в которой будет храниться значение, мы лишь храним адрес на это значение.
Но в этом случае переменная не удалится автоматически, а останется в памяти приложения.
Чуть добавлю, хотя автор и так всё это знает. Вся эта фигня — указатели, ссылки — так или иначе связана с косвенной адресацией. Зачем она нужна? Затем, чтобы увеличить производительность. Так как при косвенной адресации, у нас не меняется сама команда: [код операции + указатель на адрес]. А указатель — например, FSR, как в PIC контроллерах. Адрес то FSR — 04h не меняется, а меняем мы лишь его содержимое, загоняя туда адреса операндов из портов МК. В итоге, удлиняется время выполнения программы, но производительность увеличивается, так как не надо каждый раз формировать новую команду с новым адресом нового операнда, как при прямой адресации. p.s. мож что соврал или недопонял)
Указатели, интересная тема на самом деле.
Сейчас пока в школе, думаю после 100 урока будет в универе.
После 200 рабочие проекты.
1) Кто-нибудь, напомните, пожалуйста, где встречалось пояснение вот этого:
А то я уже или забыла или пропустила, похоже.
2) В примере, где иллюстрируется сбой программы при обращении к неинициализированному указателю, поясните, пожалуйста, почему так сложно записан параметр для функции foo: void foo(int *&p) . Что это значит? Если мы хотим показать, что её параметр — указатель, то почему нельзя было написать void foo(int *p) ?
1) Урок №55. Неявное преобразование типов данных
2) Мне кажется новичкам правильнее было бы показать так:
Нет, не пропустили. Означает: вывести тип переменной
А вы уверены, Константин?
я дополнил функцию foo простым кодом:
и у меня и segfault пропал, и вывелось 7 при выводе разыменнованного указателя.
Немного не понял по поводу как правильно int* ptr или int *ptr. Проблема в том, что я написал в своем коде второй вариант, поставил точку с запятой, и VS автоматические исправил на первый вариант. Но почему?
Правильны оба варианта, пока Вы не начнёте объявлять сразу несколько указателей. В этом случае только *ptr.
Здравствуйте !
В тексте написано: "Как и обычные переменные, указатели не инициализируются при объявлении."
, но, если я правильно понимаю,
это и есть объявление указателя и его инициализация.
Вы правильно понимаете. В тексте написано "Как и обычные переменные, указатели не инициализируются при объявлении.". Это означает, что если только объявить указатель и не инициализировать его, то там будет что попало, поэтому и надо их сразу инициализировать. При объявлении.
Лол! Попробовал я значит вывести указатель с мусором, в итоге упал visual studio)
А в Xcode мусорные значения всегда забиваются нулями.
На Linux выводятся рандомные числа
Юрий спасибо! С++ начал осваивать недавно и с указателями — ступор. Теперь появился свет к конце туннеля.
Пожалуйста 🙂
Очень классные уроки! Спасибо за труд! Все по делу, ничего лишнего!
Очень странный термин "разыменование". Никак не клеится со смыслом. Это общепринятая терминология или вольный перевод автора?
Как вы определяете общепринятую терминологию? Разве у нас есть орган, который однозначно указывает перевод слов? Слово "разыменование" используется во многих ресурсах о программировании, но ручаться за все источники и что это общепринятая терминология — я не могу.
В этих уроках используется термин "разыменование".
Это не вольный перевод автора. В других книгах этот оператор так же называют.