Array Multidimensionali in C++

Concetti Chiave
  • Gli array multidimensionali sono in realtà array di array.
  • Gli array multidimensionali possono essere inizializzati usando liste di inizializzatori annidate.
  • Gli elementi di un array multidimensionale possono essere indicizzati usando un indice separato per ogni dimensione.
  • Le variabili di controllo del ciclo in un range for che itera su un array multidimensionale devono essere riferimenti, tranne per la variabile di controllo del ciclo più interno.

Array Multidimensionali

A rigor di termini, non esistono array multidimensionali in C++. Ciò che viene comunemente chiamato array multidimensionale è in realtà un array di array. Può essere utile tenere presente questo fatto quando si usa quello che sembra essere un array multidimensionale.

Definiamo un array i cui elementi sono array fornendo due dimensioni: la dimensione dell'array stesso e la dimensione dei suoi elementi. Ad esempio:

// array di dimensione 3;
// ogni elemento è un array di int di dimensione 4
int ia[3][4];

// array di dimensione 10;
// ogni elemento è un array di 20 elementi
// i cui elementi sono array di 30 int
int arr[10][20][30] = {0}; // inizializza tutti gli elementi a 0

Come abbiamo visto in precedenza, possiamo comprendere più facilmente queste definizioni leggendole dall'interno verso l'esterno. Iniziamo con il nome che stiamo definendo (ia) e vediamo che ia è un array di dimensione 3. Continuando a guardare a destra, vediamo che anche gli elementi di ia hanno una dimensione. Quindi, gli elementi in ia sono essi stessi array di dimensione 4. Guardando a sinistra, vediamo che il tipo di quegli elementi è int. Quindi, ia è un array di dimensione 3, ciascuno dei cui elementi è un array di quattro int.

Leggiamo la definizione di arr nello stesso modo. Prima vediamo che arr è un array di dimensione 10. Gli elementi di quell'array sono essi stessi array di dimensione 20. Ognuno di quegli array ha 30 elementi che sono di tipo int. Non c'è limite a quanti indici vengono usati. Cioè, possiamo avere un array i cui elementi sono array di elementi che sono array, e così via.

In un array bidimensionale, la prima dimensione è solitamente chiamata riga e la seconda colonna.

Inizializzazione degli Elementi di un Array Multidimensionale

Come per qualsiasi array, possiamo inizializzare gli elementi di un array multidimensionale fornendo una lista di inizializzatori tra parentesi graffe. Gli array multidimensionali possono essere inizializzati specificando valori tra parentesi graffe per ogni riga:

// tre elementi; ogni elemento è un array di dimensione 4
int ia[3][4] = {
    // inizializzatori per la riga indicizzata da 0
    {0, 1, 2, 3},
    // inizializzatori per la riga indicizzata da 1
    {4, 5, 6, 7},
    // inizializzatori per la riga indicizzata da 2
    {8, 9, 10, 11}
};

Le parentesi graffe annidate sono opzionali. La seguente inizializzazione è equivalente, sebbene considerevolmente meno chiara:

// inizializzazione equivalente senza le parentesi graffe
// annidate opzionali per ogni riga
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

Come nel caso degli array a singola dimensione, gli elementi possono essere omessi dalla lista di inizializzatori. Possiamo inizializzare solo il primo elemento di ogni riga come segue:

// inizializza esplicitamente solo l'elemento 0 in ogni riga
int ia[3][4] = {{ 0 }, { 4 }, { 8 }};

Gli elementi rimanenti sono inizializzati per valore nello stesso modo degli array ordinari a singola dimensione. Se le parentesi graffe annidate fossero omesse, i risultati sarebbero molto diversi. Questo codice

// inizializza esplicitamente la riga 0;
// gli elementi rimanenti sono inizializzati per valore
int ix[3][4] = {0, 3, 6, 9};

inizializza gli elementi della prima riga. Gli elementi rimanenti sono inizializzati a 0.

