Dal C al C++: differenze tra le versioni

Contenuto cancellato Contenuto aggiunto
Nessun oggetto della modifica
Riga 213:
* La parola-chiave "class" è ''quasi'' sinonima di "struct", ed è molto più usata per strutture contenenti funzioni membro. D'ora in poi, invece di "struct", e ''struttura'' useremo prevalentemente "class" e ''classe''. Quindi una variabile "x" di tipo "class C" potrà essere definita semplicemente con la seguente riga:
C x;
 
Un termine molto usato (e abusato) è la parola "oggetto".
La si usa impropriamente a volte per indicare una classe, altre per indicare una variabile.
In realtà non è nessuna delle due cose.
Un oggetto è una cosa che esiste anche in C, sotto il nome di "r-value".
 
L'espressione "r-value", abbreviazione di "right-hand value" (cioè "valore di destra"), significa ''espressione che può stare a destra dell'operatore "="''.
Cioè si tratta di un'espressione che rappresenta una sequenza di bit che può essere copiata in una locazione di memoria tramite un assegnamento.
Un oggetto può risiedere in memoria, oppure essere solo teorico, cioè esiste concettualmente ma l'ottimizzatore di codice ne evita la creazione.
Facciamo qualche esempio valido sia in C che in C++:
char x;
char * px = &x;
Se le due righe precedenti sono poste fuori da qualunque funzione e struttura, costituiscono altrettante definizioni di variabili globali "x" e "px", la prima di tipo "char" e la seconda di tipo "char *", ossia puntatore a "char".
Entrambe le variabili rappresentano anche degli oggetti, il primo lungo "sizeof (char)" byte, cioè tipicamente 1 byte, e il secondo lungo "sizeof (char *)" byte cioè tipicamente 2 byte su una macchina a 16 bit, 4 byte su una macchina a 32 bit, e 8 byte su una macchina a 64 bit.
 
La variabile "px" è inizializzata a puntare all'oggetto rappresentato dalla variabile "x".
Pertanto l'espressione "*px" rappresenta ''lo stesso oggetto'' rappresentato da "x".
Adesso aggiungiamo la seguente riga in una funzione.
px = malloc(7);
Dopo aver eseguito questa istruzione, "px" o è nullo (se la malloc fallisce) o punta a un array di sette oggetti di tipo carattere.
Tali oggetti non sono associati a nessuna variabile, eppure possono essere assegnati. Per esempio, con la seguente istruzione:
px[2] = 'a';
 
Consideriamo la seguente istruzione:
int a = 3 + 5;
Il numero "3" in quanto tale non è un oggetto, in quanto è un valore letterale, ma quando entra a far parte di una espressione come questa, concettualmente viene istanziato un oggetto che contiene una sequenza di bit che rappresenta il valore "3".
Chiaramente qualunque compilatore ottimizzante a fronte di questa riga di codice genera un'istruzione che assegna alla variabile "a" il valore 8.
 
Tuttavia, concettualmente, avvengono le seguenti operazioni:
* Viene allocato sullo stack un oggetto di tipo "int", non inizializzato, che rappresenta la variabile "a".
* Viene allocato sullo stack un oggetto di tipo "int", e viene posto il valore "3" in tale oggetto.
* Viene allocato sullo stack un oggetto di tipo "int", e viene posto il valore "5" in tale oggetto.
* Viene allocato sullo stack un oggetto di tipo "int", non inizializzato, che serve come area temporanea per memorizzare il risultato della somma.
* Viene eseguita l'operazione di somma tra il secondo e il terzo oggetto, assegnando il risultato al quarto oggetto.
* Viene copiato il contenuto del quarto oggetto nel primo oggetto.
* Vengono deallocati tutti gli oggetti tranne il primo.
 
Quindi, nel corso dell'esecuzione di un programma scritto in C++, concettualmente vengono allocati e deallocati moltissimi oggetti, anche se in pratica a fronte di tali allocazioni e deallocazioni concettuali il compilatore ottimizzante genera molte meno istruzioni di quante sembrerebbero necessarie.
 
In C++, ma non in C, è anche possibile, come vedremo in seguito, definire una variabile senza allocare nessun oggetto.
Si tratta delle variabili di tipo "riferimento" ("reference" in inglese), usate in alcuni linguaggi, come BASIC e Pascal, per passare parametri a funzioni.
 
== Le variabili riferimento ==
Nel linguaggio C, si possono definire variabili che non sono puntatori, come in "int a;", oppure variabili che sono puntatori a oggetti che non sono puntatori, come in "int * a;", oppure variabili che sono puntatori a oggetti che sono puntatori a oggetti che non sono puntatori, come in "int ** a;", e così via.
 
