con cpp.docx · Web viewLa OOP (Object Oriented Programming cioè programmazione orientata agli...

34
Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016 La OOP (Object Oriented Programming cioè programmazione orientata agli oggetti) è la risposta alla crescente complessità dei software richiesta dal mercato. Questa complessità deriva da molteplici fattori: il dover garantire un numero sempre maggiore di funzionalità per rimanere competitivi sul mercato, interfacce utente sempre più sofisticate (touch, vocale, realtà virtuale o aumentata), necessità di fornire versioni del proprio software per diverse piattaforme hardware/software (Windows, Linux , Apple OS, Android su computer desktop, portatile, tablet, smartphone, tv ecc.), capacità di usare servizi remoti tramite le reti informatiche, utilizzo di un'ampia varietà di periferiche (per la memorizzazione di dati esterni, apparecchiature audiovideo, dispositivi portatili ecc.). Questo ha accresciuto la dimensione media misurata in righe di codice sorgente di un applicativo da decine di migliaia a centinaia di migliaia o di milioni (una distribuzione Linux può raggiungere tranquillamente i 200 milioni di righe di codice sorgente). Alcuni strumenti per tenere sotto controllo questa complessità vi sono già noti e mantengono tutta la loro efficacia anche con la OOP : mi sto riferendo a modularità e information hiding. La OOP prosegue su questi binari tentando di rendere più naturale e sicuro rappresentare oggetti complessi (e loro comportamenti) del mondo reale. Alcuni aspetti sono infatti ancora decisamente migliorabili: 1 Separazione dati / funzioni: la rappresentazione di un oggetto del mondo reale sotto forma di variabili che ne definiscono le caratteristiche è separata, sintatticamente, dalle funzioni che ne definiscono il comportamento; pensate ad una struct che rappresenta un cliente ed a qualche funzione in grado di compiere elaborazioni con variabili di quel tipo: nulla lega sintatticamente le variabili alle funzioni; è certamente possibile (e consigliabile) isolare quella struct e le sue funzioni in un sorgente a parte (pensate ai progetti multisorgente) ma si tratta di una organizzazione del codice tutta sulle spalle del programmatore senza supporto da parte del compilatore (per il quale non esiste nel linguaggio un tipo ‘cliente’ ma solo struct e funzioni tra loro slegate) La OOP fornisce invece un supporto sintattico pensato appositamente per descrivere in modo naturale oggetti del mondo reale in modo 1

Transcript of con cpp.docx · Web viewLa OOP (Object Oriented Programming cioè programmazione orientata agli...

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

La OOP (Object Oriented Programming cioè programmazione orientata agli oggetti) è la risposta alla crescente complessità dei software richiesta dal mercato. Questa complessità deriva da molteplici fattori: il dover garantire un numero sempre maggiore di funzionalità per rimanere competitivi sul mercato, interfacce utente sempre più sofisticate (touch, vocale, realtà virtuale o aumentata), necessità di fornire versioni del proprio software per diverse piattaforme hardware/software (Windows, Linux , Apple OS, Android su computer desktop, portatile, tablet, smartphone, tv ecc.), capacità di usare servizi remoti tramite le reti informatiche, utilizzo di un'ampia varietà di periferiche (per la memorizzazione di dati esterni, apparecchiature audiovideo, dispositivi portatili ecc.).

Questo ha accresciuto la dimensione media misurata in righe di codice sorgente di un applicativo da decine di migliaia a centinaia di migliaia o di milioni (una distribuzione Linux può raggiungere tranquillamente i 200 milioni di righe di codice sorgente). Alcuni strumenti per tenere sotto controllo questa complessità vi sono già noti e mantengono tutta la loro efficacia anche con la OOP: mi sto riferendo a modularità e information hiding.

La OOP prosegue su questi binari tentando di rendere più naturale e sicuro rappresentare oggetti complessi (e loro comportamenti) del mondo reale. Alcuni aspetti sono infatti ancora decisamente migliorabili:

1 Separazione dati / funzioni: la rappresentazione di un oggetto del mondo reale sotto forma di variabili che ne definiscono le caratteristiche è separata, sintatticamente, dalle funzioni che ne definiscono il comportamento; pensate ad una struct che rappresenta un cliente ed a qualche funzione in grado di compiere elaborazioni con variabili di quel tipo: nulla lega sintatticamente le variabili alle funzioni; è certamente possibile (e consigliabile) isolare quella struct e le sue funzioni in un sorgente a parte (pensate ai progetti multisorgente) ma si tratta di una organizzazione del codice tutta sulle spalle del programmatore senza supporto da parte del compilatore (per il quale non esiste nel linguaggio un tipo ‘cliente’ ma solo struct e funzioni tra loro slegate)

La OOP fornisce invece un supporto sintattico pensato appositamente per descrivere in modo naturale oggetti del mondo reale in modo completo cioè in termini di caratteristiche e comportamento insieme all’interno di una stessa struttura sintattica: sono LE CLASSI di cui fornirò a breve maggiori dettagli.

2 Difficile riutilizzo del codice: senza OOP non esiste alcun supporto efficace da parte dei linguaggi per rappresentare nel codice nuovi nuovi tipi di oggetti basandosi su quelli già esistenti; sostanzialmente si ricorre ad un copia/incolla del codice per poi procedere ad un adattamento della copia; ma in questo modo si vengono a formare nel tempo ‘N’ versioni modificate del codice originale ; una modifica effettuata per migliorare o correggere il codice originale dovrebbe allora essere riportata sulle N copie: operazione non banale e rischiosa (non è neppure detto che la modifica abbia gli stessi effetti a causa delle differenze tra la base di codice originale e quelle generate con copia/incolla più modifiche). (segue)

1

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Ad esempio potremmo introdurre un tipo cliente_straniero copiando ed adattando nella copia il codice che descriveva un cliente italiano. Da notare come non sia l’unica strada percorribile: potremmo decidere anche di aggiungere nel cliente italiano ciò che serve a gestirlo, se serve, come straniero ma saremmo obbligati ad aggiungere una logica (parametri, if … else, switch… ecc.) a seconda che serva un cliente italiano o uno straniero. Ma il codice diventerebbe così più lungo, più complesso da capire e da manutenere e in poco tempo ingestibile.

Di nuovo, la OOP fornisce un supporto sintattico pensato appositamente per queste situazioni evitando il copia incolla: è il meccanismo dell’EREDITARIETÀ che permette di definire un tipo di oggetto prendendo come modello uno già esistente e indicando in cosa è differente quello nuovo (aggiungendo variabili e/o funzioni, eventualmente con lo stesso nome così che oscurino e sostituiscano quelle dell’oggetto preso a modello. Tornando all’esempio del cliente italiano e straniero potremmo definire il cliente_straniero prendendo a modello quello italiano: il codice di quest’ultimo valido anche per quello derivato non verrebbe duplicato ma condiviso. Viceversa con il copia/incolla il cliente straniero sarebbe descritto con un blocco di codice che duplicherebbe le parti comuni; con l’erditarietà queste parti rimarrebbero in copia unica (unica da modificare, da correggere,migliorare ecc.). Alle variabili del cliente straniero ereditate da quello italiano potremmo aggiungere la nazionalità e sostituire la funzione che calcola il costo finale di un prodotto con una che mantiene lo stesso nome (ed è chiaramente un vantaggio) ma che tiene conto non dell’IVA ma della corrispondente tassa e metodo di calcolo del paese stranierno.

Parlando di ereditarietà si dice che rende possibile programmare per differenze cioè definire nuovi tipi di oggetto che assomigliano ad altri indicando solo in cosa differiscono da quelli vecchi.

3 (parte difficile da capire, direi per voti alti intendendo che si deve studiare anche la parte più dettagliata presentata più in là nella dispensa) difficoltà nell’estendere il codice: idealmente vorremmo aggiungere varianti di tipi di oggetto già gestiti dal nostro applicativo, estendendone le funzionalità senza modifiche al codice già scritto. Questa volta prendiamo ad esempio un CAD: mesi dopo averlo completato dovrebbe essere possibile aggiungere nuovi tipi di figure richieste dai clienti solo aggiungendo le classi che le descrivono; naturalmente questo dovrà essere fatto in modo che la nuova figura esponga le stesse proprietà (colore, tipo tratto, spessore tratto, colore riempimento ecc.) e le stesse funzioni (disegna, sposta, ruota, calcola_area, calcola_perimetro ecc.) di quelle già presenti. A parte l’inserire nel progetto il file sorgente che descrive la classe del nuovo tipo di figura e comandare la ricompilazione del rpogetto non dovrebbe essere necessaria alcuna modifica al codice preesistente: il CAD sapeva come trattare con triangoli, rettangoli, cerchi e pentagoni ed ora sa trattare i nouvi aggiunti esagoni ed eptagoni. (segue)

2

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

(continua) La cosa è MOLTO MENO scontata di quello che sembra. Infatti se non stessimo usando la OOP:

a. dove memorizzare i nuovi tipi di figura senza aggiungere un array, una lista o un altro contenitore per esagoni ed eptagoni? Saremmo costretti a modificare il vecchio codice almeno per definire contenitori adatti ai nuovi tipi di figura. Grazie alla OOP possiamo invece memorizzare oggetti di tipo diverso nello stesso array o lista anche per tipi di figura sconosciute al momento della scrittura del codice! Questo, lo vedremo, sarà possibile nella OOP grazie alla cosiddetta conformità di tipo.

b. il vecchio codice deve essere in grado di differenziare i metodi di calcolo in base ai tipi di figure esistenti: quando chiedo l’area di un triangolo deve richiamare la funzione area per i triangoli, per un cerchio quella dei cerchi e probabilmente lo farà con un if o uno switch sul tipo di figura; ma come può questo stesso codice, senza aggiungere degli else o dei nuovi case allo switch essere capace di gestire nuove figure? E se, grazie alla conformità di tipo tutte le figure sono in uno stesso contenitore (array o lista) quando si invoca una funzione su uno degli elementi come fa il compilatore a sapere se in quella posizione verrà memorizzato un cerchio o un quadrato? Come può decidere in anticipo quale funzione chiamare? Non può. La scelta viene ritardata a run time cioè durante il funzionamento del programma grazie al meccanismo del collegamento ritardato (late o dynamic binding). Anche di questo parleremo in seguito.

Riassumendo, le parole chiave della OOP che ora approfondiremo sono tre: incapsulamento, ereditarietà e polimorfismo .

OOP: incapsulamento ( classi, oggetti, stato interno e metodi)

Con il termine incapsulamento si intende la possibilità di definire in un programma rappresentazioni di oggetti del mondo reale usando strutture linguistiche (le classi) che contengono al loro interno (incapsulati, appunto) sia i dati che descrivono le caratteristiche degli oggetti sia le funzioni (metodi) che corrispondono al comportamento degli oggetti.

Sulla base della precedente potremmo allora definire così una classe: un modello per creare oggetti (esemplari) di un tipo definito dal programmatore descritti dalle stesse caratteristiche (stato interno) e che esibiscono un comportamento definito dai sottoprogrammi (metodi). In pratica viene superata la storica separazione tra strutture dati e sottoprogrammi che le usano.

Ecco qui a lato come si potrebbe definire una classe per rappresentare il concetto di geometrico di cerchio in C++.

All’inizio sono elencate le caratteristiche (dette anche proprietà, properties in inglese): le coordinate del centro e la

3

class Cerchi{

private: int centroX=0, centroY=0, r=0; //raggio

public: Cerchi(int x, int y, int raggio) { centroX = x; centroY = y; r = raggio; }

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

misura raggio che definiscono il cosiddetto stato interno di un oggetto; di seguito i sottoprogrammi (chiamati metodi in OOP) che potranno essere invocati su un oggetto di tipo Cerchi.

I ‘metodi’ con lo stesso nome della loro classe sono speciali e sono chiamati costruttori perché si occupano di operazioni da compiere al momento della creazione di un oggetto di quella classe (maggiori informazioni a breve)

La classe introduce di fatto un nuovo tipo : il programmatore dovrà dichiarare variabili di tipo Cerchi per creare istanze (cioè esemplari) di oggetti di tipo Cerchi; ad esempio per creare una istanza di Cerchi chiamata unCerchio con le coordinate del centro (6,8) raggio lungo 20: Cerchi unCerchio(6,8,20) .

La creazione potrà avvenire anche a run time con allocazione dinamica della RAM con il comando new ed usando una variabile puntatore: Cerchi *unCerchio = new Cerchi(6,8,20); … delete unCerchio;

Il programmatore rispetto ai linguaggi non OOP può con facilità distinguere nel codice la parte che corrisponde alla rappresentazione di un cerchio: il costrutto linguistico class racchiude ogni particolare che definisce un cerchio tra una parentesi graffa aperta ed una chiusa.

(per voti alti) In realtà per meglio gestire progetti multisorgente sarà possibile inserire in un header la classe con lo stato interno e solo i prototipi dei metodi e scrivere in un file separato i metodi completi; questi ultimi avranno però nel loro nome come prefisso il riferimento alla classe (Cerchi::) mantenendo con essa un chiaro legame (e non cambierà nulla per il loro utilizzo rispetto a quelli scritti interamente alla classe):

main#include <iostream>#include "cerchi.h"using namespace std;

int main(){ Cerchi c(6,8,20); return 0;}

cerchi.hclass Cerchi{ private: int centroX=0, centroY=0, r=0;

public: //qui solo prototipi; Cerchi(int x, int y, int raggio); double Area(); };NB: qui dovremmo usare un ‘include guard’; cerca su Internet per sapere cosa sono e perché servono

cerchi.cpp#include "cerchi.h"

Cerchi::Cerchi(int x, int y, int raggio){ centroX = x; centroY = y; r = raggio;}

double Cerchi::Area(){ return 3.14 * r * r;}

Le classi non sono gli oggetti e viceversa …Una classe da sola non serve a niente, come del resto non servirebbe a niente il tipo int da solo. E’ necessario creare esemplari di oggetti di una classe (cioè variabili di tipo di quella classe), allo stesso modo in cui è necessario definire variabili int per lavorare con numeri interi.

DEFINIZIONE. Un oggetto è un ben preciso esemplare (istanza) di una classe.

Il programmatore definisce una classe e crea sulla base di essa tutte le istanze che ritiene necessarie. Definisce ad esempio la classe ‘Cane’ e crea (istanzia) tanti oggetti di quella classe quanti sono i cani di cui ha bisogno nel suo programma. Quindi non confondete: la classe è il modello, gli oggetti sono gli esemplari che si basano su quel modello.

4

class Cerchi{

private: int centroX=0, centroY=0, r=0; //raggio

public: Cerchi(int x, int y, int raggio) { centroX = x; centroY = y; r = raggio; }

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Nella terminologia OOP i sottoprogrammi di una classe sono chiamati metodi. La funzione Area dei cerchi è quindi l’unico metodo definito per la classe Cerchi. Metodi di classi diverse potrebbero avere lo stesso nome. Ad esempio potremmo aggiungere nel programma la classe dei Triangoli ed anche per questi ultimi decidere di scrivere un metodo chiamato Area. Il compilatore non farebbe confusione: dal tipo dell’oggetto saprebbe quale metodo mandare in esecuzione. In generale quando ci si riferisce alle variabili dello stato interno o ai metodi si parla di membri della classe.

CostruttoriCaratteristi che li rendono ‘speciali’:

a) come nome hanno lo stesso della classe

b) non deve essere indicato un tipo restituito (neppure void) Esempio (ripreso dalla classe Cerchi poco sopra) class Cerchi { public: Cerchi(…); // COSTRUTTORE: nessun tipo restituito; stesso nome della classe

c) se è presente almeno un costruttore non è possibile creare un oggetto senza invocarne uno; ne deriva che se nel costruttore vengono messe tutte le istruzioni importanti per l’inizializzazione di un oggetto sarà impossibile ritrovarsi a lavorare con un oggetto mal formato; se invece l’inizializzazione avvenisse con un metodo normale il programmatore potrebbe anche dimenticarsi di chiamarlo; perciò i costruttori aumentano di molto l’affidabilità del codice

I costruttori sono spesso più d’uno sia per dare la possibilità creare un oggetto con diverse liste di parametri a seconda dei dati che si hanno a disposizione; ad esempio una classe per rappresentare date potrebbe avere un costruttore che riceve giorno, mese ed anno come tre interi ed un altro che riceve la data come una singola stringa (“12-05-2016”):class dataCalendario { private: int giorno, mese_, anno_; dataCalendario(int giorno, int mese, int anno) {…} //creazione con tre numeri interi dataCalendario(string dataComeStringa) {…} //data passata come stringa … }

OverloadingQuando sono presenti più costruttori si parla di overloading dei costruttori nel senso che si sta sovraccaricando (overload) il significato di un identificatore (quello dei costruttori) con versioni caratterizzate da diverse configurazioni dei parametri. Non possono ad esempio esistere due costruttori che entrambi ricevono due interi.

Il concetto di overloading si applica allo stesso modo anche ai metodi che non sono dei costruttori ed anche alle funzioni esterne ad una classe. Ad esempio potremmo avere più metodi(o funzioni non contenute in una classe) con lo stesso nome ‘converti_in_data’ che si occupano di generare un oggetto di tipo Data a partire da diversi tipi di altro oggetto:

5

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Data converti_in_data(int n) //n = numero secondi a partire da una data di riferimentoData converti_in_data( char v[] ) //v è una stringa C terminata da nullData converti_in_data(int giorno, string mese, int anno) // 12 Settembre 2016

Si possono avere quanti overload di un metodo si vogliono a patto che ciascuno sia distinguibile dagli altri nella lista dei parametri (diversamente il compilatore non saprebbe usare in una chiamata). Perciò i due seguenti overload di costruttore sono incompatibili:

Data(int giorno, int mese, int anno) //per data italiana Data(int anno, int mese, int giorno) //per data anglosassone

entrambi hanno tre interi come parametri (il nome non conta ma solo il tipo, il numero e la sequenza) ed il compilatore non saprebbe quale usare nella chiamata Data(12, 4, 30)

Costruttore vuoto (di default)Quando il programmatore non dota una classe di alcun costruttore il compilatore si comporta come se ne fosse stato scritto uno minimale senza parametri detto di default o vuoto. Questo è necessario perché la fase di creazione di un oggetto per uniformità deve sempre prevedere dopo l’allocazione della memoria per lo stato interno il richiamo di un costruttore (ricordate? nessun oggetto può essere creato senza passare per un costruttore) anche se il costruttore di fatto in questo caso non fa nulla! Per i seguenti esempi si fa riferimento ad una classe C senza costruttori:

C x = C(); //x viene creato usando il costruttore vuoto della classe CC x(); //equivalente a C x = C();C x; //equivalente a C x = C();

NB: dal momento in cui ad una classe viene aggiunto un costruttore il compilatore non aggiungerà più in automatico il costruttore di default e gli esempi precedenti produrrebbero un errore; ed è giusto che sia così se ci pensate: se il programmatore aggiunge un costruttore significa che ritiene molto importante che venga eseguito quel codice; se rimamesse quello di default automatico si potrebbero creare oggetti non inizializzati come previsto dal programmatore; se quest’ultimo vuole mantenere la possibilità di usare un costruttore vuoto dovrà scriverne uno lui in modo esplicito.

Alcuni errori tipici da evitareCerchi.raggio = 10 è un uso sbagliato: infatti Cerchi è il nome della classe e non di un oggetto; in generale è sempre necessario infatti indicare prima del punto una variabile di quella classe.

centroX = 100 Anche usare il nome di una variabile dello stato interno senza mettere davanti l’identificatore dell’oggetto è sbagliato: non si saprebbe di quale istanza (di quale cerchio) si vuole modificare la coordinata X del centro (ammesso che lo si possa fare, che cioè centroX sia dichiarata public anche se abbiamo sottolineato come questa sia una pratica assolutamente da evitare!).

6

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Costruttori ‘delegate’ (delegati) ovvero costruttori che ne richiamano altriQuando sono presenti più costruttori essi di solito condividono buona parte de loro codice ed è logico se ci pensate: se alcune istruzioni sono importanti in un costruttore per la corretta partenza di un oggetto le stesse dovrebbero essere comandate da tutti i costruttori. Spesso la differenza tra i costruttori risiede solamente nella lista dei parametri che ricevono per cui ci si ritroverebbe con N copie di uno steso blocco di istruzioni eccetto qualche semplice operazioni sui parametri ricevuti. Ad esempio (sempre per la classe Cerchi):

Ho aggiunto il colore con cui va disegnato il cerchio: è la stringa colore nello stato interno.

Di solito tra i costruttori ce n’è uno che riceve più parametri rispetto agli altri: nel nostro esempio è il primo che riceve le coordinate del centro, la misura del raggio ed il colore.

Immaginando che sia utile ( onestamente è poco verosimile ma giusto per capire…) poter creare comodamente circonferenze centrate nell’origine e di colore rosso ho aggiunto un secondo costruttore che riceve solo la misura del raggio: infatti se le circonferenze sono centrate nell’orgine si sa che le due coordinate sono per forza zero; anche il colore si sa che è rosso e quindi sarebbe davvero inutile passare questi dati come parametri.

Infine (sempre di dubbia reale utilità…) un terzo costruttore che rispetto al secondo permette di scegliere il colore (ma x ed y saranno sempre 0).

COSA SI NOTA? che le istruzioni dei metodi e la logica delle loro operazioni è praticamente identica (assegnamenti alle variabili dello stato interno). Eppure sono state ripetute per ben tre volte (di più probabilmente se dovessimo aggiungere altri costruttori). Oltre allo spreco di spazio e alla minore leggibilità del codice della classe si aggiunge una potenziale inefficienza nel caso una qualunque modifica venisse apportata perché dovremmo riportarla su tutti i costruttori.

C++ 11 introduce il meccanismo per il quale un costruttore può chiamarne altri prima di iniziare ad eseguire le sue istruzioni specifiche: in questo modo si evita di duplicare le istruzioni svolte dal costruttore richiamato detto anche delegato (o target). Ecco come apparirebbe la classe delegando praticamente la totalità delle operazioni al primo costruttore:

7

class Cerchi{

private: int centroX=0, centroY=0, r=0; //raggio string colore="black"; public:

//costruttore più 'completo' Cerchi(int x, int y, int raggio, string col) { centroX = x; centroY = y; r = raggio; colore = col;} //costruttore specializzato nel creare //cerchi centrati nell'origine Cerchi(int raggio) { centroX = 0; centroY = 0; r = raggio; colore = "rosso";} //costruttore specializzato nel creare //cerchi centrati nell'origine Cerchi(int raggio, string col) { centroX = 0; centroY = 0; r = raggio; colore = col;} double Area() { return 3.14 * r * r; } };

class Cerchi{

private: int centroX=0, centroY=0,

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Quando si crea un cerchio usando il secondo costruttore si fornisce come parametro solo il raggio (coordinate e colore prefissati); si richiama allora il primo costruttore indicando (0, 0, r, “rosso”); è importante sottolineare che il la delega può avvenire solo se si è in grado di fornire tutti i parametri previsti per il costruttore che si vuole richiamare. Nota: tutte le duplicazioni di codice sono state eliminate; eventuali altre operazioni da inserire in ogni costruttore potranno essere scritte solo nel primo ed automaticamente richiamate; gli errori o miglioramenti per le parti di codice condivise comporteranno modifiche solo nel primo costruttore.

NOTA: tutto questo significa che quando un costruttore ne delega un altro non conterrà alcuna sua istruzione? No: conterrà solo eventuali istruzioni più specifiche da eseguire solo quando si usa quel particolare costruttore; ad esempio il delegante potrebbe ricevere un parametro non previsto tra quelli del delegato e quindi le istruzioni che usano questo parametro in più non potrebbero essere scritte nel delegato (non potremmo inviare il parametro!) ma rimarranno nel delegante.

Metodi ‘getter’ e ‘setter’Poiché è importante mantenere private le variabili dello stato interno se è necessario accedere al loro valore o consentire la modifica del lorovalore dall’esterno della classe l’unico modo è aggiungere metodi public che svolgano questi compiti.

Qui a lato un metodo che restituisce all’esterno della classe il valore della variabile raggio. Metodi come questo sono chiamati getter dal verbo ‘to get=prendere’.

NB: in questo caso il metodo getter si limita ad un return immediato della variabile ma in altre situazioni potrebbe svolgere controlli critici o altre operazioni importanti che potrebbero essere dimenticate da un programmatore se si potesse accedere ad una variabile public. Ad esempio pensiamo ai dati bancari di un conto corrente: la richiesta di informazioni potrebbe essere gestita da un ‘getter’ che prima identifica il richiedente e che allo stesso tempo registra su un file la data è l’identità di chi ha richiesto il dato:

E’ evidente come qui l’uso del getter va ben oltre il solo compito di

8

class Cerchi{

private: int centroX=0, centroY=0, r=0;

public: Cerchi(int x, int y, int raggio) { centroX = x; centroY = y; r = raggio; }

double Area() { return 3.14 * r * r; }

int get_raggio() {return raggio;}

};};

class contoCorrente{ private: double saldo; … altro …

public: … altro … double getSaldo_Conto(string user, string psw) { registra_dati_accesso(user, psw, file_log); if (! ha_privilegi(user, psw) ) { segnala_accesso_non_autorizzato(); return -999999999;}

class Cerchi{

private: int centroX=0, centroY=0,

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

restituire un valore: aumenta in modo drastico l’affidabilità del codice; in nessun modo un programmatore distratto potrà causare un accesso non autorizzato ad un dato importante.

9

class contoCorrente{ private: double saldo; … altro …

public: … altro … double getSaldo_Conto(string user, string psw) { registra_dati_accesso(user, psw, file_log); if (! ha_privilegi(user, psw) ) { segnala_accesso_non_autorizzato(); return -999999999;}

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

A maggior ragione anche la modifica di una variabile privata deve avvenire tramite un metodo pubblico questa volta definito setter (dal verbo ‘to set’ = impostare):

Se fosse possibile modificare direttamente la variabile health qualche programmatore potrebbe dimenticarsi di avviare le animazioni previste guando un personaggio guadagna o perde salute…

Essendo costretti ad usare il metodo set_health questo è semplicemente impossibile

E’ talmente importante e comune servirsi di getter e setter che quasi tutti gli editor per linguaggi OOP offrono la possibilità di generarli in automatico per tutte le variabili dello stato interno. Ovvio che il codice generato in questo modo si limiterà all’essenziale (il return per i getter e l’assegnamento alla variabile usando il parametro in ingresso per i setter). Con code::blocks si può usare il wizard File / New / Class.

Il puntatore speciale thisOgni metodo riceve in modo implicito (cioè ‘nascosto’) un puntatore al suo oggetto (quello sul quale è stato invocato):

Cerchi c1(…);c1.Area();

Il metodo Area è richiamato sull’oggetto c1 ed il compilatore gli invia un parametro in più corrispondente all’indirizzo di c1 (ovviamente questo avviene durante il processo di traduzione ed il codice scritto da noi non è modificato). A questo parametro viene assegnato un identificatore speciale: this. Quindi ogni qual volta in un metodo si ha bisogno dell’indirizzo dell’oggetto è possibile usare this.

10

class monster{ private int health; //livello salute del mostro … altro … public: … altro … void set_health(int livello) { if (livello <= 0) animazione_morte(); if (livello > health) animazione_potenza(); else animazione_debolezza(); health=livello; }

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Il puntatore this torna utile in diverse situazioni:

- quando un metodo deve richiamarne un altro (di un’altra classe ovviamente) e deve passargli il suo indirizzo; ad esempio ad un metodo che costruisce una lista con l’inserimento in testa (come ricorderete bisogna passare alla ins_testa l’indirizzo dell’oggetto da aggiungere): x.metodo_da_chiamare(this, …)

- come sopra ma quando a dover essere passato è una copia dell’oggetto: x.metodo_da_chiamare(*this, …) se this è l’indirizzo, l’oggetto è *this

- quando il metodo deve terminare restituendo l’indirizzo dell’oggetto: return this. - quando il metodo deve terminare restituendo una copia dell’oggetto: return *this;- quando nei costruttori si vuole dare ai parametri gli stessi identici nomi delle variabili dello stato

interno ed è necessario distinguerli:

Dobbiamo leggere il codice in questo modo: assegna alla variabile centroX di questo (this) oggetto il valore del parametro centroX.

Ricordiamoci che se this punta ad una struct (o una classe) per accedere agli oggetti interni si usa la sintassi puntatore->

Il compilatore diversamente non saprebbe cosa assegnare a cosa se scrivessimo semplicemente:

Non sarebbe infatti chiaro se si vuole assegnare il valore del parametro ricevuto alla variabile dello stato interno o viceversa …

Naturalmente si può anche decidere di usare idendificatori simili, ma non identici, per i parametri come visto in precedenza.

Oppure (linee guida per i programmatori di Google) far terminare il nome delle variabili dello stato interno con un underline:

11

class Cerchi{ private: int centroX=0, centroY=0, r=0;

public: Cerchi(int centroX, int centroY, int r) { this->centroX = centroX; this->centroY = centroY; this->r = r; }

class Cerchi{ private: int centroX=0, centroY=0, r=0;

public: Cerchi(int centroX, int centroY, int r) { centroX = centroX; ??? centroY = centroY; ??? r = r; }

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

centroX_, centroY_, r_

Information Hiding con la OOPCome si diceva la OOP adotta ed anzi rinforza il rispetto del principio dell’oscuramento delle informazioni. Infatti ogni variabile ed ogni metodo sono in partenza inaccessibili (privati, private) dall’esterno di una classe a meno che non li si indichi pubblici (public).

Ecco un esempio di codice che tenta di accedere a una variabile privata e che genererebbe un errore da parte del compilatore (fate riferimento alla classe Cerchi dichiarata poco sopra):

Cerchi c(6,8,20); //creazione di un oggetto invocando il costruttore /* il costruttore in questo caso si limita a memorizzare i tre valori nelle corrispondenti variabili dello stato interno ma avrebbe potuto scriverlo con controlli od altre operazioni importanti prima di poter iniziare ad usare l’oggetto. */

c.r = 38; //ERRORE: LA VARIABILE RAGGIO E’ INACESSIBILE (private)cout << c.Area(): //OK il metodo Area è accessibile (public)

Una classe ben progettata dovrebbe rendere visibile solo una piccola parte dei metodi (che definiscono la sua interfaccia con l’esterno) e mantenere privato tutto lo stato interno e altri metodi ‘di supporto’ usati internamente e che non dovrebbero esser richiamati dall’esterno. Questo per almeno tre ottimi motivi (gli stessi incontrati nella parte della dispensa dedicata ai progetti multisortente ed alla programmazione modulare:

1 Un programmatore che deve servirsi di una certa classe non è costretto a conoscere i (potenzialmente numerosissimi) dettagli implementativi interni come il significato di ogni variabile, l’uso corretto di ogni metodo coordinato con gli altri ecc.; solo l’autore della classe dovrebbe confrontarsi con questi particolari ‘al completo’. Una buona classe è come un macchinario estremamente sofisticato e complesso che un operatore può però comandare con una semplice console di comando. Pensate alla classe Vector (vettore dinamico) che abbiamo già avuto modo di usare molto tempo prima di parlare di OOP proprio per la semplicità d’uso; non sappiamo come sia organizzata internamente la classe ma i comandi che ci permettono di usarla sono pochi e facili da comprendere. Potremmo dire che il programmatore è protetto dalla complessità della classe.

2 E’ molto più difficile che un programmatore commetta errori perché potrà usare solo i metodi pensati dall’ideatore della classe che a loro volta si occuperanno di richiamare nella giusta sequenza e nelle giuste condizioni tutti gli altri, senza dimenticanze. Esempio: se per una classe che rappresenta una console di comando di un macchinario industriale fosse possibile modificare direttamente una variabile dello stato interno potremmo non sapere o dimenticarci che se cambia quel valore è indispensabile fare prima e/o dopo alcune cose critiche diversamente mettendo a rischio il funzionamento della macchina o peggio. Se l’unico modo dall’esterno per modificare quel parametro è invece tramite una chiamata ad un metodo che in automatico fa tutte le cose necessarie è semplicemente impossibile commettere errori nell’uso

12

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

di un oggetto di quella classe. In questo caso potremmo invece dire che è la classe che è protetta da usi scorretti da parte dei programmatori.

13

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

3 A patto di lasciare immutata l’interfaccia esposta è possibile modificare (correggere, adattare, migliorare o addirittura sostituire completamente) la parte privata di una classe senza essere costretti a modificare il codice già scritto che usava la classe prima delle modifiche.

Potremmo ad esempio decidere di migliorare drasticamente le prestazioni di una classe sostituendo un metodo privato di ordinamento cambiando l’algoritmo usato con un altro ideato dopo la scrittura originale della classe; se questa modifica non tocca l’inerfaccia (i nomi dei metodi e i parametri che ricevono sono invariati) allora il codice già scritto che usava la classe con il vecchio algoritmo non si accorge (se non per il miglioramento delle prestazioni) della sostituzione dell’algoritmo.

Oppure decidere di cambiare il formato di memorizzazione interno di una data da un'unica stringa (“12-09-2016”) a tre interi (giorno, mese ed anno). Se la stringa della data era privata nessun codice accedeva ad essa direttamente ma tramite un getter e sarà allora sufficiente modificare quest’ultimo in modo che il codice già scritto (che si aspetta dal getter una stringa) continui a funzionare senza modifiche; per così dire non si accorge delle modifiche allo stato interno:

main

Data x(…);

cout << x.getData;

Vecchia classe Dataclass Data{ Private: string d; Public: string getData() {return d;}

Nuova classe Dataclass Data{ Private: int g, m, a; //cambio stato interno! Public: string getData() //ma restituisco sempre string {return to_string(g) + “-“ + to_string(m) + “-“ + to_string(a); }

Commento: prima del cambio dello stato interno della classe data il main si aspetta di ricevere come valore dal getter la stringa con la data; dopo la modifica dello stato interno che sostituisce la stringa d con i tre interi g, m e a è sufficiente modificare il getter in modo che costruisca la stessa stringa che restituiva prima: il main letteralmente non se ne accorge e nessua modifica è quindi richiesta al codice che usava la classe con il vecchio stato interno

Se invece il mani avesse potuto usare direttamente la variabile d dello interno (maldestramente lasciata public) e comandasse cout << x.d chiaramente questo comando dopo la modifica non funzionerebbe più (la variabile d è addirittura sparita dalla stato interno!) e sarebbero necesari adattamenti anche al codice esterno alla classe

14

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

L’ereditarietà

E’ quel meccanismo sintattico che consente di creare nuove classi (dette figlie, derivate, o sotto classi) prendendone altre a modello (dette madri, antenate, super classi); una classe figlia eredita quasi tutto dalla madre (variabili dello stato interno, metodi che non siano costruttori) senza bisogno di copia/incolla. Nell’esempio che segue viene derivata la classe dei quadrati da quella più generale dei rettangoli (il quadrato è un rettangolo con i lati uguali). conseguire

class Rettangoli{ protected: int base=0, altezza=0;

public: Rettangoli() {}

Rettangoli(int _base, int _altezza) {base=_base; altezza=_altezza;}

int area() {return base*altezza;}

int perimetro() {return 2*(base+altezza);}

}

classe Quadrati: public Rettangoli{ private: //sufficiente cio' che eredita

public: Quadrati() {} Quadrati(int lato) : Rettangoli(lato, lato) {}

int perimetro() {return 4*base;} }

Nuovo livello di accessibilità (protected)Il livello di accesso dello stato interno dei rettangoli è diventato protected che concede l’accesso diretto (senza essere costretti ad usare getter e setter quindi) a queste variabili da parte dei metodi della classe figlia. La stessa possibilità esiste anche per i metodi. E’ un privilegio che il progettista concede, se crede, in modo da lasciare più libertà al codice della classe figlia; è abbastanza logico: la classe figlia dovrà differenziarsi per essere utile e se avesse tutte le strade bloccate sarebbe più difficile. Nel nostro esempio il metodo perimetro dei quadrati dovrà accedere alla variabile ‘base’ dei rettangoli. E’ comunque possibile mantenere un blocco private e cambiare in protected lo stato solo di alcune variabili o metodi

Livello di accesso delle parti ereditateNel dichiarare la classe dei quadrati abbiamo indicato che deriva pubblicamente (: public) dai Rettangoli; questo significa che i Quadrati il livello delle parti ereditate: i membri public ereditati rimangono public, quelli protected rimangono protected ed i private rimangono private.Indicando :protected le parti pubbliche diventano protected e le parti protected e private rimangono tali.Indicando :private tutte le parti ereditate diventano private.

15

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Delega ai costruttori della classe madreUn costruttore della classe figlia può (anzi è di solito una buona pratica) delegare uno dei costruttori della classe madre con l’idea per prima cosa di inizializzare correttamente la parte ereditata; il meccanismo è identico a quello visto per la delega ad altri costruttori della stessa classe.

Nota: non si può delegare contemporaneamente un costruttore della stessa classe ed uno della classe madre.

Nota: quando si crea un oggetto di una classe derivata il compilatore DEVE poter sempre chiamare un costruttore della classe madre. Ecco perché è necessario in situazioni di ereditarietà aggiungere nelle classi il costruttore di default in modo esplicito.

Come la classe figlia può differenziarsi rispetto alla madre

- nuove variabili nello stato interno- nuovi metodi- nuove variabili e metodi con lo stesso nome di variabili o metodi di altri già presenti nella classe

madre;

In questo caso (se sono public o protected) nella classe figlia gli elementi della classe madre con lo stesso nome sarebbero di per sé oscurati ma ci si può comunque riferire a quelli della classe madre con la sintassi: nomeClasseMadre::variabile/metodo.

Ad esempio se la classe Rettangoli dell’esempio precedente avesse una variabile int n; e la classe figlia avesse una variabile con lo stesso nome string n; (avrebbe potuto essere int anch’essa senza problemi) scrivendo in un metodo della classe figlia semplicemente ‘n’ si farebbe riferimento alla stringa mentre con ‘Rettangoli::n’ si farebbe riferimento all’int della classe madre. Lo stesso discorso vale per i metodi.

Conformità di tipoE’ la proprietà per cui è possibile memorizzare oggetti di classi figlie in variabili di tipo corrispondente ad una classe madre. Ad esempio se ‘d’ è una variabile di tipo Rettangoli allorà potrà memorizzare riferimenti ad oggetti di classe Quadrati. Questo particolare consente di memorizzare oggetti di tipo diverso, ma tutti di una classe derivata da una stessa madre in unico array o lista (superando il limite degli array tradizionali dove tutti gli elementi devono essere dello stesso tipo). Immaginiamo infatti una classe Figure madre di Rettangoli, Cerchi e Triangoli (nella classe figure avremo concentrato tutte le variabili comuni ed alcuni metodi che le usano in modo da non ripetere il codice in ciascuna delle classi figlie); in uno stesso array di tipo Figure grazie alla conformità di tipo potremo memorizzare un mix di Rettangoli, Cerchi e Triangoli. Senza la conformità avremmo dovuto gestire N array, uno per ciascun tipo di figura.

16

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Late (detto anche Dynamic) Binding e polimorfismoSono le ultime interessanti caratteristiche della OOP che prenderemo in considerazione. Tornando all’esempio precedente è logico supporre che ciascuna classe dei diversi tipi di figura abbia dei metodi con lo stesso nome; ad esempio perimetro() le cui istruzioni ovviamente cambiano per ogni tipo di figura. Immaginiamo poi di aver memorizzato 100 figure miste in modo casuale in un array di tipo Figure:

Figure disegno[100];for(…){ int tipo=rand()%3; if (tipo==0) disegno[i] = Rettangoli(...); else if (tipo==1) disegno[i] = Cerchi(...); else if (tipo==2) disegno[i] = Triangoli(...); ecc.

La casualità vuole simulare l’ovvia imprevedibilità delle azioni di un utente che sta realizzando un disegno. La conformità di tipo ha consentito di memorizzare tutte le figure in unico contenitore (l’array disegno) e questo è certamente un grosso passo avanti. Ma se volessimo calcolare il consumo di inchiostro per la stampa del disegno e volendo quindi calcolare la somma dei perimetri delle figure da tracciare e scrivessimo:

//calcolo somma perimetridouble somma_perimetri=0;

//NB NB: moltiplicando anche per 1000 il numero dei tipi di figure diverse// le istruzioni rimango esattamente le stesse!!for(int i=0; i<n_figure; i++) somma_perimetri += disegno[i].perimetro();

Il compilatore al momento della traduzione non può sapere che tipo di figura conterrà disegno[i]! Sono estratte a sorte a programma funzionante (a run time, come si dice). Non può fare altro allora che ragionare in modo statico (a programma fermo, durante la compilazione insomma) in base al tipo dell’array. L’array disegno è di tipo Figure quindi il metodo perimetro verrebbe cercato in questa classe senza richiamare il giusto metodo di calcolo. Questa modalità di scegliere da parte del compilatore il metodo da invocare, che va benissimo normalmente, è chiamata static binding (collegamento statico) o early binding (collegamento anticipato).

Sarebbe però interessante che il programma ‘riconoscesse’ a run time, cioè a programma funzionante, il tipo di figura esatta in disegno[i] e chiamasse il metodo perimetro di quella figura. Questo può essere fatto attivando una diversa modalità di collegamento chiamata dynamic binding (collegamento dinamico) o late binding (collegamento ritardato). NOTA: in C++ questo richiede che gli oggetti siano creati con il comando new allocando quindi dinamicante la RAM necessaria ed usando i puntatori.

17

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Le modifiche alle classi sono poi limitatissime e molto semplici:

- nella classe madre deve essere presente un prototipo (blocco istruzioni vuoto quindi) dei metodi coinvolti marchiato con la parola chiave virtual; i parametri saranno quelli previsti per tutti i metodi con lo stesso nome presenti nelle classi figlie:

class Figure{

protected: … variabili comuni da far ereditare alle classi figlie …

public: virtual double perimetro() {}; virtual double area() {};

};

In ciascuna delle classi figlie i metodi ‘veri’ corrispondenti a quelli virtuali saranno invece marchiati con la parola chiave override:

class Rettangoli : public Figure{ protected: … public: … double perimetro() override { return ( 2*(b+h)); }

double area() override { return b*h; }};

Ora sarà possibile richiamare con successo il metodo perimetro() : come per magia il programma saprà quale versione scegliere a seconda della figura che troverà in quel momento in quella posizione dell’array:

Figure *disegno[100]; //cento puntatori a Figurefor(…){ int tipo=rand()%3; if (tipo==0) disegno[i] = new Rettangoli(...); else if (tipo==1) disegno[i] = new Cerchi(...); else if (tipo==2) disegno[i] = new Triangoli(...); ecc.

18

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

//calcolo somma perimetridouble somma_perimetri=0;

//NB NB: moltiplicando anche per 1000 il numero dei tipi di figure diverse// le istruzioni rimango esattamente le stesse!!for(int i=0; i<n_figure; i++) somma_perimetri += disegno[i]->perimetro();

Il termine polimorfismo indica appunto la possibilità di metodi (perimetro, area in questo caso) con la stessa identica signature (stesso tipo, stesso nome, stessi parametri) richiamabili da una stesso tipo di oggetto (Figure) ma ciascuno con un comportamento diverso a seconda della reale natura dell’oggetto. Polimorfo = molte forme

Senza questi meccanismi avremmo dovuto scrivere MOLTO più codice ed avere molti array da da gestire, ciascuno con il suo contatore:

Segmenti v_segmenti[100];Cerchi v_cerchi[100];Rettangoli v_rettangoli[100];Quadrati v_quadrati[100];int n_segmenti=0, n_cerchi=0, n_rettangoli=0, n_quadrati=0;

//calcolo somma perimetridouble somma_perimetri;

for(int i=0; i<n_segmenti; i++) somma_perimetri += v_segmenti[i].perimetro();

for(int i=0; i<n_cerchi; i++) somma_perimetri += v_cerchi[i].perimetro();

for(int i=0; i<n_rettangoli; i++) somma_perimetri += v_rettangoli[i].perimetro();

for(int i=0; i<n_quadrati; i++) somma_perimetri += v_quadrati[i].perimetro();

Non solo: con il polimorfismo potremmo aggiungere altri 50 tipi di figure SENZA modificare una riga nel calcolo della somma dei perimetri; senza polimorfismo altri 50 for! E’ davvero evidente come ereditarietà, conformità di tipo e late binding consentano come promesso di programmare per differenze ed estendere con semplicità un progetto semplicemente aggiungendo nuove classi e ricompilando.

19

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

LA PARTE DELLE DISPENSE CHE SEGUE NON E’ DA STUDIARE

Allocazione statica e dinamica degli oggetti e restituzione di oggetti da parte dei metodi

Fino ad ora abbiamo definito variabili oggetto di una classe senza porci il problema di come e da chi venga allocata la RAM necessaria a memorizzare i dati di un oggetto. Quando gli oggetti sono creati SENZA usare il comando new la RAM necessaria agli oggetti viene allocata automaticamente in un’area della RAM definita STACK. E’ un’area che non è nella disponibilità del programmatore ma gestita in automatico; è la stessa area in cui sono depositati i parametri passati ad una funzione e le variabili locali ed ha due caratteristiche fondamentali:

4 il suo uso è efficiente (per le istruzioni in linguaggio macchina che si possono usare che sfruttano uno schema di indirizzamento della RAM semplificato)

5 il suo è sicuro: non appena la funzione/metodo termina la sua esecuzione questa memoria viene recuperata dal sistema; quindi non possono verificarsi quei problemi tipici (uso di puntatori non validi, leaks di memoria) della gestione dinamica della RAM

Esempio:

void test() { Cerchi c(8); //RAM allocata quando la funzione inizia la sua esecuzione … … } //la RAM per l’oggetto c viene recuperata in automatico dal sistema

IMPORTANTE Questo implica che il main o in un'altra funzione che invoca la funzione test() NON PUO’ accedere all’oggetto c perché non esiste più.

Allocazione dinamica – HEAPQuando invece viene usato il comando new la memoria viene allocata in un’area definita HEAP (mucchio) e rimane allocata (finchè non viene esplicitamente restituita con delete/delete[] dal programmatore) anche dopo il termine dell’esecuzione della funzione che l’ha allocata (la funzione deve però restituire l’indirizzo dell’oggetto o tramite un parametro per indirizzo o con il return.

Analizziamo ora alcuni metodi corretti per rispondere a questa necessità.

Metodo 1 (oggetto sullo stack): restituzione di una COPIA dell’oggetto:

Cerchi test() { Cerchi c(8); … return c; }

20

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Commento: la funzione ha come tipo la classe dell’oggetto da restituire (Cerchi). Il return restituisce una copia al chiamante:

.. main …

Cerchi cerchio = test(); //viene creato un oggetto copia di quello restituito dalla funzione test()

Nota: anche il main non deve preoccuparsi di restituire la RAM per l’oggetto cerchio: lo sarà automaticamente alla fine dell’esecuzione del main

Metodo 2 (oggetto sullo stack): tramite un parametro per indirizzo:

void test(Cerchi &risultato) { Cerchi c(8); … risultato = c; }

.. main …

Cerchi cerchio;test( cerchio );

21

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Metodo 3 (oggetto sullo heap): creare dinamicamente l’oggetto e restituendo un puntatore ad esso

Cerchi *test() { Cerchi *c = new Cerchi(8); … return c; }

.. main …

Cerchi *cerchio;cerchio = test(); //si memorizza il puntatore restituito…delete cerchio;

Copia degli oggetti, copy constructor (cenni)

Negli esempi n. 1 e n. 2 abbiamo queste due istruzioni:

Cerchi cerchio = test(); //dall’esempio 1 … risultato = c; //dall’esempio 2

Nella prima i dati restituiti da test() vengono copiati Per l’ultimo assegnamento sarebbe possibile definire il cosiddetto copy constructor cioè un metodo che specifica come correttamente fare la copia di un oggetto di una classe per assegnarla ad una variabile dello stesso tipo di classe. Questo ha un senso quando non è sufficiente fare una semplice copia di tutti i byte in memoria associati ad un oggetto per farne un duplicato. Pensate infatti ad una classe che rappresenta una fotografia e che al suo interno ha un puntatore ad un grosso blocco di byte che rappresentano i pixel della fotografia; fare la semplice copia dei byte dello stato interno di un oggetto di questa classe significherebbe copiare il puntatore ma non duplicare il blocco dei byte! Quindi avremmo due oggetti fotografia che entrambi punterebbero allo stesso blocco di pixel; modificando la fotografia per il primo lo faremmo anche per il secondo. Se non è quello che si vuole allora dovremmo definire come portare a termine correttamente la copia per un oggetto di questa classe: nel codice di questo ‘costruttore copia’ allocheremmo un altro blocco di byte per la copia della fotografia per il secondo oggetto e copieremmo in esso i byte della fotografia del primo e nel puntatore del secondo oggetto metteremmo l’indirizzo di questo secondo blocco. Ora i due oggetti sarebbero indipendenti: modificare la fotografia di uno non cambierebbe anche quella dell’altro. Per il momento tralasceremo però questi costruttori ‘speciali’ e ci limiteremo a quelli che variano solo per la sequenza dei loro parametri in ingresso.

22

Aspetti teorici dela OOP e implementazione con C++ Versione 14-04-2016

Oggetti costruiti con altri (da fare)

23