Химерний спосіб виділення двовимірного масиву?


110

У проекті хтось просунув цю лінію:

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

Який нібито створює двовимірний масив (n + 1) * (n + 1) подвійних.

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

Можливо, я пропускаю щось очевидне, але я вдячний, якби хтось міг мені пояснити вище рядка. Тому що особисто я відчував би себе набагато краще, якби ми використали щось, що насправді розуміємо.


15
Навіть якщо це працює, вистріліть штовхач.
Мартін Джеймс

22
@MartinJames Чому? Як би ви виділили 2D масив у сусідній пам'яті інакше? Напружений 1D масив? Це програмування 1990-х, зараз у нас є VLA.
Лундін

43
Для запису, це єдиний спосіб динамічного розподілу фактичного 2D масиву.
Квентін

15
@Kninnug Ні, це оголосило б масив покажчиків, а не 2D масив. Якщо ви хочете 2D масив, немає жодної причини, чому б вам хотілося виділити масив покажчиків. Це повільно через фрагментацію купи та погану використання кеш-пам'яті, і це небезпечно, тому що ви не можете використовувати її як масив (з memcpy тощо). Плюс код роздутий. Вам знадобиться декілька безкоштовних () дзвінків, і це буде легко викликати витоки пам'яті. Широко поширений такий код може бути, це погано.
Лундін

15
Цю проблему було б зрозуміліше пояснити / відповісти, якби зразок не використовував однакові n+1double (*e)[rows] = malloc(columns * sizeof *e);
величини,

Відповіді:


87

Змінна eє вказівником на масив n + 1елементів типу double.

Використання оператора dereference увімкнює 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 щоб вказувати на перший масив у цьому масиві масивів.
  • Це дозволяє використовувати в eякості e[i][j]доступу до окремих елементів 2D масиву.

Особисто я думаю, що цей стиль читати набагато простіше:

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

12
Хороша відповідь, за винятком того, що я не згоден із запропонованим вами стилем, віддаючи перевагу ptr = malloc(sizeof *ptr * count)стилю.
chux

Гарна відповідь, і мені подобається твій улюблений стиль. Невелике поліпшення може зазначити, що вам потрібно зробити це таким чином, оскільки між рядками може бути прокладка, яку потрібно враховувати. (Принаймні, я вважаю, що це потрібно зробити так.) (Дайте мені знати, чи я помиляюся.)
davidbak

2
@davidbak Це те саме. Синтаксис масиву - це просто самодокументуючий код: він говорить "виділити місце для двовимірного масиву" із самим вихідним кодом.
Лундін

1
@davidbak Примітка. Незначний недолік коментаря malloc(row*col*sizeof(double)) виникає, коли row*col*sizeof()переповнює, але не sizeof()*row*colробить. (наприклад, рядок, кол є int)
chux - Відновіть Моніку

7
@davidbak: sizeof *e * (n+1)простіше в обслуговуванні; якщо ви коли - небудь вирішите змінити базовий тип (від doubleдо long double, наприклад), то вам потрібно всього лише змінити декларацію e; вам не потрібно змінювати sizeofвираз у mallocвиклику (це економить час і захищає вас від зміни його в одному місці, але не в іншому). sizeof *eзавжди надасть вам потрібний розмір.
Джон Боде

39

Ця ідіома природно випадає з розподілу 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 .

Ця ідіома має такі переваги:

  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.