Puntatori a puntatori
 
I puntatori a puntatori vengono usati quando si ha la necessità di puntare a variabili che sono esse stesse dei puntatori (indirizzamento indiretto).
Ad esempio la dichiarazione di un puntatore ad un puntatore a char è:

char **   ppunt;

In pratica occorre aggiungere un asterisco, in quanto siamo ad un secondo livello di indirezione.
La variabile ppunt  non punta direttamente ad un char ma ad una variabile  *ppunt  che a sua volta punta a char.

  

ESEMPIO
 
#include <iostream.h>

main()
{
int i,j;
char* p;
char**  q;
char x = ’A’;
p=&x;
q=&p;
cout<<**q<<”  =  “<<*p<<”  =  “<<x;
}

timeelapsed% g++ b.c
atimeelapsed% .out
A  =  A  =  A
Generazione dinamica di una matrice di interi di k righe e z colonne

#include <iostream.h>
#include <new.h>
main()
{
int**   mat;     /* variabile puntatore */
int k, i,j,z;
cout<<"\nInserire il numero di righe\n";
cin>>k;
cout<<"\nInserire il numero di colonne\n";
cin>>z;

/* creazione di un array di puntatori (alle righe  della matrice)*/
mat=new  int*  [k];
/* ora posso creare le righe della matrice (di z elementi) */
for (i=0;i<k;i++)
     mat[i] = new int[z];
/* ora posso utilizzare la matrice mat[k][z] */

for (i=0;i<k;i++)
  for (j=0;j<z; j++)
    mat[i][j] = 2*i; /* i primi k numeri pari */
for (i=0;i<k;i++)
  {for (j=0;j<z;j++)
    cout<<" -"<<mat[i][j];
     cout<<endl;
    }
}
 
timeelapsed% a.out

Inserire il numero di righe
4

Inserire il numero di colonne
6
 -0 -0 -0 -0 -0 -0
 -2 -2 -2 -2 -2 -2
 -4 -4 -4 -4 -4 -4
 -6 -6 -6 -6 -6 -6
timeelapsed%



#include <iostream.h>
#include <new.h>

/*funzione che crea dinamicamente una matrice di r righe e c colonne*/
int**  creamat(int r, int c )
{
    int** m;
    int i,j;
     m=new  int*  [r];
    /* ora posso creare le righe */
    for (i=0;i<r;i++)
     m[i] = new int[c];
     return m;
}

void leggimat(int r, int c, int** m )
{
int i,j;
    for (i=0; i<r; i++)
    {
    cout<<”inserire “<<c<<” elementi  riga  “<<i<<endl;
     for (j=0; j<c; j++) cin>> (m[i][j]);
    }
}
 

void stampamat(int r, int c, int** m)
{
    int i,j;
    for (i=0; i<r; i++)
     {
     cout<<endl;
     for (j=0; j<c; j++) {cout<< m[i][j]<<”  “; }
     }
}
 

main()
{
int**   mat;     /* variabile puntatore */
int k, i,j,z;
cout<<"\nInserire il numero di righe\n";
cin>>k;
cout<<"\nInserire il numero di colonne\n";
cin>>z;
mat=creamat(k,z);
leggimat(k,z,mat);
stampamat(k,z,mat);
}
 



ESEMPIO

Programma che calcola il prodotto fra  matrici.
Il prodotto matriciale A*B di due matrici  è la matrice PROD (che ha tante righe quante la matrice A e tante colonne quante
la matrice B)  in cui, per ogni i, j, PROD[i][j] si ottiene come la somma dei prodotti dei valori degli elementi della riga i di
A per i corrispondenti elementi della colonna j di B, ovvero PROD[i][j] = A[i][0]*B[0][j] + A[i][1]*B[1][j] + … +
A[i][4]*B[4][j].
In questo esempio per evitare di scrivere funzioni per ciascuna matrice ci siamo riferiti al tipo mat[c2][c2]

#include <iostream.h>
const int  r1=3;
const int c1=2;
const int c2=4;
typedef int mat1[r1][c1];
typedef int mat2[c1][c2];
typedef int mat3[r1][c2];
typedef int mat[c2][c2];

void matprod(const mat a, const mat b, mat prod)
{
int i, j, k;
/* inizializzazione di c  a 0*/
for (i=0; i < r1; i++)
    for (j=0; j < c2; j++) prod[i][j] = 0;

/* calcolo prodotto prod = a * b */

for (i=0; i < r1; i++)
    for (j=0; j < c2; j++)
        for (k=0; k < c1; k++)
        prod[i][j] = prod[i][j] + a[i][k] * b[k][j];

}

void leggimat(int nr, int nc,  mat m )
{
cout<<”inserire una matrice di  “<<nr<<“  righe  “<<”  e  “<<nc<<”  colonne”<<”\n”;
    int i,j;
    for (i=0; i<nr; i++)
    {
     cout<<”riga “<<i<<”\n”;
    for (j=0; j<nc; j++) cin>> (m[i][j]);
    }
}
 

void stampamat(int nr, int nc,  mat m )
{
    int i,j;
    for (i=0; i<nr; i++)
     {
     cout<<endl;
     for (j=0; j<nc; j++) {cout<< m[i][j]<<”  “; }
     }
}

main()
{
mat m1,m2,m3;
leggimat(r1,c1,m1);
leggimat(c1,c2,m2);
matprod(m1,m2,m3);
cout<<endl;
stampamat(r1,c1,m1);
cout<<endl;
stampamat(c1,c2,m2);
cout<<endl;
stampamat(r1,c2,m3);
cout<<endl;
}
 
 

timeelapsed% a.out
inserire una matrice di  3  righe    e  2  colonne
riga 0
2 3
riga 1
1 1
riga 2
4 5
inserire una matrice di  2  righe    e  4  colonne
riga 0
1 2 3 4
riga 1
1 0 3 4
 

2  3
1  1
4  5

1  2  3  4
1  0  3  4

5  4  15  20
2  2  6  8
9  8  27  36
timeelapsed%
 

ESEMPIO
Uso dei puntatori nella gestione di array bidimensionali, in questo secondo modo utilizzanzo int** puntatori a puntatori posso creare le matrici a dimensioni variabili ed utilizzare funzioni che si adattano alle dimensioni per effettuare le operazioni di lettura scrittura e calcolo del prodotto.

#include <iostream.h>
#include <new.h>
typedef int**  mat;

int**  creamat(int r, int c )
{
    int** m;
    int i,j;
     m=new  int*  [r];
    /* ora posso creare le righe */
    for (i=0;i<r;i++)
     m[i] = new int[c];
     return m;
}
 

void  matprod(int r1, int c1, int c2, const mat a, const mat b, mat prod)
{
int i, j, k;
/* inizializzazione di c  a 0*/
for (i=0; i < r1; i++)
    for (j=0; j < c2; j++) prod[i][j] = 0;

/* calcolo prodotto prod = a * b */

for (i=0; i < r1; i++)
    for (j=0; j < c2; j++)
        for (k=0; k < c1; k++)
        prod[i][j] = prod[i][j] + a[i][k] * b[k][j];
}

void leggimat(int nr, int nc,  mat m )
{
cout<<”inserire una matrice di  “<<nr<<“  righe  “<<”  e  “<<nc<<”  colonne”<<”\n”;
    int i,j;
    for (i=0; i<nr; i++)
    {
     cout<<”riga “<<i<<”\n”;
    for (j=0; j<nc; j++) cin>> (m[i][j]);
    }
}
 

void stampamat(int nr, int nc,  mat m )
{
    int i,j;
    for (i=0; i<nr; i++)
     {
     cout<<endl;
     for (j=0; j<nc; j++) {cout<< m[i][j]<<”  “; }
     }
}

main()
{
mat m1,m2,m3;
int r1,c1,c2, i,j;
cout<<"\nInserire il numero di righe\n";
cin>>r1;
cout<<"\nInserire il numero di colonne\n";
cin>>c1;
m1=creamat(r1,c1);
cout<<"\nInserire il numero di colonne\n";
cin>>c2;
m2=creamat(c1,c2);
m3=creamat(r1,c2);
leggimat(r1,c1,m1);
stampamat(r1,c1,m1);
cout<<endl;
leggimat(c1,c2,m2);
stampamat(c1,c2,m2);
cout<<endl;
matprod(r1,c1,c2,m1,m2,m3);
stampamat(r1,c2,m3);
cout<<endl;
}
 
 


Dichiarazioni typedef

