-- Introduzione
-- Obiettivi
Implementazione pratica
-- Dichiarazione di classe
Ereditarietà

Progetto Yed
Tecniche di programmazione ad oggetti con il linguaggio C.

Dichiarazione di una classe

Dopo aver identificato i componenti di un sistema, cioè le entità che costruiscono il sistema in termini di singole funzionalità e di interazioni fra di esse, l' obiect oriented modeling indica la classe come il contenitore che incapsula al proprio interno, astraendolo dal mondo circostante, ciascuna di queste entità. Il C++ traduce il concetto di classe con la keyword class e le keywords aggiuntive public , private e protected.
Una classe definita in C++ risulta alla fine molto simile ad una normale struttura definita in C, anche se le differenze non sono certo di poco conto. Prima di tutto, una classe non incapsula solo i dati che identificano una entità, ma anche i metodi che li manipolano, e per questo consente di inserire delle funzioni come membri di una classe, cosa che una struttura non permette direttamente. Inoltre, una classe è in grado di proteggere alcuni dati dall'accesso esterno diretto; in generale, nella programmazione a oggetti tutti i dati di una classe sono protetti, e questo è uno dei primi problemi di perdita di performance dei tools object oriented rispetto a quelli strutturati. Una struttura è un aggregatore trasparente: tutto è disponibile direttamente a tutti.
Ma la struttura è anche il costrutto C che più si avvicina al costrutto C++ che implementa una classe, ed è quindi questa la nostra naturale base di partenza.
Il nostro obiettivo è l'incapsulamento e l'astrazione dei dati. Dunque, per prima cosa abbiamo bisogno di poter inserire delle funzioni all'interno della nostra struttura. E in questo, ci vengono in aiuto le entità più potenti che il linguaggio C ci dona: i puntatori.
Inserire il prototipo di una funzione all'interno di una classe non equivale a dichiarare un puntatore ad una funzione come membro di una struttura? A livello di definizione di interfaccia, sicuramente. A livello di implementazione, occorre effettuare alcune operazioni preliminari che il compilatore C++ effettua in fase di creazione del codice, ma di questo ce ne occuperemo in seguito. Dunque, immaginando di comporre la nostra interfaccia nei due mondi C e C++ ( attenzione, parlo ancora di interfaccia !!! ), ecco il nostro piccolo esempio di partenza, dal quale poi svilupperemo la nostra idea.

MONDO C

typedef struct
   {
   int (*metodo)(void *);
   }Cprova;

MONDO C++

class Cprova
  {
  int metodo();
  };

Il parametro void * passato al metodo non è casuale. Vedremo in seguito a cosa serve: per ora, basti dire che è il nostro puntatore this.
Come vedete, a livello di interfaccia cambia poco o nulla. Nel mondo C,
metodo è un puntatore esplicito a una funzione che ritorna un intero. Nel mondo C++, se non proprio la stessa cosa, è un qualcosa di molto simile.
Proseguiamo con la nostra dichiarazione di interfaccia. Per incapsulare un dato, abbiamo bisogno di proteggerlo dagli accessi diretti del mondo esterno, o di costruire attorno al dato l'idea della sua protezione, che ci sembra la definizione più esatta, e più avanti vedremo perchè. Il C++ realizza tutto questo attraverso le keywords
public, private e protected . Eliminiamo subito la keyword protected dalla nostra trattazione: esplica un concetto che siamo ben lungi dall'applicare, e concentriamoci sulle keywords private e public.
Che vuol dire
public? Vuol dire che il dato, o il metodo, è disponibile direttamente all'esterno. Un dato public è un dato che si può accedere direttamente da un oggetto che "istanzia" la classe, in ambiente C++; dunque, trasportando tutto nel dominio C, un dato public è qualsiasi dato dichiarato in una struttura.
Che vuol dire
private? Vuol dire che il dato, o il metodo, non è disponibile direttamente all'esterno, ma può essere manipolato solo utilizzando dei metodi definiti all'interno dell'entità. Nel dominio C, occorre quindi trovare il modo di impedire che si possa accedere direttamente ad un dato una volta dichiarato come facente parte della nostra struttura-classe, perchè per definizione ogni elemento di una struttura C è pubblico.
E' quindi giunto il momento di introdurre il file header base del progetto
Yed, importantissimo, perchè contiene una serie di macro fondamentali per il funzionamento della nostra simulazione di programmazione object oriented. Lo chiameremo yedstd.h e ne analizzeremo di volta in volta ogni elemento del suo contenuto.
L'header è il seguente:

/*************************************************
NOME : YEDSTD.H
DESCRIZIONE : Header base progetto Yed
*************************************************/


#ifndef _YEDSTD
#define _YEDSTD

/******************  
 DEFINIZIONE MACRO
******************/
#define PUBLIC
#define PRIVATE(x)          __P##x

#define PRIVATE_FUNCTION    static

/**** CREAZIONE-DISTRUZIONE DINAMICA ****/
#define New(x) (x *)_##x()
#define Delete(x,a) _D##x(a)

/****************/
#endif

Spieghiamo in dettaglio il significato delle macro evidenziate in rosso, perchè esse ci aiutano a realizzare il vero e proprio incapsulamento dei dati di una entità.
Partiamo dalla macro
PUBLIC: visto che un elemento in una struttura è pubblico per definizione, essa viene semplicemente eliminata in fase di pre-compilazione dal compilatore C. Il senso del suo utilizzo sta nella chiarezza che ne deriva in fase di dichiarazione di interfaccia. Proseguiamo nel nostro esempio:

MONDO C

#include "yedstd.h"

typedef struct
  {
  PUBLIC int (*metodo)(void *);
  PUBLIC char elemento;
  } Cprova;

MONDO C++



class Cprova
  {
  public: int metodo();
  char elemento;
  };

Il dato elemento , di tipo char , è in entrambi i casi immediatamente disponibile per essere modificato in qualsiasi oggetto di tipo Cprova. Nel caso della nostra simulazione, infatti, il compilatore C sostituirà la macro PUBLIC con nulla; è dunque possibile non specificare affatto la macro PUBLIC in fase di dichiarazione dell'interfaccia, poichè è il default naturale; per chiarezza, è bene farlo per ciascun elemento pubblico della nostra classe simulata.
La macro
PRIVATE(x), invece, simula la protezione di un attributo semplicemente rinominandone il simbolo che lo identifica, attraverso l'uso dei token di concatenazione '##' utilizzati dal preprocessore. In tal modo, se viene dichiarato un oggetto di tipo Cprova e si tenta di accedere ad un attributo privato allo stesso, il compilatore C ci risponderà ( provare per credere, ma mi sembra naturale ), che l'attributo non fa parte della struttura Cprova; indipendentemente dal messaggio, in questo modo il compilatore ci ha impedito di accedere ad un attributo dichiarato privato nell'interfaccia.
Dunque, proseguendo:

MONDO C

#include "yedstd.h"

typedef struct
  {
  char PRIVATE(foo_priv);


  PUBLIC int (*metodo)(void *);
  PUBLIC char elemento;
  } Cprova;

MONDO C++



class Cprova
  {
  char foo_priv;

  public:
    int metodo();
    char elemento;
  };

Sostituendo la macro con l'equivalente valore, ne risulterà la vera struttura interna della nostra classe simulata, nella quale il dato foo_priv non sarà accessibile direttamente dall'esterno. Chiaramente, questa protezione è facilmente aggirabile, ma che senso ha sviluppare software seguendo regole che poi si tenta di violare? Se stiamo simulando, abbiamo interesse a simulare, altrimenti una banale struttura farebbe normalmente il proprio lavoro...
Per ovvie ragioni, utilizzando questa sintassi tutti gli attributi privati devono essere dichiarati attraverso la macro PRIVATE(x); anche perchè, tutto quello che è fuori da questa macro è considerato pubblico.
La macro
PRIVATE(x) è anche il passaggio che consente di accedere agli elementi di tipo privato, e deve essere usata all'interno dei metodi della nostra classe simulata. PRIVATE(x) è una macro delicata. Nessuno può impedire ad uno sviluppatore di usarla nel codice in cui un oggetto è stato istanziato esternamente al contesto in cui è previsto che operi, oltre che nell'implementazione dei metodi facenti parte della classe simulata. A voi la scelta sul suo utilizzo.
Ecco un esempio di utilizzo di un oggetto di tipo Cprova in entrambi i mondi. Nel caso del mondo C, è una prova ancora da non compilare e testare direttamente, ma da valutarsi esclusivamente in termini di sintassi. E bene comunque tenere subito presente che
un oggetto di una classe simulata può essere dichiarato esclusivamente come puntatore del tipo della classe simulata, mai come istanza statica della stessa.
Ipotizzando che l'header file
yedprova.h contenga la definizione della nostra classe simulata Cprova, abbiamo:

MONDO C

#include "yedstd.h"
#include "yedprova.h"

Cprova *cWrk;

// consentito
cWrk->elemento=12;

// Non consentito
cWrk->foo_priv=3;

// Consentito, ma
// concettualmente non valido

cWrk->PRIVATE(foo_priv)=3;

MONDO C++




Cprova *cWrk;

// consentito
cWrk->elemento=12;

// Non consentito
cWrk->foo_priv=1;

Infine, ricordiamo che, ovviamente, è possibile dare alle macro il nome che si desidera.

 


Copyright (C) - Giulio A. - ynoxia(at)gmail(dot)com