Урок №93. Указатели на указатели

  Юрий  | 

  |

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

 103982

 ǀ   39 

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

Указатели на указатели

Обычный указатель типа int объявляется с использованием одной звёздочки:

Указатель на указатель типа int объявляется с использованием двух звёздочек:

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

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

7
7

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

Это связано с тем, что оператор адреса (&) требует l-value, но &value — это r-value. Однако указателю на указатель можно задать значение null:

Массивы указателей


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

Это тот же обычный динамически выделенный массив, за исключением того, что элементами являются указатели на тип int, а не значения типа int.

Двумерные динамически выделенные массивы

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

Динамическое выделение двумерного массива немного отличается. У вас может возникнуть соблазн написать что-то вроде следующего:

Здесь вы получите ошибку. Есть два возможных решения. Если правый индекс является константой типа compile-time, то вы можете сделать следующее:

Скобки здесь потребуются для соблюдения приоритета. В C++11 хорошей идеей будет использовать ключевое слово auto для автоматического определения типа данных:

К сожалению, это относительно простое решение не работает, если правый индекс не является константой типа compile-time. В таком случае всё немного усложняется. Сначала мы выделяем массив указателей (как в примере, приведенном выше), а затем перебираем каждый элемент массива указателей и выделяем динамический массив для каждого элемента этого массива. Итого, наш динамический двумерный массив — это динамический одномерный массив динамических одномерных массивов!

Доступ к элементам массива выполняется как обычно:

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

В примере, приведенном выше, array[0] — это массив длиной 1, а array[1] — массив длиной 2 и т.д.

Для освобождения памяти динамически выделенного двумерного массива (который создавался с помощью этого способа) также потребуется цикл:

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

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

Простая математика используется для конвертации индексов строки и столбца прямоугольного двумерного массива в один индекс одномерного массива:

Указатель на указатель на указатель на указатель и т.д.


Также можно объявить указатель на указатель на указатель:

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

Или сделать еще большую вложенность, если захотите. Однако на практике такие указатели редко используются.

Заключение

