Da WIKI CoderDojo Firenze.

Tutorial C++ con le librerie Qt – Parte III

Puntatori e references

Soluzione al problema del tutorial precedente

Avete risolto il problema con cui ci siamo lasciati nell’ultimo tutorial?

Per chi non ce l’avesse fatta, ecco qui una possibile soluzione:

#include <iostream>

using namespace std;

void spiegazioni();
int mossaCane(int fiammiferi);
int mossaAnatra(int fiammiferi);

int main()
{
   spiegazioni();
   int fiammiferi = 21;

   do {
      fiammiferi = mossaCane(fiammiferi);
      fiammiferi = mossaAnatra(fiammiferi);
   } while ( 1 < fiammiferi );

   cout << endl << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
         << endl;
   cout << "Mi spiace, sei rimasto con l'ultimo"
            " fiammifero..." << endl;
   cout << "Ho vinto io." << endl;
   cout << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
         << endl << endl;

   return 0;
}

void spiegazioni()
{
   cout << "Ci sono 21 fiammiferi." << endl;
   cout << "Ne possiamo prendere da 1 a 4 per volta." << endl;
   cout << "Chi rimane con l'ultimo ha perso." << endl;
   cout << endl << "Hai la prima mossa." << endl << endl;
}

int mossaCane(int fiammiferi)
{
   int presa = 0;

   while ( presa < 1 || presa > 4 ){

   cout << "Quanti fiammiferi prendi (da 1 a 4)? " << endl;
   cin >> presa;

   if( presa < 1 || presa > 4) {
   cout << "Spiacente. Devi prendere almeno un fiammifero, "
   "ma non più di quattro..." << endl << endl;
   }

}

   fiammiferi -= presa;
   cout << "Hai preso " << presa << " fiammiferi." << endl;
   cout << "Ne rimangono " << fiammiferi << "." << endl;

   return fiammiferi;
}

int mossaAnatra(int fiammiferi)
{
   int presa;

   if( 1 < fiammiferi ) {
   presa = fiammiferi - 1;
   }
   if( 6 < fiammiferi ) {
   presa = fiammiferi - 6;
   }
   if( 11 < fiammiferi ) {
   presa = fiammiferi - 11;
   }
   if( 16 < fiammiferi ) {
   presa = fiammiferi - 16;
   }

   fiammiferi -= presa;

   cout << "Io prendo " << presa << " fiammiferi." << endl;
   cout << "Adesso ne rimangono " << fiammiferi << "." << endl
         << endl;

   return fiammiferi;
}

Il codice è rimasto pressoché identico, salvo che adesso le due funzioni più importanti ricevono valori in ingresso e restituiscono valori in uscita.

Qualcuno è rimasto sorpreso del vedere usare sempre la variabile fiammiferi in ogni funzione?

Beh, non c’è nulla di insolito: stiamo tenendo il conto del numero di fiammiferi, quindi non c’è nulla di strano a chiamarla ‘fiammiferi’ o ‘num_fiammiferi’ o altri nomi simili. Quel che bisogna tenere presente, però, è che non si tratta sempre della stessa variabile. Infatti ‘fiammiferi’ è dichiarata all’interno di main(), pertanto è una variabile locale: esiste solo dentro main() e non può essere vista da altre funzioni.

Ribadiamo il concetto: se una variabile è locale, allora nella altre funzioni possono esistere altre variabili con lo stesso nome e, finché saranno tutte locali, non si daranno noia fra di loro. Saranno a tutti gli effetti variabili diverse, il computer non le mischierà, esattamente come il nome Mario sulla vostra rubrica potrebbe riferirsi a Mario Rossi mentre il nome Mario sulla rubrica di un vostro amico potrebbe essere quello di Mario Bianchi.


Vi ricordate cosa abbiamo detto fin dall’inizio riguardo al passare un valore a una funzione? Il valore che passiamo finisce dentro la variabile che è indicata nelle parentesi accanto al nome. Per cui se scrivo:

void funzione_qualsiasi_n_1()
{
   int una_variabile = 100;
   funzione_qualsiasi_n_2(una_variabile);
}

void funzione_qualsiasi_n_2(int valore_ricevuto)
{
   cout << "Ho ricevuto il valore " << valore_ricevuto << endl;
}

Il risultato sarà la scritta:

Ho ricevuto il valore 100

né più né meno che se avessi scritto:

void funzione_qualsiasi_n_1()
{
   int una_variabile = 100;
   funzione_qualsiasi_n_2();
}

void funzione_qualsiasi_n_2()
{
   int valore_ricevuto = 100;
   cout << "Ho ricevuto il valore " << valore_ricevuto << endl;
}

La differenza consiste ovviamente nel fatto che nel secondo caso le due funzioni non si ‘parlano’ fra di loro, bensì una si limita a chiamare l’altra senza passargli alcun valore.

Nel primo caso invece quel che succede è che il compilatore crea, al momento della chiamata, la variabile ‘valore_ricevuto’ e vi inserisce il dato 100. Se ci chiedessero: «Dopo la chiamata, nella funzione funzione_qualsiasi_n_2() c’è o no una variabile ‘valore_ricevuto’ con valore 100?», dovremmo rispondere di sì in entrambi i casi.


Fin qui dovrebbe essere semplice perché ne avevamo già parlato.

Nella nostra nuova versione del gioco dei fiammiferi succede esattamente la stessa cosa.

all’interno di main() troviamo l’istruzione

int fiammiferi = 21;

che definisce una variabile locale ‘fiammiferi’ e la inizializza al valore 21.

Da questo rigo in poi la variabile ‘fiammiferi’ è disponibile dentro main() e ne possiamo fare ciò che vogliamo.

Quando viene chiamata mossaCane(), le viene passato il valore della variabile fiammiferi.

Se leggiamo com’è definita la funzione mossaCane(), però, scopriamo che è definita così:

int mossaCane(int fiammiferi)


Ciò significa che il compilatore creerà una nuova variabile, interna (locale!) alla funzione mossaCane(), la chiamerà ‘fiammiferi’, stabilirà che può contenere solo valori interi, e le assegnerà il valore che ha lì con sé, ossia quello che ha letto nella variabile ‘fiammiferi’ di main().


Di nuovo: stiamo creando variabili con lo stesso nome, ma queste variabili sono e rimarranno sempre indipendenti l’una dall’altra. Il computer si limiterà a COPIARE I VALORI di una all’interno dell’altra, per cui capiterà che in certi momenti i loro valori siano identici, ma per il computer si tratterà sempre di due variabili distinte.


A noi esseri umani, invece, fa comodo aver la possibilità di riciclare i nomi, non solo perché così non ci dobbiamo preoccupare che qualcun altro, all’interno dello stesso programma, usi lo stesso nome, ma anche perché ci rende molto più facile ricordare, mesi dopo, perché diamine abbiamo creato quella variabile e a cosa ci servisse.


Continuando a seguire il codice, scopriamo che alla fine di mossaCane() il computer torna in main(), ma si porta dietro il VALORE della variabile locale ‘fiammiferi’

return fiammiferi;

E chi c’è lì ad aspettare il valore? La variabile locale ‘fiammiferi’ di main(), la quale sostituirà il suo valore con quello ricevuto:

fiammiferi = mossaCane(fiammiferi);

Capisco che, essendo noi abituati a leggere da sinistra verso destra, ci rimanga un po’ ostico comprendere che ciò che capita a destra del segno di uguale viene eseguito prima di ciò che capita a sinistra, ma è solo questione di farci l’abitudine.

L’istruzione dice: «Ehi, Mister Compilator, inserisci il valore di ritorno della funzione mossaCane() nella variabile ‘fiammiferi’. Ah! A proposito, quando invochi la funzione mossaCane(), ricordati di portarti dietro il valore che c’è adesso in ‘fiammiferi’». Il nostro computer, per sapere quale valore inserire in ‘fiammiferi’, non può far altro che seguire tutta mossaCane() fino all’istruzione return.


In realtà la cosa è così semplice che a continuare a parlarne corro il rischio di farla sembrare più complicata di quello che è.

L’unica cosa su cui rimane da mettersi d’accordo è un altro parolone. Coraggio, questo è semplice :) Avete già digerito ‘invocare’ e ‘locale’, che sarà mai ‘parametro’? Beh, è solo il nome della variabile che si trova all’inizio della funzione, quella tra parentesi. Nel piccolo esempio di prima, ‘valore_ricevuto’ sarebbe stato un parametro.

Un parametro di una funzione è quindi una variabile come tutte, solo che non viene dichiarata dentro il corpo di una funzione, ossia il blocco fra le due parentesi graffe, bensì prima, fra le parentesi tonde; il suo scopo è ricevere i valori che arrivano dalle altre funzioni. Eccetto per questi due aspetti, un parametro si usa poi come tutte le altre variabili locali.


Ah, dimenticavo! Anche il valore che viaggia fra una funzione e l’altra di solito viene chiamato con un nome particolare: argomento. La distinzione non è molto importante; se però abbiamo voglia di essere precisi, dovremmo usare il termine parametro per parlare della variabile che riceve il valore e il termine argomento per riferisci al valore medesimo. Se invece non fossimo sempre così precisi, una volta inteso che la differenza è minima riusciremo lo stesso a comprendere il senso di ciò che si legge.


Avete visto con quanta poca fatica abbiamo eliminato una pericolosa variabile globale? Però possiamo fare di meglio; andiamo avanti.


Indirizzi di memoria: vita e morte delle variabili

Quando il numero è proprio grosso

Dicevamo che il C++ offre altri strumenti per affrontare questo genere di problemi, ossia il passaggio di parametri, alcuni ereditati dal C (es. i puntatori), altri ripresi da altri linguaggi (es. i references). Nessuno di questi due costrutti compare esplicitamente nel JavaScript perciò non potremo usare quel linguaggio come apripista. Possiamo però usarlo per sottolineare di nuovo le differenze con il C++, aspetto che può contribuire a chiarire i vari aspetti della questione.


Intanto dobbiamo tornare a Qt, dove chiuderemo il progetto esistente e ne apriremo uno nuovo, con un nome generico che potrebbe essere “Prove”. Infatti ciò di cui andiamo a parlare presenta più di una sfaccettatura e, invece di creare un programma ‘completo’, seppur minimale, useremo spezzoni di codice. Pertanto ci serve una specie di lavagna che riuseremo più volte – altrimenti dovremmo creare tanti progettini minuscoli di poche righe, cosa che ci farebbe perdere un sacco di tempo.

Non avete bisogno di indicazioni, ormai, per aprire un nuovo progetto: la directory rimane la solita, il nome è Prove.


Per introdurre l’argomento dobbiamo indagare un po’ come lavora un computer quando deve eseguire un programma. Vediamo di scoprirlo.


Vi ricordate che per esprimere la dimensione di un array abbiamo detto che avremmo usato come unità di misura i byte? Beh, non avevamo molta scelta: il byte è l’unità di misura della memoria di un computer. Qualunque dato inseriate con qualsiasi mezzo nella memoria di un computer, questo non potrà occupare meno di un byte. Diciamo che un byte è un mattoncino di memoria, la casella più piccola che ci possa essere.

Quando dichiariamo una variabile, il computer prende uno spazio di memoria e lo riserva a noi, ai nostri dati. Non è detto però che un singolo byte possa bastare a farci entrare quello che ci serve.

Nel C++ quando diciamo al computer di lasciarci uno spazio di memoria, gli specifichiamo anche che cosa ci metteremo dentro, ossia il tipo di dato per cui ci servirà quello spazio di memoria.

In questo modo, se gli diciamo che ci dobbiamo mettere qualcosa di voluminoso, lui si preparerà lasciandoci uno spazio composto da più celle affiancate, ossia da più byte.


È ora di entrare un po’ più dentro a questo argomento.

Proviamo con il seguente programmino:

#include <iostream>

using namespace std;

int main()
{
   int dimensione;

   dimensione = sizeof(int);

   cout << "Su questa macchina, una variabile intera è composta da "
         << dimensione << " byte." << endl << endl;
}

Non dovrebbe essere difficile intuire cosa fa, ma ora lo analizzeremo insieme. L’avete già fatto girare? Bene.

A me dice che una variabile intera è composta da 4 byte, ma ad altri può aver dato un’altra risposta: dipende dal vostro computer, infatti, non è universale.


Questo significa che, sul mio pc, quando scrivo:

int unavariabile;

il compilatore mi riserva uno spazio di memoria composto da quattro celle affiancate di un byte ciascuna.

Se non afferrate perché questo sia così importante, ve lo dico subito: significa che lo spazio a mia disposizione è limitato. Se ho ‘solo’ 4 byte a disposizione, nella mia variabile non potrò inserire qualsiasi valore, bensì al massimo 4.294.967.295. Il numero successivo causerebbe un errore.


Capito ora perché il C++ è così pignolo? Lui ci riserva esattamente l’area di memoria che chiediamo, senza sprechi.

Il perché venga un numero così strano e non uno tondo, come 4.000.000.000 o 10.000.000.000, è presto detto: il computer al suo interno non usa la base dieci, ossia quella che si rappresenta i numeri con le dieci cifre 0123456789, ma la base due. Quando converte dalla base 2 alla base 10, è raro che venga fuori un numero ‘tondo’. Ma anche questa è solo una difficoltà apparente: una volta che lo si sa e ci si è fatta l’abitudine, l’importante è solo ricordarsi che c’è un limite.

Ma andiamo avanti, prima che qualcuno abbia a che ridire, giacché non sono stato molto preciso.


Ho infatti paura che qualcuno dei miei lettori non abbia grande confidenza con i numeri negativi…

Vedere magari li avete visti tutti: -3, -86, -6.875.273, e così via. Non sono diversi dai positivi, in effetti, se ci si abitua a riportare correttamente il segno.

Già, il segno…

Domandina: come fa il computer a memorizzare il segno?

Risposta: in effetti lo deve memorizzare assieme al numero, nello stesso spazio di memoria.

Conclusione: il mio spazio di memoria in realtà non può arrivare a contenere il numero 4.294.967.295 perché una parte di quei quattro byte dovranno servire a contenere il segno (infatti io potrei desiderare di memorizzare un numero negativo).


Adesso possiamo essere un altro poco più precisi: sul mio computer una variabile intera può contenere i numeri che non sono più piccoli di -2.147.483.647 e non sono più grandi di 2.147.483.647.

È comunque un bel numero, non vi pare?

Immagino che vi starete domandando: «E se mi serve un numero più grande?». Beh, a tutto c’è una soluzione.

Intanto, se il numero che vi serve sta tra 2.147.483.647 e 4.294.967.295, allora potete usare un trucchetto: potete dire al compilatore: «Carissimo, sappi che userò solo numeri positivi, perciò non lasciare spazio per il segno: voglio tutti i miei 4 byte interi per il numero!».

Questo si può fare in modo semplice, basta scrivere unsigned prima di int:

unsigned int unavariabile;

A questo punto avrete tutti e 4 i byte per voi, pertanto potrete metterci un numero che va da 0 a 4.294.967.295 (il ché implica che, se ci mettete -1 o un altro numero negativo, il computer vi darà errore!).

Oltre a questo, quei geniacci che hanno lavorato sul C++ hanno fatto davvero un buon lavoro: esistono altre due possibilità: long e long long.


Un long int è una variabile dove ci sono più byte che in un int, ma quanti esattamente dipende sempre dal vostro computer; in un long long int ce ne sono ancora di più! Proviamo a misurare quanti:

#include <iostream>

using namespace std;

int main()
{
   int dimensione;

   dimensione = sizeof(long long int);

   cout << "Su questa macchina, una variabile long long int è composta da "
         << dimensione << " byte." << endl << endl;
}

Wow! Per me sono ben 8 byte. Un bel po’ di spazio, mi sembra.

Come dite? Vi sembra comunque troppo poco? Come? Al massimo ci può stare il numero 8.589.934.590?

No, non è così: in 8 byte, se la variabile è dichiarata unsigned, si può arrivare ad inserire il numero 18.446.744.073.709.551.615, che è più che sufficiente per la maggior parte degli scopi :)


Adesso però devo scusarmi: vi avevo promesso che avremmo guardato il programma insieme e non l’ho fatto. Provvedo subito.

L’unica riga oscura, però, è questa:

dimensione = sizeof(long long int);

Beh, sizeof l’abbiamo già incontrata quando dovevamo calcolare la dimensione totale di un array.

Qui stiamo facendo la stessa cosa: sizeof ci dice (ritorna) quanti byte occupa un oggetto che può stare in memoria. Non sempre è in grado di dare una risposta, ma di solito sì e su cose semplici come gli int e i long è garantito che ce la fa.

Il resto dovrebbe essere chiaro.


Bene, ora che abbiamo introdotto i long abbiamo tutti gli strumenti per procedere verso i references.


Scusi, sa mica l’indirizzo di…?

Sappiamo che una variabile è una cella di memoria che è contraddistinta da un nome e che contiene i dati che scegliamo noi di metterci. Questo ci dovrebbe far sospettare che esistano altre celle di memoria, che magari non hanno nome, e che contengono dati che il computer sceglie di metterci senza stare a sentire la nostra opinione. Ebbene, è proprio così. Consideriamo il seguente blocco di codice:

#include <iostream>

using namespace std;

void miafunzione(int mioargomento);

int globale;

int main()
{
   globale = 1;
   int var_di_main = 1;
   var_di_main++; // adesso var_di_main == 2

   cout << "globale = " << globale  << endl;
   cout << "var_di_main = " << var_di_main  << endl;

   miafunzione(3);
}

