Note Ingegneria Informatica · UniTN
Programmazione 1
Lezione
11 set
in corso

Il Linguaggio C

Indice 5 sezioni
  1. 1Concetti
  2. 1.1Linguaggio di programmazione
  3. 1.2Livelli di astrazione
  4. 1.3Il linguaggio C
  5. 1.4Compilazione e interpretazione
  6. 1.5Dal sorgente all’eseguibile
  7. 1.6Il primo programma
  8. 1.7Standard I/O e memoria
  9. 1.8Lessico
  10. 1.9Struttura di un programma
  11. 1.10Istruzioni di input/output
  12. 1.11Variabili
  13. 1.12Dichiarazione
  14. 1.13Assegnazione
  15. 2Metodo
  16. 3Esercizi tipo esame
  17. 4Errori tipici
  18. 5Domande

Lezione 03 di Programmazione 1, 11 settembre 2026. Slide: 2.2 Introduzione al C, slide 1-56 e 64-65. Prima: Architettura hardware e software di un calcolatore. Dopo: Espressioni, operatori e costanti, che copre il resto del deck (slide 57-76: espressioni, precedenze, ++ e --, costanti, caratteri e ASCII).

Il filo della lezione:

linguaggio di programmazione    sintassi (forma) + semantica (significato)
          |
          v
livelli di astrazione           linguaggio macchina -> assembly -> C
          |
          v
compilare o interpretare        il C si compila
          |
          v
macchina astratta C             memoria a celle, standard input e output
          |
          v
dal sorgente all'eseguibile     editor -> preprocessore -> compilatore -> linker -> loader
          |
          v
lessico                         case sensitive, spazi, commenti, identificatori, keyword
          |
          v
struttura di un programma       main { istruzioni; }
          |
          v
variabili                       nome, tipo, l-value (dove), r-value (cosa)
          |
          v
assegnazione                    valuta a destra, scrivi a sinistra

1Concetti

1.1Linguaggio di programmazione

Un algoritmo scritto in linguaggio naturale va tradotto in un linguaggio che l’esecutore, il computer, capisce (slide 2-3). La sequenza di istruzioni in quel linguaggio è il programma. Per non lasciare ambiguità il linguaggio deve essere preciso e rigoroso su due piani.

Le due cose sono indipendenti:

La grammatica completa del C sta nell’appendice E del libro di testo (slide 39).

1.2Livelli di astrazione

Fra il programmatore, che conosce il problema, e la macchina, che capisce solo circuiti a due valori (0 e 1), c’è il linguaggio di programmazione. Il suo livello di astrazione è una scelta (slide 4-6): troppo vicino alla macchina e programmare è difficile, troppo vicino al programmatore e i programmi diventano inefficienti.

La stessa istruzione, “il totale è la paga più gli straordinari”, a tre livelli:

C                  TOT = PAGA + STRAORD;          alto
                                                    |
assembly           LOAD  PAGA                       |
                   ADD   STRAORD                    |
                   STORE TOT                        |
                                                    v
linguaggio         0100001111                     basso
macchina           1100111001
                   0110001111

In assembly si vede il lavoro della CPU visto in Architettura hardware e software di un calcolatore: carica un valore, somma, salva in una cella. In C basta una riga.

Cosa chiediamo a un linguaggio (slide 9):

Macchina fisica (slide 8). Una macchina fisica esegue istruzioni nel suo linguaggio, il codice macchina. A una macchina corrisponde un linguaggio (il suo), ma un linguaggio in generale può essere eseguito da più macchine. Il cuore della macchina fisica è il ciclo fetch-decode-execute, che fa da interprete del codice macchina.

1.3Il linguaggio C

(slide 14-17)

LivelloLinguaggi
altoAda, Pascal, BASIC
medioC, FORTH
bassoassembly

Il C++ (Stroustrup, Bell Labs) è un’evoluzione del C: aggiunge astrazione sui dati, programmazione a oggetti e la Standard Template Library.

