Skip to the content.

Lezione 3 - Variabili, Ambito, Passaggio per Valore e Tipi di Dato nel C

Video di riferimento: Lezione 3 - Variabili Globali


1. Ambito delle Variabili (Scope)

1.1 Variabili Locali

Le variabili locali sono dichiarate all’interno di una funzione e:

Esempio:

void incr(void) {
    int x = 1;
    x = x + 1;
    printf("%d\n", x);
}

int main(void) {
    incr();  // stampa 2
    incr();  // stampa 2
    incr();  // stampa 2
    incr();  // stampa 2
    return 0;
}

Output:

2
2
2
2

La variabile locale x viene creata e distrutta ogni volta che incr() viene chiamata, quindi il valore è sempre 2.


1.2 Variabili Globali

Le variabili globali sono dichiarate fuori da qualsiasi funzione e:

Esempio:

int x = 0;  // variabile globale

void incr(void) {
    x = x + 1;
    printf("%d\n", x);
}

int main(void) {
    incr();  // stampa 1
    incr();  // stampa 2
    incr();  // stampa 3
    incr();  // stampa 4
    return 0;
}

Output:

1
2
3
4

Quando incr viene chiamata, il compilatore non trova la variabile x a livello locale e la cerca nell’ambito globale. Il suo stato persiste per tutta la durata del programma.


1.3 Variabili Statiche (static)

Le variabili statiche locali combinano le caratteristiche di entrambe:

Esempio:

void incr(void) {
    static int x = 0;  // inizializzata solo la prima volta
    x = x + 1;
    printf("%d\n", x);
}

int main(void) {
    incr();  // stampa 1
    incr();  // stampa 2
    incr();  // stampa 3
    incr();  // stampa 4
    
    // printf("%d\n", x);  // ERRORE: x non è visibile qui
    return 0;
}

Output:

1
2
3
4

Se provi ad accedere a x nel main, otterrai un errore di compilazione: undeclared identifier.

Variabili statiche globali

Le variabili statiche possono anche essere dichiarate nell’ambito globale:

static int x = 0;  // statica globale

In questo caso:

Nota sulla sicurezza nei thread

Le variabili statiche locali non sono thread-safe. Se più thread accedono contemporaneamente alla stessa variabile statica, possono verificarsi problemi di sincronizzazione (race condition).

Per proteggerle in ambienti multi-threading è necessario utilizzare meccanismi di sincronizzazione come i mutex (mutual exclusion).


2. Passaggio per Valore vs Riferimento

2.1 Passaggio per Valore (Pass by Value)

In C, i parametri delle funzioni vengono passati per valore di default. Questo significa che:

Esempio che dimostra il problema:

int incr(int x) {
    x = x + 1;
    return x;
}

int main(void) {
    int a = 10;
    incr(a);  // a NON viene modificato
    printf("%d\n", a);  // stampa 10
    return 0;
}

Output:

10

Il valore di a rimane 10 perché incr lavora su una copia del valore di a.

Per modificare effettivamente a devi riassegnare il risultato:

int main(void) {
    int a = 10;
    a = incr(a);  // riassegno il risultato
    printf("%d\n", a);  // stampa 11
    return 0;
}

2.2 Passaggio per Riferimento (con Puntatori)

Per passare per riferimento in C è necessario utilizzare i puntatori. Questo verrà approfondito nelle lezioni successive.


3. Tipi di Dato

3.1 Tipi Primitivi Base

In C esistono diversi tipi di dato primitivi:

Tipo Dimensione Descrizione
char 8 bit (1 byte) Carattere o intero piccolo (-128 a 127)
unsigned char 8 bit (1 byte) Intero senza segno (0 a 255)
short 16 bit (2 byte) Intero corto
int Dipende dall’architettura Intero (tipicamente 32 bit)
unsigned int Dipende dall’architettura Intero senza segno
long Dipende dall’architettura Intero lungo
float 32 bit Numero in virgola mobile (precisione singola)
double 64 bit Numero in virgola mobile (precisione doppia)

unsigned: interi senza segno

Gli interi possono essere dichiarati unsigned per rappresentare solo valori positivi:

unsigned int x = 100;

Proprietà importanti:


3.2 Uso di printf con i tipi

Esempio base:

int main(void) {
    int a = 10;
    float b = 1.234;
    printf("%d %f\n", a, b);  // output: 10 1.234000
    return 0;
}

Specifier comuni per printf:

Cosa succede se usi lo specifier sbagliato?

int main(void) {
    int a = 10;
    float b = 1.234;
    
    printf("%d %f\n", b, a);  // TIPI INVERTITI!
    return 0;
}

Output:

warning: format specifies type 'int' but the argument has type 'float'
warning: format specifies type 'double' but the argument has type 'int'

-1073741824 0.0000000

Cosa è successo?


3.3 Conversione Implicita e Promozione dei Tipi

Perché il warning parla di double se abbiamo usato float?

Le funzioni variadiche (come printf, che accettano un numero variabile di argomenti) non conoscono i tipi a compile-time. Per questo motivo, il C applica delle regole di promozione automatiche:

Regole di promozione per funzioni variadiche:

  1. Interi più piccoli di int → promossi a int
    short s = 400;
    printf("%d\n", s);  // s viene promosso a int
    
  2. float → promosso a double
    float f = 1.5;
    printf("%f\n", f);  // f viene promosso a double
    

Questo spiega perché il warning menziona double anche quando passi un float.

Conversione nelle espressioni

Nelle operazioni aritmetiche, i tipi vengono promossi secondo regole specifiche:

char c = 127;
int i = c + 1;  // c viene promosso a int prima dell'operazione
printf("%d\n", i);  // stampa 128

Regole generali:


3.4 Overflow e Wrapping

Comportamento con unsigned (garantito)

Con gli interi unsigned, il wrapping dopo overflow è garantito dallo standard:

unsigned char c = 254;
c++;  // 255
c++;  // 0 (wrapping)
printf("%d\n", c);  // stampa 0

L’aritmetica modulo 2^n garantisce che:

Comportamento con signed (undefined behavior)

Con gli interi con segno, l’overflow è undefined behavior:

char c = 127;  // valore massimo per char signed
c++;  // UNDEFINED BEHAVIOR!

Cosa può succedere:

Il compilatore potrebbe darti un warning:

warning: implicit conversion from 'int' to 'char' changes value from 128 to -128

Esempio di comportamento non standardizzato

char c = 128;  // ATTENZIONE!
printf("%d\n", c);  // potrebbe stampare -128

Questo accade perché:

Ma questo non è garantito dallo standard!

L’operazione modulo (simbolo %) ti dà il resto di una divisione:

10 % 3 = 1    (10 diviso 3 fa 3 con resto 1)
15 % 4 = 3    (15 diviso 4 fa 3 con resto 3)
7 % 7 = 0     (7 diviso 7 fa 1 con resto 0)

Aritmetica modulo N significa: qualsiasi operazione, dopo averla fatta, dividi per N e prendi il resto.

Perché 2^n?

n = numero di bit disponibili

Per un unsigned char (8 bit):

Quindi lavori in modulo 256 (modulo 2^8).


Come Funziona il Wrapping

Ogni operazione viene automaticamente “ridotta” modulo 2^n:

unsigned char c = 255;  // valore massimo
c = c + 1;              // 256 % 256 = 0
// c vale ora 0

Altro esempio:

unsigned char c = 254;
c = c + 5;  // 259

// 259 % 256 = 3  <--- Modulo 256 (2^8)
// c vale ora 3

E all’indietro:

unsigned char c = 0;
c = c - 1;  // -1

// -1 % 256 = 255
// c vale ora 255

Perché “Non Va in Overflow”?

Tecnicamente NON è overflow - è wrapping garantito.

Overflow = comportamento indefinito, errore
Wrapping = comportamento definito dallo standard

Con unsigned:

Con signed invece:


Esempio Concreto: Orologio

Un orologio a 12 ore è “modulo 12”:

10:00 + 3 ore = 13:00 → 1:00 (13 % 12 = 1)
11:00 + 2 ore = 13:00 → 1:00

Un unsigned char è un “orologio a 256 ore”:

250 + 10 = 260 → 4 (260 % 256 = 4)

In Sintesi

2^n è il numero totale di valori rappresentabili con n bit.

L’aritmetica modulo 2^n garantisce che:

Quindi quando dici “non va in overflow”, intendi: il wrapping è il comportamento corretto e garantito, non un errore.


3.5 Rappresentazione dei Numeri in Virgola Mobile

Formato IEEE 754

I numeri in virgola mobile in C seguono lo standard IEEE 754:

Struttura di un numero in virgola mobile

Un numero in virgola mobile è composto da tre parti:

  1. Segno (S): 1 bit
  2. Esponente (E): 8 bit (float) o 11 bit (double)
  3. Mantissa/Frazione (M): 23 bit (float) o 52 bit (double) IEEE 754

Formula generale:

valore = (-1)^S × M × 2^E

Dalla notazione scientifica al formato IEEE

Notazione scientifica:

123.45 = 1.2345 × 10^2

Notazione binaria normalizzata:

5.25 (decimale) = 101.01 (binario) = 1.0101 × 2^2

Dove:

Esempio pratico

float f = 1.5;
printf("%f\n", f);   // stampa 1.500000
printf("%a\n", f);   // stampa 0x1.8p+0 (notazione esadecimale)

La notazione %a mostra la rappresentazione esadecimale:

Limitazioni dei float

Precisione limitata:

float f = 0.1 + 0.2;
printf("%.20f\n", f);  // NON è esattamente 0.3!
// output: 0.30000001192092895508

Questo accade perché molti numeri decimali non hanno rappresentazione esatta in binario.


4. Approfondimenti

Spazio per ulteriori approfondimenti


Note Finali

Punti chiave da ricordare:

  1. Scope delle variabili:
    • Locali → esistono solo nella funzione
    • Globali → esistono per tutto il programma
    • Statiche → visibilità locale, lifetime globale
  2. Passaggio parametri:
    • Il C passa sempre per valore
    • Per modificare l’originale serve usare i puntatori
  3. Tipi di dato:
    • Usa unsigned quando vuoi solo valori positivi e wrapping garantito
    • Gli overflow su signed sono undefined behavior
    • float ha precisione limitata, usa double quando serve maggiore precisione
  4. Printf:
    • Usa sempre lo specifier corretto (%d, %f, ecc.)
    • Le funzioni variadiche promuovono automaticamente i tipi
    • Il compilatore ti avvisa ma compila comunque → sei tu responsabile!