Рекомендуется применять указатели на указатели только в самых крайних случаях, так как они сложны в использовании и потенциально опасны. Достаточно легко разыменовать нулевой или «висячий» указатель в ситуациях с использованием обычных указателей, вдвое легче это сделать в ситуациях с указателем на указатель, поскольку для получения исходного значения потребуется выполнить двойное разыменование!


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

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

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

  1. Виталий:

    Решил разобраться. Начал экспериментировать с разными вариантами. Например, я хочу написать функцию для выделения памяти для двумерного массива. Как это сделать? Как указать тип возврата «указатель на массив»? Попробовал вот так:

    Не работает. Попробовал вот так:

    Так работает. Вопрос: а как сделать то же самое без использования using?

    1. Николай:

      Может использовать указатель типа void:

  2. Lukin:

    Скобки здесь потребуются для соблюдения приоритета.

    Не понял этого момента объясните пожалуйста

    1. Илья:

      по ссылке на приоритеты (урок 38) можно увидеть, что оператор массива имеет более высокий приоритет, чем разыменование.

      Таким образом, int *arr[7] объявит нам массив из 7 указателей, поскольку сначала отработает оператор массива.

      Если же мы заключаем разыменование в скобки, то мы сначала проводим разыменование — а это и есть объявление динамического массива (урок 86), а затем сработает оператор массива — то есть мы получим массив динамических массивов, двумерный массив.

    2. Виталий:

      Внесу свою лепту. Вот эта строчка есть указатель на массив. И он отличается от указателя на указатель. По сути мы просто создаём указатель на массив. Указателю array мы присваиваем адрес начала массива.

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

  3. Edman:

    в случае объявления двумерного массива таким способом, как освобождать выделенную динамическую память? Просто:

    или также к и в случае с int **array= new int *[15] ?

    ?

  4. Андрей:

    Объясните пожалуйста второе предложение из Заключения этого урока.
    "Достаточно легко разыменовать нулевой или «висячий» указатель в ситуациях с использованием обычных указателей, вдвое легче это сделать в ситуациях с указателем на указатель, поскольку для получения исходного значения потребуется выполнить двойное разыменование!"
    Почему "вдвое легче", если надо выполнить двойное разыменование?

    1. Борис:

      Суть вопроса здесь в возможности допущения ошибки. Разыменование "висячего" или нулевого указателя это ошибка. Так вот когда у тебя указатель на указатель, то допустить такую ошибку в 2 раза легче. Я понял это так.

      1. Андрей:

        Ясно, спасибо

  5. Дмитрий Н:

    Почему между auto и array не стоит * ? array это же указатель.

    1. SmilingRe:

      Здесь Я приведу своё сугубо личное мнение:
      Стоит определиться со звездочкой. Я всегда её ставлю не к переменной, а к указателю. Всё потому, что звездочка на мой взгляд есть неотъемлемая часть имени типа данных. Т.е. когда мы пишем int мы указываем, что в переменной хранится целочисленная переменная, а когда указывает int* мы указывает, что переменная хранит указатель. Это разные типы данных и поэтому на мой взгляд звездочка должна стоять именно возле типа данных, а не переменной.
      Отсюда становится понятным почему между auto и array нет звездочки. auto автоматически определяет используемый тип данных, по r-value. Справа мы видим оператор new, т.е. компилятор понимает, что динамические выделяется память. На такую память всегда указывают указатели, т.е. тип данных должен быть int*. Дальше он видит, что массив многомерный и он автоматически определяет его "мерность" (здесь двумерный), и также автоматически выделяет двумерный указатель, т.е. int**. Поэтому при использовании auto слева всегда будет одно и то же… Но будет меняться в зависимости от правой части:
      auto array = new int[15][7][6] — будет соответственно int*** и т.д.

    2. Lodi:

      Потому, что тип auto определяет компилятор(auto может превратиться в int * || char ** …)

  6. Владимир:

    Я так и не понял, почему размер массивов фиксированного размера может задавать только переменными времени компиляции, а динамического — переменными времени выполнения.
    Я бы понял, если бы компилятор проверял проверял размер фиксированного размера и высчитывал что он укладывается в размер стека, но он ведь этого не делает.

    1. Михаил:

      Я точно не знаю. Есть версия:
      Место под локальные переменные выделяется в стеке при входе в функцию. Делается это одной инструкцией — декрементом регистра rsp на размер ВСЕХ локальных переменных (ну и плюс еще константный размер на служебные нужды). Одна инструкция с одним регистром на выделение стека под все локальные переменные. Мегабыстро. От этого вызов функции сравнительно не дорогая операция. А вот если заложить возможность динамического определения размера, то так дешево вызывать функцию уже не получится.
      Ну то есть, моя версия — как все в плюсах это продиктовано стремлением к высокой производительности. То есть: делаем самым производительным способом путь даже это будет неудобно. А удобным непроизводительным способом делать лучше вообще не будем, ведь это все таки C++.

  7. Any:

    Когда я запускаю этот код:

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

    Ошибка C1090 Произошел сбой при вызове API PDB, код ошибки "3"

    1. Михаил:

      Тут проблема не в коде, а в чем-то другом.
      У меня это компилится clang-ом и работает без проблем.

  8. Юрий:

    Почему в функции освобождении памяти компилятор ругается, что "Использование неинициализированной памяти " на delete[]parray[i]?

    1. Spardoks:

      Здесь в delete[] parray лишнее. За цикл нужно вынести, чтоб освободить память только 1 раз

  9. Данил:

    В параметрах функции мне необходимо сделать константный указатель на указатель(т.к. функция только выводит двумерный массив), но тогда при ее вызове компилятор ругается на то, что невозможно преобразовать аргумент из "double **" в "const double **. Почему так происходит?
    Вот сама функция:

    Вот ее вызов:

    1. Andrey:

  10. Nicolai:

    Паровозик ptrptrptr….

    1. Сергей:

      После прочтения этой темы понимаю, что мозг возмущенно отказывается переваривать эти указатели на указатели массивов и тут твой комментарий, поржал)

  11. alex_1988:

    Накопившиеся вопросы:

    1) Почему при передачи в функции двухмерных массивов типы указателей разнятся?
    пример:

    2) Как правильно почистить переменную через delete чтобы не произошла утечка памяти в этом коде:

    1. alex_1988:

      Покумекал сам и думаю пришел к правильным выводам:

      1) Фиксированный массив передается в функцию таким образом потому что является настоящим двухмерным массивом, где все данные хранятся в памяти поочередно и не содержут указатель на другие подмассивы как это сделано в динамическом.

      2) Чтобы удалить динамический указатель на указатель без утечки памяти нужно сделать так:

  12. Артём:

    Объясните,пожалуйста,конкретнее,как происходит конвертации индексов строки и столбца прямоугольного двумерного массива в один индекс одномерного массива.

    1. Никита:

      Ну как одну строку на 100 символов, разбить на 5 строк по 20 символов понятно?

      1. Артём:

        Понятно. Я прошу объяснить на данном примере. Непонятно, что означают числа 9,4,5,что такое array[9,4] и как получается формула в return.

        1. Nikita:

          Главное я тогда когда был на этом материале, тоже не понял, долго вникал, расписывал и разрисовывал себе этот массив , и всёравно понадобилось достаточно много времени чтобы наконец-то разобраться что к чему.
          Тут приходит уведомление на почту о твоём сообщении тут, я захожу, и опять не понимаю… И смешно и грустно одновременно))

          Но поднапрягшись таки вспомнил)
          Смотри. 9 и 4 это координаты элемента массива который нам нужен для присваивания 3ки (в случае выше)
          Число 5 — это количество столбцов в нашем массиве (не адрес элемента) Нужно нам количество столбцов чтобы умножить их на количество строк. Таким образом мы как бы восстанавливаем 2мерный массив из одномерного.
          Для этого мы умножаем 9 (строка на самом деле 10я, но отсчет с 0)
          на реальное количество столбцов (тут разумеется отсчёт с 1)) на 5.

          Получаем "индекс" 45 (9*5).
          Теперь осталось подобраться к самому элементу.
          Добавляем 4. Тем самым мы перепрыгиваем с координат(8,4 — адрес 45ого элемента для ПК(для нас, по человечески это 9я строка 5 столбец) и оказываемся уже на 10строке (для ПК 9я, ведь с 0 считаем)
          В общем по человечески это 50й элемент в массиве. Для ПК индекс 49.
          Знаю, мудрено очень обьяснил, но надеюсь хоть часть моей инфы, будет в помощь.

        2. Nikita:

          Дополнение:
          То что в скобках, удаляем , написал неверно:
          "Для этого мы умножаем 9 (строка на самом деле 10я, но отсчет с 0)"

          строка на этот момент 9я для человека и 8я для ПК. После того как добавим 4ку, строка +1

  13. Артём:

    Не совсем понял этот момент. Воссоздал массив на листочке, элемент [9,4] как будто является 50-ым, хотя по формуле он 49-ый. И что значит параметр "numberOfColumnsInArray"? Помогите, пожалуйста. Заранее спасибо!

    1. Артём:

      Всё. Разобрался(я просто затупил).

      1. Никита:

        Объясни пожалуйста. А то я остался на стадии тупняка. Массив у нас какой изначально ? 15*7?

  14. Владимир:

    Оставлю это здесь. Может, кому интересно будет на это посмотреть:

    1. Алексей:

      Пятимерный массив… Пятимерное пространство… Хмм, это было неплохо)

      1. rutrud:

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

  15. Виталий:

    зачем здесь второй индекс [7]??

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

      Здесь (*array) — это одномерный динамический массив, а [7] — это одномерный фиксированный массив. [7] нужен для создания двумерного динамического массива.

    2. Михаил:

      Потому что вот это

      на русском читается примерно так:
      1) выдели мне указатель на 7 интов.
      2) а также выдели массив из 15 раз по 7 интов и присвой адрес первой семерки интов в мой указатель (с первого шага).

      Итак, почему нужен указатель на 7 интов для этого.

      Похоже разобрался с этим, может полезно будет кому.
      Вот рассмотрим такой код, когда имеет двумерный массив представлен одномерным массивом указателей на одномерные массивы:

      Тут указатель на начало массива один. А весь массив (15*7 элементов) распологается в памяти спошняком — первый идет элемент [0][0], потом [0][1] и так до [0][7], потом идет [1][0] и т. д.
      И когда мы делаем так

      компилятор генерит для этого код, который арифметикой указателей вычисляет адрес нужного элемента двумерного массива:
      arr+y*размер_внутреннего_измерения
      *sizeof(int)+ x*sizeof(int)
      А откуда компилятору взять этот размер_внутреннего_измерения, поэтому нам приходится его указывать.

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

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