disegno di Davide ConteC++

Tutti gli esempi riportati sono stati tratti dal testo [Conte 96]

Alcuni esempi richiedono conoscenze approfondite del linguaggio, si consiglia di consultare un buon manuale.


Classe Cronometro esempio semplice introduttivo

Come poter misurare i tempi di esecuzione di certi algoritmi, utilizzando degli oggetti simili ai cronometri della realtà?

In C++ possiamo definirci una classe opportuna che ci permetta di avere (istanziare) oggetti per la misurazione del tempo.
 

Gli oggetti che potremo ottenere da questa classe saranno del tutto simili ai cronometri veri, ciascuno avrà una sua vita, nel senso che ognuno di essi sarà adoperato per misurare certi intervalli di tempo e "ricorderà" il tempo misurato.

 

La classe Cronometro rappresenta il nuovo tipo e le istanze della classe rappresenteranno i nuovi oggetti.
Al generico cronometro possiamo dare dei comandi (invocare metodi):
 
 
 

  • Start per farlo partire;
  • Stop per fermarlo;
  • Reset per azzerarlo.

Ovviamente dobbiamo poter leggere il tempo misurato o che scorre, servirà un metodo per interrogare od osservare il cronometro:


Invocare un comando significa attivare una funzione che agisce sull'oggetto o meglio sullo stato interno dell'oggetto della classe; interrogare od osservare lo stato interno equivale ad invocare una funzione che restituisce il risultato voluto, elaborando i dati interni.
All'interno il cronometro ha una struttura nascosta che contiene una serie di meccanismi sia per farlo funzionare, sia per memorizzare le informazioni (stato dell'oggetto).
 
 


 
 
 

Definizione della Classe Cronometro 
class Cronometro 

{ public: //------------------------ parte visibile all'utente della Classe 

Cronometro( ); // Costruttore

void Start( ); // Comando di partenza 

void Stop( ); // Comando di arresto 

void Reset( ); // Comando di azzeramento 

float Time( ) const; // funzione di osservazione: valore segnato dal cronometro

private: //------------------------------- parte nascosta all'utente della Classe 

float Ti; // Tempo iniziale: misura assoluta 

float TempoAccumulato; // tempo conteggiato dal reset 

bool StatoConteggio; // fermo: false, in funzione: true

}; //* Cronometro */ 

In questo esempio i metodi sono dichiarati in modo da esser visibili dall'utilizzatore (parte pubblica della classe), mentre invece i dati sono nascosti (parte privata).

I dati Ti, TempoAccumulato e StatoConteggio all'interno della classe serviranno per definire lo stato dell'oggetto di classe Cronometro, cioè per memorizzare delle informazioni relative al cronometro.

Vi è pure un metodo, con lo stesso nome della classe, che assomiglia ad una funzione ma che non restituisce alcunché neppure il tipo void, vedremo che servirà per inizializzare gli oggetti: per questo lo chiameremo costruttore (constructor).

Una porzione di codice che prova il Cronometro è la seguente:

 

Collaudo 
Cronometro CASIO; // creazione dell'oggetto di tipo Cronometro (viene invocato il costruttore)

CASIO.Reset( ); // non servirebbe, ma lo resettiamo lo stesso

CASIO.Start( ); // per farlo partire

int dato; // definizione di uno spazio di nome dato per memorizzare un intero

cin >> dato; // lettura da tastiera del numero con inserimento in dato 

CASIO.Stop( ); // ferma il cronometro

cout << CASIO.Time( );// visualizza il tempo impiegato dall'utente per inserire un dato intero

CASIO.Start( );// riprende la misura del tempo 

cin >> dato; 

CASIO.Stop(); 

cout << CASIO.Time( );// visualizza il tempo totale impiegato dall'utente per inserire due interi

Vediamo come definire la classe Cronometro definendo i vari metodi:
 
 
 

Costruttore
Cronometro::Cronometro( ) 
{ TempoAccumulato= 0; 
   StatoConteggio = false; // il cronometro inizialmente è fermo 
} //* Costruttore */

Questo è il costruttore che viene invocato ogni volta che un oggetto viene creato.

