Unità 4

Puntatori ed operatori new e delete

 

Si è avuto modo di conoscere l’operatore & (indirizzo di) utilizzato nel passaggio di un parametro, per riferimento, ad una funzione. L’operatore * è il secondo operatore utilizzato per la manipolazione di indirizzi di memoria: è utilizzato per specificare un puntatore.


Un puntatore è una variabile che contiene un indirizzo di memoria. Un puntatore è quindi una variabile che non contiene dati ma l’indirizzo dei dati. Una variabile per essere utilizzata come puntatore è necessario che sia dichiarata come tale. La codifica seguente è relativa ad un programma che esegue la somma di due numeri interi contenuti in variabili allocate dinamicamente e accessibili tramite puntatori:



	#include <iostream.h>
	main(){
	  int *a,*b,*c;											/*1*/	

	  a = new (int);										/*2*/
	  b = new (int);										/*2*/
	  c = new (int);										/*2*/

	  cout << "\nPrimo numero ";
	  cin  >> *a;											/*3*/
	  cout << "\nSecondo numero ";
	  cin  >> *b;											/*3*/
	  *c = *a + *b; 										/*3*/
	  cout << "\nSomma = " << *c;

	  delete a;												/*4*/
	  delete b;												/*4*/
	  delete c;												/*4*/
	}

Nella 1 vengono dichiarati tre puntatori ad interi. Con tale terminologia si vuole intendere che tali variabili (a, b e c) conterranno ognuna il primo indirizzo di una quantità di locazioni di memoria tali da poter contenere un dato del tipo specificato.

Nelle 2 si alloca in memoria lo spazio dove conservare i valori dei tipi specificati. La variabile a, per esempio, non può essere utilizzata per conservare dei valori essendo stata dichiarata come puntatore: in pratica l’operatore new alloca in memoria uno spazio raggiungibile attraverso la variabile a. Fra le parentesi che seguono l’operatore, e che possono essere omesse, si specifica quanto spazio di memoria deve essere allocato (tanto quanto ne basta per conservare un dato di tipo int).

Nelle 3 viene utilizzato lo spazio allocato per conservare i dati provenienti da input o calcolati. In questo caso, attraverso l’operatore *, si accede alla zona di memoria dove conservare i dati.

Nelle 4 si libera lo spazio di memoria allocato nelle 2. Tale operazione si rende necessaria per recuperare lo spazio di memoria dedicato alla conservazione di dati che ora non servono più: in questo esempio tale operazione non è strettamente necessaria, visto che subito dopo il programma termina, ma in generale è bene liberare lo spazio con l’uso esplicito dell’operatore delete, in modo che il compilatore possa riallocare, qualora serva, lo spazio.


L’uso dei puntatori e dell’allocazione dinamica della memoria è dettato da una doppia esigenza:


  1. Gestire in maniera ottimale la memoria: lo spazio è allocato giusto per il tempo necessario. Quando si dichiara una variabile, lo spazio allocato dipende dalla dimensione prevista per quella variabile, se invece si dichiara un puntatore, lo spazio allocato è quello necessario per conservare un indirizzo di memoria che è inferiore, specie se la variabile dichiarata non è di un tipo elementare. Se poi si libera la memoria, lo spazio è occupato giusto per il tempo minimo indispensabile. Nell’esempio, essendo lo spazio dedicato ad un int esiguo, non è giustificato l’uso di puntatori, ma fra poco si esamineranno casi in cui la differenza è rilevante.

  2. Velocizzare l’accesso ai dati: nel passaggio ad una funzione di un parametro una cosa è passare un puntatore, altra una variabile soprattutto se quest’ultima contiene dati estesi. Si pensi, per esempio, ad un array che è composto di una collezione, anche abbastanza numerosa, di variabili; in questo caso passare un puntatore (come scelto per default dal C++, quando si tratta di utilizzare l’array come parametro di una funzione) vuol dire copiare nei parametri della funzione una quantità di dati inferiore di quella che si avrebbe se invece si copiasse l’intera struttura.

Inizio


Le strutture



Una struttura è un insieme di variabili di uno o più tipi, raggruppate da un nome in comune. Anche gli array sono collezioni di variabili come le strutture solo che un array può contenere solo variabili dello stesso tipo, mentre le variabili contenute in una struttura non devono essere necessariamente dello stesso tipo.


