CATEGORII DOCUMENTE |
Bulgara | Ceha slovaca | Croata | Engleza | Estona | Finlandeza | Franceza |
Germana | Italiana | Letona | Lituaniana | Maghiara | Olandeza | Poloneza |
Sarba | Slovena | Spaniola | Suedeza | Turca | Ucraineana |
DOCUMENTE SIMILARE |
|
I costrutti analizzati finora costituiscono giÀ un linguaggio che ci consente di realizzare anche programmi complessi e di fatto, salvo alcune cose, quanto visto costituisce il linguaggio C; tuttavia il C++ È molto di piÙ e offre caratteristiche nuove che estendono e migliorano il C: programmazione a oggetti, template (modelli) e gestione delle eccezioni.
Si potrebbe apparentemente dire che si tratta solo di qualche aggiunta, in realtÀ nessun'altra affermazione potrebbe essere piÙ errata: mentre l'ultima caratteristica (la gestione delle eccezioni) È in effetti una estensione (l'aggiunta di qualcosa che mancava), le prime due non sono semplici aggiunte in quanto non si limitano a fornire nuove funzionalitÀ, ma impongono un nuovo modo di concepire e realizzare codice e caratterizzano il linguaggio fino a influenzare il codice prodotto in fase di compilazione (notevolmente diverso da quello prodotto dal compilatore C).
Inizieremo ora a discutere dei meccanismi offerti dal C++ per la programmazione orientata agli oggetti, si ricorda che l'autore assume noti al lettore i concetti di tale paradigma, per chi non avesse tali conoscenze (o desiderasse riportarli alla mente) È disponibile un articolo sufficientemente completo all'indirizzo https://www.nsm.it/mondobit/mb1/mb1prog.html.
La programmazione orientata agli oggetti (OOP) impone una nuova visione di concetti quali 'Tipo di dato' e 'Istanze di tipo'. Sostanzialmente mentre gli altri paradigmi di programmazione vedono le istanze di un tipo di dato come una entitÀ passiva, nella programmazione a oggetti invece tali istanze diventano a tutti gli effetti entitÀ (oggetti) attive.
L'idea È che non bisogna piÙ manipolare direttamente i valori di una struttura (intesa come generico contenitore di valori), meglio lasciare che sia la struttura stessa a manipolarsi e a compiere le operazioni per noi. Tutto ciÒ che bisogna fare È inviare all'oggetto un messaggio che specifichi l'operazione da compiere e attendere poi che l'oggetto stesso ci comunichi il risultato. Il meccanismo dei messaggi viene sostanzialmente implementato tramite quello della chiamata di funzione e l'insieme dei messaggi cui un oggetto risponde viene definito associando al tipo dell'oggetto un insieme di funzioni.
In C++ ciÃ’ puÃ’ essere realizzato tramite le strutture:
struct Complex ;
CiÒ che sostanzialmente cambia, rispetto a quanto visto, È che una struttura puÒ possedere campi di tipo funzione (detti 'funzioni membro' oppure 'metodi') che costituiscono (insieme ai campi ordinari ('membri dato' o 'attributi') l'insieme dei messaggi a cui quel tipo È in grado di rispondere (interfaccia).
L'esempio non mostra come implementare le funzioni membro, per adesso ci basta sapere che esse vengono definite da qualche parte fuori dalla dichiarazione di struttura.
Una funzione dichiarata come campo di una struttura puÒ essere invocata ovviamente solo se associata ad una istanza della struttura stessa, dato che quello che si fa È inviare un messaggio ad un oggetto, e nella pratica effettuata tramite la stessa sintassi utilizzata per selezionare un qualsiasi altro campo:
Complex A;
Complex * C;
A.Set(0.2, 10.3);
A.Print();
C = new Complex;
C->Set(1.5, 3.0);
float FloatVar = C->Abs();
Nell'esempio viene mostrato come inviare un messaggio: la quarta riga invia il messaggio Print() all'oggetto A, l'ultima invece invia il messaggio Abs() all'oggetto puntato da C e assegna il valore ottenuto alla variabile FloatVar.
Il vantaggio principale di questo modo di procedere È il non doversi piÙ preoccupare di come È fatto quel tipo, se si vuole eseguire una operazione su una sua istanza (ad esempio visualizzarne il valore) basta inviare il messaggio corretto, sarÀ l'oggetto in questione ad eseguirla per noi. Ovviamente perché tutto funzioni È necessario evitare di accedere direttamente agli attributi di un oggetto, altrimenti crolla uno dei capisaldi della OOP, e sfortunatamente per noi il meccanismo delle strutture consente l'accesso diretto a tutto ciÃ’ che fa parte della dichiarazione di struttura, annullando di fatto ogni vantaggio:
// Con riferimento agli esempi riportati sopra:
A.Set(6.1, 4.3); // Setta il valore di A
A.Re = 10; // Ok!
A.Im = .5; // ancora Ok!
A.Print();
Il problema viene risolto introducendo una nuova sintassi per la dichiarazione di un tipo oggetto. Un tipo oggetto viene dichiarato tramite una dichiarazione di classe, che differisce dalla dichiarazione di struttura sostanzialmente per i meccanismi di protezione offerti; per il resto tutto ciÃ’ che si applica alle classi si applica allo stesso modo alla dichiarazione di struttura senza alcuna differenza.
Vediamo dunque come sarebbe stato dichiarato il tipo Complex tramite la sintassi della classe:
class Complex ;
La differenza È data dalle keyword public e private che consentono di specificare i diritti di accesso alle dichiarazioni che le seguono:
public : le dichiarazioni che seguono questa keyword sono visibili a chi usa una istanza della classe e l'invocazione (selezione) di uno di questi campi È sempre possibile;
private : tutto ciÒ che segue È visibile solo alla classe stessa, una invocazione di uno di questi campi È possibile solo dai metodi della classe stessa;
come mostra il seguente esempio:
Complex A;
Complex * C;
A.Re = 10.2; // Errore!
C->Im = .5; // ancora errore!
A.Print(); // Ok!
C->Print() // Ok!
Ovviamente le due keyword sono mutuamente esclusive, nel senso che alla dichiarazione di un metodo o di un attributo si applica la prima keyword che si incontra risalendo in su; se la dichiarazione non È preceduta da nessuna di queste keyword, il default È private:
class Complex ;
In realtÀ esiste una terza categoria di visibilitÀ definibile tramite la keyword protected (che perÒ analizzeremo quando parleremo di ereditarietÀ); la sintassi per la dichiarazione di classe È dunque:
class ClassName ; // notare il punto e virgola finale!
Non ci sono limitazioni al tipo di dichiarazioni possibili dentro una delle tre sezioni di visibilitÀ: definizioni di variabili o costanti (attributi), funzioni (metodi) oppure dichiarazioni di tipi (enumerazioni, unioni, strutture e anche classi); tuttavia esiste una differenza per quanto riguarda le regole di scoping sui tipi annidati:
tipi definiti nella sezione public sono visibili anche alle altre due sezioni, ma non viceversa;
tipi definiti nella sezione protected sono visibili anche alla sezione private, ma non a public;
tipi definiti nella sezione private sono visibili ovviamente solo ad essa;
Il motivo È ovvio, se ad esempio un metodo pubblico dovesse restituire un tipo privato, l'utente della classe non sarebbe in grado di gestire il valore ottenuto perché non È in grado di accedere alla definizione di tipo; questo naturalmente non vale per i metodi e gli attributi che se sono privati possono essere direttamente acceduti solo da metodi della classe stessa senza porre alcun problema di visibilitÀ all'esterno della classe.
La definizione dei metodi di una classe puÃ’ essere eseguita o dentro la dichiarazione di classe, facendo seguire alla lista di argomenti una coppia di parentesi graffe racchiudente la sequenza di istruzioni:
class Complex
private:
/* */
};
oppure riportando nella dichiarazione di classe solo il prototipo e definendo il metodo fuori dalla dichiarazione di classe, nel seguente modo (anch'esso applicabile alle strutture):
// Questo modo di procedere richiede l'uso
// dell'operatore di risoluzione di scope e
// l'uso del nome della classe per indicare
// esattamente quale metodo si sta definendo
// (classi diverse possono avere metodi con
// lo stesso nome).
void Complex::Print()
La differenza È che nel primo caso implicitamente si richiede una espansione inline del codice della funzione, nel secondo caso se si desidera tale accorgimento bisogna utilizzare esplicitamente la keyword inline nella definizione del metodo:
inline void Complex::Print()
Se la definizione del metodo Print() È stata studiata con attenzione, il lettore avrÀ notato che la funzione accede ai membri dato senza ricorrere alla notazione del punto, ma semplicemente nominandoli: quando ci si vuole riferire ai campi dell'oggetto cui È stato inviato il messaggio non bisogna adottare alcuna particolare notazione, lo si fa e basta!
Il problema di risolvere correttamente ogni riferimento viene risolto automaticamente dal compilatore: all'atto della chiamata, ciascun metodo riceve un parametro aggiuntivo, un puntatore all'oggetto a cui È stato inviato il messaggio e tramite questo È possibile risalire all'indirizzo corretto; ciÒ inoltre consente la chiamata di un metodo da parte di un altro metodo:
class MyClass ;
/* definizione di SmallOp() e PrivateOp() */
void MyClass::BigOp()
Ovviamente un metodo puÒ avere parametri e/o variabili locali che sono istanze della stessa classe cui appartiene (il nome della classe È giÀ visibile all'interno della stessa classe), in questo caso per riferirsi al parametro o alla variabile locale deve utilizzare la notazione del punto:
class MyClass ;
void MyClass::Func(MyClass A, /* */ )
In alcuni rari casi puÒ essere utile avere accesso al puntatore che il compilatore aggiunge tra i parametri di un metodo, l'operazione È fattibile tramite la keyword this (che in pratica È il nome del parametro aggiuntivo), tale pratica quando possibile È comunque da evitare.
L'uso di un metodo Set() per eseguire l'inizializzazione di un oggetto (come mostrato per la struct Complex) È poco elegante e alquanto insicuro: il programmatore che usa la classe potrebbe dimenticare di chiamare tale metodo prima di cominciare ad utilizzare l'oggetto appena dichiarato. Si potrebbe pensare di scrivere qualcosa del tipo:
class Complex ;
ma il compilatore rifiuterÀ di accettare tale codice. Il motivo È semplice, stiamo definendo un tipo e non una variabile (o una costante) e non È possibile inizializzare i membri di una classe (o di una struttura) in quel modo.
Il metodo corretto È quello di fornire un costruttore che il compilatore possa utilizzare quando una istanza della classe viene creata, in modo che tale istanza sia sin dall'inizio in uno stato consistente. Un costruttore altro non È che un metodo il cui nome È lo stesso di quello della classe, che puÒ avere dei parametri, ma che non restituisce alcun tipo (neanche void); il suo scopo È quello di inizializzare le istanze della classe:
Class Complex
/* altre funzioni membro */
private:
float Re; // Parte reale
float Im; // Parte immaginaria
};
In questo modo possiamo eseguire dichiarazione e inizializzazione di un oggetto Complex in un colpo solo:
Complex C(3.5, 4.2);
La definizione appena vista introduce un oggetto C di tipo Complex che viene inizializzato chiamando il costruttore con gli argomenti specificati tra le parentesi. Si noti che il costruttore non viene invocato come un qualsiasi metodo; un sistema alternativo di eseguire l'inizializzazione sarebbe:
Complex C = Complex(3.5, 4.2);
ma È poco efficiente perché quello che si fa È creare un oggetto Complex temporaneo e poi copiarlo in
C, il primo metodo invece fa tutto in un colpo solo.
Un costruttore puÒ eseguire compiti semplici come quelli dell'esempio, tuttavia non È raro che una classe necessiti di costruttori molto complessi, specie se alcuni membri sono dei puntatori; in questi casi un costruttore puÒ eseguire operazioni complesse quali allocazione di memoria o accessi a unitÀ a disco se si lavora con oggetti persistenti.
In alcuni casi, alcune operazioni possono richiedere la certezza assoluta che tutti o parte dei campi dell'oggetto, che si vuole creare, siano subito inizializzati prima ancora che incominci l'esecuzione del corpo del costruttore; la soluzione in questi casi prende il nome di lista di inizializzazione.
La lista di inizializzazione È una caratteristica propria dei costruttori e appare sempre tra la lista di argomenti del costruttore e il suo corpo:
class Complex ;
Complex::Complex(float a, float b) : Re(a), Im(b)
L'ultima riga dell'esempio implementa il costruttore della classe Complex; si tratta esattamente dello stesso costruttore visto prima, la differenza sta tutta nel modo in cui sono inizializzati i membri dato: la notazione Attributo(<Espressione>) indica al compilatore che Attributo deve memorizzare il valore fornito da Espressione; Espressione puÃ’ essere anche qualcosa di complesso come la chiamata ad una funzione.
Nel caso appena visto l'importanza della lista di inizializzazione puÒ non essere evidente, lo sarÀ di piÙ quando parleremo di oggetti composti e di ereditarietÀ.
Una classe puÒ possedere piÙ costruttori, cioÈ i costruttori possono essere overloaded, in modo da offrire diversi modi per inizializzare una istanza; in particolare alcuni costruttori assumono un significato speciale:
il costruttore di default ClassName::ClassName();
il costruttore di copia ClassName::ClassName(ClassName& X);
altri costruttori con un solo argomento;
Il costruttore di default È particolare, in quanto È quello che il compilatore chiama quando il programmatore non utilizza altri costruttori in seguito alla dichiarazione di un oggetto:
#include <iostream.h>
class Trace
Trace(int a, int b) : M1
private:
int M1, M2;
};
void main
Eseguendo tale codice si ottiene l'output:
definizione di Bcostruttore Trace(int, int)
definizione di Ccostruttore di default
Ma l'importanza del costruttore di default È dovuta soprattutto al fatto che se il programmatore della classe non definisce alcun costruttore, automaticamente il compilatore ne fornisce uno (che perÒ non dÀ garanzie sul contenuto dei membri dato dell'oggetto). Se non si desidera il costruttore di default fornito dal compilatore, occorre definirne esplicitamente uno (anche se non di default).
Il costruttore di copia invece viene invocato quando un nuovo oggetto va inizializzato in base al contenuto di un altro; modifichiamo la classe Trace in modo da aggiungere il seguente costruttore di copia:
Trace::Trace(Trace& x) : M1(x.M1), M2(x.M2)
e aggiungiamo il seguente codice a main():
cout << 'definizione di D';
Trace D = B;
CiÒ che viene visualizzato ora, È che per D viene chiamato il costruttore di copia.
Se il programmatore non definisce un costruttore di copia, ci pensa il compilatore. In questo caso il costruttore fornito dal compilatore esegue una copia bit a bit degli attributi; in generale questo È sufficiente, ma quando una classe contiene puntatori È necessario definirlo esplicitamente onde evitare problemi di condivisione di aree di memoria.
I principianti tendono spesso a confondere l'inizializzazione con l'assegnamento; benché sintatticamente le due operazioni sono simili, in realtÀ esiste una profonda differenza semantica: l'inizializzazione viene compiuta una volta sola, quando l'oggetto viene creato; un assegnamento invece si esegue su un oggetto precedentemente creato. Per comprendere la differenza facciamo un breve salto in avanti.
Il C++ consente di eseguire l'overloading degli operatori, tra cui quello per l'assegnamento; come nel caso caso del costruttore di copia (e di quello di default), anche per l'operatore di assegnamento vale il discorso fatto nel caso che tale operatore non venga definito esplicitamente.
Il costruttore di copia viene utilizzato quando si dichiara un nuovo oggetto e si inizializza il suo valore con quello di un altro; l'operatore di assegnamento invece viene invocato successivamente in tutte le operazioni che assegnamo all'oggetto
dichiarato un altro oggetto. Vediamo un esempio:
#include <iostream.h>
class Trace
Trace(int a, int b) : M1(a), M2(b)
Trace& operator=(const Trace& x)
private:
int M1, M2;
};
void main()
Eseguendo tale codice otteniamo il seguente output:
definizione di Acostruttore Trace(int, int)
definizione di Bcostruttore Trace(int, int)
definizione di Ccostruttore di copia
assegnamento a Coperatore =
Restano da esaminare i costruttori che prendono un solo argomento. A tale proposito basta semplicemente dire che a tutti gli effetti essi sono dei veri e propri operatori di conversione di tipo(vedi appendice A) che convertono il loro argomento in una istanza della classe. Ecco una classe che fornisce diversi operatori di conversione:
class MyClass ;
void main()
Le prime tre dichiarazioni sono concettualmente identiche, in tutti e tre i casi convertiamo un valore di un tipo in quello di un altro; il fatto che l'operazione sia eseguita per inizializzare degli oggetti non modifica in alcun modo il significato dell'operazione stessa; al piÙ l'unica differenza È che nel primo caso si esegue in un colpo solo conversione e inizializzazione, mentre nel secondo e nel terzo caso prima si esegue la conversione e poi si chiama il costruttore di copia.
Solo l’ultima dichiarazione puÃ’ apparentemente sembrare diversa, in pratica È comunque la stessa cosa: si crea un oggetto di tipo Complex e poi lo si converte (implicitamente) al tipo MyClass, infine viene chiamato il costruttore di copia per inizializzare C.
Per finire, ecco un confronto tra costruttori e metodi (o normali funzioni) che riassume quanto detto:
Costruttori |
Metodi |
|
Tipo restituito |
nessuno |
qualsiasi |
Nome |
quello della classe |
qualsiasi |
Parametri |
nessuna limitazione |
nessuna limitazione |
Lista di inizializzazione |
si |
no |
Overloading |
si |
si |
Altre differenze e similitudini verranno esaminate nel seguito.
Poiché ogni oggetto ha una propria durata (lifetime) È necessario disporre di un metodo che permetta una corretta distruzione dell'oggetto, un distruttore.
Un distruttore È un metodo che non riceve parametri, non ritorna alcun tipo (neanche void) ed ha lo stesso nome della classe preceduto da una ~ (tilde):
class Trace
private:
/* */
};
Il compito del distruttore È quello di assicurarsi della corretta deallocazione delle risorse e se non ne viene esplicitamente definito uno, il compilatore genera per ogni classe un distruttore di default che chiama alla fine della lifetime di una variabile:
void MyFunc() // qui viene invocato automaticamente il distruttore per A
Si noti che nell'esempio non c'È alcuna chiamata esplicita al distruttore, È il compilatore che lo chiama alla fine del blocco applicativo (le istruzioni racchiuse tra ) in cui la variabile È stata dichiarata (alla fine del programma per variabili globali e statiche).
Poiché il distruttore fornito dal compilatore non tiene conto di aree di memoria allocate tramite membri puntatore, È sempre necessario definirlo esplicitamente ogni qual volta esistono membri puntatori; come mostra il seguente esempio:
#include <iostream.h>
class Trace ;
Trace::Trace(long double a)
Trace::~Trace()
In tutti gli altri casi, spesso il distruttore di default È piÙ che sufficiente e non occorre scriverlo.
Solitamente il distruttore È chiamato implicitamente dal compilatore quando un oggetto termina il suo ciclo di vita, oppure quando un oggetto allocato con new viene deallocato con delete:
void func() // chiamata distruttore per A
In alcuni rari casi puÒ tuttavia essere necessario una chiamata esplicita, in questi casi perÒ il compilatore puÒ non tenerne traccia (in generale un compilatore non È in grado di ricordare se il distruttore per una certa variabile È stato chiamato) e quindi bisogna prendere precauzioni onde evitare che il compilatore, richiamando il costruttore alla fine della lifetime dell'oggetto, generi codice errato.
Facciamo un esempio:
void Example() // Possibile errore!
Si genera un errore poichÈ, se Cond È vera, È il programma a distruggere esplicitamente B, e la chiamata al distruttore fatta dal compilatore È illecita. Una soluzione al problema consiste nell'uso di ulteriore blocco applicativo e di un puntatore per allocare nello heap la variabile:
void Example()
/* */
}
if (TVarPtr) delete TVarPtr; }
Comunque si tenga presente che i casi in cui si deve ricorrere ad una tecnica simile sono rari e spesso (ma non sempre) denotano un frammento di codice scritto male (quello in cui si vuole chiamare il distruttore) oppure una cattiva ingegnerizzazione della classe cui appartiene la variabile.
Si noti che poiché un distruttore non possiede argomenti, non È possibile eseguirne l'overloading; ogni classe cioÈ possiede sempre e solo un unico distruttore.
Normalmente istanze diverse della stessa classe non condividono direttamente risorse di memoria, l'unica possibilitÀ sarebbe quella di avere puntatori che puntano allo stesso indirizzo, per il resto ogni istanza riceve nuova memoria per ogni attributo. Tuttavia in alcuni casi È desiderabile che alcuni attributi siano comuni a tutte le istanze; per utilizzare un termine tecnico, si vuole realizzare una comunicazione ad ambiente condiviso.
Per rendere un attributo comune a tutte le istanze occorre dichiararlo static:
class MyClass ;
Gli attributi static possono in pratica essere visti come elementi propri della classe, non dell'istanza. In questo senso non È possibile inizializzare un attributo static tramite la lista di inizializzazione del costruttore, tutti i metodi (costruttore compreso) possono accedere sia in scrittura che in lettura all'attributo, ma non si puÒ assegnare un valore ad esso tramite lista di inizializzazione:
MyClass::MyClass() : Counter(0)
L'inizializzazione di un attributo static va eseguita fuori dalla classe, nel seguente modo:
<MemberType> <ClassName>::<StaticMember> = <Value> ;
Nel caso dell'attributo Counter, si sarebbe dovuto scrivere:
int MyClass::Counter = 0;
Successivamente l'accesso a un attributo static avviene come se fosse un normale attributo, in particolare l'idea guida dell'esempio era quella di contare le istanze di classe MyClass esistenti in un certo momento; i costruttori e il distruttore sarebbero stati quindi piÙ o meno cosÌ:
MyClass::MyClass() : /* inizializzazione membri non static */
MyClass::~MyClass()
Oltre ad attributi static È possibile avere anche metodi static; la keyword static in questo caso vincola il metodo ad accedere solo agli attributi statici della classe, un accesso ad un attributo non static costituisce un errore :
class MyClass ;
int MyClass::Counter = 0;
static int MyClass::GetCounterValue()
Ci si puÃ’ chiedere quale motivo ci possa essere per dichiarare un metodo static, ci sono essenzialmente tre motivi:
maggiore controllo su possibili fonti di errore: dichiarando un metodo static, chiediamo al compilatore di accertarsi che il metodo non acceda ad altre categorie di attributi;
minor overhead di chiamata: i metodi non static per sapere a quale oggetto devono riferire, ricevono dal compilatore un parametro aggiuntivo che altro non È che un puntatore all'istanza di classe per cui il metodo È stato chiamato; i metodi static per loro natura non hanno bisogno di tale parametro e quindi non richiedono tale overhead;
i metodi static oltre a poter essere chiamati come un normale metodo, associandoli ad un oggetto (con la notazione del punto), possono essere chiamati come una normale funzione senza necessitÀ di associarli ad una particolare istanza, ricorrendo al risolutore di scope come nel seguente esempio:
MyClass Obj;
int Var1 = Obj.GetCounterValue(); // Ok!
int Var2 = MyClass::GetCounterValue(); // Ancora Ok!
Non È possibile dichiarare static un costruttore o un distruttore.
Oltre ad attributi di tipo static, È possibile dichiarare un attributo const; in questo caso perÒ l'attributo const non È trattato come una costante: esso viene allocato per ogni istanza come un normale attributo, tuttavia il valore che esso assume per ogni istanza viene stabilito una volta per tutte all'atto della creazione dell'istanza stessa e non potrÀ mai cambiare durante la vita dell'oggetto. Il valore di un attributo const, infine, va settato tramite la lista di inizializzazione del costruttore:
class MyClass ;
MyClass::MyClass(int a, float b) : ConstMember(a), AFloat(b) ;
Il motivo per cui bisogna ricorrere alla lista di inizializzazione È semplice: l'assegnamento È una operazione proibita sulle costanti, l'operazione che si compie tramite la lista di inizializzazione È invece concettualmente diversa (anche se per i tipi primitivi È equivalente ad un assegnamento).
È anche possibile avere funzioni membro const (in questo caso la keyword const va posta dopo la lista dei parametri del metodo) analogamente a quanto avviene per le funzioni membro statiche. Dichiarando un metodo const si stabilisce un contratto con il compilatore: la funzione membro si impegna a non accedere in scrittura ad un qualsiasi attributo della classe e il compilatore si impegna a segnalare con un errore ogni tentativo in tal senso.
Oltre a ciÃ’ esiste un altro vantaggio a favore dei metodi const: sono gli unici a poter essere eseguiti su istanze costanti.
class MyClass {
public:
MyClass(int a, float b) : ConstMember(a), AFloat(b) ;
int GetConstMember() const
void ChangeFloat(float b)
private:
const int ConstMember;
float AFloat;
};
void main()
Come per i metodi static, non È possibile avere costruttori e distruttori const (sebbene essi vengano utilizzati per costruire e distruggere anche le istanze costanti).
Poiché gli attributi const altro non sono che attributi a sola lettura, ma che vanno inizializzati tramite lista di inizializzazione, È chiaro che non È possibile scrivere codice simile:
class Bad ;
perché non si puÃ’ stabilire a tempo di compilazione il valore di Size. La soluzione al problema viene dalla keyword enum; se ricordate bene, È possibile stabilire quali valori interi associare alle costanti che appaiono tra parentesi graffe al fine di rappresentarle.
Nel nostro caso dunque la soluzione È:
class Ok ;
char String[Size];
};
Chi ha fatto attenzione avrÀ notato che la keyword enum non È seguita da un identificatore, ma direttamente dalla parentesi graffa; il motivo È semplice: non ci interessava definire un tipo enumerato, ma disporre di una costante e quindi abbiamo creato una enumerazione anonima il cui unico effetto in questo caso È quello di creare una associazione nome-valore all'interno della tabella dei simboli del compilatore.
Il C++ È un linguaggio adatto a qualsiasi tipo di applicazione, in particolare a quelle che per loro natura si devono interfacciare direttamente all'hardware. Una prova in tal proposito È fornita dalla keyword volatile che posta davanti ad un identificatore di variabile comunica al compilatore che quella variabile puÒ cambiare valore in modo asincrono rispetto al sistema:
volatile int Var;
/* */
int B = Var;
/* */
In tal modo il compilatore non ottimizza gli accessi a tale risorsa e ogni tentativo di lettura di quella variabile È tradotto in una effettiva lettura della locazione di memoria corrispondente.
Gli oggetti volatile sono normalmente utilizzati per mappare registri di unitÀ di I/O all'interno del programma e per essi valgono le stesse regole viste per gli oggetti const; in particolare solo funzioni membro volatile possono essere utilizzate su oggetti volatile e non si possono dichiarare volatile costruttori e distruttori (che sono comunque utilizzabili sui tali oggetti).
Si noti che volatile non È l'opposto di const: quest'ultima indica al compilatore che un oggetto non puÒ essere modificato indipendentemente che sia trattato come una vera costante o una variabile a sola lettura, volatile invece dice che l'oggetto puÒ cambiare valore al di fuori del controllo del sistema; quindi È possibile avere oggetti const volatile. Ad esempio unitÀ di input, come la tastiera, sono solitamente mappati tramite oggetti dichiarati const volatile:
const volatile char Byte;
// Byte È un oggetto a sola lettura il
// cui stato varia in modo asincrono
// rispetto al sistema
In taluni casi È desiderabile che una funzione non membro possa accedere direttamente ai membri (attributi e/o metodi) privati di una classe. Tipicamente questo accade quando si realizzano due o piÙ classi, distinte tra loro, che devono cooperare per l'espletamento di un compito complessivo e si vogliono ottimizzare al massimo le prestazioni, oppure semplicemente quando si desidera eseguire l'overloading degli operatori ostream& operator<<(ostream& o, T& Obj) e istream& operator>>(istream& o, T& Obj) (T È la classe cui appartengono i membri privati) per estendere le operazioni di I/O alla classe T. In situazioni di questo genere, una classe puÒ dichiarare una certa funzione friend (amica) abilitandola ad accedere ai propri membri privati.
Il seguente esempio mostra come eseguire l'overloading dell'operatore di inserzione in modo da poter visualizzare il contenuto di una nuova classe:
#include <iostream.h>
class MyClass ;
void MyClass::Func()
// essendo stato dichiarato friend dentro MyClass, il seguente operatore
// puÃ’ accedere ai membri privati della classe come una qualunque
// funzione membro.
ostream& operator<<(ostream& o, MyClass& Obj)
in tal modo diviene possibile scrivere:
MyClass Object;
/* */
cout << Object;
L'esempio comunque risulterÀ meglio comprensibile quando parleremo di overloading degli operatori, per adesso È sufficiente considerare ostream& operator<<(ostream& o, MyClass& Obj) alla stessa stregua di una qualsiasi funzione.
La keyword friend puÒ essere applicata anche a un identificatore di classe, abilitando cosÌ una classe intera:
class MyClass ;
in tal modo qualsiasi membro di AnotherClass puÃ’ accedere ai dati privati di MyClass.
Si noti infine che deve essere la classe proprietaria dei membri privati a dichiarare una funzione (o una classe) friend e che non ha importanza la sezione (pubblica, protetta o privata) in cui tale dichiarazione È fatta.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 753
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved