La regione di memoria libera, e che quindi il programmatore puņ richiedere dinamicamente per memorizzare variabili create in fase di esecuzione, in C è chiamata l'heap. La stessa regione di memoria disponibile, In C++, è denominata memoria libera. La differenza risiede nelle funzioni che si usano per accedere a questa zona di memoria.
Per richiedere memoria dall'heap, in C, si usa la funzione malloc. Per esempio si può allocare dinamicamente una struttura Data, nel modo seguente:
struct Data *pData;
pData = (struct Data *)malloc( sizeof( struct Data ) );
La funzione malloc assegna un blocco di memoria, sufficientemente grande, per contenere una struttura Data e assegna il puntatore a questa area alla variabile pData.
La funzione malloc restituisce un puntatore void che, quindi, deve essere convertito all'appropriato tipo quando lo si assegna a pData. A questo punto quel blocco di memoria può essere trattato come una struttura di tipo Data
In C++, la funzione malloc non è adatta per riservare dinamicamente uno spazio ad un'istanza di una classe, perchè quando si crea un'istanza di una classe si deve anche richiamare il costruttore della classe. La funzione malloc riserva lo spazio e restituisce il puntatore a quell'area di memoria, ma non inizializza i campi dell'oggetto.È vero che poi si potrebbero inizializzare i campi membro richiamando le appropriate funzioni di accesso, ma i risultati potrebbero essere errati, per esempio:
Data *pData;
int i;
pData = (Data *)malloc( sizeof( Data ) );
i = pData->leggiMese(); // restituisce un valore indefinito
L'uso di malloc per creare dinamicamente oggetti fa perdere i vantaggi offerti dal costruttore.
l'operatore new
Il C++, in sostituzione di malloc fornisce l'operatore new per riservare spazio nella memoria libera.
La funzione malloc non riconosce il tipo della variabile che deve creare: la sua sintassi richiede un parametro
che rappresenta il numero di byte da riservare e restituisce il puntatore a quello spazio. L'operatore new,
invece, riconosce la classe dell'oggetto e chiama automaticamente il costruttore per assegnare anche i valori iniziali
ai campi dell'oggetto. Confrontare l'esempio precedente con il seguente:
Data *primoPtr, *secondoPtr;
int i;
primoPtr = new Data; // chiama il costruttore di Default
i = primoPtr->leggiMese(); // Ritorna 1 (valore di default )
secondoPtr = new Data( 3, 15, 1985); // chiama il Costruttore
i = secondoPtr->leggiMese(); // Ritorna 3
L'operatore new chiama il costruttore appropriato della classe Data, in dipendenza dell'argomento specificato. Non è nemmeno necessario usare l'operatore sizeof per determinare la dimensione dell'oggetto, perchè new è in grado di sapere qual è lo spazio richiesto dalla classe. Inoltre, il puntatore restituito da new non deve essere convertito. Il compilatore controlla che il tipo del puntatore all'oggetto corrisponda al tipo del puntatore a cui viene assegnato il valore e genera un errore se sono di tipo diverso. Per esempio:
void *ptr;
ptr = new Data; // Errore; tipo diverso
Se non è possibile riservare lo spazio, l'operatore new restituisce 0, il corrispondente del puntatore NULL del C.
L'operatore delete
Quando la variabile dinamica, creata con malloc, non serve più lo spazio di memoria sull'heap viene
restituito al sistema operativo chiamando la funzione free. Se la variabile è stata creata con
l'operatore new, invece, lo spazio viene liberato chiamando l'operatore delete, che rimette lo spazio
occupato dalla variabile nuovamente nella memoria libera per essere, eventualmente, assegnato per altre richieste.
La sintassi per richiamare l'operatore delete è:
Data *primoPtr;
int i;
primoPtr = new Data( 3, 15, 1985); // chiama il Costruttore
i = primoPtr->leggiMese(); // Ritorna 3
delete primoPtr; // chiamato il Distruttore, liberata la memoria
Il distruttore di un oggetto viene richiamato automaticamente dall'operatore delete per rilasciare correttamente la memoria occupata. Poichè il distruttore di un oggetto di classe Date non fa niente di particolare, il motivo della sua necessità, in questo esempio, non è evidente, ma in seguito si mostrerà un caso in cui il distruttore, prima di liberare la memoria, deve svolgere qualche operazione.
L'operatore delete può essere applicato ai puntatori che sono stati inizializzati con l'operatore new. È importante cancellarli una sola volta. Il compilatore non riconosce questi due tipi di errore. Un puntatore Null (con valore 0) può essere cancellato senza problemi.
La memoria libera e i tipi predefiniti
Gli operatori new e delete possono essere usati, oltre che per cancellare oggetti, anche per variabili
di tipo predefinito, come int, char, ecc. Per esempio:
int *ip;
ip = new int; // riserva lo spazio per un intero
// il puntatore ip viene usato
delete ip; // al termine viiene eliminato;
È possibile riservare spazio anche per un array la cui dimensione è nota solo in fase di esecuzione:
int lung; char *cp;
// si assegna il valore a lung
cp = new char[lung]; // riserva un array di char
// Uso del puntatore cp
delete [] cp;
Notare la sintassi per dichiarare un array creato dinamicamente: la dimensione dell'array è una variabile che si trova tra le parentesi quadre, dopo il nome del tipo. Notare anche la sintassi per cancellare l'array: si mette una coppia di parentesi quadre prima del puntatore. Se c'è un numero tra quelle parentesi quadre, il compilatore lo ignora.
L'operatore new permette di creare un array multi dimensionale, con il vincolo che tutte le dimensioni siano costanti, eccetto la prima. Per esempio.
int (*matrice)[10];
int NrElem;
// Assegnazione di un valore a NrElem,
matrice = new int[NrElem][10]; // riserva lo spazio per una matrice
// Uso della matrice
delete [] matrice;
Classi con campi membro puntatori
Gli operatori new e delete possono essere usati dalle funzioni membro interne alle classi.
Ad esempio, si vuole scrive una classe Stringa, in cui ogni oggetto contiene una stringa di caratteri.
Se si memorizzasse la stringa come array di caratteri, si dovrebbe scegliere una dimensione per la stringa.
Un metodo più corretto consiste nell'assegnare alla classe un campo membro di tipo puntatore a stringa
e poi riservare, ad ogni oggetto, dinamicamente lo spazio per la stringa. Per esempio:
#include <string.h>
class Stringa {
public:
Stringa();
Stringa( const char *s );
Stringa( char c, int n);
void scrivi( int indice, char nuovoCar );
char leggi( int indice ) const;
int leggiLung() const { return lung; }
void mostra() const { cout << buf; }
~Stringa();
private:
int lung;
char *buf;
};
Stringa::Stringa() { // costruttore di Default
buf = 0;
lung = 0;
}
Stringa::Stringa( const char *s ) { // Costruttore che riceve un puntatore a una stringa costante
lung = strlen( s );
buf = new char[lung + 1];
strcpy( buf, s );
}
Stringa::Stringa( char c, int n) { // Costruttore che riceve un carattere e un intero.
lung = n;
buf = new char[lung + 1]; // bisogna assegnare un byte per il carattere di fine stringa
memset( buf, c, lung );
buf[lung] = '\0'; )
void Stringa::scrivi( int indice, char nuovoCar ) { // scrive un carattere in una Stringa
if( (indice > 0) && (indice <= lung) )
buf[indice - 1] = nuovoCar;
}
char Stringa::leggi( int indice ) const { // Legge un carattere da una Stringa
if( (indice > 0) && (indice <= lung) )
return buf [indice - 1];
else
return 0;
}
Stringa::-Stringa() { // Distruttore per una Stringa
delete [] buf; // funziona anche con stringhe vuote
}
main() {
Stringa unaStringa( "ECco la prima stringa" );
unaStringa.scrivi( 1, 'c' );
}
Il costruttore della classe Stringa, che riceve un puntatore a un carattere, usa l'operatore new per riservare uno spazio di memoria sufficientemente grande per contenere tutta la stringa, poi copia la stringa in questa area di memoria. Di conseguenza, l'oggetto Stringa non è contenuto in locazioni di memoria contigue, ma consiste di due blocchi: uno contiene i campi della classe (Lung e buf) e l'altro contiene i caratteri della Stringa.
Se si applica l'operatore sizeof ad un oggetto Stringa si ottiene un risultato che rappresenta la somma dello spazio occupato dai campi Lung e buf. Comunque, oggetti diversi puntano a stringhe diverse. Infatti si può aggiungere una funzione membro che cambia la lunghezza dell'area di memoria della stringa:
void Stringa::accoda( const char *codaStr ) {
char *temp;
lung += strlen( codaStr );
temp = new char [lung + 1]; // riserva un nuovo buffer
strcpy(temp, buf ); // Copia il contenuto del buffer precedente
strcat(temp, codaStr); // Aggiungi in coda la nuova stringa.
delete [] buf; // rilascia il vecchio buffer di memoria.
buf = temp;
}
Questa funzione aggiunge in coda alla stringa esistente una nuova stringa. Per esempio:
Stringa unaStringa( "Ecco una stringa" );
unaStringa.append( " ed eccone un'altra" );
// unaStringa adesso punta a: "Ecco una stringa ed eccone un'altra"
L'oggetto Stringa, quindi, si può ridimensionare dinamicamente. Le operazioni di ridimensionamento sono gestite dalle funzioni membro.
La classe Stringa è un esempio di classe che richiede un distruttore. Quando il flusso del programma esce fuori dalla regione di visibilità di un oggetto Stringa il blocco di memoria che conteneva i campi Lung e buf viene rilasciato automaticamente, mentre il blocco di memoria che contiene i caratteri della stringa deve essere recuperato esplicitamente. Infatti la classe Stringa definisce un distruttore che usa l'operatore delete per restituire alla memoria libera lo spazio assegnato. Se la classe non avesse un distruttore adibito a questa operazione, l'area di memoria occupata dai caratteri del buffer non verrebbe mai recuperata e, continuando a creare altri oggetti, si potrebbe provocare un esaurimento della memoria a disposizione del programma.
La classe Stringa, comunque, presenta un potenziale rischio. Si supponga che nel main si esegua questa operazione:
Stringa altraStringa( "Ecco un'altra stringa" );
altraStringa = unaStringa;
Il programma crea un'altra istanza della classe Stringa chiamata altraStringa e ad essa assegna il contenuto dell'oggetto unaStringa. Apparentemente è una comune operazione di assegnazione, quale problema dovrebbe creare?
Quando si assegna il contenuto di un oggetto ad un altro oggetto, il compilatore svolge l'assegnazione dei valori dei campi membro, cioè compie le assegnazioni seguenti:
// equivalente altraStringa = unaStringa
altraStringa.lung = unaStringa.lung;
altraStringa.buf = unaStringa.buf;
La prima assegnazione, quella relativa al campo lung non crea problemi. La seconda assegnazione invece deve essere esaminata attentamente. Il campo membro buf è un puntatore. Dopo l'assegnazione del valore unaStringa.buf al campo altraStringa.buf, i due puntatori fanno riferimento alla stessa area di memoria. Come mostrato nella figura seguente:
Il risultato dell'assegnazione altraStringa = unaStringa; è mostrato nella figura seguente:
Come conseguenza di questa assegnazione, ogni modifica ad un oggetto influenza anche l'altro oggetto. Ad esempio se si chiama il metodo: unaStringa.scrivi(), si modifica anche altraStringa. Probabilmente non è un comportamento desiderato.
Altri problemi nascono quando il flusso del programma esce dalla regione di visibilità degli oggetti. Appena l'oggetto unaStringa non è più visibile, viene richiamato il suo distruttore. Questo cancella il puntatore e rilascia la memoria a cui punta. Poi viene richiamato il distruttore dell'oggetto altraStringa che cancella il campo buf, ma tenta di rilasciare per la seconda volta la stessa area già rilasciata. Questo può provocare degli errori, ma l'area a cui puntava originariamente altraStringa, quella contenente la stringa: ed eccone un'altra, non verrà più cancellata.
Questi problemi sono associati alle classi che posseggono campi puntatore e che riservano spazio nella memoria libera. Il comportamento del compilatore per questi casi non è adatto, bisogna ricorrere ad una funzione speciale che si chiama operatore di assegnazione.
l'operatore di assegnazione
Così come si può ridefinire una funzione è possibile ridefinire anche un operatore, ad esempio
quello di assegnazione, affinchè possa avere più di un significato. Ad esempio si può definire
come deve comportarsi quando si trova tra istanze di classi.
Per ridefinire il significato dell'operatore di assegnazione per una classe si scrive una funzione membro con il nome:
operator=
Se la classe definisce una tale funzione, il compilatore la chiama ogni volta che incontra l'assegnazione di un oggetto
ad un altro oggetto. Il compilatore interpreta la seguente assegnazione:
altraStringa = unaStringa;
come la chiamata di funzione seguente:
altraStringa.operator=( unaStringa );
Infatti si può usare anche la seconda sintassi per eseguire l'assegnazione, ma la prima è più
leggibile. L'operatore per la classe Stringa può essere scritto come segue:
#include <string.h>
class Stringa {
public:
Stringa();
Stringa( const char *s );
Stringa( char c, int n);
void operator=( const Stringa &altra );
// etc...
// operatore di assegnazione
void Stringa::operator=( const String &altra ) {
lung = altra.lung;
delete [] buf;
buf = new char[lung + 1];
strcpy( buf, altra.buf );
}
L'operatore di assegnazione riceve il riferimento ad un oggetto come parametro. (si noti che è stato usato un riferimento costante, perchè la funzione non modifica l'oggetto). Per eseguire l'assegnazione la funzione prima copia il campo lung, poi cancella l'area puntata dal campo membro buf, restituendola alla memoria libera (anche se la stringa non esistesse, cancellare un puntatore 0, o nullo, non produce errori). Quindi la funzione riserva un nuovo buffer di memoria e vi copia il contenuto della stringa. Questo è mostrato nella figura seguente:
Il seguente è un programma che usa la nuova classe Stringa con l'appropriato operatore di assegnazione:
main() {
Stringa unaStringa( "Ecco una stringa" );
unaStringa.mostra();
cout << endl;
Stringa altraStringa( " ed eccone un'altra" );
altraStringa.mostra();
cout << endl;
altraStringa = unaStringa;
altraStringa.mostra();
cout << endl;
}
Questo programma stampa i seguenti messaggi:
Ecco una stringa
ed eccone un'altra
Ecco una stringa
Cosa succede se, usando la classe Stringa, il programmatore, involontariamente, assegna un oggetto a se stesso? Per esempio:
unaStringa = unaStringa; // auto-assegnazione
È difficile che un programmatore scriva una simile espressione di assegnazione, ma l'auto-assegnazione assume forme inaspettate. Per esempio:
Stringa *ptrStringa = &unaStringa;
// più avanti ...
unaStringa = *ptrStringa; // auto-assegnazione
Si esamini come viene eseguita tale assegnazione: l'operatore ridefinito cancella il buffer di unaStringa e riserva un nuovo spazio. Poi copia il contenuto del buffer appena allocato su se stesso. Il risultato è imprevedibile.
Affinchè la funzione operator= funzioni sempre correttamente, deve prevenire l'auto assegnazione. Al riguardo deve ricorrere al puntatore this.
il puntatore this
il puntatore this è un puntatore che possono usare tutte le funzioni membro di una classe. Questo
puntatore punta all'oggetto a cui appartiene la funzione che lo sta usando.
Quando si chiama una funzione membro di un oggetto, il compilatore assegna l'indirizzo dell'oggetto al puntatore
this e poi chiama la funzione. Ogni volta che una funzione membro accede a un campo membro della classe
usa implicitamente il puntatore this.
Per esempio, si consideri il seguente frammento di programma in cui viene definita una funzione e poi c'è la chiamata della funzione:
void Data::scriviMese( int m ) {
mese = mn;
}
// chiamata della funzione membro
unaData.scriviMese( 3 );
Questo equivale al seguente frammento di programma C:
void Data_scriviMese( Data *const this, int m ) {
this->mese = m;
}
// chiamata di Funzione
Data_scriviMese( &unaData, 3 );
Notare che, per le funzioni membro della classe Data, al parametro this è stato dato il tipo Data *; (puntatore ad oggetto di classe Data). Il tipo sarà diverso per le funzioni membro di altre classi.
Quando si scrive una funzione membro, è consentito usare il puntatore this per accedere ai campi membro, anche se non è necessario. È anche ammesso usare l'espressione *this per fare riferimento all'oggetto a cui appartiene la funzione membro. Nei tre esempi che seguono, le istruzioni sono equivalenti, fanno tutte la stessa cosa:
void Data::mese mostra() {
cout << mese;
cout << this->mese;
cout << (*this).mese;
}
Un oggetto membro può usare il puntatore this per confrontare se un oggetto passato come parametro è lo stesso oggetto a cui appartiene la funzione membro chiamante. Ad esempio, la funzione operator= della classe Stringa può essere riscritta nel modo seguente:
void Stringa::operator=( const Stringa &altra ) {
if( &altra == this )
return;
delete [] buf;
lung = altra.lung;
buf = new char[lung + 1];
strcpy( buf, altra.buf );
}
La funzione interroga il puntatore this per assicurarsi che l'indirizzo dell'altro oggetto sia diverso. Se lo trova uguale, si accorge che sta per avvenire un'auto assegnazione ed esce dalla funzione.
Usare *this in una istruzione return
Il puntatore this può anche essere usato in una funzione membro come parametro restituito da una
istruzione return. Sia in C che in C++ una tale istruzione equivale ad un'espressione che ha il valore di
ciò che viene ritornato. Per esempio l'istruzione:
i = 3;
è un'espressione che ha il valore 3.
Come conseguenza, si possono usare le assegnazioni multiple:
a=b=c;
Quando si incontra un operatore di assegnazione, si valuta prima l'espressione a destra del segno di "=", è come se per specificare la priorità delle operazioni si usassero le parentesi:
a = (b = c);
Affinchè l'operatore ridefinito funzioni in questo modo, l'operatore di assegnazione deve ritornare l'oggetto a cui appartiene. L'operatore, essendo una funzione membro, conosce l'indirizzo dell'oggetto, contenuto nel puntatore this.
Ritornare *this richiede alcune semplici modifiche all'operatore di assegnazione (nella funzione operator=):
Stringa &Stringa::operator=( const Stringa &altra ) {
if( &altra == this )
return *this;
delete [] buf;
lung = altra.lung;
buf = new char[lung + 1];
strcpy( buf, altra.buf );
return *this;
}
Adesso, con questa versione della funzione membro operator= si possono formare assegnazioni multiple di oggetti Stringa:
terzaStringa = altraStringa = unaStringa;
La funzione ritorna un riferimento ad una Stringa.
Ritornare *this spiega come funzionano le istruzioni cout. In un'istruzione simile:
cout << a << b << c;
L'operatore di scorrimento a sinistra valuta prima l'espressione a sinistra, in questo caso l'operatore di scorrimento è ridefinito e ritorna *this, che è il riferimento all'oggetto cout, e così ogni variabile viene stampata.
Assegnazione e Inizializzazione
Si considerino i seguenti due frammenti di programma:
int i;
i = 3;
e
int i = 3;
In linguaggio C questi due frammenti di programma sono equivalenti. In C++, invece, sono diversi. Nel primo esempio viene assegnato un valore alla variabile di tipo intero i, nel secondo la variabile i è inizializzata con un valore.
Le differenze sono le seguenti:
Un'assegnazione è la scrittura di un valore in una variabile esistente, che sostituisce il valore precedente. Ad una variabile si possono assegnare un numero illimitato di nuovi valori.
Una inizializzazione avviene quando ad una variabile si assegna un valore nel momento in cui viene dichiarata. Una variabile può essere inizializzata una sola volta.
Per discutere queste differenze si considerino le variabili dichiarate const. Una variabile costante può essere inizializzata una sola volta. Non le si può assegnare un nuovo valore. (Come i riferimenti, che sono inizializzati con una variabile, ma non gli si può assegnare una nuova variabile.)
Questa distinzione è importante con gli oggetti. Se nell'esempio precedente, l'intero viene sostituito dalla classe Stringa:
Stringa unaStringa( "Ecco una stringa" );
Stringa altraStringa;
altraStringa = unaStringa; // Assegnazione del valore di una Stringa ad un'altra
L'inizializzazione è:
String unaStringa( "Ecco una stringa" );
String altraStringa = unaStringa; // inizializza una stringa con un'altra
Come già discusso, quando il compilatore incontra l'istruzione di assegnazione chiama la funzione operator= ridefinita nella classe. L'inizializzazione non richiama la stessa funzione. La funzione operator= può essere richiamata solo per un oggetto che è già stato costruito. Nel precedente esempio l'oggetto altraStringa è stato costruito nello stesso momento che riceve il valore di un altro oggetto.
Per costruire un oggetto in questo modo il compilatore richiama un costruttore speciale, chiamato il costruttore "copia".
Il costruttore Copia
Il costruttore Copia riceve, come parametro, un oggetto dello stesso tipo. Viene richiamato ogni volta che un
oggetto viene inizializzato con il valore di un altro. Può essere richiamato con il segno "=", oppure
con una normale chiamata di funzione.
Per esempio, l'inizializzazione precedente può essere riscritta in questo modo:
String altraStringa( unaStringa );
che rispetta la stessa sintassi per richiamare un costruttore della classe.
Per il modo in cui è stata scritta la classe Stringa, il compilatore esegue la dichiarazione precedente inizializzando ciascun campo membro di altraStringa con i valori dei campi membro di unaStringa. Esattamente come succede con l'assegnazione. Questa operazione, come visto, può avere delle gravi conseguenze quando la classe contiene dei puntatori. Infatti l'effetto della precedente inizializzazione è di dare a entrambi gli oggetti altraStringa e unaStringa lo stesso buffer di caratteri, che può causare errori quando viene richiamato il distruttore.
La soluzione consiste nello scrivere un costruttore copia personalizzato, come nel seguente esempio:
#include <string.h>
class Stringa {
public:
>Stringa();
Stringa( const char *s );
Stringa( char c, int n);
Stringa( const Stringa &altra); // Costruttore Copia
// etc...
};
Stringa::Stringa( const Stringa &altra ) { // Costruttore Copia
lung = altra.lung;
buf = new char[lung + 1];
strcpy( buf, altra.buf );
}
La definizione del costruttore Copia è simile a quella dell'operatore di assegnazione, che riserva un nuovo buffer di caratteri all'oggetto che viene creato. Si noti che il costruttore Copia riceve, come parametro, il riferimento ad un oggetto, anzichè l'oggetto stesso.
Le differenze tra l'operatore di assegnazione e il costruttore copia si possono così riepilogare:
Un operatore di assegnazione agisce su un oggetto esistente, mentre il costruttore Copia crea un nuovo oggetto. Di conseguenza l'operatore di assegnazione potrebbe aver bisogno di cancellare l'area di memoria puntata dall'oggetto destinazione
Un operatore di assegnazione deve prevenire l'auto assegnazione. Il costruttore Copia non ha bisogno di prevenirla perchè l'auto assegnazione è impossibile.
Per consentire l'assegnazione multipla, l'operatore di assegnazione deve ritornare *this. Il costruttore Copia non può farlo perchè un costruttore non restituisce valore.
Passaggio di oggetti
Ci sono altri due casi, oltre alle dichiarazioni, in cui viene richiamato il costruttore Copia:
Quando una funzione riceve un oggetto come parametro,
Quando una funzione restituisce un oggetto.
Nel seguente esempio una funzione riceve un oggetto come parametro:
// Funzione che riceve un parametro di tipo Stringa
void usa( String parm ) {
// Usa il parametro
}
void main() {
String unaStringa( "Ecco una stringa" );
usa( unaStringa );
}
La funzione usa riceve un oggetto di classe Stringa passato per valore. Questo significa che la funzione opera su una copia privata dell'oggetto. Il parametro della funzione viene inizializzato con l'oggetto che riceve dalla funzione chiamante. Implicitamente, il compilatore chiama il costruttore Copia per eseguire questa inizializzazione. Svolge l'equivalente della seguente istruzione:
// inizializzazione del parametro
String parm( unaStringa ); // chiama il costruttore Copia
Si consideri cosa succede se il programmatore non definisce il costruttore Copia per gestire l'inizializzazione. Il comportamento del compilatore prevede che i due oggetti facciano riferimento allo stesso buffer di caratteri. Ogni operazione sul buffer di parm modifica anche il buffer di unaStringa.
Non si dimentichi, inoltre, che la regione di visibilità del parametro è locale alla funzione, quindi, al termine della funzione viene richiamato il distruttore. Questo significa che unaStringa possiede un puntatore a un'area di memoria cancellata.
Il seguente esempio mostra una funzione che restituisce un oggetto:
// Funzione che restituisce una Stringa
String produci() {
String valoreRit( "Ecco una stringa di ritorno" );
return valoreRit;
}
void main() {
String altraStringa;
altraStringa = produci();
}
La funzione produci ritorna un oggetto Stringa. Il compilatore chiama il costruttore Copia per inizializzare un oggetto temporaneamente nascosto nella regione di visibilità del chiamante, usando l'oggetto specificato nell'istruzione di ritorno della funzione. Questo oggetto temporaneamente nascosto viene usato sul lato destro dell'istruzione di assegnazione. In altri termini, il compilatore esegue qualcosa di equivalente a questo:
// ipotetica inizializzazione del valore di ritorno
Stringa temp( valoreRit); // chiama il costruttore Copia
altraStringa = temp; // assegnazione
Anche in questo caso è necessario il costruttore Copia, altrimenti l'oggetto temporaneo condivide lo stesso buffer di caratteri di valoreRit, che viene eliminato quando la funzione termina e l'assegnazione ad altraStringa non risulta corretta.
Come regola, una classe deve sempre possedere un costruttore Copia e un operatore di assegnazione ridefinito quando possiede campi membro che sono puntatori a cui assegna indirizzi della memoria libera.
Passare e Ritornare Riferimenti a Oggetti
Il passaggio di un oggetto per valore come parametro di una funzione comporta il lavoro di copia dei campi membro
in un'area locale alla funzione chiamata. Questo lavoro si può evitare passando un riferimento a un oggetto
costante:
void usa( const String &parm ) {
// Usa l'oggetto parm
}
void main() {
String unaStringa( "Ecco una stringa" );
usa( unaStringa );
}
Il costruttore Copia non viene chiamato quando il parametro viene passato in questo modo, perchè non si deve costruire un nuovo oggetto. Però viene inizializzato un riferimento all'oggetto che deve essere passato. Il compilatore svolge la seguente operazione:
// inizializzazione del riferimento
const Stringa &parm = unaStringa; // inizializza il riferimento
Come conseguenza la funzione chiamata usa lo stesso oggetto della funzione chiamante. Notare che si è usato il termine const davanti al parametro passato, quindi la funzione non può modificarlo. In questo modo, si ricordi che, si possono richiamare solo le funzioni membro const per accedere all'oggetto.
Anche il costruttore Copia riceve come parametro un riferimento a un oggetto anzichè un oggetto. Se il Costruttore Copia ricevesse un oggetto come parametro, dovrebbe richiamare se stesso per inizializzare il parametro. Si verificherebbe una ricorsione senza fine.
Restituire un riferimento a un oggetto al termine di una funzione potrebbe essere più efficiente che restituire l'oggetto. Si ritorni all'esempio della ridefinizione dell'operatore di assegnazione:
Stringa &Stringa::operator=( const Stringa &altra ) {
//...
return *this;
}
void main() {
Stringa unaStringa( "Ecco una stringa" );
Stringa altraStringa, terzaStringa;
terzaStringa = altraStringa = unaStringa;
}
Il costruttore Copia non viene chiamato quando la funzione ritorna, perchè non viene creato un oggetto temporaneo, ma solo un riferimento temporaneo. Quando l'oggetto terzaStringa riceve il valore dal risultato dell'assegnazione altraStringa = unaStringa, il compilatore si comporta così:
// inizializzazione del riferimento di ritorno
Stringa &tempRif = altraStringa; // Inizializza il riferimento
// NOTA: altraStringa = *this
terzaStringa = tempRif; // Assegnazione del riferimento temp
// Equivalente a terzaStringa = altraStringa
Bisogna prestare attenzione quando si ritorna un riferimento ad un oggetto o ad una variabile diversa da *this. Le regole per ritornare riferimenti sono simili a quelle per ritornare puntatori. Non è possibile ritornare un puntatore ad una variabile locale:
// ERRORE: ritorna il puntatore ad una variabile che non esiste più
int *produciPtr() {
int i;
return &i;
}
L'intero i viene eliminato quando la funzione termina, quindi il chiamante riceve il puntatore ad una variabile inesistente. La stessa limitazione vale per i riferimenti:
// ERRORE: ritornare riferimenti a variabili locali
int &produciRif() {
int i;
return i;
}
Non ci sono rischi di errori se si restituisce un riferimento o un puntatore a una variabile creata dinamicamente. Queste, infatti, esistono fino a quando non vengono rilasciate esplicitamente, anche quando si esce dalla funzione. Per la stessa ragione si possono restituire riferimenti a variabili statiche o globali.