Nel nostro caso le azioni fatte dal costruttore sono di inizializzazione di alcune variabili nascoste nella parte privata della Classe.
 
 
 

metodo 

Start 

comando

#include <time.h> 
void Cronometro::Start( ) 
{ if ( ! StatoConteggio ) // se non e` in fase di conteggio 
    { StatoConteggio= true
       Ti = clock() / CLK_TCK; 
        // prendi il tempo assoluto in secondi e frazioni di secondo 
    } 
} //* Start */

Nel nostro cronometro facciamo l'ipotesi che il comando di start inibisca se stesso, nel senso che non si possono dare due comandi di start in successione.

clock è una funzione della libreria time.h che restituisce il tempo trascorso dall'inizio dell'invocazione del programma misurato in unità di impulsi-processore; per avere il tempo in secondi bisogna dividere il valore restituito da clock per la macro CLK_TCK, il risultato è un numero reale.
 
 
 

metodo 

Reset

comando

void Cronometro::Reset( ) // azzera il conteggio 
{ if ( StatoConteggio ) return; // se e` in fase di conteggio non funziona 
  TempoAccumulato = 0; 
} //* Reset */
metodo 

Stop

comando

void Cronometro::Stop( ) // ferma il conteggio 
{ if ( StatoConteggio ) // solo se e` in fase di conteggio
    { float Tf = clock( ) / CLK_TCK; // prendi il tempo assoluto 
      TempoAccumulato= TempoAccumulato + (Tf -Ti); 
      StatoConteggio= false
    } 
} //*Stop*/

Anche questo comando inibisce se stesso. Calcola l'istante assoluto Tf e memorizza il tempo accumulato fino a quell'istante nella variabile TempoAccumulato; in questo modo si può far ripartire il cronometro dal punto in cui era rimasto. 



 
 

metodo 

Time

osservazione

float Cronometro::Time( ) const // const significa funzione di osservazione
{ 
    if ( StatoConteggio ) // se il cronometro sta contando 
    { float Tf = clock( ) / CLK_TCK; // prendi il tempo assoluto attuale 
       return TempoAccumulato + (Tf -Ti); 
    } 
    else return TempoAccumulato; 
} //*Time*/

La funzione di interrogazione legge il tempo accumulato fino a quell'istante dal cronometro, se il cronometro è fermo legge direttamente dalla variabile TempoAccumulato.


  Sovrapposizione degli operatori in C++

Un esempio di insieme di caratteri


 

Il concetto di Insieme è un concetto primitivo: un Insieme è una collezione di oggetti distinguibili chiamati membri o elementi dell'insieme. Gli insiemi possono avere una dimensione, o cardinalità, finita o infinita.

In questa slide proponiamo la realizzazione di una classe per definire un nuovo tipo: SetCaratteri. L'insieme è un Insieme finito e contenente solo i caratteri ASCII.

Vediamo una porzione di programma per il collaudo della classe SetCaratteri :
 
 

Collaudo
//creazione di alcuni insiemi di caratteri con inizializzazione
SetCaratteri SetA = "ABXY", // il SetA contiene i caratteri ABXY
                  SetB = "ABCXYZ", 
                  SetC = "ADHXW", 
                  SetD; // insieme privo di elementi (vuoto)

//Visualizzazione degli insiemi creati
cout << "SetA"<< SetA << endl 
<< "SetB"<< SetB << endl 
<< "SetC"<< SetC << endl 
<< "SetD"<< SetD << endl; 
//Prova delle operazioni sugli insiemi 
cout <<"Intersezione: SetA * SetB =" << (SetA * SetB) << endl; 
cout <<"Unione: SetA + SetB =" << (SetA + SetB) << endl; 

 

cout <<"Differenza: SetA - SetB =" << (SetA - SetB) << endl; 

SetD += "JKLA"; //aggiunta nuovi elementi ad un insieme

 

cout <<"aggiunta nel SetD degli elementi JKLA =" << SetD << endl; 


if ('A' IN SetD ) 
cout <<"Verificata la presenza dell'elemento 'A' nel SetD \n";

 



cout <<" Verifica della prima legge di DeMorgan con i complementi: \n" 
"NOT (SetA * SetB) == NOT SetA + NOT SetB "; 

bool Verificata = !(SetA * SetB) == !SetA + !SetB; 
// oppure bool Verificata = NOT (SetA * SetB) == NOT SetA + NOT SetB; 

cout << (Verificata ? "Verifica riuscita" : "ERRORE nella verifica") << endl; 

Stiamo usando gli insiemi come se fossero tipi predefiniti del linguaggio. Si sarebbero potuti addirittura definire i termini "UNIONE" ed "INTERSEZIONE" al posto degli operatori " + " e " - ".

Vediamo l'uscita a video provocata dalle nostre istruzioni:


SetA [ ABXY ] 
SetB [ ABCXYZ ] 
SetC [ ADHWX ] 
SetD [ ] 

Intersezione: SetA * SetB = [ ABXY ] 
Unione: SetA + SetB = [ ABCXYZ ] 
Differenza: SetA - SetB = [ ] 

aggiunta nel SetD degli elementi JKLA = [ AJKL ] 

Verificata la presenza dell'elemento 'A' nel SetD 

Verifica della prima legge di DeMorgan con i complementi:
NOT (SetA * SetB) == NOT SetA + NOT SetB 

Verifica riuscita

Uscita a video


Così come le funzioni possono essere sovrapposte, anche gli operatori, sia binari che unari, possono essere sovrapposti, cioè può essere ridefinito il loro comportamento quando si trovano ad operare con operandi-oggetti definiti dall'utente.

Questo ci permette un alto livello di astrazione.
 
 

Definizione della Classe SetCaratteri
con la sovrapposizione degli operatori
class SetCaratteri 
{ public:

  SetCaratteri( ); // costruttore per il Set vuoto 
  SetCaratteri(char *); // inizializzazione con un insieme di caratteri 
  SetCaratteri operator + (const SetCaratteri & ) const; // unione 
  SetCaratteri operator * (const SetCaratteri & ) const; // intersezione 
  SetCaratteri operator - (const SetCaratteri & ) const; // differenza 
  SetCaratteri operator ! ( ) const; // complemento 
  void operator += ( char ); // aggiunta di un elemento 
  void operator += ( char* ); // aggiunta di una sequenza di caratteri 
  friend bool operator <= ( char, const SetCaratteri & ); // appartenenza 
  #define IN <= 

  #define NOT !  

  friend ostream & operator << (ostream &, const SetCaratteri & ); 

private

  bool Vett[128]; // 128 è la cardinalità massima dell'insieme 
  // vettore che indica la presenza (true) o l'assenza (false) dell'elemento dell'insieme
  void CreaInsiemeVuoto( ); // funzione di comodo 

}; //*class SetCaratteri*/

La struttura interna che contiene le informazioni è costituita da un semplice vettore a dimensione fissa.
L'idea di base, per la realizzazione della classe SetCaratteri, è quella di utilizzare un vettore di booleani il quale con la presenza del valore true/false nella posizione i-esima, indichi la presenza/assenza del carattere i-esimo (di codice i). 
Questa soluzione ci permette di semplificare le definizioni delle operazioni tipiche degli Insiemi; ma non sarebbe adatta per Insiemi che possono avere una elevata cardinalità.

Passiamo alla definizione dei vari metodi:
 

void SetCaratteri:: CreaInsiemeVuoto( ) // funzione di comodo
{ for ( int i=0; i<128; i++ ) Vett[i]= false; }
//------------------------------------------------------------- Costruttore di default 
SetCaratteri::SetCaratteri( ) // inizializzazione insieme vuoto 
{ CreaInsiemeVuoto( ); }
//--------------------------------------------------------------- Costruttore 
SetCaratteri::SetCaratteri(char * S) // inizializzazione con un insieme di caratteri 
{ CreaInsiemeVuoto( ); 
   int L = strlen(S); 
   for ( i=0; i<L; i++ ) Vett[ S[i] & 127 ]= true
}
//--------------------------------------------------- Inserimento di un elemento 
void SetCaratteri:: operator += ( char elem ) 
{ Vett[elem & 127] = true; }
//-------------------------------------------------- Inserimento di una sequenza di caratteri 
void SetCaratteri:: operator += ( char * S ) 
{ if (S) while (*S) Vett[*S++ & 127] = true; }

Si suppone che i caratteri immessi abbiano codice compreso tra lo zero e il 127. Un approccio difensivo imporrebbe il controllo di tutti i caratteri inseriti, eliminando i caratteri non ASCII. Noi abbiamo risolto il problema "filtrando" i dati d'ingresso:
Vett[ elem & 127 ] = true;      Vett[ (*S++) & 127 ] = true;

La definizione degli operatori è del tutto simile alla definizione delle funzioni, se pensiamo di sostituire "operator + " con il termine "somma", più nulla distingue la definizione di una funzione dalla definizione di un operatore.
     

UNIONE SetCaratteri SetCaratteri:: operator + (const SetCaratteri & SetB ) const
{ SetCaratteri Ris; // contenitore del risultato 
   for ( int i=0; i< 128; i++ ) 
   { if ( Vett[i] == SetB.Vett[i] ) Ris.Vett[i] = Vett[i]; 
     else Ris.Vett[i] = true; // almeno uno dei due insiemi possiede l'elemento 
   }
 return Ris; 
}//* Unione */ 
INTERSEZIONE SetCaratteri SetCaratteri:: operator * (const SetCaratteri & SetB ) const
{ SetCaratteri Ris; // contenitore del risultato 
   for ( int i=0; i< 128; i++ ) 
   { if ( Vett[i] == SetB.Vett[i] ) Ris.Vett[i] = Vett[i]; 
     else Ris.Vett[i] = false; // solo uno dei due insiemi possiede l'elemento 
   }
 return Ris; 
}//* Intersezione */ 
DIFFERENZA SetCaratteri SetCaratteri:: operator - (const SetCaratteri & SetB ) const
{ SetCaratteri Ris; // contenitore del risultato 
   for ( int i=0; i< 128; i++ ) 
    { if ( Vett[i] != SetB.Vett[i] ) Ris.Vett[i] = Vett[i]; 
      else Ris.Vett[i] = false
    }
 return Ris; 
}//* Differenza */ 

Infine, sfruttando la relazione d'ordine tra il valore false e true, possiamo risolvere i problemi di inclusione dei sotto insiemi.
   

//-------------- Appartenenza di un elemento all'insieme (operatore ridefinito con IN
bool operator <=( char elem , const SetCaratteri & Set ) 
{
      return Set.Vett[elem]; 
}
//----------------------------------------------------------- operatore di flusso d'uscita 

ostream & operator << (ostream & Flusso, const SetCaratteri & Set ) 
{ Flusso << " [ "; 
  for ( int i=0; i< 128; i++ ) if ( Set.Vett[i] ) Flusso << char(i); 
  Flusso << " ] "; 
  return Flusso; 
}

La definizione dell'operazione di complemento viene lasciata al lettore. Nel nostro caso l'insieme universo possiede 128 caratteri e con questa implementazione risulta semplice ricavare l'insieme complemento.



 Classi generiche o tipi parametrizzati (template)

Un esempio di vettore di oggetti generici

In C++ un tipo "parametrizzato" viene chiamato una classe modello (class template).

Una classe modello permette di definire un "prototipo" per la definizione di classi; per questo viene anche detta classe generica o generatrice di classi. Con il meccanismo delle classi modello abbiamo la possibilità di generare istanze di una classe (e da questa oggetti) legate ad un particolare tipo di dato, scelto di volta in volta.

Un template definisce una famiglia di tipi

I contenitori di oggetti sono i tipi più adatti a dimostrare l'utilità delle classi modello, infatti un modello contenitore, come può essere una classe array, potrebbe essere utile per generare contenitori indicizzati per ogni tipo di dato
   

Definizione della Classe modello Vettore
template <class T, int N>
class Vettore
{ public:
Vettore( ); // costruttore di default
Vettore( const Vettore & ); // Copy Constructor
~Vettore( ); // distruttore
Vettore & operator= ( const Vettore & ); //assegnamento
T & operator[ ] ( unsigned int ); //operatore di indicizzazione
private:
T *pV; // puntatore alla struttura vettore di T
unsigned int Dim; // dimensione del Vettore

};//* classe Vettore*/

Implementazione della classe Vettore
 

template <class T, int N> // costruttore
Vettore<T,N>::Vettore( )
{
pV = new T[Dim= N]; // creo uno spazio di N elementi generici T
}//*Constructor*/
template <class T, int N> // costruttore di copia
Vettore<T,N>::Vettore( const Vettore<T,N> & vett )
pV= new T[Dim= vett.Dim];
for ( int r = 0; r < Dim; r++ ) pV[r] = vett.pV[r]; // ricopio
}//*CopyConstructor*/
template <class T, int N> // distruttore
Vettore<T,N>::~Vettore( )
{ delete [ ] pV; }
template <class T, int N> // assegnamento
Vettore<T,N> & Vettore<T,N>::operator= ( const Vettore<T,N> & vett )
if (this== &vett) return *this; // caso: VettA = VettA
delete[ ] pV;
pV= new T[Dim= vett.Dim]; // creo nuovo spazio per il vettore
for ( int r = 0; r < Dim; r++ ) // ricopio
pV[r] = vett.pV[r];
   return *this;

}//*assegnamento*/

template <class T, int N> // operatore di indicizzazione
T& Vettore<T,N>::operator[ ] ( unsigned int i )
{
      return pV[i];
}//*operatore di indicizzazione*/


Un programma principale di collaudo che crea un vettore di oggetti di un tipo definito dall'utente. In questo caso si tratta del tipo Tartaruga (come le tartaruge del linguaggio LOGO) che viene istanziato su sette elementi del Vettore:
 

Collaudo
#include <easy_C3.h> 
main( )//=== programma di prova: vettore di oggetti Tartaruga =====
     const int N = 7;
     Vettore<Tartaruga, N> ListaT; // crea una lista di Tartarughe
    for( int i = 0; i < N; i++ ) 
    { ListaT[ i ].Destra( rand( )*360 ); // gira a destra di un angolo a caso
       ListaT[ i ].Avanti( 70 ); 
    }
    WaitESC( );//aspetta che l'utente prema il tasto <ESC>
return 0;

}

Uscita a video


Un altro esempio di collaudo crea una matrice di interi, o meglio, un vettore i cui elementi sono vettori di interi:
 

Collaudo
#include <iostream.h>
main( )//=== programma di prova: vettore di vettori di interi =========
{
    const int R=7, C=10;
    Vettore <Vettore< int, C>, R> mat; // definizione di una matrice R x C
    // carico la matrice con i valori della tavola pitagorica
    for( int r = 0; r < R; r++ ) 
    for( int c = 0; c < C; c++ )
    mat[r][c]=(r+1)*(c+1);
    // mando a video la tavola pitagorica
    cout <<"\n\t========== TAVOLA PITAGORICA =========\n";
    for( int r = 0; r < R; r++ )
    { for( int c = 0; c < C; c++ ) cout << mat[r][c] <<'\t';
       cout <<endl;
    }
cin.get( ); //aspetta che l'utente premi un tasto

return 0; 
}

===== TAVOLA PITAGORICA ========
 1   2   3   4   5   6   7   8   9  10
 2   4   6   8  10  12  14  16  18  20
 3   6   9  12  15  18  21  24  27  30
 4   8  12  16  20  24  28  32  36  40
 5  10  15  20  25  30  35  40  45  50
 6  12  18  24  30  36  42  48  54  60
 7  14  21  28  35  42  49  56  63  70
Uscita a video


La classe SetCaratteri, la classe Tartaruga e molte altre classi di utilità didattica, sono state implementate dall'autore per il compilatore Borland C++ 3.1 e disponibili nella "easy_c3.lib", con la relativa documentazione.
Si possono scaricare dal sito http://margherita.record.unipd.it/~conte/indext.html
oppure dalla directory "easy_c".

Questa sezione è incompleta, molti altri argomenti sarebbero ancora da trattare, e questi esposti da approfondire, cercheremo di completare l'opera in futuro.