Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 66 di 93
  • livello avanzato
Indice lezioni

Passaggio di array a funzioni

In C++, possiamo utilizzare anche gli array come possibili parametri di funzioni o metodi: vediamo come fare, e quali sono le principali implicazioni.
In C++, possiamo utilizzare anche gli array come possibili parametri di funzioni o metodi: vediamo come fare, e quali sono le principali implicazioni.
Link copiato negli appunti

Nelle lezioni C++ precedenti abbiamo visto le modalità di dichiarazione e definizione di array a dimensione fissa e array allocati dinamicamente. In questa lezione analizzeremo il passaggio di array a metodi membro o funzioni.

A differenza di quanto visto per altre variabili, gli array, statici o dinamici, sono sempre passati per riferimento e mai per valore.

La differenza tra queste due modalità è stata discussa in precedenza a proposito della definizione di funzioni. Il riferimento in questione è tipicamente l'indirizzo di memoria del primo elemento dell'array.

Questa regola ha due ricadute fondamentali sul piano semantico:

  1. ogni eventuale modifica agli elementi dell'array è applicata direttamente all'istanza originale e non ad una copia temporanea
  2. il tipo di un array statico viene implicitamente convertito a quello di puntatore al tipo dell'elemento. In conseguenza di ciò, nel contesto di una funzione o metodo membro, l'informazione sulla dimensione di un array statico passato come argomento non è accessibile.

Quest'ultimo fenomeno in C++ prende il nome di decadimento da array a puntatore (array to pointer decay) e l'analisi del listato seguente può essere di aiuto per comprendere meglio cosa significa:

#include <iostream>
#include <typeinfo> // typeid()
void printArrayOf4(int a[4])
{
std::cout << "Type of arg: " << typeid(a).name() << "\n";
for (int i=0; i<4; i++)
{
std::cout << "index: " << i << " - value: " << a[i] << "\n";
}
}
int main()
{
std::cout << "\n-- Array test -------------\n";
int a1[4] = {1, 2, 3, 4};
std::cout << "Type of a1: " << typeid(a1).name() << "\n";
printArrayOf4(a1);
std::cout << "\n-- Pointer test ------------\n";
int* ptr1 = new int[4] {0};
std::cout << "Type of ptr1: " << typeid(ptr1).name() << "\n";
printArrayOf4(ptr1);
return 0;
}

La funzione printArrayOf4() consente di stampare a schermo array monodimensionali composti esattamente da quattro elementi. Essa viene usata nel contesto della funzione main() due volte, rispettivamente con l'array a dimensione fissa a1 ed uno allocato dinamicamente, cioè ptr1.

Si noti che il tipo nominale dell'argomento di printArrayOf4() è int[4], cioè un array statico di quattro elementi.

Per illustrare l'effetto della conversione implicita, in questo esempio ci si avvale della funzione typeid(), inclusa nella libreria standard C++ e definita nello header <typeinfo>.

typeid() restituisce una struttura dati con informazioni specifiche sul tipo dell'istanza passata come argomento. Ad esempio, per il tipo int il valore restituito dal metodo name() può essere i o int, a seconda del compilatore in uso. Nel caso in esame verrà stampato a schermo il nome del tipo di a1 e ptr1, sia nel contesto della funzione printArrayOf4(), che in quello esterno.

Usando GCC v 8.2.1 su piattaforma Intel x86_64, l'output prodotto dal programma in questione è il seguente:

-- Array test -------------
Type of a1: A4_i
Type of arg: Pi
index: 0 - value: 1
index: 1 - value: 2
index: 2 - value: 3
index: 3 - value: 4
-- Pointer test ------------
Type of ptr1: Pi
Type of arg: Pi
index: 0 - value: 0
index: 1 - value: 0
index: 2 - value: 0
index: 3 - value: 0

Si noti che il tipo nominale di a1 è A4_i (array di 4 interi), mentre quello di ptr1 e Pi (puntatore a intero), ma entrambi, nel contesto della funzione printArrayOf4() sono di tipo Pi, per effetto del decadimento di a1 a puntatore al primo elemento.

La caratterizzazione che abbiamo dato all'argomento di printArrayOf4() è ininfluente. Lo stesso risultato lo si può ottenere sostituendo int a[4] con int a[] o int* a nella firma della funzione.

Per effetto del decadimento, l'informazione relativa al numero di elementi viene sempre persa nella conversione. Quindi l'invocazione della funzione printArrayOf4() su istanze che hanno dimensione differente espone al rischio di violazioni di accesso per array di dimensione inferiore a 4.

In ragione di ciò, è quasi universalmente in uso una convenzione per il passaggio di array, indifferentemente dal loro tipo nominale, che consiste nel passare il puntatore al primo elemento e le sue dimensioni come argomenti distinti. Ad esempio, nel caso precedente l'implementazione della funzione di stampa potrebbe essere la seguente:

void printArrayOfN(int *a, size_t n)
{
for (size_t i=0; i<n; i++)
{
std::cout << "index: " << i << " - value: " << a[i] << "\n";
}
}

size_t è uno tipo definito dallo standard, usato comunemente per l'indicizzazione di array o per rappresentare la dimensione in byte di istanze. Esso è tipicamente un alias per un tipo intero senza segno, che cambia in base all'architettura target ed al compilatore in uso.

Array multidimensionali come argomento di funzione

Il decadimento consente di rendere omogenea e consistente una API che opera su array monodimensionali siano essi a dimensione fissa o allocati dinamicamente.

Purtroppo, questa regola non si applica con la stessa eleganza al caso di array multidimensionali. Pertanto, è necessario distinguere il caso di funzioni che operano su array statici da quelle per array allocati dinamicamente.

