Menu

Matrici in C: array 2D, array multidimensionali e memoria

Come dichiarare, inizializzare e scorrere gli array 2D in C, cosa significa davvero in memoria la disposizione per righe, perché passare un array 2D a una funzione richiede il numero di colonne e un esempio svolto con le matrici.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un array multidimensionale è un array i cui elementi sono a loro volta array. int grid[3][4]; non è un tipo speciale di griglia: sono tre elementi, ognuno dei quali è un array di quattro int, memorizzati uno dopo l'altro. Una volta capito questo, il resto viene da sé: la disposizione in memoria, l'aritmetica degli indici e la regola, altrimenti incomprensibile, su come passarli alle funzioni.

Due dimensioni coprono quasi ogni uso pratico, cioè griglie, tabelle, matrici, scacchiere e immagini, quindi questa pagina lavora con quelle.

Dichiarazione e inizializzazione

int grid[3][4];        // 3 righe, 4 colonne: 12 int

Il primo numero indica quante righe, il secondo quante colonne. Gli inizializzatori si possono scrivere in forma piatta o con graffe interne; conviene usare le graffe perché mostrano la forma.

La forma int e[][3] è importante: puoi lasciare vuoto il numero di righe e farlo decidere all'inizializzatore, ma il numero di colonne non è mai facoltativo. La prossima sezione spiega perché.

Disposizione per righe

Il C memorizza un array 2D per righe (row-major): tutta la riga 0, poi tutta la riga 1 e così via, in un unico blocco di memoria senza interruzioni. Dietro le quinte non c'è nessun array di puntatori alle righe.

int grid[3][4] = {{ 1, 2, 3, 4},
                  { 5, 6, 7, 8},
                  { 9,10,11,12}};

come lo immagini                 come sta davvero in memoria
+----+----+----+----+
|  1 |  2 |  3 |  4 |   riga 0   +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+            | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
|  5 |  6 |  7 |  8 |   riga 1   +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+             \_ riga 0 __/\_ riga 1 __/\_ riga 2 _/
|  9 | 10 | 11 | 12 |   riga 2
+----+----+----+----+            grid[i][j] sta all'indice i*4 + j

Quella formula, i * columns + j, è tutto il meccanismo, ed è il motivo per cui il compilatore deve conoscere il numero di colonne per accedere a qualsiasi elemento. Il numero di righe non entra mai nel calcolo.

Puoi vedere la disposizione direttamente stampando gli indirizzi:

Gli indirizzi salgono di sizeof(int) senza buchi, anche nel punto in cui una riga finisce e inizia la successiva. Il ciclo appiattito lo dimostra: flat[k] percorre tutti e dodici gli elementi come un'unica sequenza.

Questa disposizione ha anche una conseguenza sulle prestazioni che è bene conoscere: ciclare prima sulle righe e poi sulle colonne tocca la memoria in ordine, cosa che la cache della CPU apprezza. Invertire l'annidamento dei cicli, così che il ciclo interno scenda lungo una colonna, salta ogni volta di un'intera riga e su un array grande può essere parecchie volte più lento.

Cicli annidati

Due dimensioni vogliono due cicli for: quello esterno sceglie la riga, quello interno scorre le colonne di quella riga.

Dai ai contatori nomi che dicano cosa rappresentano (i/row per le righe, j/col per le colonne) e mantieni l'ordine coerente: grid[row][col] ovunque. Metà dei bug con gli array 2D sono una coppia di indici invertita.

Nemmeno le dimensioni definite con #define sono decorative: ora i limiti dei cicli e la dichiarazione non possono più divergere quando cambi la forma.

Passare un array 2D a una funzione

Ecco la regola che fa inciampare tutti: il parametro della funzione deve dichiarare il numero di colonne.

