Урок №80. Указатели

  Юрий  | 

  |

  Обновл. 4 Сен 2021  | 

 187207

 ǀ   25 

На уроке №10 мы узнали, что переменная — это название кусочка памяти, который содержит значение.

Оператор адреса &

При выполнении инициализации переменной, ей автоматически присваивается свободный адрес памяти, и, любое значение, которое мы присваиваем переменной, сохраняется по этому адресу в памяти. Например:

При выполнении этого стейтмента процессором, выделяется часть оперативной памяти. В качестве примера предположим, что переменной b присваивается ячейка памяти под номером 150. Всякий раз, когда программа встречает переменную b в выражении или в стейтменте, она понимает, что для того, чтобы получить значение — ей нужно заглянуть в ячейку памяти под номером 150.

Хорошая новость — нам не нужно беспокоиться о том, какие конкретно адреса памяти выделены для определенных переменных. Мы просто ссылаемся на переменную через присвоенный ей идентификатор, а компилятор конвертирует это имя в соответствующий адрес памяти. Однако этот подход имеет некоторые ограничения, которые мы обсудим на этом и следующих уроках.

Оператор адреса & позволяет узнать, какой адрес памяти присвоен определенной переменной. Всё довольно просто:

Результат на моем компьютере:

7
0046FCF0

Примечание: Хотя оператор адреса выглядит так же, как оператор побитового И, отличить их можно по тому, что оператор адреса является унарным оператором, а оператор побитового И — бинарным оператором.

Оператор разыменования *


Оператор разыменования * позволяет получить значение по указанному адресу:

Результат на моем компьютере:

7
0046FCF0
7

Примечание: Хотя оператор разыменования выглядит так же, как и оператор умножения, отличить их можно по тому, что оператор разыменования — унарный, а оператор умножения — бинарный.

Указатели

Теперь, когда мы уже знаем об операторах адреса и разыменования, мы можем поговорить об указателях.

Указатель — это переменная, значением которой является адрес ячейки памяти. Указатели объявляются точно так же, как и обычные переменные, только со звёздочкой между типом данных и идентификатором:

Синтаксически язык C++ принимает объявление указателя, когда звёздочка находится рядом с типом данных, с идентификатором или даже посередине. Обратите внимание, эта звёздочка не является оператором разыменования. Это всего лишь часть синтаксиса объявления указателя.

Однако, при объявлении нескольких указателей, звёздочка должна находиться возле каждого идентификатора. Это легко забыть, если вы привыкли указывать звёздочку возле типа данных, а не возле имени переменной. Например:

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

Присваивание значений указателю


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

Приведенное выше можно проиллюстрировать следующим образом:

Вот почему указатели имеют такое имя: ptr содержит адрес значения переменной value, и, можно сказать, ptr указывает на это значение.

Еще очень часто можно увидеть следующее:

Результат на моем компьютере:

003AFCD4
003AFCD4

Тип указателя должен соответствовать типу переменной, на которую он указывает:

Следующее не является допустимым:

Это связано с тем, что указатели могут содержать только адреса, а целочисленный литерал 7 не имеет адреса памяти. Если вы все же сделаете это, то компилятор сообщит вам, что он не может преобразовать целочисленное значение в целочисленный указатель.

Язык C++ также не позволит вам напрямую присваивать адреса памяти указателю:

Оператор адреса возвращает указатель

Стоит отметить, что оператор адреса & не возвращает адрес своего операнда в качестве литерала. Вместо этого он возвращает указатель, содержащий адрес операнда, тип которого получен из аргумента (например, адрес переменной типа int передается как адрес указателя на значение типа int):

Результат выполнения программы:

int *

Разыменование указателей


Как только у нас есть указатель, указывающий на что-либо, мы можем его разыменовать, чтобы получить значение, на которое он указывает. Разыменованный указатель — это содержимое ячейки памяти, на которую он указывает:

Результат:

0034FD90
5
0034FD90
5

Вот почему указатели должны иметь тип данных. Без типа указатель не знал бы, как интерпретировать содержимое, на которое он указывает (при разыменовании). Также, поэтому и должны совпадать тип указателя с типом переменной. Если они не совпадают, то указатель при разыменовании может неправильно интерпретировать биты (например, вместо типа double использовать тип int).

Одному указателю можно присваивать разные значения:

Когда адрес значения переменной присвоен указателю, то выполняется следующее:

   ptr — это то же самое, что и &value;

   *ptr обрабатывается так же, как и value.

Поскольку *ptr обрабатывается так же, как и value, то мы можем присваивать ему значения так, как если бы это была обычная переменная. Например:

Разыменование некорректных указателей