void miafunzione(int mioargomento)  // mioargomento == 3
{
   int var_di_miafunz = mioargomento; // var_di_miafunz == 3
   var_di_miafunz++; // adesso var_di_miafunz == 4

   cout << "mioargomento = " << mioargomento << endl;
   cout << "var_di_miafunz = " << var_di_miafunz << endl << endl;
}

Ciò che questo programma fa è immediatamente evidente e l’output che si ottiene è del tutto prevedibile. Comunque provate: copiatelo in main.cpp di Prove, compilatelo e fatelo girare per confermarvi che avete visto giusto.

In questo programma vengono immessi alcuni dati in delle variabili, che abbiamo scelto intere per non complicarci la vita, e poi vengono presentati a schermo. Questo, possiamo dire, è il modo ‘normale’ di usare delle variabili, indicando al compilatore la loro etichetta (detta anche nome della variabile).


Ma esiste un altro modo; proviamo a modificare il programma così:

#include <iostream>

using namespace std;

void miafunzione(int mioargomento);

int globale;

int main()
{
   globale = 1;
   int var_di_main = 1;
   var_di_main++; // adesso var_di_main == 2

   cout << "Indirizzo di globale = " << (unsigned int) (&globale) << endl;
   cout << "Indirizzo di var_di_main = " << (unsigned int) (&var_di_main) << endl;

   miafunzione(3);
}

void miafunzione(int mioargomento) // mioargomento == 3
{
   int var_di_miafunz = mioargomento; // var_di_miafunz == 4
   var_di_miafunz++; // adesso var_di_miafunz == 5

   cout << "Indirizzo di mioargomento = " << (unsigned int) (&mioargomento) << endl;
   cout << "Indirizzo di var_di_miafunz = "
   << (unsigned int) (&var_di_miafunz) << endl << endl;
}

ATTENZIONE!! Questo programma potrebbe darvi errore quando provate a eseguirlo.

Il fatto è che stiamo lavorando con numeri piuttosto grandi, per cui alcuni potrebbero essere costretti a modificare le scritte (unsigned int) dentro le istruzioni cout in (unsigned long int) o, se non bastasse, a (unsigned long long int). In quest’ultimo caso, dovremmo riuscire a sfangarla.


Anche dopo questa modifica può darsi che il risultato che ottenete vi sembri incomprensibile. Calma e sangue freddo! :)

Intanto vi dico cosa viene fuori a me:

Indirizzo di globale = 134520884 Indirizzo di var_di_main = 3215607996 Indirizzo di mioargomento = 3215607968 Indirizzo di var_di_miafunz = 3215607948</pre>

Ma la cosa divertente è che, se chiudo la finestra e faccio girare il programma un’altra volta… il risultato cambia! Ecco a voi il secondo round:


Qt_III_01.png

Domanda: cosa saranno quegli strani numerini?

E cosa vorrà dire quello strano simbolo ‘&’ che abbiamo usato?


Okay, saltiamoci subito dentro a piè pari, tanto è inutile rimandare.

Fin qui ci siamo abituati a fare una cosa molto comoda: quando avevamo bisogno di una variabile dicevamo al compilatore: «Ehi, metti un’etichetta a questo spazio di memoria e tienilo libero per me!». Da quel momento in poi eravamo in grado di accedere a quello spazio di memoria semplicemente usando quell’etichetta (il nome della variabile).

Questo però non è il sistema che il computer usa. Il computer non usa le etichette, bensì numera in modo progressivo tutte le celle di memoria, ossia assegna ad ognuno di loro un numerino identificativo, che è univoco. Quando noi gli chiediamo di accedere allo spazio di memoria con l’etichetta ‘miavariabile’, lui va a consultare i suoi appunti e verifica in che punto si trova quella cella di memoria, ossia qual è il suo numero progressivo, e solo a quel punto riesce a recuperare i nostri dati.


Vi ricordate l’esempio del postino che metteva le buste nelle cassette della posta a seconda del nome che c’era scritto sulla busta? Immaginate il postino che entra in un condomino dove c’è una bella fila di cassette della posta, su alcune delle quali però le targhette con i nomi sono tutte sbiadite. Il povero postino ha in mano una busta dove compare un nome che non è fra quelli leggibili, per cui non sa cosa fare.

Per fortuna in quel momento entra un condomino, così il postino può chiedere a lui se sa qual è la cassetta della posta giusta. Il condomino risponde: «Ma certo! È la numero 5 partendo da sinistra.»

A quel punto la busta può essere inserita nella giusta cassetta della posta giacché è indifferente riconoscerla in base alla targhetta che c’è scritta sopra o in base al numero progressivo nella serie.


La memoria del computer funziona alla stessa maniera: quando il computer carica il programma in memoria, gli assegna uno spazio riservato a cui gli altri programmi in funzione non possono accedere (come lui non potrà accedere a quello degli altri); all’interno di questo grossa scatola di memoria verranno via via inserite le istruzioni che vengono lette dai vari blocchi di codice dividendole in tante scatoline più piccole, tutto in maniera molto ordinata; all’interno di queste scatoline ne vengono create altre più piccole che rappresentano le nostra variabili.

Ogni cella di memoria ha un suo numero e il computer le riconosce in base a quel numero.


L’esempio del postino calza a pennello perché in informatica quel numero viene chiamato indirizzo. Ogni cella di memoria ha quindi un indirizzo; una variabile è una cella di memoria con un nome; ogni variabile ha quindi un nome E un indirizzo.

Per noi esseri umani è molto comodo riferirsi a una variabile tramite il suo nome giacché i numeri si ricordano male; per il computer è l’esatto opposto: lui preferisce il numero progressivo, cioè l’indirizzo.

Se ci punge vaghezza di scoprire qual è l’indirizzo di memoria di una variabile, possiamo recuperarlo tramite il simbolo ‘&’. Questo simbolo, usato alla sinistra di un nome di variabile (vale a dire così: &miavariabile), significa “indirizzo di”. Quando il compilatore legge quel simbolo prima del nome di variabile, capisce che quello che stiamo cercando non è il dato contenuto nella variabile, ma l’indirizzo della variabile.


In altre parole:

int miavariabile = 1;

crea una variabile e vi inserisce il valore “1”.

Questa variabile, oltre al nome, ha anche un indirizzo, di solito un numero molto grande perché in un computer ci sono una sacco di celle di memoria; però in questo momento di quell’indirizzo non ce ne curiamo.

cout << miavariabile;

farà comparire a schermo la scritta “1”. Anche qui non siamo interessati all’indirizzo della variabile.

D’un tratto, però, ci viene voglia di scoprirlo:

cout << (unsigned long int) (&miavariabile);

Quest’istruzione farà comparire a schermo l’indirizzo della variabile ‘miavariabile’ dove è contenuto il valore “1”. La variabile non subisce alcuna modificazione, soltanto noi abbiamo chiesto al compilatore un’informazione diversa: non il valore contenuto nella cella di memoria identificata dall’etichetta ‘miavariabile’, ma l’indirizzo della cella di memoria che abbiamo chiamato ‘miavariabile’.


Alcune precisazioni.

La parola magica cout cerca di capire di che tipo è il dato che stiamo cercando di mandare a schermo. Infatti se non riuscisse a capirlo non saprebbe come accontentarci. Pensate a quanto possa essere diverso rappresentare a schermo un semplice ‘1’ oppure una string come: “Non sapevo come mettere in crisi cout e perciò, dopo lungo meditare, ho deciso di inviargli questa scritta davvero lunga”.

Di solito se la cava egregiamente perché il compilatore è in grado di avvisarla: «Ehi, quello che ti sta arrivando è chiuso tra virgolette, perciò è facile che sia una string!»; oppure: «La variabile che ti è stata passata è stata dichiarata int, perciò dovresti prepararti a rappresentare un valore intero.» E così via, un sacco di buoni consigli.

Quando però gli passiamo un indirizzo di variabile, il compilatore non ha indizi da passare a cout perché l’unica cosa che sa è che si tratta di un bel numerone grande e nient’altro. cout, a sua volta, non sapendo cosa vogliamo noi, nel dubbio prova a rappresentarlo come un int, operazione che non sempre dà buoni frutti perché un int potrebbe non essere sufficiente per gestire un numero così grosso.


Per evitare tutte queste complicazioni sia al compilatore che a cout possiamo specificare quale tipo di dato stiamo inviando, gesto che il compilatore del C++ gradisce sempre moltissimo.

Ecco il significato di quell’istruzione fra parentesi:

(unsigned long int)

In pratica stiamo avvisando cout in questi termini: «Voglio che tu gestisca il dato che ti arriva come se fosse un numero intero lungo positivo.»

Le parentesi sono necessarie solo perché si tratta di più parole: senza le parentesi cout non sarebbe stato in grado di capire dove iniziava e dove finiva la nostra istruzione, invece con le parentesi può interpretare correttamente la parte unsigned long int come “informazione su come devo considerare il dato che segue” e la parte &miavariabile come “ecco il dato da rappresentare!”.


E poi distrugge tutto

È più chiaro ora perché a ogni riavvio del programma i numeri che vengono inviati a schermo sono diversi? In modo molto semplice, quando il programma finisce (→ termina), il computer lo cancella dalla memoria, rendendola libera per altri programmi che ne avessero bisogno.

Nel momento in cui lo fate partire un’altra volta, il computer gli assegna di nuovo uno spazio di memoria, ma non è assolutamente detto che sia lo stesso – anzi! Quasi mai sarà identico.

Pertanto le celle di memoria a disposizione del computer avranno indirizzi diversi.


Però possiamo farci una prima, banale idea di come il compilatore organizza la memoria a disposizione.

Prendiamo come esempio il due ‘round’ che ho riportato poco sopra.

Se diamo un’occhiata ai numeri, possiamo notare come le variabili locali siano un po’ tutte piazzate nella stessa area; infatti gli indirizzi divergono poco fra di loro. Quelle che appartengono alla stessa funzione sono anche un po’ più vicine fra di loro di quella dichiarata in un’altra funzione (quanto a ‘mioargomento’, abbiamo già visto come non ci sia nessuna differenza fra una variabile locale dichiarata dopo il segno di aperta graffa e una dichiarata fra parentesi tonda se non che quest’ultima si chiama parametro e riceve il valore spedito alla funzione, detto argomento).

La variabile globale si trova invece molto distante da tutte le altre.


Pensiamoci un po’: che fine fanno le variabili quando una funzione è finita e si torna nella funzione chiamante? Beh, se non ci avete ancora pensato, adesso dovreste essere in grado di intuire la risposta: la memoria non è infinita, pertanto il compilatore non si può permettere di sprecarla, perciò, una volta terminata l’esecuzione di una funzione, tutta la memoria che era da lei occupata viene cancellata, comprese le variabili, e ritorna disponibile per altre funzioni.

In altre parole, i dati contenuti nelle variabili locali vengono persi appena si esce dalla funzione.


Attenzione! Questo non accade quando si lascia una funzione per saltare dentro un’altra, perché la funzione da cui stiamo partendo non si è ancora conclusa. Però succede tutte le volte che il compilatore incontra un segno di parentesi graffa chiusa ‘}’.

Quando trova il simbolo di graffa chiusa, il compilatore parte dal presupposto che quel blocco di codice non serva più, pertanto lo distrugge, recuperando spazio per caricare in memoria le prossime istruzioni che deve eseguire.

SE prima del segno di graffa chiusa ha incontrato un’istruzione return, allora, state bene attenti, FA UNA COPIA del valore che è indicato dopo la parola return in un altro spazio di memoria che sa lui, al quale noi non possiamo accedere e nemmeno sapere quale sia l’indirizzo, e, dopo aver distrutto il blocco di codice che è terminato, lo consegna alla funzione chiamante.

Ciò che segue l'istruzione return è pertanto l’unica cosa che si salva, e si salva perché viene copiata in una zona di memoria che il compilatore gestisce per sé, ma il blocco originario, comprese tutte le variabili che erano state dichiarate lì, viene distrutto senza possibilità di recupero.


Questo ci dovrebbe dare un’idea di come il compilatore divida la memoria in blocchi logici, mettendo da una parte le cose che si suppone abbiano un ciclo di ‘vita’ più lungo (le variabili globali, che saranno cancellate solo alla fine del programma), e usando un’area separata per le cose che ‘moriranno’ presto (le variabili locali, create ex novo ad ogni chiamata di funzione e distrutte ad ogni conclusione di funzione).


Un po’ troppa teoria? Vi capisco, ma prima o poi l’argomento andava affrontato. È perché ora arrivano…


I references

Che cosa sono

Eccole qua, quelle brutte bestiacce. Pensavano di farci paura, ma ora che abbiamo scoperto i loro segreti siamo perfettamente in grado di domarle.


Vi confesso che, essendo incappato nel C++ dopo aver battuto il naso contro il C, dove i references non c’erano, il primo pensiero che mi è sorto è stato: «E che ci faccio con questa roba? Non li userò mai.»

Infatti nel C ci sono i puntatori (anche nel C++, che li ha ereditati da lui) che possono fare tutto quello che fanno i references e anche di più, per cui mi sembrava che si trattasse solo di una brutta copia di quelli. Qualcosa di superfluo, quindi, di ridondante.

Mi ci è voluto un bel po’ di tempo per capire che, sì, se siamo maligni possiamo definire i references come dei puntatori semplificati, ma la loro semplicità di utilizzo li rende molto più maneggevoli dei puntatori in un sacco di casi, ossia in tutti quelli in cui le ‘funzioni avanzate’ dei puntatori (leggi: l’aritmetica dei puntatori) non è richiesta.

Oggi mi sono convinto che il percorso di apprendimento tradizionale (prima i puntatori, che sono più difficile, e poi i references, che sono più facili) è assurdo! L’uso dei references rende i puntatori molto meno ostici da capire.


Ma cosa sono infine questi references?

In realtà qualcosa di molto semplice. Capire cosa sono è immediato. Capire perché sono così comodi richiede un po’ più di tempo.

Mettiamo che io abbia una variabile qualsiasi, cui possiamo dare il classico nome di ‘miavariabile’:

int miavariabile;

Il tipo int è il più semplice da usare, perciò quando si deve introdurre un nuovo argomento di solito si sceglie quello, ma il discorso che stiamo per fare vale per qualsiasi tipo di variabile.

Alla mia comunissima variabile posso assegnare un valore qualsiasi, diciamo 10:

miavariabile = 10;

Abbiamo visto che di questa variabile possiamo ottenere l’indirizzo. Questo indirizzo è solo un numero, per quanto grande; perciò può stare dentro un’altra variabile che, per coerenza, posso chiamare ‘indirizzo’ (ma anche ‘Pluto’, se mi piacesse farlo). Proviamoci:

unsigned long int indirizzo;
indirizzo = (unsigned long int) (&miavariabile);

Anche in questo caso mi devo garantire che l’indirizzo sia correttamente interpretato come unsigned long int, altrimenti il compilatore mi darebbe un messaggio di errore (per ora non possiamo stare ad analizzare il perché). A questo punto potrei divertirmi a controllare il contenuto delle due variabili:

cout << "miavariabile = " << miavariabile << endl;
cout << "indirizzo = " << indirizzo << endl << endl;

Nota: come vedete stavolta non abbiamo dovuto avvertire cout scrivendo (unsigned long int) prima di indirizzo, perché il compilatore sa già che ‘indirizzo’ è di tipo unsigned long int, perciò può avvisare cout senza bisogno del nostro aiuto.

Il risultato è quello che penso tutti si aspettassero:


Qt_III_02.png

Il valore di ‘indirizzo’ sarà diverso computer per computer e anche sullo stesso computer potrebbe cambiare tutte le volte che si fa girare il programma.


Per i pigroni cui ha fatto fatica di scrivere il codice fin qui, eccolo:

#include <iostream>

using namespace std;

int main()
{
   int miavariabile;
   miavariabile = 10;

   unsigned long int indirizzo;
   indirizzo = (unsigned long int) (&miavariabile);

   cout << "miavariabile = " << miavariabile << endl;
   cout << "indirizzo = " << indirizzo << endl << endl;
}

Però badate che d’ora innanzi non è garantito che vi copi tutti i pezzi di codice, anche quelli di poche righe come questo, nel tutorial: dovete abituarvi a scrivere le istruzioni dentro l’ambiente di programmazione.

A questo punto sembra che non abbiamo niente di particolarmente utile: abbiamo una variabile che contiene il valore 10 e un’altra che contiene un numero grande che, per l’appunto, corrisponde all’indirizzo della prima variabile. Ma ci servirà a qualcosa?

Ad esempio, cosa succederebbe se decidessimo di cambiare il numero contenuto in ‘indirizzo’? Il fatto che corrisponda all’indirizzo di un’altra variabile avrà qualche conseguenza? Proviamo:

indirizzo = 13; // un numero a caso, dedicato ai superstiziosi :)

Se riscriviamo

cout << "miavariabile = " << miavariabile << endl;
cout << "indirizzo = " << indirizzo << endl << endl;

otteniamo proprio il risultato che ci aspettavamo:


Qt_III_03.png


Perché non avrebbe dovuto andare così? Le due variabili sono due celle di memoria distinte e il valore che è contenuto in una non influenza quello che è contenuto nell’altra. Non ha nessuna importanza che possano avere entrambe lo stesso valore o che il valore di una sia la metà di quello dell’altra o un terzo o la radice quadrata, oppure per l’appunto il suo indirizzo: non c’è alcun rapporto fra le due. Punto e chiuso.


