Come vengono formattati gli array multidimensionali in memoria?


185

In C, so di poter allocare dinamicamente un array bidimensionale sull'heap usando il seguente codice:

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

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

Chiaramente, questo in realtà crea una matrice unidimensionale di puntatori a un gruppo di matrici unidimensionali separate di numeri interi e "Il sistema" può capire cosa intendo quando chiedo:

someNumbers[4][2];

Ma quando dichiaro staticamente un array 2D, come nella seguente riga ...:

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

... una struttura simile viene creata nello stack o è completamente di un'altra forma? (ovvero è un array 1D di puntatori? In caso contrario, che cos'è e come vengono individuati i riferimenti ad esso?)

Inoltre, quando ho detto "Il sistema", che cosa è effettivamente responsabile di capirlo? Il nocciolo? O il compilatore C lo ordina durante la compilazione?


8
Darei più di +1 se potessi.
Rob Lachlan,

1
Avvertenza : non esiste un array 2D in questo codice!
troppo onesto per questo sito il

@toohonestforthissite Effettivamente. Per espanderlo: il looping e la chiamata malloc()non determinano un array N-dimensionale. . Si traduce in matrici di puntatori [in matrici di puntatori [...]] per separare completamente matrici unidimensionali . Vedere Allocazione corretta di array multidimensionali per vedere come allocare un array N-dimensionale VERO .
Andrew Henle,

Risposte:


145

Un array statico bidimensionale assomiglia a un array di array: è semplicemente disposto contiguo nella memoria. Le matrici non sono la stessa cosa dei puntatori, ma poiché spesso puoi usarle in modo abbastanza intercambiabile, a volte può creare confusione. Il compilatore tiene traccia correttamente, tuttavia, il che rende tutto allineato bene. Devi stare attento con le matrici 2D statiche come dici, poiché se provi a passarne una a una funzione che accetta un int **parametro, accadranno cose brutte. Ecco un breve esempio:

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

In memoria si presenta così:

0 1 2 3 4 5

esattamente uguale a:

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

Ma se provi a passare array1a questa funzione:

void function1(int **a);

riceverai un avviso (e l'app non riuscirà ad accedere correttamente all'array):

warning: passing argument 1 of function1 from incompatible pointer type

Perché un array 2D non è lo stesso di int **. Il decadimento automatico di un array in un puntatore va solo "a un livello di profondità" per così dire. È necessario dichiarare la funzione come:

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

o

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

Per rendere tutto felice.

Questo stesso concetto si estende alle matrici n- dimensionali. Sfruttare questo tipo di attività divertente nella tua applicazione generalmente rende solo più difficile da capire. Quindi state attenti là fuori.


Grazie per la spiegazione. Quindi "void function2 (int a [] [2]);" accetterà sia i 2D dichiarati staticamente che dinamicamente? E suppongo che sia ancora buona pratica / essenziale passare la lunghezza dell'array anche se la prima dimensione viene lasciata come []?
Chris Cooper,

1
@Chris Non credo - farai fatica a trasformare C in un array allocato a livello globale o stack in un gruppo di puntatori.
Carl Norum,

6
@JasonK. - no. Le matrici non sono puntatori. Array "Decay" in puntatori in alcuni contesti, ma sono assolutamente non la stessa.
Carl Norum,

1
Per essere chiari: Sì, Chris "è ancora buona norma passare la lunghezza dell'array" come parametro separato, altrimenti usa std :: array o std :: vector (che è C ++ non vecchio C). Penso che siamo d'accordo su @CarlNorum sia concettualmente per i nuovi utenti sia praticamente, per citare Anders Kaseorg su Quora: “Il primo passo per imparare C è capire che puntatori e array sono la stessa cosa. Il secondo passo è capire che i puntatori e le matrici sono diversi. "
Jason K.,

