FUNZIONI
 
  Infatti l'esecuzione di un programma inizia proprio con la prima istruzione contenuta nella funzione main(); questa può chiamare altre funzioni, che a loro volta ne possono chiamare altre ancora. L'unico limite è rappresentato dalla quantità di memoria disponibile.

 
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>;
 }
 

Occorre però soffermarsi brevemente sull'istruzione return.
   
 

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() */
 
 
 
 

Esempio:

#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.
 
 
 

 Quando il controllo passa dalla funzione chiamante alla funzione chiamata vengono effettuate automaticamente le seguenti operazioni:
  Al termine dell’esecuzione il valore calcolato dalla funzione viene trasmesso alla funzione chiamante e le variabili locali della funzione chiamata vengono rilasciate (scompaiono).
 
 

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

Le  prime si chiamano per calcolare un valore ( il valore da loro restituito), pertanto saranno presenti in espressioni, istruzioni di assegnamento o di stampa, le seconde invece poiché non generano un valore in output corrispondono alla chiamata di sottoprogrammi (in altri linguaggi vengono chiamate procedure o subroutine).

 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));
}
 



Trasmissione dei parametri tramite l’area stack

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:

Nella trasmissione dei parametri fra funzione chiamante e funzione chiamata vengono utilizzate liste di tipo stack.
Quando  la funzione main viene eseguita  le sue variabili locali vengono allocate in un’area di memoria, detta appunto stack, quando questa chiama un'altra funzione (B) vengono allocati sullo stack  un insieme di dati comprendenti: le variabili locali, gli argomenti (parametri formali della funzione B) e l’indirizzo di rientro nel main  necessari alla funzione B, se anche B a sua volta chiama un’altra funzione C, sistema nell’area stack il proprio insieme di dati  “impilato” sopra il precedente.
 

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.



               Visibilità e tempo di vita di una variabile

 

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.
 

 



 

Array

Un array è un insieme di variabili omogenee a cui è associato un unico nome.
Un array è un insieme di variabili, caratterizzate dall’appartenere tutte allo stesso tipo ( tipo dell’array), che occupano locazioni consecutive di memoria.
Ogni variabile di tale insieme è detta elemento dell’array ed è identificata dalla posizione che occupa rispetto a tutta la struttura  (indice dell’array).
Il numero degli elementi dell’array è predefinito e invariabile, l’indice può assumere i valori fra 0 e il numero degli elementi –1.
   
  <tipo> <nome>[<dimensione>];
 
  <tipo> = tipo di dati di ogni elemento dell'array (int, float...)
  <nome> = identificatore che diamo all'array
  <dimensione> = numero degli elementi che compongono l'array.
 
Esempio:
 
  int vett[4];  * Dichiarazione di un array composto da 4 interi *
 
 

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.
 

 
  <tipo> < nome>[<dimensione>] = {lista argomenti};
 <dimensione> deve essere una costante int.
<lista argomenti> corrisponde ai valori da dare agli elementi dell'array, separati da una virgola. Il primo valore corrisponde al primo elemento, e così di seguito.
  Esempio:
 
  int vett[5]={10,20,30,40,50};
 
In questo caso abbiamo inizializzato tutti gli elementi dell'array.
Attenzione al fatto che se non si inizializza un array, il contenuto dei suoi elementi non è zero, bensì indeterminato (valori  presenti in quel punto della memoria)!

 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}      };
 



Programma che usa un array bidimensionale nxn per creare la tabellina pitagorica:
 
#include <stdio.h>
main()
{
const int n=13;
int i,j;  /* indici per righe e colonne dell'array */
int tab[n][n]; /* Tabella di n*n elementi */

/* 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.



 
                          Array multidimensionali
 

È 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:
 

#include <stdio.h>

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:
 
 

 #include <stdio.h>
const int n=13;

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.
 



 
Array e stringhe
Una stringa è una sequenza di caratteri.

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>.



Pertanto il programma è:

#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.
 

 printf("Il cap è %d", indirizzo.cap);
   
 gets(indirizzo.nome);

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";
 
 



Vediamo ora un semplice programma

#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.



                                             ARRAY DI STRUTTURE

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);

}
 }
 



Si noti che
 
1) Se due variabili struttura sono del medesimo tipo, è possibile assegnare l'una all'altra (ossia copiare l'una nell'altra).
 
2) Gli elementi di una struttura possono essere di qualsiasi tipo di dati valido in C++, inclusi array e strutture. Quindi è possibile creare anche delle STRUTTURE NIDIFICATE.

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]);

}