venerdì 30 novembre 2018

C e C++: Appunti di una riscoperta 9

A questo punto è il caso di iniziare a parlare di espressioni, anche perchè fra costanti e variabili abbiamo un sacco di dati ma come li manipoliamo?

Iniziamo con l'operatore di assegnazione che è l'uguale. Lo abbiamo già incontrato quando abbiamo parlato di costanti e di inizializzazione del valore delle variabili. Ed abbiamo accettato che scrivere

int i = 10;

non fa altro che mettere nella variabile i il valore 10. Ma se ancora scriviamo


int i = 10;
i = 20;

abbiamo dato valore iniziale 10 ad i, per poi sostituire il suo contenuto con 20 nell'istruzione successiva. Quindi l'uguale assegna a ciò che si trova a destra il valore dell'espressione calcolata a sinistra. Ma cos'è un'espressione? Un'espressione è una sequenza di operandi e operatori che producono un valore.

Osserviamo che, dato che = è un operatore e l'espressione è una sequenza di operandi e operatori, allora potremo scrivere

#include "stdio.h"

#define FORMATO_ETICHETTA_VALORE "%3s = %-5i\n"

main() {
    int i = 10;
    int j = 20;

    i = j = i + j;      /*questo e' quello che ci interessa*/

    printf(FORMATO_ETICHETTA_VALORE, "i", i);
    printf(FORMATO_ETICHETTA_VALORE, "j", j);
}


Cosa significa i = j = i + j? Il programma non farà altro che risolvere i + j che è 10 + 20. Quindi assegna il valore 30 a j. A questo punto, dato che = è un operatore, j diventa espressione e viene risolta con 30. Questo valore è assegnato a i che assume valore 30. Alla fine avremo quindi

Abbiamo intanto fatto la conoscenza con un altro operatore. L'operatore + è banalmente un operatore di somma aritmetica. Gli altri operatori aritmetici sono - per la sottrazione, * per la moltiplicazione e / per la divisione.

Mettiamoli subito alla prova creando una nuova soluzione e inserendo il seguente codice.

#include "stdio.h"

#define STAMPA_OPERAZIONE_FLOAT(OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO) printf("%.2f %c %.2f = %.2f\n", OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO);

main() {
    float i = 10;
    float j = 20;
   
    STAMPA_OPERAZIONE_FLOAT(i, '+', j, i + j)
    STAMPA_OPERAZIONE_FLOAT(i, '-', j, i - j)
    STAMPA_OPERAZIONE_FLOAT(i, '*', j, i * j)
    STAMPA_OPERAZIONE_FLOAT(i, '/', j, i / j)
}

Santo cielo com'è complicato questo programmino. Innanzi tutto ricordiamo che la #define deve essere scritta su una sola riga e terminata con un accapo. Il punto e virgola appartiene quindi a ciò che sarà sostituito alla macro definita dalla #define.



Approfittiamo dell'occasione per introdurre un nuovo concetto legato alla direttiva del preprocessore #define. La define l'abbiamo vista per definire delle costanti o sostituire pezzi di codice. Ma la define permette di creare anche macro parametrizzate.

Cos'è una macro? In Informatica la macro è un modello che specifica come una sequenza in input debba essere mappata in una sequenza sostituiva in output secondo una procedura definita. Il processo di trasformazione è chiamato espansione della macro.

Cos'è una macro parametrizzata? E' una macro che accetta dei parametri come nell'esempio. E' stata quindi definita, per facilitare la stampa delle operazioni, una macro chiamata STAMPA_OPERAZIONE_FLOAT che accetta 4 segnaposto che sono OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO. In fase di espansione della macro, ciò che è dato in pasto alla macro è sostituito nelle relative posizioni, ossia nel caso della prima invocazione della macro

STAMPA_OPERAZIONE_FLOAT(i, '+', j, i + j)

OPERANDO_1 è sostituito con i, OPERATORE è sostituito da '+', OPERANDO_2 è sostituito da j e RISULTATO è sostituito da i + j. Quindi in espansione

printf("%.2f %c %.2f = %.2f\n", OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO);

diventerà

printf("%.2f %c %.2f = %.2f\n", i, '+', j, i+j);

Tutto questo lo fa il preprocessore quindi il compilatore non troverà le macro, ma la loro espansione nel seguente main

main() {
   float i = 10;
   float j = 20;
   

   printf("%.2f %c %.2f = %.2f\n", i, '+', j, i+j); 
   printf("%.2f %c %.2f = %.2f\n", i, '-', j, i-j);    
   printf("%.2f %c %.2f = %.2f\n", i, '*', j, i*j);    
   printf("%.2f %c %.2f = %.2f\n", i, '/', j, i/j);         
}