Le strutture del linguaggio C++ coincidono con quelli che in Informatica sono comunemente definiti record. Il raggruppamento sotto un nome comune permette di rappresentare, tramite le strutture, entità logiche in cui le variabili comprese nella struttura rappresentano gli attributi di tali entità.


Per esempio con una struttura possiamo rappresentare l’entità libro i cui attributi potrebbero essere: titolo, autore, editore, prezzo, prestato. In tale caso la definizione potrebbe essere:



	struct libro {
	  char titolo[50];
	  char autore[20];
	  char editore[20];
	  long int prezzo;
	  bool prestato;
	};

La sintassi del linguaggio prevede, dopo la parola chiave struct, un nome che identificherà la struttura (il tag della struttura). Racchiuse tra le parentesi sono dichiarate le variabili che fanno parte della struttura (i membri della struttura). È bene chiarire che in questo modo si definisce la struttura logica libro, che descrive l’aspetto della struttura, e non un posto fisico dove conservare i dati. In pratica si può considerare come se si fosse definito, per esempio, com’è composto il tipo int: ciò è necessario per dichiarare variabili di tipo int.



	#include <iostream.h>
	...
	struct libro {											/*1*/
	  char titolo[50];
	  char autore[20];
	  char editore[20];
	  long int prezzo;
	  bool prestato;
	};
	...
	main(){
	  libro lib1,lib2;										/*2*/
	  ...
	  cin.get(lib1.titolo,50,’\n’);							/*3*/
	  ...
	  cout << lib2.prezzo;									/*4*/
	  ...
	}

Nella riga 1 si dichiara il tipo struct libro. La dichiarazione è globale in modo che tutte le funzioni presenti possano accedere a tale definizione. Si evita così di ridefinire la struttura in ogni funzione.

Nella 2 si dichiarano due istanze (lib1 e lib2) di libro (due variabili di tipo struct libro).

La funzione della 3 effettua l’input nella componente titolo della istanza lib1. Per fare riferimento ad un membro deve, infatti, essere specificata l’istanza oltre che il membro: i due nomi sono separati dal punto. La specifica dell’istanza è indispensabile: ci sono, infatti, nel caso in esame, due titolo (quello di lib1 e quello di lib2). Analogo discorso vale per la componente prezzo: la 4 infatti si occupa dell’output di tale componente relativamente all’istanza lib2.

Inizio


Puntatori a strutture



Una struttura può occupare parecchio spazio in memoria centrale, in relazione alla quantità di membri contenuti nella struttura stessa e al tipo di membri. Ricorre spesso la necessità di usare puntatori a strutture: per esempio per passare ad una funzione una o più strutture. Utilizzando riferimenti invece di passare le strutture per valore si può ottenere una elaborazione più veloce: si evita, infatti, di ricopiare nelle variabili locali della funzione le strutture passate.


Tali necessità sono così frequenti che il linguaggio C++ mette a disposizione l’operatore -> (il trattino seguito dal simbolo di maggiore) per accedere ai membri della struttura:



	...
	struct libro {											/*1*/
	  char titolo[50];
	  char autore[20];
	  char editore[20];
	  long int prezzo;
	  bool prestato;
	};
	...
	main(){
	  libro *lib1;											/*2*/
	  lib1 = new (libro);									/*3*/
	  ...
	  cin.get(lib1->titolo,50,’\n’);						/*4*/
	  cin >> lib1->prezzo;									/*4*/
	  ...
	}

Nella 1 si dichiara la struttura che ha visibilità globale, da questo momento in poi è possibile dichiarare variabili di tipo libro come anche puntatori al tipo libro.

Nella 2 si dichiara un puntatore a libro e nella 3 si assegna lo spazio di memoria dove saranno conservati i dati.

Nelle 4 si acquisiscono i dati che vengono conservati nelle variabili membri della struttura. Questi sono accessibili, mediante l’uso dell’operatore ->, per mezzo del puntatore lib1.

Inizio


Estensione della struttura libro



