Salta ai contenuti

Lezione 3: La Cassetta degli Attrezzi del C

Copertina Lezione 3

Nelle lezioni precedenti abbiamo costruito le fondamenta del C: le variabili int e double, i formati base di printf e scanf, gli operatori aritmetici e la logica condizionale con if. In questa lezione completiamo la nostra cassetta degli attrezzi fondamentale: impareremo a scegliere il tipo più adatto per ogni situazione grazie ai modificatori (unsigned, short, long), a formattare l’output con precisione chirurgica grazie al padding di printf, a nominare i valori fissi con const e #define, a convertire dati tra tipi diversi in modo consapevole e ad accedere alle funzioni matematiche avanzate della libreria <math.h>. Chiuderemo con una demo sui limiti dei tipi numerici: overflow, underflow, inf e NAN.


Ricordate che un tipo di dato definisce due cose: quanta memoria viene allocata e come vengono interpretati i bit al suo interno. Abbiamo già incontrato int (4 byte, numeri interi) e double (8 byte, numeri reali in doppia precisione). Ma il C mette a disposizione una famiglia più ampia di tipi interi.

Modificatori di tipo, il tipo char (ASCII) e la dipendenza dalla macchina

Sezione intitolata “Modificatori di tipo, il tipo char (ASCII) e la dipendenza dalla macchina”
  • La dipendenza dalla macchina (Machine Dependency) Una delle caratteristiche più importanti del C è che la dimensione in byte dei tipi di dato non è fissa o universale. Lo standard C definisce solo dei requisiti minimi e delle relazioni d’ordine (es. sizeof(short) <= sizeof(int) <= sizeof(long)). La dimensione effettiva dipende dall’architettura del processore (16, 32 o 64 bit) e dal compilatore (il sistema operativo e le sue convenzioni di ABI). Il tipo char è l’unico garantito occupare sempre 1 byte (8 bit) in qualsiasi sistema compatibile.

  • I modificatori degli interi Permettono di adattare il tipo alle proprie esigenze senza sprecare memoria:

    • short int (o short): tipicamente 2 byte, valori da −32.768 a +32.767. Utile per piccoli numeri.
    • int: tipicamente 4 byte, valori da −2 miliardi a +2 miliardi.
    • long int (o long): tipicamente 8 byte su sistemi moderni a 64 bit, per numeri molto grandi.
    • unsigned int / unsigned long: rimuovono il segno, consentendo di rappresentare solo valori positivi e raddoppiando il range positivo.
  • Il tipo char è un intero piccolo sotto la maschera! Spesso si pensa al tipo char come a qualcosa di radicalmente diverso perché serve a memorizzare lettere o simboli (es. 'a', '?'). Sotto il cofano del calcolatore, un char è semplicemente un numero intero a 8 bit (1 byte). Può memorizzare valori numerici compresi tra −128-128 e +127+127 (se con segno) o da 00 a 255255 (se unsigned char).

    L’associazione tra il numero binario memorizzato in RAM e il carattere visualizzato a schermo è governata da una convenzione di codifica: la tabella ASCII (American Standard Code for Information Interchange). Ad esempio, memorizzare il carattere 'A' significa scrivere in RAM il valore intero 65 (pari al binario 01000001).

    L’equivalenza in pratica:

    #include <stdio.h>
    int main(void) {
    char c = 'A';
    printf("Visualizzato come carattere: %c\n", c); // Stampa: A
    printf("Visualizzato come intero: %d\n", c); // Stampa: 65
    // Possiamo fare operazioni matematiche sui caratteri!
    c = c + 1;
    printf("Nuovo carattere: %c\n", c); // Stampa: B (66 in ASCII)
    return 0;
    }
  • La Cultura dello Sviluppatore: Consultare la tabella ASCII Non bisogna imparare i codici a memoria. Su qualsiasi sistema Unix/macOS basta digitare nel terminale:

    Terminal window
    man ascii

    In alternativa, si può consultare la risorsa web di riferimento su devdocs.io/c/language/ascii.

  • Tabella dei range tipici e dipendenza dall’implementazione Ecco i range tipici su una macchina a 64 bit moderna:

TipoByte (tipici)Intervallo di ValoriSpecificatore printf
char1da −128-128 a +127+127%c (carattere) o %d (intero)
unsigned char1da 00 a 255255%d o %u
short2da −32.768-32.768 a +32.767+32.767%d
unsigned short2da 00 a 65.53565.535%u
int4da −2.147.483.648-2.147.483.648 a +2.147.483.647+2.147.483.647%d
unsigned int4da 00 a 4.294.967.2954.294.967.295%u
long8da ≈−9.22×1018\approx -9.22 \times 10^{18} a ≈+9.22×1018\approx +9.22 \times 10^{18}%ld
unsigned long8da 00 a ≈1.84×1019\approx 1.84 \times 10^{19}%lu
float4da ≈1.17×10−38\approx 1.17 \times 10^{-38} a ≈3.40×1038\approx 3.40 \times 10^{38}%f (o %e / %g)
double8da ≈2.22×10−308\approx 2.22 \times 10^{-308} a ≈1.80×10308\approx 1.80 \times 10^{308}%f (o %e / %g)

L’operatore sizeof restituisce a tempo di compilazione il numero di byte occupato da un tipo o da una variabile. È molto utile per verificare il comportamento del compilatore sulla propria macchina:

#include <stdio.h>
int main(void) {
printf("char: %zu byte\n", sizeof(char));
printf("short: %zu byte\n", sizeof(short));
printf("int: %zu byte\n", sizeof(int));
printf("long: %zu byte\n", sizeof(long));
printf("float: %zu byte\n", sizeof(float));
printf("double: %zu byte\n", sizeof(double));
return 0;
}

Non è necessario ricordare i valori limite a memoria. La libreria standard mette a disposizione costanti predefinite che il compilatore calcola automaticamente per la piattaforma corrente:

#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("INT_MAX = %d\n", INT_MAX); // +2.147.483.647
printf("INT_MIN = %d\n", INT_MIN); // -2.147.483.648
printf("UINT_MAX = %u\n", UINT_MAX); // +4.294.967.295
printf("LONG_MAX = %ld\n", LONG_MAX);
printf("DBL_MAX = %e\n", DBL_MAX); // il più grande double positivo
printf("DBL_MIN = %e\n", DBL_MIN); // il più piccolo double positivo normalizzato
return 0;
}

La competenza chiave non è memorizzare questi numeri: è sapere dove cercarli. Aprire <limits.h> dalla documentazione di sistema è una delle prime abitudini da coltivare.


Un double ha 64 bit. Lo standard IEEE 754 li divide così:

CampoBitRuolo
Segno1Indica positivo/negativo
Esponente11Scala il numero (potenza di 2), codificato con bias 1023
Mantissa52Le cifre significative

Il valore rappresentato è:

(−1)s×1.mantissa×2(e−1023)(-1)^s \times 1.\text{mantissa} \times 2^{(e - 1023)}

È la notazione scientifica, ma in base 2.

L’esponente reale può essere negativo (numeri molto piccoli) o positivo (numeri molto grandi).
Gli 11 bit dell’esponente possono rappresentare valori da 0 a 2047. Pre rappresentare esponenti negativi, il bias 1023 “centra” la gamma:

  • e_mem = 0 → e_reale = -1023 (numeri minuscoli)
  • e_mem = 2047 → e_reale = +1024 (numeri enormi)

Così un double può rappresentare valori che vanno da circa (10^{-308}) a (10^{308}).

0.1 in binario è infinito:

0.1=0.00011001100110011001100…20.1 = 0.00011001100110011001100\ldots_2

Normalizzato:

0.1=1.1001100110011…×2−40.1 = 1.1001100110011\ldots \times 2^{-4}

Quindi:

  • segno = 0
  • esponente reale = −4 (mi sono “spostato” di 4 posizioni dopo la virgola)
  • esponente memorizzato = −4 + 1023 = 1019
  • mantissa = primi 52 bit dell’espansione infinita
CampoBit (valore binario)SignificatoValore decimale
Segno0Numero positivo+
Esponente01111111011 (1019)Esponente memorizzatoEsponente reale = 1019 − 1023 = −4
Mantissa1001100110011001100110011001100110011001100110011001Frazione binaria normalizzata≈ 1.100110011001100…

Ricostruzione:

0.1IEEE754=1.1001100110011…×2−40.1_{\text{IEEE754}} = 1.1001100110011\ldots \times 2^{-4}

Valore effettivo memorizzato:

0.10000000000000000555…0.10000000000000000555\ldots

Non è esattamente 0.1.

La conseguenza pratica: la maggior parte dei numeri decimali comuni, come 0.1, non si possono rappresentare esattamente in base 2, esattamente come 1/3 non si può scrivere esattamente in base 10. La mantissa da 52 bit introduce un errore di arrotondamento minuscolo ma reale.

Possiamo osservarlo direttamente:

#include <stdio.h>
int main(void) {
double a = 0.1 + 0.2;
printf("%.1f\n", a); // stampa: 0.3 (sembra corretto...)
printf("%.17f\n", a); // stampa: 0.30000000000000004 (la verità!)
return 0;
}

I valori memorizzati sono:

0.1≈0.100000000000000005550.1 \approx 0.10000000000000000555

0.2≈0.200000000000000011100.2 \approx 0.20000000000000001110

Somma:

0.300000000000000044410.30000000000000004441


In L2 abbiamo usato principalmente %d e %f. Ecco il quadro completo dei formati che userete più spesso in questo corso:

FormatoTipo CEsempio di output
%dint (con segno)42, -7
%uunsigned int4294967295
%ldlong int9223372036854775807
%luunsigned long18446744073709551615
%zusize_t (risultato di sizeof)8
%fdouble (virgola fissa, 6 decimali default)3.141593
%edouble (notazione scientifica)3.141593e+00
%gdouble (sceglie il formato più compatto)3.14159
%ccharA
%sstringa (char *)hello
%%carattere letterale %%

In L2 abbiamo visto %f con il formato di default (6 decimali). Possiamo controllare esattamente quante cifre mostrare usando il modificatore .<N> tra % e la lettera di formato:

  • %.2f → 2 cifre dopo la virgola (es. 3.14)
  • %.0f → solo la parte intera, senza virgola (es. 3)
  • %.3e → notazione scientifica con 3 cifre decimali (es. 3.142e+00)
  • %.17f → 17 cifre: utile per esporre gli errori di arrotondamento IEEE 754
#include <stdio.h>
int main(void) {
double pi = 3.14159265358979;
printf("Default: %f\n", pi); // 3.141593
printf("2 decimali: %.2f\n", pi); // 3.14
printf("0 decimali: %.0f\n", pi); // 3
printf("Scientifica:%.3e\n", pi); // 3.142e+00
return 0;
}

Quando dobbiamo stampare dati in forma tabellare allineata, possiamo specificare l’ampiezza minima del campo aggiungendo un numero intero tra % e la lettera del formato:

SpecificatoreEffetto
%10dOccupa almeno 10 caratteri, allineato a destra (spazi a sinistra)
%-10dAllineato a sinistra (spazi a destra)
%010dPadding con zeri invece di spazi
%10.2fAlmeno 10 caratteri totali, di cui 2 decimali

Esempio pratico — generare una tabella ben allineata:

#include <stdio.h>
int main(void) {
printf("%-20s %8s %10s\n", "Prodotto", "Quantità", "Prezzo");
printf("%-20s %8d %10.2f\n", "Mela", 12, 0.89);
printf("%-20s %8d %10.2f\n", "Cioccolato", 3, 2.50);
printf("%-20s %8d %10.2f\n", "Acqua minerale", 6, 0.45);
return 0;
}

Output:

Prodotto Quantità Prezzo
Mela 12 0.89
Cioccolato 3 2.50
Acqua minerale 6 0.45

4. Costanti: Tre Modi per Dare un Nome a un Valore Fisso

Sezione intitolata “4. Costanti: Tre Modi per Dare un Nome a un Valore Fisso”

Immaginate di scrivere il valore 3.14159 cinque volte in uno stesso programma. Cosa succede se vi siete sbagliati di una cifra in un solo punto? Le costanti nominali risolvono questo problema.

I valori che inserisco direttamente nelle istruzioni del programma vengono chiamati letterali. Un valore letterale come 75 o 3.14 ha un tipo implicito. Il compilatore lo determina automaticamente, ma a volte dobbiamo essere espliciti:

75 // int
75u // unsigned int
75l // long int
3.14 // double (attenzione: non float!)
3.14f // float
3.14159L // long double

Questo è importante quando si passano argomenti a funzioni che si aspettano un tipo specifico, o quando si sommano tipi diversi.

Il modo più diretto e moderno per definire una costante nominale è la keyword const:

const double PI = 3.14159265358979;
const int MAX_STUDENTI = 200;

Il compilatore garantisce che nessun’altra parte del codice possa modificare il valore. Se tentate di farlo, otterrete un errore di compilazione:

const double PI = 3.14;
PI = 3.1415; // ERRORE: assignment of read-only variable 'PI'

Quando usare const: ogni volta che un valore ha un significato semantico preciso e non deve mai cambiare nel programma (aliquote fiscali, costanti fisiche, dimensioni di array, etc.).

Un’alternativa storica a const è la macro del preprocessore:

#define VOTO_MINIMO 18
#define CORSO "Programmazione 1"

A differenza di const, #define non crea una variabile: il preprocessore sostituisce meccanicamente ogni occorrenza del nome con il testo corrispondente prima ancora che il compilatore veda il codice.

Possiamo osservare questo comportamento direttamente usando il flag -E di GCC, che ferma il processo dopo la fase di preprocessing e scrive il risultato in un file .i:

/* demo_define.c — senza nessuna #include per vedere l'output pulito */
#define VOTO_MINIMO 18
int main(void) {
return VOTO_MINIMO;
}
Terminal window
gcc -E demo_define.c -o demo_define.i
cat demo_define.i

Output (semplificato):

# 1 "demo_define.c"
# 1 "<built-in>" 1
...
int main(void) {
return 18;
}

Il nome VOTO_MINIMO è completamente scomparso: il compilatore vedrà solo 18. Per convenzione i nomi di macro si scrivono in MAIUSCOLO per distinguerli visivamente dalle variabili.

La libreria standard offre già molte costanti pronte all’uso, definite tramite #define:

  • M_PI (in <math.h>) → π con massima precisione da double
  • M_E (in <math.h>) → il numero di Eulero ee
  • INT_MAX, INT_MIN (in <limits.h>) → limiti degli interi
  • DBL_MAX, DBL_EPSILON (in <float.h>) → limiti e precisione dei double

Quando un’espressione mescola tipi diversi, il C promuove automaticamente al tipo “più capiente” per evitare la perdita di informazioni:

  • int + double → il compilatore converte l’int in double prima di sommare
  • double assegnato a int → la parte decimale viene troncata (non arrotondata!)
double x = 7.9;
int y = x; // y vale 7, non 8! Troncamento silenzioso.
printf("%d\n", y); // stampa: 7

Questo troncamento silenzioso è una fonte comune di bug. Attenzione: né -Wall né -Wextra lo segnalano; con gcc serve il flag -Wconversion (warning: conversion from 'double' to 'int' may change value). Se il warning compare, non ignoratelo.

Per convertire un tipo in modo deliberato e controllato, si usa il cast esplicito: si scrive il tipo di destinazione tra parentesi tonde prima dell’espressione.

int a = 5, b = 2;
double risultato = (double)a / b; // 2.5 — cast prima della divisione
double sbagliato = (double)(a / b); // 2.0 — divisione intera, poi cast
printf("%.1f vs %.1f\n", risultato, sbagliato);

Il troncamento implicito di (int)x non è sempre il comportamento desiderato. La libreria <math.h> offre tre funzioni di arrotondamento con semantiche diverse:

  • round(x) → arrotondamento tradizionale: 2.4 → 2.0, 2.5 → 3.0
  • floor(x) → sempre verso il basso (pavimento): 2.9 → 2.0, -2.1 → -3.0
  • ceil(x) → sempre verso l’alto (soffitto): 2.1 → 3.0, -2.9 → -2.0

Tutte e tre restituiscono un double. Per ottenere un int, combinate con il cast:

#include <math.h>
int n = (int)round(7.6); // n = 8
int m = (int)floor(7.6); // m = 7
int p = (int)ceil(7.2); // p = 8

Le funzioni trigonometriche, le potenze e le radici richiedono istruzioni macchina specializzate (FPU — Floating Point Unit). Il C non le include nel nucleo del linguaggio: si importano esplicitamente con #include <math.h>.

FunzioneDescrizioneEsempio
sqrt(x)Radice quadratasqrt(9.0) → 3.0
pow(x, y)xyx^ypow(2, 10) → 1024.0
fabs(x)Valore assoluto per doublefabs(-3.5) → 3.5
sin(x), cos(x), tan(x)Funzioni trigonometriche (in radianti)sin(M_PI) ≈ 0.0
log(x)Logaritmo naturale (ln⁡\ln)log(M_E) → 1.0
log10(x)Logaritmo in base 10log10(100.0) → 2.0
round(x), floor(x), ceil(x)Arrotondamenti (vedi §5)

La costante M_PI definisce π con la massima precisione disponibile nel tipo double.

Demo Pratica: Calcolatore di Moto Uniformemente Accelerato

Sezione intitolata “Demo Pratica: Calcolatore di Moto Uniformemente Accelerato”

Il seguente programma utilizza tutto ciò che abbiamo visto in questa lezione: tipi, formati con padding, costanti, <math.h> e casting:

#include <stdio.h>
#include <math.h>
int main(void) {
double v0, a, t;
printf("Velocità iniziale (m/s): ");
scanf("%lf", &v0);
printf("Accelerazione (m/s²): ");
scanf("%lf", &a);
printf("Tempo (s): ");
scanf("%lf", &t);
double s = v0 * t + 0.5 * a * t * t; // spazio percorso
double vf = v0 + a * t; // velocità finale
double ec = 0.5 * vf * vf; // energia cinetica specifica (massa=1 kg)
printf("\n--- Risultati ---\n");
printf("Spazio percorso: %10.3f m\n", s);
printf("Velocità finale: %10.3f m/s\n", vf);
printf("Energia cinetica: %10.3f J/kg\n", ec);
return 0;
}

Cosa Succede Quando Superiamo i Limiti degli Interi?

Sezione intitolata “Cosa Succede Quando Superiamo i Limiti degli Interi?”

Un int a 32 bit ha un valore massimo di INT_MAX = 2.147.483.647. Cosa succede aggiungendo 1?

#include <stdio.h>
#include <limits.h>
int main(void) {
int x = INT_MAX;
printf("INT_MAX = %d\n", x);
printf("INT_MAX + 1 = %d\n", x + 1); // overflow!
return 0;
}

Output:

INT_MAX = 2147483647
INT_MAX + 1 = -2147483648

Il risultato è negativo: siamo tornati al valore minimo rappresentabile, come un orologio che dopo le 23:59 torna alle 00:00. Questo fenomeno si chiama overflow. Attenzione: qui il compilatore non emette alcun warning, nemmeno con -Wall -Wextra, perché il valore di x è noto solo durante l’esecuzione. gcc avvisa (-Woverflow, attivo di default) solo quando l’overflow avviene in un’espressione costante calcolata in compilazione, come INT_MAX + 1 scritto direttamente. Per scoprire l’overflow durante l’esecuzione si può compilare con -fsanitize=undefined, che stampa runtime error: signed integer overflow.

Overflow e Valori Speciali nei Numeri in Virgola Mobile

Sezione intitolata “Overflow e Valori Speciali nei Numeri in Virgola Mobile”

I double si comportano diversamente dagli interi: invece di “wrapare”, producono valori speciali definiti dallo standard IEEE 754:

#include <stdio.h>
int main(void) {
float troppo_grande = 10e10f * 10e30f;
printf("%f\n", troppo_grande); // inf
printf("%f\n", 0.0 / 0.0); // nan
printf("%f\n", 1.0 / 0.0); // inf
printf("%f\n", -1.0 / 0.0); // -inf
return 0;
}
  • inf: il risultato è troppo grande per essere rappresentato (overflow floating point).
  • nan (Not A Number): risultato di un’operazione matematicamente indefinita (0.0/0.0, sqrt(-1.0), etc.).
  • -inf: infinito negativo.

La caratteristica più pericolosa di questi valori è che si propagano: qualsiasi operazione che coinvolge nan produce nan. Un nan che entra in un calcolo complesso può silenziosamente corrompere tutti i risultati successivi senza generare errori.

La Regola d’Oro: Compilate Sempre con -Wall -Wextra

Sezione intitolata “La Regola d’Oro: Compilate Sempre con -Wall -Wextra”
Terminal window
gcc -Wall -Wextra -o programma programma.c

Questi flag attivano i principali avvisi del compilatore (ad esempio formati printf errati, variabili inutilizzate o non inizializzate). Non coprono tutto: le conversioni implicite con perdita di dati richiedono -Wconversion e l’overflow su variabili si scopre solo in esecuzione (-fsanitize=undefined). Un programma che compila con warnings non è un programma corretto.


Mettetevi alla prova con queste 10 domande per verificare la comprensione dei concetti della lezione. Cliccate su ogni domanda per scoprire la risposta corretta e la spiegazione.

1. Qual è la differenza fondamentale tra int e unsigned int in termini di rappresentazione in memoria?

  • A) unsigned int occupa il doppio dei byte di int.
  • B) int e unsigned int occupano lo stesso numero di byte; cambia solo l’interpretazione del bit più significativo.
  • C) unsigned int non può essere usato in espressioni aritmetiche.
  • D) int è più preciso di unsigned int nei calcoli in virgola mobile.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Entrambi i tipi occupano tipicamente 4 byte (32 bit). In int, il bit più significativo è il bit del segno (0=positivo, 1=negativo) che dimezza il range positivo. In unsigned int, quel bit contribuisce al valore numerico, portando il massimo da ~2 miliardi a ~4 miliardi.

2. Quale specificatore di formato va usato in printf per stampare correttamente un valore di tipo long int?

  • A) %d
  • B) %ld
  • C) %li
  • D) Sia %ld che %li sono corretti.
▶ Mostra Risposta Corretta

Risposta corretta: D

Spiegazione: Per long int, sia %ld (long decimal) che %li (long integer) sono corretti e producono lo stesso output. La scelta convenzionale è %ld.

3. Cosa produce il codice printf("%.17f\n", 0.1 + 0.2); in C?

  • A) 0.30000000000000000
  • B) Un errore di compilazione perché la precisione richiesta è troppo alta.
  • C) 0.30000000000000004 (o un valore simile diverso da 0.3 esatto).
  • D) 0.3000000 (7 cifre decimali, il massimo per double).
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Per lo standard IEEE 754, né 0.1 né 0.2 si rappresentano esattamente in base 2. La loro somma introduce un errore di arrotondamento che diventa visibile aumentando la precisione di stampa. Questo è il motivo fondamentale per cui non si devono confrontare due double con ==.

4. Cosa stampa il seguente codice?

int a = 5, b = 2;
double r = (double)(a / b);
printf("%.1f\n", r);
  • A) 2.5
  • B) 2.0
  • C) 0.0
  • D) Un errore di compilazione.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Il cast (double) viene applicato dopo la divisione. Poiché sia a che b sono int, la divisione a / b è una divisione intera che produce 2 (troncando il resto). Solo successivamente il valore 2 viene convertito in 2.0. Per ottenere 2.5 bisogna scrivere (double)a / b.

5. Quale delle seguenti affermazioni su const è corretta?

  • A) Una variabile const può essere modificata usando il cast (int).
  • B) Una variabile const può essere dichiarata senza inizializzarla e inizializzata in seguito.
  • C) Il compilatore emette un errore se si tenta di modificare una variabile const.
  • D) const è equivalente a #define e produce lo stesso codice macchina.
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Il compilatore C garantisce l’immutabilità di una variabile const emettendo un errore di compilazione per qualsiasi tentativo di assegnamento successivo. A differenza di #define, const ha un tipo e uno scope ben definiti, ed è soggetto al type checking del compilatore.

6. Il flag -E di GCC serve a:

  • A) Attivare tutti i warning di errore (-E sta per “Errors enabled”).
  • B) Fermare la compilazione dopo la fase di preprocessing e produrre un file .i.
  • C) Ottimizzare il codice generato per la massima efficienza.
  • D) Abilitare le estensioni GNU non standard del linguaggio.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Con gcc -E sorgente.c -o sorgente.i, GCC esegue solo il preprocessore (che risolve #include, #define e le direttive condizionali) e scrive il risultato nel file .i. Questo è utile per capire cosa “vede” effettivamente il compilatore dopo che le macro sono state espanse.

7. Cosa restituisce floor(-2.1) in C?

  • A) -2.0
  • B) -3.0
  • C) 2.0
  • D) -2 (di tipo int).
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: floor restituisce il numero intero più grande che sia minore o uguale all’argomento (in italiano: arrotonda sempre verso il basso, cioè verso −∞-\infty). Per -2.1, il numero intero più grande che è ancora ≤ -2.1 è -3. Nota: tutte le funzioni di arrotondamento di <math.h> restituiscono un double.

8. Quale dei seguenti formati printf stampa il numero 42 occupando esattamente 8 caratteri con zeri a sinistra?

  • A) %8d
  • B) %-8d
  • C) %08d
  • D) %z8d
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: %08d specifica un’ampiezza minima di 8 caratteri con padding tramite zeri (flag 0). Produce 00000042. %8d produce 42 (spazi a sinistra). %-8d produce 42 (allineato a sinistra con spazi a destra).

9. Cosa stampa questo codice?

#include <stdio.h>
#include <limits.h>
int main(void) {
int x = INT_MAX;
printf("%d\n", x + 1);
return 0;
}
  • A) 2147483648
  • B) 0
  • C) -2147483648
  • D) Il programma non compila.
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Aggiungere 1 a INT_MAX causa un overflow: il valore “wrapa” e torna al valore minimo rappresentabile da un int a 32 bit, che è -2147483648. Il compilatore non emette alcun warning (nemmeno con -Wall -Wextra), perché l’overflow avviene su una variabile durante l’esecuzione. Tecnicamente si tratta di comportamento indefinito (undefined behavior), ma in pratica su architetture comuni si osserva questo comportamento.

10. Su Linux con GCC, perché è necessario aggiungere -lm al comando di compilazione quando si usa <math.h>?

  • A) Per abilitare le estensioni GNU non standard.
  • B) Per istruire il linker a includere la libreria matematica (libm) che contiene il codice delle funzioni come sqrt e sin.
  • C) Perché -lm attiva la modalità “long math” che usa numeri a 128 bit.
  • D) Non è necessario: gcc include automaticamente tutte le librerie standard.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: In C, la libreria matematica (libm) è separata dalla libreria standard C (libc). Il flag -lm dice al linker di collegare libm all’eseguibile finale, rendendo disponibili le implementazioni di sqrt, sin, pow, etc. Senza questo flag, il linker non troverà queste funzioni e la compilazione fallirà con errori “undefined reference”. Su macOS e con Clang questo non è necessario perché libm è inclusa automaticamente.


Scrivete un programma che stampi una tabella formattata con tre colonne (TIPO, DIMENSIONE, VALORE MASSIMO) per i tipi int, unsigned int, long e unsigned long, usando <limits.h>. Assicuratevi che le colonne siano allineate correttamente usando i modificatori di ampiezza di printf.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <limits.h>
int main(void) {
printf("%-20s %8s %25s\n", "TIPO", "BYTE", "VALORE MASSIMO");
printf("%-20s %8zu %25d\n", "int", sizeof(int), INT_MAX);
printf("%-20s %8zu %25u\n", "unsigned int", sizeof(unsigned int), UINT_MAX);
printf("%-20s %8zu %25ld\n", "long", sizeof(long), LONG_MAX);
printf("%-20s %8zu %25lu\n", "unsigned long", sizeof(unsigned long), ULONG_MAX);
return 0;
}

Spiegazione: Usiamo %-20s per la colonna del tipo (allineata a sinistra, 20 caratteri), %8zu per la dimensione (8 caratteri, %zu perché sizeof restituisce size_t) e l’ampiezza appropriata per il valore massimo.


Scrivete un programma che legga due interi a e b da tastiera e stampi: il risultato della divisione intera, il risultato della divisione reale (usando il cast), e il resto.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
int a, b;
printf("Inserisci il dividendo: ");
scanf("%d", &a);
printf("Inserisci il divisore: ");
scanf("%d", &b);
int quoziente = a / b;
double reale = (double)a / b;
int resto = a % b;
printf("Divisione intera: %d\n", quoziente);
printf("Divisione reale: %.4f\n", reale);
printf("Resto: %d\n", resto);
return 0;
}

Spiegazione: Il cast (double)a prima della divisione promuove l’operazione a divisione reale. L’operatore % funziona solo tra interi e restituisce il resto della divisione intera.


Scrivete un programma che legga il raggio di un cerchio e stampi area e circonferenza usando la costante M_PI definita in <math.h>. Formattate l’output con 4 cifre decimali.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <math.h>
int main(void) {
double raggio;
printf("Inserisci il raggio: ");
scanf("%lf", &raggio);
double area = M_PI * raggio * raggio;
double circonferenza = 2.0 * M_PI * raggio;
printf("Area: %.4f\n", area);
printf("Circonferenza: %.4f\n", circonferenza);
return 0;
}

Spiegazione: M_PI è una macro predefinita in <math.h> che contiene il valore di π con la massima precisione del tipo double. Non è necessario definire manualmente 3.14159.


Scrivete un programma che legga i due cateti di un triangolo rettangolo e calcoli l’ipotenusa usando sqrt e pow.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <math.h>
int main(void) {
double a, b;
printf("Cateto a: ");
scanf("%lf", &a);
printf("Cateto b: ");
scanf("%lf", &b);
double ipotenusa = sqrt(pow(a, 2) + pow(b, 2));
// oppure, equivalentemente: sqrt(a*a + b*b)
printf("Ipotenusa: %.4f\n", ipotenusa);
return 0;
}

Spiegazione: Il teorema di Pitagora si implementa direttamente con sqrt e pow. Notare che a*a è preferibile a pow(a, 2) per prestazioni (meno overhead), ma entrambe le versioni sono corrette.


Scrivete un programma che legga un angolo in gradi e stampi il suo seno, coseno e tangente, dopo averlo convertito in radianti (usando M_PI). Formattate l’output a 6 cifre decimali.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <math.h>
int main(void) {
double gradi;
printf("Inserisci un angolo in gradi: ");
scanf("%lf", &gradi);
double radianti = gradi * M_PI / 180.0;
printf("Angolo in radianti: %.6f\n", radianti);
printf("Seno: %.6f\n", sin(radianti));
printf("Coseno: %.6f\n", cos(radianti));
printf("Tangente: %.6f\n", tan(radianti));
return 0;
}

Spiegazione: La formula di conversione è rad=gradi×π/180\text{rad} = \text{gradi} \times \pi / 180. Le funzioni trigonometriche del C lavorano sempre in radianti.


Scrivete un programma che legga un numero reale e stampi in una tabella i risultati di (int)x (troncamento), round(x), floor(x) e ceil(x).

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <math.h>
int main(void) {
double x;
printf("Inserisci un numero reale: ");
scanf("%lf", &x);
printf("\n%-20s %10.4f\n", "Valore originale:", x);
printf("%-20s %10d\n", "Troncamento (int):", (int)x);
printf("%-20s %10.4f\n", "round(x):", round(x));
printf("%-20s %10.4f\n", "floor(x):", floor(x));
printf("%-20s %10.4f\n", "ceil(x):", ceil(x));
return 0;
}

Spiegazione: Confrontare i quattro metodi sullo stesso valore (es. -2.7) evidenzia chiaramente la differenza tra troncamento, arrotondamento tradizionale, arrotondamento verso il basso e verso l’alto.


Scrivete un programma che dimostri l’overflow degli interi: partite da INT_MAX, sommategli 1 e stampate il risultato. Poi risolvete il problema promuovendo l’operazione a long.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <limits.h>
int main(void) {
int x = INT_MAX;
printf("INT_MAX = %d\n", x);
printf("INT_MAX + 1 = %d\n", x + 1); // overflow: risultato negativo!
printf("(long)INT_MAX+1 = %ld\n", (long)x + 1); // corretto: promozione a long
return 0;
}

Spiegazione: x + 1 produce un overflow perché l’addizione avviene tra due int (il letterale 1 è un int). (long)x + 1 promuove prima x a long, poi la somma avviene in aritmetica a 64 bit e il risultato è corretto.


Esercizio 8: Cedolino Stipendio con const e Padding

Sezione intitolata “Esercizio 8: Cedolino Stipendio con const e Padding”

Scrivete un programma che legga il reddito lordo mensile da tastiera e stampi un cedolino formattato che mostri: reddito lordo, imponibile IRPEF (23% della parte fino a 15.000€/anno), contributi INPS (9.19%), e netto mensile. Definite tutte le aliquote come costanti const.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <math.h>
int main(void) {
const double ALIQUOTA_IRPEF = 0.23;
const double ALIQUOTA_INPS = 0.0919;
double lordo_mensile;
printf("Reddito lordo mensile (€): ");
scanf("%lf", &lordo_mensile);
double inps = lordo_mensile * ALIQUOTA_INPS;
double irpef = lordo_mensile * ALIQUOTA_IRPEF;
double netto = lordo_mensile - inps - irpef;
printf("\n--- CEDOLINO ---\n");
printf("%-25s %10.2f €\n", "Lordo mensile:", lordo_mensile);
printf("%-25s %10.2f €\n", "Contributi INPS:", inps);
printf("%-25s %10.2f €\n", "Ritenuta IRPEF:", irpef);
printf("%-25s %10.2f €\n", "Netto mensile:", netto);
return 0;
}

Spiegazione: Le aliquote definite con const rendono il codice auto-documentante e sicuro. Il padding con %-25s e %10.2f produce un output tabellare allineato e professionale.


Esercizio 9: Verifica Teorema di Pitagora con Tolleranza

Sezione intitolata “Esercizio 9: Verifica Teorema di Pitagora con Tolleranza”

Scrivete un programma che legga tre numeri reali e verifichi se possono essere i lati di un triangolo rettangolo, usando fabs e una tolleranza di 1e-6 per confrontare i double (senza usare if/else).

💻 Mostra Soluzione e Codice C
#include <stdio.h>
#include <math.h>
int main(void) {
double a, b, c;
printf("Inserisci i tre lati: ");
scanf("%lf %lf %lf", &a, &b, &c);
// c deve essere il lato più lungo (ipotenusa candidata)
double ipotenusa_sq = c * c;
double cateti_sq = a * a + b * b;
double differenza = fabs(ipotenusa_sq - cateti_sq);
int e_rettangolo = (differenza < 1e-6);
printf("È un triangolo rettangolo (con c come ipotenusa): %d\n", e_rettangolo);
return 0;
}

Spiegazione: Non si usa == per confrontare i double (errori di arrotondamento IEEE 754). Si calcola la differenza assoluta con fabs e si verifica che sia sotto la soglia di tolleranza 1e-6. Il risultato dell’espressione (differenza < 1e-6) è 1 (Vero) o 0 (Falso), utilizzabile direttamente senza if.


Scrivete un programma che stampi il numero di Avogadro (6.022×10236.022 \times 10^{23}) e la costante di Planck (6.626×10−346.626 \times 10^{-34}) in tre formati diversi: %f, %e, e %g. Osservate le differenze nell’output.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
double avogadro = 6.022e23;
double planck = 6.626e-34;
printf("Numero di Avogadro:\n");
printf(" %%f: %f\n", avogadro);
printf(" %%e: %e\n", avogadro);
printf(" %%g: %g\n\n", avogadro);
printf("Costante di Planck:\n");
printf(" %%f: %f\n", planck);
printf(" %%e: %e\n", planck);
printf(" %%g: %g\n", planck);
return 0;
}

Spiegazione: %f tenta di stampare tutti i decimali (inutile per numeri molto grandi o piccoli), %e forza sempre la notazione scientifica, %g sceglie automaticamente il formato più compatto e leggibile. Per costanti fisiche, %e o %g sono quasi sempre la scelta migliore.