È tutto vero. Ma solo perché abbiamo usato il simbolo ‘&’ senza creare un reference :)

Proviamo a scrivere le stesse cose in un’altra maniera:

#include <iostream>

using namespace std;

int main()
{
   int miavariabile = 10;
   int& indirizzo = miavariabile;

   cout << "miavariabile = " << miavariabile << endl;
   cout << "indirizzo = " << indirizzo << endl << endl;   
}

Avete visto dov’è finito il simbolo ‘&’? Provate a far girare questo programma e ditemi se dà il risultato che vi aspettavate.


Qt_III_04.png


Sorpresi? Spero che lo siate.

Quello che abbiamo ottenuto spostando il simbolo ‘&’ dopo l’indicatore del tipo di variabile (int) è stata la creazione di un nuovo tipo di variabile: la variabile riferimento, ossia un reference.

Un reference è una normale variabile, ma con una piccola differenza: quando la si crea, l’unica sintassi lecita per dichiararla è facendola seguire da un segno di uguale e dal nome di un’altra variabile già dichiarata in precedenza.

In pratica, NON si può scrivere:

int& indirizzo;

È semplicemente proibito, non si può fare. Si può fare solo:

int& indirizzo = nome di un’altra variabile già dichiarata.

A quel punto il reference diventa un nome sostitutivo per l’altra variabile. Ossia, da quel momento in poi potremo far riferimento allo spazio di memoria contrassegnato con l’etichetta ‘miavariabile’ anche con il nome ‘indirizzo’. In pratica, lo stesso spazio di memoria avrà due etichette e usare l’una o l’altra sarà indifferente.

Vogliamo verificare?

Proviamo ad aggiungere le stesse righe di prima al nuovo programma:

#include <iostream>

using namespace std;

int main()
{
   int miavariabile = 10;
   int& indirizzo = miavariabile;

   cout << "miavariabile = " << miavariabile << endl;
   cout << "indirizzo = " << indirizzo << endl << endl;

   indirizzo = 13; // un numero a caso, dedicato ai superstiziosi :)

   cout << "miavariabile = " << miavariabile << endl;
   cout << "indirizzo = " << indirizzo << endl << endl;
}

Qual è il nuovo risultato che avete ottenuto?


Qt_III_05.png


Se la cosa vi lascia perplessi, ricordatevi quello che vi ho detto: capire cos’è un reference è semplice; intuire a cosa serve è un po’ meno immediato. Però ci arriveremo molto presto, adesso non fatevi bloccare dall’idea che a prima vista sembri una cosa del tutto inutile.

Un reference non è altro che questo: un secondo nome per la stessa variabile.


Riuscite a intuire almeno perché sia stato scelto proprio il simbolo ‘&’ per creare i reference? Beh, a dir la verità questo nessuno lo sa eccetto il signor Stroustrup :) Però possiamo specularci un po’ sopra.

Si è detto che il computer usa gli indirizzi delle variabili per accedere ai valori contenuti, e non i nomi: quelli sono una comodità per noi esseri umani. Bene, cosa ci potrebbe essere di più semplice per un compilatore che sentirsi chiedere: «Ehi, attaccami questa etichetta a questo indirizzo di memoria»?

Per il compilatore, che lì ci sia già un’etichetta non ha la minima importanza: lui si limita a eseguire gli ordini.


Adesso vediamo perché vogliamo dare più nomi alle stesse variabili.


Ma a che diavolo servono?

Per dare una prima idea dell’utilità dei reference, torniamo al nostro gioco dei fiammiferi e apportiamoci qualche piccola modifica.

Per ora ci limiteremo a cambiare il nome della variabile ‘fiammiferi’ che si trova nelle due funzioni mossaCane() e mossaAnatra(). Abbiamo già visto che non sarebbe necessario: il compilatore non fa alcuna confusione fra queste variabili; però ora stiamo per mettere in luce un nuovo aspetto e mi voglio garantire che nessuno faccia confusione con i nomi.

Perciò ‘fiammiferi’ dentro mossaCane() diventerà ‘fiammi_cane’ e fiammiferi dentro mossaAnatra() diventerà ‘fiammi_anatra’. Tanto per capirci, viene così:

#include <iostream>

using namespace std;

void spiegazioni();
int mossaCane(int fiammi_cane);
int mossaAnatra(int fiammi_anatra);

int main()
{
   spiegazioni();
   int fiammiferi = 21;

   do {
   fiammiferi = mossaCane(fiammiferi);
   fiammiferi = mossaAnatra(fiammiferi);
   } while ( 1 < fiammiferi );

   cout << endl << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
         << endl;
   cout << "Mi spiace, sei rimasto con l'ultimo"
         " fiammifero..." << endl;
   cout << "Ho vinto io." << endl;
   cout << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
         << endl << endl;

   return 0;
}

void spiegazioni()
{
   cout << "Ci sono 21 fiammiferi." << endl;
   cout << "Ne possiamo prendere da 1 a 4 per volta." << endl;
   cout << "Chi rimane con l'ultimo ha perso." << endl;
   cout << endl << "Hai la prima mossa." << endl << endl;
}

int mossaCane(int fiammi_cane)
{
   int presa = 0;

   while ( presa < 1 || presa > 4 ){

      cout << "Quanti fiammiferi prendi (da 1 a 4)? " << endl;
      cin >> presa;

      if( presa < 1 || presa > 4) {
         cout << "Spiacente. Devi prendere almeno un fiammifero, "
         "ma non più di quattro..." << endl << endl;
      }

   }

   fiammi_cane -= presa;
   cout << "Hai preso " << presa << " fiammiferi." << endl;
   cout << "Ne rimangono " << fiammi_cane << "." << endl;

   return fiammi_cane;
}

int mossaAnatra(int fiammi_anatra)
{
   int presa;

   if( 1 < fiammi_anatra ) {
      presa = fiammi_anatra - 1;
   }
   if( 6 < fiammi_anatra ) {
      presa = fiammi_anatra - 6;
   }
   if( 11 < fiammi_anatra ) {
      presa = fiammi_anatra - 11;
   }
   if( 16 < fiammi_anatra ) {
      presa = fiammi_anatra - 16;
   }

   fiammi_anatra -= presa;

   cout << "Io prendo " << presa << " fiammiferi." << endl;
   cout << "Adesso ne rimangono " << fiammi_anatra << "." << endl
         << endl;

   return fiammi_anatra;
}

Per ora è identico a prima, a parte la variazione sui nomi.

Adesso andiamo a inserirci i nostri references (commento le righe modificate):

#include <iostream>

using namespace std;

void spiegazioni();
void mossaCane(int& fiammi_cane);     // modificata
void mossaAnatra(int& fiammi_anatra); // modificata

int main()
{
   spiegazioni();
   int fiammiferi = 21;

   do {
      mossaCane(fiammiferi);   // modificata
      mossaAnatra(fiammiferi); // modificata
   } while ( 1 < fiammiferi );

   cout << endl << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
         << endl;
   cout << "Mi spiace, sei rimasto con l'ultimo"
            " fiammifero..." << endl;
   cout << "Ho vinto io." << endl;
   cout << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
         << endl << endl;

   return 0;
}

void spiegazioni()
{
   cout << "Ci sono 21 fiammiferi." << endl;
   cout << "Ne possiamo prendere da 1 a 4 per volta." << endl;
   cout << "Chi rimane con l'ultimo ha perso." << endl;
   cout << endl << "Hai la prima mossa." << endl << endl;
}

void mossaCane(int& fiammi_cane) // modificata
{
   int presa = 0;

   while ( presa < 1 || presa > 4 ){

      cout << "Quanti fiammiferi prendi (da 1 a 4)? " << endl;
      cin >> presa;

      if( presa < 1 || presa > 4) {
         cout << "Spiacente. Devi prendere almeno un fiammifero, "
         "ma non più di quattro..." << endl << endl;
      }

   }

   fiammi_cane -= presa;
   cout << "Hai preso " << presa << " fiammiferi." << endl;
   cout << "Ne rimangono " << fiammi_cane << "." << endl;
   // modificata
}

void mossaAnatra(int& fiammi_anatra) // modificata
{
   int presa;

   if( 1 < fiammi_anatra ) {
      presa = fiammi_anatra - 1;
   }
   if( 6 < fiammi_anatra ) {
   presa = fiammi_anatra - 6;
   }
   if( 11 < fiammi_anatra ) {
      presa = fiammi_anatra - 11;
   }
   if( 16 < fiammi_anatra ) {
      presa = fiammi_anatra - 16;
   }

   fiammi_anatra -= presa;

   cout << "Io prendo " << presa << " fiammiferi." << endl;
   cout << "Adesso ne rimangono " << fiammi_anatra << "." << endl
         << endl;
   // modificata
}

Riuscite a vedere tutte le differenze? Mi rendo conto che non è semplice, perciò guardiamole insieme.

Partendo dall’alto:


n.
precedente
attuale
1 int mossaCane(int fiammi_cane); void mossaCane(int& fiammi_cane);
2 int mossaAnatra(int fiammi_anatra); void mossaAnatra(int& fiammi_anatra);
3 fiammiferi = mossaCane(fiammiferi); mossaCane(fiammiferi);
4 fiammiferi = mossaAnatra(fiammiferi); mossaAnatra(fiammiferi);
5 int mossaCane(int fiammi_cane) void mossaCane(int& fiammi_cane)
6 return fiammi_cane; -
7 int mossaAnatra(int fiammi_anatra) void mossaAnatra(int& fiammi_anatra)
8 return fiammi_anatra; -


Prima di guardarle una per una cerchiamo di capire complessivamente cos’è cambiato.


Nel momento in cui definiamo ‘fiammi_cane’ e ‘fiammi_anatra’ come due references invece che due variabili ‘ordinarie’, queste diventano degli alias, ossia dei nuovi nomi, della variabile ‘fiammiferi’. Ciò significa che tutto ciò che viene fatto ai valori di ‘fiammi_cane’ e ‘fiammi_anatra’ si riflette seduta stante sul valore di ‘fiammiferi’. Anzi! Direi proprio che mi sono espresso male: ciò che viene fatto a ‘fiammi_cane’ e ‘fiammi_anatra’ viene fatto a ‘fiammiferi’ perché è sempre la stessa variabile, solo che ora ha tre nomi.

Compreso questo, si capisce anche che non c’è più bisogno che le funzioni mossaCane() e mossaAnatra() restituiscano il valore di ‘fiammi_cane’ e ‘fiammi_anatra’ perché ‘fiammiferi’ È GIÀ STATA MODIFICATA prima ancora che il compilatore torni dentro main(). ‘fiammiferi’ viene modificata nelle altre due funzioni tramite i suoi nuovi nome, rispettivamente ‘fiammi_cane’ e ‘fiammi_anatra’.


Se vi sembra che questo contraddica quanto detto sopra, cioè che non si può dichiarare un reference senza connetterlo subito a una variabile esistente, direi che formalmente avete ragione, ma nella pratica la regola viene rispettata.

Prendiamo il caso di ‘fiammi_cane’ (ma lo stesso ragionamento si applica a ‘fiammi_anatra’). Si tratta di una normale variabile che però è anche un reference nonché il parametro di una funzione. Quando il flusso di esecuzione del programma arriva sulla sua dichiarazione, il compilatore la crea e immediatamente la connette alla variabile di cui sta trasportando il valore – esattamente ciò che si fa con i references.


Adesso, se permettete, ridico lo stesso concetto espresso poco sopra usando termini più tecnici: il vantaggio dell’usare un reference come parametro di una funzione consiste nell’includere nello spazio di visibilità della funzione una variabile che ne era esclusa, ma usando per essa un nuovo nome.

All’uscita della funzione, il nuovo nome andrà distrutto, ma la variabile in sé no, perché è ancora visibile dalla funzione chiamante.


Adesso diamo un’occhiata alle modifiche al codice.

Le modifiche n. 1 e n. 2 ricalcano la 5 e la 7: le nuove funzioni non hanno più bisogno di restituire valori a main(), perciò non devono più essere dichiarate int mossa…(), bensì void (= sono una funzione che non restituisce alcun valore).

Inoltre ora i rispettivi parametri non sono più ‘normali’ variabili, bensì references, perciò saranno accompagnati dal simbolo ‘&’.


Le modifiche 3 e 4 sono inevitabili, visto che le funzioni mossaCane() e mossaAnatra() non restituiscono più alcun valore. Inoltre, ‘fiammiferi’ viene già modificata all’interno di quelle due funzioni, perciò…


Le modifiche 6 e 8 rappresentano il ‘vantaggio’ che abbiamo ottenuto dall’uso dei refrences, ossia la possibilità di non stare a restituire alcun valore.


Quel che abbiamo visto è solo un assaggio dei references, ovviamente. La comodità di poter accedere a una variabile dall’esterno dello spazio di visibilità in cui è nata va molto oltre al fatto che non è più necessario rimandare indietro copia del valore tramite return, però per ora accontentiamoci di aver capito cosa sono.


Domanda.

Secondo voi, a questo punto sarà possibile tornare a chiamare le variabili ‘fiammi_cane’ e ‘fiammi_anatra’ con il vecchio nome di ‘fiammiferi’? Oppure, trattandosi di references, ossia alias della stessa variabile, il compilatore corre il rischio di fare confusione?

Datevi la vostra risposta dopodiché provate. Stavolta non vi darò la soluzione perché siete più che in grado di risolvervi il problema da soli.


Adesso non ci resta che, nell’ordine, vedere cosa ne pensa il JavaScript dei references, ribadire il concetto chiave che abbiamo visto oggi, dare un’occhiata a un po’ di paroloni e a un limite dei references, dopodiché passare ai puntatori.

E lì vedrete che il gioco si fa duro ;-)


Differenze fra C++ e JavaScript: i references

Abbiamo detto che nel JavaScript i references non ci sono. Andiamo subito a vedere se possiamo essere smentiti. Il codice che segue, ad esempio, sembra proprio dimostrare l’opposto.

Il file index.html:

<!DOCTYPE html>

<html lang="it">
<head>
  <meta charset="utf-8">

  <link rel="stylesheet" type="text/css" href="standard.css" />
  <script type="text/javascript" src="Riferimenti.js"></script>

  <title>HTML</title>
</head>

<body>
  <h1>JavaScript e i references</h1>

  <div class="invisibile" id="principale">
    <p>var int_1 = 1;</p>
    <p>var int_2 = int_1;</p>

    <div class="stacco_1em">
      <p>Il valore di int_1 ora è:</p>
      <p id="primoID"> </p>
    </div>
    <div>
      <p>Il valore di int_2 ora è:</p>
      <p id="secondoID"> </p>
    </div>

    <p class="stacco_1em">int_2 = 2;</p>
    <div>
      <p>Il valore di int_1 ora è:</p>
      <p id="terzoID"> </p>
    </div>
    <div>
      <p>Il valore di int_2 ora è:</p>
      <p id="quartoID"> </p>
    </div>

    <p class="stacco_1em">Conclusione: Le due variabili non interferiscono fra di loro.</p>

    <p class="stacco_2em">var str_1 = "Sono un testo di prova";</p>
    <p>var str_2 = str_1;</p>

    <div class="stacco_1em">
      <p>Il valore di str_1 ora è:</p>
      <p id="quintoID"> </p>
    </div>
    <div>
      <p>Il valore di str_2 ora è:</p>
      <p id="sestoID"> </p>
    </div>

    <p class="stacco_1em">str_2 = "Nuovo testo";</p>
    <div>
      <p>Il valore di str_1 ora è:</p>
      <p id="settimoID"> </p>
    </div>
    <div>
      <p>Il valore di str_2 ora è:</p>
      <p id="ottavoID"> </p>
    </div>

    <p class="stacco_1em">Conclusione: Le due variabili non interferiscono fra di loro.</p>

    <p class="stacco_2em">var elenco_1 = {"Elemento0", "Elemento1", "Elemento2"};</p>
    <p>var elenco_2 = elenco_1;</p>

    <div class="stacco_1em">
      <p>Il valore dell'elemento 0 di elenco_1 ora è:</p>
      <p id="nonoID"> </p>
    </div>
    <div>
      <p>Il valore dell'elemento 0 di elenco_2 ora è:</p>
      <p id="decimoID"> </p>
    </div>

    <p class="stacco_1em">elenco_2[0] = "Nuovo elemento";</p>
    <div>
      <p>Il valore dell'elemento 0 di elenco_1 ora è:</p>
      <p id="undicesimoID"> </p>
    </div>
    <div>
      <p>Il valore dell'elemento 0 di elenco_1 ora è:</p>
      <p id="dodicesimoID"> </p>
    </div>

    <p class="stacco_1em">Conclusione: i due oggetti INTERFERISCONO fra di loro!</p>
  </div>

  <div class="stacco_1em" id="ultimo">
    <label>Premi il pulsante per iniziare</label>
    <input type="button" value="Esegui codice JS" onclick="prove();" />
  </div>
</body>
</html>

Il file standard.css:

p {
   margin       : 0.2em 0 0 0;
   padding      : 0;
}

div div p {
   display      : inline;
}

.stacco_1em {
   margin-top   : 1em;
}

.stacco_2em {
   margin-top   : 1em;
}

.invisibile{
   display      : none;
   visibility   : hidden;
}

.visibile{
   display      : initial;
   visibility   : visible;
}

Il file Riferimenti.js:

