Sei sulla pagina 1di 4

C++

PartE 2

C++11: puntatori smart

Eccovi altri interessanti consigli sul C++, questa volta sfruttando lultima
versione disponibile del linguaggio di programmazione: C++11

n questo tutorial affronterete altri aspetti di C++. In particolare


vedrete i puntatori e il loro uso; imparerete qualcosa in pi
sullereditariet e qualcosaltro sui fondamentali del linguaggio.
Inoltre lavorerete con la versione attuale di C++, C++11

(del quale il padre di C++, Bjarne Stroustrup, disse C++11 sembra


un linguaggio totalmente nuovo). Ritroverete anche alcune
particolarit legate allereditariet introdotte nella scorsa puntata.
Tutti gli esempi sfrutteranno le classi Shape e Circle gi definite.

Puntatori e puntatori smart


C++ , in pratica, unestensione di C. Questo linguaggio
supporta i puntatori tipizzati e le espressioni dei puntatori.
Un puntatore (pointer) semplicemente una variabile
che pu registrare lindirizzo di unaltra variabile in memoria,
nel qual caso si dice che il puntatore punta alla variabile.
Un puntatore pu puntare potenzialmente a qualsiasi cosa del
tipo definito. Per esempio, un puntatore a int pu registrare
lindirizzo di un qualsiasi intero, ma non di altri tipi di dato.
Cos per tutti gli altri tipi di puntatori. Essi sono utilizzati
per varie ragioni: offrono una metafora comoda e pratica
con un bonus nascosto, il valore 0 indicante che il puntatore
non punta a nulla. Luso dei puntatori potenzialmente pi
efficiente delle alternative in alcuni casi, e in altri richiesto,
per esempio, per gestire gli indirizzi della memoria allocata
dinamicamente. Tali puntatori grezzi sono un mezzo molto
potente e altrettanto pericoloso. Errori anche minimi nei
puntatori possono portare a bug seri e talvolta difficili
da trovare. Per questo, C++ li completa con alcuni cosiddetti
puntatori smart, ognuno con un ventaglio di scorciatoie per
cui, alla fine, possono essere pi semplici e sicuri da usare di
quelli grezzi. Cos come quelli offerti dalla C++ Standard
Library, un utente pu potenzialmente scrivere il proprio
puntatore smart per i propri scopi. I puntatori base sono
definiti con la seguente sintassi:
int *p_int // definisce un puntatore (variabile)

Shape

Shape

oppure
int *p_int(0);
// o int *p_int = 0; // definisce e
inizializza
p_int = &i;
// imposta p_int perch punti a i
*p_int // utilizza il valore dellintero al quale p_int sta
attualmente puntando

Puntatori a costanti
Potete avere puntatori a valori di sola lettura, per esempio
const. importante notare, comunque, che una dichiarazione
o definizione della forma
const char *p;
(che definisce p come puntatore che il compilatore
controller) utilizzata solo per la lettura del dato puntato.
In altre parole al puntatore p negato laccesso in scrittura
delloggetto al quale punta (in questo caso il target di p un
char, o un array di char, se avesse valore 0 non punterebbe
a nulla). Nel codice che usa p il puntatore stesso non const:
pu essere cambiato, ma la cosa a cui punta (un char o array
di char in questo caso) non pu essere modificata attraverso
di esso. Per oggetti di una classe MyClass con un puntatore
definito cos
MyClass my_obj;
MyClass *p = &my_obj;
// definisce un puntatore p
che punta ad un oggetto pre-esistente (my_obj) del tipo
MyClass
i membri possono essere letti
p->m // valuta il membro m delloggetto my_obj
referenziato dal puntatore
Dal momento che i data member di una classe sono
tipicamente privati e le member function pubbliche,
la convenzione
p->mf(); // invoca la member function mf delloggetto
referenziato da p

Puntatori e array
Fig.1 Loggetto base e il derivato
in memoria. Un oggetto Circle
ha tutti i data member della classe
Circle, pi quelli della classe Shape
(per ereditariet), memorizzati
come mostrato

78

Circle

Gli array e i puntatori sono legati a doppio filo. Il nome


di un array viene trattato come un puntatore costante allinizio
dei suoi elementi: cio larray viene memorizzato da qualche
parte nella memoria e il suo indirizzo viene riportato
nel puntatore. Il compilatore tratta il nome dellarray, perlopi,
come il suo indirizzo. Una copia di questo valore pu essere
cambiata, e lelemento dellarray pu essere modificato

C++
a runtime, ma la variabile (nome) con la quale larray
definito avr un valore fisso corrispondente allindirizzo
dellarray in memoria. I nomi di array sono dunque trattati
come puntatori costanti. Viceversa, i puntatori possono essere
trattati come se fossero nomi di array allo scopo di indirizzare
elementi di array. quindi possibile scrivere una definizione
di questo tipo, per esempio:
int a[10]; // a un array di 10 int
oppure
MyClass a[10];
// a un array di 10 istanze di MyClass

Puntatori come parametri


di funzioni
I puntatori possono essere passati come parametri alle
funzioni, con lovvia sintassi, definendole per esempio cos:
void foo(int *p);
oppure
void foo(const int *p);
Nel secondo esempio la chiave const si applica alla referenza
del puntatore: possibile modificare il valore di p allinterno
della funzione, ma il compilatore non permetter a p
di cambiare la propria referenza in modo diretto.
Incidentalmente, dal momento che i nomi di array sono
perlopi trattati come puntatori allinizio (elemento 0)
dellarray, gli array stessi possono essere passati come
argomenti a funzioni che ammettano un puntatore
del tipo appropriato. Per esempio, dato
MyClass arr[7];
e
void foo(MyClass *pmc);
la funzione foo() pu essere chiamata semplicemente cos:
foo(arr); // arr un puntatore allinizio di arr
I puntatori possono essere altres ritornati dalle funzioni, cos:
int *bar(); // bar() ritorna un puntatore a int

Puntatori smart
ed ereditariet

Base
Derivata
Fig.2 Una generalizzazione dellesempio in Fig.1

La Fig.1 vi mostra quello che ci si potrebbe aspettare


in memoria per un oggetto Shape e un oggetto Circle.
Ci si aspetta che ognuno abbia poco o niente pi dei data
member delloggetto stesso. Perci un oggetto Circle
ci si aspetta abbia il data member radius, r, adiacente
a quelli della classe base (Shape), ovvero le coordinate x e y.
La Fig.2 mostra la stessa cosa generalizzata, per oggetti base
e derivati. La Fig.3, nella pagina successiva, mostra come
un puntatore a classe base possa puntare alla parte
base delloggetto derivato.

Puntatori e referenze
In molti casi la scelta tra luso dei tipi di puntatori e referenze
questione di gusti o convenzione. Una differenza semantica
importante, tuttavia,
che mentre una
referenza deve
referenziare qualcosa
(un oggetto che
contenuto nella sua
definizione), un puntatore
pu puntare a nulla.
Una funzione che accetta
un parametro per referenza avr loggetto corrispondente
disponibile allinterno della sua definizione; se il parametro
fosse passato come puntatore sarebbe necessario controllare
se il suo valore diverso da 0. Viceversa, una funzione
find(), per esempio, potrebbe ritornare un valore puntatore
indicante dove loggetto stato trovato, se lo stato;
un valore di 0 indicherebbe che loggetto non stato trovato
per nulla. Questa tecnica non sarebbe disponibile se il tipo
di ritorno fosse una referenza, dal momento che questultima
deve necessariamente referenziare un oggetto.

Normalmente
i puntatori possono
puntare solo a membri
del loro tipo

I puntatori smart sono oggetti


di classi disegnati per fare qualsiasi
cosa specifichiate possano fare,
quando sono usati con le sintassi
mostrate sopra. Cio, possono
essere inizializzati per referenziare
indirizzi specifici (tipicamente
valori) quando impiegati con
loperatore * e per accedere a membri quando usati
con loperatore ->. Per i puntatori smart, queste operazioni
sono definite come funzioni. Nel contesto dellereditariet,
un oggetto derivato trattato come un tipo di (a kind of,
AKO) oggetto del suo tipo base (seppure, come avete visto,
con possibili variazioni o estensioni della versione base).
In questo modo, un puntatore al tipo base pu puntare
a un oggetto di qualsiasi tipo derivato da tale classe
(normalmente i puntatori possono puntare solo a oggetti
del loro tipo; questo comportamento rispetto ai derivati
consistente: gli oggetti dei tipi derivati sono considerati
AKO delle loro basi). Questo comportamento pu essere
spiegato anche in termini di layout della base e derivato
in memoria. Considerate le classi Shape e Circle definite
nella prima parte del tutorial, con Circle pubblicamente
derivato da Shape aggiungendo un data member per il raggio,
il proprio costruttore e la propria implementazione di draw().

I puntatori nel C++ moderno


Con lampia disponibilit dei puntatori smart e dei componenti
standard che gestiscono i dettagli di allocazione
e deallocazione dinamica della propria memoria, luso
dei puntatori base diventato sempre meno necessario.
Per esempio i contenitori standard, come i vettori, possono