In C++ è possibile definire tramite la parola chiave typedef degli identificatori di tipo che successivamente possono essere usati per dichiarare  altre variabili di quel tipo.
Ad esempio la seguente dichiarazione:
typedef int matrice[4][4];

permette poi di dichiarare le variabili m1 ed m2 riferendosi all'identificatore mat.

ESEMPIO
matrice m1,m2;

In genere questo tipo di definizione migliora la leggibilità e manutenzione dei programmi in quanto è più semplice modificare il tipo delle variabili.




 Puntatori a Funzione
 

I puntatori a funzione si usano quando si vuole passare una funzione come parametro ad un'altra funzione.
Tale necessità nasce quando si vuole sviluppare una funzione che riceve come parametro di input una funzione variabile, ovvero si vogliono sviluppare funzioni generali adatte ad input provenienti da più funzioni specifiche.

Ad esempio se volessimo dichiarare una funzione che calcola la somma dei valori assunti da una sola  funzione es:
f1=x*x
per x che varia tra 0 e n con incrementi di 1, potremmo scrivere il programma:

#include <iostream.h>

double  f1(int n)
{return n*n;};
 
 

double  somma (int n)
{double temp=0.0;
for (int i=0; i<n;i++) temp+=f1(i);
return temp
}

main ()
{
double z;
int k;
cout<<”inserire un valore”;
cin>>k;
z=somma(k);
cout<<z;
}

 Nel caso in cui volessimo estendere il calcolo della somma non solo alla funzione f1 ma anche ad altre funzioni, come ad esempio:

double  f1(int n)
{return n*n;};

double f2(int n)
{return n*n*n;};

allora è possibile dichiarare un altro parametro in input alla funzione somma che è rappresentato da un  puntatore a funzione.

double  somma1 (double (*funz) (int), int n)
{double s=0.0;
for (int i=0; i<n;i++) s+=(*funz)(i);
return s ;
}
 
 

Nel main  il puntatore a funzione deve essere dichiarato e  inizializzato con il nome della  funzione “vera”, che deve essere utilizzata.
 

Ad esempio:
 
if(a==1)
    funvera=f1;
    else
    funvera=f2;
 
 

ESEMPIO

#include <iostream.h>

double  f1(int n)
{return n*n;};

double f2(int n)
{return n*n*n;};

double  somma1 (double (*funz) (int), int n)
{double s=0.0;
for (int i=0; i<n;i++) s+=(*funz)(i);
return s ;
}

main ()
{
double (*funvera) (int);
double z;
int a,k;
cout<<”inserire due  valori”;
cin>>a>>k;
if(a==1)
    funvera=f1;
    else
    funvera=f2;
z=somma1 (funvera,k);
cout<<z;
}
 

Nell'esempio precedente la dichiarazione:

double (*funz) (int);

sta ad indicare che  funz è un puntatore ad una funzione che restituisce un double e accetta come parametro un int.
La sintassi può apparire complessa, ma un esame più approfondito mostra che ciò non è vero infatti  l'asterisco che precede il nome funz indica che si tratta di un puntatore,  la parola chiave double indica il tipo di ritorno, la lista di parametri tra  parentesi tonde contiene i parametri di input.
Restano da spiegare le parentesi che racchiudono *funz: esse sono indispensabili per distinguere la dichiarazione di un puntatore a funzione dalla dichiarazione di  un prototipo di funzione.
Infatti se scrivessimo la stessa  dichiarazione dell'esempio omettendo la prima coppia di parentesi, otterremmo

double*  funz (int i);
 
che corrisponde alla dichiarazione di un prototipo di funzione.



            Ancora sulle Funzioni 

In C++ il passaggio di parametri alle funzioni può avvenire:

 

Il passaggio dei parametri per valore, gia analizzato  in precedenza, permette alle funzioni di restituire un solo parametro di ritorno, di cui è obbligatorio specificare il tipo,   e  non permette di modificare  i parametri attuali  in quanto queste lavorano sui parametri formali che rappresentano una copia dei parametri attuali.

Il passaggio dei parametri per indirizzo e per riferimento permettono alla funzione di restituire  in output dalle funzioni più parametri di ritorno, realizzando ciò che in altri linguaggi si ottiene tramite subroutine o procedure.
Se si vuole che la funzione restituisca più parametri in output occorre che questi siano passati per indirizzo o per riferimento.
Si ricorda che nel caso in cui il parametro per valore è un array il C++ forza automaticamente il passaggio per indirizzo, facendo coincidere l’indirizzo del vettore (che rappresenta il parametro attuale) con l’indirizzo del parametro formale.
Questo fatto consente quindi alla funzione di alterare il contenuo dell'array anche se, è bene sottolinearlo, si usa la copia del puntatore , e non l'area di RAM referenziata.
Quando si vuole evitare che le variabili indirizzabili dai puntatori, passati come parametri alle funzioni siano modificate si può aggiungere la parola riservata const. 



Passaggio dei parametri per indirizzo

Il passaggio dei parametri per indirizzo consente di modificare il contenuto delle variabili puntate dal parametro.

 

ESEMPIO

void incrementa(int *p)
{
(*p)++;

/*
poiché l'argomento è un puntatore a un intero posso incrementare di 1 la variabile a cui punta */
}

main()
{
int i;
printf("Inserire un intero: ");
scanf("%d", &i);
incrementa(&i);         /* occorre passare l'indirizzo! */
printf("Il numero incrementato vale %d\n", i);
}


ESEMPIO

Funzione che scambia due interi.
  #include <iostream.h>
void swap1(int a, int b)

{ /* passati per valore! */
int t;
t = a;
a = b;
b = t;         /* scambio ok ... */
               /* ... ma ho scambiato le copie tutto va perso! */
}
 

main()
{
int x = 4, y = 8;
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
swap1( x, y );          /* in realtà non scambia nulla!! */
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
}

#include <iostream.h>
void swap2(int *a, int *b)      { /* due puntatori */
int t;
t = *a;
*a = *b;
*b = t;         /* scambio ok */
/* ho scambiato gli originali ok! */
}

main(){
int x = 4, y = 8;
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
swap2( &x, &y ); /* occorre passare gli indirizzi */
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
}

Quando si vuole evitare che le variabili a cui si può accedere per mezzo del parametro puntatore vengano mofifiate dalla funzione si deve far precedere  il parametro dalla parola chiave const.



Passaggio di parametri per riferimento

In C++  (a differenza del C ove il passaggio dei parametri  avviene sempre e soltanto per valore) il passaggio dei parametri può avvenire anche per riferimento (by reference).

I riferimenti permettono al programmatore di riferirsi alla stessa variabile con più nomi.

Ad esempio data la seguente dichiarazione:

int x;

è possibile scrivere:

int&   y=x ;

La dichiarazione indica che la variabile y è un riferimento alla variabile x, ovvero l'identificatore y si riferisce alla variabile x, pertanto qualsiasi modifica apportata a y la ritroviamo su x e viceversa in quanto y è un altro nome della variabile x.
Si noti che i riferimenti vanno sempre inizializzati in fase di dichiarazione e non possono essere mai modificati.
Infatti la dichiarazione di un riferimento non comporta la costruzione di una nuova variabile mediante copia, in quanto, per il programma, si tratta sempre della stessa variabile (la differenza fra i nomi “scompare” dopo la compilazione).
 

Quando passiamo come parametro ad una funzione un riferimento ad es:

 funz(int& num)

in questo caso l’argomento è passato by reference, cioè non ne viene costruita una copia, ma la variabile num é un alias di riferimento della sua corrispondente nel programma chiamante. Ne consegue che ogni modifica apportata a num in funz viene effettuata anche nel programma chiamante.

ESEMPIO

#include <iostream.h>
void incrementa(int& p)
{
p++;
/*
poiché l'argomento è un riferimento a un intero posso incrementare di 1 la variabile a cui si riferisce */
}

main()
{
int i;
printf("Inserire un intero: ");
cin>>i;
cout<<i;
incrementa(i);         /* non occorre passare l'indirizzo! */
cout<<"Il numero incrementato vale "<< i;
}
 

ESEMPIO

Funzione che scambia due interi.
 
Versione 3 ( corretta)
 

#include <iostream.h>

void swap3(int& a, int& b)       /* due riferimenti  */

int t;
t = a;
a = b;
b = t;                             /* scambio ok */
}

main()
{
int x = 4, y = 8;
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
swap3( x, y );            /* non occorre passare gli indirizzi */
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
}



 

Parametro di ritorno per riferimento

 In C++ può essere dichiarato per riferimento anche il valore di ritorno di una funzione.
