it-roy-ru.com

Причудливый способ размещения двумерного массива?

В проекте кто-то толкнул эту строку:

double (*e)[n+1] = malloc((n+1) * sizeof(*e));

Который предположительно создает двумерный массив (n + 1) * (n + 1) удваивается.

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

Возможно, я упускаю что-то очевидное, но я был бы признателен, если бы кто-нибудь мог объяснить мне вышеизложенную строку. Потому что лично я чувствовал бы себя намного лучше, если бы мы использовали то, что на самом деле понимаем.

110
User1291

Переменная e является указателем на массив элементов n + 1 типа double.

Использование оператора разыменования для e дает вам базовый тип e, который представляет собой "массив n + 1 элементов типа double".

Вызов malloc просто берет базовый тип e (объясненный выше) и получает его размер, умножает его на n + 1 и передает этот размер в функцию malloc. По существу выделение массива n + 1 массивов n + 1 элементов double.

87
Some programmer dude

Это типичный способ динамического размещения 2D-массивов.

  • e - указатель массива на массив типа double [n+1].
  • Поэтому sizeof(*e) дает тип указательного типа, который является размером одного массива double [n+1].
  • Вы выделяете место для n+1 таких массивов.
  • Вы устанавливаете указатель массива e так, чтобы он указывал на первый массив в этом массиве массивов.
  • Это позволяет использовать e в качестве e[i][j] для доступа к отдельным элементам в 2D-массиве.

Лично я думаю, что этот стиль гораздо легче читать:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );
56
Lundin

Эта идиома естественно выпадает из распределения 1D Начнем с выделения одномерного массива произвольного типа T:

T *p = malloc( sizeof *p * N );

Просто, правда? expression*p имеет тип T, поэтому sizeof *p дает тот же результат, что и sizeof (T), поэтому мы выделяем достаточно места для массива N- элемента T. Это верно для любого типа T.

Теперь давайте заменим T типом массива, таким как R [10]. Тогда наше распределение становится

R (*p)[10] = malloc( sizeof *p * N);

Семантика здесь точно такая же как метод размещения 1D; все, что изменилось, это тип p. Вместо T * теперь R (*)[10]. Выражение *p имеет тип T, который имеет тип R [10], поэтому sizeof *p эквивалентен sizeof (T), что эквивалентно sizeof (R [10]). Таким образом, мы выделяем достаточно места для N массивом 10 элемента R.

Мы можем пойти еще дальше, если захотим; предположим, что R сам по себе является типом массива int [5]. Замените это на R и мы получим

int (*p)[10][5] = malloc( sizeof *p * N);

То же самое: sizeof *p - это то же самое, что и sizeof (int [10][5]), и мы выделяем непрерывный кусок памяти, достаточно большой, чтобы вместить N с 10 с 5 массивом int.

Так что это сторона распределения; как насчет стороны доступа?

Помните, что операция индексации []определена в терминах арифметики указателя: a[i] определяется как *(a + i)1, Таким образом, оператор индекса []неявно разыменовывает указатель. Если p является указателем на T, вы можете получить доступ к указанному значению, явно разыменовав с помощью унарного оператора *:

T x = *p;

или с помощью оператора индекса []:

T x = p[0]; // identical to *p

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

T arr[N];
T *p = arr; // expression arr "decays" from type T [N] to T *
...
T x = p[i]; // access the i'th element of arr through pointer p

Теперь давайте снова выполним операцию замещения и заменим T на тип массива R [10]:

R arr[N][10];
R (*p)[10] = arr; // expression arr "decays" from type R [N][10] to R (*)[10]
...
R x = (*p)[i];

Одна сразу очевидная разница; мы явно разыменовываем p перед применением оператора индекса. Мы не хотим вставлять в p, мы хотим подписаться на то, что pуказывает на (в данном случае массивarr[0]). Поскольку унарный * имеет более низкий приоритет, чем оператор нижнего индекса [], мы должны использовать скобки для явной группировки p с *. Но помните сверху, что *p такой же, как p[0], поэтому мы можем заменить его на

R x = (p[0])[i];

или просто

R x = p[0][i];

Таким образом, если p указывает на 2D-массив, мы можем индексировать этот массив через p, например, так:

R x = p[i][j]; // access the i'th element of arr through pointer p;
               // each arr[i] is a 10-element array of R

Приняв это к тому же выводу, что и выше, и заменив R на int [5]:

int arr[N][10][5];
int (*p)[10][5]; // expression arr "decays" from type int [N][5][10] to int (*)[10][5]
...
int x = p[i][j][k];

Это работает точно так же если p указывает на обычный массив или если он указывает на память, выделенную через malloc.

Эта идиома имеет следующие преимущества:

  1. Все просто - всего одна строка кода, в отличие от метода частичного выделения
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
    
  2. Все строки выделенного массива являются * смежными *, что не имеет место с методом поэтапного выделения, описанным выше;
  3. Выделение массива так же просто с помощью одного вызова free. Опять же, это не так с методом поэтапного выделения, когда вам нужно освободить каждый arr[i], прежде чем вы сможете освободить arr.

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


1. Помните, что массивы не являются указателями - вместо этого массив выражения при необходимости преобразуется в выражения указателя.
39
John Bode