Array come Argomenti di Funzioni in Linguaggio C

Una funzione in linguaggio C può accettare dei parametri in ingresso. Questi parametri possono essere di diversi tipi, come interi, float, char, ecc.

Finora abbiamo studiato il caso di tipi semplici che vengono passati per copia. In questa lezione, invece, approfondiamo il caso in cui i parametri sono degli array.

Quando si passa un array come argomento di una funzione ci sono una varietà di aspetti da considerare. In particolare, ci sono delle differenze rispetto al passaggio di tipi semplici.

Vediamo quali sono queste differenze, analizzando prima il caso degli array monodimensionali e poi quello degli array multidimensionali.

Array Monodimensionali come Argomenti di una Funzione

In linguaggio C è possibile definire funzioni che hanno come parametri degli array ed è possibile passare degli array come argomenti.

Quando il parametro di una funzione è un'array monodimensionale, la lunghezza dell'array può essere omessa.

Ad esempio, possiamo scrivere la seguente definizione di funzione:

int funzione(int a[]) {
    /* codice */
}

In questo caso, possiamo passare alla funzione funzione un qualunque array di qualunque lunghezza del tipo corrispondente che nell'esempio è int.

Ad esempio, possiamo invocare la funzione in questo modo:

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

funzione(array);

A questo punto, però, sorge un problema. Come fa la funzione a sapere quanti elementi ci sono nell'array che le è stato passato?

Purtroppo in linguaggio C non esiste un modo semplice per determinare la lunghezza di un array passato come argomento ad una funzione.

L'operatore sizeof nemmeno ci può aiutare in questo caso. Esso può essere adoperato per calcolare la dimensione di una variabile array, ma non può essere usato per calcolare la dimensione di un argomento array:

/* Funziona */
int array[5] = {1, 2, 3, 4, 5};

int dimensione = sizeof(array) / sizeof(array[0]);
/* NON FUNZIONA !!! */
int funzione(int a[]) {
    int dimensione = sizeof(a) / sizeof(a[0]);
}

Per capire il perché dobbiamo aspettare di aver studiato i puntatori e la loro relazione con gli array. Lo vedremo in futuro.

Quindi, in questo caso, come possiamo risolvere il problema?

L'unico modo è passare anche la dimensione dell'array come argomento:

int funzione(int a[], int dimensione) {
    /* codice */
}

Vediamo un esempio. Proviamo ad implementare una funzione somma che prende in ingresso un array di interi, la sua dimensione e restituisce la somma di tutti gli elementi dell'array:

int somma(int a[], int dimensione) {
    int somma = 0;

    for (int i = 0; i < dimensione; i++) {
        somma += a[i];
    }

    return somma;
}

Questa funzione può essere invocata in questo modo:

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

int risultato = somma(array, 5);

Una cosa importante da notare è che l'argomento array non deve essere seguito dalle parentesi quadre:

int risultato = somma(array, 5); // OK
int risultato = somma(array[], 5); // ERRORE

Abbiamo detto che quando definiamo una funzione che accetta come parametro un array, la lunghezza dell'array può essere omessa. Cosa accade, invece, se la specifichiamo? In realtà nulla.

Il compilatore, infatti, ignora la dimensione specificata:

int funzione(int a[5]) {
    /* codice */
}

In questo caso, possiamo passare alla funzione funzione un array di qualsiasi dimensione. Il compilatore, infatti, ignorerà la dimensione specificata e accetterà qualsiasi array del tipo corrispondente.

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

funzione(array); // OK

Perché, allora, specificare la dimensione dell'array come parametro? L'unico scopo è quello di rendere più chiaro il codice, in modo che chi legge il codice sappia che l'array passato come argomento deve avere una dimensione specifica. In altre parole, specificare la dimensione di un parametro array ha senso solo a fini documentativi.

Ricapitolando:

Definizione

Array Monodimensionali come Argomenti di una Funzione

In C è possibile usare un array monodimensionale come parametro di una funzione. La sintassi è la seguente:

tipo funzione(tipo array[], ...) {
    /* codice */
}

La dimensione dell'array non è necessaria nè tantomeno viene controllata dal compilatore.

Quando si invoca la funzione, l'array passato come argomento non deve essere seguito dalle parentesi quadre.

tipo array[dimensione] = {valori};

funzione(array, ...);

La dimensione dell'array non può essere calcolata dalla funzione. Se necessario, la dimensione dell'array deve essere passata come argomento.

tipo funzione(tipo array[], int dimensione) {
    /* codice */
}

Parametro dimensione di un Array

Abbiamo visto che l'unico modo per far conoscere ad una funzione la dimensione di un array passato come argomento è quello di passare anche la dimensione dello stesso.

A questo punto ci domandiamo: come fa la funzione in questione a capire che la dimensione passata come argomento è corretta?

La risposta è molto semplice: non può.

Non esiste, purtroppo, un modo in C per controllare che la dimensione passata come argomento sia effettivamente corretta. Se si passa una dimensione errata, la funzione non potrà fare nulla per impedirlo.

Possono verificarsi, quindi, due casi in cui la dimensione passata come argomento non è corretta:

  1. La dimensione passata è minore della dimensione effettiva dell'array.
  2. La dimensione passata è maggiore della dimensione effettiva dell'array.

Nel primo caso, la funzione andrebbe a lavorare su un numero di elementi inferiore a quello effettivo. Questo potrebbe non essere un problema in certi casi e, anzi, potrebbe essere sfruttato a nostro vantaggio.

Ad esempio, ritornando all'esempio della funzione somma, possiamo sommare soltanto i primi n elementi dell'array in questo modo:

int a[] = {1, 2, 3, 4, 5};
int n = 3;

int risultato = somma(a, n);

In questo caso, la funzione somma sommerà soltanto i primi 3 elementi dell'array a, ignorando gli ultimi due. Anzi, la funzione somma in questo caso non saprà nemmeno che esistono.

Nel secondo caso, invece, la funzione andrebbe a lavorare su un numero di elementi maggiore di quello effettivo. Questo potrebbe essere un problema, in quanto la funzione potrebbe accedere a zone di memoria non assegnate all'array. Potrebbe verificarsi un crash del programma o comunque potrebbe essere prodotto un risultato errato. In tal caso il comportamento non sarà predicibile.

Nota

Attenzione al parametro dimensione di un Array

Bisogna prestare estrema cautela quando si passa la dimensione di un array come argomento ad una funzione. Non esiste un modo in C per controllare che la dimensione passata sia corretta. Se si passa una dimensione errata, il comportamento del programma non sarà predicibile.

Ciò può diventare particolarmente catastrofico quando la dimensione passata è maggiore della dimensione effettiva dell'array.

Passaggio per Riferimento

Nelle lezioni precedenti abbiamo visto che, di default, gli argomenti di una funzione vengono passati per valore. Questo significa che un argomento passato ad una funzione viene copiato e la funzione lavora su una copia dell'argomento originale.

Ciò non accade per gli array. Essi, infatti, vengono passati per riferimento.

Questo vuol dire che quando si passa un array ad una funzione, la funzione lavora direttamente sull'array originale e non su una copia dello stesso. Pertanto, qualunque modifica apportata all'array all'interno della funzione sarà visibile anche all'esterno della funzione. Questo dettaglio è di fondamentale importanza.

Prendiamo un esempio. Vogliamo realizzare una funzione che incrementa di 1 tutti gli elementi di un array di interi:

void incrementa(int a[], int dimensione) {
    for (int i = 0; i < dimensione; i++) {
        a[i]++;
    }
}

Se proviamo ad invocare la funzione in questo modo:

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

incrementa(array, 5);

Dopo l'invocazione della funzione, l'array array sarà modificato in questo modo:

array = {2, 3, 4, 5, 6};