La presenza del membro prestato all’interno della struttura libro fa presupporre che, sui libri conservati, si effettuano comunemente operazioni di prestito. L’operazione di prestito di un libro potrebbe fare parte integrante di ciò che si intende per libro: per esempio se il programma che si sta scrivendo gestisce una biblioteca che effettua servizio di prestito dei propri libri ai soci. In questo caso si potrebbe inserire dentro la struttura libro la funzione che effettua il prestito del libro: si rende evidente, in tal modo, il fatto che per la nostra elaborazione un libro è un oggetto che è dato in prestito.



	...
	struct libro {		
	  char titolo[50];
	  char autore[20];
	  char editore[20];
	  long int prezzo;
	  bool prestato;

	  bool pEffettuato(){									/*1*/
	    bool prestitoOk;
	    if (!prestato){
	      prestato   = true;								/*2*/
	      prestitoOk = true;
	    }else
	      prestitoOk = false;
	    return prestitoOk;
	  };
	};
	...
	main(){
	  libro *lib1;											/*3*/
	  libro lib2;											/*4*/
	  lib1 = new (libro);									/*5*/
	  ...
	  if (!lib1->pEffettuato())								/*6*/
	    cout << “\nLibro già prestato”;
	  else
	    cout << “\nPrestito effettuato”;	
	  ...
	}

Nella 1 viene definita la funzione per effettuare il prestito di un libro: tale funzione setta a true il valore del membro prestato e ritorna un valore logico sul risultato dell’operazione effettuata (se il libro è già stato prestato l’operazione non può avere luogo). Si noti che la funzione è inserita nella struttura per intendere che è una proprietà distintiva del libro, cosi come il titolo o l’autore. La posizione della definizione della funzione, per quanto si è osservato fino a questo punto, è un fatto logico. Se però fosse definita fuori della struttura, non si potrebbe effettuare l’assegnazione della 2: la proprietà prestato è, infatti, un membro di una struttura e quindi non si può accedere ad essa se non si specifica la struttura di appartenenza. In altri termini la funzione pEffettuato definita fuori dalla struttura modifica la proprietà prestato di un oggetto concreto che deve essere passato ad essa come parametro o accessibile mediante una definizione globale, ma sempre, in ogni caso, come parte di un oggetto (per es. lib2.prestato). Il fatto che la funzione pEffettuato è qui definita dentro la struttura comporta la conseguenza che si riferisce a libro in astratto e quindi modifica la proprietà prestato del libro in generale.

Nella 3 e nella 4 vengono definite due istanze di libro, due oggetti concreti di libro, uno (lib1) definito come puntatore che, in virtù della 5, diventa un posto dove conservare un libro esattamente come lib2.

Nella 6 viene effettuato il prestito del libro accessibile tramite lib1, viene cioè modificato il valore di prestato di lib1. In questo caso viene eseguita la funzione pEffettuato associata all’oggetto lib1; se si fosse voluto effettuare il prestito del libro conservato in lib2 (modificare l’indicatore prestato di lib2), si sarebbe dovuto modificare la 6 in lib2.pEffettuato().

Inizio


La classe libro



Le classi sono uno degli elementi fondamentali della programmazione orientata agli oggetti (OOP). Una classe è un tipo di dato definito dall’utente che ha un proprio insieme di dati e di funzioni (Abstract Data Type).



	// esempio non funzionante !!

	class libro {											/*1*/
	  char titolo[50];
	  char autore[20];
	  char editore[20];
	  long int prezzo;
	  bool prestato;

	  bool pEffettuato(){
	    bool prestitoOk;
	    if (!prestato){
	      prestato   = true;
	      prestitoOk = true;
	    }else
	      prestitoOk = false;
	    return prestitoOk;
	  };
	};
	...
	libro lib2;									/*2*/
	...
	cin >> lib2.prezzo;							/*3*/
	...

Nella 1 si definisce la classe astratta libro, cioè un tipo con attributi (titolo, autore ecc..) e comportamenti (le funzioni definiti nella classe).

Nella 2 si definisce una istanza di libro o, come si dice in OOP, un oggetto della classe libro.


