la programmazione orientata agli oggetti - itis magistri cumacini

magistricumacini.it

la programmazione orientata agli oggetti - itis magistri cumacini

1

LA PROGRAMMAZIONE ORIENTATA AGLI OGGETTI

(© I.D. Falconi ~ 24.01.09)

INTRODUZIONE

La programmazione orientata agli oggetti (OOP) rappresenta una modalità di programmazione

strutturata attraverso la quale il problema da risolvere viene affrontato rappresentando la realtà come

un insieme di elementi (oggetti) interagenti fra loro, ciascuno in grado di compiere una serie di azioni

specifiche.

Ad esempio, supponiamo di voler progettare un applicazione che realizzi una lavagna elettronica

(schermo) sulla quale si possano disegnare, cancellare, spostare, ingrandire e rimpicciolire una serie di

figure (punti, cerchi, triangoli, ...).

In tale applicazione sono individuabili

- un oggetto lavagna, caratterizzato da una serie di proprietà che ne descrivono le caratteristiche

(colore, larghezza, altezza,...) e dalle azioni che la lavagna è in grado di eseguire su richiesta dell’utente

(cancellazione, visualizzazione,...).

Inoltre, sulla lavagna potranno essere rappresentate figure di vario tipo:

- oggetti di tipo punto, caratterizzati da posizione e colore e in grado di essere visualizzati o nascosti,

spostati,...

- oggetti di tipo cerchio, caratterizzati da posizione, colore, raggio e in grado di compiere le stesse

azioni degli oggetti di tipo punto, oltre a poter essere ingranditi o rimpiccioliti.

- oggetti di tipo rettangolo ...

Da quanto detto sopra, possiamo dire che, nelle linee essenziali, per definire un oggetto se ne devono

specificare le caratteristiche e le azioni che è in grado di compiere.

In termini di codice, ciò equivale a definire un record i cui campi descrivono le caratteristiche

(proprietà) dell'oggetto e una serie di procedure o funzioni (metodi) che realizzano le azioni

dell'oggetto.

type

TCoordinata = integer;

TPunto = OBJECT

{proprietà}

x,y: TCoordinata;

colore: byte;

{metodi}

procedure crea (x0,y0:TCoordinata;col0:byte);

procedure cambiaColore(nuovoColore:byte);

...

END;

Si noti che:

• La dichiarazione delle proprietà deve precedere quella dei metodi.

• I nomi delle proprietà e quelli dei parametri formali dei metodi devono essere diversi.


2

Il processo utilizzato per definire i metodi ricorda le unit PASCAL:

all'interno dell'oggetto si specifica solo l'intestazione dei metodi, come nella sezione

interface di una unit, mentre la definizione del codice dei metodi avviene all'esterno della

definizione dell'oggetto, nella sezione della dichiarazione delle procedure, similmente a

quanto accade nella sezione implementation delle unit.

E' pratica comune creare delle librerie (unit) di oggetti, dichiarando i tipi oggetto nella sezione di

interfaccia e il codice dei metodi nella sezione di implementazione.

unit Punti;

interface (***************************************)

type

TCoordinata = integer;

TPunto = OBJECT

{proprietà}

x,y: TCoordinata;

colore: byte;

{metodi}

procedure crea (x0,y0:TCoordinata;col0:byte);

procedure cambiaColore(nuovoColore:byte);

procedure disegna;

END;

implementation (*********************************)

uses GRAPH;