Il fatto che una funzione possa modificare gli elementi di un array sembra in contraddizione con l'affermazione che tutti gli argomenti vengano passati per copia. In realtà, non c'è alcuna contraddizione. In effetti una copia avviene quando si passa un array ad una funzione. La copia, però, non è l'array in sé, ma il riferimento all'array. Per comprendere questo, però, dobbiamo aspettare di studiare i puntatori e la loro relazione con gli array.

Definizione

Gli array vengono passati per Riferimento ad una funzione

Ogniqualvolta un array viene passato come argomento ad una funzione, la funzione lavora direttamente sull'array originale e non su una copia dello stesso. Qualunque modifica apportata all'array all'interno della funzione sarà visibile anche all'esterno della funzione.

Questa caratteristica prende il nome di passaggio per riferimento.

Nella pratica è possibile adoperare il passaggio per riferimento anche per altri tipi di dato, come i tipi interi, i tipi float, i tipi char, ecc. Nelle prossime lezioni vedremo come fare adoperando i puntatori.

Array Multidimensionali come Argomenti di una Funzione

Nella sezione precedente abbiamo visto come usare array monodimensionali come argomento di una funzione.

Ci domandiamo, adesso, come possiamo fare per passare array multidimensionali come argomenti.

In tal caso la situazione si complica leggermente. Abbiamo visto che la dimensione di un array monodimensionale viene ignorata dal compilatore quando usato come parametro di una funzione. Per gli array multidimensionali, invece, siamo obbligati a specificare sempre l'ultima dimensione.

Detto in altri termini, se una funzione, ad esempio, accetta un array bidimensionale di interi come argomento, dobbiamo specificare sempre la seconda dimensione, ossia il numero di colonne.

Per cui, se vogliamo definire una funzione che accetta un array bidimensionale di interi come argomento, la sintassi sarà la seguente:

int funzione(int a[][5]) {
    /* codice */
}

La conseguenza è che possiamo passare alla funzione array bidimensionali con un numero arbitrario di righe ma con un numero fisso di colonne.

Se proviamo, infatti, ad invocare la funzione usando un array con un numero differente di colonne otteniamo un errore di compilazione:

int array[2][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8}
};

funzione(array); // ERRORE
Definizione

Array Multidimensionali come Argomenti di una Funzione

Una funzione può accettare come parametro un array multidimensionale.

Il vincolo, in tal caso, è che l'ultima dimensione dell'array deve essere sempre specificata nella definizione della funzione. Inoltre, la dimensione specificata deve corrispondere alla dimensione effettiva dell'array passato come argomento.

La sintassi per definire una funzione che accetta un array multidimensionale di dimensione:

n_1 \times n_2 \times \ldots \times n_k

è la seguente:

tipo_ritorno funzione(tipo array[][]...[n_k]) {
    /* codice */
}

Dove n_k specifica l'ultima dimensione dell'array.

Il non poter passare un array multidimensionale con un numero di colonne diverso da quello specificato nella definizione della funzione può essere un problema. Fortunatamente, questo problema può essere risolto in C99 utilizzando un array a lunghezza variabile come vedremo nella prossima sezione.

Array a lunghezza variabile come Argomenti di una Funzione in C99

Lo standard C99 aggiunge delle nuove funzionalità al linguaggio C, tra cui la possibilità di passare array a lunghezza variabile come argomenti di una funzione.

Come abbiamo visto nella lezione relativa, un array a lunghezza variabile, o VLA per brevità, è un array la cui dimensione è determinata a runtime attraverso un'espressione costante.

In C99 possiamo, infatti, definire un array in questo modo:

int n = 5;
int array[n];

In questo caso, la dimensione dell'array array è determinata dalla variabile n il cui valore è noto solo a tempo di esecuzione.

Possiamo sfruttare questa caratteristica a nostro vantaggio quando definiamo funzioni che accettano un array. Ritorniamo all'esempio della funzione somma che calcola la somma di tutti gli elementi di un array di interi.

Originariamente, avevamo scritto la funzione in questo modo:

int somma(int a[], int dimensione) {
    int somma = 0;

    for (int i = 0; i < dimensione; i++) {
        somma += a[i];
    }

    return somma;
}

