Як форматуються багатовимірні масиви в пам'яті?


185

У C я знаю, що можу динамічно виділити двовимірний масив на купі, використовуючи наступний код:

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

Зрозуміло, що це фактично створює одновимірний масив покажчиків на купу окремих одновимірних масивів цілих чисел, і "Система" може зрозуміти, що я маю на увазі, коли я запитую:

someNumbers[4][2];

Але коли я статично оголошую 2D масив, як у наступному рядку ...:

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

... чи створюється подібна структура на стеці, чи це зовсім інша форма? (тобто це 1D масив покажчиків? Якщо ні, що це таке, і як з'ясовуються посилання на нього?)

Крім того, коли я сказав: "Система", що насправді відповідає за з'ясування цього? Ядро? Або компілятор С розбирається під час компіляції?


8
Я дав би більше +1, якби міг.
Роб Лаклан

1
Попередження : У цьому коді немає двовимірного масиву!
занадто чесний для цього сайту

@toohonestforthissite Дійсно. Для розширення цього питання: циклічне підключення та виклик malloc()не призводять до N-мірного масиву. . Це призводить до того, що масиви покажчиків [до масивів покажчиків [...]] повністю розділяють одновимірні масиви. Див. Розділ Правильне розподіл багатовимірних масивів, щоб побачити, як розподілити ІСТИНА N-мірний масив.
Ендрю Генле

Відповіді:


145

Статичний двовимірний масив схожий на масив масивів - він просто викладається безперервно в пам'яті. Масиви - це не те саме, що вказівники, але оскільки ви часто можете користуватися ними в значній мірі взаємозамінно, це може часом заплутатися. Однак компілятор відстежує належним чином, завдяки чому все чітко вирівнюється. Ви повинні бути обережними зі статичними 2D-масивами, як ви згадуєте, оскільки якщо ви спробуєте передати його функції, яка приймає int **параметр, то трапляться погані речі. Ось короткий приклад:

int array1[3][2] = {{0, 1}, {2, 3}, {4, 5}};

У пам'яті виглядає так:

0 1 2 3 4 5

точно так само, як:

int array2[6] = { 0, 1, 2, 3, 4, 5 };

Але якщо ви спробуєте перейти array1до цієї функції:

void function1(int **a);

ви отримаєте попередження (і додаток не зможе отримати доступ до масиву правильно):

warning: passing argument 1 of function1 from incompatible pointer type

Тому що 2D масив - це не те саме, що int **. Автоматичне розпадання масиву в покажчик йде, так би мовити, лише "на рівні глибини". Вам потрібно оголосити функцію як:

void function2(int a[][2]);

або

void function2(int a[3][2]);

Щоб все було щасливим.

Ця ж концепція поширюється на n -вимірні масиви. Користуючись цим смішним бізнесом у вашому додатку, як правило, лише ускладнює розуміння. Тож будьте обережні там.


Дякую за пояснення. Отже, "void function2 (int a [] [2]);" прийме як статично, так і динамічно оголошені 2D? І я здогадуюсь, що це все-таки хороша практика / важливо пропустити також довжину масиву, якщо перший вимір залишиться як []?
Кріс Купер

1
@Chris Я не вважаю так - вам буде важко перетворити C swizzle на стек або глобально виділений масив у купу покажчиків.
Карл Норум

6
@JasonK. - немає. Масиви не є покажчиками. Масиви "розпадаються" на покажчики в деяких контекстах, але вони абсолютно не однакові.
Карл Норум

1
Щоб було зрозуміло: Так, Кріс, "все-таки хороша практика пропускати довжину масиву" як окремий параметр, інакше використовувати std :: array або std :: vector (який C ++ не старий C). Я думаю, що ми погоджуємося @CarlNorum як концептуально для нових користувачів, так і практично, щоб процитувати Андерса Касеорга на Quora: «Перший крок до вивчення C - це розуміння того, що вказівники та масиви - це одне і те ж. Другий крок - розуміння того, що вказівники та масиви різні ».
Джейсон К.

2
@JasonK. "Перший крок до вивчення C - це розуміння того, що вказівники та масиви - це одне і те ж". - Ця цитата настільки неправильна і оманлива! Це дійсно найважливіший крок, щоб зрозуміти, що вони не однакові, але те, що масиви перетворюються в покажчик на перший елемент для більшості операторів! sizeof(int[100]) != sizeof(int *)(якщо ви не знайдете платформу з 100 * sizeof(int)байтами / int, але це інша річ.
занадто чесний для цього сайту

85

Відповідь заснована на ідеї, що C насправді не має 2D-масивів - у ній є масиви-масиви. Коли ви заявляєте про це:

int someNumbers[4][2];

Ви просите someNumbersстати масивом з 4-х елементів, де кожен елемент цього масиву має тип int [2](який сам є масивом у 2 intс).

Інша частина головоломки полягає в тому, що масиви завжди безперервно розкладаються в пам'яті. Якщо ви попросите:

sometype_t array[4];

то це завжди буде виглядати так:

| sometype_t | sometype_t | sometype_t | sometype_t |

(4 sometype_tоб’єкти, розкладені поруч, без проміжків між ними). Отже, у вашому someNumbersмасиві масивів це буде виглядати приблизно так:

| int [2]    | int [2]    | int [2]    | int [2]    |

І кожен int [2]елемент є самим масивом, який виглядає приблизно так:

| int        | int        |

Таким чином, ви отримуєте це:

| int | int  | int | int  | int | int  | int | int  |

1
дивлячись на остаточний макет, змушують мене думати, що доступ до int a [] [] можна отримати як int * ... правда?
Narcisse Doudieu Siewe

2
@ user3238855: типи не сумісні, але якщо ви отримаєте вказівник на перший intв масиві масивів (наприклад, шляхом оцінки a[0]або &a[0][0]), то так, ви можете компенсувати це для послідовного доступу до кожного int).
caf

28
unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

в пам'яті дорівнює:

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

5

У відповідь на ваше також: Обидва, хоча компілятор робить більшу частину важкого підйому.

У разі статично розподілених масивів компілятором буде "Система". Він збереже пам'ять, як і для будь-якої змінної стека.

У випадку з malloc'd масивом "Система" буде реалізатором malloc (ядро зазвичай). Весь компілятор виділить базовий покажчик.

Компілятор завжди буде обробляти тип як те, про що вони оголошені, за винятком прикладу, який Карл дав, де він може з'ясувати взаємозамінне використання. Ось чому, якщо ви переходите в функцію [] [], ви повинні вважати, що це статично розподілена площина, де ** вважається вказівником на покажчик.


@ Jon L. Я б не сказав, що malloc реалізований ядром, а libc на вершині примітивів ядра (наприклад, brk)
Мануель Сельва

@ManuelSelva: Де і як mallocреалізується не визначено стандартом і залишається до реалізації, відповідно. середовище. Для автономних середовищ це необов’язково, як і всі частини стандартної бібліотеки, що вимагають функцій зв’язку (саме до цього насправді призводять вимоги, а не буквально те, що у стандарті). Для деяких сучасних середовищ, що розміщуються, він дійсно покладається на функції ядра, або повний матеріал, або (наприклад, Linux), як ви писали, використовуючи обидва, stdlib та ядра-примітиви. Для однопроцесорних систем невіртуальної пам’яті це може бути лише stdlib.
занадто чесний для цього сайту

2

Припустимо, у нас є a1і a2та инициализируется , як показано нижче (c99):

int a1[2][2] = {{142,143}, {144,145}};
int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };

a1являє собою однорідний 2D масив з простим безперервним розташуванням в пам'яті і вираження (int*)a1оцінюється вказівником на його перший елемент:

a1 --> 142 143 144 145

a2ініціалізується з гетерогенного 2D масиву і є вказівником на значення типу int*, тобто вираз dereference *a2оцінюється у значення типу int*, макет пам'яті не повинен бути безперервним:

a2 --> p1 p2
       ...
p1 --> 242 243
       ...
p2 --> 244 245

Незважаючи на абсолютно інший розміщення пам’яті та семантику доступу, граматика на мові С для виразів доступу до масиву виглядає абсолютно однаково як для однорідного, так і для гетерогенного 2D масиву:

  • Вираз a1[1][0]отримає значення 144з a1масиву
  • Вираз a2[1][0]отримає значення 244з a2масиву

Компілятор знає, що вираз доступу для операційного a1типу працює int[2][2], коли вираз доступу для операційного a2типу працює int**. Створений код складання буде слідувати однорідній або неоднорідній семантиці доступу.

Код зазвичай виходить з ладу під час виконання, коли масив тип набирається int[N][M]на тип і потім отримує доступ до типу int**, наприклад:

((int**)a1)[1][0]   //crash on dereference of a value of type 'int'

1

Для доступу до певного двовимірного масиву розгляньте карту пам'яті для декларації масиву, як показано в коді нижче:

    0  1
a[0]0  1
a[1]2  3

Для доступу до кожного елемента достатньо просто передати той масив, який вас цікавить як параметри функції. Потім використовуйте зміщення для стовпця для доступу до кожного елемента окремо.

int a[2][2] ={{0,1},{2,3}};

void f1(int *ptr);

void f1(int *ptr)
{
    int a=0;
    int b=0;
    a=ptr[0];
    b=ptr[1];
    printf("%d\n",a);
    printf("%d\n",b);
}

int main()
{
   f1(a[0]);
   f1(a[1]);
    return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.