Tutto questo ha molteplici vantaggi. Intanto non dobbiamo riscrivere più e più volte le stesse cose, tipo la stringa di formato, quindi saremo meno soggetti a errori. In caso di modifica basta correggere la macro e non ogni riga di codice. Avremmo potuto scrivere una funzione, come ce ne sono tante nel linguaggio fornite dalle librerie, ma una chiamata ad una funzione provoca una serie di operazioni oltre ad allocare memoria per le variabili locali (parametri inclusi) prima della sua esecuzione. Tutto ciò, grazie al processo di sostituzione, con la macro non avviene. Quindi se una funzione è chiamata centinaia, migliaia o centinaia di migliaia di volte nel programma, avere una macro può rendere il programma più veloce, e il C che è stato pensato come un linguaggio intermedio sa essere molto veloce (ovviamente molto dipende anche dalla qualità del compilatore).

Osserviamo come alla printf si possa passare anche un'espressione e non necessariamente una variabile o una costante. In realtà, sia la costante che la variabile, sono forme semplici di espressioni. In particolare la costante è una espressione già risolta nel valore che la costante esprime, mentre la variabile è una espressione risolta con il valore in essa contenuto. Qualunque espressione sarà comunque risolta prima di passare il valore alla funzione. Perchè in C il passaggio dei parametri alle funzioni è sempre per valore.

Otterremo quindi come risultato dell'esecuzione


Che era ciò che ci aspettavamo. Ossia le 4 operazioni aritmetiche fondamentali. Gli operatori aritmetici sono polimorfi, ossia possono essere applicati a tipi differenti. Infatti se per noi è normale pensare ai numeri interi come sottoinsieme dei numeri reali, per il computer non è così, infatti gli interi sono le relative conversioni in formato binario in complemento a 2 se negativi, mentre i numeri in virgola mobile utilizzano le specifiche IEEE (Institute of Electrical and Electronics Engineers).

Quindi l'espressione assume il tipo più preciso ossia in una operazione tra float il risultato sarà float, mentre in una operazione tra int il risultato sarà int. In operazioni miste int e float, l'int è prima convertito in float e poi è eseguita l'operazione che restituirà un float. Vediamo come si comportano gli operatori aritmetici con gli int e con operazioni miste

#include "stdio.h"

#define STAMPA_OPERAZIONE_FLOAT(OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO) printf("%.2f %c %.2f = %.2f\n", OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO);
#define STAMPA_OPERAZIONE_INT(OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO) printf("%i %c %i = %i\n", OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO);
#define STAMPA_OPERAZIONE_INT_FLT(OPERANDO_1_INT, OPERATORE, OPERANDO_2_FLT, RISULTATO) printf("%i %c %.2f = %.2f\n", OPERANDO_1_INT, OPERATORE, OPERANDO_2_FLT, RISULTATO);

main() {
    float i = 10;
    float j = 20;
    int z = 5, k = 7;
   
    printf("Operatori aritmetici su numeri in virgola mobile\n");
    STAMPA_OPERAZIONE_FLOAT(i, '+', j, i + j)
    STAMPA_OPERAZIONE_FLOAT(i, '-', j, i - j)
    STAMPA_OPERAZIONE_FLOAT(i, '*', j, i * j)
    STAMPA_OPERAZIONE_FLOAT(i, '/', j, i / j)

    printf("\n\n");
    printf("Operatori aritmetici su numeri interi\n");
    STAMPA_OPERAZIONE_INT(z, '+', k, z + k)
    STAMPA_OPERAZIONE_INT(z, '-', k, z - k)
    STAMPA_OPERAZIONE_INT(z, '*', k, z * k)
    STAMPA_OPERAZIONE_INT(z, '/', k, z / k)
    STAMPA_OPERAZIONE_INT(z, '%', k, z % k)

    printf("\n\n");
    printf("Operatori aritmetici su numeri interi\n");
    STAMPA_OPERAZIONE_INT_FLT(z, '+', j, z + j)
    STAMPA_OPERAZIONE_INT_FLT(z, '-', j, z + j)
    STAMPA_OPERAZIONE_INT_FLT(z, '*', j, z + j)
    STAMPA_OPERAZIONE_INT_FLT(z, '/', j, z + j)
}


Sono quindi state aggiunte un paio di variabili intere e le stampe delle operazioni aritmetiche per i tipi interi e per i tipi misti interi e in virgola mobile. Iniziamo con l'osservare che nel caso delle operazioni fra interi il risultato della divisione sarà un intero. Per avere il resto della divisione esiste l'operatore % che richiede necessariamente che i suoi operandi siano di tipo integrale ossia appartenenti alla famiglia dei numeri interi. Quindi la divisione intera 5/7 produce 0 con resto di 5. Nel caso delle operazioni miste invece, tipo z + j con z int e j float, il valore intero è promosso a float, quindi è eseguita l'operazione che produce un valore di tipo float. E' stata quindi fatta una conversione, o cast, implicita. Ciò non crea alcun problema fintanto non c'è perdita d'informazione. Perdita che si potrebbe avere nel caso in cui si tenti magari di assegnare una espressione valutata float a un int, che di certo non potrà avere la parte frazionaria. Se si tenta di assegnare una variabile di tipo float a una variabile di tipo int, il compilatore emette un warning come questo

warning C4244: '=': conversione da 'float' a 'int'. Possibile perdita di dati.

è però possibile informare il compilatore che sappiamo a cosa andiamo incontro per mezzo di un cast o conversione esplicita del tipo. Il cast esplicito si fa mettendo fra parentesi tonde il tipo in cui convertire ciò che segue. Quindi (int)j converte il valore prodotto dall'espressione j ossia il suo contenuto in un valore intero senza emettere alcun warning. Completiamo l'esempio.

#include "stdio.h"

#define STAMPA_OPERAZIONE_FLOAT(OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO) printf("%.2f %c %.2f = %.2f\n", OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO);
#define STAMPA_OPERAZIONE_INT(OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO) printf("%i %c %i = %i\n", OPERANDO_1, OPERATORE, OPERANDO_2, RISULTATO);
#define STAMPA_OPERAZIONE_INT_FLT(OPERANDO_1_INT, OPERATORE, OPERANDO_2_FLT, RISULTATO) printf("%i %c %.2f = %.2f\n", OPERANDO_1_INT, OPERATORE, OPERANDO_2_FLT, RISULTATO);

main() {
    float i = 10;
    float j = 20;
    int z = 5, k = 7;
   
    printf("Operatori aritmetici su numeri in virgola mobile\n");
    STAMPA_OPERAZIONE_FLOAT(i, '+', j, i + j)
    STAMPA_OPERAZIONE_FLOAT(i, '-', j, i - j)
    STAMPA_OPERAZIONE_FLOAT(i, '*', j, i * j)
    STAMPA_OPERAZIONE_FLOAT(i, '/', j, i / j)

    printf("\n\n");
    printf("Operatori aritmetici su numeri interi\n");
    STAMPA_OPERAZIONE_INT(z, '+', k, z + k)
    STAMPA_OPERAZIONE_INT(z, '-', k, z - k)
    STAMPA_OPERAZIONE_INT(z, '*', k, z * k)
    STAMPA_OPERAZIONE_INT(z, '/', k, z / k)
    STAMPA_OPERAZIONE_INT(z, '%', k, z % k)

    printf("\n\n");
    printf("Operatori aritmetici su numeri interi\n");
    STAMPA_OPERAZIONE_INT_FLT(z, '+', j, z + j)
    STAMPA_OPERAZIONE_INT_FLT(z, '-', j, z + j)
    STAMPA_OPERAZIONE_INT_FLT(z, '*', j, z + j)
    STAMPA_OPERAZIONE_INT_FLT(z, '/', j, z + j)

    printf("\n\n");
    printf("La conversione esplicita\n");
    z = i / j; /* conversione che produce warning */
    k = (int)(i / j);
    printf("z = %i e k = %i", z, k);

    printf("\n\n");
}


Questo programma produrrà


possiamo osservare come nella finestra Output il compilatore lanci un warning per la riga 35 in cui troviamo

z = i / j;

in cui i e j sono float. Quindi il risultato dell'espressione è float, e lo vediamo in esecuzione che è 0.5, ma si tenta di assegnarlo a z che è un int.

Il warning invece non è prodotto per la riga successiva che recita

k = (int)(i / j);

In cui è applicato il cast (int) al risultato dell'espressione. Le parentesi tonde intorno all'espressione servono a far convertire il risultato dell'espressione. In loro assenza il cast sarebbe stato applicato alla sola i, ma poi essendo j di tipo float, il valore di i convertito in int sarebbe stato promosso a float e il risultato dell'espressione sarebbe stato float con produzione del warning.


Ma non si pensi che l'informazione vada persa solo se si passa da un float a un int. Si pensi al caso del post precedente in cui alla printf si dava un valore int ma le si diceva di trattarlo come char utilizzando lo specificatore di tipo hh. Nella conversione da int a char il 1024 diventava 0. esattamente ciò che accade facendo:

char c = (char)1024;

Il cast (char) non farà apparire l'avvertimento del compilatore, circa il troncamento dell'informazione dopo i primi 8 bit, ma c conterrà comunque 0 e non 1024.

Ok, ci scoppia il cervello ma abbiamo imparato tante cose tra cui
  • parametrizzare la direttiva del preprocessore #define
  • l'operatore di assegnazione
  • realizzare semplici espressioni utilizzando gli operatori aritmetici 
  • la conversione implicita ed esplicita fra tipi di dato