Come si può osservare, il difetto della funzione scritta in questo modo è che non c'è nessuna relazione tra il parametro dimensione e la dimensione effettiva dell'array a.

Anche se la funzione somma usa il parametro dimensione per determinare la lunghezza dell'array, esiste comunque il problema che esso possa non corrispondere alla dimensione effettiva dell'array. In altre parole, nessuno controlla che la dimensione passata come argomento sia corretta.

Usando i VLA in C99 possiamo legare esplicitamente la dimensione dell'array al parametro dimensione in maniera tale che il compilatore controlli che la dimensione passata come argomento sia corretta. Possiamo riscrivere, infatti, la funzione somma in questo modo:

int somma(int dimensione, int a[dimensione]) {
    int somma = 0;

    for (int i = 0; i < dimensione; i++) {
        somma += a[i];
    }

    return somma;
}

Il primo parametro dimensione specifica la dimensione dell'array a. Il secondo parametro a è un VLA di dimensione dimensione. Da notare che abbiamo invertito l'ordine dei parametri. Se avessimo scritto la funzione in questo modo:

int somma(int a[dimensione], int dimensione) {
    /* codice */
}

Il compilatore avrebbe generato un errore di compilazione. Questo perché, quando il compilatore incontra int a[dimensione], non sa ancora chi sia dimensione.

Definizione

Array a Lunghezza Variabile come Argomenti di una Funzione in C99 - Caso Monodimensionale

In C99 è possibile passare array a lunghezza variabile monodimensionali come argomenti di una funzione.

La sintassi per definire una funzione che accetta un array a lunghezza variabile è la seguente:

tipo_ritorno funzione(int dimensione, tipo array[dimensione]) {
    /* codice */
}

Dove dimensione specifica la dimensione dell'array array.

Fondamentale è l'ordine dei parametri. Il parametro dimensione deve precedere l'array a lunghezza variabile.

Lo standard C99 fornisce, inoltre, una certa flessibilità nel modo in cui possiamo dichiarare i prototipi di funzioni che accettano i VLA.

Riprendiamo l'esempio della funzione somma. Possiamo dichiararne il prototipo in tre modi:

  1. Possiamo dichiarare il prototipo allo stesso modo in cui abbiamo definito la funzione:

    int somma(int dimensione, int a[dimensione]);
    
  2. Possiamo dichiarare il prototipo usando un asterisco all'interno delle parentesi quadre:

    int somma(int dimensione, int a[*]);
    

    In tal caso, l'asterisco indica che la dimensione dell'array a è determinata a runtime. Il compilatore comunque sa che a è un VLA e che la sua dimensione dipende dal parametro dimensione in quanto nella definizione effettiva della funzione a è dichiarato come int a[dimensione].

  3. Il terzo modo consiste nel non specificare alcuna dimensione lasciando le parentesi quadre vuote:

    int somma(int dimensione, int a[]);
    

    Anche in questo caso, il compilatore sa che a è un VLA e che la sua dimensione dipende dal parametro dimensione dalla definizione.

    Questo terzo modo, però, è sconsigliato perché non documenta esplicitamente che a ha una dimensione dipendente da dimensione. Quindi, agli occhi di uno sviluppatore che usa la funzione somma non è esplicita questa dipendenza.

La potenza degli array a lunghezza variabile sta nel fatto che la loro dimensione può essere una qualunque espressione costante. Pertanto, possiamo anche scrivere funzioni più complesse.

Supponiamo, ad esempio, di voler realizzare una funzione che concateni due array in un terzo. Possiamo realizzare la funzione in questo modo:

void concatena(int m, int n, int a[m], int b[n], int c[m + n]) {
    for (int i = 0; i < m; i++) {
        c[i] = a[i];
    }

    for (int i = 0; i < n; i++) {
        c[m + i] = b[i];
    }
}