Ad esempio  nella funzione:

int& funz()  { ...... return b; .... }

In questo caso la funzione non restituisce il valore di una variabile (b)  ma la variabile b stessa, il che equivale quindi a modificare il parametro attuale.

Ad esempio:
 
#include <iostream.h>

int& max(int& a, int& b)

return (a>b ? a : b);
}

main()
{
int x = 4, y = 8;
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
cout<<max( x, y )<<"\n";            /* non occorre passare gli indirizzi */
max(x,y)=99;
cout<<"x = "<<x<<"  y=  "<<y<<"\n";
}

In questo programma la funzione max viene chiamata due volte: la prima:

cout<<max( x, y )<<"\n";

per trovare il valore massimo tra x e y e la seconda volta:

max(x,y)=99;

per sostituire al valore massimo trovato il valore 99.
Mentre per il primo tipo di chiamata i parametri reference potrebbero essere eliminati, per il secondo tipo di uso sono indispensabili.
 



Funzioni con argomenti di default

In C++ é consentito dichiarare funzioni il cui comportamento è definito anche quando in una chiamata non tutti i parametri sono specificati.  Ciò è possibile se durante la dichiarazione si associano ai parametri dei valori cosiddetti di default. Ad  esempio:

  int Sum (int a = 0, int b = 0)
{
    return a+b;
  }
 

Nella funzione Sum agli argomenti sono stati associati a dei valori di default (in questo caso 0 per entrambi gli argomenti), pertanto se se la funzione Sum viene chiamata senza specificare il valore di a e/o b il compilatore sostituisce,  al parametro non specificato, il valore di default (0) .
La specifica degli argomenti di default deve essere fatta un’unica volta (e quindi in generale nella dichiarazione del prototipo della funzione, ma non nella sua definizione).

Es.
prototipo:
void scrivi(char saluto[ ] = “Benvenuto”);

chiamata:

scrivi();                  equivale a:  scrive(“Benvenuto”);
scrivi("Ciao");    scrive(“Ciao”);

definizione:

void scrivi(char saluto[ ])  { ............ };

Se una funzione ha diversi argomenti, di cui alcuni di default, è necessario che quelli obbligatori precedano tutti quelli di default.
  



Funzioni con overload

A differenza del C, il C++ consente di dichiarare, con lo stesso nome, più funzioni, che differiscono per il numero e/o per il tipo dei loro argomenti. Queste sono chiamate “Funzioni con overload” (sovrapposizione).

Il compilatore permette ciò in quanto distingue una funzione dall’altra in base alla lista degli argomenti, infatti due funzioni con overload devono differire per il numero e/o per il tipo dei loro argomenti.

Ad esempio le due chiamate:

funz(int a);
funz(float b);

 hanno lo stesso nome funz, ma in realtà si riferiscono a due funzioni diverse, in quanto la prima ha un argomento int, la seconda un argomento float.

Non sono ammesse funzioni con overload che differiscano solo per il tipo del valore di ritorno (in quanto questo può non essere utilizzato); né sono ammesse funzioni che differiscano solo per argomenti di default in quanto,  generano ambiguità.

Es.  void funz(int a);  e   int funz(int b);

non sono accettate, perché differiscono solo per il tipo de parametro di ritorno.

Es. funz(int a);  e   funz(int a, int b=0);

non sono accettate, in quanto differiscono solo per argomenti di default, infatti,  in una chiamata tipo funz(n), il programma non saprebbe se trasferirsi alla prima funzione (che ha un solo argomento), oppure alla seconda (che ha due argomenti, ma il secondo può essere omesso per default).
 

La tecnica dell’overload delle funzioni  e degli operatori é molto usata in C++, perché permette di programmare in modo più semplice.  Infatti è comodo chiamare con lo stesso nome funzioni che eseguono operazioni concettualmente simili su dati diversi.

Ad eccezione dell'operatore %, che agisce solo su dati di tipo int, fornendo come risultato un intero, gli altri operatori + - * / possono essere usati sia con operandi interi, sia con operandi reali. Il C++ infatti a seconda degli operandi attiva operazioni diverse, mantenendo gli stessi operatori.



ESEMPIO

Per sommare tramite funzione 3 coppie di variabili di 3 tipi diversi: int, long, float in C occorrerebbe scrivere 3 funzioni di somma, una per gli int, una per i long, e una per i float:

int sommaint(int a,int b) {...}
long sommalong(long c,long d) {...}
float sommafloat(float e,float f) {...}

e in seguito, nel programma, dovremmo stare attenti ad usare la funzione giusta a seconda del tipo di dati da sommare:

q = sommafloat(f1,f2);
r = sommalong(l1,l2);
z = sommaint(i1,i2);

In C++, invece, grazie all’overloading delle funzioni è possibile scrivere scrivere 3 funzioni diverse dandogli lo stesso nome , purche' abbiano parametri diversi:

int somma(int a,int b) {...}
long somma(long c,long d) {...}
float somma(float a,float b) {...}

In questo modo (facendo una sovrapposizione di funzioni, detta "overload") nel programma possiamo chiamare sempre la stessa funzione somma() per sommare numeri int,long e float, in quanto sara' il compilatore a scegliere la funzione giusta volta per volta, rendendo più semplice il nostro compito.

q = somma(f1,f2);
r = somma(l1,l2);
z = somma(i1,i2);

Questo approccio e' molto migliore dal punto di vista logico, infatti quando programmiamo e sommiamo la variabile a con la variabile b, spesso non ci ricordiamo il tipo di queste ultime, non essendo fondamentale, ma se dobbiamo usare funzioni diverse a seconda del tipo, allora siamo costretti a far entrare nel nostro ragionamento anche questo, facendoci distrarre.
 

#include <iostream.h>
int somma(int a,int b)
{
        return(a+b);
}

long somma(long c,long d)
{
        return(c+d);
}

float somma(float e,float f)
{
        return(e+f);
}
 

int main(void)
{
 int i1 = 15;
 int i2 = 50;

 long l1 = 350000;
 long l2 = 168776000;

 float f1 = 3.024;
 float f2 = 12.264;

// sommiamo coppie di variabili di 3 tipi diversi usando sempre somma()

 cout << i1 << " + " << i2 << " = ";
 cout << somma(i1,i2) << "\n";

 cout <<l1 << " + " << l2 << " = ";
 cout << somma(l1,l2) << "\n";

 cout << f1 << " + " << f2 << " = ";
 cout << somma(f1,f2) << "\n";

 return 0;
}
 


Funzioni inline

In  C++ esiste la possibilità, dichiarando le funzioni inline ovvero  premettendo alla definizione di una funzione la parola chiave inline, di chiedere al compilatore di sostituire ogni chiamata della funzione con il codice del corpo della funzione stessa.
 

Esempio:

inline float cubo(float x)
  {   return x*x*x ; }
 

ogni volta che il compilatore trova nel programma la chiamata:

cubo(espressione);

 la sostituisce con le istruzioni del corpo della funzione, in questo caso la  trasforma nell’istruzione :

(espressione) * (espressione) * (espressione) ;
 

L’uso della specifica inline é molto comune, in quanto permette di eliminare il sovraccarico di lavoro dovuto alla gestione della comunicazione fra programma e funzione e di mantenere il vantaggio legato all'uso delle funzioni (anche se piccole) che consentono di scomporre in più parti un grosso programma facilitandone sia la realizzazione che la successiva manutenzione.
 
 

In ogni caso il compilatore si riserva il diritto di accettare o rifiutare la specifica inline: in pratica, una  funzione che consista di più di 4 o 5 righe di istruzioni viene compilata come funzione separata, indipendentemente dalla presenza o meno della specifica inline.
 


Direttiva  #define di una macro

Quando il preprocessore incontra la seguente direttiva:

  #define  <nome>(<lista di argomenti>)  <espressione>

riconosce, (grazie alla presenza della parentesi tonda subito dopo <nome> senza blank in mezzo) una macro (macroistruzione).

Una macro é molto simile a una funzione. Il suo uso é chiarito dal seguente esempio:

 #define MAX(a,b)  a > b ? a : b

ciò significa che tutte le volte che il preprocessore trova nel programma una chiamata della macro effettua la opportuna sostituzione.
Ad esempio nella istruzione:

c = MAX(x,y);

 MAX(x,y),  viene sostituita  con:    x > y ? x : y

mentre nella istruzione:

c = MAX(x+1,y);
 
Max(x+1,y) viene sostituita  con:    x+1 > y ? x+1 : y