1.4Compilazione e interpretazione

Un programma scritto in un linguaggio LL si può eseguire in due modi (slide 19):

CompilazioneInterpretazione
cosa fatraduce tutto il programma in un altro linguaggio (per esempio codice macchina), senza eseguirloesegue direttamente il codice sorgente, un’istruzione alla volta
esecuzioneveloce: la decodifica è già stata fattapiù lenta: ogni istruzione va decodificata mentre gira
costola traduzione può richiedere temponessuna traduzione a parte
proefficienza, ottimizzazioniflessibilità, portabilità, interattività (debug a run-time)
contropoca flessibilità, si perde informazione sulla struttura del sorgentescarsa efficienza

Il C è un linguaggio compilato. Python, per confronto, è interpretato.

Macchina astratta (slide 21-23). Dato un linguaggio LL, la macchina astratta MLM_L è un qualsiasi insieme di strutture dati e algoritmi capace di memorizzare ed eseguire programmi scritti in LL. È un’astrazione del calcolatore e delle sue risorse: memoria (dati e programma), controllo della sequenza, controllo dei dati, gestione della memoria, operazioni. Il componente essenziale è l’interprete:

        start
          |
          v
  +-> acquisisci la prossima istruzione
  |       |
  |       v
  |   decodifica
  |       |
  |       v
  |   acquisisci gli operandi
  |       |
  |       v
  |   seleziona l'operazione ---------------------+
  |       |          |            |               |
  |       v          v            v               v
  |   esegui OP1  esegui OP2 ... esegui OPn    esegui HALT
  |       |          |            |               |
  |       +----------+------------+               v
  |       |                                     stop
  |       v
  +-- memorizza il risultato

È lo stesso ciclo fetch-decode-execute della CPU, scritto per un linguaggio qualsiasi.

La macchina astratta C (slide 26). Il programma C non gira direttamente: il compilatore lo traduce nel linguaggio della macchina ospite MoM_o, che poi lo esegue interpretandolo con il suo ciclo fetch-decode-execute.

programma C  --compilazione-->  binario per Mo  --interpretazione (CPU)-->  esecuzione

1.5Dal sorgente all’eseguibile

(slide 27-29)

FaseChiCosa succede
Editeditorscrivi il programma e lo salvi su disco (ciao.c)
Preprocesspreprocessoreelabora le direttive che iniziano con #: per esempio #include <stdio.h> viene sostituito dal contenuto del file. Il risultato è un sorgente “espanso”
Compilecompilatoretraduce il sorgente in codice oggetto (ciao.o) e lo salva su disco. Qui escono gli errori di sintassi
Linklinkercollega il tuo codice oggetto con altro codice già disponibile, le librerie (per esempio quella che contiene printf), e produce l’eseguibile
Loadloadercarica l’eseguibile dal disco nella memoria centrale
ExecuteCPUprende un’istruzione alla volta e la esegue, eventualmente scrivendo nuovi valori in memoria

Su Linux con gcc un solo comando fa preprocessore, compilazione (con l’assemblatore, che le slide non nominano a parte) e link. L’editing lo fai tu prima, il caricamento e l’esecuzione partono quando lanci il programma:

gcc -std=c11 -Wall -Wextra -Wpedantic ciao.c -o ciao
./ciao

-Wall -Wextra -Wpedantic accendono gli avvisi (warning): il compilatore segnala anche cose che si compilano ma sono probabilmente sbagliate.

1.6Il primo programma

Le slide propongono di provarlo su onlinegdb.com scegliendo il C (slide 10):

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello World!\n");
}
PezzoSignificato
#include <stdio.h>direttiva per il preprocessore: include la libreria di input/output standard, dove sono dichiarate printf e scanf
int main(int argc, char *argv[])intestazione: main è il punto da cui parte l’esecuzione. argc e argv sono gli argomenti passati da riga di comando
{ ... }il corpo: la sequenza di istruzioni
printf("Hello World!\n");stampa la stringa sullo standard output. \n è il carattere “a capo”
;chiude ogni istruzione