In questo caso, la funzione concatena accetta tre array a lunghezza variabile a, b e c. La dimensione dell'array c è la somma delle dimensioni degli array a e b specificate nei parametri m e n.

L'utilizzo di array a lunghezza variabile nel caso monodimensionale non ha una grande utilità. Il compilatore, effettivamente, controlla che la dimensione passata come argomento sia corretta, ma nella maggior parte dei casi ciò che fa è emettere un warning o avvertimento. In altre parole, è comunque possibile passare una dimensione sbagliata.

Il programma viene compilato comunque, anche se la dimensione è sbagliata.

Diciamo che nel caso monodimensionale il vantaggio riguarda il fatto che le nostre funzioni sono documentate meglio.

Il vero vantaggio degli array a lunghezza variabile riguarda il caso multidimensionale.

Sopra abbiamo visto che quando usiamo array multidimensionali come argomenti di una funzione, siamo obbligati a specificare sempre l'ultima dimensione. Inoltre, siamo limitati a passare sempre array con un numero fisso di colonne. Usando gli array a lunghezza variabile possiamo superare queste limitazioni.

Supponiamo, ad esempio, di voler realizzare una funzione che somma tutti gli elementi di una matrice m x n. Possiamo realizzare la funzione in questo modo in C99:

int somma(int m, int n, int a[m][n]) {
    int somma = 0;

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            somma += a[i][j];
        }
    }

    return somma;
}

In questo caso, la funzione somma accetta una matrice m x n di interi. La dimensione della matrice è specificata nei parametri m e n e possiamo passare una matrice con un numero arbitrario di righe e colonne.

Possiamo invocare la funzione in questo modo:

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

int risultato = somma(2, 3, matrice);
Definizione

Array a Lunghezza Variabile come Argomenti di una Funzione in C99 - Caso Multidimensionale

In C99 è possibile passare array a lunghezza variabile multidimensionali come argomenti di una funzione.

La sintassi per definire una funzione che accetta un array a lunghezza variabile multidimensionale è la seguente:

tipo_ritorno funzione(int m, int n, tipo array[m][n]) {
    /* codice */
}

Dove m e n specificano rispettivamente il numero di righe e colonne della matrice array.

Uso della parola chiave static per le dimensioni in C99

Lo standard C99 introduce anche un'importante novità. Possiamo usare la parola chiave static per specificare la dimensione di un array.

La parola chiave static può essere usata davanti la dimensione dell'array. Ad esempio, possiamo scrivere una funzione in questo modo:

int funzione(int n, int a[static 3]) {
    /* codice */
}

In questo modo stiamo dicendo al compilatore che garantiamo che gli array che verranno passati alla funzione avranno sempre almeno 3 elementi.

All'atto pratico non cambia nulla. Il nostro programma funzionerà sempre allo stesso modo. La differenza sta nel fatto che il compilatore può ottimizzare il codice in maniera più efficiente. Infatti, static serve come suggerimento per il compilatore.

Il compilatore potrebbe (o meno) generare codice più efficiente se sa che la dimensione dell'array è sempre almeno 3. Ad esempio, potrebbe precaricare in memoria i primi 3 elementi dell'array in modo da accedere più velocemente a questi elementi. Si tratta quindi di un suggerimento di ottimizzazione. A livello di funzionalità, invece, il comportamento del programma non cambia.

Esiste un limite, però, all'uso di static per specificare le dimensioni. Quando si usano array multidimensionali come argomenti di una funzione, static può essere usato solo per specificare la prima dimensione. Ad esempio:

int funzione(int m, int n, int a[static 4][n]) {
    /* codice */
}
Definizione

Uso della parola chiave static per le dimensioni in C99

In C99 possiamo usare la parola chiave static per specificare la dimensione di un array.

La parola chiave static può essere usata davanti la dimensione dell'array. Ad esempio:

tipo_ritorno funzione(int n, tipo a[static m]) {
    /* codice */
}

In questo modo stiamo dicendo al compilatore che garantiamo che gli array che verranno passati alla funzione avranno sempre almeno m elementi.

Array anonimi in C99

