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


110

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

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

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

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

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


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-массивов нет никакой разницы в расположении памяти (за исключением количества строк для выделения и освобождения), поэтому предпочитайте использовать несмежную память.
— Олег Локшин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.