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

  Юрий  | 

    | 

  Обновл. 23 мая 2019  | 

 20369

 ǀ   14 

В уроке №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 (123 оценок, среднее: 4,90 из 5)
Загрузка...

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

  1. Аватар Алексей:

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

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

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

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

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

  3. Аватар Алексей:

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

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

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

  4. Аватар Татьяна:

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

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

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

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

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

  5. Аватар Рустам:

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

    1. Аватар somebox:

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

  6. Аватар Владимир:

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

    1. Юрий Юрий:

      Пожалуйста 🙂

  7. Аватар Vado:

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

  8. Аватар Виталий:

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

    1. Юрий Юрий:

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

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

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

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

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

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