In generale, è utile suddividere l'algoritmo in parti bene definite,
e codificare ciascuna di esse mediante una funzione dedicata; ciò
può rivelarsi particolarmente opportuno soprattutto per quelle parti
di elaborazione che devono essere ripetute più volte su dati differenti.
La ripetitività non è però l'unico criterio che
conduce ad individuare porzioni di codice atte ad essere racchiuse in funzioni:
l'importante, come si è accennato, è isolare compiti logicamente
indipendenti dal resto del programma; è infatti usuale, in C++,
definire funzioni che nel corso dell'esecuzione vengono chiamate una volta
sola. Di solito si fa in modo che una funzione svolga un unico compito,
almeno quando occorre svolgere nuovamente quel dato compito basta richiamare
la funzione.
Fino ad ora i programmi sono stati visti come una sequenza di istruzioni
all'interno della funzione main(), ma in effetti i comandi printf() e scanf()
non sono altro che chiamate di funzioni esterne al blocco di istruzioni
main(), passandogli degli argomenti.
Ad esempio:
printf("Ciao Ciao!");
chiama una funzione ( subroutine o sottoprogramma), già pronta,
che si occupa di stampare sul video i caratteri "Ciao Ciao!" passati come
argomento.
Il C++ permette non solo di utilizzare funzioni già esistenti
ma anche di crearne nuove.
Vediamo il formato di una funzione:
<TipoRitorno>
<Ident.Funzione>(<Lista tipo e nome argomenti>)
{
<corpo della funzione>;
}
A tale proposito si ricorda che main() essendo
una funzione, senza argomenti (parametri e valore restituito), può
essere dichiarata senza specificare il tipo di ritorno ed il tipo dei parametri,
e nel corpo può essere omesso return.
#include <stdio.h>
int
somma(int x,int y)
{
return(x+y);
}
int
main(void) /* Funzione principale, eseguita per prima */
{
/* Inizio della funzione main() */
int a,b,c; /* Definiamo 3 variabili intere */
printf("\nDammi
un numero: ");
scanf("%d",&a);
printf("\nDammi
un altro numero: ");
scanf("%d",&b);
c
= somma(a,b); /* Chiamata della funzione!
a,b parametri attuali*/
printf("\nLa
somma dei due è %d\n", c);
return(0);
/* la funzione main restituisce uno 0 intero */
}
/* Fine della funzione main() */
#include <stdio.h>
/* Dichiarazione anticipata (prototipo) della funzione */
int
somma(int,int);
int
main(void)
{
int
a,b,c; /* Definiamo 3 variabili intere */
printf("\nDammi
un numero: ");
scanf("%d",&a);
printf("\nDammi
un'altro numero: ");
scanf("%d",&b);
c
= somma(a,b); /* Chiamata della funzione! */
printf("\nLa
somma dei due è %d\n", c);
return(0);
/* la funzione main restituisce uno 0 intero */
}
/* Fine della funzione main() */
int
somma(int x,int y)
{
return(x+y);
}
Tale dichiarazione è sufficiente al compilatore per verificare che le chiamate a somma(a,b) siano eseguite correttamente.
I prototipi sono inoltre l'unico strumento disponibile per comunicare
al compilatore i nomi delle funzioni di libreria richiamate nei sorgenti:
infatti, essendo disponibili sotto forma di codice oggetto precompilato,
esse non vengono mai definite.
La direttiva #include in testa al codice dell'esempio presentato, che
determina l'inclusione nel sorgente del file stdio.h ha proprio la
finalità di rendere disponibili al compilatore i prototipi delle
funzioni di libreria printf, scanf.
All'interno di una funzione si possono anche definire anche delle
variabili LOCALI, ossia utilizzabili solo all'interno della funzione.
Esempio:
int
somma(int x, int y)
{
int risultato;
* Definisco una variabile locale *
risultato=x+y;
return(risultato);
}
Generalmente una funzione richiede dei parametri in ingresso e opzionalmente produce un risultato in uscita ma, comunque, certe funzioni possono non richiedere dati in ingresso e/o non restituire alcun valore in uscita, ad esempio:
Funzione
che non ha parametri né in entrata né in uscita:
void
acapo(void)
{
printf("\n");/* se una funzione non restituisce alcun valore, si può
omettere il return*/
}
Funzione
che ha solo un parametro in entrata:
void stampanumero(int a)
{
printf("%d",a); /* se una funzione non restituisce alcun valore, si può
omettere il return*/
}
Funzione
che ha solo un parametro in uscita :
int legginumero(void)
{
int a; /* serve una variabile locale...*/
scanf("%d",&a);
return(a)
}
Esempio:
#include <stdio.h>
/*Dichiarazione anticipata (prototipo) delle funzioni */
int
somma(int, int);
void
acapo(void);
void
stampanumero(int);
int
legginumero(void);
/* Funzione principale e inizio del programma */
main()
{
int
a,b,c; /* Definiamo 3 variabili intere */
acapo();
printf("Dammi
un numero: ");
a
= legginumero();
acapo();
printf("Dammi
un altro numero: ");
b
= legginumero();
c
= somma(a,b);
acapo();
printf("La
somma dei due e’");
acapo();
stampanumero(c);
}
int
somma(int a,int b)
{
return(a+b);
}
void
acapo(void)
{
printf("\n");/* se una funzione non restituisce
alcun valore, si può omettere il return*/
}
void
stampanumero(int a)
{
printf("%d",a); /* se una funzione non restituisce
alcun valore, si può omettere il return*/
}
int
legginumero(void)
{
int a; /* serve una variabile locale...*/
scanf("%d",&a);
return(a);
}
Si noti il diverso uso tra
La chiamata di una funzione che restituisce un valore di ritorno può essere inserita come operando in qualsiasi espressione o come argomento nella chiamata di un’altra funzione (previo controllo da parte del compilatore che il tipo della funzione sia ammissibile): la chiamata viene eseguita con precedenza rispetto alle altre operazioni e al suo posto viene sostituito il valore di ritorno restituito dalla funzione.
Se invece la funzione é di tipo void (oppure il suo valore di
ritorno non interessa) la chiamata deve assumere la forma di un’istruzione
a sé stante.
Usare le funzioni in un programma così corto può sembrare
controproducente, ma più il programma diventa grande e più
le funzioni divengono utili.
Osservazioni
Esempio di funzione ricorsiva:
#include
<stdio.h>
int
fatt (int);
int
main(void)
{
int
num;
printf("\nDammi
il numero ");
scanf
("%d",&num);
printf("fattoriale
di %d = %d\n",num,fatt(num));
}
int
fatt (int n)
{
if
(n<=1) return (1);
else
return
(n*fatt(n-1));
}
Nei problema di programmazione è sentita la necessità di gestire due particolari categorie di liste caratterizzate da metodi di accesso ben definiti e utilizzate in numerose circostanze:
Esempio:
Argomento 1 passato da B
Variabili locali a C
Indirizzo di rientro in B
La funzione B chiama la funzione C con un argomento
------------------------------
Parametro 1 passato a C
Argomento 1 passato da A
Argomento 2 passato da A
Variabili locali a B
Indirizzo di rientro in A
La funzione A chiama la funzione B con due argomenti
------------------------------
Parametro 1 passato a B
Parametro 2 passato a B
Variabili locali ad A
------------------------------
Si noti che durante l'esecuzione il programma fa riferimento all’ultimo
insieme di dati entrato nello stack, quando una funzione termina, legge
l'indirizzo di ritorno e rimuove il gruppo di dati dallo stack.
Nell'esempio considerato quando C imposta l’indirizzo di rientro in
B rimuove il gruppo di dati dallo stack. La stessa cosa succede
quando il controllo rientra da B nel main : finché quando termina
il main lo stack rimane vuoto.
Ricorsione delle funzioni
Tornando all’esempio precedente, la trasmissione dei parametri attraverso lo stack garantisce che il meccanismo funzioni comunque, sia che B e C siano funzioni diverse, sia che si tratti della stessa funzione (ogni volta va a cercare nello stack l’indirizzo di rientro nella funzione chiamante e quindi non cambia nulla se tale indirizzo si trova all’interno della stessa funzione).
Ne consegue che in C++ le funzioni possono chiamare se stesse (ricorsività delle funzioni). Ovviamente tali funzioni devono sempre contenere un’istruzione di controllo che, se si verificano certe condizioni, ha il compito di interrompere la successione delle chiamate.
Esempio tipico di una funzione chiamata ricorsivamente è quello del calcolo del fattoriale di un numero intero:
int fatt (int n)
{
if (n<=1)
return (1);
<--istr. di controllo
else
return (n*fatt(n-1));
}
alternativamente, cioè senza usare la ricorsione, si produce codice meno compatto:
int fatt(int n)
{
int i = 2, m =1;
while ( i <= n
) m *= i++ ;
return m;
}
La funzione ricorsiva fatt()non necessita di iterazioni, ma include
internamente il calcolo del risultato, infatti
restituisce 1 se il parametro ricevuto è minore o uguale di
1 (cioè vale 0 o 1), mentre in caso contrario il valore restituito
è il prodotto di n per il fattoriale di n-1, pertanto fatt() calcola
il valore da restituire chiamando se stessa e "passandosi" come parametro
il parametro appena ricevuto, diminuito di uno.
Il termine "chiamare se stessa" implica una nuova istanza della funzione,
ovvero la generazione nella memoria stack di una nuova area di lavoro.
La nuova istanza della funzione alloca le proprie variabili locali,
ignorando l'esistenza di quelle dell'istanza precedente, grazie a questo
meccanismo la funzione ha un proprio spazio "riservato" in cui operare.
A volte, però, può essere utile che una istanza conosca qualcosa delle altre: ad esempio un contatore, che consenta di sapere in qualunque istanza a che profondità si sia spinta la ricorsione.
Tale esigenza è soddisfatta dalle variabili static, in quanto esse sono locali alla funzione in cui sono definite, ma comuni a tutte le sue istanze.
Tale dichiarazione è comprensibile se si tiene conto che le variabili statiche sono accessibili solo alla funzione in cui sono definite, ma esistono e conservano il loro valore per tutta la durata del programma.
Infatti quando una funzione è chiamata per la prima volta ed assegna un valore ad una variabile statica, questa mantiene il proprio valore anche in una seconda istanza (e in tutte le successive) della stessa funzione; mentre delle variabili locali è generata una nuova copia in ogni istanza, una variabile static è unica in tutte le istanze e poiché essa esiste e mantiene il proprio valore anche in uscita dalla funzione, ogni istanza può conoscere non solo il valore che tale variabile aveva nell'istanza precedente, ma anche nell'istanza successiva (ovviamente dopo il termine di questa).
Le variabili globali, infine, sono accessibili a tutte le istanze ma,
a differenza di quelle statiche, lo sono anche alle altre funzioni.
#include <stdio.h>
void incrementa()
{
static
int n=10;
n++;
printf("valore
di n = %d\n",n);
return;
}
main
()
{
for
(int i=0; i<3;i++) incrementa();
}
Il programma darà in esecuzione:
valore di n = 11
valore di n = 12
valore di n = 13
In C++ la ricorsione è usata raramente.
Il punto debole dell'approccio ricorsivo alla definizione di un algoritmo
consiste in un utilizzo dello stack più pesante rispetto alla soluzione
iterativa, infatti ogni variabile locale definita in una funzione ricorsiva
è duplicata nello stack per ogni istanza attiva.
E' perciò necessario, onde evitare disastrosi problemi in fase di esecuzione, contenere il numero delle variabili locali (soprattutto se ingombranti es: array), o richiedere al compilatore la generazione di uno stack di maggiori dimensioni.
Per i motivi precedenti è decisamente sconsigliabile definire
array nelle funzioni ricorsive.
(Ad essi, come vedremo successivamente, può essere sostituito
un puntatore, che comporta meno occupazione di spazio in termini di stack).
Visibilità di una variabile
Ambito di azione
Abbiamo visto che, in via del tutto generale, si definisce ambito di
azione (o ambito di visibilità o
scope) l'insieme di istruzioni di programma comprese fra due parentesi
graffe: {....}
Le istruzioni di una funzione devono essere comprese tutte nello stesso
ambito; ciò non esclude che si
possano definire più ambiti innestati l'uno dentro l'altro (ovviamente
il numero di parentesi chiuse
deve bilanciare quello di parentesi aperte, e ogni parentesi chiusa
termina l'ambito iniziato con la
parentesi aperta più interna)
In ogni caso una variabile è visibile al programma e utilizzabile
solo nello stesso ambito in cui è
dichiarata (variabili locali). Se si tenta di utilizzare una variabile
in ambiti diversi da quello in cui è
dichiarata (o in ambiti superiori in caso di più ambiti innestati),
il compilatore non la riconosce.
Il C++ ammette che si usi più volte lo stesso identificatore
purché in ambiti diversi.
Variabili globali
Una variabile è globale, cioè è visibile in tutto
il programma, solo se è dichiarata al di fuori di
qualunque ambito. Le dichiarazioni (con eventuali inizializzazioni)
sono le uniche istruzioni del
linguaggio che possono anche risiedere esternamente all'ambito delle
funzioni.
In caso di concorrenza fra una variabile globale e una locale viene riconosciuta la variabile locale.
Variabili automatiche
Una variabile è detta automatica (o dinamica), se cessa di esistere
non appena il flusso del
programma esce dalla funzione in cui la variabile è definita.
Se il flusso del programma torna nella
funzione, la variabile viene ricreata ex-novo e, in particolare, viene
reinizializzata sempre con lo stesso
valore. Tutte le variabili locali sono, per default, automatiche ("tempo
di vita" limitato all'esecuzione
della funzione).
Variabili statiche
Una variabile è detta statica se il suo "tempo di vita" coincide
con l'intera durata del programma:
quando il flusso del programma torna in una funzione in cui è
definita una variabile statica, ritrova la
variabile come l'aveva lasciata (cioè con lo stesso valore);
ciò significa in particolare che l'istruzione di
dichiarazione (con eventuale annessa inizializzazione) viene eseguita
solo la prima volta.
Per ottenere che una variabile sia statica, bisogna preporre il qualificatore
static nella dichiarazione
della variabile (davanti a tutti gli altri eventuali qualificatori).
Esiste anche, per le variabili
automatiche, il qualificatore auto, ma è inutile in quanto di
default (può essere usato per migliorare
la leggibilità del programma)
Variabili globali
Una variabile locale può essere automatica o statica; una variabile
globale è sempre statica.
Come si vede la dichiarazione di un array è analoga a quella di una variabile, ad eccezione del fatto che il nome dell'array è seguito dal numero di elementi che lo compongono, racchiuso tra parentesi quadre.
Per accedere a ciascuno di essi occorre fare riferimento al nome dell'array seguito da un indice tra parentesi quadre, che ne indica la posizione.
Si noti che se l'array
ha dimensione 4, ovvero contiene 4 elementi, gli indici sono 0,1,2,3.
Infatti gli indici
sono numeri interi non negativi, e vanno da 0 a n-1, dove n è il
numero di elementi.
vett[0]=3; * Assegno al primo elemento il valore 3 *
vett[1]=6; * Assegno al secondo elemento il valore 6 *
vett[2]=2; * Assegno al terzo elemento il valore 2 *
vett[3]=9; * Assegno al quarto elemento il valore 9 *
Abbiamo definito un
array di 4 elementi e gli abbiamo assegnato dei valori. Dato che questi
4 elementi sono variabili di tipo int, gli elementi che abbiamo inserito
sono dei numeri interi.
IMPORTANTISSIMO: Il compilatore non verifica il limite di un array, per cui è possibile superarlo senza segnalazioni di errore, permettendo di scrivere in una qualsiasi locazione di memoria.
Se per esempio dopo la dichiarazione
int vett[4];
scriviamo:
vett[15]=9;
Non appaiono errori di compilazione, ma dove va a finire il 9 che scriviamo?
Per il compilatore,
vett[15] è semplicemente la locazione di memoria che si trova a
30 byte dall'indirizzo al quale l'array è memorizzato (ipotizzando
che ogni int occupi due byte). Che farne, è affare del programmatore.
Pertanto se vett[0]
si trova all'indirizzo 2000, l'ultimo elemento vett[3], si trova all'indirizzo
di memoria 2006, ciò significa che andremmo a scrivere 9 all'indirizzo
2030, e ciò è pericolosissimo, in quanto si rischia di perdere
(sovrascrivendo sulla locazione di memoria individuata) qualche altro dato
importante!
Sarebbe
a questo punto lecito chiedersi come mai il C, che è il linguaggio
più usato per applicazioni importanti, non effettui questi controlli.
In
effetti tali controlli non vengono eseguiti sia perché si suppone
che chi usa il C++ sia consapevole di tali rischi, sia perché un
programma senza controlli risulta più veloce.
E' possibile inizializzare
un array, ossia assegnare un valore ai suoi elementi, al momento della
dichiarazione, in modo analogo alle variabili inizializzate alla dichiarazione.
Esempio:
int vett[]={10,20,30,40};
In questo caso abbiamo
dichiarato ed inizializzato un array di 4 elementi.
#include
<stdio.h>
main()
{
int
vett[4]={10,20,30,40};
/*
Array con tutti gli elementi già inizializzati */
int
a;
for
(a=0; a<4; a++)
printf("\n%d",
vett[a]);;
/* Con questo ciclo inizializziamo l'array vett con i valori 0,1,2,3 */
for
(a=0; a<4; a++) vett[a]=a;
for
(a=0; a<4; a++)
printf("\n%d",
vett[a]);
}
Programma che calcola la media di n numeri immessi da tastiera.
#include
<stdio.h>
main()
{
const
int n=5;
int
num[n];
int
cont;
float
media=0.0;
/*
Con questo ciclo leggiamo i 10 valori da tastiera con degli scanf */
for
(cont=0; cont<n; cont++)
{
printf("\nDammi il numero %d: ",cont);
scanf("%d",&num[cont]); /* Scrivo nell'array */
}
for
(cont=0; cont<n; cont++)
{
printf("\n numero %d: ", num[cont]);
}
/* Con questo ciclo calcolo la somma tra gli n numeri */
for
(cont=0; cont<n; cont++) media=media+num[cont];
/* si poteva scrivere media += num[cont]*/
media
/= (float) n; /* Ossia media=media/n, in forma
abbreviata */
printf("\nLa
media tra i numeri introdotti è %f.\n",media);
}
Senza gli array avremmo
dovuto definire n variabili distinte!!!!
Array a 2 dimensioni
Un array a 2 dimensioni
si definisce:
<tipo> <nome>[dimensione1] [dimensione2];
e si può considerare come una matrice righe-colonne, o come un array di array monodimensionali.
Come esempio, definiamo
un array con 3 righe e 5 colonne, ossia di 3*5=15 elementi:
int
tab[3][5];
In questo modo definiamo
una tabella di elementi del genere:
tab [0][0] tab [0][1]
tab [0][2] tab [0][3] tab [0][4]
tab [1][0] tab [1][1]
tab [1][2] tab [1][3] tab [1][4]
tab [2][0] tab [2][1]
tab [2][2] tab [2][3] tab [2][4]
Nella coppia di indici
il primoè quello che corrisponde alle righe, mentre il secondo corrisponde
alle colonne.
Dato che gli
array bidimensionali hanno 2 dimensioni, un solo ciclo non basta per gestirli,
ma saranno necessari due cicli nidificati (un ciclo è nidificato
quando si trova all'interno di un altro ciclo), ad esempio questi
due cicli:
for (i=0; i<3; i++) /* ciclo
esterno per gestire le righe */
{
for (j=0; j<5; j++) tab[i][j]=0;
/* ciclo interno che gestisce una riga (tutte le colonne)*/
}
inizializzano a zero
l'array tab.
Il ciclo for più
interno, la prima volta che è eseguito, ha il primo indice i impostato
a 0 mentre j varia da 0 a 4:
[0][0] [0][1]
[0][2] [0][3] [0][4]
La seconda volta
che è eseguito, i vale 1:
[1][0] [1][1]
[1][2] [1][3] [1][4]
e così via, finché il ciclo esterno non termina con i=3.
Per inizializzare un
array bidimensionale, durante la dichiarazione basta elencare tra parentesi
graffe gli elementi separati da virgola. Per maggior chiarezza si può
adottare una diversa la forma :
int prova[4][2] = {
4,5
8,3
2,2
9,1 };
che rende più
chiara la posizione di righe e colonne .
Oppure innestaretanti
gruppi di parentesi graffe quante sono le singole porzioni monodimensionali
dell'array ed elencare gli elementi nello stesso ordine in cui saranno
memorizzati.
int prova[4][2] = {
{4,5 },
{8,3 },
{2,2 },
{9,1} };
/*
Con questi 2 cicli nidificati creiamo la tabellina pitagorica, ossia diamo
come valore di ogni elemento dell'array la moltiplicazione del suo (indice
di riga)*(indice di colonna)*/
/*
saltiamo gli indici 0,che non interessano. */
for
(i=1; i<n; i++)
{
for (j=1; j<n; j++)
{
tab[i][j]=i*j;
}
}
for
(i=1; i<n; i++)
{
for (j=1; j<n; j++)
{
printf("%4d",tab[i][j]);
}
printf("\n"); /* a capo ad ogni incremento di i */
}
}
timeelapsed% a.out
1
2 3 4 5
6 7 8 9 10
11 12
2
4 6 8 10 12 14
16 18 20 22 24
3
6 9 12 15 18 21 24
27 30 33 36
4
8 12 16 20 24 28 32 36
40 44 48
5
10 15 20 25 30 35 40 45
50 55 60
6
12 18 24 30 36 42 48 54
60 66 72
7
14 21 28 35 42 49 56 63
70 77 84
8
16 24 32 40 48 56 64 72
80 88 96
9
18 27 36 45 54 63 72 81
90 99 108
10 20
30 40 50 60 70 80 90 100 110 120
11 22
33 44 55 66 77 88 99 110 121 132
12 24
36 48 60 72 84 96 108 120 132 144
Si noti che nel printf,
al posto del solito %d, c'è %4d.
In questo modo, dichiariamo
che vogliamo stampare sempre e solo 4 caratteri per ogni numero; questo
per allineare la tabella, che altrimenti con il solo %d non lo sarebbe
per la differente lunghezza dei numeri ad una cifra rispetto a quelli con
più cifre.
È possibile definire array multidimensionali ad esempio la seguente definizione:
int prova[2][3][4]
si riferisce ad un
array di nome prova a 3 dimensioni di 2*3*4=24 elementi.
Questo si può considerare come un array di array bidimensionali.
Il primo indice potrebbe essere associato al numero di pagine, il secondo
al numero di righe ed il terzo al numero di colonne, pertanto la precedente
definizione riguarda 2 pagine, numerate da 0 a 1 in ciascuna delle quali
si hanno tabelle da 3 righe e 4 colonne.
#include
<stdio.h>
main()
{
const
int n1=2, n2=3, n3=4;
int
i,j,k;
/*
indici per righe, colonne e pagine dell'array */
/*
Tabella di n1 (pagine)*n2 (righe)*n3(colonne) elementi */
int prova [n1][n2][n3] ={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24};
/*
Con questi 3 cicli nidificati stampiamo la tabella creata e inizializzata
*/
for
(k=0; k<n1; k++)
{
for (i=0; i<n2; i++)
{
for (j=0; j<n3; j++)
{
printf("%4d",prova[k][i][j]);
}
printf("\n"); /* a capo ad ogni incremento di i */
}
printf("\n"); /* a capo ad ogni incremento di k */
}
}
timeelapsed%
g++ b.c
timeelapsed%
a.out
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24
Allo stesso modo con l'inizializzazione:
int prova[2][3][4]=
{
{{1,
2, 3, 4 },
{5, 6, 7, 8 },
{9,10,11,12 }},
{{13,14,15,16},
{17,18,19,20},
{21,22,23,24}}
};
si sarebbe ottenuto in output:
timeelapsed%
g++ b.c
timeelapsed%
a.out
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24
Array come parametri di funzioni
Array
singoli o multidimensionali possono essere passati alle funzioni
come parametri.
Quando come argomento di una funzione dobbiamo passare un array
potremmo seguire più strade, ad esempio:
#include
<stdio.h>
float
media(int vett[5])
{
int
i,k=5;
float
sum=0.0;
for
(i = 0; i < k; i++) sum+=vett[i];
return(sum/k);
}
main()
{
const
int n=5;
int
num[n];
int
cont;
/*
Con questo ciclo leggiamo i valori da
tastiera
con degli scanf */
for
(cont=0; cont<n; cont++)
{
printf("\nDammi il numero %d: ",cont);
scanf("%d",&num[cont]);
/* Scrivo nell'array*/
}
for
(cont=0; cont<n; cont++)
{
printf("\n numero %d: ", num[cont]);
}
/*
Con questo ciclo calcolo la somma tra gli n numeri */
printf("\nLa
media tra i numeri introdotti e’ %f.\n",media(num));
}
come si vede la funzione dichiarata agisce solo su un array di 5 elementi,
ma in C++ è anche possibile utilizzare un’altra soluzione:
#include
<stdio.h>
float
media(int vett[])
{
int
i,k=5;
float
sum=0.0;
for
(i = 0; i < k; i++) sum+=vett[i];
return(sum/k);
}
main()
{
const
int n=5;
int
num[n];
int
cont;
for
(cont=0; cont<n; cont++)
{
printf("\nDammi il numero %d: ",cont);
scanf("%d",&num[cont]);
/* Scrivo nell'array*/
}
for
(cont=0; cont<n; cont++)
{
printf("\n numero %d: ", num[cont]);
}
printf("\nLa
media tra i numeri introdotti e’ %f.\n",media(num));
}
ove come si vede la funzione riceve in input un array di cui non è nota la dimensione, ma all’interno di questa abbiamo definito un parametro k che viene impostato a 5 per elaborare l’array.
Per rendere più generale la funzione ovvero adeguarla ad un array
di dimensione qualsiasi conviene utilizzare nella funzione due parametri,
uno che individua la dimensione dell’array e l’altro che indica il nome
dell’array, pertanto:
float
media(int k,int vett[])
{
int
i;
float
sum=0.0;
for
(i = 0; i < k; i++) sum+=vett[i];
return(sum/k);
}
main()
{
const
int n=5;
int
num[n];
int
cont;
/*
Con questo ciclo leggiamo i valori da tastiera con degli scanf */
for
(cont=0; cont<n; cont++)
{
printf("\nDammi il numero %d: ",cont);
scanf("%d",&num[cont]); /* Scrivo nell'array */
}
for
(cont=0; cont<n; cont++)
{
printf("\n numero %d: ", num[cont]);
}
/* Con questo ciclo calcolo la somma tra gli n numeri */
printf("\nLa
media tra i numeri introdotti e’ %f.\n",media(n,num));
}
oppure con il
prototipo:
#include
<stdio.h>
float
media(int k,int vett[]);
main()
{
const
int n=5;
int
num[n];
int
cont;
for
(cont=0; cont<n; cont++)
{
printf("\nDammi il numero %d: ",cont);
scanf("%d",&num[cont]); /* Scrivo nell'array */
}
for
(cont=0; cont<n; cont++)
{
printf("\n numero %d: ", num[cont]);
}
printf("\nLa
media tra i numeri introdotti e’ %f.\n",media(n,num));
}
float
media(int k,int vett[])
{
int
i;
float
sum=0.0;
for
(i = 0; i < k; i++) sum+=vett[i];
return(sum/k);
}
Allo stesso modo si ha che:
void
stampatab(int nr, int nc,int tabel[][n])
{int
i,j;
for
(i=1; i<nr; i++)
{
for (j=1; j<nc; j++)
{
printf("%4d",tabel[i][j]);
}
printf("\n");
}
}
main()
{
int
i,j; /* indici per righe e colonne dell'array */
int tab[n][n]; /* Tabella di n*n elementi */
for
(i=1; i<n; i++)
{
for
(j=1; j<n; j++)
{
tab[i][j]=i*j;
}
}
stampatab(n,
n, tab);
}
In questo esempio
int tabel[][n] dichiara al C++ che tabel e' un array di interi di
dimensioni []* n (ove n è una costante e vale 13).
E' importante notare
che è obbligatorio specificare la seconda dimensione (e le successive
in caso di array multidimensionali) del vettore, ma non necessariamente
la prima dimensione.
Riepilogando, nel
caso di array singoli non e' necessario specificare la dimensione
dell'array nella definizione come parametro della funzione, mentre
nel caso di array multidimensionali si può non specificare
solo la prima dimensione.
In C++ non esistono le stringhe come tipo base, infatti esse sono
definite come array di caratteri.
Ad esempio, la seguente istruzione definisce una stringa di 5
caratteri:
char
str[5];
La stringa si può subito inizializzare al momento della dichiarazione:
char
str[5] = {'C','I','A','O','\0'};
un altro modo è:
char
str[5] = "CIAO";
A differenza dei normali array il compilatore, nel memorizzare una
stringa in un array di caratteri aggiunge automaticamente un NULL ('\0')
dopo l’ultimo carattere in essa memorizzato, pertanto le stringhe sono
array di caratteri che terminano sempre con il carattere NULL.
Si faccia attenzione a non confondere ‘k’ con “k” infatti mentre la prima notazione rappresenta il carattere k la seconda denota la stringa con contenuto k, che deve essere seguita dal carattre \0, pertanto ha lunghezza 2.
Poiché una stringa di caratteri deve terminare con
NULL, occorre riservare un carattere tale carattere, per esempio
se devo memorizzare una stringa è di 4 caratteri, come "CIAO",
occorre dichiarare un array lungo 5, infatti:
char str[4]="CIAO";
/* non c'è posto per lo zero finale */
char prova[5]="CIAO";
/* c'è posto per lo zero finale*/
In questo modo, se dichiariamo una stringa di 50 caratteri:
char nome[50]= “Mario”;
il suo contenuto (a partire da sinistra) sarà la parola
Mario immediatamente seguito dal carattere di fine stringa '\0',
e quindi tutti gli altri caratteri (fino ad arrivare alla lunghezza
di 50) risulteranno vuoti.
Comunque, dato che alle volte può essere noioso contare quanto sono lunghe le stringhe dell'inizializzazione, si può omettere il numero tra le parentesi quadre, e l'array sarà inizializzato automaticamente della lunghezza giusta.
In questo caso:
char prova[]="Prova di inserimento";
la lunghezza dell'array sarà 21.
Per stampare una <stringa> basta scrivere:
printf(<stringa>);
In questo caso:
char prova[]="Prova di inserimento";
printf(prova);
Se invece si vuol stampare un singolo carattere, e non l'intera stringa,
si deve usare il %c, ad esempio:
printf("\nIl primo carattere
è %c\n", prova[0]);
In questo modo possiamo utilizzare l'indice per indicare quale carattere
della stringa vogliamo stampare.
Esempio
#include <stdio.h>
main()
{
char
prova[]="Prova di inserimento";
printf(prova);
}
Il C++ però non ha un sistema maneggevole per costruire le stringhe, infatti, data la definizione:
const int c1=5;
char vett[c1]; /* spazio
per c1-1 caratteri */
le seguenti assegnazioni, come mostano i programmi di seguito riportati, non sono valide:
vett=”BBBBB”;
vett=”BB\0”;
#include
<stdio.h>
main()
{
const
int c1=5;
char
vett[c1]; /* spazio per c1-1
caratteri */
int
i;
printf("Inserire
la stringa\n");
for
(i=0;i<c1;i++) scanf("%c", &vett[i]);
for
(i=0;i<c1;i++) printf("%c ", vett[i]);
printf("\n");
vett=”BBBBB”;
printf(vett);
}
timeelapsed%
g++ b.c
b.c:
In function `int main()':
b.c:11:
incompatible types in assignment of `char[6]' to `char[5]'
timeelapsed%
#include
<stdio.h>
main()
{
const
int c1=5;
char
vett[c1]; /* spazio per c1-1
caratteri */
int
i;
printf("Inserire
la stringa\n");
for
(i=0;i<c1;i++) scanf("%c", &vett[i]);
for
(i=0;i<c1;i++) printf("%c ", vett[i]);
printf("\n");
vett=”BB\0”;
printf(vett);
}
timeelapsed%
g++ b.c
b.c:
In function `int main()':
b.c:11:
incompatible types in assignment of `char[4]' to `char[5]'
timeelapsed%
mentre risulta valida vett=”BBBB”;
#include
<stdio.h>
main()
{
const
int c1=3;
char
vett[c1]; /* spazio per c1-1
caratteri */
int
i;
printf("Inserire
la stringa\n");
for
(i=0;i<c1+2;i++) scanf("%c", &vett[i]);
for
(i=0;i<c1+2;i++) printf("%c ", vett[i]);
printf("\n");
vett=”BB”;
printf(vett);
}
timeelapsed%
g++ b.c
timeelapsed%
a.out
Inserire
la stringa
prova
p
r o v a
BBtimeelapsed%
Si noti che utilizzando la stringa come un vettore posso uscire
fuori dal range, causando eventuali errori.
Per assegnare da input ad una variabile di
tipo stringa un valore (con numero di caratteri variabile) si usa
la funzione gets.
La funzione gets(), legge una stringa da tastiera e la copia nell'array
di char indicato come argomento (senza indice). La forma è :
gets(<nome_array>);
Vediamo un esempio:
#include
<stdio.h>
main()
{
char
stringa[30];
printf("Scrivi
una stringa di caratteri: ");
gets(stringa);
printf("\nLa
stringa è: %s\n", stringa);
/*
Stampiamo la stringa da sola: */
printf(stringa);
/*
Stampiamo un carattere solamente con il %c */
printf("\nIl
primo carattere è %c\n", stringa[0]);
}
timeelapsed% a.out
Scrivi una stringa di caratteri: sto provando ad inserire!
La stringa è: sto provando ad inserire!
sto provando ad inserire!
Il primo carattere è s
Si noti che se si immette una stringa più lunga
di 30 caratteri, ovvero si supera la fine del vettore, si può,
sovrascrivendo qualche dato, causare dei malfunzionamenti.
Un esempio della potenza degli array di char per la gestione
delle stringhe è questo programma che stampa al contrario una stringa
immessa da tastiera.
Data una stringa di dimensione nota n, per invertirla
basterebbe tramite un ciclo for partire dall'ultimo carattere della stringa
e stamparlo, poi stampare il penultimo e così via, fino al primo:
for(i=n ; i>=0 ; i--) printf("%c", stringa[i]);
Ma se facciamo immettere la stringa da tastiera, non potendo sapere
quanto sarà lunga la stringa immessa le cose si complicano.
Però in C++ esiste una funzione standard, chiamata strlen(),
che ci può dire quanto è lunga una stringa, ossia dopo quanti
caratteri si trova il carattere NULL, che segna la fine della stringa stessa.
La sintassi è:
strlen(<nomestringa>)
Per poter utilizzare questa funzione però
occorre includere la libreria <string.h>.
#include
<stdio.h>
#include
<string.h> /* Libreria con le funzioni per le stringhe */
main()
{
char
stringa[80];
int
i; /* variabile intera che ci servirà come indice */
printf("\nInserisci
la stringa da invertire: ");
gets(stringa);
for(i=strlen(stringa)-1
; i>=0 ; i--) printf("%c", stringa[i]);
printf("\n");
return(0);
}
timeelapsed% g++ b.c
timeelapsed% a.out
Inserisci la stringa da invertire: prova di inversione!
!enoisrevni id avorp
timeelapsed%
Si noti il -1 nella istruzione for, messo appositamente per
evitare di stampare per primo il carattere di terminazione.
Funzioni di manipolazione
delle stringhe:
| File string.h | strcpy(s1,s2) | copia s2 in s1 |
| strcat(s1,s2) | concatena nell'ordine s1s2 | |
| strlen(s1) | restituisce la lunghezza di s1 | |
| strcmp(s1,s2) | restituisce
x:
x=0 se s1=s2 x<0 se s1<s2 x>0 se s1>s2 |
|
| strchr(s1,ch) | restituisce un puntatore alla prima occorrenza del carattere ch | |
| strstr(s1,s2) | restituisce un puntatore alla prima comparsa della sottostringa s2 in s1 | |
| File stdio.h | gets(s1) | legge da tastiera i caratteri e li assegna all'array puntato da s1 |
Esempio
#
include "stdio"
#
include "string.h"
main()
{
char s1[80],s2[80];
gets(s1);
gets(s2);
printf("lunghezze:%d %d\n", strlen(s1),strlen(s2));
strcat(s1,s2);
printf("%s\n",s1);
strcpy(s1,"programma di prova";
printf("%s\n",s1);
}
timeelapsed%
a.out
per
programmare in C++
bisogna
studiare!
lunghezze:22
17
per
programmare in C++bisogna studiare!
programma
di prova
timeelapsed%
STRUCT
Viste le variabili, le stringhe e gli array, possiamo introdurre un nuovo tipo di dati, le STRUTTURE, che rappresentano un insieme di dati non omogeneo.
In altri termini, in C++ una struttura è un insieme di variabili
definito da un unico nome.
Una struttura è utile quando dobbiamo gestire un gruppo di dati
diversi (ad esempio l'array di caratteri che contiene il nome, quello
che contiene il cognome ecc.) uniti tra loro da una relazione (insieme
formano il dato indirizzo).
La parola chiave per la dichiarazione di una struttura è "struct",
struct
indirizzo
{
char nome[30];
char via[60];
char citta[20];
char stato[2];
int cap;
};
Si noti che all'interno della struttura ci possono essere tipi di dati
diversi (array char, int, ecc.).
Questo tipo di dichiarazione, in questa forma, non genera variabili, ma un modello di struttura chiamato "indirizzo".
Per creare fisicamente una struttura del tipo descritto, occorre scrivere:
struct
indirizzo indir1;
È possibile creare subito uno o più esemplari di strutture, al momento della dichiarazione, mettendo i nomi delle stesse separati da virgola, dopo la parentesi graffa chiusa.
struct indirizzo {
char nome[30];
char via[60];
char citta[20];
char stato[2];
int cap;
} indir1, indir2, indir3;
In questo modo, oltre a dichiarare la forma della struttura indirizzo, ne abbiamo creati subito 3 esemplari: indir1, indir2 e indir3.
Se si volesse creare una sola struttura, potremmo omettere l’identificatore della struttura e scrivere:
struct
{
char nome[30];
char via[60];
char citta[20];
char stato[2];
int cap;
} indirizzo;
I campi elementari della struttura sono individuati tramite l’operatore
punto ".", che si mette tra il nome della struttura e il nome dell'elemento,
ad es.:
indirizzo.cap = 55100;
In altri termini, il nome della struttura, seguito da un punto e dal
nome dell'elemento, è un riferimento a quel singolo elemento della
struttura.
ed accedere ai singoli elementi degli array all'interno della struttura:
indirizzo.nome[0]
= "P";
indirizzo.nome[1] = "r";
indirizzo.nome[2] = "o";
indirizzo.nome[3] = "v";
indirizzo.nome[4] = "a";
#include <stdio.h> /* Includiamo la libreria standard */
struct
anagrafico { /* Definiamo e creiamo una struttura
*/
char nome[30];
char via[60];
char citta[20];
};
main()
{
struct
anagrafico anag;
/*
dichiariamo una variabile di tipo anag */
printf("Scrivi il tuo nome: ");
gets(anag.nome);
printf("Scrivi la via: ");
gets(anag.via);
printf("Città: ");
gets(anag.citta);
/* Stampiamo le stringhe dalla struttura */
printf("\n\nTi chiami %s", anag.nome);
printf("\abiti a %s", anag.citta);
printf("\n in via %s", anag.via);
printf("\n\nE Il tuo nome inizia per %c.\n", anag.nome[0]);
}
Le strutture possano essere utili, se riunite in array o scritte in un file.
Per definire un array di strutture è necessario definire una struttura e poi dichiarare un array di quel tipo.
Ad es.:
struct anagrafico
{
char nome[30];
char via[60];
char citta[20];
};
struct anagrafico agenda[100];
Come si vede, con l'ultima istruzione abbiamo creato un array (agenda)
di 100 elementi di tipo anagrafico.
Per stampare i campi nome e citta del quarto elemento dell'agenda, si
può fare così:
printf("%s", agenda[3].nome); * Stampa il nome
printf("%s", agenda[3].citta); * Stampa la città
Esempio:
#include <stdio.h> /* Includiamo la libreria standard */
struct
anagrafico { /* Definiamo e creiamo una struttura
*/
char nome[30];
char via[60];
};
main()
{
const
n=3;
int
i;
struct
anagrafico agenda[n];
for
(i=0; i<n; i++)
{
printf("Scrivi
il tuo nome: ");
gets(agenda[i].nome);
printf("Scrivi la via: ");
gets(agenda[i].via);
}
/*
Stampiamo le stringhe dalla struttura */
for
(i=0; i<n; i++)
{
printf("\n\nTi chiami %s", agenda[i].nome);
printf("\n abiti in via %s", agenda[i].via);
}
}
Esempio
#include
<stdio.h> /* Includiamo la libreria standard */
struct
datar { /* Definiamo e creiamo una struttura */
int gg,mm,aa;
};
struct
anagrafico { /* Definiamo e creiamo una struttura
*/
char nome[30];
char via[60];
char citta[20];
datar d;
};
main()
{
struct
anagrafico anag1,anag2;
/*
dichiariamo una variabile di tipo anag */
printf("Scrivi il tuo nome: ");
gets(anag1.nome);
printf("Scrivi la via: ");
gets(anag1.via);
printf("Città: ");
gets(anag1.citta);
printf("giorno: ");
scanf(“%d”,&anag1.d.gg);
printf("mese: ");
scanf(“%d”,&anag1.d.mm);
printf("anno: ");
scanf(“%d”,&anag1.d.aa);
/* Stampiamo le stringhe dalla struttura */
printf("\n\nTi chiami %s", anag1.nome);
printf("\n abiti a %s", anag1.citta);
printf("\n in via %s", anag1.via);
printf("\n\nE Il tuo nome inizia per %c.\n", anag1.nome[0]);
printf("\n data %2d\n", anag1.d.gg);
printf("-%2d", anag1.d.mm);
printf("-%2d", anag1.d.aa);
/*
Stampiamo le stringhe dalla struttura */
anag2=anag1;
printf("\n\nTi chiami %s", anag2.nome);
printf("\n abiti a %s", anag2.citta);
printf("\n in via %s", anag2.via);
printf("\n\nE Il tuo nome inizia per %c.\n", anag2.nome[0]);
}