Miglioramento dell'IO |
Gli operatori di I/O che abbiamo scritto per la nostra classe fraction
lasciano un po' a desiderare. << stampa il denominatore anche quando
è 1 e il formato di stampa è solo monolinea. >> non gestisce
il caso di un carattere : o / in input fra numeratore e denominatore. Queste
erano le versioni iniziali:
ostream& operator<<(ostream& os, const fraction&
f)
istream& operator>>(istream& is, fraction& f)
Vogliamo che << non stampi il denominatore 1 e che l'eventuale segno - appaia solo davanti al numeratore. Ecco la nuova versione. Possiamo utilizzare il passaggio per valore se non vogliamo alterare i segni di _num e _denom del chiamante; oppure il passaggio per riferimento che però adesso non può più essere il riferimento a una costante (occorre rimuovere il const perchè modifichiamo l'oggetto di invocazione). Ho scelto il passaggio per valore, poichè permette di stampare anche una costante di tipo fraction. ostream& operator<<(ostream& os, fraction f)
Adesso (-3,-5) viene stampata come 3/5 e non come -3/-5, la frazione apparente 40/2 viene stampata come 20 e non come 20/1 ecc... Per quanto riguarda la stampa in formato multilinea, come 3
scriviamo una funzione membro display che effettua questo tipo di visualizzazione. Per fortuna la programmazione spesso è divertente :) In questo caso io trovo divertente l'implementazione di questa funzione, nonostante le difficoltà. Per prima cosa dobbiamo vedere se c'è il segno meno (infatti ne stamperemo uno solo). Potremmo volendo anche stampare (3,-4) con i propri segni, così: 3
fare così è ancora più facile, ma non mi piace. Per stabilire il numero di trattini da stampare, occorre trovare la lunghezza (in caratteri) del numero più lungo e aggiungerci due. La lunghezza del numeratore e del denominatore serve anche per calcolare la dimensione del campo con cui essi stessi devono essere stampati. Questa va calcolata in modo che i numeri appaiano approssimativamente entrambi centrati: XXX
Se c'è un meno davanti alla frazione occorre aggiungere due alla dimensione del campo. Nel caso di numeratore lungo 3 cifre e denominatore lungo 5 cifre senza alcun segno meno, vedi disegno di sopra, occorrono: max(3,5)+2=5+2=7 trattini e YYYYY va stampato allineato a destra su un campo di 5+1=6 caratteri, o più in generale (7-5)/2+5 XXX va stampato allineato a destra su un campo di (7-3)/2+3=4/2+3=5 caratteri se ci fosse un meno, occorrerebbe aggiungere due ai campi così calcolati che sarebbero così 8 e 7 rispettivamente. E' immediato (anzi divertente) tradurre queste considerazioni nel codice della funzione display( ): # include <iomanip.h>
char _numlen=numlen(_num),
cout << setw((dashes-_numlen)/2+_numlen) << _num << endl; // print dashes
cout << endl;
Questa prima versione non gestisce i numeri negativi. numlen è una funzione di nome simile a strlen del C che restituisce la lunghezza di una stringa: numlen analogamente restituisce la "lunghezza" di un numero, inteso non come il suo valore assoluto, ma come il numero di cifre che lo compongono. La funzione per il valore assoluto è standard e si chiama abs, questa invece la dobbiamo scrivere noi. Niente di difficile: si contano le cifre dividendo ripetutamente per 10 finchè si ottiene 0: static char numlen(long n)
Notate che ho usato i char come piccoli interi. Siccome non vogliamo esportare questa funzione all'esterno (sarebbe fuori luogo in una classe di questo tipo), la soluzione più elegante mi è sembrato di renderla una funzione statica in frac.cc max è una funzione inline (pure statica di frac.cc) generica: template <class T>
Provate il codice finora scritto. Bello, no? Ok. Aggiungiamo ora la gestione del segno. Ho aggiunto una variabile minus che se è zero significa che non deve essere stampato il segno meno davanti alla frazione, altrimenti vale 2, appunto lo spazio occupato dal segno meno e dal blank che lo segue. Il segno della frazione è pari al segno del prodotto di numeratore e denominatore. Notare che occorre aggiungere le chiamate a labs. Mentre abs opera su int, labs opera su long (e restituisce un long). Entrambe sono standard (ereditate dal C) e definite in <stdlib.h>. # include <iomanip.h>
char _numlen=numlen(_num),
cout << setw((dashes-_numlen+1)/2+_numlen+minus) << labs(_num) << endl; // print the possible minus sign
// print dashes
cout << endl;
Ho aggiunto quel +1 a dashes-_numlen e dashes-_denomlen prima di dividerlo per 2, perchè mi piace di più un arrangiamento del tipo 1
10
invece che 1
10
questa è una pura questione di gusti! Infine occorre far sì che il denominatore non venga stampato se è 1. Questo è banale, basta gestirlo come caso particolare. Inoltre ora viene data la possibilità di stampare con display su qualsiasi stream (ad es. su un file) passandolo come argomento. Lo stream di default naturalmente è cout. # include <iomanip.h>
const char dash='-'; char _numlen=numlen(_num),
os << setw((dashes-_numlen+1)/2+_numlen+minus) << labs(_num) << endl; // print the possible minus sign
// print dashes
os << endl;
Niente male per un classe standard che opera I/O in modo testo, no? Nota: la dichiarazione di display nel file frac.h si scrive in questo modo: void display(ostream& = cout) const; ho appositamente non voluto mettere il nome del parametro, per illustrarvi un punto. Se scrivete la dichiarazione in questo modo: void display(ostream&=cout) const; più precisamente senza spazi tra & e =, il lexical scanner (l'analizzatore sintattico del compilatore) considera la sequenza &= come un unico operatore (infatti questo operatore esiste in C/C++) ed otterrete un errore in compilazione. Qundi il primo spazio, quello tra, ripeto, & e =, è necessario. Il problema non si verifica se nominate il parametro nella dichiarazione, ovviamente. Un ultimo miglioramento lo apportiamo all'operator>>, facendo sì che salti un eventuale (cioè opzionale) carattere / o : tra i due numeri: #include <ctype.h>
is >> f._num; while (isspace(c=is.get()))
is >> f._denom; f.ExceptionIfDenom0();
return is;
Dopo aver letto il numeratore, un ciclo salta tutti i bianchi e simili (compreso il newline e i tab). Il primo non bianco letto viene rimesso nell'input, a meno che non sia il carattere / o : che invece viene ignorato. La funzione (metodo) standard putback è l'analogo di una vecchia conoscenza del C, detta ungetc. putback può rimettere nell'input anche un carattere diverso dall'ultimo letto; un'altra soluzione è usare is.unget( ) che non richiede argomenti, in quanto la classe istream conosce l'ultimo carattere che abbiamo letto. L'operator>> sembrerebbe finito, ma c'è un ultimo dettaglio di cui tener conto: se l'istruzione is >> f._denom; fallisce, perchè ad es. è finito l'input oppure si è incontrato un carattere non cifra, operator>> invece di lasciare in f._denom il suo precedente valore, dovrebbe assumere che il denominatore è 1 (se scrivete come frazione "1 ciccia" allora si intende la frazione uguale ad 1). Ecco come ciò può essere ottenuto: if (!is)
Per i nostri scopi, potete scrivere anche così: if (is.fail())
oppure if (!is.good())
Ho scelto la prima soluzione che è più breve. Per maggiori informazioni, sotto Linux o DOS con DJGPP digitare: info iostream streams ios states Pensandoci bene, nel caso dovete leggere più frazioni contemporaneamente, questa versione obbliga ad indicare i denominatori anche se sono uno. Ad es. supponendo volete dare in input i numeri 1 e 2, dovete scrivere: 1/1 2/1
se scrivete 1 2 viene interpretato come la frazione 1/2. Questa versione di operator>> obbliga l'uso di / o : e non presenta questo problema. Inoltre la lettura del numero frazionario è considerata terminata al sopraggiungere di un newline, perchè questo è il comportamento "interattivo" da tenere. Il newline viene saltato. Se in input non c'è un numero legale, viene letto uno zero. Il chiamante può pui verificare lo stato dello stream (vedi 4/test_io.cc). E' quella che preferisco: istream& operator>>(istream& is, fraction& f)
f._denom=1; if (!(is >> f._num))
Qualche ultima funzionalità: un convertitore di tipo da fraction a double che permette ad es. di assegnare una frazione ad un double. Naturalmente si ha una perdita di "precisione", così come si ha una perdita di precisione quando si assegna un double ad un float o peggio ad un tipo intero. operator double( ) const { return (double)_num/_denom; } il cast (double) è necessario, altrimenti viene eseguita la divisione tra interi long. Es.
cout << d; // stampa 0.333333 Un problema introdotto dall'aggiunta di questo convertitore di tipo è il seguente. Provate a compilare il seguente programma: // prova.cc #include "frac.h" int main( )
5*f; exit(0);
Ecco l'errore che otterrete: prova.cc: In function `int main()':
Notate che il problema si risolve con un semplice cast: frac(5)*f; oppure (frac)5*f; Se non ci fosse il convertitore di tipo non ci sarebbero questi problemi. Infatti il problema è un classico problema di ambiguità nella chiamata delle funzioni sovraccariche: il compilatore non sa se convertire f in double e chiamare l'operator* predefinito (builtin) che fa il prodotto di un int (il 5) con un double, oppure se chiamare il costruttore di frac su 5 e moltiplicare due frazioni. Entrambe le operazioni implicano delle conversioni ed entrambi gli operator* hanno la stessa priorità di chiamata. Naturalmente lo stesso problema si verifica se scrivete f*5; E' una noia costringere l'utente al ricorso a cast espliciti in questo caso: infatti f è una specie di sovratipo di int, una estensione degli interi: se moltiplicate un int con un double, il risultato è un double senza bisogno di castare (ehilà un nuovo termine gente) l'int a double. Dovrebbe essere possibile la stessa cosa con frac e interi. Per imporre il cast automatico basta definire operatori-macro (cioè inline) tipo questo: friend fraction operator*(const fraction& f, long n)
Queste definizioni fanno si che mescolando fraction e long (o tipi convertibili a long) con gli operatori, venga fornito il cast implicito a fraction. Per esempio se scrivo 5*f viene chiamato il costruttore per creare una frazione temporanea 5/1 che poi viene moltiplicata per f chiamando l'operator*(const fraction&,const fraction&). Questo funziona perfettamente: l'operatore moltiplica il 5 per il numeratore di f, l'uno per il denominatore e riduce ai mintermini la frazione ottenuta. Per ragioni di ottimizzazione però ho preferito fare un po' di lavoro in più e fornire delle versioni apposite di questi operatori (nel caso di * ad es. evito la moltiplicazione per 1 che per il calcolatore ha lo stesso costo di una qualsiasi altra moltiplicazione tra interi e soprattutto evito la chiamata del costruttore che il cast comporta, cioè la costruzione e distruzione di un oggetto temporaneo): friend fraction operator*(const fraction& f, long n)
La prima di queste funzioni, così come operator*(const fraction&, const fraction&) possono ora essere anche implementate come funzioni membro anzichè friend. Per simmetria comunque ho deciso di continuare ad usare funzioni friend. Notare che l'operator*(const fraction&, long) può essere ricavato anche chiamando operator*(long, const fraction&) e viceversa per la proprietà commutativa del prodotto, senza alcun overhead se la definizione è inline. Questo è preferibile perchè riduce la dimensione del codice se il primo non è inline e sottolinea elegantemente la proprietà commutativa. Ecco la terna di operatori * (tutte le altre terne sono simili): friend fraction operator*(const fraction&, const
fraction&);
Lo stesso problema si presenta con gli operatori di confronto, perciò ci sono 3 versioni per ogni operatore. Ho deciso di rendere le versioni miste tutte inline, vista la loro semplicità. Conviene poi implementare gli operatori + e - unari. Questi possono essere implementati come funzioni membro. L'operatore + unario non fa niente, eccetto che ritornare un valore copia dell'oggetto chiamando il costruttore di copia (predefinito dal compilatore), oppure il costruttore con due argomenti, mentre il - unario inverte il segno della frazione: possiamo scegliere di invertire il segno del numeratore o del denominatore. Scegliamo ad es. il numeratore. fraction operator+( ) const { return fraction(*this); }
Infine vogliamo poter effettuare facilmente potente delle nostre frazioni. Possiamo fare una funzione membro, chiamando ad es. pow (abbreviazione di power), ma è sintatticamente più bello ridefinire l'operatore ^ (chiamato caret). Siccome elevare ad un esponente razionale può produrre numeri irrazionali (es. elevare ad 1/2 equivale a fare la radice di 2), ci limiteremo a esponenti appartenenti all'insieme Z dei numeri relativi (i numeri interi con segno). L'algoritmo deve comportarsi bene nel caso di esponente 0 (il risultato è 1). Notiamo inoltre che se la frazione da elevare a potenza è già ridotta ai minimi termini (e siamo in questo caso), cioè numeratore e denominatore non hanno fattori in comune, nemmeno ce li avranno le loro potenze. In altri termini non occorre ridurre il risultato ai minimi termini (gioco di parole). Non è quindi conveniente chiamare l'operatore * per implementare ^, anche perchè vogliamo evitare chiamate dei costruttori ad ogni moltiplicazione. Quando l'esponente è negativo, basta scambiare numeratore e denominatore. Es.
Infine occorre gestire un caso di errore di divisione per zero: quando x=0 e y<=0 e voglio calcolare x^y (x e y essendo due frazioni). operator^ opera sempre tra una frazione (1° operando) è un numero relativo (2° operando), quindi può essere una funzione membro (e conviene implementarlo così). Per semplicità non controlliamo le situazioni di overflow (non lo abbiamo mai fatto). Come al solito operiamo per raffinamenti successivi (può sembrare una banalità ma questa è una delle tecniche di programmazione principali). Questa prima versione gestisce solo esponenti positivi o nulli e base diversa da zero: fraction fraction::operator^(long exp)
num=den=1;
return fraction(num, den);
Se l'esponente è negativo, basta calcolare la potenza con esponente positivo e poi invertire i termini di num e den nella chiamata del costruttore: fraction fraction::operator^(long exp)
num=den=1;
if (exp<0)
Infine un test dell'errore all'inizio conclude la nostra routine: fraction fraction::operator^(long exp) const
long num, den; num=den=1;
if (exp<0)
Qui c'è da precisare che 00 (zero elevato zero) costituisce pure un caso di errore. Che ne dite poi una funzione valore assoluto? Eccola: inline fraction fraction::abs( )
Nello zip allegato, nella sottodirectory 4/ trovate il codice completo
di questa "ultima" versione della classe fraction. Ovviamente potrebbe
esserci qualche funzione che ho dimenticato o dei miglioramenti da fare.
"Un programma non è mai finito ma da qualche
parte bisogna pure fermarsi"
- Peter Norton Come vedremo nella prossima pagina avremo ancora dei miglioramenti e delle aggiunte per la nostra classe fraction. |
![]() |
![]() |
![]() |