function prove() {
    var int_1 = 1;
    var int_2 = int_1;

    document.getElementById("primoID").innerHTML = int_1;
    document.getElementById("secondoID").innerHTML = int_2;

    int_2 = 2;

    document.getElementById("terzoID").innerHTML = int_1;
    document.getElementById("quartoID").innerHTML = int_2;

    var str_1 = "Sono un testo di prova";
    var str_2 = str_1;

    document.getElementById("quintoID").innerHTML = str_1;
    document.getElementById("sestoID").innerHTML = str_2;

    str_2 = "Nuovo testo";
    document.getElementById("settimoID").innerHTML = str_1;
    document.getElementById("ottavoID").innerHTML = str_2;

    var elenco_1 = ["Elemento0", "Elemento1", "Elemento2"];
    var elenco_2 = elenco_1;

    document.getElementById("nonoID").innerHTML = elenco_1[0];
    document.getElementById("decimoID").innerHTML = elenco_2[0];

    elenco_2[0] = "Nuovo elemento";

    document.getElementById("undicesimoID").innerHTML = elenco_1[0];
    document.getElementById("dodicesimoID").innerHTML = elenco_2[0];

    document.getElementById("principale").setAttribute("class","visibile");
    document.getElementById("ultimo").setAttribute("class","invisibile");
}

Anche se mi rendo conto che è un po’ noioso, vi chiedo di analizzare il codice con calma. Dovreste essere in grado di capire tutto ciò che fa.


In realtà ci limitiamo a riproporre tre volte lo stesso schema: dichiariamo due variabili dello stesso tipo; passiamo il valore della prima alla seconda; controlliamo che siano uguali; modifichiamo la seconda; controlliamo che sia cambiata solo lei.

Il file HTML serve solo come lista di segnaposti dove far comparire i valori delle variabili, con in più qualche riga per fra comprendere meglio quello che succede.

La parte interessante è solo il codice JavaScript. Dividiamolo in tre parti e analizziamole.


I parte

Dichiariamo una variabile e assegniamole un valore intero. Poi creiamo un’altra variabile e stabiliamo che deve copiare il suo valore dalla prima.

In apparenza va tutto bene: quando controlliamo i due valori sono identici.

Poi modifichiamo il valore della seconda impostandola su un altro numero a caso.

Adesso, quando andiamo a confrontare i due valori, scopriamo che sono diversi: la prima variabile è rimasta com’era, la seconda è cambiata come noi abbiamo deciso di cambiarla.


Questo è esattamente ciò che ci aspettavamo: le due variabili sono entità distinte e possono essere modificate separatamente. Ciò che capita a una non ha alcun riflesso su ciò che capita all’altra.

In altre parole, il segno ‘=’ non ha creato alcun legame fra di loro: ha solo fatto in modo che il valore di una fosse COPIATO nell’altra.


II parte

Questa parte è identica alla prima, ma per pignoleria abbiamo voluto controllare che anche le variabili cui era stata assegnata una sequenza di caratteri (un testo) si comportassero come quelle cui era stato assegnato un numero.

Anche in questo caso non si è creato alcun legame.


III parte

Nella terza parte non abbiamo definito due variabili, bensì due array. Nel JavaScript gli array non sono variabili, bensì oggetti, anche se di un tipo particolare.

Gli oggetti ci riservano una sorpresa.

Anche in questo caso le prime righe sembrano riproporre lo stesso schema: il secondo oggetto assume gli stessi valori del primo (beh, noi ne controlliamo solo uno, ma fidatevi: i due oggetti sono diventati identici).

La novità salta alla luce quando si modifica il valore del secondo:

elenco_2[0] = "Nuovo elemento";

Quando andiamo a controllare i rispettivi valori, nelle due righe successive, scopriamo che non solo il secondo è cambiato, bensì anche il primo.


Cos’è successo?

È successo che nel caso degli oggetti l’operazione ‘=’ non significa COPIA IL VALORE, bensì ASSEGNA UN NUOVO NOME.

In altre parole, mentre le variabili funzionano come nel C++, gli oggetti non possono essere copiati, bensì solo ricevere un nome in più. Essi si comportano COME SE fossero i references del C++.


Anche in questo caso possiamo dire che sì, la sintassi del JavaScript è studiata per semplificare le cose e risultare molto amichevole, ma va lo stesso conosciuta prima di scriverci del codice perché i risultati possono essere inattesi.


Ma come mai il JavaScript non dà la possibilità di copiare gli oggetti? Perché è un’operazione molto dispendiosa. Anche nel C++ si cerca di evitarla ogni volta che è possibile.


Prendiamo l’esempio di un array, che nel JavaScript è un oggetto dalla sintassi semplice e somiglia molto all’equivalente del C++. Un array consiste in una serie di valori; sarebbe come dire che è una serie di variabili. Ma quanto è lunga questa serie? Beh, non c’è un limite: un array potrebbe in via teorica contenere centinaia o addirittura migliaia di dati.

Ora provate a immaginarvi il lavorone che dovrebbe fare il compilatore se, entrando in una funzione, trovasse un parametro che aspetta una copia di un array di duemila elementi? Se poi la funzione compisse un’operazione molto semplice, come una ricerca, per poi finire subito, tutta quella copia sarebbe stata fatta per essere distrutta poco dopo…

Ma se poi la funzione fosse chiamata un’altra volta, tutto la copia dovrebbe essere effettuata di nuovo, e così via.

Lo spreco di risorse sarebbe assurdo e l’esecuzione del programma lentissima.


Se invece si passa un array per riferimento, a quel punto il compilatore deve trasportare un solo valore: l’indirizzo dell’array originario. Su quello spazio di memoria sarà attaccata una nuova etichetta e tutto funzionerà benissimo.


In conclusione, affermare che nel JavaScript i references non esistono è formalmente vero nel senso che non c’è un’istruzione o un simbolo da usare che permetta di creare in modo esplicito un reference, ma l’operazione di assegnazione di nuovo nome esiste ed è l’unica scelta quando si vuole passare un oggetto.


Anche in questo caso il C++ presenta le sue differenze: sia le variabili che gli oggetti possono essere copiati oppure può essere assegnato loro un nuovo nome. La sintassi è la medesima in entrambi i casi:

int var_1 = 1;
var_2 = 2;
cout << "dopo 'var_2 = 2':" << endl;
cout << "var_1 = " << var_1 << endl;
cout << "var_2 = " << var_2 << endl;
cout << "var_3 = " << var_3 << endl << endl;

var_3 = 3;
cout << "dopo 'var_3 = 3':" << endl;
cout << "var_1 = " << var_1 << endl;
cout << "var_2 = " << var_2 << endl;
cout << "var_3 = " << var_3 << endl << endl;

vector<int> vec_1 = {1, 2, 3, 4}; // crea un vector di 4 elementi
vector<int> vec_2 = vec_1; // *copia* il primo vector nel secondo vector
vector<int>& vec_3 = vec_1; // crea un reference al primo vector

vec_2[0] = 100;
cout << "dopo 'vec_2[0] = 100':" << endl;
cout << "vec_1[0] = " << vec_1[0] << endl;
cout << "vec_2[0] = " << vec_2[0] << endl;
cout << "vec_3[0] = " << vec_3[0] << endl << endl;

vec_3[0] = 13;
cout << "dopo 'vec_3[0] = 13':" << endl;
cout << "vec_1[0] = " << vec_1[0] << endl;
cout << "vec_2[0] = " << vec_2[0] << endl;
cout << "vec_3[0] = " << vec_3[0] << endl << endl;

Il codice non è completo, ma adesso siete perfettamente in grado di farlo funzionare.

Ricordatevi:

  1. dovete includere il file con il codice per i vector nelle prime righe (riguardate gli esempi passati!);
  2. dovete aggiungere la riga CONFIG += c++11 nel file Prove.pro.

Provate a scrivete le poche righe mancanti di questo programma e a farlo girare. Cercate anche di prevedere cosa farà prima di guardare il risultato.


Se proprio non ce la fate, qui sotto c’è la soluzione, ma provateci prima da soli. È facilissimo.

#include <iostream>
#include <vector>

using namespace std;

int main()
{
   int var_1 = 1;
   int var_2 = var_1; // copia il valore
   int& var_3 = var_1; // crea un reference

   var_2 = 2;
   cout << "dopo 'var_2 = 2':" << endl;
   cout << "var_1 = " << var_1 << endl;
   cout << "var_2 = " << var_2 << endl;
   cout << "var_3 = " << var_3 << endl << endl;

   var_3 = 3;
   cout << "dopo 'var_3 = 3':" << endl;
   cout << "var_1 = " << var_1 << endl;
   cout << "var_2 = " << var_2 << endl;
   cout << "var_3 = " << var_3 << endl << endl;

   vector<int> vec_1 = {1, 2, 3, 4}; // crea un vector di 4 elementi
   vector<int> vec_2 = vec_1; // *copia* il primo vector nel secondo vector
   vector<int>& vec_3 = vec_1; // crea un reference al primo vector

   vec_2[0] = 100;
   cout << "dopo 'vec_2[0] = 100':" << endl;
   cout << "vec_1[0] = " << vec_1[0] << endl;
   cout << "vec_2[0] = " << vec_2[0] << endl;
   cout << "vec_3[0] = " << vec_3[0] << endl << endl;

   vec_3[0] = 13;
   cout << "dopo 'vec_3[0] = 13':" << endl;
   cout << "vec_1[0] = " << vec_1[0] << endl;
   cout << "vec_2[0] = " << vec_2[0] << endl;
   cout << "vec_3[0] = " << vec_3[0] << endl << endl;

   return 0;
}

Come vedete il C++ è pignolo, però coerente con le sue scelte. Una volta capita come funziona una sintassi, le sorprese sono poche.


Differenze fra C++ e JavaScript: string

Nel C++ string è un oggetto come tutti gli altri. L’unica sua particolarità è che si può usarlo anche solo dichiarando:

#include <iostream>

senza stare a scrivere:

#include <string>

Ma si tratta di una mera comodità che non differenzia string in modo particolare.

Una string in C++ può essere modificata, copiata, passata ad un reference o quant’altro volete.

Vediamo cosa succede invece nel JavaScript. Chiamerò il file HTML ProvaString.html e quello JavaScript ProvaString.js.

<!DOCTYPE html>

<html lang="it">
  <head>
    <meta charset="utf-8">

    <link rel="stylesheet" type="text/css" href="standard.css" />
    <script type="text/javascript" src="ProvaString.js"></script>

    <title>JavaScript e string</title>
  </head>

  <body>
    <h1>JavaScript e string</h1>

    <div class="invisibile" id="principale">
      <p>var str_1 = "Sono un testo di prova";</p>
      <p>var str_2 = str_1;</p>

      <p class="stacco_1em">str_2 = "Nuovo testo";</p>
      <div>
        <p>str_1 == </p>
        <p id="ID_1"> </p>
      </div>
      <div>
        <p>str_2 == </p>
        <p id="ID_2"> </p>
      </div>

      <p class="stacco_1em">// La riga seguente modifica str_2 da "Nuovo testo" a "Vecchio testo"</p>
      <p>str_2 = str_2.replace("Nuovo", "Vecchio");</p>
      <div>
        <p>str_1 == </p>
        <p id="ID_3"> </p>
      </div>
      <div>
        <p>str_2 == </p>
        <p id="ID_4"> </p>
      </div>
    </div>

    <div class="stacco_1em" id="ultimo">
      <label>Premi il pulsante per iniziare</label>
      <input type="button" value="Esegui codice JS" onclick="prove();" />
    </div>
  </body>
</html>

ProvaString.js:

function prove() {
    var str_1 = "Sono un testo di prova";
    var str_2 = str_1;

    str_2 = "Nuovo testo";
    document.getElementById("ID_1").innerHTML = str_1;
    document.getElementById("ID_2").innerHTML = str_2;

    // La riga seguente modifica str_2 da "Nuovo testo" a "Vecchio testo"
    str_2 = str_2.replace("Nuovo", "Vecchio");
    document.getElementById("ID_3").innerHTML = str_1;
    document.getElementById("ID_4").innerHTML = str_2;

    document.getElementById("principale").setAttribute("class","visibile");
    document.getElementById("ultimo").setAttribute("class","invisibile");
}

Stavolta sono più brevi :-)

L’unica novità è rappresentata dalla riga

str_2 = str_2.replace("Nuovo", "Vecchio");

che però fa una cosa molto ovvia: il metodo replace() (in italiano, ‘sostituisci’) accetta ben due parametri invece che il classico uno e poi agisce così: cerca nella string il valore che gli è stato passato come primo argomento e lo sostituisce con il secondo.

Ci viene da domandarci: ci sarà qualcosa di simile nel C++?

Ebbene sì: anche nel C++ c’è un metodo replace() di string, che però ha bisogno di… ancora più parametri.


Guardiamolo insieme.

Se volessi sostituire “Nuovo” con “Vecchio” nella sequenza “Nuovo testo” dovrei dire a relace():

  1. partendo dalla posizione 0 (anche per string la prima posizione è la numero 0)
  2. togli 5 caratteri (N-u-o-v-o)
  3. e al posto loro metti “Vecchio”

Quindi viene:

nome_della_variabile.replace(0, 5, "Vecchio");

Vediamo se funziona.

#include <iostream>

using namespace std;

int main()
{
   string str_1 = "Sono un testo di prova";
   string str_2 = str_1; // copia il valore

   str_2 = "Nuovo testo";
   cout << "dopo 'Nuovo testo':" << endl;
   cout << "str_1 = " << str_1 << endl;
   cout << "str_2 = " << str_2 << endl << endl;

   str_2.replace(0, 5, "Vecchio");
   cout << "dopo 'str_2.replace':" << endl;
   cout << "str_1 = " << str_1 << endl;
   cout << "str_2 = " << str_2 << endl << endl;

   return 0;
}

Provate a fare girare il programma. Direi che anche replace() dell’oggetto string del C++ fa il suo sporco lavoro.


Vedete però una differenza nella sintassi, oltre al fatto che nel C++ abbiamo bisogno di 3 parametri?

In JavaScript:

str_2 = str_2.replace("Nuovo", "Vecchio");

Nel C++:

str_2.replace(0, 5, "Vecchio");

C’è qualcosa che vi salta all’occhio?

La differenza consiste in quel str_2 = in più. In pratica, nel JavaScript dovete specificare che la ‘nuova’ stinga str_2 deve diventare uguale alla ‘vecchia’ stringa str_2 una volta che siano stati sostituiti dei caratteri. Torniamo indietro con la memoria: la sintassi dell’istruzione JavaScript l’abbiamo già incontrata in precedenza, quando dicevamo che per incrementare di 5 il valore di un numero intero potevamo scegliere se scrivere:

var miavar = 3;
miavar += 5; //  miavar == 8
miavar =  miavar + 5; // miavar == 13

In quest’ultimo codice, se sostituissimo il primo var con int, la stessa sintassi (derivata dal C) si potrebbe applicare pari pari al C++.

Entrambe le operazioni trasformano il valore contenuto nella variabile ‘miavar’ sommandogli il numero 5.


Anche in questo caso nel JavaScript è necessario ripetere che la variabile ‘str_2’ deve diventare uguale a ciò che sarà la variabile ‘str_2’ DOPO che “Nuovo” sarà stato sostituito con “Vecchio”.

Nel C++ invece basta dire: agisci su ‘str_2’ sostituendo “Vecchio” ai primi 5 caratteri.


Per dirla meglio: il metodo replace() di string nel JavaScript non modifica l’oggetto cui si riferisce (quello a cui è ‘attaccato’ con un punto), bensì lo usa solo come testo di partenza per creare una nuova string. Questo è dovuto al fatto che le string nel JavaScript non si possono modificare.

Se modificate una string, create qualcosa di nuovo che deve essere ri-passato alla variabile tramite il segno di ‘=’.


Ancora non abbiamo parlato di cosa siano i tipi primitivi e cosa siano gli oggetti perché la mia idea è arrivare a scrivere il codice di un oggetto prima di spenderci tante parole teoriche sopra. Però possiamo arrivare al punto da una via secondaria.


Vi ricordate che nel C++, a seconda del tipo di dato a cui è destinata una variabile, il compilatore vi fornisce (→ assegna) qualche byte in più o in meno? Ok, ma ora facciamoci un’altra domanda: come diavolo fa il compilatore a sapere quanti byte lasciare liberi per la mia string se non ha idea di quanto cavolo di testo ci voglio mettere dentro? E se ci volessi mettere un libro intero?


Una bella domanda in effetti, ma la risposta è ancora al di là della nostra portata. Posso dire solo questo: come un vector può contenere un numero elevatissimo di elementi così una string può contenere un numero enorme di lettere. Questo è dovuto al fatto che non sono variabili come gli int o i long int, bensì oggetti, ossia qualcosa che non ha una misura prefissata in anticipo. Più vi aggiungete testo, e più la string si ‘dilata’, chiedendo (e ottenendo!) dal compilatore sempre nuovi byte – almeno entro certi limiti, tra cui la memoria che il computer rende disponibile.


Bene, il JavaScript vuole semplificare la vita al programmatore e consentirgli di trattare le cose come gli int come fossero oggetti; le string purtroppo cadono a metà fra questi due mondi, perché potete metterle in una ‘semplice’ variabile, ma potrebbero chiedere di gestire una quantità enorme di memoria. Per gestire le string il JavaScript deve scendere a qualche compromesso.

Tutta questa teoria ci tornerà più chiara dopo che avremo fatto conoscenza con gli oggetti.


I limiti dei references

I references funzionano molto bene, ma hanno anche qualche limite che è bene conoscere.