{----------- implementazione metodi dell'oggetto TPunto -------------}

procedure TPunto.crea (x0,y0:TCoordinata;col0:byte);

begin

x:=x0;

y:=y0;

colore:=col0;

end;

procedure TPunto.cambiaColore(nuovoColore:byte);

begin

colore:=nuovoColore;

end;

procedure TPunto.disegna;

begin

putPixel(x,y,colore);

end;

{------------------------------------------------------------------------------}

BEGIN (**************************************)

grD:=Detect;

initGraph(grD,grM,' ');

clearDevice

END.


3

program usaPunti;

uses Crt,Punti;

var

puntoRossoBlu:TPunto;

BEGIN

puntoRossoBlu.crea(100,50,red);

puntoRossoBlu.disegna;

delay(2000);

with puntoRossoBlu do

begin

cambiacolore(blue);

disegna;

end;

repeat until keypressed;

END.

ISTANZE DI TIPI OBJECT

Spesso si fa riferimento ai TIPI OBJECT definendoli CLASSI e riservando il termine OGGETTO alle

ISTANZE (ovvero alle variabili) di tali classi.

puntoRossoBlu è un oggetto, TPunto è la classe di cui esso è un'istanza.

Le istanze dei tipi object possono essere dichiarate, come qualsiasi altra variabile, sia in memoria

statica che dinamica:

type

TPuntoPtr = ^TPunto;

var

punto : TPunto; {statica: già utilizzabile}

puntoPtr : TPuntoPtr; {dinamica: da allocare con new prima di poter essere utilizzata}

ACCESSO ALLE PROPRIETA' E AI METODI

Dall'esempio sopra riportato emergono alcune osservazioni:

• Per accedere alle parti di un oggetto si utilizza, come nei record, la notazione puntata o il

costrutto with.

• Dall'interno del codice di un metodo è possibile fare riferimento direttamente alle proprietà

del relativo oggetto.

• Poiché i metodi di un tipo di oggetto vengono implementati come procedure esterne agli oggetti,

è necessario che nell'intestazione dei metodi questi siano preceduti dal nome del tipo di

oggetto a cui appartengono.


4

procedure TPunto.cambiaColore(nuovoColore:byte);

begin

colore := nuovoColore;

end;

osserviamo ora il seguente codice:

punto1.cambiaColore(red);

punto2.cambiaColore(blue);

Sembra presentarsi un problema:

i due oggetti punto1 e punto2, essendo istanze della stessa classe TPunto, condividono lo stesso codice

per il metodo cambiaColore. Il compilatore traduce quindi le due chiamate di cambiaColore con un salto

allo stesso indirizzo. A tale indirizzo si trova l'assegnazione di un nuovo colore alla proprietà colore, ma

di quale istanza

In realtà, ogni volta che viene richiamato un metodo, viene inizializzata una variabile di sistema,

chiamata self, contenente l'indirizzo dell'istanza dell'oggetto che ha effettuato la chiamata.

E' come se all'interno di ogni metodo vi fosse il seguente codice nascosto:

with self do

begin

{codice del metodo}

...

end

PROPRIETA' FONDAMENTALI

La OOP si basa su tre proprietà fondamentali:

• INCAPSULAMENTO

• EREDITARIETA

• POLIMORFISMO

INCAPSULAMENTO : tutto ciò che riguarda l'oggetto deve essere definito al suo interno. In teoria,

l'utilizzatore dell'oggetto si deve servire unicamente dei metodi, evitando l'accesso diretto alle

proprietà.

Per modificare il colore del punto si dovrà utilizzare il metodo cambiaColore piuttosto che modificare

direttamente le proprietà colore. In teoria, ogni proprietà disponibile all'utilizzatore dovrebbe avere

un corrispondente metodo che ne possa impostare o leggere il valore.


5

TPunto = OBJECT

{proprietà}

x,y: TCoordinata;

colore: byte;

{metodi}

procedure crea (x0,y0:TCoordinata;col0:byte);

function leggiX: TCoordinata;

procedure cambiaX(nuovaX:TCoordinata);

function leggiY: TCoordinata;

procedure cambiaY(nuovaX:TCoordinata);

function leggiColore: byte;

procedure cambiaColore(nuovoColore:byte);

...

END;

Rispettare il principio dell'incapsulamento rende il codice più sicuro e facilmente modificabile: il fatto

di mantenere nascosti i dettagli implementativi all'utilizzatore degli oggetti rende possibili future

modifiche del codice dei metodi senza dover necessariamente riscrivere i programmi già rilasciati che

utilizzano tali metodi (ovviamente, pur di mantenerne invariata l'intestazione).

Ad esempio, nei metodi cambiaX e cambiaY si potrebbe aggiungere un controllo che limiti i valori delle

coordinate in un certo intervallo.

EREDITARIETA': è possibile definire una gerarchia di oggetti che condividono proprietà e metodi. I

discendenti ereditano tutto ciò che appartiene agli antenati, con la possibilità di aggiungere ulteriori

proprietà e metodi che ne caratterizzano il comportamento.

Ad esempio, tutte le figure hanno delle caratteristiche in comune con il punto: una posizione, un colore,

la capacità di cambiare colore ...

E' conveniente allora definire l'oggetto punto come antenato e far discendere da esso tutti i diversi

tipi di figure.

Si noti che l'antenato potrebbe essere un oggetto che non sarà mai utilizzato in un'applicazione, ma la

cui funzione è semplicemente quella di "trasmettere" delle proprietà e dei metodi ai suoi discendenti.

La sintassi per indicare il rapporto di discendenza è la seguente:

type

TFiglio = OBJECT (TPadre)

….

END;


6

Aggiungiamo qualche metodo alla dichiarazione di TPunto (premettendo che successivamente sarà

necessario modificarla) e facciamo discendere da esso un nuovo tipo TCerchio:

type

TPunto = OBJECT

{proprietà}

x,y: TCoordinata;

colore: byte;

{metodi}

procedure crea (x0,y0:TCoordinata;col0:byte);

function leggiX: TCoordinata;

procedure cambiaX(nuovaX:TCoordinata);

function leggiY: TCoordinata;

procedure cambiaY(nuovaX:TCoordinata);

function leggiColore: byte;

procedure cambiaColore(nuovoColore:byte);

procedure disegna;

procedure cancella;

procedure sposta(nuovaX,nuovaY:TCoordinata);

END;

TCerchio = OBJECT (TPunto)

{ proprietà e metodi ereditati da non dichiarare

x,y: TCoordinata;

colore: byte;

procedure crea (x0,y0:TCoordinata;col0:byte);

function leggiX: TCoordinata;

procedure cambiaX(nuovaX:TCoordinata);

function leggiY: TCoordinata;

procedure cambiaY(nuovaX:TCoordinata);

function leggiColore: byte;

procedure cambiaColore(nuovoColore:byte);

procedure disegna;

procedure cancella;

procedure sposta(nuovaX,nuovaY:TCoordinata);

}

{ nuove proprietà e metodi caratteristici del cerchio }

raggio:integer;

function leggiRaggio: integer;

procedure cambiaRaggio(nuovoRaggio:integer);

END;

Si noti, nella dichiarazione di TCerchio, l'indicazione fra parentesi dell'antenato.


7

POLIMORFISMO: tradotta alla lettera, tale proprietà fa riferimento alla capacità di un oggetto di

assumere "più forme". Più esattamente, in una gerarchia di oggetti, ciascun oggetto realizza le proprie

azioni secondo una propria modalità specifica.

Il punto e il cerchio sono visivamente rappresentati in modo diverso. Il metodo disegna, comune a

entrambi perché ereditato, dovrà disegnare figure differenti a seconda dell'oggetto che lo invoca. In

un certo senso è come se il metodo si "adattasse" all'oggetto.

E' anche vero, però, che il metodo crea, non sarà in grado di assegnare un valore al raggio, poiché

questa proprietà non è posseduta dal punto.

Per capire come possa realizzarsi quanto richiesto dal POLIMORFISMO è indispensabile entrare

maggiormente nei dettagli.

RIDEFINIZIONE DEI METODI EREDITATI

Un oggetto discendente eredita proprietà e metodi dell'antenato. Capita spesso, però, che il

comportamento del discendente sia diverso da quello dell'antenato. Si rende quindi necessario

"personalizzare" ovvero ridefinire alcuni metodi.

Per ridefinire un metodo ereditato bisogna specificare nel discendente un nuovo metodo con lo stesso

nome e, se necessario, con un diverso insieme di parametri. Anche il codice, ovviamente, andrà riscritto.

In generale, tutti gli oggetti necessitano di un metodo di inizializzazione per "riempire" le proprietà

dell'oggetto (ciò che rende un'istanza diversa da un'altra). Un discendente, quindi, avendo ulteriori

proprietà oltre a quelle ereditate, necessiterà sicuramente di un'inizializzazione diversa rispetto ai

propri antenati.

procedure TPunto.crea (x0,y0:TCoordinata;col0:byte);

begin

x := x0;

y := y0;

colore := col0;

end;

procedure TCerchio.crea(x0,y0:TCoordinata;col0:byte;r0:integer);

begin

TPunto.crea(x0,y0,col0); {ereditata}

raggio := r0;

end;

E' una buona norma inizializzare un'istanza servendosi del metodo di inizializzazione ereditato

dall'antenato. Così facendo, eventuali modifiche apportate successivamente agli antenati verranno

automaticamente ereditate da tutti i discendenti.

Si noti la sintassi per richiamare il metodo ereditato: il nome del metodo viene fatto precedere dal

nome della classe.


8

Ma a che serve ereditare un metodo se poi è necessario ridefinirlo Per dare una risposta a questa

domanda, bisogna prima chiarire i meccanismi di compilazione che rendono possibile la realizzazione del

polimorfismo. Lo facciamo attraverso un esempio:

consideriamo l'implementazione dei metodi disegna,cancella,sposta per le due classi viste:

(************** metodi della classe TPunto ***********************************)

...

procedure TPunto.disegna;

begin

putPixel(x,y,colore);

end;

procedure TPunto.cancella;

begin

putPixel(x,y,black); {supponendo che lo sfondo sia nero}

end;

procedure TPunto.sposta(nuovaX,nuovaY:TCoordinata);

begin

cancella;

x:=nuovaX;

y:=nuovaY;

disegna;

end;

(************** metodi della classe TCerchio ***********************************)

...

procedure TCerchio.disegna;

begin

setColor(colore);

circle(x,y,raggio);

end;

procedure TCerchio.cancella;

begin

setColor(black); {supponendo che lo sfondo sia nero}

circle(x,y,raggio);

end;

procedure TCerchio.sposta(nuovaX,nuovaY:TCoordinata);

begin

cancella;

x:=nuovaX;

y:=nuovaY;

disegna;

end;

Possiamo notare che il codice del metodo sposta è identico per le due classi. Si potrebbe pensare di non

ridichiarare il metodo sposta e di ereditarlo da TPunto.

Vediamo cosa succederebbe in tal caso.

Supponiamo di avere un oggetto cerchio di tipo TCerchio e di richiamarne il metodo sposta:

cerchio.sposta(50,70);


9

Quando il compilatore trova una chiamata ad un metodo, per prima cosa cerca tale metodo fra quelli del

tipo di oggetto chiamante. Se non lo trova, risale ordinatamente lungo la gerarchia. Una volta trovato,

traduce la chiamata con un salto all'indirizzo assegnato al metodo durante la sua compilazione.

Nell'ipotesi di avere ereditato il metodo sposta da TPunto, il compilatore, non avendolo trovato tra

quelli di TCerchio, avrà tradotto tale chiamata con un salto all'indirizzo di compilazione del metodo

TPunto.sposta.

Una volta all'interno di tale codice, quindi, tutte le successive chiamate saranno relative all'oggetto

TPunto e quindi si cercherà erroneamente di cancellare e disegnare un punto.

Per poter invece realizzare quanto proposto dal POLIMORFISMO, bisognerà fare in modo che i metodi

cancella e disegna siano relativi all'istanza dell'oggetto chiamante.

E' evidente, allora, che l'indirizzo di salto non può essere fissato rigidamente in fase di compilazione,

ma dovrà essere ricavato successivamente, quando sarà noto l'oggetto chiamante. Ciò è quanto si

definisce un collegamento rinviato (late binding) e per tale motivo i metodi a cui si applica vengono

definiti "dinamici" o virtuali, in opposizione di quelli classici che vengono detti "statici", il cui indirizzo è

stabilito immediatamente (early binding).

In termini di sintassi, la dichiarazione corretta di tali metodi prevede la specifica della direttiva di

compilazione virtual nella definizione del metodo, all'interno della dichiarazione del tipo di oggetto

(NON nell'implementazione).

Inoltre, per realizzare il collegamento rinviato, ovvero il collegamento fra l'istanza e la classe di

appartenenza, all'interno della classe dovrà essere presente un metodo che consenta, con la sua

chiamata, di "costruire" tale collegamento PRIMA che uno qualsiasi dei metodi virtuali vada in

esecuzione. Ciò si ottiene con la parola riservata constructor (al posto di procedure). E' pratica comune

scegliere il metodo crea, essendo l'unico che sicuramente sarà sempre chiamato prima dell'utilizzo

dell'oggetto.

type

TPunto = OBJECT

x,y: TCoordinata;

colore: byte;

constructor crea (x0,y0:TCoordinata;col0:byte);

function leggiX: TCoordinata;

procedure cambiaX(nuovaX:TCoordinata);

function leggiY: TCoordinata;

procedure cambiaY(nuovaX:TCoordinata);

function leggiColore: byte;

procedure cambiaColore(nuovoColore:byte);

procedure disegna; virtual;

procedure cancella; virtual;

procedure sposta(nuovaX,nuovaY:TCoordinata);

END;

TCerchio = OBJECT (TPunto)

raggio:integer;

constructor crea (x0,y0:TCoordinata;col0:byte;r0:integer);

procedure disegna; virtual;

procedure cancella; virtual;

function leggiRaggio: integer;

procedure cambiaRaggio(nuovoRaggio:integer);

END;


10

• Ogni tipo di oggetto che possiede dei metodi virtuali deve avere un costruttore.

• La semplice chiamata del costruttore inizializza il collegamento eseguendo un codice nascosto.

Si potrebbe utilizzare anche una procedura vuota, ottenendo lo stesso risultato.

• Dichiarando un metodo come virtual in una classe, questo sarà tale in tutti i discendenti. A

differenza dei metodi statici, quindi, dovrà necessariamente mantenere invariata l'intestazione.

MECCANISMI CHE IMPLEMENTANO IL POLIMORFISMO

Per ogni CLASSE (TIPO di oggetto) contenente dei metodi virtuali, il compilatore crea nel segmento

dati, una TABELLA DEI METODI VIRTUALI (VMT).

• Esiste una sola VMT per tipo di oggetto (e NON una per ogni istanza).

Come lascia intuire il nome, la VMT contiene gli indirizzi dei metodi virtuali presenti nella classe.

VMT della classe TPunto VMT della classe TCerchio

dimensioni di TPunto

dimensioni di TCerchio

... ...

@TPunto.cancella

@TCerchio.cancella

@TPunto.disegna

@TCerchio.disegna

... ...

Per ogni ISTANZA di classe con metodi virtuali, il compilatore alloca nell'area di memoria riservata

all'istanza, un campo supplementare destinato a contenere l'indirizzo (offset) della VMT della classe

corrispondente.

var punto

x

y

colore

@VMT TPunto

var cerchio

x

y

colore

@VMT TCerchio

raggio

L'indirizzo verrà effettivamente scritto in tale campo quando avverrà la chiamata del costruttore,

durante l'esecuzione del programma (e NON durante la compilazione).

• Tale caratteristica consente di lavorare con oggetti il cui tipo è ignoto durante la fase di

compilazione, che è la vera essenza del polimorfismo.

Essa si basa sul fatto che un discendente eredita, oltre a tutto il resto, anche la COMPATIBILITA'

con i suoi antenati, il che equivale a dire che è possibile assegnare ad un antenato uno qualsiasi dei suoi

discendenti.


11

In base a ciò, è del tutto lecito (anche se apparentemente privo di significato) fare un assegnamento

del tipo:

punto:=cerchio;

Dove va a finire il raggio

Più significativo può risultare sfruttare tale compatibilità impiegandola nella dichiarazione di un

parametro di una procedura:

procedure lampeggia(var figura:TPunto);

var i:integer;

begin

for i:= 1 to 30 do

begin

figura.cancella;

delay(100);

figura.disegna;

end;

end;

Si noti il passaggio del parametro per riferimento, che consente al parametro attuale, all’atto della

chiamata, di essere un punto, un cerchio o addirittura una figura non ancora ideata (purché discendente

da TPunto).

In effetti, i vantaggi del polimorfismo vengono alla luce con oggetti gestiti tramite il loro indirizzo,

ovvero allocati in memoria dinamica.

type

TPuntoPtr : ^TPunto;

var

puntoPtr:TPuntoPtr;

per consentire di creare un'istanza dinamica, esistono tre possibilità:

• new(puntoPtr);

puntoPtr^.crea(50,70,red);

• new(puntoPtr,crea(50,70,red);

• puntoPtr:=new(TpuntoPtr,crea(50,70,red));

la seconda e la terza, ideate apposta per la OOP, sono da preferire in quanto garantiscono la chiamata

del costruttore.

In particolare, la terza, essendo una function e utilizzando come primo parametro il type del

puntatore alla classe e non l'istanza , risulta direttamente applicabile senza l'esplicita

dichiarazione di un puntatore (ad esempio, come parametro di una procedura).


12

UN ESEMPIO APPLICATIVO

album

TPunto

TCerchio

type

TNumFigure=1..5;

TFigure = OBJECT

elenco=array[TNumFigure] of TPuntoPtr;

procedure crea;

procedure inserisci(posizione:TNumFigure;figura:TPuntoPtr);

procedure visualizzaTutto;

...

END;

var

album : TFigure;

procedure TFigure.crea;

var i:TNumFigure;

begin

for i:= 1 to High(TNumFigure) do

elenco[i]:= NIL;

end;

procedure TFigure.inserisci(posizione:TNumFigure;figura:TPuntoPtr);

begin

elenco[posizione]:= figura;

end;

procedure TFigure.visualizzaTutto;

var i:TNumFigure;

begin

for i:= 1 to High(TNumFigure) do

if (elenco[i] NIL) then

elenco[i]^.disegna;

end;

BEGIN

...

album.crea;

album.inserisci(1,new(TPuntoPtr, crea(50,50,green)));

album.inserisci(2,new(TCerchioPtr, crea(50,50,25,red)));

...

album.visualizzaTutto;

END.


13

LIBERAZIONE DELLA MEMORIA

Riprendiamo in considerazione il metodo TFigure.inserisci :

Potrebbe capitare di voler sostituire una figura. Dovremmo inserire quindi la nuova figura in una

posizione già occupata. In tal caso, per non lasciare occupata una parte di memoria dinamica, prima

dell'inserimento dovremmo liberare tale memoria.

La rimozione di un oggetto allocato dinamicamente può avvenire in due modi:

• dispose(puntoPtr);

• dispose(puntoPtr,libera);

La seconda abbina alla richiesta di rilascio di un'istanza, la chiamata di un suo metodo particolare,

definito destructor.

destructor TPunto.libera;

begin

...

end;

La sintassi estesa della procedura dispose diventa obbligatoria quando l'oggetto da rimuovere è

allocato dinamicamente. In modo particolare, se tale oggetto contiene al suo interno dei riferimenti a

strutture dati o oggetti allocati anch'essi dinamicamente, il distruttore si occupa del rilascio ordinato

della memoria occupata da tali risorse. Il distruttore deve quindi essere richiamato immediatamente

prima della distruzione dell'istanza.

I distruttori agiscono solo sugli oggetti allocati dinamicamente: oltre a svolgere il lavoro di "pulizia" (a

carico del programmatore) essi si assicurano che venga rilasciata la quantità corretta di memoria per

l'istanza che li invoca (ricavando tale informazione dalla VMT). Tale compito viene realizzato da una

parte di codice "nascosta" che il compilatore genera automaticamente in coda al distruttore. Per tale

motivo, il distruttore potrebbe anche essere un metodo vuoto.

Richiedere il rilascio secondo la prima modalità, non consente di calcolare le esatte dimensioni

dell'istanza: a causa del polimorfismo, un puntatore ad un oggetto potrebbe puntare ad un qualsiasi

discendente.

• La presenza di un distruttore non è legata a quella dei metodi virtuali.

• I distruttori possono essere sia statici che virtuali. Poiché però i diversi tipi di oggetti

richiedono in genere operazioni di rilascio diverse, è buona norma che essi siano virtuali.


14

TFigure = OBJECT

elenco=array[TNumFigure] of TPuntoPtr;

constructor crea;

destructor libera;virtual;

procedure inserisci(posizione:TNumFigure;figura:TPuntoPtr);

procedure visualizzaTutto;

END;

....

TFigurePtr = ^TFigure;

destructor TFigure.libera;

begin

for i:= 1 to High(TnumFigure) do

if (elenco[i]NIL) then

dispose(elenco[i],libera);

end;

var

albumPtr : TFigurePtr;

BEGIN

albumPtr:=new(TFigurePtr,crea);

albumPtr^.inserisci(1,new(TPuntoPtr, crea(50,50,green)));

albumPtr^.inserisci(2,new(TCerchioPtr, crea(50,50,25,green)));

...

dispose(albumPtr,libera);

END.

L’ultima istruzione chiama il distruttore TFigure.libera che, come illustrato dal codice del metodo,

rilascia la memoria occupata da tutte le figure inserite nell’array richiamando il distruttore di ciascuna.

Infine, il codice nascosto di TFigure.libera passa alla dispose le dimensioni dell'oggetto puntato da

albumPtr prelevandole dalla VMT.

More magazines by this user
Similar magazines