Dal C al C++/Utilizzo basilare di librerie/L'uso di classi e oggetti

Indice del libro

Le funzioni membro modifica

Si consideri la seguente porzione di codice in linguaggio C:

struct S1 {
    int i;
    double x;
};
struct S1 a1 = { 2, 3.14 };

Le prime quattro righe definiscono un nuovo tipo, chiamato struct S1. L'ultima riga definisce una variabile, di nome a1, di tipo struct S1, e inizializzata con i due valori 2 e 3.14.

Nel linguaggio C++ la possibilità di definire nuovi tipi è stata estesa con l'aggiunta delle "funzioni membro". Si consideri la seguente porzione di codice in linguaggio C++:

struct S2 {
    int i;
    double x;
    double f() { return x * 2; }
};
struct S2 a2 = { 2, 3.14 };

Rispetto al codice precedente, è stata aggiunta solo la quarta riga, in cui si definisce una funzione di nome f, che non prende parametri, e che ritorna un valore di tipo double. Tale funzione è stata definita all'interno della struttura struct S2.

Così come le variabili definite all'interno di una struttura, oltre a essere dette i campi della struttura, sono anche dette essere le sue variabili membro, così le funzioni definite all'interno di una struttura sono dette essere le sue funzioni membro. In generale, variabili membro e funzioni membro sono i membri di una struttura.

Si noti che f non è un puntatore a funzione, ma è proprio una funzione, con tanto di corpo. Forse qualcuno si chiederà: "Ma allora la struttura a2 contiene il codice macchina della funzione o solo il suo indirizzo?"

La risposta è che non contiene nessuno dei due.

Quando il compilatore elabora la definizione di struct S2 non alloca spazio di dati, in quanto si tratta solo di un tipo. Quando invece elabora la definizione di a2 alloca una struttura contenente i due soli membri dato. La compilazione della funzione f genera del codice macchina, che è memorizzato nel segmento di codice del programma, quindi ben lontano dalla variabile a2 e da ogni altra istanza del tipo struct S2.

Tale funzione membro genera pressappoco lo stesso codice che genererebbe il seguente codice in linguaggio C:

double S2_f(struct S2 * t) { return t->x * 2; }

Il nome della funzione S2_f indica semplicemente che tale funzione è distinta da eventuali altre funzioni di nome f dichiarate fuori dalla struttura.

Il parametro t è un puntatore a un oggetto avente lo stesso tipo della struttura.

La variabile x, che nel corpo originale della funzione era indicata come variabile non qualificata, è diventata un membro della struttura puntata da t.

La variabile "a2" ha la stessa dimensione e lo stesso contenuto della variabile "a1". È solo il tipo a essere diverso, in quanto su "a2" si può applicare l'operazione "f", che non si può applicare su "a1". Ecco come si invoca la funzione "f" su "a2":

  double y = a2.f();

L'operatore ".", come consente di accedere alle variabili membro, così consente di accedere alle funzioni membro.

Il codice generato dalla riga qui sopra è equivalente a quello generato dalla seguente riga:

  double y = S2_f(&a2);

Quindi, chiamare una funzione membro su una variabile di tipo struttura significa chiamare la funzione passandole come primo parametro un puntatore alla variabile stessa.

Ecco un esempio realistico basato sulla classe standard "string".

  string s = "ABCDEFG";
  cout << s.substr(4, 2);

Questa porzione di codice stampa sulla console la stringa "EF", in quanto la funzione membro "substr" estrae dalla stringa "s" una sottostringa saltando 4 caratteri da sinistra e prendendone 2.

"substr" è una funzione membro della classe "string", quindi riceve implicitamente come primo parametro nascosto un puntatore alla stringa a cui la funzione viene applicata. Inoltre, tale funzione accetta due parametri interi; il primo è il numero di caratteri da contare a partire dall'inizio della stringa, e il secondo è il numero di caratteri da prelevare. Il valore reso dalla funzione è ovviamente un altro valore di tipo "string".

Il C++ introduce rispetto al C alcune comodità sintattiche:

  • Il nome del tipo della variabile a2 può essere "struct S2", ma può essere abbreviato in "S2".
  • La parola-chiave "class" è quasi sinonima di "struct", ed è molto più usata per strutture contenenti funzioni membro. D'ora in poi, invece di "struct", e struttura useremo prevalentemente "class" e classe. Quindi una variabile "x" di tipo "class C" potrà essere definita semplicemente con la seguente riga:
  C x;

Gli oggetti modifica

Un termine molto usato (e abusato) è la parola "oggetto". La si usa impropriamente a volte per indicare una classe, altre per indicare una variabile. In realtà non è nessuna delle due cose.

Un oggetto è una cosa che esiste anche in C, sotto il nome di "r-value". L'espressione "r-value", abbreviazione di "right-hand value" (cioè "valore di destra"), significa espressione che può stare a destra dell'operatore "=". Cioè si tratta di un'espressione che rappresenta una sequenza di bit che può essere copiata in una locazione di memoria tramite un assegnamento.

Un oggetto può risiedere in memoria, può risiedere solo in un registro del processore, oppure può essere solo teorico, cioè esiste concettualmente, a livello di semantica del linguaggio, ma l'ottimizzatore di codice genera un codice macchina che ne evita del tutto la creazione.

Una distinzione fondamentale e netta tra classi e oggetti, è che in C++ le classi sono concetti statici, come le funzioni e i programmi, mentre gli oggetti sono concetti dinamici, come le chiamate di funzione e i processi. Le classi sono create dal programmatore, e ci si può chiedere quante classi contiene un dato programma. Gli oggetti sono creati dal programma in fase di esecuzione, e non ha senso chiedersi quanti oggetti contiene un dato programma, perché il loro numero varia continuamente durante l'esecuzione del programma stesso.

Ecco qualche esempio valido sia in C che in C++:

  char x;
  char * px = &x;

Se le due righe precedenti sono poste fuori da qualunque funzione e struttura, costituiscono altrettante definizioni di variabili globali "x" e "px", la prima di tipo "char" e la seconda di tipo "char *", ossia puntatore a "char".

Entrambe le variabili rappresentano anche degli oggetti, il primo lungo "sizeof (char)" byte, cioè tipicamente 1 byte, e il secondo lungo "sizeof (char *)" byte, cioè tipicamente 2 byte su una macchina a 8 bit, 4 byte su una macchina a 16 bit o a 32 bit, e 8 byte su una macchina a 64 bit.

La variabile "px" è inizializzata a puntare all'oggetto rappresentato dalla variabile "x". Pertanto l'espressione "*px" rappresenta lo stesso oggetto rappresentato da "x".

Supponiamo che una funzione contenga la seguente riga:

  px = malloc(7);

Dopo aver eseguito questa istruzione, "px" o è nullo (se la malloc fallisce) o punta a un array di sette oggetti di tipo carattere. Tali oggetti non sono associati a nessuna variabile, eppure possono ricevere un assegnamento, per esempio, con la seguente istruzione:

  px[2] = 'a';

Consideriamo la seguente istruzione:

  int a = 3 + 5;

Il numero "3" in quanto tale non è un oggetto, in quanto è un valore letterale, ma quando entra a far parte di una espressione come questa, concettualmente viene istanziato un oggetto che contiene una sequenza di bit che rappresenta il valore "3". Chiaramente qualunque compilatore ottimizzante a fronte di questa riga di codice genera un'istruzione che assegna alla variabile "a" il valore 8, senza generare nessuna istruzione di somma, e tantomeno allocare spazio per gli addendi.

Tuttavia, concettualmente, avvengono le seguenti operazioni:

  • Viene allocato sullo stack un oggetto di tipo "int", non inizializzato, che rappresenta la variabile "a".
  • Viene allocato sullo stack un oggetto di tipo "int", e viene posto il valore "3" in tale oggetto.
  • Viene allocato sullo stack un oggetto di tipo "int", e viene posto il valore "5" in tale oggetto.
  • Viene allocato sullo stack un oggetto di tipo "int", non inizializzato, che serve come area temporanea per memorizzare il risultato della somma.
  • Viene eseguita l'operazione di somma tra il secondo e il terzo oggetto, assegnando il risultato al quarto oggetto.
  • Viene copiato il contenuto del quarto oggetto nel primo oggetto.
  • Vengono deallocati dallo stack tutti gli oggetti tranne il primo.

Quindi, nel corso dell'esecuzione di un programma scritto in C++, concettualmente vengono allocati e deallocati moltissimi oggetti, anche se in pratica a fronte di tali allocazioni e deallocazioni concettuali il compilatore ottimizzante genera molte meno istruzioni di quante sembrerebbero necessarie.

In C++, ma non in C, è anche possibile, come vedremo in seguito, definire una variabile senza allocare nessun oggetto. Si tratta delle variabili di tipo "riferimento" ("reference" in inglese), usate anche in altri linguaggi, come BASIC e Pascal, per passare parametri a funzioni.