Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 8 marzo 2013

C orientato agli oggetti II - l'eredità
come usare la OOP in C - pt.2

Come promesso torniamo su luogo del delitto: dopo il mirabolante esempio di classe in C visto da queste parti, oggi cercheremo di implementare anche l'ereditarietà di classe in C. Vi anticipo che tutto quanto segue serve a poco, ma potrebbe essere una utile esercitazione sulle potenzialità nascoste del nostro linguaggio preferito. Se non avete ancora letto la prima parte del post correte a leggerla, specialmente la parte introduttiva, altrimenti potreste pensare che sono diventato matto a scrivere roba come questa.

Veniamo al dunque: scriviamo un (altra) classe elementare in C++ che implementa un (altro) contatore. Scriveremo, secondo la prassi classica, file di header, implementazione e uso. Vediamo l'header counter.hpp:
#ifndef COUNTER_HPP
#define COUNTER_HPP
/* counter.hpp - header classe CCounter
 */

class CCounter {
protected:
    // attributi privati
    int value;

public:
    // metodi pubblici
    CCounter();          // costruttore
    void incValue(int n);
    int getValue();
};
#endif /* COUNTER_HPP */
Anche questa volta è una classe semplicissima, con un attributo privato, due metodi di lettura e scrittura e un costruttore esplicito. Passiamo al file di implementazione, counter.cpp:
/* counter.cpp - implementazione classe CCounter
 */
#include "counter.hpp"

// CCounter() - costruttore della classe CCounter
CCounter::CCounter()
{
    value = 100;
}

// incValue() - metodo di incremento attributo value
void CCounter::incValue(int n)
{
    value += n;
}

// getValue() - metodo di lettura attributo value
int CCounter::getValue()
{
    return value;
}
Questa volta, visto che parliamo di ereditarietà di classe, avremo anche una classe derivata, il cui header file è counter_der.hpp:
#ifndef COUNTER_DER_HPP
#define COUNTER_DER_HPP
/* counter_der.hpp - header classe CCounter_der
 */
#include "counter.hpp"

class CCounter_der : public CCounter {
public:
    // metodi pubblici
    void incValue(int n);
};
#endif /* COUNTER_DER_HPP */
mentre il file di implementazione è counter_der.cpp:
/* counter_der.cpp - implementazione classe CCounter_der
 */
#include "counter_der.hpp"

// incValue() - metodo di incremento attributo value
void CCounter_der::incValue(int n)
{
    value -= n;
}
Per ultimo vediamo il file di uso della classe, countermain.cpp:
/* countermain.cpp - esempio d'uso delle classi CCounter e CCounter_der
 */
#include <iostream>
#include "counter.hpp"
#include "counter_der.hpp"

using namespace std;

main()
{
    CCounter cnt;
    CCounter_der cnt_der;

    // leggo valori
    cout << "cnt value = " << cnt.getValue() << endl;
    cout << "cnt_der value = " << cnt_der.getValue() << endl;

    // incremento valori
    cnt.incValue(5);
    cnt_der.incValue(7);

    // leggo valori
    cout << "cnt value = " << cnt.getValue() << endl;
    cout << "cnt_der value = " << cnt_der.getValue() << endl;
}
L'uso della classe è un po' stupido (come l'altra volta), ma per quest'esempio è più che sufficiente. Istanziamo due oggetti della classe CCounter, cnt e cnt_der, stampiamo i valori di value per entrambi gli oggetti, incrementiamo e ristampiamo: se compilate ed eseguite il programma l'uscita sarà:
cnt value = 100
cnt_der value = 100
cnt value = 105
cnt_der value = 93
Come vedete, per la magia della ereditarietà, l'oggetto cnt_der (istanza della semplicissima CCounter_der) è un contatore che decrementa, visto che la classe derivata differisce dalla classe base (in questo caso) solo per il metodo incValue(), per cui, alla fine, il campo value ha valore incrementato in cnt e decrementato in cnt_der. Fantastico.

Notare, che nei due header-file ho usato l'opportuna include guard, per evitare inclusioni multiple, visto che counter_der.h richiama counter.h (in effetti, questo esempio è un po' più rigoroso di quello del post precedente).

Una ultima nota: nella classe base il metodo incValue() non è stato definito virtual, sia perché per l'esempio d'uso proposto non era necessario (il compilatore, infatti, non si lamenta...), sia perché esula dall'obiettivo del post: non è mica un blog di C++ questo...

Anche l'ereditarietà si può implementare in C? Si! Beh, il risultato finale non sarà (come vedrete) elegantissimo, ma funziona. Useremo la stessa struttura a header, implementazione e uso. Cominciamo con l'header ccounter.h:
#ifndef CCOUNTER_H
#define CCOUNTER_H
/* ccounter.h - header classe CCounter in C
 */

typedef struct _ccounter {
    // attributi
    int value;

    // metodi
    void (*construct)(struct _ccounter *this);    // costruttore
    void (*incValue)(struct _ccounter *this, int n);
    int (*getValue)(struct _ccounter *this);
} CCounter;

// prototipi globali
void pubConstruct(CCounter *this);
#endif /* CCOUNTER_H */
Come già fatto notare su queste pagine somiglia molto a ccounter.hpp. Passiamo alla implementazione in ccounter.c:
/* ccounter.c - implementazione classe CCounter in C
 */
#include "ccounter.h"

// incValue() - metodo di incremento attributo value
static void incValue(CCounter *this, int n)
{
    this->value += n;
}

// getValue() - metodo di lettura attributo value
static int getValue(CCounter *this)
{
    return this->value;
}

// construct() - costruttore della classe CCounter
static void construct(CCounter *this)
{
    this->incValue = &incValue;
    this->getValue = &getValue;
    this->value = 100;
}

// pubConstruct() - costruttore pubblico della classe CCounter
void pubConstruct(CCounter *this)
{
    this->construct = &construct;
    this->construct(this);
}
Come già fatto notare nell'altro post, rispetto alla versione C++ dobbiamo fare in maniera esplicita ciò che il compilatore C++ fa implicitamente: a ogni metodo passiamo un puntatore all'oggetto chiamante, i metodi sono assegnati ai puntatori a funzione, e dobbiamo aggiungere un metodo in più, un costruttore pubblico, da chiamare dopo l'istanziazione dell'oggetto.

E, adesso passiamo alla parte più interessante, la classe derivata CCounter_der. Vediamo l'header ccounter_der.h:
#ifndef CCOUNTER_DER_H
#define CCOUNTER_DER_H
/* ccounter_der.h - header classe CCounter in C
 */
#include "ccounter.h"

typedef struct _ccounter_der {
    // attributi
    CCounter cnt_base;

    // metodi
    void (*construct)(struct _ccounter_der *this);    // costruttore
    void (*incValue)(struct _ccounter_der *this, int n);
} CCounter_der;

// prototipi globali
void pubConstruct_der(CCounter_der *this);
#endif /* CCOUNTER_DER_H */
Come si nota è una versione ridotta dell'header della classe base. Negli attributi c'è un oggetto di tipo CCounter (è il trucco che ci permette di parlare di ereditarietà) e nei metodi c'è il solito costruttore più l'unico metodo da ridefinire (come nella versione C++).

Passiamo ora alla implementazione in ccounter.c:
/* ccounter.c - implementazione classe CCounter_der in C
 */
#include "ccounter_der.h"

// incValue() - metodo di incremento attributo value
static void incValue(CCounter_der *this, int n)
{
    this->cnt_base.value -= n;
}

// construct() - costruttore della classe CCounter
static void construct(CCounter_der *this)
{
    this->incValue = &incValue;
}

// pubConstruct() - costruttore pubblico della classe CCounter
void pubConstruct_der(CCounter_der *this)
{
    pubConstruct(&this->cnt_base);
    this->construct = &construct;
    this->construct(this);
}
Credo che il codice sia chiarissimo: anche questa è una versione ridotta dell'implementazione della classe base, con la ridefinizione del metodo incvalue() e del suo assegnamento. Notare che il costruttore pubblico richiama il costruttore pubblico della classe base. Ci resta solo da vedere il file d'uso, ccountermain.c:
/* ccountermain.c - esempio d'uso della classe CCounter in C
 */
#include <stdio.h>
#include "ccounter.h"
#include "ccounter_der.h"

main()
{
    CCounter cnt;
    pubConstruct(&cnt);

    CCounter_der cnt_der;
    pubConstruct_der(&cnt_der);

    // leggo valori
    printf("cnt value = %d\n", cnt.getValue(&cnt));
    printf("cnt_der value = %d\n", cnt_der.cnt_base.getValue(&cnt_der.cnt_base));

    // incremento valori
    cnt.incValue(&cnt, 5);
    cnt_der.incValue(&cnt_der, 7);

    // leggo valori
    printf("cnt value = %d\n", cnt.getValue(&cnt));
    printf("cnt_der value = %d\n", cnt_der.cnt_base.getValue(&cnt_der.cnt_base));
}
Beh, anche questa volta è praticamente identico alla versione C++, salvo la chiamata esplicita al costruttore pubblico, e all'uso della printf() per la stampa dei risultati. Se compilate e eseguite, il risultato (giuro di nuovo!) è identico a quello della versione C++. Come volevasi dimostrare anche con il C possiamo usare (simulare? emulare?) l'ereditarietà di classe.

Visto che mi sono dilungato un po' con il codice e, come sempre, non voglio annoiare nessuno, rimando al prossimo post le disquisizioni filosofiche, che avevo promesso, su C vs C++  (si, non mi sono dimenticato...), dove cercherò di essere il più diplomatico possibile...

Ciao e al prossimo post.