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

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

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

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

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

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

Ответ 1

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

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

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

Ответ 2

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

  • 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]) );

Ответ 3

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

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

Простой, не так ли? Выражение *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.

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

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

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


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