In C++ rimangono tutte queste possibilità, ma si aggiunge una nuova tipologia di variabile, il "riferimento".
Consideriamo le seguenti righe, di cui le prime due sono valide anche in C:
int a = 34;
int * b = & a;
int & c = a;
La prima riga definisce una variabile di nome "a", di tipo "int", e inizializzata al valore intero "34".
La seconda riga definisce una variabile di nome "b", di tipo puntatore a "int", e inizializzata all'indirizzo dell'oggetto rappresentato dalla variabile "a".
La terza riga, non valida in linguaggio C, definisce una variabile di nome "c", di tipo ''riferimento'' a "int", e inizializzata alla variabile "a".
La variabile "c" non ha un suo oggetto, ma rappresenta lo stesso oggetto rappresentato dalla variabile "a", nonché dall'espressione "* b".
In seguito si può usare la variabile "c" con lo stesso significato della variabile "a".
Tale fatto si esprime dicendo che "c" è un ''alias'' di "a".
Per esempio, eseguendo il seguente codice.
c = 7;
cout << a;
Viene stampato il numero "7", in quanto l'oggetto riferito da "c" è lo stesso oggetto riferito da "a".
 
Tale associazione è permanente per la vita della variabile.
Come non si può fare in modo che "a" rappresenti un oggetto diverso da quello iniziale, così anche "c", da quando viene inizializzata, è vincolata a rappresentare sempre lo stesso oggetto.
 
Consideriamo le seguenti righe.
int a2;
int * b2;
int & c2;
Le prime due continuano a definire variabili a cui corrispondono oggetti, anche se tali oggetti non sono inizializzati e quindi hanno valori indefiniti, ossia il loro utilizzo come r-value ha effetto impredicibile.
Inoltre, l'espressione "* b2" non rappresenta più un oggetto valido.
La dichiarazione di variabili non inizializzate è pericolosa e andrebbe evitata.
Alcuni hanno proposto di vietarle in C++, ma siccome questo avrebbe reso non validi molti vecchi programmi, sono state mantenute.
Per fortuna, molti compilatori emettono un avvertimento quando si usa come l-value una variabile non inizializzata e a cui non è ancora stato assegnatpo un valore.
 
La terza riga è invece illegale sia in C che in C++.
Infatti, tenterebbe di dichiarare una variabile che costituisca un riferimento alternativo a un oggetto, ma non specifica a quale oggetto.
Siccome il collegamento di una variabile riferimento al suo oggetto può avvenire solo all'atto della definizione della variabile, tale variabile rimarrebbe per sempre non legata a un oggetto, e quindi completamente inutile.
 
L'uso principale delle variabili riferimento, è però per dichiarare parametri di funzione.
Consideriamo il seguente programma:
#include <iostream>
using namespace std;
double doppio1(double a) { return a * 2; }
double doppio2(double * a) { return *a * 2; }
double doppio3(double & a) { return a * 2; }
int main() {
double x = 1;
cout << doppio1(x);
cout << doppio2(&x);
cout << doppio3(x);
}
 
Le funzioni "doppioN" sono equivalenti, tanto che il programma stamperà "222".
 
#include <iostream>
using namespace std;
void raddoppia1(double a) { a *= 2; }
void raddoppia2(double * a) { *a *= 2; }
void raddoppia3(double & a) { a *= 2; }
int main() {
double x = 1;
raddoppia1(x);
cout << x;
raddoppia2(&x);
cout << x;
raddoppia3(x);
cout << x;
}
 
Le funzioni "raddoppiaN" non sono equivalenti, tanto che il programma stamperà "124".
 
Chiamando la funzione "raddoppia1", il valore dell'oggetto riferito dalla variabile "x" viene copiato sullo stack.
All'interno della funzione tale valore viene riferito dal parametro "a".
Il corpo della funzione modifica tale valore allocato sullo stack, e quando la funzione termina l'oggetto viene deallocato dallo stack, senza che ci siano effetti sul mondo esterno.
Quindi l'oggetto rappresentato dalla variabile "x" continua a valere "1".
 
Chiamando la funzione "raddoppia2", l'indirizzo dell'oggetto riferito dalla variabile "x" viene copiato sullo stack.
All'interno della funzione tale valore viene riferito dal parametro "a".
Il corpo della funzione modifica il valore puntato da tale valore allocato sullo stack, che è poi lo stesso valore associato a "x", e quando la funzione termina l'oggetto puntatore viene deallocato dallo stack.
Quindi l'oggetto rappresentato dalla variabile "x" è stato raddoppiato, passando dal valore "1" al valore "2".
 
Chiamando la funzione "raddoppia3", si ''comunica'' alla funzione che il parametro "a" dovrà rappresentare lo stesso oggetto rappresentato dalla variabile "x".
Questa ''comunicazione'' può avvenire copiando sullo stack l'indirizzo dell'oggetto rappresentato dalla variabile "x".
Il corpo della funzione modifica il valore puntato da tale valore allocato sullo stack, e quando la funzione termina l'oggetto puntatore viene deallocato dallo stack.
Quindi l'oggetto rappresentato dalla variabile "x" è stato raddoppiato, passando dal valore "2" al valore "4".
 
Come si vede, il corpo della funzione "raddoppia3" è uguale a quello della funzione "raddoppia1", ma il comportamento è uguale a quello della funzione "raddoppia2".
Questi fatti si esprimono con la frase: '''I riferimenti hanno la sintassi dei valori, ma la semantica dei puntatori'''.
 
{{wikipedia|titolopedia=C plus plus|titolobooks=il linguaggio C++}}