A dif­fe­ren­za dei linguaggi OOP C++ e Objective-C, C non è dotato di base di alcuna fun­zio­na­li­tà orientata agli oggetti. A causa della dif­fu­sio­ne del lin­guag­gio e della po­po­la­ri­tà della pro­gram­ma­zio­ne orientata agli oggetti, esistono approcci all’uso di OOP in C.

OOP in C: è davvero possibile?

Il lin­guag­gio di pro­gram­ma­zio­ne C non è di per sé destinato alla pro­gram­ma­zio­ne orientata agli oggetti. Questo lin­guag­gio è un ottimo esempio dello stile di pro­gram­ma­zio­ne strut­tu­ra­to all’interno della pro­gram­ma­zio­ne im­pe­ra­ti­va. Cio­no­no­stan­te, è possibile emulare approcci orientati agli oggetti in C, poiché dispone di tutti i com­po­nen­ti necessari a questo scopo. Ad esempio, è servito come base per rea­liz­za­re la pro­gram­ma­zio­ne orientata agli oggetti in Python.

Con OOP, è possibile definire i propri “tipi di dati astratti” (ADT). Un ADT può essere con­si­de­ra­to come un insieme di valori possibili e di funzioni che operano su di essi. È im­por­tan­te che l’in­ter­fac­cia visibile all’esterno e l’im­ple­men­ta­zio­ne interna siano di­sac­cop­pia­te tra loro. In questo modo, l’utente può fare af­fi­da­men­to sul fatto che gli oggetti del tipo si com­por­ti­no secondo la de­scri­zio­ne.

I linguaggi orientati agli oggetti come Python, Java e C++ uti­liz­za­no il concetto di “classe” per modellare i tipi di dati astratti. Le classi servono come modello per la creazione di oggetti simili; questa ope­ra­zio­ne viene anche chiamata istan­zia­zio­ne. C non conosce in­trin­se­ca­men­te le classi, che non possono essere modellate all’interno del lin­guag­gio. Esistono invece diversi approcci per im­ple­men­ta­re le fun­zio­na­li­tà OOP in C.

Come funziona OOP in C?

Per capire come funziona OOP in C, dobbiamo in­nan­zi­tut­to porci la domanda: cos’è esat­ta­men­te la pro­gram­ma­zio­ne orientata agli oggetti (OOP)? Si tratta di uno stile di pro­gram­ma­zio­ne comune che è una ma­ni­fe­sta­zio­ne del paradigma di pro­gram­ma­zio­ne im­pe­ra­ti­vo. OOP si con­trap­po­ne quindi alla pro­gram­ma­zio­ne di­chia­ra­ti­va e alla sua spe­cia­liz­za­zio­ne, la pro­gram­ma­zio­ne fun­zio­na­le.

L’idea di base della pro­gram­ma­zio­ne orientata agli oggetti consiste nel modellare gli oggetti e farli in­te­ra­gi­re tra loro. Il flusso del programma risulta dalle in­te­ra­zio­ni degli oggetti e non viene quindi fissato fino all’ese­cu­zio­ne. In sostanza, OOP comprende solo tre proprietà:

  1. Gli oggetti in­cap­su­la­no il loro stato interno.
  2. Gli oggetti ricevono messaggi at­tra­ver­so i loro metodi.
  3. I metodi sono assegnati di­na­mi­ca­men­te in fase di ese­cu­zio­ne.

Un oggetto in un lin­guag­gio OOP puro come Java è un’unità autonoma. Comprende una struttura di dati di qualsiasi com­ples­si­tà e metodi (funzioni) che operano su di essa. Lo stato interno dell’oggetto, rap­pre­sen­ta­to dai dati che contiene, può essere letto e mo­di­fi­ca­to solo at­tra­ver­so i metodi. Per la gestione della memoria degli oggetti, viene so­li­ta­men­te uti­liz­za­ta una funzione del lin­guag­gio chiamata “garbage collector”.

In C, non è facile collegare le strutture di dati e le funzioni agli oggetti. Invece, si crea un sistema gestibile di strutture di dati, de­fi­ni­zio­ni di tipi, puntatori e funzioni. Quindi, come di consueto in questo lin­guag­gio, il pro­gram­ma­to­re o la pro­gram­ma­tri­ce è re­spon­sa­bi­le della corretta al­lo­ca­zio­ne e abi­li­ta­zio­ne della memoria.

Il codice C a oggetti che ne risulta non as­so­mi­glia molto a quello a cui siamo abituati nei linguaggi OOP, ma funziona. Di seguito vi forniamo un riepilogo dei concetti centrali della pro­gram­ma­zio­ne orientata agli oggetti e del loro equi­va­len­te in C:

Concetto in OOP Equi­va­len­te in C
Classe Tipo struct
Instanza di classi Instanza struct
Metodo di istanza Funzione che accetta i puntatori alla variabile struct
Variabile this/self Puntatori alla variabile struct
Istan­zia­zio­ne Al­lo­ca­zio­ne e referenza tramite puntatori
Parola chiave new Chiamata di malloc

Modellare gli oggetti come strutture dati

Vediamo in­nan­zi­tut­to come la struttura dei dati di un oggetto può essere modellata in C nello stile dei linguaggi OOP. C è un lin­guag­gio compatto che se la cava con pochi costrutti lin­gui­sti­ci. Per creare strutture di dati ar­bi­tra­ria­men­te complesse, si uti­liz­za­no i co­sid­det­ti “struct”, il cui nome deriva dal termine “Data Structure” (struttura dei dati).

Struct in C definisce una struttura di dati che include campi chiamati “members” (membri). In altri linguaggi, un costrutto di questo tipo è chiamato anche “record”, e quindi si può im­ma­gi­na­re uno struct come la riga di una tabella di un database: una com­bi­na­zio­ne di più campi, even­tual­men­te di tipo diverso.

La sintassi della di­chia­ra­zio­ne di uno struct in C è molto semplice:

struct struct_name;
C

Op­zio­nal­men­te, definiamo anche struct spe­ci­fi­can­do i membri con nome e tipo. A titolo di esempio, con­si­de­ria­mo un punto nello spazio bi­di­men­sio­na­le con coor­di­na­te x e y. Vi mostriamo la de­fi­ni­zio­ne di struct:

struct point {
    /*X-coordinate*/
    int x;
    /*Y-coordinate*/
    int y;
};
C

Nel codice C con­ven­zio­na­le, questa ope­ra­zio­ne è seguita dall’istan­zia­zio­ne di una variabile struct. Creiamo la variabile e ini­zia­liz­zia­mo entrambi i campi con 0:

struct point origin = {0, 0};
C

In seguito, i valori dei campi possono essere letti e reim­po­sta­ti. L’accesso ai membri avviene tramite la sintassi origin.x e origin.y, già nota in altri linguaggi:

/*Read struct member*/
origin.x == 0
/*Assign struct member*/
origin.y = 42
C

Tuttavia, questo viola il requisito dell’in­cap­su­la­men­to: si può accedere allo stato interno di un oggetto solo tramite metodi definiti a tale scopo. Quindi al nostro approccio manca ancora qualcosa.

Definire i tipi per la creazione di oggetti

Come già men­zio­na­to, C non conosce il concetto di classe. Invece, i tipi si possono definire con l’istru­zio­ne typedef. Con typedef, diamo a un tipo di dati un nuovo nome:

typedef <old-type-name> <new-type-name>
C

Questo ci permette di definire un cor­ri­spon­den­te tipo Point per il nostro struct point:

typedef struct point Point;
C

La com­bi­na­zio­ne di typedef con una de­fi­ni­zio­ne di struct cor­ri­spon­de all’incirca alla de­fi­ni­zio­ne di una classe in Java:

typedef struct point {
    /*X-coordinate*/
    int x;
    /*Y-coordinate*/
    int y;
} Point;
C
N.B.

Nell’esempio, “point” è il nome di struct, mentre “Point” è il nome del tipo definito.

Di seguito la de­fi­ni­zio­ne della classe cor­ri­spon­den­te in Java:

class Point {
    private int x;
    private int y;
};
Java

L’uso di typedef ci permette di creare una variabile Point senza usare la parola chiave struct:

Point origin = {0, 0}
/*Instead of*/
struct point origin = {0, 0}
C

Ciò che ancora manca è l’in­cap­su­la­men­to dello stato interno.

In­cap­su­la­men­to dello stato interno

Gli oggetti mappano il loro stato interno nella loro struttura di dati. Nei linguaggi OOP come Java, le parole chiave “private”, “protected”, ecc. vengono uti­liz­za­te per limitare l’accesso ai dati degli oggetti. Questo impedisce l’accesso diretto dall’esterno e ga­ran­ti­sce la se­pa­ra­zio­ne tra in­ter­fac­cia e im­ple­men­ta­zio­ne.

Per rea­liz­za­re OOP in C, si usa un mec­ca­ni­smo diverso. Uti­liz­zia­mo una co­sid­det­ta di­chia­ra­zio­ne forward nel file di in­te­sta­zio­ne come in­ter­fac­cia, creando così un “In­com­ple­te type”:

/*In C header file*/
struct point;
/*Incomplete type*/
typedef struct point Point;
C

L’im­ple­men­ta­zio­ne dello struct point segue in un file del codice sorgente in C separato che incorpora l’in­te­sta­zio­ne tramite la macro include. Questo approccio impedisce la creazione di variabili statiche del tipo Point. È comunque possibile uti­liz­za­re puntatori del tipo. Poiché gli oggetti sono strutture di dati create di­na­mi­ca­men­te, vengono comunque re­fe­ren­zia­ti con puntatori. I puntatori alle istanze struct cor­ri­spon­do­no all’incirca ai ri­fe­ri­men­ti agli oggetti uti­liz­za­ti in Java.

So­sti­tui­re i metodi con le funzioni

Nei linguaggi OOP come Java e Python, gli oggetti includono funzioni che operano su di essi oltre ai loro dati. Queste funzioni sono chiamate metodi o metodi di istanza. Quando scriviamo codice OOP in C, invece dei metodi usiamo funzioni che accettano un puntatore su un’istanza struct:

/*Pointer to `Point` struct*/
Point * point;
C

Poiché C non conosce le classi, non è possibile rag­grup­pa­re le funzioni ap­par­te­nen­ti a un tipo sotto un nome comune. Al contrario, forniamo i nomi delle funzioni con un prefisso con­te­nen­te il nome del tipo. Le firme delle funzioni cor­ri­spon­den­ti vengono prima di­chia­ra­te nel file di in­te­sta­zio­ne in C:

/*In C header file*/
/*Function to move update a point’s coordinates*/
void Point_move(Point * point, int new_x, int new_y);
C

Suc­ces­si­va­men­te, im­ple­men­tia­mo la funzione nel file del codice sorgente in C:

/*In C source file*/
void Point_move(Point * point, int new_x, int new_y) {
    point->x = new_x;
    point->y = new_y;
};
C

L’approccio ricorda i metodi di Python, che sono normali funzioni che accettano self come primo parametro. Inoltre, il puntatore su un’istanza struct cor­ri­spon­de più o meno alla variabile this in Java o Ja­va­Script. La dif­fe­ren­za è che quando la funzione C viene chiamata, il puntatore viene trasmesso espli­ci­ta­men­te:

/*Call function with pointer argument*/
Point_move(point, 42, 51);
C

Con la chiamata di funzione equi­va­len­te in Java, l’oggetto point è di­spo­ni­bi­le all’interno del metodo come variabile this:

// Call instance method from outside of class
point.move(42, 51)
// Call instance method from within class
this.move(42, 51)
Java

Python consente di chiamare i metodi come funzioni con un argomento self esplicito:

# Call instance method from outside or from within class
self.move(42, 51)
# Function call from within class
move(self, 42, 51)
Python

Istan­zia­re gli oggetti

Una ca­rat­te­ri­sti­ca di C è la gestione manuale della memoria: i pro­gram­ma­to­ri e le pro­gram­ma­tri­ci sono re­spon­sa­bi­li dell’al­lo­ca­zio­ne della memoria per le strutture di dati. I linguaggi dinamici orientati agli oggetti, come Java e Python, li sollevano da questo compito. In Java, per istan­zia­re un oggetto si usa la parola chiave new. Sotto la scocca, la memoria viene allocata au­to­ma­ti­ca­men­te:

// Create new Point instance
Point point = new Point();
Java

Quando scriviamo codice OOP in C, definiamo una funzione co­strut­to­re speciale per l’istan­zia­zio­ne. Questa funzione alloca la memoria per la nostra istanza struct, la ini­zia­liz­za e re­sti­tui­sce un puntatore a essa:

Point * Point_new(int x, int y) {
    /*Allocate memory and cast to pointer type*/
    Point *point = (Point*) malloc(sizeof(Point));
    /*Initialize members*/
    Point_init(point, x, y);
    // return pointer
    return point;
};
C

Nel nostro esempio, di­sac­cop­pia­mo l’ini­zia­liz­za­zio­ne dei membri struct dall’istan­zia­zio­ne. Anche in questo caso, viene uti­liz­za­ta una funzione con il prefisso point:

void Point_init(Point * point, int x, int y) {
    point->x = x;
    point->y = y;
};
C

Come si può ri­scri­ve­re un progetto in C in modo orientato agli oggetti?

Ri­scri­ve­re un progetto esistente in C uti­liz­zan­do le tecniche OOP descritte è con­si­glia­bi­le solo in casi ec­ce­zio­na­li. I seguenti approcci sono più ra­gio­ne­vo­li:

  1. Ri­scri­ve­re il progetto in un lin­guag­gio simile a C con ca­rat­te­ri­sti­che OOP e uti­liz­za­re la base di codice C esistente come specifica
  2. Ri­scri­ve­re parti del progetto in un lin­guag­gio OOP e mantenere com­po­nen­ti C specifici

Se la base di codice C è scritta in modo pulito, il secondo approccio dovrebbe dare buoni risultati. È prassi comune im­ple­men­ta­re parti di programma critiche per le pre­sta­zio­ni in C e accedervi da altri linguaggi. Pro­ba­bil­men­te nessun altro lin­guag­gio è più adatto di C a questo scopo. Ma quali sono i linguaggi adatti per ri­co­strui­re un progetto C esistente uti­liz­zan­do i principi OOP?

Linguaggi C-like orientati agli oggetti

Esiste una ricca selezione di linguaggi C-like con orien­ta­men­to agli oggetti in­cor­po­ra­to. Pro­ba­bil­men­te il più noto è C++; tuttavia, data la sua com­ples­si­tà, c’è stato un al­lon­ta­na­men­to da questo lin­guag­gio negli ultimi anni. Poiché i suoi costrutti di base sono in gran parte gli stessi, il codice C è re­la­ti­va­men­te facile da in­cor­po­ra­re in C++.

Molto più leggero di C++ è Objective-C. Il dialetto C, basato sul lin­guag­gio OOP originale Smalltalk, è stato uti­liz­za­to prin­ci­pal­men­te per la pro­gram­ma­zio­ne di ap­pli­ca­zio­ni su Mac e sui primi sistemi operativi iOS. In seguito, è stato seguito dallo sviluppo del lin­guag­gio Swift di Apple. Le funzioni scritte in C possono essere ri­chia­ma­te da entrambi i linguaggi.

Linguaggi orientati agli oggetti basati su C

Anche altri linguaggi di pro­gram­ma­zio­ne OOP che non sono legati a C in termini di sintassi sono adatti alla ri­scrit­tu­ra di un progetto in C. Per Python, Rust e Java esistono approcci standard per includere il codice C.

In Python, i co­sid­det­ti binding per­met­to­no di includere codice C. È possibile che i tipi di dati Python debbano essere tradotti nei cor­ri­spon­den­ti ctypes. Esiste anche la C Foreign Function Interface (CFFI), che au­to­ma­tiz­za in una certa misura la tra­du­zio­ne dei tipi.

Rust supporta anche la chiamata di funzioni C con poco sforzo. La parola chiave extern può essere usata per definire una Foreign Function Interface (FFI). Le funzioni di Rust che accedono a funzioni esterne devono essere di­chia­ra­te unsafe (non sicure):

extern "C" {
    fn abs(input: i32) -> i32;
}
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Rust
Vai al menu prin­ci­pa­le