sabato, novembre 04, 2006

Puntatori


#include <stdio.h>
#include <stdlib.h>

int main() {

/*
Una variabile corrisponde a una locazione di memoria capace di ospitare
valori corrispondenti al tipo di variabile dichiarata.
Una variabile intera, ad esempio, corrisponde ad una locazione di 4 byte
che vengono interpretati in base alla codifica dei numeri interi.
Oppure, una variabile double corrisponde ad una locazione di 8 byte
interpretati in base alla codifica dei numeri floating-point.

Un puntatore, da questo punto di vista, e' una variabile come le altre:
corrisponde ad una locazione di memoria capace di ospitare dei valori.
La differenza (essenziale) e' che il tipo di valori ospitati in una
variabile di tipo puntatore e' proprio l'INDIRIZZO DI MEMORIA (di
una qualche variabile).
Cosi' una variabile di tipo puntatore-a-interi, potra' ospitare (come
valore!) l'indirizzo di memoria corrispondente a una variabile intera.
Un puntatore-a-double ospitera' indirizzi di memoria corrispondenti a
variabili double.
E' importante sottolineare che i puntatori sono sempre
"puntatori-a-qualcosa": non esistono (in realta' esistono, ma hanno
una sintassi propria, esplicita...) puntatori a locazioni di memoria
"generici", senza specificazione del tipo di locazione di cui il
puntatore gestisce gli indirizzi.

*/

//dichiarazione di una variabile:
float variabile_float;
int variabile_intera;

//dichiarazione di un puntatore:
float *puntatore_a_float;
int *puntatore_a_int;
//antepongo un asterisco davanti al nome della variabile

//una variabile float puo' contenere un valore numerico in virgola mobile:
variabile_float=5.5;
//e similmente:
variabile_intera=10;

//un puntatore a float, puo' contenere un indirizzo di una variabile float:
puntatore_a_float = &variabile_float;
//l'operatore & davanti al nome di una variabile (qualsiasi) ne restituisce
//l'indirizzo di memoria

//Similmente:
puntatore_a_int = &amp;variabile_intera;

//mentre, ovviamente, sarebbe sbagliata un'assegnazione del tipo:
//puntatore_a_int = &variabile_float;
//perche', come si diceva, un puntatore e' sempre legato a un certo "tipo" di
//variabili cui puo' immagazzinare l'indirizzo

/*
Per avere un'idea piu' concreta, proviamo a stampare a video il contenuto
di alcune variabili normali e di variabili di tipo puntatore:
*/

printf("\n\n");
printf(
"variabile_intera contiene il valore: %d\n", variabile_intera);
printf(
"puntatore_a_int contiene il valore: %p\n", puntatore_a_int);
//il codice %p e' usato come segnaposto per i puntatori nel comando printf
//il formato di printf per un puntatore e' un numero in base 16 con
//un prefisso di due caratter: 0x (zero, ics)
printf("e in effetti l'indirizzo di variabile_intera e': %p\n", &variabile_intera);
printf(
"l'indirizzo di puntatore_a_int, invece, e': %p\n",&puntatore_a_int);
printf(
"che non ha nulla a che fare col suo contentuo...\n");

//Se volessimo "giocare" con l'indirizzo di memoria di puntatore_a_int,
//dovremmo usare una variabile capace di gestire questo tipo di indirizzi:
int **puntatore_a_puntatore_a_int;
//notare il doppio asterisco (puntatore-a-puntatore-a-...)
//(e ovviamente la generalizzazione con un numero fissato ma arbitrario di
//asterischi e' del tutto lecita in C++)

puntatore_a_puntatore_a_int = &puntatore_a_int;
printf(
"il contenuto di puntatore_a_puntatore_a_int, invece, e': %p\n",puntatore_a_puntatore_a_int);
printf(
"ovvero, proprio l'indirizzo di puntatore_a_int!!\n");

printf(
"\n\n");


/*
Oltre all'operatore &, ovvero l'operatore "indirizzo di", riferito
a una variabile, esiste l'operatore contrario: "contenuto di", riferito a un
indirizzo, ossia a un puntatore: datemi l'indirizzo di una variabile, e io vi
restituiro' il contenuto di quella variabile di cui mi avete dato l'indirizzo.
Il contetto e' molto semplice. La cosa molto confusionaria e' che questo
operatore e' realizzato in C++ con lo stesso simbolo che si usa per DICHIARARE
un variabile-puntatore: l'asterisco.
*/
printf("Il contenuto della variabile all'indirizzo %p, e' %d\n\n",
puntatore_a_int,
*puntatore_a_int);
/*
Chiariamo, dunque:

- in DICHIARAZIONE, l'asterisco anteposto a al nome di una variabile,
indica che la variabile che stiamo dichiarando e' un puntatore;

- in un'ESPRESSIONE, l'asterisco davanti al nome di una variabile-puntatore
indica che stiamo considerando il contenuto della locazione di memoria
all'indirizzo memorizzato dalla quella variabile-puntatore
(non e' possibile anteporre un asterisco ad una variabile comune)

*/

//Pertanto, espressioni come le seguenti sono del tutto legittime:

int i = 10; //dichiarazione (variabile normale)
int j = 20;
int *pi, *pj; //dichiarazione (asterisco: e' un puntatore!)

//espressione: l'asterisco rappresenta "il contenuto all'indirizzo:"
pi = &i; //uso di "indirizzo di"
j = *pi; //uso di "contenuto all'indirizzo in"
//e' come se avessimo scritto: j = i; dal momento che pi contiene proprio
//l'indirizzo di i, per l'assegnazione immediatamente precedente!

/*
Una volta chiarito il senso degli operatori unari, il loro "risultato" (ovvero
"l'indirizzo di questa variabile" o "il contenuto all'indirizzo di questo
puntatore", si comportano del tutto normalmente, e possono essere usati anche
in contesti complessi:
*/

int z;

z = *(&i) + *(&j);
//modo complicato per scrivere z = i + j;
z = (*pi + 5); //sommo 5 al contenuto (intero) all'indirizzo memorizzato in pi

//attenzione a questa:
*pi = 5;
/*
Assegno il valore 5 alla cella di memoria il cui indirizzo e' memorizzato in pi.
Se quell'indirizzo era l'indirizzo di una variabile gia' dichiarata (nel nostro
caso la variabile i, secondo l'assegnazione a riga 219), significa che stiamo
effettivamente modificando il contenuto di quella variabile: e' come aver
scritto i=5;
*/

float x=355.0/113.0;
float *px;
px = &x;
//[*]
printf("x vale: %g \n",x);
*px =
2 * x; //e' come aver scritto: x = 2*x; dal moemnto che [*]
printf("x ora vale: %g \n",x);

printf(
"\n\n");



return 0;
}


Array


#include <stdio.h>
#include <stdlib.h>

int main() {

int array_interi[10]; //collezione di dieci interi

/*
Nella dichiarazione di un array, va indicato il numero di elementi
fra le parentesi quadre.
I singoli elementi dell'array sono delle variabili normali a tutti
gli effetti. Ciascuna di esse viene individuata dai primi
interi *** A PARTIRE DA ZERO ***
(in questo caso, quindi, da 0 a 9, 10 escluso!!!)
*/

//il tipico ciclo per "visitare" gli elementi dell'array:
for (int i=0; i<10; i++) { //notare il minore (NON minore-uguale)
array_interi[i]=0; //inizializzazione a zero di ogni elemento dell'array
}


int matrice[4][4]; //array bidimensionale (a due indici)

//il tipico doppio-ciclo per scorrere gli elementi di un array bidimensionale
for (int i=0; i<4; i++) {
for (int j=0; j<4; j++) {
matrice[i][j]=
0;
}
}

//ma il numero di indici puo' essere arbitrario
int tensore[3][4][5][2];
for (int i=0; i<3; i++) {
for (int j=0; j<4; j++) {
for (int k=0; k<5; k++) {
for (int l=0; l<2; l++) {
tensore[i][j][k][l]=
0;
}
}
}
}

/*
E' anche possibile specificare i valori iniziali dei singoli elementi
di un array tramite una dichiarazione "per elencazione":
*/
int esiti_dado[6] = {1, 2, 3, 4, 5, 6};
int matrice_unita[3][3] = { {1,0,0}, {0,1,0}, {0,0,1} };

int ordine_indici[4][3][2] = { {{1, 2}, {3, 4}, {5, 6}},
{{
7, 8}, {9, 10}, {11, 12}},
{{
13, 14}, {15, 16}, {17, 18}},
{{
19, 20}, {21, 22}, {23, 24}}
};

int numeri[] = {-11,12,32,14,1,0,0,43};
int matrix[][3] = { {1,2,3}, {4,5,6}, {7,8,9} };


//stringhe come array di caratteri:

char Parola[] = "Caratteri";

/*
La dimensione dell'array e' pari a quella della stringa "Caratteri",
piu' uno, dal momento che il compilatore aggiunge automanticamente il
carattere nullo ("\0") alla fine della stringa.

Una volta che e' stato inizializzato un array di caratteri, esso diviene
un array a tutti gli effetti, e l'accesso ai suoi elementi (i caratteri)
puo' avvenire solo facendo riferimento a ciascuno di essi (in pratica
non si puo' ri-assegnare un valore a una stringa come
nell'inizializzazione...
Dopo l'istruzione di sopra, quindi, istruzioni come le seguenti:

Parola[] = "Nuova"; // errore!
Parola = "Nuova"; // errore!

non sono consentite!

*/

Parola[4] = 'w'; // assegna 'w' al quinto carattere (non al quarto!)
//(il primo e' il carattere 0, come in tutti gli array!)


//Anche per gli array di caratteri e' possibile l'inizializzazione
//"per elencazione"
char Lettere[] = { 'L', 'e', 't', 't', 'e', 'r', 'e'};
char Caratteri[6] = {'P','a','r','o','l','a' };

/*
In questi casi pero' non si ottiene una stringa terminata da '\0',
ma semplici array di caratteri il cui numero di elementi e'
esattamente quello specificato.
*/

return 0;

}


giovedì, novembre 02, 2006

organizzare il codice in librerie

Quando il numero di funzioni in un programma cresce molto, la compilazione rallenta perche' ad ogni modifica e' necessario ri-compilare tutto quanto e' stato scritto.

Per accelerare questo processo si suddivide il codice scritto in moduli divisi fra di loro, solitamente per funzionalita': tutto quello che e' stato scritto con una certa funzione e con carattere di generalita' viene impacchettato in un singolo "oggetto" (del compilatore, non del C++: e' un caso di omonimia).

Un esempio calzante di questo puo' essere tratto dalle funzioni di generazione di numeri casuali che abbiamo scritto.
In questo caso, divido le funzioni che generano numeri casuali dal main program, creando:


  • randomMIB.h le definizioni delle funzioni
  • randomMIB.cc le implementazioni delle funzioni
  • testRandom.cpp il programma che le utilizza

E' necessario definire il file randomMIB.h diviso dal randomMIB.cc, perche' nel randomMIB.h stanno gli header delle funzioni, quindi dovra' essere incluso in tutti gli eseguibili che ne fanno uso perche' il parser ne conosca il prototipo.
Lasciare randomMIB.cc separato permette di compilarlo una volta sola e di utilizzare la parte gia' compilata linkandola al codice in esecuzione (come Enzo ha spiegato).

Insomma, per provare effettivamente come si fa.
Per compilare la libreria di funzioni di genralzione di numeri casuali:

c++ -c randomMIB.cc -lm

per compilare il programma di test:

c++ -lm randomMIB.o testRandom.cpp -o testRandom

per eseguire il programma di test:

./testRandom

Nel caso si modificasse il programma di test, oppure se ne creasse un altro, e' sufficiente compilare quest'ultimo, perche' la libreria non e' cambiata.

Ecco di seguito i codici dei tre pezzi.

PS: ho modificato randFunc seguendo la implementazione di Eugenio.

randomMIB.h

#ifndef randomMIB_h
#define randomMIB_h

#include <iostream>
#include <cmath>
#include <ctime>
#include <cstdlib>

double randFlat (double min, double max) ;
double randFunc (double func (double),
double minx, double maxx,
double miny, double maxy) ;

#endif


randomMIB.cc

#include "randomMIB.h"
#include <iostream>
#include <cmath>
#include <ctime>
#include <cstdlib>


//! restituisce un numero casuale fra min e max
//! secondo una distribuzione uniforme
double randFlat (double min, double max)
{
return min + (max-min) * rand () / static_cast<double> (RAND_MAX) ;
}


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


//! restituisce un numero casuale fra minx e maxx
//! secondo una distribuzione che segue func
double randFunc (double func (double),
double minx, double maxx,
double miny, double maxy)
{
if (maxx < minx) return maxx - 1 ;
if (maxy < miny) return minx - 1 ;
double x,y ;
do
{
x = randFlat (minx, maxx) ;
y = randFlat (miny, maxy) ;
}
while (func (x) < y) ;
return x ;
}


testRandom.cpp

#include <iostream>
#include <cmath>
#include <ctime>
#include <cstdlib>

#include "randomMIB.h"

double generationPDF (double x) ;


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


int main ()
{

// inizializzo il seme di generazione casuale
srand (time (NULL)) ;

// loop sugli eventi da generare
for (int i=0; i<10; ++i)
{
// genero un numero casuale
double numero = randFunc (generationPDF,
0,2 * M_PI,
-0.1,2.1) ;
std::cout << "numero casuale n. " << i
<< " : " << numero << std::endl ;
} // loop sugli eventi da generare

return 0 ;

}


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


double generationPDF (double x)
{
// return 1 ;
return cos (x) + 1 ;
}

programmazione ad oggetti

compilatori e interpreti

Estremi del codominio

1/ In effetti, come avevo lasciato immaginare, quello dell'estremo (superiore) del codominio non è un problema di facile soluzione. Per trovare gli estremi di una funzione (in una o più variabili) esistono diversi algoritmi, più o meno efficienti e più o meno efficaci, ma tenete conto che si tratta di un problema sofisticato (soluzioni ai "problemi di minimizzazione", come vengono chiamati, sono tuttora oggetto di ricerca specifica), che rappresenta un elemento centrale in moltissimi ambiti della fisica, della matematica e in generale in tutte quelle discipline che fanno uso pesante della computing science. Un metodo di minimizzazione piuttosto semplice è il cosiddetto metodo della sezione aurea: potete trovare alcuni riferimenti sulla wikipedia (sempre sia lodata!) oppure qui. Sono in inglese, ma la cosa non dovrebbe spaventarvi (e comunque molto presto dovrete abituarvi!).

2/
Eugenio nota giustamente il problema che sorgerebbe nel casi di pdf nulle in qualche punto: come si può risolvere il problema, modificando randFunc()?

3/
Rinnovo le domande di un post precedente: davvero l'estremo inferiore del codominio deve essere necessariamente 0 (zero)? L'algoritmo che avevamo proposto, dunque, non funziona? Se ci stiamo impuntando così tanto sulla proprietà di non-negatività della distribuzione di probabilità, che dire allora dell'altra sua proprietà, quella di normalizzazione? Sono legate, le due questioni?

4/
Ottima l'idea di Eugenio di generare numeri nel chiuso e non nel semiaperto per evitare di dover moltiplicare per quasi-uno. Perchè, invece, a lezione, abbiamo sempre generato numeri casuali nel semiaperto? Può sembrare una sottigliezza, ma il punto è che avevamo già in mente proprio la costruzione di istogrammi. Eugenio, ad esempio, correttamente quando conteggia i dati e trascrive il risultato, effettua un controllo asimmetrico nel singolo bin che via via considera, ossia assume i vari bin come intervalli semiaperti. Solo se il dominio è anch'esso semiaperto (nella stessa maniera, cioè chiuso a sinistra e aperto a destra), allora la suddivisione in bin è realmente un'equiripartizione del dominio stesso. Altrimenti l'ultimo bin si ritroverebbe con entrambi gli estremi inclusi. E siccome stiamo lavorando su una macchina discreta (e non nell'astrazione continua dell'infinità non-numerabile di punti reali), questa apparentemente sottile differenza potrebbe manifestarsi concretamente in alterazioni della distribuzione di probabilità così come verrebbe fotografata dall'istogramma. Dunque, a voler essere precisi, nella funzione randFunc io utilizzerei un generatore random con immagine nel chiuso per generare y, mentre continuerei ad usare il "solito" generatore random nel semiaperto per generare x.
Detto tutto questo: avevate dato un'occhiata alla soluzione di Pietro che avevamo pubblicato? (Abbiamo modificato il post, ora non contiene più parti di codice "incomprensibili" perchè già orientato agli oggetti, quindi potete leggerlo benissimo...) Avete capito il trucchetto che usa per conteggiare i risultati, utilizzando la funzione
floor? Cosa ne pensate, alla luce di quello che abbiamo detto or ora?

mercoledì, ottobre 25, 2006

Istogramma


int main ()
{
// creo l'istogramma
// -----------------

// parametri generali
double somma = 0. ;
double sommaSq = 0. ;
int eventi = 0 ;
// creo il vettore di frequenze

int mioIstogramma[20] ;
// azzero tutti i suoi elementi
for (int i=0; i<20; ++i)
{
mioIstogramma[i] =
0 ;
}

// riempio l'stogramma
// --------------------

// inizializzo il seme di generazione casuale
srand (time (NULL)) ;

double step = (2*M_PI - 0) / 20 ;
// loop sugli eventi da generare

for (int i=0; i<100000; ++i)
{
// genero un numero casuale
double numero = randFunc (generationPDF,
0,2 * M_PI,
-
0.1,2.1) ;

// calcolo in quale dei 10 elementi dell'istogramma contarlo
// la funzione "double floor (double)" ritorna il numero intero
// non maggiore piu' vicino all'argomento
int bin = static_cast<int> (floor (numero / step)) ;
somma += numero ;
sommaSq += numero * numero ;
++eventi ;
// sommaSq += pow (numero,2) ;

// incremento l'elemento relativo
++mioIstogramma[bin] ;
}
// loop sugli eventi da generare

// disegno l'mioIstogramma
// --------------------

// cerco il massimo dell'istogramma:
int maxIsto = 0 ;
// loop sugli elementi dell'istogramma

for (int i=0; i<20; ++i)
{
if (maxIsto < mioIstogramma[i]) maxIsto = mioIstogramma[i] ;
}
// chiuso loop sugli elementi dell'istogramma


// disegno le singole barre:
int maxColumns = 20 ;
// loop sugli elementi dell'istogramma
std::cout << "plot dell'mioIstogramma:\n" ;
std::cout <<
" +---------------------->\n" ;
for (int i=0; i<20; ++i)
{
int numeroSimboli = mioIstogramma[i] * maxColumns / maxIsto ;
std::cout <<
" | " ;
for (int j=0; j<numeroSimboli; ++j)
{
std::cout <<
"#" ;
}
for (int j=numeroSimboli; j<22; ++j)
{
std::cout <<
" " ;
}
std::cout <<
"mioIstogramma[" << i << "] : " << mioIstogramma[i] ;
std::cout <<
"\n" ;
}
// chiuso loop sugli elementi dell'istogramma

std::cout << " |\n\\/\n\n" ;
std::cout <<
"media = " << somma / eventi << "\n" ;
std::cout <<
"sigma = " << sqrt (sommaSq / eventi -
(somma / eventi) * (somma / eventi))
<<
"\n" ;

return 0 ;

}