martedì, novembre 28, 2006

array

Vorrei una classe che gestisse in maniera automatica l'allocazione dinamica della memoria, una maniera (efficiente) per poter dichiarare un vettore di lunghezza determinata run-time, ma con una sintassi analoga a quella dei vettori statici. Consideriamo, per semplicità, il caso di vettori di interi.
Chiamiamo questa classe ArrayInt, vorrei poter fare una dichiarazione come segue:

int a;
std::cin >> a;
ArrayInt A(a);


in cui A rappresenti un vettore (dichiarato dinamicamente) di a interi. La classe, poi fornisca elementari metodi per accedere ai singoli elementi di A. Inoltre - importantissimo - la classe si preoccupi di definire correttamente un distruttore che rilasci la memoria allocata dinamicamente, in maniera da evitare memory-leak.
NB: la soluzione di questo esercizio sarà il punto di partenza della prossima lezione, verrà dato per scontato che abbiate provato (e siate riusciti!) a risolverlo.

domenica, novembre 26, 2006

esercizi

Come ripetiamo sempre, per un corso come questo la teoria serve molto poco: bisogna provare, provare, provare, provare, pro...vare, provare, provare e poi ci si riesce bene... (anche due, tre volte!).
Ecco dunque qualche esercizio davvero molto semplice, che possa servirvi come pretesto per mettervi davanti al computer e programmare, ed in particolare per completare una classe istogramma ed usarla (mi raccomando: anche usare direttamente l'implementazione di pietro della classe istogramma va benissimo, l'importante è che proviate almeno ad usarla in un programmino scritto da voi, in modo da impratichirvi con i concetti di classe, metodo, et cetera...!)
Ebbene, ecco gli esercizi: con una quache classe di istogramma a disposizione,
  1. studiare la distribuzione statistica della funzione stocastica float X4() qui definita (visualizzarne un'istogramma generato con un numero elevato, 1 000, 1 000 000 di punti, calcolare la media e la deviazione standard della distribuzione...); in che intervallo potranno cadere i punti generati dalla funzione? è proprio questo intervallo che conviene scegliere per disegnare l'istogramma?
  2. studiare la distribuzione statistica della funzione stocastica float X(const int& n) qui definita, per un fissato valore del parametro di ingresso n; in che intervallo potranno cadere i punti generati dalla funzione? è proprio questo intervallo che conviene scegliere per disegnare l'istogramma?
  3. studiare la distribuzione statistica della funzione stocastica float XG(const int& n) qui definita, per un fissato valore del parametro di ingresso n; quale intervallo conviene scegliere, per visualizzare l'istogramma? come cambia la distribuzione generata dalla funzione al variare del parametro n? come cambia il valor medio? e la deviazione standard?
  4. Cosa indicherà mai la G in quest'ultima funzione stocastica? Sareste in grado di scrivere una funzione stocastica float XG12(const float& m, const float& s) che generi valori secondo una distribuzione di valor medio m e varianza s, passati come argomenti?
  5. Sapreste scrivere una funzione stocastica float G()che generi valori secondo una distribuzione davvero gaussiana (ops!), chessò, centrata in zero e larga uno? Magari usando il metodo dell'inversione visto accennato in una delle prime lezioni?
poscritto
Ok, l'ultimo esercizio è davvero difficile. Se qualcuno dovesse arrivare con la soluzione... sapremmo con certezza che non l'avrebbe "inventata" lui (ma, se sapesse anche spiegarla, meriterebbe comunque un bonus di 100 punti!). Se poi qualcuno vuole provare comunque a "inventarsi" la soluzione, può partire dalle slides delle prime lezioni sul metodo dell'inversione, che può scaricare (ovvero non visualizzare, ma downloadare) qui.

venerdì, novembre 24, 2006

l'istogramma implementato

ecco una possibile implementazione di istogramma.cc

#include "istogramma.h"
#include <cmath>
#include <iostream>


istogramma::istogramma () :
m_somma (0.) ,
m_sommaSq (0.) ,
m_entries (0) ,
m_binsNumber (0) ,
m_bins (0) ,
m_minimum (0) ,
m_maximum (0) ,
m_invStep (0)
{}


// ----------------------------------------------------------------------------


istogramma::istogramma (const double & minimo,
const double & massimo,
const int & bins) :
m_somma (0.) ,
m_sommaSq (0.) ,
m_entries (0) ,
m_binsNumber (bins) ,
m_bins (new int[m_binsNumber]) ,
m_minimum (minimo) ,
m_maximum (massimo) ,
m_invStep (m_binsNumber/(m_maximum - m_minimum))
{
// TODO: controlli
// azzero gli elementi dell'istogramma
for (int i=0; i<m_binsNumber; ++i)
{
m_bins[i] = 0 ;
}
}


// ----------------------------------------------------------------------------


istogramma::istogramma (const istogramma & original) :
m_somma (original.m_somma) ,
m_sommaSq (original.m_sommaSq) ,
m_entries (original.m_entries) ,
m_binsNumber (original.m_binsNumber) ,
m_bins (new int[m_binsNumber]) ,
m_minimum (original.m_minimum) ,
m_maximum (original.m_maximum) ,
m_invStep (m_binsNumber/(m_maximum - m_minimum))
{
for (int i=0; i<m_binsNumber; ++i)
{
m_bins[i] = original.m_bins[i] ;
}
}


// ----------------------------------------------------------------------------


istogramma::~istogramma ()
{
delete [] m_bins ;
}


// ----------------------------------------------------------------------------


istogramma & istogramma::operator= (istogramma const & original)
{
if (m_binsNumber) delete [] m_bins ;
m_somma = original.m_somma ;
m_sommaSq = original.m_sommaSq ;
m_entries = original.m_entries ;
m_binsNumber = original.m_binsNumber ;
m_bins = new int[m_binsNumber] ;
m_minimum = original.m_minimum ;
m_maximum = original.m_maximum ;
m_invStep = m_binsNumber/(m_maximum - m_minimum) ;

for (int i=0; i<m_binsNumber; ++i)
{
m_bins[i] = original.m_bins[i] ;
}
return *this ;
}


// ----------------------------------------------------------------------------


int
istogramma::fill (const double & value)
{
m_somma += value ;
m_sommaSq += value * value ;
++m_entries ;
int bin = static_cast<int> (floor ((value-m_minimum) * m_invStep)) ;
++m_bins[bin] ;
return m_bins[bin] ;
}


// ----------------------------------------------------------------------------


double
istogramma::getMean () const
{
if (m_entries) return m_somma / m_entries ;
else return 0 ;
}


// ----------------------------------------------------------------------------


double
istogramma::getSigma () const
{
if (m_entries) return sqrt ((m_sommaSq - m_somma*m_somma/m_entries)/m_entries) ;
else return -1 ;
}


// ----------------------------------------------------------------------------


int
istogramma::getEntries () const
{
return m_entries ;
}


// ----------------------------------------------------------------------------


void
istogramma::print (int altezza, int larghezza) const
{
int maxIsto = getMaxValue () ;

// disegno le singole barre:
std::cout << " +---------------------->\n" ;
// loop sugli elementi dell'istogramma
for (int i=0; i<m_binsNumber; ++i)
{
int numeroSimboli = m_bins[i] * altezza / maxIsto ;
std::cout << " | " ;
for (int j=0; j<numeroSimboli; ++j) std::cout << "#" ;
for (int j=numeroSimboli; j<altezza+2; ++j) std::cout << " " ;
std::cout << "m_bins[" << i << "] : " << m_bins[i] ;
std::cout << "\n" ;
} // chiuso loop sugli elementi dell'istogramma
std::cout << " |\n\\/\n\n" ;
std::cout << "media = " << getMean () << "\n" ;
std::cout << "sigma = " << getSigma () << "\n" ;
return ;
}


// ----------------------------------------------------------------------------


void
istogramma::printOr (int altezza, int larghezza) const
{
int maxIsto = getMaxValue () ;
// istogramma "re-binnato" per farne il plot in orizzontale
int * localBins ;

// se larghezza e' maggiore del numero di bin,
// cerca di plottare l'istogramma rispettando le proporzioni
// allargandolo il piu' possibile
if (larghezza/m_binsNumber)
{
int volte = larghezza/m_binsNumber ;
localBins = new int[m_binsNumber*volte] ;
for (int i=0 ; i<m_binsNumber ; ++i)
for (int j=0 ; j<volte; ++j)
localBins[i*volte+j] = m_bins[i] ;
larghezza = volte * m_binsNumber ;
}
else
{
int volte = m_binsNumber/larghezza ;
localBins = new int[m_binsNumber/(volte+1)+1] ;
for (int i=0 ; i<m_binsNumber/(volte+1)+1 ; ++i) localBins[i] = 0 ;
for (int i=0 ; i<m_binsNumber ; ++i)
localBins[i/(volte+1)] += m_bins[i] ;
larghezza = m_binsNumber/(volte+1)+1 ;
maxIsto *= (volte+1) ;
}

std::cout << "\n/\\\n|\n" ;
// loop sulle righe
for (int i=0 ; i<altezza; ++i)
{
std::cout << "| " ;
// loop sui caratteri della riga
for (int j=0 ; j<larghezza; ++j)
{
int numeroSimboli = localBins[j] * altezza / maxIsto ;
if (altezza-i <= numeroSimboli) std::cout << "#" ;
else std::cout << " " ;
} // chiuso loop sui caratteri della riga
std::cout << "\n" ;
} // chiuso loop sulle righe
std::cout << "+-" ;
for (int j=0 ; j<larghezza+1; ++j) std::cout << "-" ;
std::cout << ">\n" ;
delete [] localBins ;
std::cout << "media = " << getMean () << "\n" ;
std::cout << "sigma = " << getSigma () << "\n" ;
return ;
}


// ----------------------------------------------------------------------------


int
istogramma::getMaxValue () const
{
// cerco il massimo dell'istogramma:
int maxIsto = 0 ;
// loop sugli elementi dell'istogramma
for (int i=0; i<m_binsNumber; ++i)
{
if (maxIsto < m_bins[i]) maxIsto = m_bins[i] ;
} // chiuso loop sugli elementi dell'istogramma
return maxIsto ;
}

venerdì, novembre 17, 2006

compiti

A/ Scrivere una classe che implementi il campo dei numeri complessi. In particolare implementi almeno:
  • il creatore void, il creatore con un argomento float/double (la parte reale), il creatore con due argomenti float/double (parte reale e immaginaria), il copy-constructor (costruttore con argomento complesso);
  • l'overload degli operatori aritmetici elementari (somma, sottrazione, meno-unario, prodotto, divisione);
  • i metodi per le operazioni più elementari sui numeri complessi (coniugio, modulo, modulo-quadro, fase...)
ed eventualmente
  • l'overload degli operatori in notazione compatta (+=, -=, *=, /=).

B/
Scrivere una funzione (non un metodo della classe complessi!!!)
int RunOut(complesso C, int N);
che si comporti nel seguente modo:
  1. inizializzi a zero un complesso Z;
  2. imposti un ciclo al cui interno il valore di tale complesso Z venga sostituito dal suo quadrato aumentato di C (il primo argomento della funzione), ovvero esegua: Z = Z*Z + C;
  3. esca dal ciclo al verificarsi (anche) di una (sola) di queste eventualità:
    1. il modulo di Z sia diventato maggiore di 2;
    2. siano state effettuate già N iterazioni del ciclo (N è il secondo parametro della funzione);
  4. una volta fuori dal ciclo, la funzione restituisca il numero di cicli effettuati (che dunque non potrà superare N, e sarà minore se il modulo di Z avrà superato il valore 2).

C/ Scrivere un programma principale che, facendo uso della classe dei numeri complessi e della funzione di cui al punto B, si comporti nel seguente modo:
  1. chieda all'utente di specificare un intervallo rettangolare I del piano complesso (traduco: chieda all'utente di specificare due numeri complessi z1 e z2 da considerare rispettivamente l'estremo superiore-sinistro e inferiore-destro di tale intervallo... (traduco ancora: chieda all'utente di specificare la parte reale e la parte immaginaria di tali due numeri complessi... (è chiaro, no? l'intervallo del piano di Argand-Gauss I è costituito da tutti quei numeri z tali che Re(z) è compreso fra Re(z1) e Re(z2) e Im(z) è compreso fra Im(z1) e Im(z2)...)));
  2. consideri tutti i numeri complessi c corrispondenti alle intersezioni di una griglia rettangolare omogenea in questo intervallo (chiedendo, ad esempio, all'utente di specificare il numero di suddivisioni di tale intervallo...);
  3. Per ciascuno di questi punti (scorrendoli, ad esempio, con un (doppio) ciclo...), il programma calcoli il valore della funzione RunOut passando appunto ciascun punto c come primo argomento C e il valore 100 come secondo argomento N.
  4. disegni a video una rappresentazione ASCII dell'intervallo I in cui ogni punto c per cui il valore di RunOut è pari a 100 venga rappresentato con una "M" e tutti gli altri (indifferentemente dal loro valore) con una "O";

giovedì, novembre 16, 2006

istogramma.h

Ecco una possibile implementazione di istogramma.h.

#ifndef istogramma_h
#define istogramma_h

/** \class istogramma
semplice istogramma con strumetnto di visualizzazione
*/

class istogramma
{
public:

//! default ctor (crea un istogramma dummy)
istogramma () ;
//! costruttore
istogramma (const double & minimo, const double & massimo, const int & bins) ;
//! copy ctor
istogramma (const istogramma & original) ;
//! distruttore
~istogramma () ;

//! operator =
istogramma & operator= (istogramma const & original) ;

//! aggiungo un elemento all'istogramma
int fill (const double & value) ;
//! ottengo la media
double getMean () const ;
//! ottengo la sigma
double getSigma () const ;
//! ottengo il numero di entry
int getEntries () const ;
//! stampa sul monitor l'istogramma a barre
void print (int altezza = 20, int larghezza = 40) const ;
//! stampa l'istogramma a barre in orizzontale
void printOr (int altezza = 20, int larghezza = 40) const ;
//! trova il valore di frequenza massimo nell'istogramma
int getMaxValue () const ;

private:

//! somma dei valori in ingresso
double m_somma ;
//! somma al quadrato dei valori in ingresso
double m_sommaSq ;
//! numero di entry
int m_entries ;
//! numero di bin
int m_binsNumber ;
//! vettore dei bin
int * m_bins ;
//! minimum of histogram
double m_minimum ;
//! maximum of histogram
double m_maximum ;
//! inverso della dimensione di un bin
double m_invStep ;

//! NB il ";" !!
} ;

#endif

martedì, novembre 14, 2006

sigleton design pattern

Un design pattern in C++ e' un particolare schema di progettazione del codice, che fornisce funzionalita' precise.
Uno dei primi esempi di design pattern e' il singleton.

Trovate qui una pagina on-line di spiegazione.

Nota bene: questo e' un approfondimento, che non ha niente a che vedere con l'esame :)

venerdì, novembre 10, 2006

precisione numerica

I conti fatti dal calcolatore risentono di errori dovuti alla precisione limitata con la quale vengono effettuate le operazioni.
Ad esempio, utilizzando il file di test di questo post direttamente con la funzione cos o con la funzione funzione si ottengono stime differenti per il punto di minimo, in uno dei due casi:


ant:codice $ ./testminmaxMIB
minimo di cos(x) fra 0 e 4 :3.14162 3.14162
minimo di cos(x) fra 4 e 0 :3.14162 3.14144

I risultati differiscono meno della precisione richiesta all'algoritmo, quindi in questo caso l'effetto e' sotto controllo.
Per tener conto di effetti di questo genere, esempio, e' buona norma non utilizzare mai operatori di uguaglianza fra oggetti di tipo double, ma scegliere una soglia al di sotto della quale i due numeri sono considerati uguali:
// invece di questo:
if (doubleUno == doubleDue) std::cout << "i due numeri sono uguali\n" ;
// e' piu' sicuro usare questo:
double soglia = 0.00001 ;
if (fabs (doubleUno - doubleDue) < soglia) std::cout << "i due numeri sono uguali\n" ;

Questi effetti inattesi, inoltre, dipendono anche da quale computer sia stato utilizzato per eseguire lo stesso programma, dalle opzioni di compilazione, dal compilatore, insomma da variabili molto poco sotto controllo.

mercoledì, novembre 08, 2006

minimizzare funzioni

Si puo' trovare il minimo di una funzione con un programma seguendo due strade, dato un dominio di definizione dove trovare il minimo e la tolleranza richiesta.


  • si suddivide l'intervallo di definizione in tanti sotto-intervalli, ciascuno lungo quanto la tolleranza (una sorta di binning) e si cerca l'intervallo tale per cui la funzione nel punto medio dell'intervallo assume valore minimo. A patto di scegliere la precisione adeguata e che la funzione sia definita con "buone" proprieta' (quindi non la funzione di Dirichlet ad esempio :) ) il minimo di trova. Chiaramente e' un metodo molto lento e che dipende linearmente dalla dimensione dell'intervallo scelto.
  • si cercano algoritmi che restringano l'intervallo di ricerca basandosi su considerazioni analitiche, per evitare di valutare la funzione su una ditribuzione di punti uniforme nel suo domino.

Nel secondo caso, data una funzione continua, definita su un intervallo chiuso [a,b], con un unico estremante in (a,b) che sia minimo, un possibile algoritmo e' il seguente.

  • Si divide l'intervallo in 3 parti, quindi si trovano due punti in (a,b), siano essi c, d tali che a < c < d < b.
  • Si valuta f(c) ed f(d). Nel caso in cui f(c) < f(d) allora il minimo cercato e' compreso fra a e d, quindi si re-itera la procedura fra a e d; altrimenti, fra c e b.
  • Ci si arresta quando la distanza fra gli estremi dell'intervallo di volta in volta considerato sono abbastanza vicini fra loro rispetto alla tolleranza, vale a dire che | max-min | < tolerance. A questo punto, il minimo della funzione e' |max-min| / 2.

Il metodo ideale in questo caso e' utilizzare una funzione ricorsiva, cioe' segua il seguente prototipo.

double ricorsiva (/*ingresso*/)
{
if (/*condizione soddisfatta*/) return /*risultato finale*/ ;
else return ricorsiva (/*ingresso modificato*/) ;
}

Questa tecnica funziona in un caso speciale, in cui la funzione abbia un unico minimo nell'insieme aperto e non abbia massimi nello stesso insieme. La parte piu' complessa del problema rimarra', a questo punto, riuscire ad isolare l'intervallo di dominio dove effettuare la minimizzazione.
Di seguito un esempio di funzione di minimizzazione, con il prototipo in minmaxMIB.h, l'implementazione in minmaxMIB.cc ed un test in testminmaxMIB.cpp. Come si vede, pare che l'ordine degli estremi non sia influente; come mai?

minmaxMIB.h
#ifndef minmaxMIB_h
#define minmaxMIB_h

//! funzione iterativa per trovare il massimo
//! locale di una funzione con errore assoluto "tolerance"
double trisection (double func (double),
const double & left,
const double & right,
const double & tolerance) ;

#endif

minmaxMIB.cc
#include <cmath>
#include <iostream>
#include "minmaxMIB.h"


double trisection (double func (double),
const double & left,
const double & right,
const double & tolerance)
{
if (fabs (right-left) < tolerance) return 0.5 * (right + left) ;
double medLeft = left + (right - left) * 0.3 ;
double medRight = left + (right - left) * 0.6 ;
if ( func (medLeft) > func (medRight))
return trisection (func,medLeft,right,tolerance) ;
if ( func (medLeft) < func (medRight))
return trisection (func,left,medRight,tolerance) ;
// questo nel caso di indecisione
if (right < left) return right - 1. ;
else return left - 1. ;
}

testminmaxMIB.cpp
#include <iostream>
#include <cmath>
#include "minmaxMIB.h"

double funzione (double x) ;


// ------------------------------------------------


int main ()
{
std::cout << "minimo di cos(x) fra 0 e 4 :"
<< trisection (cos,0,4,0.001)
// << trisection (funzione,0,4,0.001)
<< std::endl ;
std::cout << "minimo di cos(x) fra 4 e 0 :"
<< trisection (cos,0,4,0.001)
// << trisection (funzione,4,0,0.001)
<< std::endl ;

return 0 ;
}


// ------------------------------------------------


double funzione (double x)
{
return cos (x) ;
}

lunedì, novembre 06, 2006

comunicazioni fra programma e funzioni

L'ultima volta a lezione abbiamo visto ripassato i due modi principali di passare un parametro ad una funzione: per valore e per riferimento. Con questo post vorrei riepilogare questi due metodi e accostarli ad altri (sottili variazioni di quelli). E, visto che siamo in tema, aggiungere anche qualche parola sui differenti modi di restituire un valore che si può prescrivere a una funzione.

[I] Partiamo dal passaggio di parametri (userò, nei prototipi, procedure, ovvero funzioni void).

  • Passaggio per valore:
void funzione(int a);
viene creata una variabile locale, a, che viene inizializzata col valore passato al momento della chiamata della funzione. Tale valore può essere fornito esplicitamente (tramite una cosiddetta "costante letterale" o litteral in inglese) oppure tramite una variabile (che sarà usata solo come "portatrice" del valore con cui inizializzare la variabile locale a). Come tutte le variabili locali, a scomparirà al termine dell'esecuzione della funzione.

  • Passaggio per valore costante:
void funzione(const int a);
è una piccola variante del caso precedente: semplicemente si specifica che la variabile locale a non potrà essere modificata dopo essere stata inizializzata col valore passato al momento della chiamata della funzione. Eventuali tentativi di modificarne il valore sono segnalati al momento della compilazione (può essere utile, dunque, per accorgersi di eventuali errori nella programmazione...). Inoltre l'informazione che a non potrà essere modificata può essere usata dal compilatore per ottimizzare l'esecuzione del codice (può essere utile, dunque, per migliorare le prestazioni del programma).

  • Passaggio per riferimento:
void funzione(int &a);
non viene creata alcuna variabile locale, ma al contrario all'interno della funzione l'identificatore a sarà usato per riferirsi direttamente alla variabile che viene passata al momento della chiamata della funzione (a sarà, come si suol dire, un alias di quella variabile). Questo implica ovviamente che al momento della chiamata non si potrà passare un valore (esplicito). Altrettanto ovviamente, quindi, tutte le modifiche che verranno fatte all'interno della funzione usando il nome a sopravviveranno alla funzione stessa, perchè la variabile a cui a faceva riferimento non è una variabile locale.

  • Passaggio per riferimento costante (o "a sola lettura"):
void funzione(const int &a);
è una piccola variante del caso precedente: semplicemente si specifica che la funzione non potrà modificare il valore della variabile che viene passata al momento della chiamata della funzione. Notate bene che questa notazione non presuppone che venga passata alla funzione una costante, bensì si limita solo a specificare che verrà trattata come una costante all'interno della funzione. Di più: un riferimento costante di questo tipo può anche essere inizializzato con non-l-value, cioè anche con un litteral, cioè con un valore esplicito: in questo caso il programma crea una variabile temporanea che inizializza col valore fornito, tale variabile ha evidentemente durata limitata al perdurare della funzione stessa e, in quanto const, non può essere modificata nel corpo della funzione.
Per tutte queste proprietà, quest'ultima è, di fatto, la più usata delle quattro varianti qui riportate. Il motivo è evidente: nel caso si chiami la funzione con valori espliciti, essa ricade automaticamente nella seconda variante, mentre se si chiama la funzione usando delle variabili, essa offre l'efficienza di non creare alcuna variabile temporanea. E questo dettaglio che può sembrare banale diventa invece notevolmente importante quando si ha a che fare non con tipi elementari (quali float, int, double...), bensì con oggetti (istanze di classi) molto complessi, la creazione dei quali può magari implicare il ricorso a un operatore di costruttore molto dispendioso.

L'unico caso di una certa frequenza in cui non è possibile usare quest'ultima variante è quello in cui si desidera modificare la variabile passata come parametro (ed è sufficiente, dunque, togliere lo specificatore const). Il caso estremo (in cui si desidera modificare il parametro dentro la funzione ma non intaccare l'eventuale variabile esterna usata come argomento) è decisamente raro e usato in casi del tutto specifici e speciali. In tale situazione si opta dunque per la prima variante.


[II] Veniamo, infine, alla restituzione del valore da parte di una funzione non-void.
  • Passaggio per valore:
float funzione(int &a);
Questo è il caso tipico.
Dal punto di vista interno alla funzione
, quando il controllo torna dalla funzione al programma chiamante, tramite l'istruzione:
return espressione;
il programma costruisce una copia del valore calcolato dell'espressione (che "muore" appena termina la funzione), creando un valore locale nell'ambito del programma chiamante. Se dunque, ad esempio, espressione è una variabile (locale), la funzione non restituisce quella variabile (che, in quanto locale, muore appena la funzione termina), ma una sua copia, che sopravvive alla funzione e diventa un valore locale (temporaneo, cioè non identificato da un nome) del programma chiamante.
Dal punto di vista del programma chiamante, invece, quel che succede è che viene eseguita la chiamata alla funzione (con precedenza rispetto alle altre operazioni) e al suo posto viene sostituito il valore di ritorno della funzione stessa.

  • Passaggio per riferimento:
float& funzione(int &a);
Questa volta, invece, il valore è passato per riferimento, cioè non ne viene costruita una copia, ma nel programma chiamante viene utilizzato direttamente il riferimento al valore restituito dalla funzione. Questo implica diverse cose:
a) l'espressione di return non può essere seguita da un'espressione, ma necessariamente da una variabile (del tipo restituito) il cui riferimento dovrà essere restituito.
b) inoltre tale variabile dovrà necessariamente essere di un tipo che sopravviva alla funzione stessa (altrimenti ci si ritrova con un errore perchè si sta tentando di restituire una variabile che non esiste più...). Siccome abbiamo visto che è fortemente sconsigliato usare variabili globali all'interno delle funzioni, l'unica altra possibilità che ci rimane è che la variabile che vogliamo restituire sia stata essa stessa passata alla funzione per riferimento, come un suo argomento.
c) siccome quel che restituiamo non è un valore, ma una variabile, questo è l'unico caso in cui possiamo chiamare la funzione a sinistra di un'operazione di assegnazione!!!


domenica, novembre 05, 2006

Sulle classi.

Piuttosto che la data, ho scelto un esempio simile, cioe' l'orario: solo perche' e' piu' facile sommare minuti e secondi piuttosto che giorni, visto che i mesi hanno lunghezze diverse in modo irregolare. La classe che ho riportato e' semplicemente un esempio, con tutti i limiti del caso.
Provo a fare una lista di commenti.


  • Come in ogni altra dichiarazione in C++, la dichiarazione di una classe deve contenere tutte a definizione della classe. Vale a dire, tutte le sue variabili e tutte le funzioni che si vogliono utilizzare.
  • Se non viene indicato nulla, il C++ inerpreta gli elementi di una classe come fossero di tipo "private". Per questo, e' sempre meglio indicare sempre le parole chiave "public" e "private" lungo la definizione della classe.
  • Rispondendo ad una domanda, si possono mettere vettori nelle classi (ad esempio per un istogramma), in una classe si puo' mettere qualunque cosa sia gia' stata definita (anche un'altra classe). Tuttavia, nel caso specifico di questo esempio non serve: anche se ore, minuti e secondi hanno due cifre, rimangono un solo numero.
  • e' buona norma cercare di dividere sempre in 3 file distinti il codice:

    • orario.h e' la definizione della classe, senza implementazioni (salvo eccezioni particolari che si vedranno in seguito);
    • orario.cce' dove la classe viene implementata, cioe' dove vengono scritti i suoi metodi (funzioni);
    • i programmmi che usano la classe.

    Sia l'implementazione (orario.cc) che i programmi che usano la classe
    includono la definizione, con #include "orario.h". Per evitare che la definizione venga fatta due volte (che per il compilatore e' un errore), si utilizzano le seguenti istruzioni al pre-processore.





    #ifndef orario_h

    controlla se e' gia' definita una variabile con nome "orario_h" (if), se NON e' definita fa quello che e' scritto sopra, altrimenti va fino all'#endif

    #define orario_h

    definisce la variabile "orario_h", sicche' al secondo tentativo di definizione il pre-compilatore salta

    #endif

    fine dell'if


Ecco i codici. Come al solito, si compilano con:

prompt> c++ -o testOra orario.cc testOra.cpp

orario.h

//! questo serve per garantire l'unicita' della definizione
//! lungo tutto il programma
#ifndef orario_h
#define orario_h

//! il prototipo della classe deve contenere _tutte_ le definizioni
//! della classe: variabili (membri), funzioni (metodi), contruttori,
//! distruttore
class orario
{
//! questa e' la parte della classe che il programma puo' vedere
public :

orario () ;

orario (const int & secondi,
const int & minuti,
const int & ore) ;

~orario () ;

void stampa () ;
void avanti (int ore,
int minuti,
int secondi) ;

//! questa e' la parte della classe accessibile solo alla classe stessa,
//! cioe' che solo le funzioni della classe possono vedere
//! ci possono stare sia variabili che funzioni
private :

//! le variabili di una classe sono spesso identificate con
//! un codice per comodita', in questo caso c'e' "m_" davanti a tutte
int m_secondi ;
int m_minuti ;
int m_ore ;

} ; //! questo punto-e-virgola e' necessario!


#endif

orario.cc
//! nel fine ".cc" vengono scritte tutte le implementazioni
//! delle funzioni di una classe.
//! per fare in modo che il C++ riconosca l'appartenenza alla classe,
//! si mette "nomeclasse::" davanti (in questo caso, "orario::")

#include <iomanip>
#include <iostream>
//! questo serve per leggere le definizioni della classe
#include "orario.h"

//! questo e' il costruttore: viene chiamato ogni volta che
//! un oggetto viene creato nel seguente modo:
//! orario nuovoOggetto ;
orario::orario () :
// questa e' la lista di inizializzazione"
// e' la prima cosa che viene fatta ed inizializza i membri della classe
// ai valori messi in parentesi
m_secondi (0) ,
m_minuti (0) ,
m_ore (0)
{
std::cout << "orario impostata: " ;
stampa () ;
}


// ----------------------------------------------------------------


//! in una classe possono essere definiti piu' di un costruttore:
//! nel nostro caso, quello sopra di chiama "di default" ed e' quello
//! che viene chiamato quando l'oggetto e' definito senza argomenti,
//! mentre questo sotto e' usato quando all'oggetto viene dato un
//! valore iniziale (es: "orario adesso (23,27,13) ;")
orario::orario (const int & secondi,
const int & minuti,
const int & ore) :
m_secondi (secondi) ,
m_minuti (minuti) ,
m_ore (ore)
{
std::cout << "orario impostata: " ;
stampa () ;

}


// ----------------------------------------------------------------


//! questo e' il distruttore della classe,
//! nel nostro caso non fa niente
orario::~orario () {} ;


// ----------------------------------------------------------------


void orario::stampa ()
{
std::cout << std::setfill ('0') << std::setw (2) << m_ore << ":"
<< std::setfill ('0') << std::setw (2) << m_minuti << ":"
<< std::setfill ('0') << std::setw (2) << m_secondi << std::endl ;
}


// ----------------------------------------------------------------


void orario::avanti (int ore,
int minuti,
int secondi)
{
m_secondi += secondi ;
m_minuti += m_secondi / 60 ;
m_secondi %= 60 ;

m_minuti += minuti ;
m_ore += m_minuti / 60 ;
m_minuti %= 60 ;

m_ore %= 24 ;
}

testOra.cpp
#include <iostream>
#include "orario.h"

int main ()
{
//! uso il costruttore di default
orario prima ;
//! uso il costruttore con 3 argomenti
orario unPoDopo (10,0,0) ;

std::cout << "stampo prima:\n" ;
prima.stampa () ;
std::cout << "stampo unPoDopo:\n" ;
unPoDopo.stampa () ;
std::cout << "mando avanti unPoDopo....:\n" ;
unPoDopo.avanti (1,3,56) ;
std::cout << "stampo unPoDopo:\n" ;
unPoDopo.stampa () ;

return 0 ;
}

sabato, novembre 04, 2006

Funzioni


#include <stdio.h>

/*
Concettualmente ci sono due tipi fondamentali di sottoprogrammi: le procedure e le funzioni.

Le procedure sono semplicemente delle porzioni di codice che offrono i vantaggi di:
- evitare la ripetizione di codice simile o uguale all'interno dello stesso programma
- facilitare la modularita' della programmazione (metodo top-down)
- permettere la ri-usabilita' del codice (procedure come building-block) anche per nuovi programmi

Le funzioni sono delle procedure che "restituiscono" un valore al termine della loro esecuzione.
Il significato del termine "restituire" risiede nella possibilita' che hanno le funzioni
(a differenza delle procedure) di poter comparire al lato destro di un'assegnazione: al lato
sinistro dell'assegnazione, cioe', verra' assegnato proprio il valore "restituito" dalla funzione.

In C++ non c'e' differenza (sintattica) fra funzione e procedura.
La dichiarazione di una funzione e' la seguente:

tipo_restituito nome_funzione(lista dei parametri) {
istruzioni;
}

Per le procedure, semplicemente il tipo_restituito sara' "void" (letteralmente: vuoto, nullo, vacante).

Le funzioni si dichiarano fuori dal main (che rappresenta una funzione anch'essa!), eventualmente su
un file diverso da includere o linkare (vedi lezioni successive...)

Per una agevole "lettura" di questo codice, cominciare a leggere direttamente il main,
ed usarlo come "indice"; tornare sopra a leggere le singole funzioni quando
quando ciascuna viene richiamata (e commentata) nel main
*/


int i,j;



//ESEMPIO DI FUNZIONE
int quadrato_di(int a) {
int res; //dichiaro una variabile che conterra' il valore "restituito"
res = a*a; //calcolo il valore che voglio "restituire"
return res; //e lo "restituisco"
//return causa l'uscita dalla funzione e la restituizione del valore che segue
}


//ESEMPIO DI PROCEDURA

void quadrami(int &a) {
a = a*a;
}

//VARIANTI ED ESEMPI ULTERIORI:


//ancora sulla differenza: "per valore", e "per riferimento"
void doppio_quadrato(int a, int &b) {
//notate che per poter usare i e j dentro questa funzione, ho dovuto dichiararle fuori dal main!
//di piu': ho dovuto dichiararle PRIMA di dichiarare questa funzione (dove voglio usarle...)
printf("Dentro \"doppio_quadrato()\", all'inizio, i e j valgono (ancora) %d e %d\n",i,j);
printf(
"Dentro \"doppio_quadrato()\", all'inizio, a e b valgono (ugualmente) %d e %d\n",a,b);
a = a*a;
b = b*b;
printf(
"Dentro \"doppio_quadrato()\", dopo la quadratura, i e j valgono (attenzione!) %d e %d\n",i,j);
printf(
"Dentro \"doppio_quadrato()\", dopo la quadratura, a e b valgono (udite udite!) %d e %d\n",a,b);
}


//funzione (procedura!!!) senza parametri (mettere comunque le parentesi tonde!!!)
void vaiAcapo() {
printf(
"\n");
}

//funzione con valore di default per alcuni parametri:
//se vengono forniti meno parametri, i paramentri mancanti assumono il valore
//dei parametri piu' a destra (vedi main...)
int somma(int a, int b=0, int c=0, int d=0) {

int res;
res = a + b + c + d;
}

//Esempio di overload di funzioni
//(dichiarazione di due funzioni con lo stesso nome, ma con parametri diversi: il C++ le considera
//funzioni diverse!

void stampa(int a) {
printf(
"%d\n",a);
}

void stampa(float a) {
printf(
"%g\n",a);
}


//Esempio di funzione ricorsiva: il fattoriale
int fattoriale(int n) {
if (n<=0)
return 1;
else
return n*fattoriale(n-1);
}


int main() {


i =
3;
j = quadrato_di(i);
printf(
"i = %d e j = %d\n",i,j);
//se non specificato in modo diverso (vedi, ad esempio, la procedura), il parametro e' considerato "per valore"
//cioe' non si passa alla funziona "la variabile", ma "il contenuto della" variabile.
//Tant'e' che possiamo addirittura NON usare alcuna variabile, come parametro:
printf("il quadrato di 3 e' %d\n",quadrato_di(3));

//diverso e' il caso se esplicitamente dichiaramo il parametro come "per riferimento" (con un & davanti alla variabile)
//In questo caso non passiamo il VALORE della variabile, ma la variabile stessa:
i = 3;
printf(
"prima di \"quadrami\", i vale %d\n",i);
quadrami(i);
printf(
"dopo \"quadrami\", i vale %d\n",i);
//e quindi, ovviamente, non posso chiamare "quadrami" con un valore al posto di una variabile:
//quadrami(3); darrebbe un errore in compilazione!

/*
notate, poi, il diverso uso delle due funzioni: la prima (vera FUNZIONE in senso astratto) compare
sempre come "lato destro" di una qualche espressione: o a destra di un uguale, o come parametro di printf
che non assume la funzione stessa, come parametro, ma il suo risultato!!!
la seconda (tecnicamente una PROCEDURA) viene semplicemente richiamata nel flusso del programma
principale come "statement" a se' stante.
*/

printf("\n\n\n");


//Vediamo meglio la differenza fra parametri "per valore" e "per riferimento":
i = 10;
j =
20;
printf(
"All'inizio i e j valgono %d e %d\n",i,j);
doppio_quadrato(i,j);
printf(
"Dopo (fuori da) \"doppio_quadrato()\", i e j valgono %d e %d\n",i,j);
printf(
"E a e b, dopo (fuori da) \"doppio_quadrato()\", quanto valgono?!?\n");


printf(
"\n\n\n");

printf(
"Ciao!"); //NON METTO IL \n DENTRO PRINTF!
vaiAcapo(); //Notare che una funzione senza parametri vuole comunque le parentesi tonde ()
vaiAcapo();

int s;
s = somma(
1,1,1,1); //a,b,c,d tutte assegnate!
printf("somma 4 unita': %d",s);
vaiAcapo();
s = somma(
1,1,1); //manca un parametro: d assume il suo valore di default (stabilito sopra!)
printf("somma 3 unita': %d",s);
vaiAcapo();
s = somma(
1,1); //mancano due paramteri: c, d prendono il default
printf("somma 2 unita': %d",s);
vaiAcapo();
s = somma(
1); //b, c e d: default
printf("somma 1 unita': %d",s);
vaiAcapo();

printf(
"Prova dell'overload delle funzioni:\n");
stampa(
1);
float prova=1.01;
stampa(prova);



printf(
"prova fattoriale: dammi un numero intero (non esagerare: il fattoriale esplode subito!\n");
scanf(
"%d",&i);
do {
printf(
"Il fattoriale di %d e':%d\n",i,fattoriale(i));
printf(
"ancora? (dammi un numero negativo per uscire)\n");
scanf(
"%d",&i);
}
while (i>0);

printf(
"Bye!\n\n");
printf(
"\aPS\nCome mai il fattoriale di 14 e' minore del fattoriale di 13?\nTe ne eri accorto?\nProva!\n\n");
return 0;

}


Algebra dei puntatori


#include <stdio.h>
#include <stdlib.h>
#include <new.h>
int main() {


//
//L'ALGEBRA DEI PUNTATORI
//

/*
Una volta allocato array dinamico, per accedere ai singoli elementi allocati
il C++ mette a disposizione delle operazioni algebriche sui puntatori,
capaci di restituire l'indirizzo di memoria di un particolare elemento
a partire dall'indirizzo "di testa" dell'array (quello che viene restituito
dalla funzione new) e da un intero che specifica la posizione ordinale
della cella che si desidera specificare.
*/


float *Pd;
printf(
"\n\n * * * ALGEBRA DEI PUNTATORI * * *\n\n");

Pd =
new float[10]; //alloco 10 float
printf("Indirizzo della locazione di memoria: %p\n",Pd);
*Pd =
355.0/113;
printf(
"Contenuto della locazione a quell'indirizzo: %g\n",*Pd);

//ADDIZIONE: (con interi, non fra puntatori!!!)

printf("Indirizzo immediatamente successivo al primo: %p\n",Pd+1);
*(Pd+
1) = 2* *Pd; //Il secondo float dell'array uguale a due volte il primo
printf("Contenuto della locazione a quell'indirizzo: %g\n",*(Pd+1));

printf(
"Attenzione alle precedenze!\n");
printf(
"*(Pd+1) = %g \nmentre\n*Pd+1 = *(Pd) + 1 = %g\n",*(Pd+1),*Pd+1);

//INCREMENTO - - - A T T E N Z I O N E - - -
printf("\n\n * * * Incremento coi puntatori * * *\n\n");

float *P; //puntatore di appoggio

printf("caso 1\n");
P = Pd;
printf(
"%p\n",P);
P++;
printf(
"%p\n",P); //P ora punta alla seconda locazione a partire da Pd!!!


printf("\ncaso 2\n");
P = Pd;
printf(
"%p\n",P);
printf(
"%p\n",P++); //Prima viene "restituito" il valore di P, poi incrementato!
printf("%p\n",P);

printf(
"\ncaso 3\n");
P = Pd;
printf(
"%p\n",P);
printf(
"%p\n",++P); //Prima viene incrementato il valore di P, poi restituito!
printf("%p\n",P);

printf(
"\nper cui:\n\n");
P = Pd;
printf(
"*P++ = *(P++) = %g = contenuto della PRIMA cella\n",*P++);
printf(
"e successivamente P si ritrova incrementato di uno: %p\n\n",P);
printf(
"MENTRE\n\n");
P = Pd;
printf(
"*++P = *(++P) = %g = contenuto della SECONDA cella\n",*++P);
printf(
"e P resta e' incrementato di uno: %p\n\n",P);


/*
SOTTRAZIONE (fra puntatori!!!)
*/
printf("\n * * * Sottrazione fra puntatori * * *\n\n");
//In teoria...
char *C;
C =
new char[1];
printf(
"C,C+1 = %p %p \n",C,C+1);
float *Q;
Q =
new float[10];
int diff = Q-P; //Distanza (in unita' di float) fra i due indirizzi
printf("diff = %d\n",diff);
printf(
"P C Q = %p %p %p\n",P,C,Q);
delete[] Q;
delete[] C;

//In pratica:
P = Pd+3; //quarto elemento dell'array...
Q = Pd+7; //ottavo elemento dell'array...

diff = Q-P; //distanza fra gli elementi DI UNO STESSO ARRAY!!!
//Infatti non e' possibile fare la differenza fra puntatori
//di "tipo" diverso...

/*
Il modo di gestire gli array allocati dinamicamente tramite l'algebra
sui puntatori, e' in realta' precisamente il modo con cui il C++ stesso
gestisce tutti gli array, anche quelli dichiarati staticamente.
Di fatto, le due sintassi sono esattamente sovrapponibili, con la seguente
equivalenza:
nome_array <--> puntatore al primo elemento dell'array
*/

P[3] = 12.3; //P era un puntatore, ma lo uso come un vettore!
//e' equivalente a:
*(P+3) = 12.3;
//Ecco spiegato perche' negli array gli indici partono da zero e non da 1!!!

int ARRAY[10];
int *pi;
pi = ARRAY;
//ARRAY senza quadre rappresenta l'indirizzo del primo elemento!
pi = ARRAY+5;

*(ARRAY) =
3;
//e' equivalente a:
ARRAY[0] = 3;


//e cosi' via...


/*
Il C++ non consente la creazione DINAMICA di array multidimensionali
a meno di specificare STATICAMENTE (compilation-time) la lunghezza di
tutte le dimensioni ad eccezione di quella piu' a sinistra
(cioe' l'unica dimensione che puo' davvero essere DINAMICA)

Questo tipo di soluzione e' estremamente macchinosa (sia per il programmatore,
da scrivere, che per il compilatore, da gestire).
Una soluzione migliore e' quella di allocare un array monodimensionale
e gestirlo esplicitamente come una matrice (o un tensore multidimensionale)
come segue:
*/

int ma_s[4][3][2]; //statico
int *ma_d; //puntatore per...
ma_d = new int[4*3*2]; //...allocazione dinamica

for (int i=0; i<(4*3*2); i++) {
ma_d[i]=
0; //accesso sequenziale
}

//oppure
for (int i=0; i<2; i++) {
for (int j=0; j<3; j++) {
for (int k=0; k<4; k++) {
ma_d[i*(
4*3) + j*4 + k] = 0; //accesso fac-simile a molti indici
//equivalente nel caso statico a:
ma_s[i][j][k] = 0;
}
}
}


/*
Analizziamo il caso piu' semplice a due dimensioni:
Ogni matrice RIGHE x COLONNE puo' essere pensata come un unico
array a una dimensione, in cui gli elementi di coordinate: (r,c)
occupano il posto: (COLONNE x r) + c :

Esempio di matrice 3x4: (3 righe e 4 colonne)

0 1 2 3
4 5 6 7
8 9 10 11

Es:
Prima riga (ovvero r=0): 0 1 2 3 (ovvero: 4 x 0 + c)
Prima colonna (ovvero c=0): 0 4 8 (ovvero: 4 x r + 0)

NOTA BENE:
L'indice che corre piu' velocemente e' quello piu' a destra (in questo caso
l'indice di colonna).
*/

printf("\n");

return 0;

}