2
@JasonK. "Il primo passo per imparare C è capire che puntatori e array sono la stessa cosa." - Questa citazione è molto sbagliata e fuorviante! È davvero il passo più importante per capire che non sono gli stessi, ma che gli array vengono convertiti in un puntatore al primo elemento per la maggior parte degli operatori! sizeof(int[100]) != sizeof(int *)(a meno che non trovi una piattaforma con 100 * sizeof(int)byte / int, ma questa è una cosa diversa.
Troppo onesto per questo sito il

85

La risposta si basa sull'idea che C in realtà non ha matrici 2D - ha array-di-array. Quando lo dichiari:

int someNumbers[4][2];

Stai chiedendo someNumbersdi essere un array di 4 elementi, in cui ogni elemento di quell'array è di tipo int [2](che è esso stesso un array di 2 ints).

L'altra parte del puzzle è che gli array sono sempre disposti contigui nella memoria. Se chiedi:

sometype_t array[4];

allora sarà sempre così:

| sometype_t | sometype_t | sometype_t | sometype_t |

(4 sometype_t oggetti disposti uno accanto all'altro, senza spazi in mezzo). Quindi nel tuo someNumbersarray di array, sarà simile a questo:

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

E ciascuno int [2] elemento è esso stesso un array, che assomiglia a questo:

| int        | int        |

Quindi, nel complesso, ottieni questo:

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

1
guardando il layout finale mi fa pensare che int a [] [] sia accessibile come int * ... giusto?
Narcisse Doudieu Siewe,

2
@ user3238855: i tipi non sono compatibili, ma se si ottiene un puntatore al primo intnell'array di array (ad es. valutando a[0]o &a[0][0]), quindi sì, è possibile compensarlo per accedere in sequenza a tutti int).
caf

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

in memoria è uguale a:

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

5

In risposta anche al tuo: entrambi, sebbene il compilatore stia facendo gran parte del lavoro pesante.

Nel caso di array allocati staticamente, "The System" sarà il compilatore. Riserverà la memoria come per qualsiasi variabile di stack.

Nel caso dell'array malloc'd, "The System" sarà l'implementatore di malloc (il kernel solitamente). Tutto il compilatore che assegnerà è il puntatore di base.

Il compilatore gestirà sempre il tipo come quello che sono dichiarati essere, tranne nell'esempio che Carl ha fornito dove può capire l'uso intercambiabile. Questo è il motivo per cui se si passa un [] [] a una funzione, si deve presumere che sia un flat allocato staticamente, dove ** si presume che sia puntatore a puntatore.


@Jon L. Non direi che malloc è implementato dal kernel, ma dalla libc in cima alle primitive del kernel (come brk)
Manuel Selva,

@ManuelSelva: dove e come mallocviene implementato non è specificato dallo standard e lasciato all'implementazione, resp. ambiente. Per gli ambienti indipendenti è facoltativo come tutte le parti della libreria standard che richiedono funzioni di collegamento (questo è ciò che i requisiti effettivamente comportano, non letteralmente ciò che afferma lo standard). Per alcuni moderni ambienti ospitati, si basa davvero sulle funzioni del kernel, o cose complete, o (ad esempio Linux) come hai scritto usando sia, stdlib che kernel-primitives. Per i sistemi a processo singolo con memoria non virtuale, può essere solo stdlib.
troppo onesto per questo sito il

2

Supponiamo, abbiamo a1e a2definito e inizializzato come qui di seguito (C99):

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

a1è un array 2D omogeneo con un semplice layout continuo in memoria e l'espressione (int*)a1viene valutata come un puntatore al suo primo elemento:

a1 --> 142 143 144 145

a2è inizializzato da un array 2D eterogeneo ed è un puntatore a un valore di tipo int*, ovvero l'espressione di dereference viene *a2valutata in un valore di tipo int*, il layout di memoria non deve essere continuo:

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

Nonostante la disposizione della memoria e la semantica dell'accesso totalmente diverse, la grammatica del linguaggio C per le espressioni di accesso all'array è esattamente la stessa per l'array 2D sia omogeneo che eterogeneo:

  • espressione a1[1][0]recupererà valore 144di a1matrice
  • espressione a2[1][0]recupererà valore 244di a2matrice

Il compilatore sa che l'espressione di accesso per a1opera sul tipo int[2][2], quando l'espressione di accesso per a2opera sul tipo int**. Il codice assembly generato seguirà la semantica di accesso omogenea o eterogenea.

Il codice di solito si arresta in modo anomalo in fase di runtime quando l'array di tipo int[N][M]viene sottoposto a cast di tipo e quindi acceduto come tipo int**, ad esempio:

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

1

Per accedere a un particolare array 2D, prendere in considerazione la mappa di memoria per una dichiarazione di array come mostrato nel codice seguente:

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

Per accedere a ciascun elemento, è sufficiente passare semplicemente l'array a cui si è interessati come parametri della funzione. Quindi utilizzare offset per colonna per accedere a ciascun elemento singolarmente.

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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.