Indicizzazione di un Array Multidimensionale

Come per qualsiasi array, possiamo usare un indice per accedere agli elementi di un array multidimensionale. Per farlo, usiamo un indice separato per ogni dimensione.

Se un'espressione fornisce tanti indici quante sono le dimensioni, otteniamo un elemento con il tipo specificato. Se forniamo meno indici di quante siano le dimensioni, allora il risultato è l'elemento dell'array interno all'indice specificato:

// assegna il primo elemento di arr
// all'ultimo elemento nell'ultima riga di ia
ia[2][3] = arr[0][0][0];

// lega row al secondo array di quattro elementi in ia
int (&row)[4] = ia[1];

Nel primo esempio forniamo indici per tutte le dimensioni per entrambi gli array. Sul lato sinistro, ia[2] restituisce l'ultima riga in ia. Non recupera un elemento da quell'array ma restituisce l'array stesso. Indicizziamo quell'array, recuperando l'elemento [3], che è l'ultimo elemento in quell'array.

Similmente, l'operando destro ha tre dimensioni. Prima recuperiamo l'array all'indice 0 dall'array più esterno. Il risultato di quell'operazione è un array (multidimensionale) di dimensione 20. Prendiamo il primo elemento da quell'array di 20 elementi, producendo un array di dimensione 30. Quindi recuperiamo il primo elemento da quell'array.

Nel secondo esempio, definiamo row come un riferimento a un array di quattro int. Leghiamo quel riferimento alla seconda riga in ia.

Come altro esempio, è comune usare una coppia di cicli for annidati per elaborare gli elementi in un array multidimensionale:

constexpr size_t rowCnt = 3, colCnt = 4;

// 12 elementi non inizializzati
int ia[rowCnt][colCnt];

// Loop per ogni riga
for (size_t i = 0; i != rowCnt; ++i) {
    // Loop per ogni colonna all'interno della riga
    for (size_t j = 0; j != colCnt; ++j) {
         // assegna l'indice posizionale dell'elemento come suo valore
         ia[i][j] = i * colCnt + j;
    }
}

Il for esterno scorre ogni elemento dell'array in ia. Il for interno scorre gli elementi di quegli array interni. In questo caso, impostiamo il valore di ogni elemento come il suo indice nell'array complessivo.

Uso di un Range for con Array Multidimensionali

Con i range for possiamo semplificare il ciclo precedente usando un range for:

size_t cnt = 0;

// per ogni elemento nell'array esterno
for (auto &row : ia)
    // per ogni elemento nell'array interno
    for (auto &col : row) {
        // assegna a questo elemento il valore successivo
        col = cnt;
        // incrementa cnt
        ++cnt;
    }

Questo ciclo assegna agli elementi di ia gli stessi valori del ciclo precedente, ma questa volta lasciamo che il sistema gestisca gli indici per noi. Vogliamo cambiare il valore degli elementi, quindi dichiariamo le nostre variabili di controllo, row e col, come riferimenti. Il primo for itera attraverso gli elementi in ia. Quegli elementi sono array di dimensione 4. Quindi, il tipo di row è un riferimento a un array di quattro int. Il secondo for itera attraverso uno di quegli array di 4 elementi. Quindi, col è int&. Ad ogni iterazione assegniamo il valore di cnt all'elemento successivo in ia e incrementiamo cnt.

Nell'esempio precedente, abbiamo usato riferimenti come nostre variabili di controllo del ciclo perché volevamo cambiare gli elementi nell'array. Tuttavia, c'è una ragione più profonda per usare i riferimenti. Come esempio, considera il seguente ciclo:

// per ogni elemento nell'array esterno
for (const auto &row : ia)
    // per ogni elemento nell'array interno
    for (auto col : row)
        cout << col << endl;

Questo ciclo non scrive negli elementi, eppure definiamo ancora la variabile di controllo del ciclo esterno come un riferimento. Lo facciamo per evitare la normale conversione da array a puntatore. Se avessimo trascurato il riferimento e scritto questi cicli come:

for (auto row : ia)
    for (auto col : row)

il nostro programma non compilerebbe. Come prima, il primo for itera attraverso ia, i cui elementi sono array di dimensione 4. Poiché row non è un riferimento, quando il compilatore inizializza row convertirà ogni elemento dell'array (come qualsiasi altro oggetto di tipo array) in un puntatore al primo elemento di quell'array. Di conseguenza, in questo ciclo il tipo di row è int*. Il for interno è illegale. Nonostante le nostre intenzioni, quel ciclo tenta di iterare su un int*.

Consiglio

Array Multidimensionali e Range for

Per usare un array multidimensionale in un range for, la variabile di controllo del ciclo per tutti gli array tranne quello più interno deve essere un riferimento.

Puntatori e Array Multidimensionali

Come per qualsiasi array, quando usiamo il nome di un array multidimensionale, viene automaticamente convertito in un puntatore al primo elemento nell'array.

Quando definisci un puntatore a un array multidimensionale, ricorda che un array multidimensionale è realmente un array di array.

Poiché un array multidimensionale è realmente un array di array, il tipo di puntatore a cui l'array si converte è un puntatore al primo array interno:

// array di dimensione 3;
// ogni elemento è un array di int di dimensione 4
int ia[3][4];

// p punta a un array di quattro int
int (*p)[4] = ia;

// p ora punta all'ultimo elemento in ia
p = &ia[2];

Applicando la relazione tra puntatori e array, iniziamo notando che (*p) dice che p è un puntatore. Guardando a destra, vediamo che l'oggetto a cui p punta ha una dimensione di 4, e guardando a sinistra che il tipo elemento è int. Quindi, p è un puntatore a un array di quattro int.

Le parentesi in questa dichiarazione sono essenziali:

// array di puntatori a int
int *ip[4];

// puntatore a un array di quattro int
int (*ip)[4];

Con l'avvento dello standard C++11, possiamo spesso evitare di dover scrivere il tipo di un puntatore a un array usando auto o decltype:

// stampa il valore di ogni elemento in ia,
// con ogni array interno su una propria riga
// p punta a un array di quattro int
for (auto p = ia; p != ia + 3; ++p) {
    // q punta al primo elemento di un array
    // di quattro int; cioè, q punta a un int
    for (auto q = *p; q != *p + 4; ++q)
        cout << *q << ' ';
    cout << endl;
}

