Passaggio di Argomenti alle Funzioni in C++
- Le funzioni possono prendere argomenti.
- Gli argomenti possono essere passati per valore o per riferimento.
- I parametri riferimento che non vengono cambiati dentro una funzione dovrebbero essere riferimenti a
const
. - I parametri riferimento possono essere usati per restituire più di un valore da una funzione.
- I parametri ellissi dovrebbero essere usati solo per interfacciarsi con funzioni C.
- I parametri
initializer_list
possono essere usati per passare un numero variabile di argomenti dello stesso tipo.
Passaggio di Argomenti
Come abbiamo visto, ogni volta che chiamiamo una funzione, i suoi parametri vengono creati e inizializzati dagli argomenti passati nella chiamata.
Infatti, l'inizializzazione dei parametri funziona allo stesso modo dell'inizializzazione delle variabili.
Come con qualsiasi altra variabile, il tipo di un parametro determina l'interazione tra il parametro e il suo argomento. Se il parametro è un riferimento, allora il parametro è legato al suo argomento. Altrimenti, il valore dell'argomento viene copiato.
Quando un parametro è un riferimento, diciamo che il suo argomento corrispondente è passato per riferimento o che la funzione è chiamata per riferimento. Come con qualsiasi altro riferimento, un parametro riferimento è un alias per l'oggetto a cui è legato; cioè, il parametro è un alias per il suo argomento corrispondente.
Quando il valore dell'argomento viene copiato, il parametro e l'argomento sono oggetti indipendenti. Diciamo che tali argomenti sono passati per valore o alternativamente che la funzione è chiamata per valore.
Passaggio di Argomenti per Valore
Quando inizializziamo una variabile di tipo non riferimento, il valore dell'inizializzatore viene copiato. Le modifiche apportate alla variabile non hanno effetto sull'inizializzatore:
int n = 0; // variabile ordinaria di tipo int
int i = n; // i è una copia del valore in n
i = 42; // il valore in i è cambiato; n è invariato
Passare un argomento per valore funziona esattamente allo stesso modo; niente di ciò che la funzione fa al parametro può influenzare l'argomento. Ad esempio, riprendiamo la funzione fattoriale
della lezione precedente:
int fattoriale(int val) // val è una copia dell'argomento passato a fattoriale
{
int ris = 1;
while (val > 1)
ris *= val--; // decrementa il valore di val
return ris;
}
In questa funzione, il parametro val
viene decrementato:
ris *= val--; // decrementa il valore di val
Anche se fattoriale
cambia il valore di val
, quel cambiamento non ha effetto sull'argomento passato a fattoriale
. Chiamare fattoriale(i)
non cambia il valore di i
.
Parametri Puntatore
I puntatori si comportano come qualsiasi altro tipo non riferimento. Quando copiamo un puntatore, il valore del puntatore viene copiato. Dopo la copia, i due puntatori sono distinti. Tuttavia, un puntatore ci dà anche accesso indiretto all'oggetto a cui quel puntatore punta. Possiamo cambiare il valore di quell'oggetto assegnando attraverso il puntatore:
int n = 0, i = 42;
int *p = &n, *q = &i; // p punta a n; q punta a i
*p = 42; // il valore in n è cambiato; p è invariato
p = q; // p ora punta a i; i valori in i e n sono invariati
Lo stesso comportamento si applica ai parametri puntatore:
// funzione che prende un puntatore e imposta il valore puntato a zero
void azzera(int *ip)
{
*ip = 0; // cambia il valore dell'oggetto a cui ip punta
ip = 0; // cambia solo la copia locale di ip; l'argomento è invariato
}
Dopo una chiamata a azzera
, l'oggetto a cui punta l'argomento sarà 0, ma l'argomento puntatore stesso è invariato:
int i = 42;
azzera(&i); // cambia i ma non l'indirizzo di i
cout << "i = " << i << endl; // stampa i=0
I programmatori abituati a programmare in C usano spesso parametri puntatore per accedere agli oggetti al di fuori di una funzione. In C++, i programmatori generalmente usano invece parametri riferimento.
Esercizio
-
Usando i puntatori, scrivi una funzione per scambiare i valori di due
int
. Testa la funzione chiamandola e stampando i valori scambiati.Soluzione:
#include <iostream> using namespace std; void scambia(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main() { int x = 5, y = 10; cout << "Prima dello scambio: x = " << x << ", y = " << y << endl; scambia(&x, &y); cout << "Dopo lo scambio: x = " << x << ", y = " << y << endl; return 0; }
Passaggio di Argomenti per Riferimento
Ricorda che le operazioni su un riferimento sono in realtà operazioni sull'oggetto a cui il riferimento si riferisce:
int n = 0, i = 42;
int &r = n; // r è legato a n (cioè, r è un altro nome per n)
r = 42; // n ora è 42
r = i; // n ora ha lo stesso valore di i
i = r; // i ha lo stesso valore di n
I parametri riferimento sfruttano questo comportamento. Sono spesso usati per permettere a una funzione di cambiare il valore di uno o più dei suoi argomenti.
Come esempio, possiamo riscrivere il nostro programma azzera
della sezione precedente per prendere un riferimento invece di un puntatore:
// funzione che prende un riferimento a un int e imposta l'oggetto dato a zero
void azzera(int &i) // i è solo un altro nome per l'oggetto passato ad azzera
{
i = 0; // cambia il valore dell'oggetto a cui i si riferisce
}
Come con qualsiasi altro riferimento, un parametro riferimento è legato direttamente all'oggetto da cui è inizializzato. Quando chiamiamo questa versione di azzera
, i
sarà legato a qualunque oggetto int
passiamo. Come con qualsiasi riferimento, le modifiche apportate a i
sono apportate all'oggetto a cui i
si riferisce. In questo caso, quell'oggetto è l'argomento ad azzera
.
Quando chiamiamo questa versione di azzera
, passiamo un oggetto direttamente; non c'è bisogno di passare il suo indirizzo:
int j = 42;
azzera(j); // j viene passato per riferimento; il valore in j viene cambiato
cout << "j = " << j << endl; // stampa j=0
In questa chiamata, il parametro i
è solo un altro nome per j
. Qualsiasi uso di i
dentro azzera
è un uso di j
.
Usare Riferimenti per Evitare Copie
Può essere inefficiente copiare oggetti di tipi di classe grandi o grandi contenitori. Inoltre, alcuni tipi di classe (inclusi i tipi di I/O) non possono essere copiati. Le funzioni devono usare parametri riferimento per operare su oggetti di un tipo che non può essere copiato.
Come esempio, scriveremo una funzione per confrontare la lunghezza di due stringhe. Poiché le stringhe possono essere lunghe, vorremmo evitare di copiarle, useremo dei parametri passati per riferimento. Poiché confrontare due stringhe non comporta la modifica delle stringhe, faremo i parametri riferenti a const
:
// confronta la lunghezza di due stringhe
bool e_piu_corta(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
Come vedremo in seguito, le funzioni dovrebbero usare riferimenti a const
per i parametri riferimento che non hanno bisogno di cambiare.
I parametri riferimento che non vengono cambiati dentro una funzione dovrebbero essere riferimenti a const
.
Usare Parametri Riferimento per Restituire Informazioni Aggiuntive
Una funzione può restituire solo un singolo valore. Tuttavia, a volte una funzione ha più di un valore da restituire. I parametri riferimento ci permettono di restituire effettivamente risultati multipli. Come esempio, definiremo una funzione chiamata trova_car
che restituirà la posizione della prima occorrenza di un dato carattere in una stringa. Vorremmo anche che la funzione restituisse un conteggio di quante volte quel carattere si verifica.
Come possiamo definire una funzione che restituisce una posizione e un conteggio di occorrenze? Potremmo definire un nuovo tipo che contiene la posizione e il conteggio. Una soluzione più facile è passare un argomento riferimento aggiuntivo per contenere il conteggio delle occorrenze:
// restituisce l'indice della prima occorrenza di c in s
// il parametro riferimento occorrenze conta quante volte c si verifica
string::size_type trova_car(const string &s, char c,
string::size_type &occorrenze)
{
auto ris = s.size(); // posizione della prima occorrenza, se presente
occorrenze = 0; // imposta il parametro conteggio occorrenze
for (decltype(ris) i = 0; i != s.size(); ++i) {
if (s[i] == c) {
if (ris == s.size())
ris = i; // ricorda la prima occorrenza di c
++occorrenze; // incrementa il conteggio delle occorrenze
}
}
return ris; // il conteggio viene restituito implicitamente in occorrenze
}
Quando chiamiamo trova_car
, dobbiamo passare tre argomenti: una stringa in cui cercare, il carattere da cercare, e un oggetto size_type
per contenere il conteggio delle occorrenze. Supponendo che s
sia una stringa, e cont
sia un oggetto size_type
, possiamo chiamare trova_car
come segue:
auto indice = trova_car(s, 'o', cont);
Dopo la chiamata, il valore di cont
sarà il numero di volte che o
si verifica, e indice
si riferirà alla prima occorrenza se ce n'è una. Altrimenti, indice
sarà uguale a s.size()
e cont
sarà zero.
Esercizi
-
Scrivi e testa la tua versione di
azzera
che prende un riferimento.*Soluzione:
#include <iostream> using namespace std; void azzera(int &i) { i = 0; } int main() { int j = 42; cout << "Prima di azzera: j = " << j << endl; azzera(j); cout << "Dopo azzera: j = " << j << endl; return 0; }
-
Riscrivi il programma di scambio valori per usare riferimenti invece di puntatori per scambiare il valore di due
int
.Soluzione:
#include <iostream> using namespace std; void scambia(int &a, int &b) { int temp = a; a = b; b = temp; } int main() { int x = 5, y = 10; cout << "Prima dello scambio: x = " << x << ", y = " << y << endl; scambia(x, y); cout << "Dopo lo scambio: x = " << x << ", y = " << y << endl; return 0; }
-
Supponendo che
T
sia il nome di un tipo, spiega la differenza tra una funzione dichiarata comevoid f(T)
evoid f(T&)
.Risposta:
void f(T)
dichiara una funzione che prende un argomento di tipoT
per valore, il che significa che il valore dell'argomento viene copiato nel parametro della funzione. Le modifiche apportate al parametro all'interno della funzione non influenzeranno l'argomento originale. D'altra parte,void f(T&)
dichiara una funzione che prende un argomento di tipoT
per riferimento. In questo caso, il parametro è un alias per l'argomento originale, quindi qualsiasi modifica apportata al parametro all'interno della funzione influenzerà direttamente l'argomento originale. -
Fornisci un esempio di quando un parametro dovrebbe essere un tipo riferimento. Fornisci un esempio di quando un parametro non dovrebbe essere un riferimento.
Risposta: Un parametro dovrebbe essere un tipo riferimento quando si desidera modificare l'argomento originale passato alla funzione o quando si vuole evitare il costo di copiare un oggetto grande. Ad esempio, una funzione che scambia i valori di due variabili dovrebbe usare riferimenti:
void scambia(int &a, int &b) { int temp = a; a = b; b = temp; }
Un parametro non dovrebbe essere un riferimento quando si desidera che la funzione lavori con una copia dell'argomento originale, in modo che le modifiche non influenzino l'argomento originale. Ad esempio, una funzione che calcola il quadrato di un numero potrebbe prendere un parametro per valore:
int quadrato(int n) { return n * n; }
-
Spiega la motivazione per il tipo di ciascuno dei parametri di
trova_car
. In particolare, perchés
è un riferimento aconst
maoccorrenze
è un riferimento semplice? Perché questi parametri sono riferimenti, ma il parametro charc
non lo è? Cosa succederebbe se facessimo dis
un riferimento semplice? Cosa succederebbe se facessimo dioccorrenze
un riferimento aconst
?Risposta: Il parametro
s
è un riferimento aconst
perché la funzione non ha bisogno di modificare la stringa passata; usare un riferimento aconst
evita la copia della stringa, migliorando l'efficienza. Il parametrooccorrenze
è un riferimento semplice perché la funzione deve modificare il valore di questo parametro per restituire il conteggio delle occorrenze del carattere. Il parametroc
non è un riferimento perché è un tipo primitivo (char) e il costo di copiarlo è trascurabile; inoltre, non c'è bisogno di modificarlo. Ses
fosse un riferimento semplice, la funzione potrebbe modificare la stringa originale, il che non è desiderato in questo caso. Seoccorrenze
fosse un riferimento aconst
, la funzione non potrebbe modificare il conteggio delle occorrenze, rendendo impossibile restituire questa informazione al chiamante.
Parametri e Argomenti const
Quando usiamo parametri che sono const
, è importante ricordare la discussione del const
di alto livello. Come abbiamo visto in quella sezione, un const
di alto livello è uno che si applica all'oggetto stesso:
const int ci = 42; // non possiamo cambiare ci; const è di alto livello
int i = ci; // ok: quando copiamo ci, il suo const di alto livello viene ignorato
int * const p = &i; // const è di alto livello; non possiamo assegnare a p
*p = 0; // ok: i cambiamenti attraverso p sono permessi; i è ora 0
Proprio come in qualsiasi altra inizializzazione, quando copiamo un argomento per inizializzare un parametro, i const
di alto livello vengono ignorati. Di conseguenza, i const
di alto livello sui parametri vengono ignorati. Possiamo passare sia un oggetto const
che non const
a un parametro che ha un const
di alto livello:
void fcn(const int i) { /* fcn può leggere ma non scrivere su i */ }
Possiamo chiamare fcn
passandogli sia un const int
che un int
semplice. Il fatto che i const
di alto livello vengano ignorati su un parametro ha un'implicazione possibilmente sorprendente:
void fcn(const int i) { /* fcn può leggere ma non scrivere su i */ }
void fcn(int i) { /* ... */ } // errore: ridefinisce fcn(int)
In C++, possiamo definire diverse funzioni diverse che hanno lo stesso nome. Tuttavia, possiamo farlo solo se le loro liste di parametri sono sufficientemente diverse. Poiché i const
di alto livello vengono ignorati, possiamo passare esattamente gli stessi tipi a entrambe le versioni di fcn
. La seconda versione di fcn
è un errore. Nonostante le apparenze, la sua lista di parametri non differisce dalla lista nella prima versione di fcn
.
Parametri Puntatore o Riferimento e const
Poiché i parametri vengono inizializzati allo stesso modo in cui vengono inizializzate le variabili, può essere utile ricordare le regole generali di inizializzazione. Possiamo inizializzare un oggetto con un const
di basso livello da un oggetto non const
ma non viceversa, e un riferimento semplice deve essere inizializzato da un oggetto dello stesso tipo.
int i = 42;
const int *cp = &i; // ok: ma cp non può cambiare i
const int &r = i; // ok: ma r non può cambiare i
const int &r2 = 42; // ok
int *p = cp; // errore: i tipi di p e cp non corrispondono
int &r3 = r; // errore: i tipi di r3 e r non corrispondono
int &r4 = 42; // errore: non si può inizializzare un riferimento semplice da un letterale
Esattamente le stesse regole di inizializzazione si applicano al passaggio di parametri:
int i = 0;
const int ci = i;
string::size_type cont = 0;
azzera(&i); // chiama la versione di azzera che ha un parametro int*
azzera(&ci); // errore: non si può inizializzare un int* da un puntatore a un oggetto const int
azzera(i); // chiama la versione di azzera che ha un parametro int&
azzera(ci); // errore: non si può legare un riferimento semplice all'oggetto const ci
azzera(42); // errore: non si può legare un riferimento semplice a un letterale
azzera(cont); // errore: i tipi non corrispondono; cont ha un tipo unsigned
// ok: il primo parametro di trova_car è un riferimento a const
trova_car("Ciao Mondo!", 'o', cont);
Possiamo chiamare la versione riferimento di azzera
solo su oggetti int
. Non possiamo passare un letterale, un'espressione che valuta a un int
, un oggetto che richiede conversione, o un oggetto const int
. Analogamente, possiamo passare solo un int*
alla versione puntatore di azzera
. D'altra parte, possiamo passare un letterale stringa come primo argomento a trova_car
. Il parametro riferimento di quella funzione è un riferimento a const
, e possiamo inizializzare riferimenti a const
da letterali.
Usare Riferimento a const
Quando Possibile
È un errore abbastanza comune definire parametri che una funzione non cambia come riferimenti (semplici). Farlo dà al chiamante della funzione l'impressione fuorviante che la funzione potrebbe cambiare il valore del suo argomento. Inoltre, usare un riferimento invece di un riferimento a const
limita indebitamente il tipo di argomenti che possono essere usati con la funzione. Come abbiamo appena visto, non possiamo passare un oggetto const
, o un letterale, o un oggetto che richiede conversione a un parametro riferimento semplice.
L'effetto di questo errore può essere sorprendentemente pervasivo. Come esempio, consideriamo la nostra funzione trova_car
della sezione precedente. Quella funzione (correttamente) ha reso il suo parametro stringa un riferimento a const
. Se avessimo definito quel parametro come un semplice string&
:
// design sbagliato: il primo parametro dovrebbe essere un const string&
string::size_type trova_car(string &s, char c,
string::size_type &occorrenze);
potremmo chiamare trova_car
solo su un oggetto string
. Una chiamata come
trova_car("Ciao Mondo", 'o', cont);
fallirebbe in fase di compilazione.
Più sottilmente, non potremmo usare questa versione di trova_car
da altre funzioni che (correttamente) definiscono i loro parametri come riferimenti a const
. Ad esempio, potremmo voler usare trova_car
dentro una funzione che determina se una stringa rappresenta una frase:
bool e_frase(const string &s)
{
// se c'è un singolo punto alla fine di s, allora s è una frase
string::size_type cont = 0;
return trova_car(s, '.', cont) == s.size() - 1 && cont == 1;
}
Se trova_car
prendesse un semplice string&
, allora questa chiamata a trova_car
sarebbe un errore in fase di compilazione. Il problema è che s
è un riferimento a una string const
, ma trova_car
era (erroneamente) definito per prendere un riferimento semplice.
Potrebbe essere allettante provare a risolvere questo problema cambiando il tipo del parametro in e_frase
. Ma quella correzione propaga solo l'errore: i chiamanti di e_frase
potrebbero passare solo stringhe non const
.
Il modo giusto per risolvere questo problema è correggere il parametro in trova_car
. Se non è possibile cambiare trova_car
, allora definisci una copia string
locale di s
dentro e_frase
e passa quella stringa a trova_car
.
Esercizi
-
La seguente funzione, sebbene legale, è meno utile di quanto potrebbe essere. Identifica e correggi la limitazione su questa funzione:
bool e_vuota(string& s) { return s.empty(); }
Risposta: La funzione
e_vuota
prende un parametro di tipostring&
, il che significa che può essere chiamata solo con oggettistring
nonconst
. Per renderla più utile, dovrebbe prendere un riferimento aconst string&
, permettendo così di essere chiamata con stringheconst
e letterali. La versione corretta è:bool e_vuota(const string& s) { return s.empty(); }
-
Scrivi una funzione per determinare se una stringa contiene lettere maiuscole. Scrivi una funzione per cambiare una stringa in tutte minuscole. I parametri che hai usato in queste funzioni hanno lo stesso tipo? Se sì, perché? Se no, perché no?
Risposta: Le due funzioni avranno parametri di tipo diverso. La funzione che determina se una stringa contiene lettere maiuscole dovrebbe prendere un riferimento a
const string&
, poiché non ha bisogno di modificare la stringa:#include <cctype> // per isupper e tolower #include <string> using namespace std; bool contiene_maiuscole(const string& s) { for (char c : s) { if (isupper(c)) return true; } return false; }
La funzione che cambia una stringa in tutte minuscole deve prendere un riferimento semplice
string&
, poiché modificherà la stringa:#include <cctype> // per isupper e tolower #include <string> using namespace std; void a_minuscole(string& s) { for (char& c : s) { c = tolower(c); } }
Quindi, i parametri non hanno lo stesso tipo perché uno deve essere
const
(non modifica la stringa) e l'altro no (modifica la stringa). -
Scrivi dichiarazioni per ciascuna delle seguenti funzioni. Quando scrivi queste dichiarazioni, usa il nome della funzione per indicare cosa fa la funzione.
-
Una funzione chiamata
confronta
che restituisce unbool
e ha due parametri che sono riferimenti a una classe chiamatamatrice
.Soluzione:
bool confronta(const matrice &m1, const matrice &m2);
-
Una funzione chiamata
cambia_val
che restituisce un iteratorevector<int>
e prende due parametri: Uno è unint
e l'altro è un iteratore per unvector<int>
.Soluzione:
vector<int>::iterator cambia_val(int val, vector<int>::iterator it);
-
-
Date le seguenti dichiarazioni, determina quali chiamate sono legali e quali sono illegali. Per quelle che sono illegali, spiega perché.
double calc(double); int conta(const string &, char); int somma(vector<int>::iterator, vector<int>::iterator, int); vector<int> vec(10); calc(23.4, 55.1); conta("abcda", 'a'); calc(66); somma(vec.begin(), vec.end(), 3.8);
Risposta:
calc(23.4, 55.1);
è illegale perchécalc
prende un solo parametro di tipodouble
, ma qui ne vengono passati due.conta("abcda", 'a');
è legale. La stringa letterale viene convertita in unconst string&
e il carattere'a'
è di tipochar
.calc(66);
è legale. L'intero66
viene convertito indouble
.somma(vec.begin(), vec.end(), 3.8);
è legale. Gli iteratori divec
sono del tipo corretto e l'intero3.8
viene convertito inint
.
-
Quando i parametri riferimento dovrebbero essere riferimenti a
const
? Cosa succede se facciamo di un parametro un riferimento semplice quando potrebbe essere un riferimento aconst
?Risposta: I parametri riferimento dovrebbero essere riferimenti a
const
quando la funzione non ha bisogno di modificare l'argomento passato. Usare un riferimento aconst
permette alla funzione di accettare sia oggetticonst
che nonconst
, aumentando la flessibilità della funzione. Se facciamo di un parametro un riferimento semplice quando potrebbe essere un riferimento aconst
, limitiamo indebitamente il tipo di argomenti che possono essere passati alla funzione, impedendo l'uso di oggetticonst
, letterali o oggetti che richiedono conversione. Questo può portare a errori di compilazione e ridurre la riusabilità della funzione.
Parametri Array
Gli array hanno due proprietà speciali che influenzano come definiamo e usiamo funzioni che operano su array: Non possiamo copiare un array, e quando usiamo un array viene (solitamente) convertito in un puntatore. Poiché non possiamo copiare un array, non possiamo passare un array per valore. Poiché gli array vengono convertiti in puntatori, quando passiamo un array a una funzione, stiamo in realtà passando un puntatore al primo elemento dell'array.
Anche se non possiamo passare un array per valore, possiamo scrivere un parametro che sembra un array:
// nonostante le apparenze, queste tre dichiarazioni di stampa sono equivalenti
// ogni funzione ha un singolo parametro di tipo const int*
void stampa(const int*);
void stampa(const int[]); // mostra l'intenzione che la funzione prende un array
void stampa(const int[10]); // dimensione per scopi di documentazione (al massimo)
Indipendentemente dalle apparenze, queste dichiarazioni sono equivalenti: Ognuna dichiara una funzione con un singolo parametro di tipo const int*
. Quando il compilatore controlla una chiamata a stampa
, controlla solo che l'argomento abbia tipo const int*
:
int i = 0, j[2] = {0, 1};
stampa(&i); // ok: &i è int*
stampa(j); // ok: j viene convertito in un int* che punta a j[0]
Se passiamo un array a stampa
, quell'argomento viene automaticamente convertito in un puntatore al primo elemento nell'array; la dimensione dell'array è irrilevante.
Come con qualsiasi codice che usa array, le funzioni che prendono parametri array devono assicurarsi che tutti gli usi dell'array rimangano dentro i confini dell'array.
Poiché gli array vengono passati come puntatori, le funzioni ordinariamente non conoscono la dimensione dell'array che gli viene dato. Devono fare affidamento su informazioni aggiuntive fornite dal chiamante. Ci sono tre tecniche comuni usate per gestire parametri puntatore.
Usare un Marcatore per Specificare l'Estensione di un Array
Il primo approccio alla gestione degli argomenti array richiede che l'array stesso contenga un marcatore di fine. Le stringhe di caratteri in stile C sono un esempio di questo approccio. Le stringhe in stile C sono memorizzate in array di caratteri in cui l'ultimo carattere della stringa è seguito da un carattere nullo. Le funzioni che trattano stringhe in stile C smettono di processare l'array quando vedono un carattere nullo:
void stampa(const char *cp)
{
if (cp) // se cp non è un puntatore nullo
while (*cp) // finché il carattere a cui punta non è un carattere nullo
cout << *cp++; // stampa il carattere e avanza il puntatore
}
Questa convenzione funziona bene per dati dove c'è un valore marcatore di fine ovvio (come il carattere nullo) che non appare nei dati ordinari. Funziona meno bene con dati, come gli int
, dove ogni valore nell'intervallo è un valore legittimo.
Usare le Convenzioni della Libreria Standard
Una seconda tecnica usata per gestire argomenti array è passare puntatori al primo e uno oltre l'ultimo elemento nell'array. Questo approccio è ispirato da tecniche usate nella libreria standard. Useremo questo approccio per stampare gli elementi in un array come segue:
void stampa(const int *inizio, const int *fine)
{
// stampa ogni elemento a partire da inizio fino a ma non includendo fine
while (inizio != fine)
cout << *inizio++ << endl; // stampa l'elemento corrente
// e avanza il puntatore
}
Il while
usa gli operatori di dereferenziazione e incremento postfisso per stampare l'elemento corrente e avanzare inizio
un elemento alla volta attraverso l'array. Il ciclo si ferma quando inizio
è uguale a fine
.
Per chiamare questa funzione, passiamo due puntatori—uno al primo elemento che vogliamo stampare e uno appena oltre l'ultimo elemento:
int j[2] = {0, 1};
// j viene convertito in un puntatore al primo elemento in j
// il secondo argomento è un puntatore a uno oltre la fine di j
stampa(begin(j), end(j)); // funzioni begin e end
Questa funzione è sicura, purché il chiamante calcoli correttamente i puntatori. Qui lasciamo che le funzioni di libreria begin
ed end
forniscano quei puntatori.
Passare Esplicitamente un Parametro Dimensione
Un terzo approccio per gli argomenti array, che è comune nei programmi C e nei programmi C++ più vecchi, è definire un secondo parametro che indica la dimensione dell'array. Usando questo approccio, riscriveremo stampa
come segue:
// const int ia[] è equivalente a const int* ia
// la dimensione viene passata esplicitamente e usata per controllare l'accesso agli elementi di ia
void stampa(const int ia[], size_t dimensione)
{
for (size_t i = 0; i != dimensione; ++i) {
cout << ia[i] << endl;
}
}
Questa versione usa il parametro dimensione
per determinare quanti elementi ci sono da stampare. Quando chiamiamo stampa
, dobbiamo passare questo parametro aggiuntivo:
int j[] = { 0, 1 }; // array int di dimensione 2
stampa(j, end(j) - begin(j));
La funzione viene eseguita in modo sicuro finché la dimensione passata non è maggiore della dimensione effettiva dell'array.
Parametri Array e const
Nota che tutte e tre le versioni della nostra funzione stampa
hanno definito i loro parametri array come puntatori a const
. La discussione nella sezione precedente si applica ugualmente ai puntatori come ai riferimenti. Quando una funzione non ha bisogno di accesso in scrittura agli elementi dell'array, il parametro array dovrebbe essere un puntatore a const
. Un parametro dovrebbe essere un puntatore semplice a un tipo non const
solo se la funzione ha bisogno di cambiare i valori degli elementi.
Parametri Riferimento Array
Proprio come possiamo definire una variabile che è un riferimento a un array, possiamo definire un parametro che è un riferimento a un array. Come al solito, il parametro riferimento è legato all'argomento corrispondente, che in questo caso è un array:
// ok: il parametro è un riferimento a un array; la dimensione fa parte del tipo
void stampa(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}
Le parentesi attorno a &arr
sono necessarie:
f(int &arr[10]) // errore: dichiara arr come un array di riferimenti
f(int (&arr)[10]) // ok: arr è un riferimento a un array di dieci int
Poiché la dimensione di un array fa parte del suo tipo, è sicuro fare affidamento sulla dimensione nel corpo della funzione. Tuttavia, il fatto che la dimensione fa parte del tipo limita l'utilità di questa versione di stampa
. Possiamo chiamare questa funzione solo per un array di esattamente dieci int
:
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
stampa(&i); // errore: l'argomento non è un array di dieci int
stampa(j); // errore: l'argomento non è un array di dieci int
stampa(k); // ok: l'argomento è un array di dieci int
Vedremo in seguito come potremmo scrivere questa funzione in un modo che ci permetterebbe di passare un parametro riferimento a un array di qualsiasi dimensione.
Passare un Array Multidimensionale
Ricorda che non ci sono array multidimensionali in C++. Invece, ciò che sembra essere un array multidimensionale è un array di array.
Come con qualsiasi array, un array multidimensionale viene passato come puntatore al suo primo elemento. Poiché stiamo trattando con un array di array, quell'elemento è un array, quindi il puntatore è un puntatore a un array. La dimensione della seconda (e di qualsiasi successiva) dimensione fa parte del tipo di elemento e deve essere specificata:
// matrice punta al primo elemento in un array i cui elementi sono array di dieci int
void stampa(int (*matrice)[10], int dim_riga) { /* ... */ }
dichiara matrice
come un puntatore a un array di dieci int
.
Di nuovo, le parentesi attorno a *matrice
sono necessarie:
int *matrice[10]; // errore: dichiara arr come un array di riferimenti
int (*matrice)[10]; // ok: arr è un riferimento a un array di dieci int
Possiamo anche definire la nostra funzione usando la sintassi array. Come al solito, il compilatore ignora la prima dimensione, quindi è meglio non includerla:
// definizione equivalente
void stampa(int matrice[][10], int dim_riga) { /* ... */ }
dichiara matrice
come ciò che sembra un array bidimensionale. In realtà, il parametro è un puntatore a un array di dieci int
.
Esercizi
-
Scrivi una funzione che prende un
int
e un puntatore a unint
e restituisce il maggiore del valoreint
o del valore a cui punta il puntatore. Quale tipo dovresti usare per il puntatore?Soluzione:
#include <iostream> using namespace std; int maggiore(int val, const int* ptr) { if (ptr) { // controlla se il puntatore non è nullo return (val > *ptr) ? val : *ptr; } return val; // se il puntatore è nullo, restituisce val } int main() { int a = 10; int b = 20; cout << "Maggiore tra " << a << " e " << b << " è: " << maggiore(a, &b) << endl; cout << "Maggiore tra " << a << " e nullptr è: " << maggiore(a, nullptr) << endl; return 0; }
In questo caso, il puntatore dovrebbe essere di tipo
const int *
perché la funzione non ha bisogno di modificare il valore a cui punta il puntatore. -
Scrivi una funzione per scambiare due puntatori
int
.Soluzione:
#include <iostream> using namespace std; void scambia(int*& p1, int*& p2) { int* temp = p1; p1 = p2; p2 = temp; } int main() { int a = 10, b = 20; int* ptrA = &a; int* ptrB = &b; cout << "Prima dello scambio: *ptrA = " << *ptrA << ", *ptrB = " << *ptrB << endl; scambia(ptrA, ptrB); cout << "Dopo lo scambio: *ptrA = " << *ptrA << ", *ptrB = " << *ptrB << endl; return 0; }
-
Spiega il comportamento della seguente funzione. Se ci sono problemi nel codice, spiega quali sono e come potresti correggerli.
void stampa(const int ia[10]) { for (size_t i = 0; i != 10; ++i) cout << ia[i] << endl; }
Risposta: La funzione
stampa
prende un parametro che sembra essere un array di dieciint
, ma in realtà è un puntatore aconst int
. Il problema principale con questa funzione è che non c'è alcun controllo sulla dimensione effettiva dell'array passato. Se l'array passato ha meno di dieci elementi, la funzione tenterà di accedere a memoria non valida, causando un comportamento indefinito. Per correggere questo problema, si potrebbe passare un secondo parametro che indica la dimensione effettiva dell'array:void stampa(const int* ia, size_t dimensione) { for (size_t i = 0; i != dimensione; ++i) cout << ia[i] << endl; }
main
: Gestione delle Opzioni da Riga di Comando
La funzione main
è un buon esempio di come i programmi C++ passano array alle funzioni. Fino ad ora, abbiamo definito main
con una lista di parametri vuota:
int main() { ... }
Tuttavia, a volte abbiamo bisogno di passare argomenti a main
. L'uso più comune degli argomenti a main
è lasciare che l'utente specifichi un insieme di opzioni per guidare l'operazione del programma. Ad esempio, supponendo che il nostro programma principale sia in un file eseguibile chiamato prog
, potremmo passare opzioni al programma come segue:
prog -d -o file data0
Tali opzioni da riga di comando vengono passate a main
in due parametri (opzionali):
int main(int argc, char *argv[]) { ... }
Il secondo parametro, argv
, è un array di puntatori a stringhe di caratteri in stile C. Il primo parametro, argc
, passa il numero di stringhe in quell'array. Poiché il secondo parametro è un array, potremmo alternativamente definire main
come
int main(int argc, char **argv) { ... }
indicando che argv
punta a un char*
.
Quando gli argomenti vengono passati a main
, il primo elemento in argv
punta al nome del programma o alla stringa vuota. Gli elementi successivi passano gli argomenti forniti sulla riga di comando. L'elemento appena oltre l'ultimo puntatore è garantito essere 0.
Data la precedente riga di comando, argc
sarebbe 5, e argv
conterrebbe le seguenti stringhe di caratteri in stile C:
argv[0] = "prog"; // o argv[0] potrebbe puntare a una stringa vuota
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "file";
argv[4] = "data0";
argv[5] = 0;
Quando si usano gli argomenti in argv
, è importante ricordare che gli argomenti opzionali iniziano da argv[1]
e non da argv[0]
. Infatti, argv[0]
contiene il nome del programma, che di solito non è un argomento utile per il programma.
Esercizi
-
Scrivi una funzione
main
che prende due argomenti. Concatena gli argomenti forniti e stampa la stringa risultante.Soluzione:
#include <iostream> #include <string> int main(int argc, char *argv[]) { if (argc != 3) { std::cerr << "Uso: " << argv[0] << " arg1 arg2" << std::endl; return 1; } std::string risultato = std::string(argv[1]) + std::string(argv[2]); std::cout << "Stringa risultante: " << risultato << std::endl; return 0; }
-
Scrivi un programma che accetta le opzioni presentate in questa sezione. Stampa i valori degli argomenti passati a
main
:prog -d -o file data0
Soluzione:
#include <iostream> int main(int argc, char *argv[]) { if (argc < 3) { std::cerr << "Uso: " << argv[0] << " [-d] [-o] file data0" << std::endl; return 1; } // I parametri -d e -o sono opzionali // Invece, ofile e datafile sono obbligatori std::cout << "Nome del programma: " << argv[0] << std::endl; std::string ofile; std::string datafile; bool debug = false; bool output = false; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "-d") { std::cout << "Opzione debug attivata" << std::endl; debug = true; } else if (arg == "-o") { std::cout << "Opzione output attivata" << std::endl; output = true; } else if (ofile.empty()) { ofile = arg; } else { datafile = arg; } } std::cout << "File di output: " << ofile << std::endl; std::cout << "File di dati: " << datafile << std::endl; /* ... resto del programma ... */ return 0; }
Funzioni con Parametri Variabili
A volte non sappiamo in anticipo quanti argomenti dobbiamo passare a una funzione. Ad esempio, potremmo voler scrivere una routine per stampare messaggi di errore generati dal nostro programma. Vorremmo usare una singola funzione per stampare questi messaggi di errore per gestirli in modo uniforme. Tuttavia, chiamate diverse alla nostra funzione di stampa degli errori potrebbero passare argomenti diversi, corrispondenti a diversi tipi di messaggi di errore.
A partire dallo standard C++11, ci sono due modi principali per scrivere una funzione che prende un numero variabile di argomenti: Se tutti gli argomenti hanno lo stesso tipo, possiamo passare un tipo di libreria chiamato initializer_list
. Se i tipi degli argomenti variano, possiamo scrivere un tipo speciale di funzione, nota come template variadico, che tratteremo in seguito.
C++ ha anche un tipo di parametro speciale, ellissi, che può essere usato per passare un numero variabile di argomenti. Daremo un'occhiata breve ai parametri ellissi in questa sezione. Tuttavia, vale la pena notare che questa facility dovrebbe ordinariamente essere usata solo in programmi che hanno bisogno di interfacciarsi con funzioni C.
Parametri initializer_list
Possiamo scrivere una funzione che prende un numero sconosciuto di argomenti di un singolo tipo usando un parametro initializer_list
. Un initializer_list
è un tipo di libreria che rappresenta un array di valori del tipo specificato. Questo tipo è definito nell'header initializer_list
. Le operazioni che initializer_list
fornisce sono elencate nella tabella che segue
Operazione | Descrizione |
---|---|
initializer_list<T> lst; |
Inizializzazione per default; una lista vuota di elementi di tipo T . |
initializer_list<T> lst{a,b,c}; |
lst ha tanti elementi quanti sono gli inizializzatori; gli elementi sono copie degli inizializzatori corrispondenti. Gli elementi nella lista sono const . |
lst2(lst) , lst2 = lst |
Copiare o assegnare un initializer_list non copia gli elementi nella lista. Dopo la copia, l'originale e la copia condividono gli elementi. |
lst.size() |
Numero di elementi nella lista. |
lst.begin() , lst.end() |
Restituisce un puntatore al primo e uno oltre l'ultimo elemento in lst . |
Come un vector
, initializer_list
è un tipo template. Quando definiamo un initializer_list
, dobbiamo specificare il tipo degli elementi che la lista conterrà:
initializer_list<string> ls; // initializer_list di stringhe
initializer_list<int> li; // initializer_list di int
A differenza di vector
, gli elementi in un initializer_list
sono sempre valori const
; non c'è modo di cambiare il valore di un elemento in un initializer_list
.
Possiamo scrivere la nostra funzione per produrre messaggi di errore da un numero variabile di argomenti come segue:
void msg_errore(initializer_list<string> il)
{
for (auto inizio = il.begin(); inizio != il.end(); ++inizio)
cout << *inizio << " " ;
cout << endl;
}
Le operazioni begin
ed end
sugli oggetti initializer_list
sono analoghe ai corrispondenti membri vector
. Il membro begin()
ci dà un puntatore al primo elemento nella lista, e end()
è un puntatore fuori-dall'intervallo uno oltre l'ultimo elemento. La nostra funzione inizializza inizio
per denotare il primo elemento e itera attraverso ogni elemento nell'initializer_list
. Nel corpo del ciclo dereferenziamo inizio
per accedere all'elemento corrente e stampare il suo valore.
Quando passiamo una sequenza di valori a un parametro initializer_list
, dobbiamo racchiudere la sequenza tra parentesi graffe:
// previsto, effettivo sono stringhe
if (previsto != effettivo)
msg_errore({"funzioneX", previsto, effettivo});
else
msg_errore({"funzioneX", "ok"});
Qui stiamo chiamando la stessa funzione, msg_errore
, passando tre valori nella prima chiamata e due valori nella seconda.
Una funzione con un parametro initializer_list
può avere anche altri parametri. Ad esempio, il nostro sistema di debug potrebbe avere una classe, chiamata CodiceErr
, che rappresenta vari tipi di errori. Possiamo rivedere il nostro programma per prendere un CodiceErr
in aggiunta a un initializer_list
come segue:
void msg_errore(CodiceErr e, initializer_list<string> il)
{
cout << e.msg() << ": ";
for (const auto &elem : il)
cout << elem << " " ;
cout << endl;
}
Poiché initializer_list
ha membri begin
ed end
, possiamo usare un for
a intervallo per processare gli elementi. Questo programma, come la nostra versione precedente, itera un elemento alla volta attraverso la lista di valori tra parentesi graffe passata al parametro il
.
Per chiamare questa versione, dobbiamo rivedere le nostre chiamate per passare un argomento CodiceErr
:
if (previsto != effettivo)
msg_errore(CodiceErr(42), {"funzioneX", previsto, effettivo});
else
msg_errore(CodiceErr(0), {"funzioneX", "ok"});
Parametri Ellissi
I parametri ellissi sono stati introdotti in C++ per permettere ai programmi di interfacciarsi con codice C che usa una facility della libreria C chiamata varargs. Generalmente un parametro ellissi non dovrebbe essere usato per altri scopi.
I parametri ellissi dovrebbero essere usati solo per tipi che sono comuni sia a C che a C++. In particolare, gli oggetti della maggior parte dei tipi di classe non vengono copiati correttamente quando passati a un parametro ellissi.
Un parametro ellissi può apparire solo come ultimo elemento in una lista di parametri e può assumere una delle due forme:
void foo(lista_parm, ...);
void foo(...);
La prima forma specifica il tipo/i per alcuni dei parametri di foo
. Gli argomenti che corrispondono ai parametri specificati vengono controllati per tipo come al solito. Nessun controllo di tipo viene fatto per gli argomenti che corrispondono al parametro ellissi. In questa prima forma, la virgola che segue le dichiarazioni dei parametri è opzionale.
Esercizi
-
Scrivi una funzione che prende un
initializer_list<int>
e produce la somma degli elementi nella lista.Soluzione:
#include <iostream> #include <initializer_list> int somma(const std::initializer_list<int>& il) { int totale = 0; for (int elem : il) { totale += elem; } return totale; } int main() { std::cout << "Somma: " << somma({1, 2, 3, 4, 5}) << std::endl; // Output: Somma: 15 return 0; }
-
Nella seconda versione di
msg_errore
che ha un parametroCodiceErr
, qual è il tipo dielem
nel ciclofor
?Risposta: Il tipo di
elem
nel ciclofor
èconst std::string&
. Questo perchéinitializer_list<string>
contiene elementi di tipostd::string
, e usandoconst auto&
nel ciclofor
,elem
viene dedotto come un riferimento costante a ciascun elemento della lista. -
Quando usi un
initializer_list
in unfor
a intervallo useresti mai un riferimento come variabile di controllo del ciclo? Se sì, perché? Se no, perché no?Risposta: Sì, è consigliabile usare un riferimento (preferibilmente
const auto&
) come variabile di controllo del ciclo quando si itera su uninitializer_list
. Questo perché gli elementi in uninitializer_list
sono di tipoconst
, e usare un riferimento evita la copia degli elementi, migliorando l'efficienza. Inoltre, poiché gli elementi sonoconst
, non c'è rischio di modificarli accidentalmente durante l'iterazione.