10.21 – Указатели на указатели и динамические многомерные массивы

Добавлено 9 июня 2021 в 21:53
Глава 10 – Массивы, строки, указатели и ссылки  (содержание)

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

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

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

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

int *ptr; // указатель на int, одна звездочка

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

int **ptrptr; // указатель на указатель на int, две звездочки

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

int value = 5;
 
int *ptr = &value;
// Косвенное обращение через указатель на int для получения значения int
std::cout << *ptr; 
 
int **ptrptr = &ptr;
// первое косвенное обращение для получения указателя на int,
// второе косвенное обращение для получения значения int
std::cout << **ptrptr;

Показанный выше код напечатает:

5
5

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

int value = 5;
int **ptrptr = &&value; // недопустимо

Это связано с тем, что оператор адреса (operator&) требует l-значение (l-value), но &value является r-значением (r-value).

Однако указатель на указатель может иметь значение null:

int **ptrptr = nullptr; // до C++11 используйте вместо этого 0

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

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

int **array = new int*[10]; // распределяем массив из 10 указателей int

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

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

Другое распространенное использование указателей на указатели – облегчение динамического размещения многомерных массивов (для обзора многомерных массивов смотрите урок «10.5 – Многомерные массивы»).

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

int array[10][5];

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

int **array = new int[10][5]; // не сработает!

Но это не сработает.

Здесь есть два возможных решения. Если крайнее правое измерение массива является константой времени компиляции, вы можете сделать так:

int (*array)[5] = new int[10][5];

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

auto array = new int[10][5]; // намного проще!

К сожалению, это относительно простое решение не работает, если какое-либо не крайнее левое измерение массива не является константой времени компиляции. В этом случае мы должны немного усложнить ситуацию. Сначала мы размещаем массив указателей (как показано выше). А затем мы перебираем этот массив указателей и для каждого элемента массива размещаем еще один динамический массив. Наш динамический двумерный массив – это динамический одномерный массив динамических одномерных массивов!

int **array = new int*[10]; // размещаем массив из 10 указателей int - это наши строки
for (int count = 0; count < 10; ++count)
    array[count] = new int[5]; // это наши столбцы

Затем мы можем получить доступ к нашему массиву, как обычно:

array[9][4] = 3; // Это то же самое, что (array[9])[4] = 3;

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

int **array = new int*[10]; // размещаем массив из 10 указателей int - это наши строки
for (int count = 0; count < 10; ++count)
    array[count] = new int[count+1]; // это наши столбцы

Обратите внимание, что в приведенном выше примере array[0] – это массив длиной 1, array[1] - это массив длиной 2, и т.д.

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

for (int count = 0; count < 10; ++count)
    delete[] array[count];
delete[] array; // это нужно сделать в последнюю очередь

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

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

// Вместо этого:
int **array = new int*[10]; // размещаем массив из 10 указателей int - это наши строки
for (int count = 0; count < 10; ++count)
    array[count] = new int[5]; // это наши столбцы
 
// Сделаем так
int *array = new int[50]; // массив 10x5, сведенный в единый массив

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

int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
     return (row * numberOfColumnsInArray) + col;
}
 
// устанавливаем значение array[9,4] равным 3, используя наш плоский массив
array[getSingleIndex(9, 4, 5)] = 3;

Передача указателя по адресу

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

Однако если мы хотим, чтобы функция могла изменять то, на что указывает аргумент-указатель, обычно лучше использовать ссылку на указатель. Поэтому мы не будем здесь больше об этом говорить.

Подробнее о передаче по адресу и передаче по ссылке мы поговорим в следующей главе.

Указатель на указатель на указатель на…

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

int ***ptrx3;

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

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

int ****ptrx4;

Или больше, если хотите.

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

Заключение

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

Теги

arrayC++ / CppLearnCppДинамическое распределение памятиДля начинающихМногомерный массивОбучениеПрограммированиеУказатель / Pointer (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.