79

C++
essere espansi dinamicamente per registrare elementi
mano a mano che vengono aggiunti, e i distruttori
gestiscono la deallocazione.

Vantaggi per lembedded


I puntatori possono essere usati esplicitamente in codice
di basso livello e con necessit di altissime performance,
dove il piccolo overhead e la perdita di flessibilit
dei puntatori smart non sono pi accettabili. Nella maggior
parte delle applicazioni per ci si pu affidare alla sicurezza
e convenienza dei puntatori smart, disegnati comunque
per essere efficienti e flessibili. Quando gli oggetti sono
creati dinamicamente con new viene restituito lindirizzo
al quale sono stati allocati. Le espressioni dei puntatori
appaiono quando loperatore new usato in questo
modo, e normalmente devono essere salvate; possono
offrire, piuttosto, valori per argomenti utilizzati
immediatamente (per esempio per creare un puntatore
smart) oppure essere registrate in un puntatore grezzo
e riusate nel codice dellapplicazione. I puntatori e/o
le espressioni di puntatori sono necessari anche nelluso
di codice legacy (per esempio C) che presenti un tipo
di puntatore nellinterfaccia. Possono anche essere usati
in C++, a seconda dei gusti; la tendenza generale
tuttavia, come gi detto, di usarli sempre meno
a livello dellapplicazione.

Base

Base
Derivata

Puntatore
a classe base
Fig.3 Un puntatore alla classe base pu puntare a un oggetto
del tipo della classe base o (alla parte base di) un oggetto
del tipo di una qualsiasi classe derivata dalla base

Funzioni membro
Dato un oggetto avete visto che potete invocarne una funzione
membro con la sintassi
obj.mf(); // mf() una member function della classe obj
Potete avere diverse classi nel vostro sistema
con una member function chiamata mf.
Il compilatore si occuper di recuperare
quella appropriata per loggetto (obj) interessato
nella chiamata; la effettuer, in altre parole,
in base al tipo di oggetto.

Ereditariet
con funzioni non virtuali

Tip
Ogni directory
degli esempi di
codice contiene
un file Readme.
Leggetelo per
istruzioni sulla
compilazione!

80

a Shape (Shape *) pensato per puntare a un oggetto di tipo


Shape; pu per puntare a oggetti derivati come Circle. Quando
viene invocata una funzione tramite puntatore, tale funzione
sar quella della
classe del tipo
definito dal
puntatore. Quindi
anche se il vostro
Shape* punta
a un Circle,
quando invocate
draw() con tale
puntatore verr chiamato il metodo di Shape:
Shape shp
// crea gli oggetti
Cicle c // con gli argomenti richiesti
Shape *sp(&shp);
Circle *cp(&c); sp->draw(); // chiama draw() di Shape
cp->draw();
// chiama draw() di Circle
// ma con
sp = cp; // ora sp punta ad un Circle
sp->draw();
// chiama draw() di Shape
Questo comportamento della selezione delle funzioni
in base al tipo dimostrato nel primo passo del box
della pagina successiva.

Potete avere diverse


classi nel sistema
con una funzione
membro chiamata mf

In casi dove si abbiano classi relazionate tramite ereditariet,


le regole di cui sopra si applicano comunque. In particolare,
se la dichiarazione di una data funzione definita a diversi
livelli di un albero di ereditariet (o rete), allora:
base_obj.mf();
// chiama mf() di Base
derived_obj.mf();
// chiama mf() di Derived
Se la classe Derived non definisce mf() allora verr chiamata
la funzione della classe pi vicina da cui eredita che implementa
tale funzione. In modo simile, se invocate una funzione come:
derived_obj.ClasseParent::mf();
non verr richiesto che mf() sia effettivamente implementata
da ClasseParent, che potrebbe semplicemente ereditare
la definizione a sua volta. Il compilatore utilizzer la funzione
implementata dalla classe chiamata o dalla pi vicina che
la definisce; se non ne vengono trovate (per esempio non
definita nemmeno nella Base) scatener un errore
di compilazione. Se utilizzate puntatori a oggetti, allo stesso
modo la funzione invocata sar in base al tipo di puntatore
(e non al tipo delloggetto puntato). Per esempio, un puntatore

Ereditariet
con le funzioni virtuali
Spesso, comunque, il suddetto comportamento non quello
desiderato. Quello che volete invocare la funzione rilevante
per il tipo di oggetto. In casi come quello dellesempio, dove
state iterando con un puntatore a classe base, vorreste che