Riprendiamo l'esempio della funzione somma per sommare tutti gli elementi di un'array di interi.

Volendo adoperare la funzione per sommare gli elementi di un array, dobbiamo scrivere un codice simile al seguente:

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

int risultato = somma(5, array);

Questo codice funziona correttamente. Ha solo il problema che siamo sempre costretti a definire un array, inizializzarlo e poi passarlo come argomento alla funzione.

Se, ad esempio, siamo interessati solo a risultato ma l'array successivamente non ci serve più, siamo stati costretti a definire un array inutile usato solo per invocare somma.

Possiamo evitare questo problema in C99 passando come argomento un'array anonimo.

Un'array anonimo consiste in un array privo di nome creato al volo nel punto in cui serve. Possiamo creare un array anonimo in questo modo:

int risultato = somma(5, (int[]){1, 2, 3, 4, 5});

In questo esempio, abbiamo creato un array anonimo di interi {1, 2, 3, 4, 5} e lo abbiamo passato come argomento alla funzione somma.

Abbiamo usato la seguente sintassi:

(int[]){1, 2, 3, 4, 5}

In generale la sintassi è composta da:

In generale, la lunghezza dell'array può essere omessa in quanto viene determinata dalla lista di inizializzazione.

Un array anonimo può anche adoperare gli inizializzatori designati. Ad esempio, possiamo creare un array anonimo in questo modo:

int risultato = somma(5, (int[]){[0] = 1, [2] = 3, [4] = 5});

Inoltre, una lista di inizializzazione può anche essere parziale. In tal caso, però, è necessario specificare la lunghezza dell'array:

int risultato = somma(5, (int[10]){1, 2, [5] = 9});

In questo caso, l'array anonimo ha dimensione 10 e i primi due elementi sono inizializzati a 1 e 2 rispettivamente. Il sesto elemento è inizializzato a 9.

Definizione

Array Anonimi in C99

In C99 possiamo creare array anonimi, ossia array privi di nome creati al volo nel punto in cui servono.

Un array anonimo può essere creato in questo modo:

(tipo[]){valori}

Dove tipo specifica il tipo dell'array e valori è la lista di inizializzazione dell'array.

Un array anonimo viene comunque passato per riferimento, quindi può essere modificato dalla funzione anche se ciò non ha molto senso in quanto l'array anonimo non è più accessibile dopo la chiamata della funzione. In effetti, un array anonimo è un l-value pertanto può essere modificato.

Per esplicitare il fatto che l'array sia a sola lettura possiamo usare la parola chiave const nella definizione dell'array anonimo. Ad esempio:

int risultato = somma(5, (const int[]){1, 2, 3, 4, 5});

In questo modo il compilatore controlla che l'array anonimo non venga modificato all'interno della funzione.

In Sintesi

In questa lezione abbiamo approfondito tutti gli aspetti riguardanti il passaggio di array come argomenti di una funzione in linguaggio C.

Abbiamo visto che:

  • In C è possibile passare array monodimensionali come argomenti di una funzione. La dimensione dell'array può essere omessa.
  • La dimensione di un array passato come argomento non può essere calcolata dalla funzione. Se necessario, la dimensione dell'array deve essere passata come argomento.
  • Gli array vengono passati per riferimento ad una funzione. Qualunque modifica apportata all'array all'interno della funzione sarà visibile anche all'esterno della funzione. In altre parole, quando si passa un array ad una funzione, la funzione lavora direttamente sull'array originale e non su una copia dello stesso.
  • In C99 possiamo passare array a lunghezza variabile come argomenti di una funzione. La dimensione dell'array può essere una qualunque espressione costante.
  • In C99 possiamo usare la parola chiave static per specificare la dimensione di un array. static serve come suggerimento per il compilatore.
  • In C99 possiamo creare array anonimi, ossia array privi di nome creati al volo nel punto in cui servono.

Nella prossima lezione, invece, approfondiremo il meccanismo dei valori di ritorno di una funzione in linguaggio C.