Указатели в языке C++ по своей природе являются небезопасными, а их неправильное использование — один из лучших способов получить сбой программы.

При разыменовании указателя, программа пытается перейти в ячейку памяти, которая хранится в указателе и извлечь содержимое этой ячейки. По соображениям безопасности современные операционные системы (ОС) запускают программы в песочнице для предотвращения их неправильного взаимодействия с другими программами и для защиты стабильности самой операционной системы. Если программа попытается получить доступ к ячейке памяти, не выделенной для нее операционной системой, то ОС сразу завершит выполнение этой программы.

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

Размер указателей

Размер указателя зависит от архитектуры, на которой скомпилирован исполняемый файл: 32-битный исполняемый файл использует 32-битные адреса памяти. Следовательно, указатель на 32-битном устройстве занимает 32 бита (4 байта). С 64-битным исполняемым файлом указатель будет занимать 64 бита (8 байт). И это вне зависимости от того, на что указывает указатель:

Как вы можете видеть, размер указателя всегда один и тот же. Это связано с тем, что указатель — это всего лишь адрес памяти, а количество бит, необходимое для доступа к адресу памяти на определенном устройстве, — всегда постоянное.

В чём польза указателей?

Сейчас вы можете подумать, что указатели являются непрактичными и вообще ненужными. Зачем использовать указатель, если мы можем использовать исходную переменную?

Однако, оказывается, указатели полезны в следующих случаях:

   Случай №1: Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву.

   Случай №2: Они являются единственным способом динамического выделения памяти в C++. Это, безусловно, самый распространенный вариант использования указателей.

   Случай №3: Они могут использоваться для передачи большого количества данных в функцию без копирования этих данных.

   Случай №4: Они могут использоваться для передачи одной функции в качестве параметра другой функции.

   Случай №5: Они используются для достижения полиморфизма при работе с наследованием.

   Случай №6: Они могут использоваться для представления одной структуры/класса в другой структуре/классе, формируя, таким образом, целые цепочки.

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

Заключение

Указатели — это переменные, которые содержат адреса памяти. Их можно разыменовать с помощью оператора разыменования * для извлечения значений, хранимых по адресу памяти. Разыменование указателя, значением которого является мусор, приведет к сбою в вашей программе.

Совет: При объявлении указателя указывайте звёздочку возле имени переменной.

Тест

Задание №1

Какие значения мы получим в результате выполнения следующей программы (предположим, что это 32-битное устройство, и тип short занимает 2 байта):

Ответ №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

Что не так со следующим фрагментом кода:

Ответ №2

Последняя строка не скомпилируется. Рассмотрим эту программу детально.

В первой строке находится стандартное определение переменной вместе с инициализируемым значением. Здесь ничего особенного.

Во второй строке мы определяем новый указатель с именем ptr и присваиваем ему адрес переменной value. Помним, что в этом контексте звёздочка является частью синтаксиса объявления указателя, а не оператором разыменования. Так что и в этой строке всё нормально.

В третьей строке звёздочка уже является оператором разыменования, и используется для вытаскивания значения, на которое указывает указатель. Таким образом, эта строка говорит: «Вытаскиваем значение, на которое указывает ptr (целочисленное значение), и переписываем его на адрес этого же значения». А это уже какая-то чепуха — вы не можете присвоить адрес целочисленному значению!

Третья строка должна быть:

В вышеприведенной строке мы корректно присваиваем указателю адрес значения переменной.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (633 оценок, среднее: 4,91 из 5)
Загрузка...

