Vai al contenuto
Topos Uranos

Riepilogo

Questa lezione spiega che cos’è un programma e che cosa fa un compilatore: la macchina esegue soltanto operazioni elementari su numeri in memoria, e il codice sorgente deve essere tradotto in codice macchina prima di essere eseguito. Con un programma di due file percorre le tre fasi di questa traduzione in C++ (preprocessing, compilazione e collegamento) e distingue ciò che accade in compilazione da ciò che accade in esecuzione, con un errore di ciascun tipo. Confronta i linguaggi compilati con quelli interpretati, riassume la storia di C++ fino agli standard C++23 e C++26 e precisa quale C++ insegna questo corso.

Obiettivi di apprendimento

  1. Spiegare, con uno schema, il percorso da un file sorgente a un eseguibile: preprocessing, compilazione e collegamento, con ciò che entra in ciascuna fase e ciò che ne esce.
  2. Classificare un evento come proprio della compilazione, del collegamento o dell’esecuzione, come un errore di sintassi, la mancanza di una funzione o una divisione per zero.
  3. Confrontare un linguaggio compilato con uno interpretato secondo il momento in cui si rilevano gli errori e ciò che il programmatore vede della macchina.
  4. Collocare nel tempo gli standard del linguaggio e spiegare perché il corso usa C++23.

Che cos’è un programma

Un programma è una successione di istruzioni che una macchina può eseguire per trasformare dei dati in dei risultati. Nella lezione precedente, Dal problema all’algoritmo, si è visto che prima del programma c’è l’algoritmo, il metodo che risolve il problema, scritto perché lo segua una persona. Il programma è quello stesso metodo scritto per una macchina, che non capisce frasi né intenzioni, ma ordini elementari: copiare un numero da un punto della memoria a un altro, sommare due numeri, confrontarli, saltare a un’altra istruzione secondo il risultato.

La memoria del computer è una lunga fila di caselle numerate, ciascuna con un numero da 0 a 255 (un byte). Tutto ciò che la macchina tratta vi è codificato come numeri: i dati e anche le istruzioni stesse. Se è così, un programma eseguibile è una successione di numeri che il processore interpreta come ordini, il codice macchina, e dipende dal processore: quelli della famiglia x86-64, comuni nei computer da scrivania, e gli ARM, comuni nei telefoni e nei Mac recenti, non capiscono gli stessi numeri.

Nessuno oggi scrive in codice macchina. Si scrive in un linguaggio di alto livello, come C++, le cui istruzioni somigliano alla notazione matematica, in un file di testo chiamato codice sorgente (o semplicemente sorgente). Ne segue la necessità di un traduttore che produca, a partire dal sorgente, il codice macchina equivalente: in C++, il compilatore.

EsempioUn programma e ciò che la macchina esegue

Questo è un programma completo in C++. Non occorre ancora capirlo: ciascuna delle sue righe sarà spiegata nella lezione 1.5. Basta vedere che è testo, che si può leggere, e che sotto compare ciò che ha scritto quando è stato eseguito.

Programma in C++
// Sarà spiegato riga per riga nella lezione 1.5.#include <print>​int main(){    std::println("Ciao, mondo");    std::println("2 + 3 = {}", 2 + 3);}
Output
Ciao, mondo2 + 3 = 5

Per vedere l’altro estremo della traduzione, si consideri una funzione che si limita a sommare due interi (le funzioni saranno studiate nell’unità 5). Non è un programma completo, perché le manca main, il punto in cui comincia l’esecuzione, ma il compilatore può tradurla da sola.

Codice C++
int add(int a, int b){    return a + b;}

Il programma objdump mostra il codice macchina che GCC 15 ha prodotto per essa su un computer x86-64 con Linux: in ciascuna riga, la posizione dell’istruzione, i suoi byte in esadecimale e il suo nome leggibile.

Terminale
$ g++ -std=c++23 -O1 -c add.cpp$ objdump -d add.o0000000000000000 <_Z3addii>:   0:	f3 0f 1e fa          	endbr64   4:	8d 04 37             	lea    (%rdi,%rsi,1),%eax   7:	c3                   	ret

La somma intera si è ridotta a otto byte. La prima istruzione, endbr64, è un contrassegno di sicurezza che non calcola nulla; la seconda, lea, somma i due numeri che la funzione ha ricevuto in due registri del processore (%rdi e %rsi) e lascia il risultato in un terzo (%eax); la terza, ret, torna al punto da cui la funzione è stata chiamata. Il nome _Z3addii è il modo in cui il compilatore codifica «funzione add con due parametri int».

Dall’esempio seguono due osservazioni. Prima: il codice macchina non conserva i nomi a e b, né l’indentazione, né i commenti; tutto ciò esisteva per chi legge il sorgente, e il compilatore lo ha usato per decidere quali istruzioni scrivere. Seconda: ciò che il programmatore C++ scrive si traduce in poche istruzioni e senza intermediari; questa vicinanza alla macchina è la ragione dell’efficienza di C++ e, come si vedrà nel corso, anche di alcuni dei suoi pericoli.