Intanto potremmo provare a vedere se questo programma potrebbe funzionare:

#include <iostream>

using namespace std;

void prova(int& test);

int main()
{
   prova(5);

   return 0;
}

void prova(int& test)
{
   cout << "Vediamo se si può fare" << endl;
}

Se aveste poca voglia di provarlo, va bene lo stesso: infatti il compilatore si rifiuterà di portare a termine l’operazione.

Come avevamo detto, un reference, o variabile riferimento, o anche solo riferimento, può essere dichiarato solo passandogli una variabile già esistente.

In questo caso stiamo passando un numero, che è una cosa che non ha un indirizzo di memoria con un’etichetta e a cui non si può attaccare un nome. Non è idoneo per essere passato ad un reference.


Questo significa che, se create una funzione che ha come parametro un reference, non potrete passarle altro che una variabile. Se invece ha come come parametro una variabile ‘normale’, allora potete passarle anche un numero.


Un altro aspetto dei references che è bene chiarire è che il legame che si crea con la variabile originaria è inscindibile. Ossia, una volta che si è scritto:

string miavar1 = "Una frase a caso";
string miavar2 = "Un’altra frase a caso";
string& miavar3 = miavar;

Non si può più ‘tornare indietro’. In seguito si potrà scrivere:

miavar3 = miavar2;

ma il risultato che si ottiene sarà di assegnare il valore della variabile ‘miavar2’ alla variabile ‘miavar3’, che è identico ad assegnarlo a ‘miavar’. Provare per credere:

#include <iostream>

using namespace std;

int main()
{
   string miavar1 = "Una frase a caso";
   string miavar2 = "Un’altra frase a caso";
   string& miavar3 = miavar1;
   miavar3 = miavar2;

   cout << "miavar1 = " << miavar1 << endl << endl;

   return 0;
}

Quindi non esiste un modo per ‘staccare’ un reference dalla variabile di cui è un alias per ‘attaccarlo’ a un’altra.


I due punti appena visti sono già sufficienti a farci desiderare qualcosa di meglio, ma poniamoci un’altra domanda: sarebbe possibile ottenere l’indirizzo della variabile originaria partendo dal reference? Ossia, una volta che avessi scritto:

int miavar = 9; // numero a caso
int& mioref = miavar; // reference a miavar

se scrivessi:

unsigned long int ind1 = (unsigned long int) &mioref; // indirizzo di mioref

cosa otterrei? Lo stesso indirizzo che avrei scrivendo

unsigned long int ind2= (unsigned long int) &miavar; // indirizzo di miavar

oppure un altro?

Trovate la domanda un po’ accademica? Ci torneremo sopra, ma intanto cerchiamo la risposta.

#include <iostream>

using namespace std;

int main()
{
   int miavar = 9; // numero a caso
   int& mioref = miavar; // reference a miavar

   unsigned long int ind1 = (unsigned long int) &mioref; // indirizzo di mioref

   cout << "ind1 = " << ind1 << endl;

   unsigned long int ind2= (unsigned long int) &miavar; // indirizzo di miavar

   cout << "ind2 = " << ind2 << endl << endl;

   return 0;
}

Ecco qui lo output:


Qt_III_06.png

Qualunque numero vi sia venuto, quello che conta è che i valori delle due variabili siano identici.

La conclusione abbastanza evidente è, se non si fosse ancora capito, che il reference non gode di vita propria: è solo un altro nome per una variabile e questo significa che noi non possiamo mai farci operazioni ‘a parte’, ossia non possiamo fare nulla su di esso che non coinvolga l’altra variabile.


Pensateci un po’: non sarebbe un miglioramento avere un reference che non si debba collegare fin da subito ad una variabile? Non sarebbe bello che potesse accettare anche numeri, oltre che variabili? Non sarebbe più flessibile se si potesse ‘staccare’ da una variabile per ‘attaccarlo’ a un’altra? Non sarebbe più comodo se avesse un suo indirizzo autonomo, in modo che si potesse decidere di fare a lui qualcosa che non vogliamo che si rifletta sulla variabile?


Sapete già di cosa sto parlando, vero? :)

È ora di andare a incontrare i puntatori, dei references dopati.


Paroloni

Facciamo una pausa per ripassare i paroloni che conosciamo:

  • chiamare una funzione si dice invocare una funzione;
  • la variabile (o le variabili) delle funzioni che aspettano valori in arrivo e sono scritte tra parentesi tonde si chiamano parametri;
  • i references si possono chiamare anche variabili riferimento o riferimenti.

Per dire che il compilatore ci mette a disposizione della memoria (ad es., quando definiamo una variabile) ho usato l’espressione assegnare. Questa a mio avviso è una idonea traduzione del verbo inglese allocate. Rimane però il fatto che la maggior parte degli informatici, per motivi a me ignoti, preferisce usare l’espressione ‘allocare’, tant’è che pure il dizionario Treccani è costretto a riportare questo termine, anche se specificando che è “sull’esempio dell’inglese to allocate”.

Chi gradisce questo neologismo lo usi senza problemi.


Aggiungiamo ora un altro parolone.

All’inizio del file main.cpp abbiamo sempre trascritto le dichiarazioni di funzione. Abbiamo detto che servono al compilatore per verificare che non ci siano nomi di funzione duplicati.

Orbene, quelle dichiarazioni di funzione in realtà si chiamano prototipi (prototype) di funzione.


I puntatori

Nel JavaScript non ci sono nemmeno i pointers, perciò rimarremo ancorati al nostro C++.


I puntatori sono forse l’eredità più potente che il C ha trasmesso al C++, anche se in quest’ultimo sono un po’ meno usati perché spesso basta un comodo reference. Nel loro caso ci accorgeremo presto che non ci sono dubbi sulla loro utilità, ma all’inizio ci verranno un po’ di dubbi del perché mai siano stati inventati.

Anche nel loro caso, infatti, capire cosa sono è davvero banale; apprendere a cosa servono un po’ più faticoso. Ma, come di più, ci sarà anche da chiarire come si usano perché la loro sintassi è un gocciolino meno amichevole di quella dei references.


Allora, intanto la definizione: i pointers, o puntatori, sono delle normali variabili; sono, tra l’altro, variabili facili da usare perché contengono solo numeri interi; non possono quindi, ad esempio, contenere string; ma hanno una particolarità: il loro valore può essere solo l’indirizzo di una variabile.

Questa definizione avrà bisogno di una limatina, ma per ora ci va benissimo.


Guardiamoli in faccia:

int miavar; // questa è una variabile ‘normale’
int* puntatore; // questo è un pointer, o puntatore

Da un punto di vista della dichiarazione, la sintassi di un puntatore è molto semplice: basta scrivere un asterisco alla destra della dichiarazione del tipo di dato.

Come vedete, non c’è l’obbligo di ‘collegarlo’ immediatamente ad una variabile: un puntatore è una variabile autonoma, che può essere collegata a un’altra variabile oppure no. Questo ci fa subito capire che non è solo un altro nome per una variabile.


Rileggiamo la definizione.

A qualcuno potrebbe non tornare una cosa: se abbiamo detto che contiene solo numeri interi, perché dichiararlo int? La dichiarazione int dovrebbe essere sottintesa…

Questo è un punto su cui torniamo fra pochissimo. Guardiamo prima un aspetto più semplice: il valore contenuto in un pointer può essere solo l’indirizzo di una variabile.

Per l’appunto noi abbiamo una variabile a disposizione… Proviamo a vedere cosa succede se passiamo il suo indirizzo al puntatore.

int miavar; // questa è una variabile ‘normale’
int* puntatore; // questo è un pointer, o puntatore
puntatore = &miavar; // adesso il puntatore contiene l’indirizzo di ‘miavar’

Beh, sembra che l’abbia digerito senza problemi.

Ma cosa dovrebbe farci supporre tutto questo? Forse che si è creato un qualche tipo di legame fra il puntatore e la variabile? Ebbene sì, è proprio così: dando in pasto al puntatore l’indirizzo della variabile lo abbiamo implicitamente autorizzato a agire sul contenuto della nostra variabile.


Un puntatore, quindi, non è un’altra etichetta per uno spazio di memoria, ma è una variabile a sé stante, con i suoi propri byte a disposizione, che però può essere istruita a ‘prendere di mira’ un’altra variabile. Per tenere di mira quella variabile non vuole sapere l’etichetta che c’è sopra, bensì direttamente il suo indirizzo di memoria; dentro di sé, come valore, terrà copia di quell’indirizzo (che è sempre un numero intero), ma, se glielo chiediamo noi, potrà usare questa informazione per andare a mettere le mani dentro la variabile che sta tenendo di mira.

Il nome puntatore è quindo molto azzeccato: l’indirizzo di memoria è il mezzo con cui può chiedere al compilatore di accedere alla variabile che punta, senza curarsi di quante e quali siano le etichette che le sono state ‘attaccate’ sopra.


Potete già capire quante potenzialità in più ci siano rispetto a un reference: avendo il puntatore il suo spazio di memoria, i suoi propri byte, può con facilità essere istruito a puntare a un’altra variabile: basta dargli il nuovo indirizzo.


Vi starete chiedendo: «Ma quindi, se ora scrivessi

puntatore = 5;

anche il valore di ‘miavar’ diventerebbe 5?»

Risposta: quasi.

Avete azzeccato l’idea di fondo, ma il puntatore ha una sua sintassi che va rispettata. Rendiamoci conto che è comunque una variabile con il suo spazio di memoria, quindi dobbiamo sempre specificare al compilatore se vogliamo agire sul valore del puntatore oppure sul valore della variabile puntata.


L’istruzione soprastante dovrebbe far fare un tentativo al compilatore di inserire il numero 5 nello spazio di memoria del puntatore. In altre parole, di sostituire il valore del puntatore con il numero 5. Il problema è che quello sarebbe poi interpretato come l’indirizzo di una variabile, cosa che porterebbe immediatamente a un errore perché non abbiamo idea di a cosa sia dedicato l'indirizzo n. 5 (lo sa solo il computer).

Conclusione: il compilatore ve lo impedirà. Si bloccherà e restituirà errore.


Per dire a un puntatore che noi intendiamo agire sulla variabile di cui ha l’indirizzo dobbiamo usare di nuovo l’asterisco. Perciò

*puntatore = 5;

otterrà esattamente l’effetto sperato: cambierà il valore di ‘miavar’ a 5.

Ricordiamoci questa regola: usare un asterisco prima di un nome di puntatore istruisce il compilatore ad andare ad agire sulla variabile puntata e non sul puntatore.

Vogliamo fare un po’ di prove?

Provo a suggerire qualche spezzone di codice, ma fate dei tentativi anche da soli.


Programma che dà errore (provate a leggere che errore vi dà):

#include <iostream>

using namespace std;

int main()
{
   int miavar; // questa è una variabile ‘normale’
   int* puntatore; // questo è un pointer, o puntatore
   puntatore = &miavar; // adesso il puntatore contiene l’indirizzo di ‘miavar’

   miavar = 8;
   puntatore = 5; // istruzione proibita

   cout << "miavar = " << miavar << endl;
   cout << "puntatore = " << puntatore << endl;

   return 0;
}

Codice un po’ più chiaro (questo compila):

#include <iostream>

using namespace std;

int main()
{
   int miavar; // questa è una variabile ‘normale’
   int* puntatore; // questo è un pointer, o puntatore
   puntatore = &miavar; // adesso il puntatore contiene l’indirizzo di ‘miavar’

   miavar = 8;
   *puntatore = 5; // ok, agisco sulla variabile puntata

   cout << "miavar = " << miavar << endl;
   cout << "indirizzo di 'miavar' = " << puntatore << endl;
   cout << "valore puntato = " << *puntatore << endl;
   cout << "indirizzo di 'puntatore' = " << &puntatore << endl << endl;

   return 0;
}

Sorpresi dalla penultima istruzione?

Non ce n’è motivo: il puntatore è una variabile come tutte, con il suo indirizzo di memoria, il suo spazio di memoria, la sua brava etichetta, ecc. ecc.

La sua unica particolarità è che i valori che contiene sono considerati sempre indirizzi di memoria.


Tornando al dubbio precedente, ossia del perché abbiamo fatto seguire il nome del puntatore da int, c’è da capire una cosa su come si legge la dichiarazione di un puntatore.

Quando scriviamo int* nomepuntatore non stiamo dicendo al compilatore di costruire un puntatore che abbia come valore numeri interi: un indirizzo di memoria sarà sempre un numero intero. Stiamo invece dicendo al compilatore di costruire un puntatore che dovrà tenere di mira una variabile dichiarata int.

Ossia, mentre questo non si può fare:

string testo;
int* punt;
punt = &testo; // errore! punt deve puntare a una variabile di tipo int

questo invece è corretto:

string testo;
string* punt;
punt = &testo; // ok. punt nasce per puntare a una variabile di tipo string

In altre parole, quello che noi facciamo è venire incontro alla proverbiale pignoleria del C++ e indicare al compilatore che tipo di dati gestiremo tramite il nostro puntatore.

Come conseguenza di quanto sopra, ricordiamoci che un puntatore può sempre essere ‘spostato’ verso un’altra variabile, ma solo se il tipo di quella variabile è coerente con la dichiarazione del puntatore.


Il gioco dei fiammiferi con i puntatori

Abbiamo detto che i puntatori fanno tutto quello che fanno i references e anche di più. Se volessimo riscrivere il gioco dei fiammiferi in un modo che fa uso dei puntatori invece che dei references, ci potremmo riuscire?

Proviamo:

#include <iostream>

using namespace std;

void spiegazioni();
void mossaCane(int *fiammiferi);
void mossaAnatra(int *fiammiferi);

int main()
{
   spiegazioni();
   int fiammiferi = 21;

   do {
      mossaCane(&fiammiferi);
      mossaAnatra(&fiammiferi);
   } while ( 1 < fiammiferi );

   cout << endl << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
       << endl;
   cout << "Mi spiace, sei rimasto con l'ultimo"
             " fiammifero..." << endl;
   cout << "Ho vinto io." << endl;
   cout << "* * * * * * * * * * * * * * * * * * * * * * * * * * *"
       << endl << endl;

   return 0;
}

void spiegazioni()
{
   cout << "Ci sono 21 fiammiferi." << endl;
   cout << "Ne possiamo prendere da 1 a 4 per volta." << endl;
   cout << "Chi rimane con l'ultimo ha perso." << endl;
   cout << endl << "Hai la prima mossa." << endl << endl;
}

void mossaCane(int* fiammiferi)
{
   int presa = 0;

   while ( presa < 1 || presa > 4 ) {

      cout << "Quanti fiammiferi prendi (da 1 a 4)? " << endl;
      cin >> presa;

      if ( presa < 1 || presa > 4) {
         cout << "Spiacente. Devi prendere almeno un fiammifero, "
                   "ma non più di quattro..." << endl << endl;
      }

   }

   *fiammiferi -= presa;
   cout << "Hai preso " << presa << " fiammiferi." << endl;
   cout << "Ne rimangono " << *fiammiferi << "." << endl;
}

void mossaAnatra(int* fiammiferi)
{
   int presa;

   if ( 1 < *fiammiferi ) {
      presa = *fiammiferi - 1;
   }
   if ( 6 < *fiammiferi ) {
      presa = *fiammiferi - 6;
   }
   if ( 11 < *fiammiferi ) {
      presa = *fiammiferi - 11;
   }
   if ( 16 < *fiammiferi ) {
      presa = *fiammiferi - 16;
   }

   *fiammiferi -= presa;

   cout << "Io prendo " << presa << " fiammiferi." << endl;
   cout << "Adesso ne rimangono " << *fiammiferi << "." << endl
       << endl;
}

Adesso guardiamo insieme i pochi punti che meritano un po’ di attenzione, ma non ci dovrebbero essere difficoltà.

In pratica, dove c’erano i references abbiamo messo i puntatori. La sorpresa è che ci tocca stare più attenti a ciò che facciamo, non meno!

Le dichiarazioni delle funzioni, o per meglio dire i prototipi di funzione (ricordate?), sono cambiati in modo minimo da così:

int mossaCane(int fiammi_cane);
int mossaAnatra(int fiammi_anatra);

a così:

void mossaCane(int *fiammiferi);
void mossaAnatra(int *fiammiferi);

Ossia: i parametri delle due funzioni non sono più references, bensì pointers.


La prima differenza vera la troviamo all’interno di main():

mossaCane(&fiammiferi);

mossaAnatra(&fiammiferi);

Giacché ad attendere i valori c’è un puntatore, e un puntatore può contenere solo indirizzi di variabile, in entrambi i casi dobbiamo spedire l’indirizzo della variabile ‘fiammiferi’.


All’interno delle due funzioni mossaCane() e mossaAnatra() dovremo poi stare molto attenti: tutte le volte che vogliamo accedere al valore contenuto nella variabile originaria, ossia di ‘fiammiferi’ contenuta in main(), dovremo far precedere in nome del puntatore dall’asterisco:

*fiammiferi -= presa;

Infatti il segno ‘*’ dice al compilatore: «non devi agire sul valore del puntatore, ma sul valore della variabile puntata!»

In particolare, non dobbiamo farci ingannare dal fatto che abbiamo dato al puntatore di mossaCane() e mossaAnatra() lo stesso nome (‘fiammiferi’) della variabile contenuta in main()! Per il compilatore sono tre cose diverse (hanno 3 indirizzi di memoria diversi!) e non farà confusione, ma la nostra mente può rimanere disorientata.


