MindView Inc.

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[
Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

traduzione italiana e adattamento a cura di Mauro Sorbo

5: Nascondere l'implementazione

Una tipica libreria C contiene una struct ed alcune funzioni associate per agire su essa. Si è visto come il C++ prenda funzioni che sono associate concettualmente e le renda associate letteralmente

mettendo le dichiarazioni della funzione  dentro lo scope della struct, cambiando il modo in cui le funzioni vengono chiamate per una struct, eliminando il passaggio dell'indirizzo di struttura come primo argomento ed aggiungendo un nuovo tipo di nome al programma ( quindi non si deve creare un typedef per la struct).

Tutto ciò aiuta ad organizzare il proprio codice e lo rende più facile da scrivere e leggere. Tuttavia ci sono altri problemi quando si creano librerie in C++, specialmente problematiche riguardanti il controllo e la sicurezza. Questo capitolo prende in esame il problema dei limiti nelle strutture.

Fissare i limiti

In qualsiasi relazione è importante avere dei limiti che sono rispettati da tutte le parti coinvolte. Quando si crea una libreria, si stabilisce una relazione con il programmatore client  che  usa quella libreria per costruire un'applicazione o un'altra libreria.

In una struct del C, come per la maggior parte delle cose in C, non ci sono regole. I programmatori client possono fare qualsiasi cosa vogliono e non c'è modo di forzare nessun particolare comportamento. Per esempio, nell'ultimo capitolo anche se si capisce l'importanza delle funzioni chiamate inizialize() e cleanup(), il programmatore client ha l'opzione di non chiamare quelle funzioni ( osserveremo un miglior approccio nel prossimo capitolo). E anche se davvero si preferirebbe che il programmatore client non manipolasse direttamente alcuni dei membri della nostra struct, in C non c'è modo di prevenirlo. Ogni cosa è palese al mondo.

Ci sono due ragioni del perchè si deve controllare l'accesso ai membri. La prima serve a tenere lontane le mani del programmatore client dalle cose che non devono essere toccate, parti che sono necessarie al funzionamento interno dei tipi di dato, ma non parti dell'interfaccia di cui il programmatore client ha bisogno per risolvere i propri problemi. Questo è davvero un servizio ai programmatori client perchè essi possono facilmente capire cos'è importante per loro e cosa possono ignorare.

La seconda ragione per il controllo d'accesso è permettere al progettista della libreria di cambiare la struttura interna senza preoccuparsi di come influenzerà il programmatore client. Nell'esempio dello Stack nell'ultimo capitolo, si può volere allocare la memoria in grandi blocchi, per rapidità, invece di creare spazio ogni volta che un elemento viene aggiunto. Se l'interfaccia e l'implementazione sono chiaramente separate e protette, si può ottenere ciò richiedendo solo un nuovo link dal programmatore client.


Il controllo d'accesso del C++

Il C++ introduce tre nuove parole riservate per fissare i limiti in una struttura: public, private e protected. Il loro uso e il loro significato sono molto semplici. Questi access specifiers (specificificatori di accesso) sono usati solo in una dichiarazione di struttura e cambiano i limiti per tutte le dichiarazioni che li seguono. In qualsiasi momento si usa un specificatore d'accesso, esso deve essere seguito dai due punti. 

Per public s'intende che tutte le dichiarazioni dei membri sono disponibili a tutti. I membri public sono come quelli dello struct. Per esempio, le seguenti dichiarazioni struct sono identiche:  

//: C05:Public.cpp
// Public è come la struct del C

struct A {
  int i;
  char j;
  float f;
  void func();
};

void A::func() {}

struct B {
public:
  int i;
  char j;
  float f;
  void func();
};

void B::func() {}  

int main() {
  A a; B b;
  a.i = b.i = 1;
  a.j = b.j = 'c';
  a.f = b.f = 3.14159;
  a.func();
  b.func();
} ///:~

La parola chiave private, invece, significa che nessuno eccetto noi può accedere a quel membro, il creatore del tipo, dentro i membri funzione di quel tipo. private è un muro di mattoni tra noi  e il programmatore client, se qualcuno prova ad accedere al membro private, avrà un errore di compilazione. In struct B  nell'esempio sopra, si potrebbe voler nascondere porzioni della rappresentazione ( cioè il membro data) , accessibile solo per noi:

//: C05:Private.cpp
// Fissare i limiti

struct B {
private:
  char j;
  float f;
public:
  int i;
  void func();
};

void B::func() {
  i = 0;
  j = '0';
  f = 0.0;
};

int main() {
  B b;
  b.i = 1;    // OK, public 
//!  b.j = '1';  // vietato, private
//!  b.f = 1.0;  // vietato, private
} ///:~

Sebbene func() può accedere a qualsiasi membro di B ( poichè func() è un membro di B, in questo modo ha automaticamente il permesso), un' ordinaria funzione globale come main() non può. Naturalmente, neanche le funzioni membro delle altre strutture. Solamente le funzioni che sono chiaramente dichiarate nella dichiarazione della struttura (il "contratto") possono avere accesso ai membri private.

Non c'è nessun ordine richiesto per gli specificatori d'accesso e possono apparire pù di una volta. Essi influenzano tutti i membri dichiarati dopo di loro e prima del prossimo specificatore d'accesso.

protected

L'ultimo specificatore d'accesso è protected. Esso funziona come private, con un' eccezione che in realtà non può essere spiegata ora: le strutture "ereditate" ( le quali non possono accedere a membri private ) hanno il permesso di accedere ai membri protected. Questo diverrà più chiaro nel capitolo 14 quando l'ereditarietà verrà introdotta. Per il momento si consideri protected come private

Friends

Cosa succede se si dà permesso di accesso ad una funzione che non è un membro della struttura corrente? Ciò si ottiene dichiarando quella funzione friend dentro la dichiarazione della struttura. È importante che la dichiarazione avvenga dentro la dichiarazione della struttura perchè si deve poter ( e anche il compilatore) leggere la dichiarazione della struttura e vedere tutte le dimensioni ed il comportamento dei tipi di dato. Una regola molto importante in tutte le relazione è: " Chi può accedere alla mia implementazione privata?".

La classe controlla quale codice ha accesso ai suoi membri. Non c'è un modo magico di intrufolarsi dal di fuori se non si è un friend; non si può dichiarare una nuova classe e dire: "Ciao, io sono un amico di Bob!" ed aspettarsi di vedere i membri private e protected di Bob

Si può dichiarare una funzione globale come un friend e si può dichiarare anche una funzione membro di un'altra struttura o perfino una struttura intera come un friend. Ecco qui un esempio:

//: C05:Friend.cpp
// Friend permette un accesso speciale
// Dichiarazione (specificazione di tipo incompleta)
struct X;

struct Y {
  void f(X*);
};

struct X { // Definizione
private:
  int i;
public:
  void inizializza();
  friend void g(X*, int); // friend globale
  friend void Y::f(X*);  // Struct membro friend 
  friend struct Z; // L'intera struct è un friend
  friend void h();
};

void X::inizializza() { 
  i = 0; 
}

void g(X* x, int i) { 
  x->i = i; 
}

void Y::f(X* x) { 
  x->i = 47; 
}

struct Z {
private:
  int j;
public:
  void inizializza();
  void g(X* x);
};

void Z::inizializza() { 
  j = 99;
}

void Z::g(X* x) { 
  x->i += j; 
}

void h() {
  X x;
  x.i = 100; // manipulazione diretta del dato
}

int main() {
  X x;
  Z z;
  z.g(&x);
} ///:~

struct Y ha una funzione membro f() che modificherà un oggetto del tipo X. Questo è un po' un rompicapo perchè il compilatore del C++ richiede di dichiarare ogni cosa prima di riferirsi a ciò, quindi la struct Y deve essere dichiarata prima degli stessi membri come un friend nel struct X. Ma per essere dichiarata Y::f(X*), deve essere prima dichiarata la struct X

Ecco qui la soluzione. Si noti che Y::f(X*) prende l'indirizzo di un oggetto X. Questo è critico perchè il compilatore sa sempre come passare un indirizzo, il quale è di lunghezza fissa indifferente all'oggetto che è passato, anche se non ha tutte le informazioni  circa la lunghezza del tipo. Se si prova a passare l'intero oggetto, comunque, il compilatore deve vedere l'intera struttura definizione di X per conoscere la lunghezza e come passarla, prima di permettere di far dichiarare una  funzione come Y::g(X).

Passando un'indirizzo di un X, il compilatore permette di fare una specificazione di tipo incompleta di X prima di dichiarare Y::f(X*). Ciò avviene nella dichiarazione: 

struct X;

Questa semplice dichiarazione dice al compilatore che c'è una struct con quel nome, quindi è giusto riferirsi ad essa se non c'è bisogno di conoscere altro che il nome.


Ora, in struct X, la funzione Y::f(X*) può essere dichiarata come un friend senza nessun problema. Se si provava a dichiararla prima il compilatore avrebbe visto la piena specificazione per Y e avrebbe segnalato un errore. Questo è una caratteristica per assicurare consistenza ed eliminare i bachi. 


Notare le altre due funzioni friend. La prima dichiara una funzione ordinaria globale g() come un friend. Ma g() non è stata precedentemente dichiarata globale! Risulta che friend può essere usato in questo modo per dichiarare simultaneamente la funzione e darle uno stato friend. Ciò si estende alle intere strutture:

friend struct Z;

è una specificazione di tipo incompleta per Z e dà all'intera struttura lo stato friend.


Friends nidificati

Usare una struttura nidificata non dà automaticamente l'accesso ai membri private. Per compiere ciò, si deve seguire una particolare forma: primo, dichiarare( senza definire ) la struttura nidificata, dopo la si dichiara come un friend, ed infine si definisce la struttura. La definizione della struttura deve essere separata dalla dichiarazione di friend, altrimenti sarebbe vista dal compilatore come un non membro. Ecco qui un esempio:

//: C05:NestFriend.cpp
// friend nidificati
#include <iostream>
#include <cstring> // memset()
using namespace std;
const int sz = 20;

struct Contenitore {
private:
  int a[sz];
public:
  void inizializza();
  struct Puntatore;
  friend struct Puntatore;
  struct Puntatore {
  private:
    Contenitore* h;
    int* p;
  public:
    void inizializza(Holder* h);
     // per muoversi nel vettore
    void prossimo();
    void precedente();
    void primo();
    void ultimo();
    // accedere ai valori:
    int leggi();
    void imposta(int i);
  };
};

void Contenitore::inizializza() {
  memset(a, 0, sz * sizeof(int));
}

void Contenitore::Puntatore::inizializza(Holder* rv) {
  h = rv;
  p = rv->a;
}

void Contenitore::Puntatore::prossimo() {
  if(p < &(h->a[sz - 1])) p++;
}

void Contenitore::Puntatore::precedente() {
  if(p > &(h->a[0])) p--;
}

void Contenitore::Puntatore::primo() {
  p = &(h->a[0]);
}

void Contenitore::Puntatore::ultimo() {
  p = &(h->a[sz - 1]);
}

int Contenitore::Puntatore::leggi() {
  return *p;
}

void Contenitore::Puntatore::imposta(int i) {
  *p = i;
}

int main() {
  Contenitore h;
  Contenitore::Puntatore hp, hp2;
  int i;

  h.inizializza();
  hp.inizializza(&h);
  hp2.inizializza(&h);
  for(i = 0; i < sz; i++) {
    hp.imposta(i);
    hp.prossimo();
  }
  hp.primo();
  hp2.ultimo();
  for(i = 0; i < sz; i++) {
    cout << "hp = " << hp.leggi()
         << ", hp2 = " << hp2.leggi() << endl;
    hp.prossimo();
    hp2.precedente();
  }
} ///:~

Una volta dichiarato Puntatore è concesso l'accesso ai membri private di Contenitore scrivendo:

friend struct Puntatore;

La struct Contenitore contiene un vettore di int e di Puntatore  che permette loro l'accesso. Poichè Puntatore è fortemente associato con Contenitore, è ragionevole farne un membro della struttura del Contenitore. Ma poichè Puntatore è una classe separata dal Contenitore, si può farne più di uno in main() ed usarlo per scegliere differenti parti del vettore. Puntatore è una struttura invece di puntatori C grezzi, quindi si può garantire che punteranno sempre dentro Contenitore.

La funzione della libreria Standard C memset() ( in <cstring> ) è usata per convenienza nel programma di sopra. Essa imposta tutta la memoria ad un indirizzo di partenza ( il primo argomento ) un particolare valore ( il secondo argomento ) per n byte dopo  l'indirizzo di partenza ( n è il terzo argomento ). Naturalmente, si potrebbe semplicemente usare un ciclo, ma memset() è disponibile, testato con successo ( così è meno probabile che si introduca un errore) e probabilmente più efficiente di quando lo si codifica a mano.


E' puro?

La definizione di classe dà una segno di verifica, quindi si può vedere guardando la classe quali funzioni hanno il permesso di modificare le parti private della classe. Se una funzione è un friend, significa che non è un membro, ma si vuole dare un permesso di modificare comunque dati private e deve essere elencata nella definizione della classe così che chiunque possa vedere che è una delle funzioni privilegiate.  

Il C++ è un linguaggio orientato a oggetti ibrido, non un puro, e friend fu aggiunto per aggirare i problemi pratici che sorgevano. 


Layout dell'oggetto

Il capitolo 4 afferma che una struct scritta per un compilatore C e poi compilata col C++ rimarrebbe immutata. Ciò è riferito al layout dell'oggetto della struct, cioè, dove lo spazio per le variabili individuali è posizionato nella memoria allocata per l'oggetto. Se il compilatore del C++ cambia il layout della struct del C, allora verrebbe corrotto qualsiasi codice C scritto in base alla conoscenza delle posizioni delle variabili nello struct

Quando si iniziano ad usare gli specificatori d'accesso, tuttavia, ci si sposta completamente nel regno del C++ e le cose cambiano un po'. Con un particolare "blocco di accesso" (un gruppo di dichiarazioni delimitate dagli specificatori d'accesso ) è garantito che le variabili siano posizionate in modo contiguo, come in C. Tuttavia i blocchi d'accesso possono non apparire nell'oggetto nell'ordine in cui si dichiarano. Sebbene  il compilatore posizionerà di solito i blocchi esattamente come si vedono, non c'è alcuna regola su ciò, perchè una particolare architettura di macchina e/o ambiente operativo forse può avere un esplicito supporto per il private e protected che potrebbe richiedere che questi blocchi siano posti in locazioni speciali di memoria. La specifica del linguaggio non vuole restringere questa possibilità.

Gli specificatori d'accesso sono parte della struttura e non influenzano gli oggetti creati dalla struttura. Tutte le informazioni degli specificatori d'accesso scompaiono prima che il programma giri; generalmente durante la compilazione. In un programma funzionante, gli oggetti diventano " regioni di memoria" e niente più. Se veramente lo si vuole, si possono violare tutte le regole ed accedere alla memoria direttamente, come si può farlo in C. Il C++ non è progettato per preventivarci dal fare cose poco saggie. Esso ci fornisce solamente di una alternativa più facile e molto desiderata.

In generale, non è una buona idea dipendere da qualcosa che è un' implementazione specifica quando si sta scrivendo un programma. Quando si devono avere dipendenze di specifiche di implementazione, le si racchiudano dentro una struttura cosicchè qualsiasi modifica per la portabilità è concentrata in un solo posto.

La classe

Il controllo d'accesso è spesso detto occultamento dell'implementazione. Includere funzioni dentro le strutture ( ciò è spesso detto incapsulazione[36]), produce un tipo di dato con caratteristiche e comportamenti, ma l'accesso di controllo pone limiti con quel tipo di dato, per due importanti ragioni. La prima è stabilire cosa il programmatore client può e non può usare. Si può costruire un proprio meccanismo interno nella struttura senza preoccuparsi che il programmatore client penserà che questi meccanismi sono parte dell'interfaccia che dovrebbero essere usati.  

Questa problematica porta direttamente alla seconda ragione, che riguarda la separazione dell'interfaccia dall'implementazione. Se la struttura viene usata in un insieme di programmi, ma i programmatori client non possono fare nient'altro che mandare messagi all'interfaccia pubblica, allora si può cambiare tutto ciò che è private senza richiedere modifiche al codice.

L'incapsulamento ed il controllo d'accesso, presi insieme, sono qualcosa di più che una struct del C. Ora siamo nel mondo della programmazione orientata agli oggetti, dove una struttura descrive una classe di oggetti come se si descriverebbe una classe di pesci o una classe di uccelli: ogni oggetto appartenente a questa classe condividerà le stesse caratteristiche e comportamenti. Ecco cosa è diventata una dichiarazione di struttura, una descrizione del modo in cui tutti gli oggetti di questo tipo appariranno e si comporteranno.

Nell'originale linguaggio OOP, Simula-67, la parola chiave class fu usata per descrivere un nuovo tipo di dato. Ciò apparentemente ispirò Stroustrup a scegliere la stessa parola chiave per il C++, per enfatizzare che questo era il punto focale dell'intero linguaggio: la creazione di nuovi tipi di dato che sono qualcosa in più che le struct del C con funzioni. Ciò certamente sembra una adeguata giustificazione per una nuova parola chiave. 

Tuttavia l'uso di una class nel C++ si avvicina ad essere una parola chiave non necessaria. E' identica alla parola chiave struct assolutamente in ogni aspetto eccetto uno: class è per default private, mentre struct è public. Ecco qui due strutture che producono lo stesso risultato:   


//: C05:Class.cpp
// Similitudini tra struct e class

struct A {
private:
  int i, j, k;
public:
  int f();
  void g();
};

int A::f() { 
  return i + j + k; 
}

void A::g() { 
  i = j = k = 0; 
}

// Identici risultati sono prodotti con:

class B {
  int i, j, k;
public:
  int f();
  void g();
};

int B::f() { 
  return i + j + k; 
}

void B::g() { 
  i = j = k = 0; 
} 

int main() {
  A a;
  B b;
  a.f(); a.g();
  b.f(); b.g();
} ///:~

La classe è il concetto fondamentale OOP in C++. È una delle parole chiave che non sarà indicata in grassetto in questo libro, diventa noiso vederla ripetuta. Il passaggio alle classi è così importante che sospetto che Stroustrup avrebbe preferito eliminare struct, ma il bisogno di compatibilità con il codice esistente non lo ha permesso.

Molti preferiscono uno stile di creazione di classi che è più simile alla struct che alla classe, perchè si può non usare il comportamento private della classe per default iniziando con public:


class X {
public:
  void funzione_di_interfaccia();
private:
  void funzione_privata();
  int rappresentazione_interna;
}; 

La logica dietro ciò sta nel fatto che il lettore è interessato a vedere prima i membri più importanti, poi può ignorare tutto ciò che è private. Infatti, le sole ragioni per cui tutti gli altri membri devono essere dichiarati nella classe sono dovute al fatto che così il compilatore conosce la grandezza degli oggetti e li può allocare correttamente e quindi garantire consistenza.

Gli esempi di questo libro, comunque, porrà i membri private per prima :

class X {
  void private_function();
  int internal_representation;
public:
  void interface_function();
}; 

qualcuno persino arricchisce i propri nomi nomi privati:

class Y {
public:
  void f();
private:
  int mX;  // nome "Self-decorated" 
}; 

Poichè mX è già nascosto nello scope di Y, la m ( sta per "membro" ) non è necessaria.Tuttavia, nei progetti con molte variabili globali ( cosa da evitare, ma a volte è inevitabile nei progetti esistenti), è di aiuto poter distinguere all'interno di una definizione di funzione membro quale dato è globale e quale è un membro.


Modificare Stash per usare il controllo d'accesso

Ha senso riprendere l'esempio del Capitolo 4 e modificarlo per usare le classi ed il controllo d'accesso. Si noti la porzione d'interfaccia del programmatore client è ora chiaramente distinguibile, quindi non c'è possibilità da parte del programmatore client di manipolare una parte della classe che non dovrebbe.

//: C05:Stash.h
// Convertita per usare il controllo d'accesso
#ifndef STASH_H
#define STASH_H

class Stash {
  int size;      // Dimensione di ogni spazio
  int quantity;  // Numero dello spazio libero
  int next;      // prossimo spazio libero
 // array di byte allocato dinamicamente:
  unsigned char* storage;
  void inflate(int increase);
public:
  void initialize(int size);
  void cleanup();
  int add(void* element);
  void* fetch(int index);
  int count();
};
#endif // STASH_H ///:~

La funzione inflate() è stata resa private perchè è usata solo dalla funzione add() ed è in questo modo parte della implementazione sottostante, non dell' interfaccia. Ciò significa che, in seguito, si può cambiare l'implementazione sottostante per usare un differente sistema per la gestione della memoria. 

Tranne l'include del file, l'header di sopra è l'uncia cosa che è stata cambiata per questo esempio. Il file di implementanzione ed il file di test sono gli stessi.


Modificare Stack per usare il controllo d'accesso

Come secondo esempio, ecco qui Stack trasformato in una classe. Ora la struttura nidificata data è private, cosa non male perchè assicura che il programmatore client non dovrà mai guardarla e non dipenderà dalla rappresentazione interna di Stack:

//: C05:Stack2.h
// struct nidificate tramite linked list
#ifndef STACK2_H
#define STACK2_H

class Stack {
  struct Link {
    void* data;
    Link* next;
    void initialize(void* dat, Link* nxt);
  }* head;
public:
  void initialize();
  void push(void* dat);
  void* peek();
  void* pop();
  void cleanup();
};
#endif // STACK2_H ///:~

Come prima, l'implementazione non cambia e quindi non è ripetuta qui. Anche il test è identico. L'unica cosa che è stata cambiata è la robustezza dell'interfaccia della classe. Il valore reale del controllo d'accesso è impedire di oltrepassare i limiti durante lo sviluppo. Infatti, il compilatore  è l'unico che conosce il livello di protezione dei membri della classe. Nessuna informazione del controllo di accesso arriva al linker, tutti i controlli di protezione sono fatti dal compilatore.

Si noti che l'interfaccia presentata al programmatore client è adesso veramente quella di uno stack di tipo push-down. È implementato come una linked list, ma la si può cambiare senza influenzare il programmatore client che interagisce con essa.

Gestire le classi

Il controllo d'accesso in C++ permette di separare l'interfaccia dall'implementazione, ma l'occultamento dell'implementazione è solamente parziale. Il compilatore deve vedere ancora le dichiarazioni di tutte le parti di un oggetto per crearlo e manipolarlo correttamente. Si potrebbe immaginare un linguaggio di programmazione che richiede solo un'interfaccia pubblica di un oggetto e permette di nascondere l'implementazione privata, ma il C++ esegue la maggior parte dei controlli sul tipo staticamente ( a tempo di compilazione) . Ciò significa che si saprà subito se c'è un errore. Significa anche che il proprio programma è più efficiente. Tuttavia, includere l'implementazione privata ha due effetti: l'implementazione è visibile anche se non è accessibile e può richiedere un inutile ricompilazione.

Nascondere l'implementazione

Alcuni progetti non possono permettersi di avere le loro implementazioni visibili al programmatore client. Essa può mostrare informazioni strategiche in un file header di libreria che un'azienda non vuole mostrare alla concorrenza. Si può star lavorando su un sistema dove la sicurezza è un requisito, un algoritmo di criptazione per esempio, e non si vuole esporre indizzi in un file header che potrebbero aiutare a crackare il codice. Oppure si può volere mettere il codice in un ambiente ostile, dove i programmatori accedono direttamente ai componenti privati, usando puntatori e casting. In tutte queste situazioni, serve avere la struttura compilata dentro un file d'implementazione piuttosto che esposta in un file header.


Ridurre la ricompilazione

Il project manager del proprio ambiente di programmazione ricompilerà un file se esso è toccato ( cioè, modificato) oppure se un altro file dipende da esso, cioè se è modificato un file header. Questo significa che ogni volta che si fa un cambiamento ad una classe, non importa se alla sua interfaccia pubblica o ai membri privati, si forzerà una ricompilazione di qualsiasi cosa che include quel header file. Ciò è spesso detto fragile base-class problem ( problema di classe base fragile). Per un progetto grosso nelle prime fasi ciò può essere ingombrante perchè l'implementazione sottostante può cambiare spesso; se il progetto è molto grande, il tempo di compilazione può proibire cambiamenti rapidi.

La tecnica per risolvere ciò è spesso detta handle classes oppure "Cheshire cat"[37], ogni cosa tranne l'implementazione scompare fatta eccezione di un singolo puntatore, lo "smile". Il puntatore si riferisce ad una struttura la cui definizione è nel file di implementazione con tutte le definizioni delle funzioni membro. Quindi, finchè l'interfaccia non viene toccata, l'header file non è modificato. L'implementazione può cambiare e solo il file di imlementazione deve essere ricompilato e relinkato con il progetto.

Ecco qui un semplice esempio dimostrante la tecnica. Il file principale contiene solo l'interfaccia pubblica e un singolo puntatore di un una incompleta classe specificata :

//: C05:Handle.h
// handle classes

#ifndef HANDLE_H
#define HANDLE_H

class Handle {
  struct Cheshire; // solo la dichiarazione della classe
  Cheshire* smile;
public:
  void inizializza();
  void pulisci();
  int leggi();
  void cambia(int);
};
#endif // HANDLE_H ///:~

Questo è tutto quello che il programmatore client può vedere. La linea

struct Cheshire;

è una incomplete type specification ( specificazione incompleta di tipo) o una dichiarazione di classe ( una definizione di classe include il corpo di una classe). Esso dice al compilatore che il Cheshire è un nome di struttura, ma non fornisce dettagli circa la struct. Questa è un' informazione sufficiente per creare un puntatore alla struct, non si può creare un oggetto prima che sia stata fornito il corpo della struttura. In questa tecnica il corpo della struttura è nascosto nel file di implementazione:

//: C05:Handle.cpp {O}
// implementazione del Handle 
#include "Handle.h"
#include "../require.h"

// Definizione dell'implementazione del Handle:
struct Handle::Cheshire {
  int i;
};

void Handle::inizializza() {
  smile = new Cheshire;
  smile->i = 0;
}

void Handle::pulisci() {
  delete smile;
}

int Handle::leggi() {
  return smile->i;
}

void Handle::cambia(int x) {
  smile->i = x;
} ///:~

Cheshire è una struttura nidificata, quindi deve essere definita con la risoluzione dello scope:

struct Handle::Cheshire {

Nel Handle::inizializza( ), il salvataggio è localizzato per una struttura Cheshire, e nel Handle::pulisci( ) questo salvataggio è rilasciato. Questo salvataggio è usato al posto di tutti gli elementi dato che normalmente si metterebbero nella sezione private della classe. Quando si compila Handle.cpp, questa definizione di struttura è celata in un oggetto dove nessuno può vederla. Se si cambiano gli elementi di Cheshire, l'unico file che deve essere ricompilato è Handle.cpp perchè il file principale non viene modificato. 

L'uso di Handle è identico all'uso di qualsiasi classe: si include l'header, si creano oggetti e si mandano messaggi.

//: C05:UseHandle.cpp
//{L} Handle
// Usare  la  Handle class
#include "Handle.h"

int main() {
  Handle u;
  u.inizializza();
  u.leggi();
  u.cambia(1);
  u.pulisci();
} ///:~

L'unica cosa a cui il programmatore client può accedere è l'interfaccia pubblica, quindi finchè l'implementazione è l'unica cosa che cambia, il file sopracitato non ha bisogno mai di essere ricompilato. Dunque sebbene questo non è un perfetto occultamento dell'implementazione, esso è una gran miglioramento. 

Sommario

Il controllo d'accesso in C++ dà un prezioso controllo al creatore di una classe. Gli utenti della classe possono chiaramente vedere cosa esattamente possono usare e cosa ignorare. Ancor più importante è l' abilità di assicurare che nessun programmatore client diventi dipendente da qualche parte della implementazione sottostante della classe. Se si conosce ciò come creatore di una classe , si può cambiare l'implementazione sottostante con la consapevolezza che nessun programmatore client sarà penalizzato dai cambiamenti, perchè essi non possono accedere a quella parte della classe.

Quando si ha la competenza di cambiare l' implementazione sottostante, non solo si può migliorare il proprio progetto dopo un po', ma si ha anche la libertà di commettere errori. Non è importante come accuratamente si pianifichi e progetti, si faranno errori. Sapendo ciò è relativamente sicuro fare questi errori e ciò significa che si sarà più sperimentatori, si imparerà più velocemente e si finira il proprio progetto più presto.  

L'interfaccia pubblica di una classe è ciò che il programmatore client vede, quindi è la parte più importante della classe per fare bene durante l'analisi e il disegno. Ma anche qui c'è margine per cambiare. Se non si ottiene la giusta interfaccia la prima volta, si possono aggiungere più funzioni mantendo quelle che i programmatori client hanno già usato nel loro codice.


Esercizi

Le soluzioni degli esercizi selezionati possono essere trovate nel documento elettronico The Thinking in C++ Annotated Solution Guide , disponibile per una piccola somma su www.BruceEckel.com

  1. Creare una classe con membri dato e membri di funzione public,private e protected. Creare un oggetto di questa classe e vedere che tipo di messaggio del compilatore si ottiene quando si prova ad accedere ai membri di tutte le classi.
  2. Scrivere una struct chiamata Lib che contiene 3 stringhe a,b e c. Nel main() creare un oggetto del Lib chiamato x ed assegnare a   x.a, x.b, e x.c. Stampare i valori. Ora rimpiazzare a,b e c con un vettore di string s[3]. Mostrare che il nostro codice in main( ) interrompe come un risultato di un cambiamento. Adesso creare una class chiamata Libc, con oggetti   private string a,b, e c, e le funzioni del membro.
  3. Creare una classe ed una funzione globale friend che manipola dati private in una classe.
  4. Scrivere due classi, una che ha una funzione membro che prende un puntatore ad un oggetto di un altra classe. Creare instanze di entrambi gli oggetti in main() e chiamare la funzione membro in ogni classe.
  5. Creare tre classi. La prima classe contiene dati private e fornisce privilegi ad una seconda classe ed ad una funzione membro della terza classe. Nel main(), dimostrare che tutte funzionano correttamente.
  6. Creare una classe Hen. Dentro essa annidare una classe Nest. Dentro Nest piazzare una classe Egg. Ogni classe dovrebbe avere una funzione membro display(). Nel main(), creare un'istanza di ogni classe e chiamare la funzione display() per ognuno.
  7. Modificare l'esercizio 6 in modo che Nest ed Egg contengano ognuno dati private. Rendere friend per permettere che le classi incluse possano accedere i dati privati.
  8. Creare una classe con i membri dato distribuiti tra numerose sezioni public, private e protected. Aggiungere una funzione membro showMap() che stampa i nomi di ognudo di questi membri dato ed i loro indirizzi. Se possibile, compilare ed eseguire questo programma su più di un compilatore e/o computer e/o sistema operativo per vedere se ci sono differenze di layout nell'oggetto.
  9. Copiare l'implementazione e i file di test per Stash del Capitolo 4 in modo che si può compilare e testare Stash.h in questo capitolo.
  10. Piazzare gli oggetti della classe Hen dell'esercizio 6 in uno Stash. Accedere ad esse e stamparli ( se ancora non si è fatto così, bisogna aggiungere Hen::print( )).
  11. Modificare Cheshire in Handle.cpp e verificare che il proprio manager del progetto ricompila e linka solo questo file, ma non ricompila UseHandle.cpp.
  12. Creare una classe StackOfInt ( uno stack che gestisce interi) usando la tecnica "Cheshire cat", che nasconde la struttura dei dati a basso livello che si usa per memorizzare gli elementi in una classe chiamata StackImp. Implementare due versioni di StackImp: una che usa un array di int a lunghezza fissa e una che usa vector<int>. Si usi una dimensione grande in modo da non preoccuparsi di incrementare la dimensione dell'array nella prima versione. Notare che la classe StackOfInt.h non deve essere cambiata con StackImp.


[36] Come notato prima, a volte il controllo di accesso è indicato come incapsulamento.

[37] Questo nome è attribuito a John Carolan, uno dei primi pionieri del C++, e naturalmente, Lewis Carroll. Questa tecnica può anche essere vista come un tipo di design pattern "bridge", descritto nel Volume 2.

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
Ultimo Aggiornamento: 08/02/2003