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%
/*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);
}
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.
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.
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.
/*
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);
}
{ /* 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";
}
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.
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;
}
#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.
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.
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.
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.
Quando si usa lo specificatore inline:
#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 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
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:
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".
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:
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().
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:
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:
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);
};
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.
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.