Insomma, che conclusione possiamo trarre? Non so voi, ma a me, leggendo il codice (ma anche scrivendolo!), il primo pensiero che viene in mente è: “che pacchia sono i riferimenti! Guarda un po’ quanti asterischi mi tocca mettere, sopra tutto in mossaAnatra()… E poi mi devo ricordare di mettere il simbolo ‘&’ nella chiamata di funzione, in main(), per passare l’indirizzo della variabile e non il valore… Insomma, questi pointers sono proprio una gran cosa, ma quando può bastare un riferimento…”

Questa idea è del tutto personale, è ovvio, e potreste trovare un’infinità di persone che la vedono in maniera opposta a me, ma è anche vero che i riferimenti sono usatissimi.

Possiamo dire che, se il problema è solo accedere a una variabile da una funzione diversa da quella in cui è definita, un reference può bastare.


Ci restano da scoprire le cose meravigliose che un reference non può fare.


Puntare a un puntatore e puntare a un references

Cerchiamo un altro dettaglio nascosto.

Cominciamo da questo programma:

#include <iostream>

using namespace std;

int main()
{
   string testo1 = "Il buongiorno si vede dal mattino";
   string* p_testo1 = &testo1; // puntatore a testo1
   string& r_testo1 = testo1; // riferimento a testo1

   cout << "testo1 = " << testo1 << endl;
   cout << endl;
   cout << "indirizzo di 'testo1' = " << (unsigned long int) &testo1 << endl;
   cout << endl;
   cout << "- - - CONFRONTO TRA I DATI DEL PUNTATORE E QUELLI DEL RIFERIMENTO - - -" << endl;
   cout << endl;
   cout << "valore p_testo1 = " << (unsigned long int) p_testo1 << endl;
   cout << "valore r_testo1 = " << r_testo1 << endl;
   cout << endl;
   cout << "valore variabile puntata da p_testo1 = " << *p_testo1 << endl;
   cout << "valore variabile puntata da r_testo1 = " << r_testo1 << endl;
   cout << endl;
   cout << "indirizzo di p_testo1 = " << (unsigned long int) &p_testo1 << endl;
   cout << "indirizzo di 'r_testo1' = " << (unsigned long int) &r_testo1 << endl << endl;

   return 0;
}

Il cui output è il seguente:


Qt_III_07.png

Come al solito, i vostri numeri saranno diversi.

Cominciamo dalle cose che abbiamo già detto: l’indirizzo di ‘testo1’ e l’indirizzo di ‘r_testo1’ sono uguali. Infatti ‘r_testo1’ è solo un altro nome per ‘testo1’.

Per la stessa ragione, “valore di r_testo1” e “valore della variabile puntata da r_testo1” danno lo stesso numero: infatti non abbiamo mai detto che un reference punta una variabile, bensì che è un alias di quella variabile.

Ancora: se proviamo ad ottenere l’indirizzo di ‘r_testo1’ otteniamo l’indirizzo di ‘tetso1’, esattamente ciò che ci aspetteremmo da un alias.


Il puntatore è invece completamente diverso. Il valore del puntatore, ossia il dato contenuto nello spazio di memoria etichettato ‘p_testo1’, è l’indirizzo di ‘testo1’.

L’indirizzo di ‘p_testo1’ è completamente diverso.

Fin qui ci eravamo già arrivati.


Complichiamoci un po’ la vita aggiungendo qualche riga al programma:

#include <iostream>

using namespace std;

int main()
{
   string testo1 = "Il buongiorno si vede dal mattino";
   string* p_testo1 = &testo1; // puntatore a testo1
   string& r_testo1 = testo1; // riferimento a testo1

   // Introduco un nuovo puntatore.
   // A questo puntatore passo il valore di p_testo1
   // Il valore di p_testo1 è l'indirizzo di testo1
   string* punta_a_punta = p_testo1;

   // Introduco un nuovo riferimento.
   // Associo questo riferimento a r_testo1.
   // r_testo1 è un alias di testo1.
   string& rif_a_rif = r_testo1;

   cout << "testo1 = " << testo1 << endl;

   cout << endl;
   cout << "indirizzo di testo1 = " << (unsigned long int) &testo1 << endl;

   cout << endl;
   cout << "- - - CONFRONTO TRA I DATI DEI PUNTATORE E QUELLI DEI RIFERIMENTI - - -" << endl;

   cout << endl;
   cout << "valore p_testo1 = " << (unsigned long int) p_testo1 << endl;
   cout << "valore punta_a_punta = " << (unsigned long int) punta_a_punta << endl;
   cout << "valore r_testo1 = " << r_testo1 << endl;
   cout << "valore rif_a_rif = " << rif_a_rif << endl;

   cout << endl;
   cout << "valore variabile puntata da p_testo1 = " << *p_testo1 << endl;
   cout << "valore variabile puntata da punta_a_punta = " << *punta_a_punta << endl;
   cout << "valore variabile puntata da r_testo1 = " << r_testo1 << endl;
   cout << "valore variabile puntata da rif_a_rif = " << rif_a_rif << endl;

   cout << endl;
   cout << "indirizzo di p_testo1 = " << (unsigned long int) &p_testo1 << endl;
   cout << "indirizzo di punta_a_punta = " << (unsigned long int) &punta_a_punta << endl;
   cout << "indirizzo di r_testo1 = " << (unsigned long int) &r_testo1 << endl;
   cout << "indirizzo di rif_a_rif = " << (unsigned long int) &rif_a_rif << endl << endl;

   return 0;
}

State cominciando a odiarmi? :)

Coraggio, la cosa è assai meno brutta di quello che sembra. Anche il nuovo puntatore e il nuovo riferimento stanno infatti facendo ciò che ci si aspettava da loro.


Qt_III_08.png

Il nuovo riferimento, ‘rif_a_rif’, pur essendo stato definito a partire da ‘r_testo1’ e non ‘testo1’, è comunque diventato un nuovo nome per la variabile ‘testo1’. Il suo valore è il valore di ‘testo1’ e il suo indirizzo è l’indirizzo di ‘testo1’.


Anche in questo caso il nuovo puntatore conferma invece di essere una variabile separata con un suo indirizzo separato.

Anche il nuovo puntatore punta a ‘testo1’, ma perché gli abbiamo detto:

string* punta_a_punta = p_testo1;

Poiché ‘r_testo1’ contiene indirizzo di ‘testo1’, e l’istruzione precedente copia i valori di una variabile dentro l’alta, adesso in ‘punta_a_punta’ è contenuto l’indirizzo di ‘testo1’.


Domanda: cosa succederebbe se decidessimo di non copiare il valore contenuto in ‘p_testo1’ in ‘punta_a_punta’, bensì di dargli in pasto l’indirizzo di ‘p_testo1’?

In altre parole, a seguente istruzione:

string* punta_a_punta = &p_testo1;

sarà lecita? E dove ci poterà?

Vogliamo provare?

#include <iostream>

using namespace std;

int main()
{
   string testo1 = "Il buongiorno si vede dal mattino";
   string* p_testo1 = &testo1; // puntatore a testo1
   string& r_testo1 = testo1; // riferimento a testo1

   // Introduco un nuovo puntatore.
   // A questo puntatore passo l'indirizzo di p_testo1
   string* punta_a_punta = &p_testo1;

   // Introduco un nuovo riferimento.
   // Associo questo riferimento a r_testo1.
   // r_testo1 è un alias di testo1.
   string& rif_a_rif = r_testo1;

   cout << "testo1 = " << testo1 << endl;

   cout << endl;
   cout << "indirizzo di testo1 = " << (unsigned long int) &testo1 << endl;

   cout << endl;
   cout << "- - - CONFRONTO TRA I DATI DEI PUNTATORE E QUELLI DEI RIFERIMENTI - - -" << endl;

   cout << endl;
   cout << "valore p_testo1 = " << (unsigned long int) p_testo1 << endl;
   cout << "valore punta_a_punta = " << (unsigned long int) punta_a_punta << endl;
   cout << "valore r_testo1 = " << r_testo1 << endl;
   cout << "valore rif_a_rif = " << rif_a_rif << endl;

   cout << endl;
   cout << "valore variabile puntata da p_testo1 = " << *p_testo1 << endl;
   cout << "valore variabile puntata da punta_a_punta = " << *punta_a_punta << endl;
   cout << "valore variabile puntata da r_testo1 = " << r_testo1 << endl;
   cout << "valore variabile puntata da rif_a_rif = " << rif_a_rif << endl;

   cout << endl;
   cout << "indirizzo di p_testo1 = " << (unsigned long int) &p_testo1 << endl;
   cout << "indirizzo di punta_a_punta = " << (unsigned long int) &punta_a_punta << endl;
   cout << "indirizzo di r_testo1 = " << (unsigned long int) &r_testo1 << endl;
   cout << "indirizzo di rif_a_rif = " << (unsigned long int) &rif_a_rif << endl << endl;

   return 0;
}

Il codice è identico a prima; ho solo sostituito queste righe:

// Introduco un nuovo puntatore.
// A questo puntatore passo il valore di p_testo1
// Il valore di p_testo1 è l'indirizzo di testo1
string* punta_a_punta = p_testo1;

con queste:

// Introduco un nuovo puntatore.
// A questo puntatore passo l'indirizzo di p_testo1
string* punta_a_punta = &p_testo1;

Sembrerebbe una cosa innocua, ma… Ahia! Il compilatore si arrabbia e non ci fa proseguire. Il messaggio di errore è inequivocabile.


Qt_III_09.png

A voi forse non dirà niente, ma abbiamo compiuto uno dei gesti che il C++ considera più ignominiosi: abbiamo sbagliato a dichiarare il tipo di una variabile!


Mi spiego meglio. Noi abbiamo cercato di salvare l’indirizzo di un puntatore dentro un altro puntatore. L’operazione che noi vogliamo compiere non è di per sé proibita, ma bisogna dar ragione al compilatore quando ci avverte che non ha alcun senso.

Ragioniamoci su. Cosa ce ne faremmo mai di un indirizzo di memoria se quell’indirizzo non ci servisse a recuperare i dati presenti in quella cella di memoria?

Per meglio dire, quali sono i dati che ci interessano, quelli che abbiamo salvato noi o gli indirizzi di memoria di dove li abbiamo messi? Beh, la risposta è scontata: ci interessano solo i dati su cui dobbiamo lavorare, ossia quelli che abbiamo inserito nella variabile, qualunque sia il tipo della variabile.


Il fatto che poi abbiamo deciso di gestirli tramite un puntatore è solo un problema di comodità. A chi interessa qual è l’indirizzo della variabile puntata? L’unica cosa che ci interessa è sapere che è quello contenuto nel puntatore. La prossima volta che faremo girare il programma, sarà un indirizzo diverso, ma l’istruzione

*p_testo1

ci consegnerà sempre il risultato.

Ma allora… perché mai ci dovremmo preoccupare di salvare l’indirizzo del puntatore da qualche altra parte?


C’è un’unica spiegazione: noi vogliamo usare il nuovo puntatore, ‘punta_a_punta’, per arrivare, in un modo o nell’altro, all’unica cosa che ci può interessare, ossia i dati che abbiamo salvato; in altre parole, a ‘testo1’!

Questo si può fare, certo, ma… Attenzione! Bisogna essere ben chiari quando si ha a che fare con un compilatore C++.

Guai a cercare di prenderlo per scemo. Quello che stiamo cercando di creare non è un puntatore ‘normale’, perché altrimenti non gli passeremmo una cosa inutile come l’indirizzo di un altro puntatore.

Noi stiamo cercando di creare un nuovo tipo di puntatore, il puntatore a puntatore, che sarà autorizzato a portarci, come su un tappeto magico, fin dentro ‘testo1’.

Allora basta dirlo chiaro e il compilatore ce lo lascerà fare:

string* punta_a_punta = &p_testo1;

non compila, ma

string** punta_a_punta = &p_testo1;

sì!

L’unico problema è che, per ottenere il risultato che vogliamo noi, dovremo anche cambiare

cout << "valore variabile puntata da punta_a_punta = " << *punta_a_punta << endl;

in

cout << "valore variabile puntata da punta_a_punta = " << **punta_a_punta << endl;

Provare per credere.

Non sto a ricopiarvi qui il codice, visto che si devono cambiare solo 2 asterischi, ma con le modifiche sopra riportate funziona al 100%.


Adesso non ci resta che farci la stessa domanda sul reference. Sarà possibile ottenere un riferimento di un riferimento? E il puntatore a un riferimento? Non ci resta che provare.

Modificate le righe

// Introduco un nuovo riferimento.
// Associo questo riferimento a r_testo1.
// r_testo1 è un alias di testo1.
string& rif_a_rif = r_testo1;

in

// Introduco un nuovo riferimento.
// Passo a questo riferimento l'indirizzo di r_testo1.
string& rif_a_rif = &r_testo1;

Il compilatore vi bloccherà e non vi farà proseguire.

Anche se provate a cambiare l’ultima riga in:

string&& rif_a_rif = &r_testo1;

non otterrete alcun risultato. Come pure

string&& rif_a_rif = r_testo1;

è proibita.


Non c’è niente da fare, non possiamo cambiare la regola del riferimento: il riferimento, al momento della creazione, deve essere associato a una variabile. Non esistono i references di references.

È una cosa che si può fare con i puntatori, ma non si può fare con i references.


Ma un puntatore a reference si può fare? Certo, perché no? Essendo però il reference un altro nome per una variabile, l’unica cosa che otterremo è un pointer alla variabile originaria. Eccolo qui:

#include <iostream>

using namespace std;

int main()
{
   string testo1 = "Il buongiorno si vede dal mattino";
   string* p_testo1 = &testo1; // puntatore a testo1
   string& r_testo1 = testo1; // riferimento a testo1

   // Introduco un nuovo puntatore.
   // A questo puntatore passo l'indirizzo di p_testo1
   string** punta_a_punta = &p_testo1;

   string& rif_a_rif = r_testo1;

   // Introduco ancora un altro puntatore.
   // Passo a questo puntatore l'indirizzo di rif_a_rif.
   string* punta_a_rif = &rif_a_rif;

   cout << "testo1 = " << testo1 << endl;

   cout << endl;
   cout << "indirizzo di testo1 = " << (unsigned long int) &testo1 << endl;

   cout << endl;
   cout << "- - - CONFRONTO TRA I DATI DEI PUNTATORE E QUELLI DEI RIFERIMENTI - - -" << endl;

   cout << endl;
   cout << "valore p_testo1 = " << (unsigned long int) p_testo1 << endl;
   cout << "valore punta_a_punta = " << (unsigned long int) punta_a_punta << endl;
   cout << "valore punta_a_rif = " << (unsigned long int) punta_a_rif << endl;
   cout << "valore r_testo1 = " << r_testo1 << endl;
   cout << "valore rif_a_rif = " << rif_a_rif << endl;

   cout << endl;
   cout << "valore variabile puntata da p_testo1 = " << *p_testo1 << endl;
   cout << "valore variabile puntata da punta_a_punta = " << **punta_a_punta << endl;
   cout << "valore variabile puntata da punta_a_rif = " << *punta_a_rif << endl;
   cout << "valore variabile puntata da r_testo1 = " << r_testo1 << endl;
   cout << "valore variabile puntata da rif_a_rif = " << rif_a_rif << endl;

   cout << endl;
   cout << "indirizzo di p_testo1 = " << (unsigned long int) &p_testo1 << endl;
   cout << "indirizzo di punta_a_punta = " << (unsigned long int) &punta_a_punta << endl;
   cout << "indirizzo di punta_a_rif = " << (unsigned long int) &punta_a_rif << endl;
   cout << "indirizzo di r_testo1 = " << (unsigned long int) &r_testo1 << endl;
   cout << "indirizzo di rif_a_rif = " << (unsigned long int) &rif_a_rif << endl << endl;

   return 0;
}

Ed ecco il risultato:


Qt_III_10.png

Vi state domandando: «Ma che ce ne faremo mai di un puntatore a puntatore?»

Calma! Li incontriamo oggi per la prima volta e già volete sapere tutto? :-)

I puntatori sono marchingegni complicati e presentano aspetti che ci porterebbero via troppo tempo, se dovessimo dettagliarli tutti, mentre invece noi abbiamo fretta di arrivare agli oggetti e alle librerie Qt.


Allora leviamoci in tre parole anche il prossimo punto.


I puntatori e gli array; nonché l’aritmetica di puntatori

Dovete sapere che a casa ho un cimelio: una copia della seconda edizione di “The C programming language.” di Brian W. Kernighan e Dennis M. Ritchie. Dalla lettura del colophon pare che abbia più di un quarto di secolo. Forse dovrei custodirlo religiosamente in una teca di cristallo :-)


Per essere sicuro di non perdere per strada le cose più importanti, l’ho ripreso in mano e ho consultato il capitolo 5: “Pointers and Arrays”. Beh, mi ci è voluto poco per convincermi che l’argomento, ora come ora, è impossibile da trattare.

Quindi mi dovete credere per fede se vi dico che qualunque cosa vogliate fare con un array, la potete fare con un puntatore – non è però vero il contrario!


C’è una cosa che mi consola: non useremo granché gli array visto che nel C++ ci sono i vector; inoltre le librerie Qt ci libereranno della maggioranza dei problemi. Perciò riusciremo a evitare l’aritmetica dei puntatori ancora per un sacco di tempo.

La mia idea è quindi di rimandare l’argomento a quando davvero sarà necessario.