C++
la funzione fosse scelta in base al tipo delloggetto puntato,
non del puntatore. in maniera simile, potreste avere una
funzione che accetta un parametro di una classe base.
Se lo passaste per valore avreste loggetto classe base
allinterno della funzione, malgrado gli passiate un qualsiasi
derivato. Se passate largomento per puntatore, o riferimento,
al tipo base, e le funzioni rilevanti sono virtuali, allora le funzioni
da chiamare sono determinate in base al tipo delloggetto
puntato o riferito. Questo mostrato nel passo 2 del box
in questa pagina.

Override in azione
in conclusione, quando una funzione ridefinita a vari livelli
(prevalenza o override) in una gerarchia di ereditariet,
la funzione rilevante pu essere chiamata in base al tipo
di oggetto, ammesso che la funzione sia dichiarata come virtuale
nella classe base. Questo si applica in due scenari comuni:
avete una collezione eterogenea di oggetti su cui iterare
con un puntatore a classe base, e volete chiamare la versione
appropriata della funzione prevalente in base al tipo di oggetto;
state passando un oggetto come argomento puntatore
o riferimento a una funzione, e il tipo di parametro usato
un puntatore/riferimento alla classe base.
il concetto e la terminologia usati qui riguardano una funzione
prevalente a vari livelli in una gerarchia dereditariet.
in questo caso la funzione ha un nome e una sequenza di tipi
di parametro a tutti questi livelli (ovvero ha la stessa segnatura).
Anche i tipi di ritorno sono identici, eccetto il fatto che possano
differire restando allinterno, a loro volta, di una gerarchia
dereditariet. Questo diverso dal sovraccarico (overload),
dove le funzioni hanno un nome comune ma diverse
sequenze di parametri.

Esempi
Gli esempi illustrano alcune caratteristiche del C++
proseguendo il disegno, allo scopo di renderizzarlo finalmente

a schermo. potete modellare il vostro disegno creando


una collezione arbitraria di forme e chiedendo a queste
ultime di disegnarsi; effettuerete questo iterando sulle
forme e chiamando la funzione appropriata per ognuna
(potrebbe servire effettivamente, per esempio, ogni volta
che la finestra viene spostata o ridimensionata). La libreria
C++ standard offre diverse classi contenitori e alcuni
algoritmi utili che possono essere usati in questo caso.
Volete che il contenitore possa memorizzare una collezione
eterogenea (Circle, rectangle, ecc.). Qualsiasi contenitore
standard C++ pu memorizzare una collezione di elementi
di tipo identico (in quel caso si chiama collezione omogenea).
potete, tuttavia, gestire le collezioni eterogenee se il
contenitore memorizza una collezione di puntatori
od oggetti simili a puntatori, ognuno puntante a un oggetto
derivante da Shape. in questo modo il contenitore sta
registrando logicamente una collezione eterogenea, mentre
fisicamente registra una collezione omogenea di oggetti
(elementi che puntano a oggetti).

Compilare gli esempi


Come prima, il codice desempio che trovate nel DVD allegato
(nel lato B) fornito di file readme.txt per mostrare
i comandi rilevanti usando g++. La maggior parte degli
esempi illustreranno specificamente le particolarit di C++11,
quindi per compilarli dovreste avere un compilatore
ragionevolmente moderno, g++ 4.6.3 o superiore.
La libreria C++ offre effettivamente da tempo diverse
classi per le collezioni con alcune particolarit utili.
Gli esempi di codice mostrano luso dei vettori per
memorizzare logicamente collezioni eterogenee di forme
e, per esempio, per iterare su tali collezioni, processando
gli elementi contenuti. illustrano anche luso di funzioni
polimorfiche e non polimorfiche, e varie altre caratteristiche
del linguaggio e della Standard Library. ora non vi resta
che mettervi a programmare! LXP

Passo passo Compilazione ed esecuzione

Creare gli oggetti

utilizzate loperatore new per creare una


collezione eterogenea di oggetti, come mostrato
nella directory Shapes_2. Scrivete e compilate
il codice C++11 con g++. Leggete il file
Readme.txt e il file main.cpp. Costruite
una collezione eterogenea di oggetti utilizzando
le istruzioni in readme e osservate
il comportamento del programma (a questo
punto non lanciate ancora main_1.cpp).

Polimorfismo

Seguendo le istruzioni nella directory Shapes_3


potete vedere come ottenere il polimorfismo.
Compilate ed eseguite main.cpp e main_1.cpp
comparandone loutput con il codice
corrispondente al passo 1.

Creare una collezione

A questo punto potete creare una collezione


di oggetti basata su input utente a runtime. per farlo,
usate i le presenti nella directory Shapes_4.

81

Potrebbero piacerti anche