La definizione di libro, tranne che per la sostituzione della parola chiave struct con la parola chiave class, sembra identica a quella adottata precedentemente, solo che ora, come d’altra parte messo in evidenza dalla riga di commento, c’è una differenza sostanziale: il compilatore se si tenta di accedere ad un attributo dell’oggetto, come per esempio usando istruzioni come quella presente nella 3, segnala che, nell’esempio, prezzo non è accessibile. Ciò è dovuto alle regole di visibilità dei vari elementi: se, infatti, non si specifica altrimenti, la visibilità è limitata solo all’interno della classe, per esempio la funzione pEffettuato può accedere a prestato. In generale in una classe possono essere specificati tre livelli di visibilità:



	class libro {
	  public:
	    ...
	  protected:
	    ...
	  private:
	    ...
	};

Nella sezione public si specificano i membri accessibili agli altri membri della classe, alle istanze della classe e alle classi discendenti (quelle che si definiscono a partire da libro e che ne ereditano le proprietà).

Nella sezione protected si specificano i membri accessibili agli elementi della classe, alle classi discendenti ma non alle istanze della classe. È la definizione assunta per default ed è quindi questo il motivo perché nell’esempio proposto prima non era visibile prezzo.

Nella sezione private si specificano i membri che devono essere accessibili solamente agli altri membri della stessa classe.

Le sezioni possono essere disposte in modo qualsiasi, figurare più volte nella definizione della classe e non è obbligatorio inserirle tutte.


La possibilità di definire diversi livelli di mascheramento dell’informazione (data hiding) è una delle caratteristiche fondamentali della programmazione ad oggetti. Aiuta, infatti, a creare una interfaccia della classe che, nascondendo l’implementazione, mostra l’oggetto definito in maniera da evidenziare le proprie proprietà e i comportamenti: nell’esempio specificato, per esempio, lib2 è un oggetto che è dotato di proprie proprietà (lib2.titolo o lib2.autore) e proprie funzioni (lib2.pEffettuato()). Mettere assieme in una classe le proprietà e le funzioni è un’altra delle caratteristiche fondamentali della OOP: l’incapsulamento. In questo modo ogni oggetto della classe non ha solo caratteristiche ma anche comportamenti esattamente come nella realtà: se si gestisce una biblioteca, un libro, non è solo un oggetto che ha, per esempio, il titolo “Pinocchio” ma è anche oggetto di prestito e restituzione. Una classe contiene tutto il necessario per usare i dati; il programma che usa la classe non ha bisogno di sapere com’è fatta. Un’ultima osservazione sulla visibilità riguarda le strutture: in C++ una struttura è una classe con tutti i metodi e proprietà public: è questo il motivo perché nel caso esaminato in precedenza tutte le proprietà e i metodi di libro erano accessibili dalle proprie istanze.


Nella OOP, in ragione del mascheramento dei dati, questi normalmente vengono dichiarati nella sezione private e si accede ad essi utilizzando dei metodi public. Questa, si può dire, è una regola generale: è l’interfaccia della classe che mette a disposizione gli strumenti per accedere ai dati. In tal modo può essere, per vari motivi, modificata la struttura interna dei dati della classe senza che cambi l’uso degli oggetti della classe.



	#include <iostream.h>

	/*
	  Definizione classe Libro con proprietà (titolo, editore, ..)
	  e metodi (registra, infoLibro, pEffettuato, lRitornato)
	*/

	class libro {
	  public:												/*1*/
	    void registra();
	    void infoLibro();
	    bool pEffettuato();
	    bool lRitornato();
	  private:												/*2*/
	    char titolo[50];
	    char autore[20];
	    char editore[20];
	    long int prezzo;
	    bool prestato;
	};

	// Implementazione metodi della classe

	void libro::registra(){									/*3*/
	  cout << "\nTitolo libro ";
	  cin.get(titolo,50,'\n');
	  while(cin.get()!='\n');
	  cout << "Autore libro ";
	  cin.get(autore,20,'\n');
	  while(cin.get()!='\n');
	  cout << "Editore libro ";
	  cin.get(editore,20,'\n');
	  while(cin.get()!='\n');
	  cout << "Prezzo libro ";
	  cin  >> prezzo;
	  while(cin.get()!='\n');
	  prestato=false;
	};

	void libro::infoLibro(){								/*4*/
	  cout << "\nDati libro";
	  cout << "\n" << titolo;
	  cout << "\n" << autore;
	  cout << "\n" << editore;
	  cout << "\n" << prezzo;
	  cout << "\n" << prestato;
	};

	bool libro::pEffettuato(){								/*5*/
	  bool prestitoOk;
	  if (!prestato){
	    prestato   = true;
	    prestitoOk = true;
	  }else
	    prestitoOk = false;
	  return prestitoOk;
	};

	bool libro::lRitornato(){								/*6*/
	  bool ritornatoOk;
	  if (prestato){
	    prestato    = false;
	    ritornatoOk = true;
	  }else
	    ritornatoOk = false;
	  return ritornatoOk;
	};