D’altronde l’aritmetica dei puntatori in altri linguaggi, come ad esempio il Java, non esiste e da alcuni è vista come il fumo negli occhi. Anche noi per ora cercheremo di scansarla.


Puntatori a funzione

Una cosa che potrebbe sorprendervi è scoprire che un puntatore può ‘tenere di mira’, invece che una variabile, una funzione.


Anche su questo argomento ho cercato ispirazione nel buon Kernighan-Ritchie appena citato, e quello che ne ho ottenuto è questo sobrio esempio:

qsort((void **) lineptr, 0, nlines-1,
(int (*)(void*,void*))(numeric ? numcmp : strcmp));

Non ditemi che non è chiaro :)

Ok, diciamo che l’argomento non è ‘soffice’, ma vedrete che riusciremo ad ammorbidirlo.

Intanto mettiamoci d’accordo sul come funziona.


Cosa avevamo detto che era un funzione? In buona sostanza è un blocco di codice con un nome. Quando si usa quel nome, il compilatore si precipita a eseguire il codice contenuto nella funzione. Se vogliamo, possiamo dotare la nostra funzione di parametri e passargli degli argomenti. Sempre se vogliamo, possiamo chiedere che ci restituisca un valore.

Per poter eseguire una funzione, il compilatore deve prima caricarla in memoria. Se si tiene in mente questo, si capisce che alla fin fine una funzione occupa uno spazio di memoria né più né meno che una variabile.

Se la memoria del computer fosse un oggetto fisico e potessimo posarlo sul tavolo davanti a noi, qualcuno potrebbe chiederci: «Mi fai vedere la differenza tra una variabile e una funzione?»

E noi, per assurdo, avremmo molte difficoltà a rispondergli. Infatti potremmo trovarci davanti molti spazi di memoria contrassegnati da etichette e alcuni di questi potrebbero essere variabili e altri funzioni. Tirando a indovinare, tra gli spazi di memoria etichettati, quelli più piccoli saranno variabili e quelli più grandi funzioni. Ma è solo un tirare a indovinare!


È vero che una variabile è uno spazio di memoria riservato a noi, in cui possiamo intervenire a nostro piacimento, mentre una funzione è caricata in uno spazio di memoria che il compilatore si gestisce come vuole lui; però alla fine sono spazi di memoria con dei nomi.

Pertanto, come posso ‘puntare’ uno spazio di memoria etichettato che contiene dei dati, così posso ‘puntare’ uno spazio di memoria che contiene delle istruzioni.

Come poi funzioni all’atto pratico non è un problema mio: ci pensa il compilatore. L’unica cosa che io devo fare è dire al compilatore che il puntatore che vado a dichiarare deve puntare a una funzione. Fine. Tutte le questioni tecniche, poi, se le sbrigherà lui.


Adesso vediamo come si fa a dichiarare un puntatore a funzione.

E qui ci troviamo nei guai. E sì, perché un puntatore è, si è già detto, una normale variabile, solo che il suo contenuto è interpretato come un indirizzo di memoria. Essendo una normale variabile, può essere usato per restituire un valore alla funzione chiamante. Ossia, all’interno di una funzione posso scrivere:

string *miafunzione()
{
   string* testo = “Testo da restituire”;

   // un sacco di istruzioni
   …

   return testo;  // restituisco un puntatore
}

Anzi, questo sarà un altro punto che dovremo trattare sui puntatori. Ma quel che ora mi preme sottolineare è come viene dichiarata una funzione che restituisce un puntatore.

Se la funzione restituisce un puntatore a string, la sintassi sarà:

string *nomefunzione()

In effetti non cambia niente rispetto alla dichiarazione ‘normale’. Se una funzione deve restituire un int, scriviamo int nomefunzione(), se deve restituire un puntatore a int, ossia un int*, allora scriveremo int *nomefunzione().

In realtà non facciamo altro che rispettare la regola: scriviamo il tipo di dato che la funzione restituisce prima del nome di funzione. Badate bene che lo spazio non ha alcuna rilevanza; in altre parole, le tre espressioni:

int* nomefunzione() // spazio tra asterisco e nomefunzione
int *nomefunzione() // spazio tra int e asterisco
int * nomefunzione() // spazio prima e dopo l’asterisco

per il compilatore sono identiche! L’abitudine a mettere l’asterisco attaccato al nome di funzione serve solo a migliorare la leggibilità del codice per quei poveri tontoloni degli esseri umani, ma non ha alcun effetto sul compilatore.

A questo punto ci dobbiamo chiedere: «Se metto un asterisco prima del nome di funzione, allora dico al compilatore che la funzione restituisce un puntatore. Se è così, come faccio a dire al compilatore che voglio un puntatore a funzione?»


Se non è chiaro qual è il problema, diamo un’occhiata al codice che segue:

// La seguente funzione non fa nulla di utile e
// in un programma vero non si troverebbe mai.
// Serve solo per far capire il problema.
int* miafunzione()
{
   int miavariabile = 78; // numero a caso
   int* miopunt = &miavariabile;
   return miopunt;
}

int main()
{
   int var1 = 5;

   // Per creare un puntatore alla variabile precedente
   // devo mettere asterisco dopo la dichiarazione di tipo.
   int* punt1 = &var1;

   // Adesso voglio creare un puntatore che punti 
   // alla funzione miafunzione()
   // miafunzione() restituisce un int, perciò
   // ipotizzo di dover dichiarare un puntatore a int
   // Dove piazzerò l’asterisco?

   // La seguente definizione è sbagliata:
   int* punt2 = &miafunzione();
}

Per il compilatore del C++ quello che ho scritto sopra è inaccettabile.

Quando definisco un puntatore il compilatore vuole che specifichi con precisione a quale tipo di dati dovrà puntare. È per questo che specifichiamo int* o string* prima di indicare il nome che vogliamo dare al puntatore. Se un puntatore è dichiarato int*, si aspetta di tenere di mira uno spazio di memoria di un certo numero di byte (sul mio computer, 4); questo significa che ignorerà il byte successivo (nel mio caso, il 5°).

In altre parole sul mio computer le istruzioni:

int var1 = 5;
int* punt1 = &var1;

creano una variabile che occupa 4 byte e un puntatore che tiene d’occhio quella variabile. Io non potrò mai sapere cose c’è nella cella di memoria successiva perché, sia che io passi al compilatore il nome della variabile, sia che gli passi il nome del puntatore preceduto da asterisco, per il mio compilatore un int è composto da 4 byte e non mi farà accedere al 5°.

Adesso rileggiamo la mia ultima istruzione:

int* punt2 = &miafunzione();

Con questa riga io pretenderei dal compilatore che creasse un puntatore, di nome ‘punt2’, che puntasse a uno spazio di memoria equivalente a un int, ossia, sul mio computer, a 4 byte. Ma poi gli assegno lo spazio di memoria occupato da un’intera funzione!

Ma poco fa abbiamo detto che lo spazio di memoria occupato da una funzione di solito è più grande di quello occupato da una variabile; in questo caso possiamo darlo per garantito: non credo proprio che possa esistere una funzione che, una volta caricata in memoria, possa occupare solo 4 byte!


E infatti il compilatore non accetterà mai questa mia pretesa. La mia dichiarazione di tipo è sbagliata: non devo chiedere al compilatore di puntare a uno spazio di memoria di tipo int, bensì di puntare a uno spazio di memoria di tipo “funzione che non ha parametri, ma restituisce un intero’:

int* punt2();

Cominciate a inquadrare il problema? Se osservate bene la riga qui sopra vi accorgerete subito che non può essere corretta.

Infatti se nel mio codice si trovasse una funzione che si chiama punt2(), non ha parametri e ritorna un puntatore a intero, la sua dichiarazione sarebbe per l’appunto:

int* punt2()

E ciò implica che, all’inizio del codice, fuori da ogni funzione, sarebbe riportato il prototipo di quella funzione, ossia una copia della sua dichiarazione, che sarebbe sempre:

int* punt2();

Mettiamo insieme i pezzi: cos’altro si trova dichiarato fuori da ogni funzione? Proprio loro: le variabili globali. Ciò significa che ciò che viene dichiarato fuori da ogni funzione ha visibilità globale, ossia viene visto da ogni parte del codice.

Quindi mi troverei un’istruzione, all’interno di una funzione, che è lettera per lettera identica a un prototipo di funzione che appare nello spazio di visibilità globale.


Ma povero compilatore! Non posso certo prendere che gestisca una situazione come questa; in fondo è solo un software. Tocca a me, bipede alla tastiera, risolvere il problema.


E infatti la soluzione esiste e non è nemmeno complicata, ma devo ammettere che non è per niente bella: bisogna mettere l’asterisco e il nome di funzione tra parentesi. Ossia: (*nomepuntatore)().

Ecco infine la dichiarazione corretta:

// Puntatore a funzione senza parametri che restituisce un int
int (*punt2)();

// Assegnazione di una funzione al puntatore precedente:
punt2 = &miafunzione();

// Oppure le due istruzione precedenti assemblate 
// in un’unica riga:
int (*punt2)() = &miafunzione();

Di conseguenza:

// funzione che restituisce un puntatore a int
int *nomefunzione()

// puntatore a funzione che restituisce un int
int (*nomepuntatore)() = …

// puntatore a funzione che restituisce un puntatore a int
int* (*nomepuntatore)() = …

Sì, lo ammetto, è proprio sgradevole. Potevano inventare un sistema migliore.

L’importante per ora non è fare uso di questa sintassi, ma solo sapere che esiste e capire cosa significhi.


Adesso che abbiamo capito come funziona e come si fa a dire al compilatore che vogliamo usare questa strana entità che è il puntatore a funzione, la domanda a cui dobbiamo dare una risposta è: «Che cosa ce ne facciamo di un puntatore a funzione?»


Qui si entra un poco nel complicato. I puntatori a funzione hanno una grandissima utilità, ma nel C++ meno che nel C. Infatti nel C++ possono essere sostituiti da cose come il polimorfismo e i functor che non esistono nel C.

Potrei anche tirare fuori dal cilindro qualche spezzone di codice di tipo accademico, ma varrà la pena di discuterne visto che non sappiamo bene quando li useremo?

Forse per ora ci può bastare questa risposta, sulla loro utilità: tramite un puntatore a funzione (function pointer) possiamo invocare una funzione anche senza sapere il suo nome: basta che sia stata in precedenza assegnata a quel puntatore.

In pratica, come si è detto tante volte, può capitare che questa operazione (di assegnazione) venga compiuta in un’altra parte del codice, che magari non abbiamo scritto noi, ma qualcun altro. Noi ci possiamo disinteressare di cosa ci sia scritto in questa parte del codice: l’unica cosa che ci interessa sapere è che al puntatore ‘x’ è stata assegnata la funzione giusta, quella che noi dobbiamo usare, e a noi ci basta invocarla tramite questo puntatore.


Anche nel JavaScript si può assegnare una funzione a una variabile e poi usare la variabile per invocare la funzione.


Ci sarà occasione di riprendere l’argomento, ma ora salutiamo i puntatori dando un’occhiata ai loro limiti.

I limiti dei puntatori

Abbiamo visto che non è possibile passare un numero a un reference (v. sopra, I limiti dei references).

Si potrà passarlo a un puntatore?

Si accettano scommesse prima di provare:

#include <iostream>

using namespace std;

void miafunz(int* punt);

int main()
{
   miafunz(5);

   return 0;
}

void miafunz(int* punt)
{
   cout << "Valore di punt = " << *punt << endl;
}

Ahia! Spiacente. Anche in questo caso il compilatore si oppone. Il parametro ‘punt’ è stato dichiarato puntatore a int e non è disposto ad accettare un ‘semplice’ int: vuole un indirizzo di memoria.

Domandona: «Ehi! Ma come fa lui a sapere che non è un indirizzo di memoria? È comunque un numero intero. Non potrebbe essere una cella di memoria che ha un indirizzo molto basso? 5, per l’appunto? E se gli passassi un numero molto alto, 3.220.819.138 per esempio, l’accetterebbe?»

Risposta: no! Non è così facile gabbare il compilatore. Comunque provate pure, se vi va: tentar non nuoce ;-)


Il problema è che il tipo non corrisponde; la dichiarazione di tipo di dato, intendo dire. Anche se gli passaste una variabile di tipo int non la accetterebbe.

#include <iostream>

using namespace std;

void miafunz(int* punt);

int main()
{
   int var1 = 5;
   miafunz(var1);

   return 0;
}

void miafunz(int* punt)
{
   cout << "Valore di punt = " << *punt << endl;
}

Anche il codice qui sopra è respinto dal compilatore e per lo stesso motivo.

Se il parametro della funzione è int*, dobbiamo per forza passare un int* come abbiamo fatto in mossaCane() e mossaAnatra():

#include <iostream>

using namespace std;

void miafunz(int* punt);

int main()
{
   int var1 = 5;
   int* punt1 = &var1;
   miafunz(punt1);

   return 0;
}

void miafunz(int* punt)
{
   cout << "Valore di punt = " << *punt << endl;
}

Finalmente compila.


Stessa domanda da un altro punto di vista: nell’esempio precedente, se in main() provassimo ad assegnare direttamente un intero al puntatore, lo prenderebbe?

Ossia se cambiassimo da

int* punt1 = &var1;

a

int* punt1 = 5;

inganneremmo il compilatore?

Non credo proprio: neanche qui il tipo di dato corrisponde, giacché 5 è un int non un int*. Ossia, non è un indirizzo di memoria!

Anche in questo caso non c’è niente da fare: la regola dice che un puntatore è una variabile che contiene un indirizzo di memoria. Il compilatore lo sa se gli state chiedendo di copiare un indirizzo di memoria o no: è lui che li gestisce!


Anche in questo caso, come in quello dei references, parrebbe che esistesse un limite dei puntatori legato al fatto se esista o no una connessione con un’altra variabile.

Nel caso dei riferimenti, però, il limite è assoluto: il riferimento può nascere solo se viene associato seduta stante con una variabile. Nel caso dei pointer sembrerebbe un limite di fatto, ma non di sintassi: il pointer può essere dichiarato, ma non avrebbe alcun utilizzo pratico finché non gli venisse passato un indirizzo di memoria a cui puntare, che sia quello di una variabile (normale, reference o pointer) o di una funzione – o di un oggetto.


Il seguente codice, però, sembra smentire questa ipotesi:

#include <iostream>

using namespace std;

int main()
{
   int* punt1;

   cout << "Se 'punt1' non è stato connesso con nessuna variabile, "
            "vuol dire che è vuoto?" << endl;

   cout << "Possiamo dare un'occhiata a cosa c'è dentro al puntatore?" << endl;

   cout << "Certo che sì! Basta *non* usare l'asterisco." << endl << endl;

   cout << "Contenuto di 'punt1' = " << (unsigned long int) punt1 << endl << endl;

   return 0;
}

Il programma ci presenta il seguente risultato:


Qt_III_11.png

Ora andiamo subito alle cose importanti, ma vorrei soffermarmi un attimo sulla prima istruzione cout:

cout << "Se 'punt1' non è stato connesso con nessuna variabile, "
            "vuol dire che è vuoto?" << endl;

Avrete notato che per rendere il codice più leggibile cerco di evitare righe troppo lunghe. A dire il vero di solito si usa farle ancora più corte, ma non ci sono regole ferree in proposito.

In questo caso, una riga come:

cout << "Se 'punt1' non è stato connesso con nessuna variabile, vuol dire che è vuoto?" << endl;

sarebbe andata ben oltre la normale lunghezza delle righe in C++.

Il problema è che tornare a capo all’interno delle frasi chiuse fra virgolette non è consentito. Però il C, e con lui il C++, offre una soluzione semplice al problema: basta chiudere le virgolette e riaprirle subito dopo. Il compilatore capisce che volevate andare a capo e provvede, quando compila il codice, a riattaccare i pezzi della frase fra di loro. È una cosa talmente semplice e comoda che dopo le prime volte viene automatica.

Quindi a schermo la frase che nel codice ho spezzato in due parti ridiventa unita.


Adesso torniamo al nostro puntatore.

In effetti parrebbe proprio già… attivo. Non contiene già un indirizzo di memoria? Allora perché non dovremmo riuscire ad assegnargli un valore? Potrebbe metterlo lì, no?

E infatti è così:

#include <iostream>

using namespace std;

int main()
{
   int* punt1;
   *punt1 = 5;

   cout << "Contenuto di 'punt1' == " << (unsigned long int) punt1 << endl << endl;
   cout << "Contenuto dello spazio di memoria puntato da 'punti1' == "
         << *punt1 << endl << endl;

return 0;

}

Pare che siamo riusciti a ingannare il compilatore!

(Tenete presente la sintassi: per accedere al valore dello spazio di memoria tenuto di mira dal puntatore bisogna usare l’asterisco. Ve lo ricordavate?)


Il programma ci risponde, in apparenza senza difficoltà:


Qt_III_12.png

Quindi sembra filare tutto liscio. Salvo, forse, fare caso a una finestrella che si è aperta in Qt sotto il nostro codice e che pone in bella vista un brutto cartello di pericolo:


Qt_III_13.png

Il nostro amico compilatore sta cercando di aiutarci a non commettere sciocchezze e forse dovremmo ascoltarlo.


Cerchiamo di capire dove sta il problema. Il seguente codice, secondo voi, che risultato darà a schermo?

#include <iostream>

using namespace std;

int main()
{
   int var1;

   cout << "Contenuto di 'var1' == " << var1 << endl << endl;

   return 0;
}

