Sesta Lezione: Dichiarazioni, assegnamenti, conversioni e caratteri in C (Italiano)
Dichiarazioni e assegnamenti
In una lezione sull’insieme di istruzioni di base, sono stati introdotti due concetti fondamentali: le dichiarazioni (dichiarazioni di variabili) e gli assegnamenti (modifica del contenuto di una variabile). Le dichiarazioni servono a introdurre una variabile e, opzionalmente, a specificarne il tipo. Ad esempio, dichiarare una variabile intera A permette di utilizzare A da quel momento in poi come contenitore di valori interi. Le dichiarazioni possono riguardare una o più variabili e, tipicamente, indicano anche il tipo della variabile. Le assegnazioni, al contrario, sono istruzioni che modificano il valore memorizzato in una variabile: una scrittura con la forma
dove a sinistra c’è una variabile, e a destra un’espressione. L’assegnamento ha una semantica ben definita: si valuta prima l’espressione a destra, si converte al tipo della variabile a sinistra se necessario, e infine si salva il valore nella cella di memoria corrispondente alla variabile a sinistra. L’ordine di esecuzione tipico è: 1) valutare il lato destro, 2) eventuale conversione al tipo di sinistra, 3) scrivere il valore inside la variabile a sinistra.
Esempi pratici (con tipo e contenuto):
Se A e B sono interi e scrivo A = B, si copia il valore di B in A; se B vale 2 e A vale 2, ora A vale 2.
Se alimento una variabile media di tipo float con una espressione intera, si applica la conversione implicita: ad es., qui 8 e 3 sono interi, quindi 8/3 è calcolato come divisione intera, risultando 2; poi 2 viene convertito a float (2.0) e memorizzato in media.
Esecuzione temporale dell’assegnamento
L’assegnamento avviene tipicamente in tre passi concreti. Consideriamo esempi concreti dove A, B, C sono interi e media è un float:
Caso 1: Nella retrospettiva dell’esecuzione, si calcola 2, non c’è conversione necessaria, si memorizza 2 in A.
Caso 2: anche qui nessuna conversione e 3 viene scritto in B.
Caso 3: qui sul valore 3.1 non si può assegnare direttamente a un intero; se C fosse di tipo int, l’assegnamento richiederebbe una conversione. In pratica, si dovrà convertire 3.1 in intero (tipicamente troncando la parte frazionaria) se il lato sinistro è int.
Compatibilità di tipo e relazione tra sinistra e destra
Quando si esegue un assegnamento, in genere la relazione richiesta tra i tipi è che il tipo a sinistra sia almeno uguale o più grande del tipo a destra, altrimenti possono sorgere problemi di perdita di precisione o di overflow. Esempi:
assegnare un valore di tipo long a una variabile di tipo int può provocare perdita di dati se il valore non rientra nell’intervallo di rappresentazione dell’int.
assegnare un valore di tipo float a una variabile di tipo int comporta un truncate (perdere la parte frazionaria).
In forma generale, quando destro e sinistro hanno tipi differenti, avviene una conversione implicita o esplicita:
Conversione implicita: se sinistro è del tipo più grande o uguale rispetto al tipo del lato destro, il valore viene convertito automaticamente al tipo di sinistra per l’assegnamento.
Conversione esplicita: quando si vuole forzare una conversione diversa da quella implicita, si usa un cast, ad es.
spiega la differenza tra la conversione esplicita (con cast) e quella implicita (automatico, a seconda dei tipi coinvolti).
Esempi dettagliati di esecuzione e di conversioni
Esempio 1: assegnamento semplice tra interi
dati: A = 2, B = 3, C = 3; esecuzione: B = 3; A = 2; C = 3.
In alcuni casi si osserva l’overflow quando si moltiplicano interi molto grandi. Se A è un int e si fa A = A * 2, e il risultato non rientra nell’intervallo rappresentabile, si verifica overflow: l’overflow implica che si scrivono i bit rimanenti (tipicamente i 32 bit meno significativi) e si ottiene un valore errato o indesiderato.
Esempio 2: operazione mista int e float durante un assegnamento
sinistra: media di tipo float; destra: A / B con A e B interi, ad es. A = 8, B = 3.
se scrivo: , la divisione è intera (8/3 = 2), quindi media diventa 2.0 (conversione implicita da int a float).
se invece scrivo: , la divisione viene fatta in float: 8.0 / 3 = 2.666\,…; media assume 2.666….
se scrivo: , ottengo lo stesso risultato di sopra: 2.666…
se scrivo: , anche qui ottengo 2.666…
Caso particolare; operazioni ibride: se una operazione contiene sia float sia int, la semantica comune è che il tipo di risultato sia il più grande tra i due; la conversione implicita avviene per la parte meno grande.
Esempio 3: conversione esplicita su assegnamento con tipo diverso
Lettura di una espressione come:
Qui prima si esegue la somma , si converte a float 11.0, e poi si divide per 3 ottenendo circa 3.6667, che viene memorizzato in media.
Caso pericoloso: overflow e uso di long
Un esempio di pericolo di overflow si mostra con una moltiplicazione tra interi molto grandi: se si fa e A è vicino al massimo valore intero, l’overflow si verifica, e il risultato è errato. Per ridurre il rischio si può usare tipi più grandi: ad es. if si sostituisce int con long, l’intervallo disponibile aumenta (in ambienti tipici, int è 32 bit, long è 64 bit). Se una operazione mista coinvolge sia int sia long, può essere utile “promuovere” l’operando più piccolo a long, ad es. o forzare una costante come long:
Esempio pratico: se si calcola dove A è int e si ottiene overflow, si può evitare l’overflow facendo la moltiplicazione tra long: che usa la rappresentazione a 64 bit di long.
Assegnamenti composti (compound assignments)
In C esistono assegnamenti composti che combinano un’operazione aritmetica con l’assegnamento, ad es. +=, -=, *=, /=, ecc. L’uso comune è:
equivale a
è anche equivalente a
è equivalente a
Anche forme compatte senza spazi come o sono comuni: V viene incrementato/decrementato di 1.
Gli studenti apprendono anche forme ancora più compatte come (non standard) oppure combinazioni con meno spazi: è la forma compatta typica; non si space-avoid i caratteri, si scrivono come una regola comune.
Esempio pratico:
è equivalente a e a sua volta a (non tradotto come tale; usare la forma corretta).
Confronti e valore booleano (operatori di confronto)
In C, gli operatori di confronto includono: <,\, >, \, <=, \, >=, \, ==, \, !=. Il risultato di un confronto è tipicamente un intero: 0 se falso, 1 se vero (nella pratica, molti compilatori trattano 0 come falso e qualsiasi valore diverso da 0 come vero; la forma standard è 0 per falso e 1 per vero).
Esempi:
Se A = 13 e B = 17, allora A < B è vero, quindi l’espressione produce 1.
Se invece scrivo A > B, risulta falso, quindi l’espressione produce 0.
Questi operatori sono binari: confrontano due espressioni. È importante non scrivere forme non standard come confrontare più di due espressioni contemporaneamente; si hanno espressioni del tipo . Inoltre, in assenza di un tipo booleano esplicito, si usa 0 o 1 come valore booleano.
Per introdurre un bool esplicito, molti linguaggi C moderni propongono un tipo booleano (bool) tramite stdbool.h, ma nel contesto della lezione si è fatto subito ricorso a 0/1.
Lettura e stampa: input, output e formato ASCII
Lettura: la funzione scanf consente di leggere input dall’utente. Per un intero si usa tipicamente:
int num;
scanf("%d", &num);
Stampa: per stampare interi si usa printf con %d; per stampare caratteri si usa %c; per stampare anche il valore numerico di un carattere si usa %d.
Codifica dei caratteri: la codifica ASCII (ASC) assegna a ogni carattere una corrispondenza numerica; ad esempio:
'A' maiuscola -> 65
'a' minuscola -> 97
lo spazio -> 32
'!'' -> 33
'$' -> 36
'@' -> 64
la gamma di lettere maiuscole è continua da 'A' a 'Z' (65–90), quella minuscola da 'a' a 'z' (97–122).
Questa tabella consente di manipolare i caratteri come numeri interi. Per stampare un carattere a partire dal suo codice numerico si usa:
dove V contiene un valore ASCII; se V = 75, viene stampata la lettera 'K' (sempre che la codifica ASCII sia usata).
Per stampare solo il codice numerico si usa: se V contiene 75.
Costanti carattere in single quotes
In C è comune rappresentare un carattere tra apici singoli: 'A' ha valore numerico 65, quindi la costante 'A' è equivalente al valore intero 65. Si può anche scrivere V = 'K' per assegnare a V il codice ASCII di 'K' (che è 75).
Operazioni sui caratteri
Si possono utilizzare operatori aritmetici sui char, ad es. incrementare un carattere: int c = 'A'; c++; // ora c è 'B'.
stampando con %c si ottiene il carattere e stampando con %d si ottiene il codice numerico.
Esempi concreti con caratteri
Se V = 'K' (75) e si stampa con %c, si ottiene la lettera K. Se si stampa con %d, si ottiene 75.
Se si imposta V = 119 (codice di 'w'), e si stampa con %c, si ottiene 'w'; con %d si ottiene 119.
L’uso di %c è particolarmente utile per stampare i caratteri in modo leggibile; l’uso di %d è utile per vedere i codici numerici.
Note sulle cifre e i caratteri di controllo
Nella tabella ASCII vi sono anche caratteri di controllo come la tabulazione orizzontale (carattere 9). Se stampiamo con %c potremmo non vederli in output, ma sono presenti nella codifica.
Le lettere maiuscole hanno codici inferiori rispetto alle minuscole; ordini: A (65) … Z (90) prima di a (97) … z (122). Incrementare una lettera permette di ottenere la lettera successiva nel alfabeto; ad esempio, 'A' + 1 -> 'B'.
Sintesi: perché questi dettagli contano per l’esame
Le dichiarazioni servono a definire variabili e i relativi tipi; gli assegnamenti modificano il contenuto di una variabile seguendo una semantica ben definita (valore destro, eventuale conversione, assegnazione).
Le conversioni tra tipi possono essere implicite (automatiche) o esplicite (con cast). In operazioni ibride (mescolando int e float), di norma il tipo più grande è utilizzato per la calcolazione prima di una eventuale assegnazione al tipo sinistro.
L’overflow è un problema concreto per tipi a dimensione fissa (es. int 32 bit); si può evitare promuovendo il calcolo a un tipo più grande (long) o cast espliciti.
Gli operatori di assegnamento composto (+=, -=, *=, /=, ecc.) offrono una forma compatta per eseguire un’operazione e assegnare il risultato.
Gli operatori di confronto producono 0 o 1 e servono per dirigere flussi di controllo; in assenza di un tipo booleano esplicito, 0 è false e non-0 è true.
I caratteri sono rappresentati come numeri secondo la codifica ASCII; si può stampare un carattere con %c o il suo codice numerico con %d. La tabella ASCII rende possibile manipolarli come numeri e comprenderne la sequenza (A prima di a, ecc.).
Note finali sull’allineamento pratica-teoria
Esercizi pratici consigliati: provare a costruire piccoli programmi che mostrino l’ordinamento di tipi, l’overflow, e l’effetto dei cast. Ad esempio: A = 2; B = 3; C = A < B; stampa C; poi prova A > B; osserva 0/1. Prova con una variabile float e interi combinati in un’espressione, sia con conversioni implicite sia esplicite.
Ripassa le differenze tra %d, %f, %c, e i corrispondenti formati per tipi diversi (int, long, float, char).
Rivedi la codifica ASCII e fai esperimenti con caratteri: stampa sia il codice che il carattere, osservando come l’incremento di una variabile char cambia il carattere stampato.
";