Questi ultimi sono, di fatto, il caso d'uso più frequente, soprattutto per istanze di dati che hanno dimensioni significative, per cui nel listato seguente cominciamo ad analizzare proprio il caso di funzioni che operano su array multidimensionali istanziati mediante l'uso di puntatori a puntatori:

#include <iostream>
void printDynamicArrayOfNxN(int** a, size_t rows, size_t cols)
{
for (size_t i=0; i<rows; i++)
{
std::cout << "row: " << i << " : { ";
for (size_t j=0; j<cols; j++)
{
std::cout << a[i][j] << " ";
}
std::cout << "}\n";
}
}
int main()
{
int** ptr1 = new int*[2];
ptr1[0] = new int[4] {1, 2, 3, 4};
ptr1[1] = new int[4] {5, 6, 7, 8};
std::cout << "\n-- Dynamic NxN --\n";
printDynamicArrayOfNxN(ptr1, 2, 4);
return 0;
}

In questo caso, la funzione printDynamicArrayOfNxN() costituisce una semplice estensione dell'implementazione di printArrayOfN() al caso di array 2D. Si osservi che, poiché l'array ha due dimensioni, il tipo dell'argomento è un puntatore doppio ed entrambe le dimensioni figurano come argomenti addizionali di nome rows e cols.

Sfortunatamente, questa funzione è applicabile solamente ad array bidimensionali allocati dinamicamente. Invocando questa funzione passando come argomento un array a dimensione fissa, come mostrato nel frammento seguente, si commetterebbe un errore sintattico e concettuale:

int a1[2][4] = { {1, 2, 3, 4}, {5, 6, 7, 8} };
printDynamicArrayOfNxN(a1, 2, 4); // errore di compilazione!

Il compilatore ci nofiticherebbe in questo caso l'impossibilità di applicare una conversione tra il tipo int(*)[4] e int**. Questo è in realtà proprio una conseguenza diretta del decadimento, poiché il tipo del primo elemento di questo array statico multidimensionale, in effetti, è int[4] e non int.

In altri termini, se int[] decade a int* poiché il primo elemento è un intero, allora int[2][4] decade a cioè int(*)[4], cioè puntatore ad array statico di 4 elementi.

La soluzione consiste nel definire una funzione specifica per array statici fissando di volta in volta le dimensioni in modo hard-coded, ad eccezione della prima, che possiamo passare come argomento aggiuntivo, in quanto soggetta a decadimento.

In alternativa, al costo di dover effettuare una conversione esplicita, è possibile sfruttare la caratteristica disposizione lineare degli elementi di un array statico per creare una API generica, non ristretta cioè ad array di dimensioni specifiche, che è molto simile a quella usata nel caso di array dinamici.

Entrambi gli approcci sono mostrati nel listato seguente:

#include <iostream>
// array con N righe e 4 colonne
void printFixedArrayOfNx4(int a[][4], size_t rows)
{
for (size_t i=0; i<rows; i++)
{
std::cout << "row: " << i << " : { ";
for (size_t j=0; j<4; j++) // il numero di collonne è hard-coded
{
std::cout << a[i][j] << " ";
}
std::cout << "}\n";
}
}
// array con N righe e N colonne
void printFixedArrayOfNxN(int* a, size_t rows, size_t cols)
{
for (size_t i=0; i<rows; i++)
{
std::cout << "row: " << i << " : { ";
for (size_t j=0; j<cols; j++)
{
// accesso sequenziale agli elementi mediante
// il calcolo di un indice lineare
std::cout << a[i * cols + j] << " ";
}
std::cout << "}\n";
}
}
int main()
{
int a1[2][4] = { {1, 2, 3, 4}, {5, 6, 7, 8} };
std::cout << "\n-- Fixed Nx4 -- \n";
printFixedArrayOfNx4(a1, 2);
std::cout << "\n-- Fixed NxN --\n";
printFixedArrayOfNxN(reinterpret_cast<int*>(a1), 2, 4);
return 0;
}

La principale differenza tra le due funzioni di stampa consiste nel tipo del primo argomento.

Nel caso di printFixedArrayOfNxN() si rende necessario convertire esplicitamente l'array statico ad un puntatore mediante l'uso dell'operatore reinterpret_cast.

Tuttavia, printFixedArrayOfNxN() è una funzione più generica di printFixedArrayOfNx4(), come si evince dall'assenza di parametri hard-coded.

L'implementazione di printFixedArrayOfNxN() si basa sul fatto che gli elementi di array a dimensione fissa sono sempre disposti in locazioni di memoria contigue, anche nel caso multidimensionale. In printFixedArrayOfNxN() quindi, l'array viene scandito usando un indice lineare opportunamente calcolato a partire da i e j.

Inoltre, poichè il numero totale di elementi dell'array è sempre maggiore o uguale al numero di elementi della prima riga, la conversione è sicura nel senso che non ci espone al rischio di violazioni di accesso.

In conclusione, il decadimento è serve ad aggirare la forte tipizzazione data agli array a dimensione fissa in C++. Tuttavia, nel caso multidimensionale emergono complicazioni aggiuntive, che possono essere mitigate mediante l'uso degli espedienti che abbiamo analizzato.

Tuttavia, va detto che gli array sono strutture dati di basso livello, utili soprattutto per manipolare la memoria in modo rapido ed efficiente. In generale, la specifica del linguaggio mette a disposizione strutture dati alternative per la gestione di istanze multiple che sono più flessibili e semplici da gestire, come vedremo meglio nel seguito.

Ti consigliamo anche