Compilato con -Wextra, gcc avvisa che argc e argv non sono usati. Quando non servono si può scrivere l’intestazione senza parametri. return 0; dice al sistema operativo che il programma è terminato bene. Dal C99 arrivare alla } finale di main senza return equivale a return 0;, quindi il programma della slide è corretto; scriverlo esplicitamente resta buona pratica, ed è quello che fanno i programmi del prof nel deck 3.2:

#include <stdio.h>

int main(void)
{
    printf("Hello World!\n");
    return 0;
}

Esercizio dalle slide (slide 28): togli pezzi al programma, per esempio il ; o il \n nella riga di printf, e leggi i messaggi di errore o il cambiamento nell’output.

1.7Standard I/O e memoria

La macchina astratta C ha un’unità centrale che esegue il programma, una memoria centrale e due periferiche standard, tutte sul bus (slide 30-33).

                        bus di sistema
  ============================================================
       |                 |                |              |
  +---------+       +---------+     +----------+   +----------+
  |  unità  |   x ->|  cella  |     | [][][][] |   | [][][][] |
  | centrale|   a ->|  cella  |     | standard |   | standard |
  +---------+ alfa->|  cella  |     |  input   |   |  output  |
                    |   ...   |     | tastiera |   |  video   |
                    +---------+     +----------+   +----------+
                  memoria centrale

Standard input e standard output. Il C astrae le periferiche. Ogni programma ha due periferiche standard:

Si possono pensare come sequenze di celle, ognuna con un dato, da cui leggere (input) o in cui scrivere (output).

Memoria. La memoria della macchina astratta è divisa in celle elementari, le variabili. Ogni cella contiene un dato usato dal programma. Si guarda solo la memoria dati: dove sta il codice del programma per ora non interessa.

I dati possono essere:

Come viene gestita la memoria per ora non conta. Conta come ci si riferisce a queste celle: con gli identificatori.

1.8Lessico

Case sensitive. Il C distingue maiuscole e minuscole (slide 34-35): Var1, var1 e VAR1 sono tre nomi diversi.

Spazi. Spazio, tab e a capo (newline) vengono ignorati, tranne dentro una stringa. Servono solo a rendere il codice leggibile. Queste due righe sono lo stesso programma per il compilatore:

x=3;myVar=5;
x = 3;
myVar = 5;

Ma "ciao mondo" e "ciaomondo" sono stringhe diverse.

Tabella ASCII. I caratteri sono memorizzati come numeri secondo la codifica ASCII: ogni carattere ha un codice. Il primo carattere della tabella, codice 0, è NUL, un carattere speciale che non si stampa.

Commenti. Testo ignorato dal compilatore, per chi legge il codice.

Identificatori (slide 37, slide 48). Sono i nomi che il programmatore dà a variabili, funzioni e altri oggetti. Regole:

  1. composti da lettere, cifre e underscore _;
  2. il primo carattere è una lettera o _, mai una cifra;
  3. diversi da tutte le parole chiave.
ValidiNon validi
a, x, alfa, a1, xy231a (inizia con una cifra)
MyFriend, DopoDomania b (contiene uno spazio)
velocita_massima, _velocitaint (parola chiave)
dopo-domani (il - è un operatore)

Parole chiave (keyword). Parole riservate del linguaggio, con un significato fissato a priori: non si possono usare come identificatori anche se lessicalmente andrebbero bene. L’ANSI C del 1989 ne ha 32 (slide 38):

auto      break     case      char      const     continue  default   do
double    else      enum      extern    float     for       goto      if
int       long      register  return    short     signed    sizeof    static
struct    switch    typedef   union     unsigned  void      volatile  while

Il numero 32 vale per il C89. Gli standard successivi ne hanno aggiunte: il C99 inline, restrict, _Bool, _Complex, _Imaginary; il C11 _Alignas, _Alignof, _Atomic, _Generic, _Noreturn, _Static_assert, _Thread_local; il C23 fra le altre bool, true, false, nullptr. Alcuni compilatori aggiungono estensioni proprie. Se usi una keyword come nome, l’editor o il compilatore segnalano errore.

1.9Struttura di un programma

(slide 40)

#include <stdio.h>                      direttive per il preprocessore
                                        
int main(int argc, char *argv[])        intestazione
{                                       
    istruzione;                         sequenza di istruzioni
    istruzione;                         fra { e }, ognuna chiusa da ;
    ...
}

Un programma C è composto da:

Le istruzioni sono le frasi del linguaggio, e ognuna termina con ;.

1.10Istruzioni di input/output

Senza input e output un programma sarebbe inutile per l’utente: non potrebbe ricevere dati né mostrare risultati (slide 41-42).

IstruzioneDirezioneForma nelle slide
scanflegge dallo standard input e mette il dato in una variabilescanf(<variabile>)
printfscrive sullo standard output il valore di un’espressioneprintf(<espressione>)

Entrambe stanno nella libreria stdio.h, che va inclusa con #include <stdio.h>.

La forma delle slide (scanf(&x);, printf((a-z)/10);) è semplificata per mostrare l’idea. In C vero serve anche una stringa di formato che dice il tipo del dato: %d per un intero.

#include <stdio.h>

int main(void)
{
    int a;
    int z;

    if (scanf("%d", &a) != 1 || scanf("%d", &z) != 1) {
        printf("servono due numeri interi\n");
        return 1;
    }
    printf("%d\n", (a - z) / 10);
    return 0;
}

Con input 57 e 7 stampa 5: (57−7)/10=5(57 - 7) / 10 = 5.

Il valore di ritorno di scanf. scanf restituisce quanti valori è riuscita a leggere e assegnare: 1 se ha letto il numero, 0 se l’input non è un numero (per esempio ciao), EOF se l’input è finito. Se non lo controlli e l’input è sbagliato, la variabile resta com’era, cioè senza valore, e il programma va avanti con quella. Con input ciao il programma sopra stampa il messaggio d’errore invece di un numero a caso. Il || valuta la seconda scanf solo se la prima è andata bene (Espressioni, operatori e costanti, lazy evaluation).

Il & davanti alla variabile in scanf vuol dire “l’indirizzo di”. scanf deve sapere dove scrivere il dato letto, non quanto vale la variabile adesso. È il concetto di l-value qui sotto. printf invece ha bisogno solo del valore, quindi niente &.

1.11Variabili

Variabile in C e in matematica (slide 43-46). In matematica una variabile è un simbolo per un valore, e x = x + 1 sarebbe una contraddizione. In un linguaggio di programmazione una variabile è associata a una cella di memoria: un contenitore il cui valore può cambiare durante l’esecuzione. x = x + 1 vuol dire “prendi quello che c’è in xx, aggiungi 1, rimettilo in xx”.

Le variabili sono denotate da identificatori, che servono a distinguerle. Il nome individua l’indirizzo della cella. Per evitare ambiguità è vietato usare:

    nome        l-value             r-value
     id    ->  [ cella @1008 ]  ->    128

I nomi vengono dal lato dell’assegnazione in cui si usano: a sinistra di = serve un posto dove scrivere (l-value), a destra serve un valore da leggere (r-value). In x = x + 1; la stessa variabile compare due volte con due ruoli: la xx a destra fornisce il suo valore, la xx a sinistra fornisce la sua cella.

1.12Dichiarazione

Prima di usare una variabile la si dichiara (slide 49-50). Quando il compilatore incontra la dichiarazione, predispone un’area di memoria grande abbastanza per il tipo scelto.

tipo identificatore;
tipo identificatore = espressione;

Nella notazione delle slide la seconda forma si scrive tipo identificatore [= espressione];: le parentesi quadre indicano una parte facoltativa, non vanno scritte nel codice.

int x;
int y = 3 * 2;

La prima crea una variabile intera senza darle un valore. Finché non le assegni qualcosa il suo valore è indeterminato, e leggerlo è comportamento indefinito: in pratica di solito trovi quello che era rimasto in memoria, ma lo standard non garantisce niente, nemmeno un numero “casuale” ma stabile. Questo vale per le variabili locali, dichiarate dentro main o dentro un blocco. Le variabili globali, dichiarate fuori da ogni funzione, partono invece da 0. La seconda riga crea la variabile già inizializzata a 66.

1.13Assegnazione

L’espressione a destra può contenere (slide 51-52):

#include <stdio.h>

int main(void)
{
    int lunghezza;
    int altezza;
    int peso;
    int x = 3 * 2;
    char alt;
    int a;
    int b;
    int c;
    int d;

    lunghezza = 10;
    alt = '2';
    altezza = lunghezza;
    peso = 10 * altezza * (lunghezza + 1);
    x = x + 1;
    a = b = c = d = 5;

    printf("peso = %d, x = %d, alt = %c\n", peso, x, alt);
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
    return 0;
}

Output:

peso = 1100, x = 7, alt = 2
a = 5, b = 5, c = 5, d = 5

La slide dichiara int a, b, c, d; in una riga sola: è C valido, ma qui si tiene una dichiarazione per riga, che si legge meglio e non inganna con i puntatori (int* p, q; dichiara un puntatore e un intero).

altezza = lunghezza; copia il valore: se dopo cambi lunghezza, altezza resta 1010. '2' è il carattere 2, non il numero 2 (in ASCII vale 50).

Esecuzione (slide 56). Due passi, sempre in quest’ordine:

  1. si valuta l’espressione a destra di =, ottenendo un r-value;
  2. si memorizza quel risultato nella cella (l-value) indicata a sinistra.
x = x + 1;        con x che vale 6

passo 1:  x + 1   ->  leggo x (r-value 6), sommo 1   ->  7
passo 2:  x =     ->  scrivo 7 nella cella di x (l-value)

Proprio perché la destra si valuta prima, x = x + 1 ha senso: quando si scrive, il vecchio valore è già stato letto.

Sintassi generale (slide 53-54 e slide 64). La forma “variabile = valore” non copre tutti i casi. La sintassi vera dell’operatore di assegnazione semplice è:

exp1 = exp2

Per questo funziona l’assegnazione a catena:

a = b = c = d = 5;
equivale a
a = (b = (c = (d = 5)));

Si parte da destra: d = 5 scrive 5 in d e vale 5, quel 5 viene assegnato a c, e così via fino ad a.

Esercizio della slide 65 (slide 64-65, aggiunte nella versione del deck di fine settembre). La slide 64 riprende la catena con cinque variabili, e = d = c = b = a = 1;, che equivale a (e = (d = (c = (b = (a = 1)))));. La 65 la fa provare su onlinegdb: stampa le variabili prima (BEFORE) e dopo (AFTER) la catena. Nel programma della slide a, d ed e sono dichiarate senza valore e stampate nel BEFORE: è proprio il caso del valore indeterminato visto sopra, e gcc con -Wall avvisa ('a' is used uninitialized). Qui sono inizializzate a 0 e la stampa è su una riga:

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 2;
    int c = 3 * 2;
    int d = 0;
    int e = 0;

    printf("BEFORE\n");
    printf("a=%d b=%d c=%d d=%d e=%d\n", a, b, c, d, e);

    e = d = c = b = a = 1;

    printf("AFTER\n");
    printf("a=%d b=%d c=%d d=%d e=%d\n", a, b, c, d, e);
    return 0;
}

Output (verificato):

BEFORE
a=0 b=2 c=6 d=0 e=0
AFTER
a=1 b=1 c=1 d=1 e=1

La catena sovrascrive anche b e c, che avevano già un valore: dopo la riga valgono tutte 1.

2Metodo

Leggere un’assegnazione.

  1. Guarda a sinistra di =: deve essere una cosa con una cella (l-value). Se è un numero o un’operazione, è un errore.
  2. Calcola il valore dell’espressione a destra usando i valori attuali delle variabili.
  3. Scrivi il risultato nella cella a sinistra. Solo adesso la variabile cambia.
  4. Con più = in fila, parti da quello più a destra.

Tracciare un programma a mano. Fai una tabella con una colonna per variabile e una riga per istruzione, e aggiorna solo la colonna della variabile a sinistra di =. Le variabili dichiarate e non ancora assegnate valgono ”?“.

istruzionelunghezzaaltezzapeso
lunghezza = 10;10??
altezza = lunghezza;1010?
peso = 10 * altezza * (lunghezza + 1);10101100

3Esercizi tipo esame

Esercizio 1. Quali di questi identificatori sono validi in C? Per quelli non validi, di’ perché.

_x1    2pi    int    Int    somma totale    somma_totale    sizeof    a-b    MAX_VAL
Soluzione

Validi: _x1, Int (diverso da int, il C è case sensitive), somma_totale, MAX_VAL. Non validi: 2pi (inizia con una cifra), int e sizeof (parole chiave), somma totale (contiene uno spazio: sono due identificatori), a-b (il - è un operatore: il compilatore legge a meno b).

Esercizio 2. Scrivi l’output esatto.

#include <stdio.h>

int main(void)
{
    int a = 3;
    int b = 4;
    int c;

    c = a;
    a = b;
    b = c;
    printf("%d %d %d\n", a, b, c);
    a = b = c = 2;
    a = a + b * c;
    printf("%d %d %d\n", a, b, c);
    return 0;
}
Soluzione
istruzioneabc
iniziale34?
c = a;343
a = b;443
b = c;433
a = b = c = 2;222
a = a + b * c;622

Output: 4 3 3 e poi 6 2 2 (verificato). Le prime tre assegnazioni scambiano a e b usando c come appoggio: senza, a = b; b = a; perderebbe il valore di a.

Esercizio 3. Quali di queste istruzioni sono sbagliate, e che tipo di errore è (sintassi o semantica)?

1)  x = 5
2)  5 = x;
3)  x + 1 = y;
4)  media = somma / 0;
5)  Printf("ciao\n");
Soluzione
  1. Sintassi: manca il ;.
  2. e 3. Il lato sinistro di = deve avere un l-value, cioè essere una cella. 5 e x + 1 sono solo valori: il compilatore rifiuta (lvalue required).
  3. Sintatticamente corretta, semanticamente sbagliata: si compila (gcc avvisa della divisione per zero) e il problema esplode a run-time.
  4. Il C è case sensitive: Printf non è printf. gcc segnala una funzione non dichiarata, e il linker non la trova.

Esercizio 4. Scrivi un programma che legge due interi e stampa la somma e la media, con la media decimale.

Soluzione
#include <stdio.h>

int main(void)
{
    int x;
    int y;

    printf("Due interi: ");
    if (scanf("%d %d", &x, &y) != 2) {
        printf("input non valido\n");
        return 1;
    }
    printf("somma = %d\n", x + y);
    printf("media = %f\n", (x + y) / 2.0);
    return 0;
}

Con 7 4 stampa somma = 11 e media = 5.500000; con ciao stampa input non valido (verificato). (x + y) / 2 darebbe 5: fra interi la divisione è intera, il 2.0 la rende decimale.

Esercizio 5. SI o NO?

  1. Il C è un linguaggio interpretato.
  2. Il linker produce il codice oggetto .o.
  3. /* ... */ può occupare più righe.
  4. In scanf("%d", &x) la & si può togliere.
  5. Una variabile locale non inizializzata vale 0.
Soluzione
  1. NO, è compilato. 2. NO, il .o lo produce il compilatore; il linker unisce i .o e le librerie nell’eseguibile. 3. SI. 4. NO, scanf ha bisogno dell’indirizzo della cella in cui scrivere. 5. NO, il valore è indeterminato (le globali invece partono da 0).

4Errori tipici

5Domande