Il motivo è il decadimento. Passare grid lo converte in un puntatore al suo primo elemento, e i suoi elementi sono righe, quindi il tipo è int (*)[4]: puntatore a un array di 4 int. Perché grid[i][j] abbia senso, il compilatore deve sapere quanto è lunga una riga, e quello è il 4. Il numero di righe è davvero assente dal tipo, ed è per questo che viaggia come argomento separato.

Nota che int (*grid)[COLS] e int grid[][COLS] sono lo stesso parametro scritto in due modi; le parentesi sono obbligatorie, perché int *grid[COLS] sarebbe invece un array di puntatori. Questa distinzione è trattata in puntatori e array.

Se il numero di colonne è noto solo a runtime, i parametri a dimensione variabile di C99 ti permettono di passarlo prima:

void print_any(int rows, int cols, int grid[rows][cols]);

rows e cols devono essere dichiarati prima del parametro array che li usa. Dove non è disponibile, l'alternativa comune è un array 1D piatto con l'aritmetica degli indici fatta a mano:

data[i * cols + j] è esattamente ciò che il compilatore scrive per te nel caso a dimensione fissa. Farlo a mano costa una riga e funziona per qualsiasi forma decisa a runtime.

Un esempio con le matrici

Il prodotto tra matrici mette insieme tutta la pagina: tre cicli annidati su una memoria disposta per righe.

Due dettagli da copiare. Il ciclo interno su k abbina a[i][k] a b[k][j]: un indice percorre una riga, l'altro una colonna. E la trasposizione fa partire il suo ciclo interno da j = i + 1: partire da 0 scambierebbe ogni coppia due volte e lascerebbe la matrice invariata.

Tre dimensioni e oltre

Lo schema si estende, e con lui la regola sui parametri delle funzioni: ogni dimensione tranne la prima deve essere dichiarata.

In pratica, con tre dimensioni gli array a dimensione fissa iniziano a diventare scomodi, e la maggior parte del codice passa a un blocco piatto con indici calcolati o a un array di struct che dà un nome al significato di ogni asse.

Errori comuni

  • Scrivere grid[i, j]. L'operatore virgola valuta i, lo scarta e indicizza con j. Compila. È sbagliato. Usa grid[i][j].
  • Invertire gli indici. grid[col][row] legge un elemento vero dal posto sbagliato, quindi non c'è nessun errore che lo segnali. Mantieni l'ordine [row][col] ovunque.
  • Omettere il numero di colonne in un parametro. void f(int grid[][]) non compila, ed è il compilatore che ti salva.
  • Uscire dai limiti. Come con qualsiasi array, non c'è nessun controllo dei limiti. grid[0][5] su una griglia [3][4] legge in silenzio grid[1][1], perché la disposizione è contigua e all'aritmetica non importa.

Domande frequenti

Come si dichiara un array 2D in C?

Indica due dimensioni tra parentesi quadre: int grid[3][4]; dichiara 3 righe da 4 colonne, 12 int in tutto. Leggilo come "un array di 3 elementi, ognuno dei quali è un array di 4 int", che è letteralmente il modo in cui il C lo memorizza.

Come viene memorizzato un array 2D in C?

Per righe: tutti gli elementi della riga 0, poi tutti quelli della riga 1 e così via, in un unico blocco contiguo. grid[i][j] si trova a i * columns + j elementi dall'inizio, ed è per questo che il numero di colonne è il dato di cui il compilatore ha bisogno.

Come si passa un array 2D a una funzione in C?

Il parametro deve dichiarare il numero di colonne: void print(int grid[][4], int rows) o, in modo equivalente, void print(int (*grid)[4], int rows). Il numero di righe si può omettere perché l'array decade a un puntatore a una riga, ma senza il numero di colonne il compilatore non può calcolare dove inizia una riga.

Si può inizializzare un array 2D tutto a zero?

Sì: int grid[3][4] = {0}; azzera ogni elemento, perché ogni elemento che non elenchi viene inizializzato a zero. int grid[3][4] = {{1, 2}}; imposta i primi due valori della riga 0 e lascia gli altri dieci a zero.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA