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

110

В проекте кто-то выдвинул эту строку:

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

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

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

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

Пользователь1291
источник
15
Даже если это сработает, стреляйте из толкателя.
Мартин Джеймс
22
@MartinJames Почему? Как бы вы иначе разместили 2D-массив в смежной памяти? Искаженный одномерный массив? Это программирование 1990-х, теперь у нас есть VLA.
Lundin
43
Для записи, что это единственный способ распределения фактического 2D массив динамически.
Квентин
15
@Kninnug Нет, это объявляет массив указателей, а не 2D-массив. Если вам нужен 2D-массив, нет причин, по которым вы когда-либо хотели бы выделять массив указателей. Это медленно из-за фрагментации кучи и плохого использования кеш-памяти и небезопасно, потому что вы не можете использовать его как массив (с memcpy и т. Д.). Плюс код раздут. Вам потребуется несколько вызовов free (), что может вызвать утечку памяти. Каким бы распространенным ни был такой код, это просто плохо.
Lundin
15
Эту проблему было бы проще объяснить / ответить, если бы в образце не использовались те же оцененные размеры, n+1а вместо этогоdouble (*e)[rows] = malloc(columns * sizeof *e);
chux - Reinstate Monica

Ответы:

87

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

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

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

Какой-то чувак-программист
источник
3
@MartinJames sizeof(*e)эквивалентен sizeof(double [n + 1]). Умножьте это на, n + 1и вы получите достаточно.
Какой-то чувак-программист
24
@MartinJames: Что с этим не так? Это не так уж больно, это гарантирует, что выделенные строки являются смежными, и вы можете индексировать его, как любой другой 2D-массив. Я часто использую эту идиому в собственном коде.
Джон Боде
3
Это может показаться очевидным, но это работает только для квадратных массивов (одинаковых размеров).
Йенс
18
@Jens: Только в том смысле, что если вы введете n+1оба измерения, результат будет квадратным. Если вы это сделаете double (*e)[cols] = malloc(rows * sizeof(*e));, результат будет иметь любое указанное вами количество строк и столбцов.
user2357112 поддерживает Монику
9
@ user2357112 Теперь я бы предпочел посмотреть. Даже если это означает, что вам нужно добавить int rows = n+1и int cols = n+1. Боже, спаси нас от хитрого кода.
Candied_orange
56

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

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

Лично я считаю, что этот стиль легче читать:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );
Лундин
источник
12
Хороший ответ, за исключением того, что я не согласен с предложенным вами стилем, предпочитая его ptr = malloc(sizeof *ptr * count).
chux
Хороший ответ, и мне нравится ваш предпочтительный стиль. Небольшое улучшение может заключаться в том, чтобы указать, что вам нужно сделать это таким образом, потому что между строками могут быть отступы, которые необходимо учитывать. (По крайней мере, я думаю, что это причина, по которой вам нужно делать это именно так.) (Дайте мне знать, если я ошибаюсь.)
Давидбак,
2
@davidbak Это то же самое. Синтаксис массива - это просто самодокументирующийся код: он говорит «выделите место для 2D-массива» с самим исходным кодом.
Lundin
1
@davidbak Примечание. Незначительный недостаток комментария malloc(row*col*sizeof(double)) возникает при row*col*sizeof()переполнении, но этого не sizeof()*row*colпроисходит. (например, row, col are int)
chux - Reinstate Monica
7
@davidbak: sizeof *e * (n+1)проще поддерживать; если вы когда - нибудь решите изменить базовый тип (от doubleдо long double, к примеру), то вам нужно всего лишь изменить декларацию e; вам не нужно изменять sizeofвыражение в mallocвызове (что экономит время и защищает вас от изменения его в одном месте, но не в другом). sizeof *eвсегда даст вам нужный размер.
Джон Боде
39

Эта идиома естественным образом выпадает из распределения одномерного массива. Начнем с выделения одномерного массива произвольного типа 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);

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

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

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

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

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

Помните, что []операция над индексом определяется в терминах арифметики указателя: 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. Помните, что массивы не являются указателями - вместо этого выражения массива при необходимости преобразуются в выражения указателя.

Джон Боде
источник
4
+1 Мне нравится, как вы представляете эту концепцию: размещение серии элементов возможно для любого типа, даже если эти элементы сами являются массивами.
logo_writer
1
Ваше объяснение действительно хорошее, но обратите внимание, что выделение непрерывной памяти не является преимуществом, пока оно вам действительно не понадобится. Непрерывная память дороже, чем несмежная. Для простых 2D-массивов нет никакой разницы в расположении памяти (за исключением количества строк для выделения и освобождения), поэтому предпочитайте использовать несмежную память.
Олег Локшин