Chi scommette che il risultato sarà: Contenuto di 'var1' == 0 ?

Nessuno? Tutti?

Andiamo a vedere?


Qt_III_14.png

Wow! A me dà un numero negativo…

A voi? Anche a voi un numero strano e inaspettato?


Scusate la domanda, ma… perché inaspettato?

Qualcuno di noi ha forse detto al compilatore di portare il valore della variabile a 0? Direi di no. E quindi perché avrebbe dovuto farlo?

Vi ricordate cosa abbiamo detto a preposito della durata delle variabili? Quando una funzione è terminata, il compilatore libera la memoria e la rende disponibile per altre funzioni che debbano essere invocate. Questo significa anche l’opposto: la nostra funzione agisce in una scatola di memoria che, fino a pochi istanti prima, era occupata da altre istruzioni, altre variabili e altri dati.

Con un istruzione del tipo:

int miavariabile;

noi chiediamo al compilatore di assegnarci alcuni byte di memoria e attaccarci un’etichetta sopra. Punto.

Questo significa che, in quei byte, quel che c’era c’è rimasto: ossia ci sono dati e valori che derivano dal codice che vi era in funzione istanti prima.


Quel che c’è in una variabile al momento della sua creazione è imprevedibile. In gergo si dice che il suo contenuto è spazzatura.


Questo vale per tutte le variabili (che non siano oggetti!), quindi pure per i puntatori. Quello che noi vogliamo considerare come un indirizzo a uno spazio di memoria in realtà è un numero del tutto casuale, che consiste solo nell’avanzo di ciò che c’era in quelle celle pochi istanti prima.


È questo che sta cercando di comunicarci il compilatore:

'punt1' is used uninitialized in this function

significa: «Ehi, tonto! Ti sei dimenticato di inizializzare la variabile prima di usarla.»

Inizializzare la variabile vuol dire assegnarle un valore, inserisci un dato. In altre parole: «il valore contenuto nella variabile che stai usando è un insieme casuale di byte. Stai andando avanti con della spazzatura.»


Badate bene: questo non è proibito! Non c’è una regola del C o del C++ che dice che non lo possiamo fare. Semplicemente è a nostro rischio e pericolo. Diciamo che in generale è una cosa molto sciocca.

Noi eviteremo di prendere questi rimproveri dal compilatore, quindi assegneremo sempre in modo esplicito uno spazio di memoria al puntatore prima di usarlo.


Una curiosità: prima ho scritto che la regola vale per ‘tutte’ le variabili. Beh, non è del tutto vero: le variabili globali vengono inizializzate a zero dal compilatore.


A questo punto possiamo correggere la nostra affermazione di prima: non esiste alcun limite per i puntatori in merito all’essere stati connessi o no a una variabile. Esiste senz’altro per i reference. Un puntatore può essere usato, sia da un punto di vista sintattico che ‘logico’, senza che gli sia stato assegnato in modo esplicito un indirizzo di memoria da tenere di mira.

Però all’atto pratico questa operazione è rischiosa e pressoché priva di senso – sopra tutto se si considera quanto poco ci vuole a creare una variabile e a connetterla al puntatore. In altre parole, usare un puntatore senza avergli assegnato un indirizzo di memoria da puntare è in teoria lecito, ma non c’è nessuna garanzia che il codice funzioni perché ciò che è contenuto nel puntatore è spazzatura.


Quando definiamo una variabile, il compilatore ha cura di crearla nella zona di memoria più idonea, una zona che poi non toccherà perché saprà che lì ci sono i nostri dati. Ma se usiamo il valore spazzatura contenuto in un puntatore alla sua nascita, beh, stiamo facendo riferimento a uno spazio di memoria che può trovarsi ovunque. Questo significa che non potremo mai sapere se e quando sarà impegnato da qualche altra funzione. I nostri dati potrebbero essere modificati senza preavviso oppure potremmo stare alterando dati in uso da altre parti del programma; o addirittura riscrivendo parti di codice caricate in memoria. Non solo, ma non c’è nessuna garanzia di non andare a toccare un’area di memoria proibita, cosa che costringerebbe il sistema operativo a chiudere forzatamente il nostro programma.

Insomma, fatta salva la teoria, nella pratica un puntatore va inizializzato con un indirizzo valido prima di poterlo usare.


Inizializzazione degli oggetti

Quel che si è detto poco sopra sul contenuto delle variabili appena definite non vale sempre: a differenza delle variabili gli oggetti possono nascere in uno stato coerente, nel senso che a seconda di come sono stati concepiti si può prevedere il valore delle loro proprietà fin da subito.

In poche parole, un oggetto concepito con cura, quando nasce, nasce sempre allo stesso modo: non contiene codice spazzatura.

Per ora abbiamo incontrato solo due oggetti, vector e string, ma si tratta già di due oggetti ‘di qualità’; questo vuol dire che tutti i vector e tutte le string che incontreremo nasceranno identici fra di loro.

Proviamo subito:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
   string miastringa; // non assegno alcun valore
   vector<int> miovector; // non assegno alcun valore

   if ( miastringa.empty() == true ) {
      cout << "miastringa è vuota!" << endl;
   }
   else {
      cout << "ERRORE! miastringa non è vuota alla nascita!!" << endl;
   }

   if ( miovector.empty() == true ) {
      cout << "miovector è vuoto!" << endl;
   }
   else {
      cout << "ERRORE! miovector non è vuoto alla nascita!!" << endl;
   }

}

empty() è un metodo, cioè una funzione, che esiste sia per string che per vector. In entrambi i casi compie la stessa operazione: ci informa se l’oggetto è vuoto oppure no. Se è vuoto restituisce ‘true’, se invece non è vuoto risponde ‘false’.

‘true’ e ‘false’ sono anche i valori su cui si basano istruzioni come if e while, il che vuol dire che le due istruzioni

if ( miastringa.empty() == true ) {
if ( miovector.empty() == true ) {

avrebbero potuto essere scritte:

if ( miastringa.empty() ) {
if ( miovector.empty() ) {

e avrebbero funzionato esattamente nello stesso modo.

In altre parole, se ‘miastringa’ è vuota, dopo il confronto “miastringa.empty() == true” nell’istruzione if è contenuto il valore true, che starebbe a significare “confronto ok”.

Se però non facciamo il confronto, dentro l’istruzione if rimarrà il valore restituito dal metodo empty(), che, se ‘miastringa’ è vuota, sarà sempre ‘true’.

Perciò in questo caso il confronto è superfluo. Questa comodità spiega perché vengano scritte tante funzioni che restituiscono solo ‘true’ o ‘false’.


Se si prova ad aggiungere altri vector o string al programma e si testano con il metodo empty(), si scoprirà che la situazione non cambia mai: tutti i vector e tutte le string nascono allo stesso modo.

Gli oggetti che faremo noi saranno invece un po’ meno raffinati :)


Il dubbio che può venire ad alcuni è se questa verità vale anche per i puntatori agli oggetti.

Secondo voi, se scrivo

string *miastringa;

potrò poi applicarci il metodo empty() e scoprire che è vuota?

Risposta: ovviamente no, ma se fate un tentativo scoprite cosa vi risponde il compilatore.

Come prima cosa, è giunto il momento di svelarvi due segreti: il primo è che i puntatori agli oggetti sono usatissimi e se ci pensate un attimo, abbiamo già spiegato come mai.

Tutte le volte che volessimo passare un nostro oggetto come argomento a una funzione, dovremmo chiedere al compilatore di fare la copia di chissà quante celle di memoria. Se invece usiamo un puntatore, tutto ciò che facciamo è copiare un singolo numero intero: il massimo dell’efficienza.


Il secondo segreto è che, se un oggetto non è dichiarato come variabile, bensì come puntatore, allora non dobbiamo usare il punto per invocare i metodi, bensì il buffo segno ‘->’.

In altre parole, la sintassi sarebbe la seguente:

string *miastringa;
if ( miastringa->empty() ) {

Quanto al resto, non cambia nulla, ma questo bisogna ricordarcelo: se è una variabile, allora i metodi si ‘attaccano’ con un punto; se è un puntatore, allora si ‘attaccano’ con un ‘->’.


Detto questo, la regola del puntatore non cambia: il puntatore è solo una variabile che contiene valori interi che vengono considerati indirizzi di memoria.

Il corollario di questa affermazione è che dopo che si è scritto:

string *miastringa;

l’unica cosa che avremo dichiarato sarà una variabile che contiene valori interi che saranno interpretati come indirizzi di memoria.

Sembra che stia giocando con le parole, ma non è così: il significato del discorso è: l’oggetto ancora non esiste.

Noi non abbiamo detto al compilatore di creare un oggetto di tipo string. Abbiamo detto che volevamo un puntatore, e questo è ciò che ci sta dando. Il puntatore non sta puntando a uno spazio di memoria dove è contenuto un oggetto string.: sta puntando uno spazio di memoria dove c’è spazzatura.

Quando invece scriviamo

string miastringa;

il compilatore prende un’area di memoria e la riserva per i nostri dati di tipo string. Dopo questa operazione si è soliti dire che l’oggetto esiste, perché occupa una quantità di celle di memoria misurabile, anche se ancora non ci abbiamo inserito alcun valore. Nel nostro caso invece l’oggetto non è stato creato in memoria, quindi non esiste. Non essendoci un oggetto, non siamo autorizzati a invocarne i metodi.


Un'alternativa consisterebbe nell’utilizzo della parolina magica new, di cui però andremo a parlare più avanti.


Il discorso sui puntatori potrebbe proseguire per chissà quante pagine, ma le cose più importanti le abbiamo accennate, perciò per ora lo chiudiamo qui.


L’ultima cosa che ci rimane è un parolone.

L’operazione di andare a consultare cos’è contenuto in una variabile tramite un puntatore a essa associata si chiama dereferenziazione.


Leviamoci un ultimo piccolo ostacolo prima di arrivare agli oggetti, così dopo non ci distrae più niente.

Stavolta non è nulla di difficile né di concettoso, perciò ce la sbrigheremo in fretta. Si tratta di una ‘raffinatezza’ del C++, che non c’è in tutti i linguaggi, e può lasciare un po’ interdetti la prima volta che la si incontra.


Il mascheramento delle variabili

Sappiamo che esistono due spazi di visibilità (scope): una visibilità globale e una locale.

Quello che non ci siamo mai domandati è se questi spazi possano interferire fra di loro.


Supponiamo di avere una variabile globale di questo tipo:

string testo = "Sono una variabile GLOBALE";

Andando avanti con il nostro programma, ci scordiamo di aver creato quella variabile e ricicliamo lo stesso nome:

string testo = "Sono una variabile LOCALE";

Secondo voi come reagirà il compilatore? Sceglierà una delle due a suo piacimento? Ci impedirà di compilare il programma? Ci darà un avviso come ha fatto per il puntatore non inizializzato?


Nessuna delle tre. Intanto proviamo:

#include <iostream>

using namespace std;

string testo = "Sono una variabile GLOBALE";

int main()
{
   // Pericolo! 'testo' esiste già
   string testo = "Sono una variabile LOCALE";

   cout << testo << endl << endl;

   return 0;
}

L’output è il seguente:


Qt_III_15.png

Il compilatore ha scelto la seconda variabile, quella locale.

Adesso, visto che ci piace fare esperimenti, insistiamo sulla stessa strada:

#include <iostream>

using namespace std;

string testo = "Sono una variabile GLOBALE";

int main()
{
   string testo = "Sono una variabile LOCALE";

   if (true) {
      string testo = "Sono una variabile in un BLOCCO IF";
      cout << testo << endl << endl;
   }

   return 0;
}

Stavolta che risultato vi aspettate di trovare?


Qt_III_16.png

Beh, sembra proprio che il nostro compilatore abbia le sue simpatie: sceglie sempre la variabile più ‘locale’, ossia quella con lo spazio di visibilità più ristretto.


Se la cosa vi sembra poco chiara, ripensiamo a ciò che abbiamo visto fin qui.

  1. Una blocco di codice nel C++ è, come regola generale, un insieme di istruzioni che è contenuto fra parentesi graffe.
  2. Una funzione è solo uno dei tanti tipi di blocchi di codice conosciuti.
    Altri tipi di blocchi di codice possono essere le istruzioni if, i cicli for e while, nonché altri che non abbiamo ancora visto.
  3. I blocchi di codice possono essere contenuti uno dentro l’altro.
  4. Lo scope di un blocco di codice finisce con la parentesi graffa chiusa.
  5. Le variabili possono essere locali ai blocchi di codice, siano essi funzioni o altro (va bene, fino a qui non avevamo mai usato una variabile all’interno di un blocco if, lo ammetto).
  6. Le variabili globali sono pericolose perché possono essere modificate in qualsiasi punto del programma.

Se mettiamo insieme quanto sopra, possiamo concludere che è ragionevole che il compilatore preferisca usare le variabili locali, e che anzi, più locali sono, e più le preferisca.

Perché mai dovrebbe partire dal presupposto che andremmo a complicarci la vita con quelle antipatiche delle variabili globali quando abbiamo a disposizione delle simpatiche variabili locali?


Questa operazione, di prevalere sul nome globale, si chiama mascheramento; ossia si dice che i nomi delle variabili locali mascherano (nascondono, oscurano, rendono inaccessibili) i nomi delle variabili globali. In inglese si parla di shadowing o hiding.


Questa è la regola generale, dopodiché ci sono una sacco di dettagli da ricordare:


Variabili definite fra le parentesi tonde prima di un blocco di codice

La regola generale è:

è come se fossero state definite subito dopo la parentesi graffa con cui inizia il blocco.


Il caso del parametro:

Si è già detto che è identico a una variabile dichiarata all’inizio di una funzione, subito dopo la parentesi graffa aperta.

Questa similitudine vale anche per la sua visibilità, che quindi vale per tutta la funzione, fino alla parentesi graffa chiusa. Pertanto il seguente codice è sbagliato:

void unafunzione(int miopar)
{
   int miopar; // errore! 'miopar' esiste già nel medesimo spazio di visibilità
   …
}

Infatti il precedente codice è del tutto identico a questo (fatta eccezione per il fatto che ‘miopar’ riceve l’argomento inviato alla funzione):

void unafunzione()
{
   int miopar;
   int miopar; // errore! 'miopar' esiste già.
   …
}

Invece il seguente codice sarebbe corretto:

void unafunzione(int miopar)
{
   if ( miopar == 3 ) {
      int miopar = 5;
   …
}
…

Tanto per verificare, l’output di questo programma:

#include <iostream>

using namespace std;

void unafunzione(int miopar);

int main()
{
   // I numeri usati in questo programma sono stati scelti
   // a caso e non hanno nessun significato particolare

   unafunzione(2);
   unafunzione(3);
}

void unafunzione(int miopar)
{
   if ( miopar == 3 ) {
      int miopar = 5;
      cout << "miopar del blocco if == " << miopar << endl;
   }

   cout << "miopar globale == " << miopar << endl << endl;

}

è questo:


Qt_III_17.png

Il caso if, for, while e simili

Se si crea una variabile fra le parentesi tonde di uno di questi blocchi, essa sarà visibile fino alla parentesi graffa alla fine del blocco.

Il caso for è un classico:

#include <iostream>

using namespace std;

int main()
{
   int numero = 0;

   for ( int numero = 1; numero < 6; numero++) {
      cout << "numero di for == " << numero << endl;
   }

   cout << "numero di main() == " << numero << endl << endl;
}


Qt_III_18.png


Nel caso si voglia, fortissimamente voglia usare la variabile globale mascherata

È possibile anche questo. Il C++ è davvero un linguaggio curato.

Esiste un simbolo apposta, che si chiama operatore di scope resolution, che può essere usato anche a questo scopo (di solito è usato in altre occasioni). Il simbolo è il seguente : ‘::’.

Sì, sono due doppi punti uno di seguito all’altro. Va scritto subito prima del nome della variabile.

#include <iostream>

using namespace std;

string testo = "Sono una variabile GLOBALE";

int main()
{
   string testo = "Sono una variabile LOCALE";

   if (true) {
      string testo = "Sono una variabile in un BLOCCO IF";
      cout << "Sorpresa! Posso usare la variabile globale da qui :)" << endl;
      cout << "testo == " << ::testo << endl << endl;
   }

}


Qt_III_19.png

Attenzione! Il simbolo ‘::’ non può essere usato per… “tornare su di un livello”. Lui va a cercare la variabile globale, se esiste, non quella del blocco più esterno (cosa confermata dall’output precedente).


Non c’è un modo per utilizzare una variabile locale che sia stata mascherata finché non si esce dal blocco in cui esiste la variabile che maschera.

Un esempio… perverso :)

Domanda: qual è il punto in cui inizia il mascheramento? Quando si apre il blocco di codice dov’è contenuta la variabile che maschera, oppure quando si incontra la definizione della variabile che maschera?


Beh, per rispondere questo dubbio amletico riporto qui un esempio del sig. Stroupstrup in persona, tratto dalla versione italiana del suo libro The C++ Programming Language – Second Edition. Anche il codice esemplificativo è suo, nonché il commento.

Un nome risulta visibile a partire dal punto di dichiarazione […]; ciò significa che un nome può essere usato anche per specificare il suo valore iniziale. Ad esempio:
int x;

void f3()
{
   int x = x; // perverse
}


A questo punto non vi resta che inventare un codice per mettere alla prova questa affermazione dell’inventore del C++. Con questo esercizio lasciamo l’argomento perché gli oggetti ci reclamano a gran voce.

Alla prossima puntata!