In 1, nella sezione public della classe, sono dichiarate le funzioni di accesso ai dati della classe. Questa è la sezione di interfaccia della classe: le funzioni sono solo dichiarate per mettere in evidenza come si usano (è come se ne specificassimo i prototipi), la definizione, si può dire, è un affare interno della classe.

Nella 2 comincia la sezione private della classe. I dati sono accessibili dall’esterno solo utilizzando le funzioni della sezione public.

Dalla 3 comincia il codice di implementazione delle funzioni. Si noti la presenza, prima del nome della funzione, del nome della classe e dell’operatore :: operatore di visibilità indicante la capacità della funzione registra di accedere alla sezione private della classe.

La funzione definita in 4 si occupa di accedere in output ai dati, al contrario della funzione definita in 3, che si occupa del contrario. Sono queste le funzioni che permettono di accedere ai dati della classe.

La 5 è la funzione, già esaminata, che si occupa di effettuare il prestito di un libro. La 6 formalmente simile si occupa della registrazione della restituzione del libro.


Per motivi di chiarezza conviene, conservare la definizione della classe in un file a parte, e inglobare il file nel programma che necessita di usare la classe, come si può notare nel programma dell’esempio seguente che si occupa della gestione dei prestiti di una biblioteca in cui i libri sono conservati in un array.



	#include <iostream.h>
	#include "c-libro.h"									/*1*/

	void main(){
	  libro biblio[10];										/*2*/
	  int n,i,tipoop,quale;

	  // Input dati dei libri

	  cout << "\nQuanti libri (max 10)? ";
	  cin >> n;
	  while(cin.get()!='\n');
	  n = (n>10 ? 10 : n);									/*3*/
	  for (i=0;i<n;i++){
	    cout << "\nLibro n. " << (i+1) << " ";
	    biblio[i].registra();								/*4*/
	  }

	  // Gestione prestiti libri

	  for(;;){
	    cout << "\n1 - prestito";
	    cout << "\n2 - restituzione";
	    cout << "\n0 - fine";
	    cout << "\nOperazione ? ";
	    cin >> tipoop;
	    if(!tipoop) break;
	    cout << "\nQuale libro (1," << n << ")? ";
	    cin >> quale;
	    quale = (quale>n ? n : quale);
	    quale--;

	    biblio[quale].infoLibro();							/*5*/
	    if (tipoop==1){
	      if (!biblio[quale].pEffettuato())					/*6*/
	        cout << "\nLibro già prestato";
	      else
	        cout << "\nPrestito effettuato";
	    }else{
	      if (!biblio[quale].lRitornato())					/*6*/
	        cout << "\nLibro già presente";
	      else
	        cout << "\nRestituzione effettuata";
	    }
	  }
	}

La direttiva presente nella 1 permette di includere nel programma presente la classe definita in precedenza. L’uso delle doppie virgolette nella include al posto delle parentesi angolari, vuol dire che il file si trova nella stessa directory del programma attuale e non nella directory standard dove si trovano tutti i file da includere.

Nella 2 si definisce la struttura di dati che conterrà i libri della biblioteca (un array). L’istruzione presente nella 3 ha solo il compito di ricondurre al valore previsto (10) un eventuale input che superasse la dimensione impostata dell’array.

Nella 4 si richiama il metodo registra per ogni elemento dell’array.

Nella 5, attraverso infoLibro, si tirano fuori i dati del libro interessato all’operazione da effettuare.

Nelle 6 si sceglie, richiamando per il libro l’opportuna funzione della classe, l’operazione da effettuare sul libro.

 

TORNA INDIETRO