Ovviamente il compilatore, non accetterà istruzioni del tipo :
Max(x+1,y)  = c ;
in quanto incompatibili con gli operandi dell’operatore ? (condizionale).

Agli effetti pratici (purché si usino le dovute attenzioni!), la definizione di una macro produce gli stessi risultati della specifica inline nella definizione di una funzione.
 



Confronto fra la direttiva  #define e lo specificatore inline

Quando si usa lo specificatore inline:

invece quando si usa la direttiva  #define: Infatti se si dichiarasse una macro del tipo:

  #define SQR(a)  a*a

l'istruzione x= 1/SQR(z) +2;

del programma:

#include <iostream.h>
#define SQR(a)  a*a
main()
{
float z=4;
float k;
k=1/SQR(z)+2;
cout<<k;
}
 

sarebbe espansa in:
x=1/a*a+2;
la quale ha un significato diverso da quello che si voleva ottenere.

In genere al posto delle macro, comode per scrivere funzioni molto brevi,  in C++ si usano le funzioni inline .



La funzione main()
 

La struttura di un programma C++ è:

  < Dichiarazioni variabili, costanti, tipi globali >
  < Dichiarazioni funzioni >

  int main()
{
  < Dichiarazioni variabili, costanti, tipi globali >
  < Corpo della funzione >
  }

Ovvero è costituito da un insieme (eventualmente vuoto) di dichiarazioni e di definizioni globali di costanti, variabili ed un insieme di dichiarazioni e definizioni di funzioni e da una funzione dal nome predefinito main, che viene invocata automaticamente dal sistema quando il programma viene eseguito.

Nello schema riportato main restituisce, come stabilito dagli standard, un valore di tipo int (che generalmente e` utilizzato per comunicare al sistema operativo la causa della terminazione),pertanto, se nel corpo della funzione non viene inserita esplicitamente una istruzione return, il compilatore inserisce automaticamente  return 0;.
Nei vecchi compilatori, non standard, che permettevano maggior libertà, main  poteva essere dichiarata di tipo void.

In effetti, come tutte le funzioni, anche main può avere dei parametri in input, infatti  il prototipo esteso è:

  int main(int argc, char* argv[ ])

ovvero  il primo parametro è di tipo int e indica il numero di parametri presenti sulla riga di comando attraverso cui è stata chiesta l'esecuzione del programma; il secondo parametro e` un array di puntatori a caratteri, (ossia un array di stringhe) il primo dei quali è argv[0], cui segue argv[1] ecc .
Se non si desidera far leggere al main dei parametri dalla linea di comanco si può utilizzare la forma
int main(void) oppure int main() oppure  main()

Se invece si vuole passare qualche parametro al programma da eseguire occorre adottare la forma:
 int main(int argc, char* argv[])

Se ad esempio si chiede al sistema di eseguire il programma:
 

#include <iostream.h>
  int main(int argc, char* argv[])
{
    cout << "Riga di comando: " << endl;
    cout << argv[0] << endl;
    for(int i=1; i < argc; ++i)
      cout << "Parametro " << i << " = "
           << argv[i] << endl;
    return 0;
  }

immettendo sulla riga di comando comando:a.out prova a di parametri  ovvero:

timeelapsed% a.out prova a di parametri

verrà generato l'output:

Riga di comando:
a.out
Parametro 1 = prova
Parametro 2 = a
Parametro 3 = di
Parametro 4 = parametri
timeelapsed%

da cui si evince che le stringhe di caratteri inserite dopo a.out formano i possibili parametri da dare in input alla funzione main.

In effetti questi parametri possono essere utilizzati per eseguire un particolare tipo di esecuzione.
Ad esempio dato il programma:
#include <iostream.h>
#include<stdlib.h>
  int main(int argc, char* argv[])
{
int i, k,x = 4;
k=atoi(argv[1]);
if ( k >0)
{
for(i=1; i < k; ++i)
           cout << "valore " << 2*i << endl;
     return 0;
}
else
{
for(i=1; i < x; ++i)
            cout << "valore " << 2*i << endl;
     return 0;
}
}
 
ove l'istruzione:
k=atoi(argv[1]);
trasforma una stringa di caratteri numerici in un intero, se richiedo la sua esecuzione  con un parametro k>0  avrò in output:
k-1 numeri pari,
timeelapsed% a.out 7
valore 2
valore 4
valore 6
valore 8
valore 10
valore 12

altrimenti per k uguale o minore di 0 avrò in output i primi tre numeri pari, diversamente il programma andrà in ABEND.
timeelapsed% a.out 0
valore 2
valore 4
valore 6



 

    Programmazione a oggetti


La programmazione orientata agli oggetti (OOP= Object Oriented Programming) rappresenta una nuova concezione nella risoluzione dei problemi, sostanzialmente

 
Pertanto alle variabili “passive” della programmazione algoritmica corrispondono delle vere e proprie entità attive, gli oggetti (istanze di classi), i cui elementi possono essere manipolati solo dalle funzioni membro.
La manipolazione dei campi degli oggetti può avvenire tramite l’invio, agli oggetti, di messaggi che specificano l'operazione da compiere.
  La programmazione OO è basata sulle seguenti caratteristiche: Il meccanismo di astrazione dei dati consente di associare ad una struttura dati anche alcune operazioni che permettono di operare sui dati in modo astratto, tale meccanismo è possibile in quanto il linguagggio consente l'incapsulazione delle componenti private di una classe, che quindi possono essere nascoste o protette dall'esterno.
L'incapsulazione aiuta il programmatore allo sviluppo incrementale dei programmi e al riuso del software. L'ereditarietà equivale  alla capacità di definire le classi secondo gerarchie, in modo tale che gli oggetti delle "sottoclassi" ereditino le capacità e i metodi della propria "classe genitrice".
Ci sono due  modelli principali di ereditarietà: singola e multipla.
Nel modello di ereditarietà singola, una determinata classe può essere una sottoclasse di un'unica classe progenitrice, nel modello di ereditarietà multipla, invece, una determinata classe può ereditare da un numero imprecisato di classi progenitrici.
  Con il termine polimorfismo si intende la capacità che hanno oggetti diversi di rispondere alla stessa operazione, ovvero il corretto riconoscimento, da parte del sistema, della funzione da eseguire quando viene inviato un messaggio ad un determinato oggetto appartente ad una classe base o derivata.
Grazie al polimorfismo è possibile realizzare un'interfaccia di programmazione senza far riferimento a un tipo specifico, in quanto è possibile utilizzare nelle stesse circostanze oggetti diversi in quanto sono in grado di rispondere agli  stessi metodi.
Il polimorfismo semplifica l'interfaccia di programmazione e il suo utilizzo dà spesso origine a codice maggiormente estendibile e riutilizzabile.

TIPO CLASSE
 

In C++ le classi sono molto simili alle strutture (record) infatti sono introdotte dalla parola-chiave class anziché struct, ma ciò che sostanzialmente cambia è che una classe può possedere oltre ai campi ordinari (membri dato o attributi)  di una struttura, dei campi di tipo funzione (detti funzioni membro oppure metodi) che costituiscono l'insieme delle operazioni (interfaccia) che la classe (di oggetti di quel tipo) è in grado di eseguire sui campi della classe stessa.
Il tipo di accesso ai campi definiti nella classe dipende dal valore degli specificatori di accesso:
 

private: rende accessibili i dati e le funzioni solo alla classe stessa,
protected: permette l’accesso anche alle classi derivate, oltre la classe stessa.
public: rende accessibili dati e le funzioni da tutte le funzioni del programma.

I campi di una classe sono, per default, protetti.
L'indicazione delle sezioni private, protected, public, può essere data in qualsiasi ordine e nella stessa classe possono apparire più sezioni private, pubbliche o protette (anche se ciò è sconsigliato in quanto riduce la leggibilità ed inoltre non è utile).

La sintassi per la dichiarazione di una classe è:

  class <NomeClasse> {
   private:
        <membri privati>
     public:
        <membri pubblici>
     protected:
        <membri protetti>
 
  };     // notare il punto e virgola finale!
 
 

Vediamo un semplice esempio di classe:
 
class  poligono        // Definisco una classe  di nome  poligono
{
    int costo;            // Questa parte e' PRIVATE di default
public:                 // Questa parte e' PUBLIC (visibile da tutti)
    float lunghezza;
    int numlati;
};

La dichiarazione del tipo classe poligono, non avendo metodi, e' praticamente uguale ad una normale struttura.
 

Dopo aver definito la classe poligono possiamo dichiarare (istanziare) alcuni oggetti di questo tipo (appartenenti a questa classe):

poligono quadrato,triangolo,esagono;   // istanziamo 3 oggetti di tipo "poligono".
 

ed utilizzare i campi (membri) definiti public:

quadrato.lunghezza = 30;        // Accediamo alle variabili pubbliche
quadrato.numlati = 4;       // dell'oggetto da qualsiasi parte del prog.
 
Come si nota, abbiamo usato l'operatore "." per dividere il nome dell'oggetto dal nome della sua variabile (pubblica), analogamente a come si fa  per le strutture.
Si noti che ogni oggetto creato dalla classe astratta " poligono" e' indipendente, per cui scrivere in quadrato.lunghezza non  modifica  triangolo.lunghezza, trattandosi di 2 oggetti fisicamente distinti:  il solo rapporto esistente tra quadrato, triangolo e poligono e' che sono oggetti dello stesso tipo, cioè appartenenti alla stessa classe.

ESEMPIO

#include
class  poligono
{        // Definisco la classe  poligono
int costo; // Questa parte e' PRIVATE di default
 public:               // La parte che segue e' PUBLIC (visibile da tutti)
    float lunghezza;   // Nota: non si dovrebbero mai mettere delle
    int numlati;   // variabili in un area public. Non lo faremo più
   };

main(void)
{

poligono quadrato,triangolo,esagono;        //  istanziamo 3 poligoni...
                                                                 // ossia creiamo 3 esemplari.
 quadrato.numlati = 4;          // Accediamo alle variabili pubbliche
 triangolo.numlati = 3;          // dell'oggetto analogamente a come
                               // accediamo a dati delle strutture.
// Richiediamo dati dall'utente.
  cout << "Lunghezza lato quadrato ? ";
  cin >> quadrato.lunghezza;
  cout << "Lunghezza lato triangolo? ";
  cin >> triangolo.lunghezza;
  cout << "Lunghezza lato esagono? ";
  cin >> esagono.lunghezza;
  cout << "Num. lati esagono? ";
  cin >> esagono.numlati;

// Visualiziamo lo stato degli poligoni.
  cout << "\nStato del quadrato: lati = " << quadrato.numlati;
  cout << ", lunghezza = " << quadrato.lunghezza;
  cout << "\nStato del triangolo: lati = " << triangolo.numlati;
  cout << ", lunghezza = " << triangolo.lunghezza;
  cout << "\nStato esagono: lati = " << esagono.numlati;
  cout << ", lunghezza = " << esagono.lunghezza << "\n";
}

In questo esempio abbiamo definito una variabile privata  "costo",  che non e' stata usata, in quanto la classe non ha al suo interno funzioni  (metodi), gli unici che avrebbero potuto accedervi.

Invece abbiamo scritto dal di "fuori" nelle due variabili pubbliche  "lunghezza" e "numlati" dell’oggetto.

Questa applicazione non e' certo molto Object Oriented in quanto:

 


CREAZIONE DI  CLASSI CON METODI PUBBLICI E VARIABILI PRIVATE
 

Poiché la programmazione ad oggetti si basa sulla dichiarazione ed istanziazione di classi (dichiarazione privata di dati e loro utilizzazione SOLO tramite gli appositi metodi), ora vedremo di creare una classe più completa, che abbia oltre ai campi dato (variabili) questa volta però privati anche delle funzioni (metodi) pubbliche!.
 

Per realizzare la programmazione ad oggetti è necessario dichiarare le variabili nella parte privata ed i prototipi delle funzioni (metodi) nella parte pubblica, ad esempio:
 
class  poligono           // Definisco la classe  poligono
  {
       float lunghezza;    // Questa parte e' PRIVATE  per default
       int numlati;
public:                    //Questa parte e'PUBLIC (visibile da tutti)
   void scrivilungh(float i);   // Questi sono i prototipi delle funzioni
   void scrivilati(int l);     // (o metodi) della nostra classe che
   float leggilungh(void);     //possono agire sui campi dichiarati.
   int leggilati(void);
    };

I prototipi delle funzioni vanno dichiararti nella parte pubblica in quanto rappresentano i metodi per utilizzare dall’esterno i campi privati delle classi, questi sono indispensabili dal momento che le variabili private, non possono essere "toccate"   direttamente dall'esterno.

Questo meccanismo realizza i concetti di "information hiding" (nascondere i dati) e "incapsulation" (unire dati e funzioni in un unica "capsula") che sono alla base della programmazione ad oggetti.
I dati delle classi sono privati, ad essi non si può accedere direttamente, sono modificabili solo tramite gli opportuni metodi, i quali saranno "pubblici".
 

I prototipi sono analoghi a quelli delle normali funzioni: si indicano i parametri in entrata e uscita.

Il codice effettivo delle funzioni si dichiara in questo modo:
 
void  poligono::scrivilungh(float i)
   {
     lunghezza = i;   // Scrive il valore nella variabile privata "lunghezza".
   }
 
L'unica differenza rispetto alla definizione delle normali funzioni, e' che occorre aggiungere l'appartenenza ad una classe, precedendo il nome della funzione con “<nomeclasse>::".
Il segno :: e' detto operatore di determinazione di visibilita' (scope resolution operator), e in questo caso dichiara che il metodo scrivilungh e' nel campo di visibilita' della classe  poligono.

Vediamo quindi l'uso di una classe con dati privati e metodi pubblici.

#include

class  poligono {         // Definisco la classe  poligono
    float lunghezza;    // Questa parte e' PRIVATE di default
    int numlati;
 public:                // Parte PUBLIC (visibile da tutti)
    void scrivilungh(float i);  // Questi sono i prototipi delle funzioni
    void scrivilati(int l);    // (o metodi) della nostra classe.
    float leggilungh(void);
    int leggilati(void);
 };
 

// Ora scriviamo le funzioni (metodi) della classe  poligono:

/ *   scrivilungh: definisce la lunghezza dei lati del poligono*/
void  poligono::scrivilungh(float i)
{
  lunghezza = i;   // Scrive il valore nella variabile privata "lunghezza".
}
 

/* scrivilati: definisce il numero di lati del poligono*/
void  poligono::scrivilati(int l)
{
  numlati = l; // Scrive il valore nella variabile privata "numlati".
}

/* leggilungh: legge e restituisce la lunghezza del poligono */
float  poligono::leggilungh(void)
{
  return lunghezza;
}

/*leggilati: legge e restituisce il num. di lati del poligono*/
int  poligono::leggilati(void)
{
  return numlati;
}
 

int main()
{
 float temp;    // Variabile temporanea per le lunghezze.
 int temp2;     // Variabile temporanea per il num. lati.

// Ora possiamo dichiarare alcuni oggetti di tipo  poligono e usarli:
  poligono quadrato,triangolo,esagono;  //creo 3 esemplari dalla
                                           // classe "astratta" poligono.

/* Ora non possiamo più accedere direttamente ai dati come abbiamo fatto prima,  (con quadrato.numlati = 4; eccetera.) ma dobbiamo mandare un messaggio all'oggetto chiedendogli di modificare la sua variabile.

 quadrato.scrivilati(4); // Chiamiamo il metodo scrivilati() di quadrato
 triangolo.scrivilati(3);             // triangolo

// Gli altri parametri li chiediamo all'utente.

  cout << "\nLunghezza lato quadrato? ";
  cin >> temp;
  quadrato.scrivilungh(temp);           // Passa a scrivilungh la variabile temp
  cout << "Lunghezza lato triangolo? ";
  cin >> temp;
  triangolo.scrivilungh(temp);           // Passa a scrivilungh la variabile temp
  cout << "Lunghezza lato esagono? ";
  cin >> temp;
  esagono.scrivilungh(temp);      // Passa a scrivilungh la variabile temp
  cout << "Num. lati esagono? ";
  cin >> temp2;
esagono.scrivilati(temp2);     // Passa a scrivilati la variabile temp2

// Ora visualizziamo lo stato degli poligoni, usando i loro metodi di lettura.

  cout << "\nStato del quadrato: lati = " << quadrato.leggilati();
  cout << ", lunghezza = " << quadrato.leggilungh();
  cout << "\nStato del triangolo: lati = " << triangolo.leggilati();
  cout << ", lunghezza = " << triangolo.leggilungh();
  cout << "\nStato dell’ esagono: lati = " << esagono.leggilati();
  cout << ", lunghezza = " << esagono.leggilungh() << "\n";

  return 0;     // main() restituisce 0
}

Come si vede i componenti privati di una classe hanno un ruolo simile a quello delle variabili locali di una funzione, cioè possono essere usati solo dai componenti della clase.
I componenti pubblici possono essere usati sia all'interno della classe che al di fuori dalle altre funzioni.
I componenti protetti possiedono un livello di mascheramento (data-hiding) intermedio tra quello privato e pubblico. Essi sono usati quando si opera una derivazione di una nuova classe da una classe preesistente.

Per operare su un componente privato (es: lunghezza)  di un oggetto (es: quadrato ) appartenente ad una certa classe (es poligono) : si spedisce il MESSAGGIO, relativo a ciò che si chiede, al particolare oggetto:
quadrato.scrivilungh(temp);   
il quale invoca il METODO che corrisponde a quel messaggio, che a sua volta interpreta (ed esegue) il messaggio (eventualmente restituendo dei dati, se richiesti).

A questo punto si potrebbe osservare ciò che abbiamo realizzato in questo esempio si poteva implementare in C standard, accedendo alle variabili di strutture solo tramite funzioni, e non direttamente.
In effetti “fino a questo punto”, si può programmare ad oggetti anche in C standard, se si seguono le regole della buona programmazione, ossia dividendo il programma in funzioni, e accedendo ai dati solamente dalle funzioni ad essi relative, senza mai accederci dal main().
 


COSTRUTTORI   E   DISTRUTTORE   DI   UNA CLASSE
 

Tra i metodi di una classe dovrebbero esser presenti due speciali metodi detti costruttore e distruttore di oggetti della classe, che servono rispettivamente per  inizializzare e rimuovere dalla memoria gli oggetti della classe.

Costruttori

I costruttori devono rispettare le seguenti regole:
 

Il C++ li distingue in base alla lista degli argomenti, pertanto non sono ammessi costruttori che differiscono solo per gli argomenti di default.

I costruttori vanno chiamati  per dichiarare l'oggetto nel programma prima dell’utilizzo dell’oggetto stesso.
 
Ad esempio potremmo dichiarare il costruttore:
 
 poligono(void);             // Costruttore

cui corrisponde :
//  costruttore
 poligono:: poligono(void)
  {
  lunghezza = 0;
  }
 
 

Distruttori
I distruttori hanno la funzione di liberare la memoria occupata da oggetti che non sono più utilizzati.

I distruttori devono rispettare le seguenti regole:
 

Un distruttoreautomatico una volta dichiarato, viene eseguito automaticamente in uno dei seguenti casi:
  In realtà, per i membri allocati nella (o che puntano alla) memoria stack, il distruttore non servirebbe, in
quanto la disallocazione è automatica; invece, se esistono membri che puntano alla memoria heap, la
presenza di un distruttore é indispensabile per la "pulizia" dei dati.
 

Esempio:

#include <iostream.h>
#include<stdio.h>
class Persona
{
char*      nome;
char*   cognome; 
 public:
    Persona(int);
     ~Persona();
void leggiPersona();
void stampaPersona();
 };
Persona::Persona(int n)
{  nome = new char [n];
   cognome = new char [n];
}
Persona::~Persona()
{delete [ ] nome;
  delete [ ] cognome; }
void Persona::leggiPersona()
{  cout<<“digitare nome”<<endl; cin>>nome; cout<<“digitare cognome”<<endl; cin>>cognome; }
void Persona::stampaPersona()
{  cout<<nome<<“  “<<cognome<<endl; }
 main()
{
 Persona studente(30);
studente.leggiPersona();
studente.stampaPersona();
}

Il concetto di incapsulazione della OOP può essere compreso analizzando l’esempio precedente della classe Persona.
Come si vede nel main noi scriviamo studente.leggiPersona() (inviamo all’oggetto studente il messaggio  “leggiPersona”) senza preoccuparci di come la classe ha implementato i singoli attributi (esempio una stringa o un puntatore a una stringa) ,  in quanto  gli attributi (nascosti all’utente esterno) sono visibili solo ai metodi che si occupano della loro trasformazione. Questo aspetto di incapsulamento dei dati all’interno della classe presenta ulteriori aspetti positivi, se infatti fosse necessario inserire nella classe Persona oltre al cognome e nome altre informazioni (es: età) basterebbe modificare la classe ed i metodi e non il main che continuerebbe ad essere lo stesso.
 

#include <iostream.h>
#include<stdio.h>
class Persona
{
char*      nome;
char*   cognome;
int  eta;
 public:
    Persona(int);
     ~Persona();
void leggiPersona();
void stampaPersona();
 };
Persona::Persona(int n)
{  nome = new char [n];
   cognome = new char [n];
eta=0;}
Persona::~Persona()
{delete [ ] nome;
  delete [ ] cognome; }
void Persona::leggiPersona()
{  cout<<“digitare nome”<<endl; cin>>nome; cout<<“digitare cognome”<<endl; cin>>cognome;
cout<<“digitare eta’”<<endl; cin>>eta; }
void Persona::stampaPersona()
{  cout<<nome<<“  “<<cognome<<“  “<<eta<<endl; }
 main()
{
 Persona studente(30);
studente.leggiPersona();
studente.stampaPersona();
}

 

Quando nel main viene dichiarato l'oggetto studente, istanza della classe Persona, il costruttore alloca l'oggetto composto dai campi (puntatore) nome e cognome nella memoria stack e due aree di 30 byte nella memoria heap,  i cui indirizzi risiedono nei campi (puntatore) studente.nome e studente.cognome  dell'oggetto studente.
Quando l'oggetto studente esce dal suo ambito di visibilità, il distruttore libera automaticamente la memoria allocata per le due aree.
Senza il distruttore, verrebbe liberata soltanto la memoria stack occupata dall'oggetto studente e dai suoi membri puntatori, ma non l'area heap indirizzata dai puntatori stessi.

Vediamo ora la classe poligono munita di costruttore e distruttore:

class  poligono
 {
float lunghezza;
int numlati;
public:
 poligono(void);             // Costruttore
~ poligono(void);            // Distruttore
void scrivilungh(float i);  // Questi sono i prototipi dei metodi
void scrivilati(int l);    // della nostra classe.
 float leggilungh(void);
 int leggilati(void);
};

//  costruttore
 poligono:: poligono(void)
  {
  lunghezza = 0;
  }

// distruttore (in questo caso dato che non serve deallocare niente scriviamo nel suo corpo semplicemente una istruzione di stampa che prima della terminazione de programma entrerà automaticamente in esecuzione)
 poligono::~ poligono(void)
{
   cout << " poligono distrutto\n";
}
 
ESEMPIO

#include
class  poligono
{
    float lunghezza;
    int numlati;
 public:
     poligono(void);             // Costruttore
    ~ poligono(void);            // Distruttore
    void scrivilungh(float i);  // Questi sono i prototipi delle funzioni
    void scrivilati(int l);    // (o metodi) della nostra classe.
    float leggilungh(void);
    int leggilati(void);
 };
 



COSTRUTTORE PARAMETRIZZATO

In alcuni casi potremmo avere la necessita' di passare subito dei parametri al costruttore di una classe.
ESEMPIO:

class  poligono {
...
public:
 poligono(int num);         // Costruttore con parametri
  ~ poligono(void);            // Distruttore
...
};

// costruttore parametrizzato
 poligono:: poligono(int num)
  {
  lunghezza = 0;
  numlati = num;                 // Il num. lati lo metto subito
  }
 

A programma per passare il parametro attuale 4 al costruttore si scriverà:

  poligono quadrato(4);
 
 

ESEMPIO

#include
class  poligono
{
    float lunghezza;
int numlati;
 public:
    poligono(int num);         // Costruttore con parametri
    ~ poligono(void);            // Distruttore
    void scrivilungh(float i);  // Questi sono i prototipi delle funzioni
    void scrivilati(int l);    // (o metodi) della nostra classe.
    float leggilungh(void);
    int leggilati(void);
 };
 

// metodi della classe  poligono:

// costruttore con Parametro: in entrata il numero di lati (int).
 poligono:: poligono(int num)
{
  lunghezza = 0;
  numlati = num;                 // Il num. lati lo metto subito
}

//  distruttore
 poligono::~ poligono(void)
{
   cout << " poligono distrutto\n";
}

/ *   scrivilungh: definisce la lunghezza dei lati del poligono*/
void  poligono::scrivilungh(float i)
{
  lunghezza = i;   // Scrive il valore nella variabile privata "lunghezza".
}
 

/* scrivilati: definisce il numero di lati del poligono*/
void  poligono::scrivilati(int l)
{
  numlati = l; // Scrive il valore nella variabile privata "numlati".
}

/* leggilungh: legge e restituisce la lunghezza del poligono */
float  poligono::leggilungh(void)
{
  return lunghezza;
}

/*leggilati: legge e restituisce il num. di lati del poligono*/
int  poligono::leggilati(void)
{
  return numlati;
}
 

int main(void)
{

 float temp;
 int temp2;

// dichiarazione tramite il costruttore di alcuni oggetti di tipo  poligono:

  poligono quadrato(4), triangolo(3);     // dichiarazione di 3 poligoni...
  poligono esagono(6);                           // ossia creo degli esemplari dalla
                                                            // classe "astratta".
                                                          // (Passandogli subito un parametro).

// Gli altri parametri li chiediamo all'utente.

  cout << "\nLunghezza lato quadrato? ";
  cin >> temp;
quadrato.scrivilungh(temp);           // Passa a scrivilungh la variabile temp
  cout << "Lunghezza lato triangolo? ";
  cin >> temp;
triangolo.scrivilungh(temp);           // Passa a scrivilungh la variabile temp
  cout << "Lunghezza lato esagono? ";
  cin >> temp;
  esagono.scrivilungh(temp);      // Passa a scrivilungh la variabile temp
  cout << "Num. lati esagono? ";
  cin >> temp2;
  esagono.scrivilati(temp2);     // Passa a scrivilati la variabile temp2

// Ora visualizziamo lo stato dei poligoni, usando i loro metodi di lettura.

  cout << "\nStato del quadrato: lati = " << quadrato.leggilati();
  cout << ", lunghezza = " << quadrato.leggilungh();
  cout << "\nStato del triangolo: lati = " << triangolo.leggilati();
  cout << ", lunghezza = " << triangolo.leggilungh();
  cout << "\nStato dell’ esagono: lati = " << esagono.leggilati();
  cout << ", lunghezza = " << esagono.leggilungh() << "\n";

  return 0;
}

timeelapsed% a.out

Lunghezza lato quadrato? 12.5
Lunghezza lato triangolo? 13.78
Lunghezza lato esagono? 03.98
Num. lati esagono? 4

Stato del quadrato: lati = 4, lunghezza = 12.5
Stato del triangolo: lati = 3, lunghezza = 13.78
Stato dell' esagono: lati = 4, lunghezza = 3.98
 poligono distrutto
 poligono distrutto
 poligono distrutto
timeelapsed%

I costruttori e i distruttori, comunque, sono facoltativi, non sempre sono indispensabili.



POLIMORFISMO

Poiché in C++ è possibile l’overloading degli identificatori delle funzioni è anche possibile  l’overloading dei metodi delle classi, in particolare dei costruttori purche' queste abbiano parametri diversi.
L'overloading permette di accedere ad un unsieme di metodi simili (almeno a livello logico) usando lo stesso nome.
 

Supponiamo di voler rendere facoltativi i parametri del costruttore della classe  poligono:

class  poligono
{         // Definisco la classe  poligono
      ...
public:              // La parte che segue e' PUBLIC (visibile da tutti)
 poligono(void);                  // Costruttore: nessun parametro
 poligono(int num);              // Costruttore: 1 par.: int num
 poligono(int num, float lungh); // Costruttore: 2 par.: num, lung
~ poligono(void);                 // Distruttore
...
  };

//   costruttore senza parametri
poligono:: poligono(void)
{
lunghezza = 0;
numlati = 0;
 }

 //   costruttore con 1 parametro: int num
  poligono:: poligono(int num)
 {
lunghezza = 0
numlati = num;                 // Il num. lati lo metto subito
}

 //   costruttore con 2 parametri: int num, float lung
   poligono:: poligono(int num, float lung)
{
lunghezza = lung;
numlati = num;
}

Le possibili dichiarazioni sono quindi:
 poligono quadrato();
 poligono quadrato(4);            // Definisco subito che ha 4 lati
 poligono quadrato(4,40.5)        // Definisco subito 4 lati e lungh. 40.5 cm
 

L'overloading dei costruttori rende più flessibile l'inizializzazione degli oggetti.

Per esempio, possiamo creare il particolare oggetto poligono quando sappiamo il numero dei suoi lati, e non all'inizio, quando non lo sappiamo e non possiamo fare altro che inizializzare tutto a zero.
 

ESEMPIO
In questo programma sono utilizzate dichiarazioni che sfruttando l’overloading dei costruttori.
 

#include
class  poligono
{
    float lunghezza;
    int numlati;
 public:
     poligono(void);                  // Costruttore: nessun parametro
     poligono(int num);              // Costruttore: 1 par.: int num
     poligono(int num, float lungh); // Costruttore: 2 par.: num, lung
    ~ poligono(void);                 // Distruttore
    void scrivilungh(float i);       // I prototipi dei metodi della
    void scrivilati(int l);         // nostra classe.
    float leggilungh(void);
    int leggilati(void);
 };

//  Funzioni (metodi) della classe  poligono:

//   costruttore senza parametri
  poligono:: poligono(void)
 {
   lunghezza = 0;
   numlati = 0;
 }

//   costruttore con 1 parametro: int num
  poligono:: poligono(int num)
 {
   lunghezza = 0;
   numlati = num;           // Il num. lati lo metto subito
 }

 //    costruttore con 2 parametri: int num, float lung
  poligono:: poligono(int num, float lung)
 {
   lunghezza = lung;
   numlati = num;
 }

 //    distruttore
 poligono::~ poligono(void)
{
   cout << " poligono distrutto\n";
}
 

/ *   scrivilungh: definisce la lunghezza dei lati del poligono*/
void  poligono::scrivilungh(float i)
{
  lunghezza = i;   // Scrive il valore nella variabile privata "lunghezza".
}
 

/* scrivilati: definisce il numero di lati del poligono*/
void  poligono::scrivilati(int l)
{
  numlati = l; // Scrive il valore nella variabile privata "numlati".
}

/* leggilungh: legge e restituisce la lunghezza del poligono */
float  poligono::leggilungh(void)
{
  return lunghezza;
}

/*leggilati: legge e restituisce il num. di lati del poligono*/
int  poligono::leggilati(void)
{
  return numlati;
}
 
 

int main(void)
{

/* gestione  primo oggetto: quadrato.
Si noti che iniziamo il programma senza aver dichiarato né classi,  né variabili!! Chiediamo all'utente di darci le informazioni*/
cout << "Num. lati quadrato? ";
int temp2;    // Variabile temporanea che usiamo per il num. lati. Da
                // notare che la abbiamo definita dopo un'istruzione, e
                // non all'inizio. Questo e' illegale in C, ma legale in C++
cin >> temp2;

poligono quadrato;
/* Istanzio un poligono di nome quadrato, senza passare parametri al costruttore*/
quadrato.scrivilati(temp2);         // Passa a scrivilati la variabile temp2
cout << "Lunghezza lato quadrato? ";
float temp;                                // Var. temporanea per la lunghezza.
cin >> temp;
quadrato.scrivilungh(temp);          // Passa a scrivilungh la variabile temp

/* gestione  secondo oggetto: triangolo.
Questa volta passiamo direttamente al costruttore il num. di lati, evitando di chiamare il metodo scrivilati().*/

  cout << "Num. lati triangolo? ";
  cin >> temp2;
  cout << "Lunghezza triangolo? ";
  cin >> temp;
  poligono triangolo(temp2);
// Istanzio triangolo, passando al costruttore solo  il numero dei lati
  triangolo.scrivilungh(temp);           // Passa a scrivilungh la la lunghezza.

/* gestione  terzo oggetto: esagono.
L’ esagono lo definiamo usando il costruttore con 2 parametri*/
  poligono esagono(6,34.5);   // Istanzio esagono, passando il num.
                               // lati e la lunghezza al costruttore.

// Ora visualiziamo lo stato degli poligoni, usando i loro metodi di lettura.

  cout << "\nStato del quadrato: lati = " << quadrato.leggilati();
  cout << ", lunghezza = " << quadrato.leggilungh();
  cout << "\nStato del triangolo: lati = " << triangolo.leggilati();
  cout << ", lunghezza = " << triangolo.leggilungh();
  cout << "\nStato del esagono: lati = " << esagono.leggilati();
  cout << ", lunghezza = " << esagono.leggilungh() << "\n";

  return 0;     // main() restituisce 0
}

E’ chiaro a questo punto che si potrebbero "overloadare" anche i metodi, permettendo di inserire altri dati, facoltativamente.


EREDITARIETA’

Sperimentiamo  ora l'ereditarieta' (inheritance) derivando dalla classe  poligono (superclasse) delle sottoclassi "figlie", che erediteranno le variabili e i metodi della classe "padre", risparmiandoci quindi di riscriverle, e limitando il nostro lavoro all'aggiunta delle variabili e dei metodi peculiari delle classi derivate. Dal principio di ereditarietà  discende la possibilità di riutilizzare del codice..

Data una classe  poligono (superclasse), è possibile derivare sottoclassi, ad esempio le specie parallelepipedo e piramide.
Queste sottoclassi erediteranno le variabili lunghezza e numlati, nonché i metodi scrivilungh(), scrivilati(), leggilungh() e leggilati(), ma a queste variabili e metodi, comuni a tutti i poligoni (gli oggetti appartenenti alla classe poligono), potremo aggiungere  variabili e i metodi specifici della classe derivata.
In particolare, la classe derivata parallelepipedo potrà avere in più una variabile altezza e un paio di metodi pubblici, scrivialtezza() e leggialtezza(), in modo da poter interfacciare tale variabile (privata) con l'esterno.
La classe derivata piramide, invece, avra' una variabile peso e i relativi metodi scrivipeso() e leggipeso().

Per derivare una classe da un'altra, si usa questa sintassi:

class <nome_nuova_classe> : <superclasse_ereditata> {...};

Ossia al nome della nuova classe si aggiunge il simbolo ":" seguito dalla superclasse padre da cui si eredita il "patrimonio " di metodi e variabili.

Ad esempio, dichiariamo la classe parallelepipedo derivata da  poligono:

class parallelepipedo :  poligono {...};

Si può anche specificare un identificatore di accesso, che noi metteremo sempre public. Ecco quindi la classe parallelepipedo figlia (cioè derivata)  di  poligono:

class parallelepipedo : public  poligono
{
 float altezza;              // var. privata peculiare di parallelepipedo
  public:                          // La parte che segue e' PUBLIC
 void scrivialtezza(float cl);     // Prototipi delle funzioni
 float leggialtezza(void);        //  di parallelepipedo.
 };

A questo punto possiamo definire i suoi metodi:

void parallelepipedo::scrivialtezza(float cl) // un metodo di parallelepipedo
{
altezza = cl;                  // accedo a una variabile di parallelepipedo.
}

float parallelepipedo::leggialtezza(void) // un metodo di parallelepipedo
{
return altezza;                  // accedo a una variabile di parallelepipedo.
}

e di seguito la subclasse piramide o altre subclassi.

Nel programma si potranno usare ( dopo averle istanziate!) sia istanze della superclasse  poligono sia delle classi derivate.

ESEMPIO

#include  <iostream.h>

class  poligono {
    float lunghezza;
    int numlati;
 public:
    void scrivilungh(float i);
    void scrivilati(int l);
    float leggilungh(void);
    int leggilati(void);
 };

/*   scrivilungh: definisce la lunghezza dei lati del poligono*/
void  poligono::scrivilungh(float i)
{
  lunghezza = i;   // Scrive il valore nella variabile privata "lunghezza".
}
 

/* scrivilati: definisce il numero di lati del poligono*/
void  poligono::scrivilati(int l)
{
  numlati = l; // Scrive il valore nella variabile privata "numlati".
}

/* leggilungh: legge e restituisce la lunghezza del poligono */
float  poligono::leggilungh(void)
{
  return lunghezza;
}

/*leggilati: legge e restituisce il num. di lati del poligono*/
int  poligono::leggilati(void)
{
  return numlati;
}

 /* Deriviamo due specie di poligoni particolari: parallelepipedo e piramide, che andranno ad aggiungere al partimonio ereditato le caratteristiche peculiari della propria specie: l’altezza del parallelepipedo, e il peso della piramide. */
// Iniziamo derivando la classe parallelepipedo dalla superclasse  poligono.

class parallelepipedo : public  poligono
{               // Classe parallelepipedo derivata da  poligono.
    float altezza;         // PRIVATE - variabile peculiare di parallelepipedo
 public:                         // La parte che segue e' PUBLIC
    void scrivialtezza(float cl);   // Questi sono i prototipi delle funzioni
    float leggialtezza(void);      // peculiari di parallelepipedo.
 };

/*scrivialtezza: definisce l’altezza del parallelepipedo */
void parallelepipedo::scrivialtezza(float cl)
{
  altezza = cl;
}

/*leggialtezza: legge e restituisce l’altezza del parallelepipedo*/
float parallelepipedo::leggialtezza(void)
{
  return altezza;
}
 

// Ora deriviamo la classe piramide dalla superclasse  poligono.
class piramide : public  poligono
{
    float peso;            // PRIVATE - variabile peculiare di piramide
 public:                        // La parte che segue e' PUBLIC
    void scrivipeso(float cn);  // Questi sono i prototipi delle funzioni
    float leggipeso(void);     // peculiari di piramide.
 };

void piramide::scrivipeso(float cn)  // scrivipeso() e' un metodo di piramide
{
  peso = cn;  // Scrive il valore nella variabile privata "leggipeso".
}

float piramide::leggipeso(void)  // leggipeso() e' un metodo di piramide
{
  return peso;
}
 
 

int main(void)
{
   poligono quadrato;        // Istanzio un oggetto della classe  poligono

// Chiediamo all'utente le informazioni. Oggetto di tipo  poligono: quadrato.

  cout << "\nNum. lati quadrato? ";
  int temp2;
  cin >> temp2;
  quadrato.scrivilati(temp2);
  cout << "Lunghezza lato quadrato? ";
  float temp;
  cin >> temp;
  quadrato.scrivilungh(temp);

// creazione di un oggetto di tipo parallelepipedo: cubo.

  parallelepipedo cubo;
  cout << "Num. lati cubo ";
  cin >> temp2;
  cout << "Lunghezza lato cubo ";
  cin >> temp;
  cubo.scrivilati(temp2);       // Passa a scrivilati la variabile temp2
  cubo.scrivilungh(temp);        // Passa a scrivilungh la la lunghezza.
  cout << "Altezza del cubo? ";
  cin >> temp;
  cubo.scrivialtezza(temp);        // Passa a scrivialtezza la variabile temp

// Infine l'oggetto di tipo piramide: piram1.

  piramide piram1;
  cout << "Num. lati piram1? ";
  cin >> temp2;
  cout << "Lunghezza lato piram1? ";
  cin >> temp;
  piram1.scrivilati(temp2);        // Passa a scrivilati la variabile temp2
  piram1.scrivilungh(temp);         // Passa a scrivilungh la la lunghezza.
  cout << "Peso di piram1? ";
  cin >> temp;
  piram1.scrivipeso(temp);         // Passa a scrivipeso la variabile temp

// Ora visualiziamo lo stato degli poligoni, usando i loro metodi di lettura.

  cout << "\nStato del poligono quadrato: lati = " << quadrato.leggilati();
  cout << ", lunghezza = " << quadrato.leggilungh();

  cout << "\nStato del parallelepipedo cubo: lati = " << cubo.leggilati();
  cout << ", lunghezza = " << cubo.leggilungh();
  cout << ", altezza = " << cubo.leggialtezza();

  cout << "\nStato della piramide piram1: lati = " << piram1.leggilati();
  cout << ", lunghezza = " << piram1.leggilungh();
  cout << ", peso = " << piram1.leggipeso();

  return 0;
}
 
 

VARIABILI STATIC

Fino ad ora abbiamo visto come le classi siano una definizione astratta degli oggetti da istanziare, i quali poi avranno ognuno una copia personale sia dei metodi che delle variabili, per cui scrivendo in una variabile di un oggetto, non si può cambiare il valore di quella variabile negli altri oggetti istanza della stessa classe.
Infatti se scriviamo in quadrato.numlati non si modifica triangolo.numlati, essendo queste due variabili indipendenti, e tra l'altro inaccessibili dall'esterno del proprio oggetto, essendo private.
Quando invece si desidera che una variabile di una classe sia condivisa da tutti gli oggetti istanziati dalla classe, e pertanto scrivere in essa da uno degli oggetti significa cambiarne il valore in tutti gli altri basta usare "static" prima della dichiarazione.

Ad esempio:

class articolo
{static int numart;
public:
    articolo();
    ~articolo();
    // altre operazioni
};
 

L’uso di variabili di tipo static richiede molta attenzione in quanto cambia molto la logica di funzionamento delle classi.