Комментариев: 25

  1. Игорь:

    Подскажите пожалуйста, какой тип переменной в параметре функции foo, int *&p, что то не встречал такого определения?
    Спасибо

    1. Fray:

      Это ссылка. Тема, связанная с ссылками будет на следующих уроках, поэтому ее синтаксис может быть вам незнаком.
      Для создания ссылки используется символ амперсанда &.
      Ссылку можно создавать на множество различных объектов, переменные, указатели и так далее.
      Ссылка — это разыменованный указатель.
      В нашем случае я полагаю, что это указатель, который содержит адрес другого указателя, а затем неявно разыменовывается.

      Эта тема будет на последующих уроках. Там будут все эти нюансы.
      Например, создаем переменную и ссылку на нее:

      ref — будет работать как псевдоним для переменной x: любое использование ref будет равносильно использованию переменной x, которая была привязана к ссылке.
      Например:

      x будет равен 5.
      То есть ref — это указатель, содержащий адрес ячейки x, который затем разыменовывается.
      Аналог ссылке можно сделать так:

      *ptr — это как ссылка, только здесь она явно разыменовывается с помощью оператора разыменования *.

  2. Fray:

    "Размер указателя зависит от архитектуры, на которой скомпилирован исполняемый файл:…"

    Странно: архитектура моего устройства(ноутбука) x64 разрядная, а размер указателя выводится в 32 бита или 4 байта. Значит эта информация не актуальна?

    1. Fray:

      Прошу прощения за флуд. Вроде бы разобрался в чем причина: в конфигурации сборки компилятора.

  3. Евгений:

    Спасибо, информация хорошо разжевана. Впервые разобрался с указателями.
    Для любителей поскрипеть мозгами и залезть глубже:

    По сути, нам не нужна переменная, в которой будет храниться значение, мы лишь храним адрес на это значение.
    Но в этом случае переменная не удалится автоматически, а останется в памяти приложения.

  4. Malkin:

    Чуть добавлю, хотя автор и так всё это знает. Вся эта фигня — указатели, ссылки — так или иначе связана с косвенной адресацией. Зачем она нужна? Затем, чтобы увеличить производительность. Так как при косвенной адресации, у нас не меняется сама команда: [код операции + указатель на адрес]. А указатель — например, FSR, как в PIC контроллерах. Адрес то FSR — 04h не меняется, а меняем мы лишь его содержимое, загоняя туда адреса операндов из портов МК. В итоге, удлиняется время выполнения программы, но производительность увеличивается, так как не надо каждый раз формировать новую команду с новым адресом нового операнда, как при прямой адресации. p.s. мож что соврал или недопонял)

  5. Алексей:

    Указатели, интересная тема на самом деле.
    Сейчас пока в школе, думаю после 100 урока будет в универе.
    После 200 рабочие проекты.

  6. Анастасия:

    1) Кто-нибудь, напомните, пожалуйста, где встречалось пояснение вот этого:

    А то я уже или забыла или пропустила, похоже.

    2) В примере, где иллюстрируется сбой программы при обращении к неинициализированному указателю, поясните, пожалуйста, почему так сложно записан параметр для функции foo: void foo(int *&p) . Что это значит? Если мы хотим показать, что её параметр — указатель, то почему нельзя было написать void foo(int *p) ?

    1. Alex_1988:

      1) Урок №55. Неявное преобразование типов данных
      2) Мне кажется новичкам правильнее было бы показать так:

    2. Анатолий:

      Нет, не пропустили. Означает: вывести тип переменной

    3. Константин:

      1. Илья:

        А вы уверены, Константин?

        я дополнил функцию foo простым кодом:

        и у меня и segfault пропал, и вывелось 7 при выводе разыменнованного указателя.

  7. Алексей:

    Немного не понял по поводу как правильно int* ptr или int *ptr. Проблема в том, что я написал в своем коде второй вариант, поставил точку с запятой, и VS автоматические исправил на первый вариант. Но почему?

    1. Анастасия:

      Правильны оба варианта, пока Вы не начнёте объявлять сразу несколько указателей. В этом случае только *ptr.

  8. Татьяна:

    Здравствуйте !
    В тексте написано: "Как и обычные переменные, указатели не инициализируются при объявлении."

    , но, если я правильно понимаю,

    это и есть объявление указателя и его инициализация.

    1. Анастасия:

      Вы правильно понимаете. В тексте написано "Как и обычные переменные, указатели не инициализируются при объявлении.". Это означает, что если только объявить указатель и не инициализировать его, то там будет что попало, поэтому и надо их сразу инициализировать. При объявлении.

  9. Рустам:

    Лол! Попробовал я значит вывести указатель с мусором, в итоге упал visual studio)

    1. somebox:

      А в Xcode мусорные значения всегда забиваются нулями.

    2. Jaroshevskii:

      На Linux выводятся рандомные числа

  10. Владимир:

    Юрий спасибо! С++ начал осваивать недавно и с указателями — ступор. Теперь появился свет к конце туннеля.

    1. Фото аватара Юрий:

      Пожалуйста 🙂

  11. Vado:

    Очень классные уроки! Спасибо за труд! Все по делу, ничего лишнего!

  12. Виталий:

    Очень странный термин "разыменование". Никак не клеится со смыслом. Это общепринятая терминология или вольный перевод автора?

    1. Фото аватара Юрий:

      Как вы определяете общепринятую терминологию? Разве у нас есть орган, который однозначно указывает перевод слов? Слово "разыменование" используется во многих ресурсах о программировании, но ручаться за все источники и что это общепринятая терминология — я не могу.

      В этих уроках используется термин "разыменование".

    2. Анастасия:

      Это не вольный перевод автора. В других книгах этот оператор так же называют.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *