sabato, novembre 04, 2006

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;

}


Allocazione dinamica della memoria


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

/*
L'importanza del fatto che ogni puntatore "sappia" a quale tipo di variabile
stia puntando (ovvero di quale tipo di variabili puo' contenere l'indirizzo)
risiede nella possibilita' di eseguire operazioni algebriche con il contenuto
di variabili puntatori, ossia di eseguire operazioni algebriche fra indirizzi.

Tali operazioni algebriche hanno senso nel contesto degli array, dal momento
che gli array non sono altro che porzioni contigue di memoria gestite proprio
(eventualmente implicitamente) tramite puntatori.

Per capire meglio l'assoluta sovrapposizione fra array e puntatori, conviene
partire dall'uso dei puntatori per una gestione dinamica della memoria.
*/


/*
Invece che assegnare ad un puntatore l'indirizzo di una variabile gia' dichiarata
e' possibie assegnargli l'indirizzo di una NUOVA zona di memoria che chiediamo
ci venga messa a disposizione (meglio: il codice che state scrivendo chiedera'
al sistema operativo di mettergli a disposizione).
Per chiedere (o rilasciare) memoria, il C++ mette a disposizione i comandi
new
delete
*/

//
//NEW e DELETE
//

int *p; //dichiaro un puntatore a interi
p = new int; //sintassi di new:
/*
alloco in memoria un intero (dopo la keyword "new" si specifica il tipo di
variabile da allocare), e ne restituisco il suo indirizzo.
Siccome a sinitra c'e' un puntatore a interi, l'espressione e' corretta,
e pi conterra' dunque l'indirizzo di una NUOVA porzione di memoria adatta
a contenere numeri interi
*/

delete p; //sintassi di delete:
/*
Attenzione: delete non elimina la variabile p (che e' un puntatore), ma
semplicemente de-alloca la memoria che era stata allocata prima con new
e il cui indirizzo era stato memorizato proprio nella variabile puntatore pi.
Nota Bene:
delete riconosce l'indirizzo di memoria, non la variabile puntatore usata
al momento dell'allocazione, con new.
Ovvero, e' possibile fare la cosa seguente:
*/

int *p1, *p2; //alloco due puntatori;
p1 = new int; //creo un intero dinamicamente, e il suo indirizzo lo assegno a p1
p2 = p1; //metto in p2 l'indirizzo che avevo memorizzato in p1;
delete p2; //disalloco la variabile intera dinamicamente creata, anche se
//l'avevo creata usando p1...
//Ovvero: ora p1 punta ad una zona di memoria che non abbiamo piu' il diritto
//di usare (il sistema operativo potrebbe usarla per altro, e quindi il suo
//contenuto essere del tutto impredicibile...)



//
//NEW e DELETE e gli array
//

//Ma l'uso piu' comune di new e delete e' con gli array dinamici:

int *P1;
P1 =
new int[10]; //alloco non uno solo, ma 10 interi
//Le variabili cosi' create sono contigue l'una all'altra, in memoria.
//In particolare e' l'indirizzo della prima di esse che viene restituito da new.
//Quindi P1 ora contine l'indirizzo della prima locazione di memoria di
//una serie contigua di 10 locazioni per numeri interi.

/*
Una delle cose importanti da notare e' che la quantita' di memoria che e'
possibile allocare con new non deve necessariamente essere predeterminata
compilation-time (come avveniva con gli array) ma puo' essere determinata
run-time per mezzo del valore corrente di una qualche variabile
*/
int quanti_double;
printf(
"Quanti double vuoi allocare?\n");
scanf(
"%d",&quanti_double);
double *Pd;
Pd =
new double[quanti_double];


//E se inserite un numero troppo elevato?
//Il programma si interrompe con un errore!!!

//E se volete gestire un errore del genere dall'interno del vostro programma?

printf("Prova ad allocare tantissimi double! Quanti?\n");
scanf(
"%d",&quanti_double);
Pd =
new (nothrow) double[quanti_double];
if (Pd == 0) {
printf(
"Bravo! Hai davvero esagerato!\n%d double sono davvero troppi per la mia memoria!!!\n\n",quanti_double);
}
else {
printf(
"Niente da fare:\nho abbastanza memoria da allocare tutti e %d i double richiesti!\n\n",quanti_double);
}

//La versione di new con l'opzione (nothrow) restituisce un puntatore nullo (0)
//nel caso in cui la richiesta di allocazione non puo' essere soddisfatta,
//e continua l'esecuzione del programma (ma, ovviamente, senza la memoria
//allocata!!!)


/*
Il comando delete usato per gli array dinamici gestisce automaticamente
la dimensione dell'array, e non e' necessario che la specifichi il
programmatore:
*/

delete[] Pd;
//Notare, pero', che siccome Pd punta al primo indirizzo di un array
//(e non ad una variabile singola), la sintassi preve l'uso delle
//parentesi quadre (vuote)
//Se per caso Pd e' il puntatore nullo (nel caso in cui abbiate
//esagerato con la richiesta di double...), il comando delete non produce
//alcun risultato (non c'e' nulla da de-allocare...)


return 0;

}


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