Il for esterno inizia inizializzando p per puntare al primo array in ia. Quel ciclo continua fino a quando non abbiamo elaborato tutte e tre le righe in ia. L'incremento, ++p, ha l'effetto di spostare p per puntare alla riga successiva (cioè, l'elemento successivo) in ia.

Il for interno stampa i valori degli array interni. Inizia facendo puntare q al primo elemento nell'array a cui p punta. Il risultato di *p è un array di quattro int. Come al solito, quando usiamo un array, viene convertito automaticamente in un puntatore al suo primo elemento. Il for interno viene eseguito fino a quando non abbiamo elaborato ogni elemento nell'array interno. Per ottenere un puntatore appena oltre la fine dell'array interno, dereferenziamo nuovamente p per ottenere un puntatore al primo elemento di quell'array. Quindi aggiungiamo 4 a quel puntatore per elaborare i quattro elementi in ogni array interno.

Naturalmente, possiamo scrivere questo ciclo ancora più facilmente usando le funzioni begin ed end della libreria:

// p punta al primo array in ia
for (auto p = begin(ia); p != end(ia); ++p) {
    // q punta al primo elemento in un array interno
    for (auto q = begin(*p); q != end(*p); ++q)
        // stampa il valore int a cui q punta
        cout << *q << ' ';
    cout << endl;
}

Qui lasciamo che la libreria determini il puntatore finale, e usiamo auto per evitare di dover scrivere il tipo restituito da begin. Nel ciclo esterno, quel tipo è un puntatore a un array di quattro int. Nel ciclo interno, quel tipo è un puntatore a int.

Gli Alias di Tipo Semplificano i Puntatori ad Array Multidimensionali

Un alias di tipo può rendere più facile leggere, scrivere e comprendere i puntatori ad array multidimensionali. Ad esempio:

// dichiarazione di alias di tipo
using int_array = int[4];

// dichiarazione typedef equivalente
typedef int int_array[4];

// stampa il valore di ogni elemento in ia,
// con ogni array interno su una propria riga
for (int_array *p = ia; p != ia + 3; ++p) {
    for (int *q = *p; q != *p + 4; ++q)
        cout << *q << ' ';
    cout << endl;
}

Qui iniziamo definendo int_array come un nome per il tipo "array di quattro int". Usiamo quel nome di tipo per definire la nostra variabile di controllo del ciclo nel for esterno.

Esercizi

  • Scrivi tre versioni diverse di un programma per stampare gli elementi di ia. Una versione dovrebbe usare un range for per gestire l'iterazione, le altre due dovrebbero usare un for ordinario in un caso usando gli indici e nell'altro usando i puntatori. In tutti e tre i programmi scrivi tutti i tipi direttamente. Cioè, non usare un alias di tipo, auto o decltype per semplificare il codice.

    Soluzione:

    Prima versione (range for):

    #include <iostream>
    using namespace std;
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (int (&row)[4] : ia) {
            for (int col : row) {
                cout << col << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    

    Seconda versione (for con indici):

    #include <iostream>
    using namespace std;
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (size_t i = 0; i < 3; ++i) {
            for (size_t j = 0; j < 4; ++j) {
                cout << ia[i][j] << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    

    Terza versione (for con puntatori):

    #include <iostream>
    using namespace std;
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (int (*p)[4] = ia; p != ia + 3; ++p) {
            for (int *q = *p; q != *p + 4; ++q) {
                cout << *q << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    
  • Riscrivi i programmi dell'esercizio precedente usando un alias di tipo per il tipo delle variabili di controllo del ciclo.

    Soluzione:

    Prima versione (range for con alias di tipo):

    #include <iostream>
    using namespace std;
    
    using int_array = int[4];
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (int_array &row : ia) {
            for (int col : row) {
                cout << col << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    

    Seconda versione (for con indici e alias di tipo):

    #include <iostream>
    using namespace std;
    
    using int_array = int[4];
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (size_t i = 0; i < 3; ++i) {
            for (size_t j = 0; j < 4; ++j) {
                cout << ia[i][j] << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    

    Terza versione (for con puntatori e alias di tipo):

    #include <iostream>
    using namespace std;
    
    using int_array = int[4];
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (int_array *p = ia; p != ia + 3; ++p) {
            for (int *q = *p; q != *p + 4; ++q) {
                cout << *q << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    
  • Riscrivi i programmi ancora una volta, questa volta usando auto.

    Soluzione:

    Prima versione (range for con auto):

    #include <iostream>
    using namespace std;
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (auto &row : ia) {
            for (auto col : row) {
                cout << col << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    

    Seconda versione (for con indici e auto):

    #include <iostream>
    using namespace std;
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (size_t i = 0; i < 3; ++i) {
            for (size_t j = 0; j < 4; ++j) {
                cout << ia[i][j] << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }
    

    Terza versione (for con puntatori e auto):

    #include <iostream>
    using namespace std;
    
    int main() {
        int ia[3][4] = {
            {0, 1, 2, 3},
            {4, 5, 6, 7},
            {8, 9, 10, 11}
        };
    
        for (auto p = ia; p != ia + 3; ++p) {
            for (auto q = *p; q != *p + 4; ++q) {
                cout << *q << ' ';
            }
            cout << endl;
        }
    
        return 0;
    }