giovedì 15 novembre 2018

C e C++: Appunti di una riscoperta 5

In Hello World abbiamo utilizzato un letterale stringa come argomento o parametro attuale della funzione printf. Fintanto che un programma è piccolino come HelloWorld va tutto bene, ma quanto sarebbe comodo davanti a un programma molto lungo avere tutti i letterali stringa, raccolti in una sezione in cui sono dichiarati come costanti?

Ed è proprio di questo che andiamo a parlare, di costanti, di come dichiararle e utilizzarle sempre avendo come obiettivo HelloWorld.


Ma che significa dichiarare una costante. Si dichiara una costante nel momento in cui si associa a un letterale stringa o numerico (che non abbiamo ancora incontrato) un token identificativo. L'identificativo individuerà un preciso valore nel programma e questo valore è immutabile.

In C le costanti possono essere dichiarate in due modi. 
  1. tramite direttiva #define al preprocessore, e tali costanti sono meglio note come macro
  2. tramite la parola chiave const
Da notare che entrambe le soluzioni non consumano memoria nel programma, perché in entrambi i casi, o il preprocessore sostituisce ogni ricorrenza della macro definita con il valore indicato subito dopo il nome della macro stessa, o nel caso di const sarà il compilatore a svolgere questo lavoro ma il risultato sarà, in fase di compilazione, la sostituzione dei nomi simbolici o identificatori di costante o token con i rispettivi valori.

Ma dove si dichiarano? I token dichiarati dal programmatore possono essere messi all'interno del file sorgente, piuttosto che in un file header separato che potrà essere a sua volta incluso nel sorgente con la direttiva #include. 

Aggiungiamo un elemento come abbiamo fatto nei post precedenti all'interno della cartella File di origine e chiamiamolo saluto.h. Al suo interno scriveremo:

#define SALUTO "Hello World - MACRO\n"
const char* saluto = "Hello World - COSTANTE\n";


Nel caso della direttiva #define stiamo dichiarando una macro di nome SALUTO che, in fase di preprocessing, sarà sostituita con "Hello World - MACRO\n". I vari elementi sono separati da uno o più spazi e la direttiva termina con un accapo.

Nel secondo caso, con l'utilizzo della parola chiave const, stiamo dichiarando una costante di nome saluto il cui contenuto è una sequenza di caratteri (sorvoliamo sul char* per ora) e più precisamente Hello World - COSTANTE\n.  Essendo una istruzione al compilatore, è terminata con un punto e virgola.

Fatto questo spostiamoci sul sorgente contenente il main e modifichiamolo con

#include "stdio.h"
#include "saluto.h"

main() {
    printf(saluto);
    printf(SALUTO);
}


Salviamo tutto e proviamo quindi ad eseguire con F5 il nostro programma Hello World modificato. Otterremo


Cosa è successo? E' successo che il preprocessore ha risucchiato il codice presente in saluto.h dentro il sorgente contenente il main, quindi il compilatore non ha avuto problemi a creare i due identificatori e utilizzarli nel codice. Anzi, il preprocessore stesso ha provveduto a sostituire tutte le ricorrenze di SALUTO con il letterale stringa nel codice stesso. Il compilatore ha invece provveduto a sostituire saluto con il suo valore nel codice. Nell'eseguibile quindi non esistono costanti, poiché le stesse occupano con il loro valore le posizioni in cui gli identificativi si presentano. In altri termini non occupano memoria ma sono cablate nel codice.

Iniziamo a fare qualche osservazione.
  • Ma come può l'identificativo saluto e SALUTO non confonde il compilatore? Il C distingue tra lettere minuscole e maiuscole, quindi saluto non è SALUTO e non è SaLuTo. 
  • Ma nelle costanti stringa c'è uno \n. Che fine ha fatto visto che non c'è nella console? Il compilatore sottopone le stringa ad una trasformazione tramite escaping. L'escape è appunto il carattere \ (back slash) che seguito da un altro permette di inserire caratteri speciali all'interno della stringa. In particolare \n è trasformato in un accapo, volendo invece rappresentare il carattere di escape \ letteralmente si userà \\. Ecco la tabella con i caratteri di escape ammessi:


L'esistenza di questo sistema è indispensabile, altrimenti come avremmo potuto inserire un doppio apice in un letterale stringa? Il compilatore avrebbe interpretato il successivo doppio apice al primo come la chiusura della stringa. Invece ora con un \" possiamo assicurarci che non ci siano problemi. Anche se l'escaping è ottenuto tramite due caratteri questo è comunque interpretato dal compilatore come un solo carattere.

Sebbene la direttiva #define sia spesso utilizzata per definire delle semplici costanti è in realtà qualcosa di più. E' un sistema per sostituire pezzi di codice con un semplice identificativo che può essere anche parametrizzato. Facciamo un altro esempio e modifichiamo il nostro file saluto.h come segue.

#define SALUTO               "Hello World - MACRO\n"
#define SALUTO_SPECIALE       printf("Quanto mi piace \"Il linguaggio C\"\n");
const char* saluto = "Hello World - COSTANTE\n";


Come si vede stiamo aggiungendo una macro chiamata SALUTO_SPECIALE che nel codice sarà sostituita, niente di meno che, con una intera istruzione printf con tanto di punto e virgola finale. Modifichiamo quindi il sorgente contenente la funzione main aggiungendo questa macro.

#include "stdio.h"
#include "saluto.h"

main() {
    SALUTO_SPECIALE
    printf(saluto);
    printf(SALUTO);   
}


Salviamo ed eseguiamo il programma e...


Cosa è successo? Il preprocessore ha sostituito ogni ricorrenza di SALUTO_SPECIALE con il valore testuale specificato. Poi, In fase di compilazione, questo valore testuale è stato ovviamente compilato come una normalissima istruzione printf ed ha prodotto il risultato atteso. Sbalorditivo! Ma le macro fanno ancora di più, ma meglio fermarci per ora.

Ora quindi sappiamo che
  • il compilatore C distingue tra caratteri maiuscoli e minuscoli
  • la direttiva #define può essere usata per le costanti ma può fare molto di più
  • la parola chiave const permette di dichiarare delle costanti nel programma
  • i letterali stringa possono contenere sequenze di escaping per rappresentare caratteri speciali
  • all'interno del nostro file esiste una sezione dichiarativa che è qualunque punto al di fuori di qualunque funzione purché prima del loro utilizzo. Però meglio mettere le dichiarazioni in cima subito dopo le direttive al preprocessore per motivi di leggibilità
  • il file di intestazione può contenere raccolte di dichiarazioni e possiamo creare i nostri file header e includerli nei nostri sorgenti