3  La sintassi dei linguaggi di programmazione

Abbiamo visto come i linguaggi di programmazione siano degli insiemi di regole formali con cui si scrivono programmi eseguibili da computer. I linguaggio di programmazione ha due componenti principali: la sintassi e la semantica.

Partiamo dalla sintassi di un linguaggio di programmazione che possiamo considerare come l’insieme di regole che definisce la struttura e la forma delle istruzioni, cioè le unità logiche di esecuzione del programma. È come la grammatica in una lingua naturale e stabilisce quali combinazioni di simboli sono considerate costrutti validi nel linguaggio.

Per esempio, la sintassi determina quali parole chiave, operatori, separatori e altri elementi sono ammessi e in quale ordine devono apparire. Una sintassi corretta è fondamentale per garantire che il programma sia privo di errori formali e possa essere eseguito in modo non ambiguo.

L’importanza della comprensione della sintassi è simile alla buona conoscenza della grammatica per un linguaggio naturale:

3.1 Il token e il lessema

Gli elementi atomici della sintassi sono i token. Essi compongono tutte le istruzioni e possono essere sia prodotti dal programmatore che generati dall’analisi del testo da parte dell’interprete o compilatore. La comprensione di quali token siano validi, ci permette sia di scrivere istruzioni corrette, sia di sfruttare appieno i costrutti del linguaggio:

  • Parole chiave: Sono termini riservati del linguaggio che hanno significati specifici e non possono essere utilizzati per altri scopi, come if, else, while, for, ecc.

  • Operatori: Simboli utilizzati per eseguire operazioni su identificatori e letterali, come +, -, *, /, =, ==, ecc.

  • Delimitatori: Caratteri utilizzati per separare elementi del codice, come punto e virgola (;), parentesi tonde (()), parentesi quadre ([]), parentesi graffe ({}), ecc.

  • Identificatori: Nomi utilizzati per identificare variabili, funzioni, classi, e altri oggetti.

  • Letterali: Rappresentazioni di valori costanti nel codice, come numeri (123), stringhe ("hello"), caratteri ('a'), ecc.

  • Commento: Non fanno parte della logica del programma e sono ignorati nell’esecuzione.

  • Spazi e tabulazioni: Sono gruppi di caratteri non visualizzabili e spesso ignorati.

Un lessema è una sequenza di caratteri nel codice sorgente che corrisponde al pattern di un token ed è identificata dall’analizzatore lessicale come un’istanza di quel token. Un token è una coppia ordinata (nome token, valore attributo), dove nome token è un simbolo astratto che rappresenta una categoria di unità lessicale (ad esempio, identificatore, parola chiave, operatore), e valore attributo è un valore opzionale associato al token (ad esempio, il valore effettivo di un identificatore o di un numero). Un pattern è una definizione formale che specifica la struttura sintattica dei lessemi che possono essere classificati come un determinato tipo di token. Per token semplici come le parole chiave, il pattern può essere una stringa letterale. Per token più complessi come identificatori o numeri, il pattern è tipicamente definito mediante espressioni regolari o automi a stati finiti.

Un esempio per visualizzare i concetti introdotti:

if x == 10:
  • Token coinvolti:

    • if: Parola chiave.
    • NAME: Identificatore.
    • EQEQUAL: Operatore.
    • NUMBER: Letterale numerico.
    • COLON: Delimitatore.
  • Lessemi:

    • Il lessema per il token if è la sequenza di caratteri “if”.
    • Il lessema per il token NAME è “x”.
    • Il lessema per il token EQEQUAL è “==”.
    • Il lessema per il token NUMBER “10”.
    • Il lessema per il token COLON “:”.
  • Pattern:

    • Il pattern per il token if è la stringa esatta “if”.
    • Il pattern per un identificatore è una sequenza di lettere e numeri che inizia con una lettera.
    • Il pattern per l’operatore == è la stringa esatta “==”.
    • Il pattern per un letterale numerico è una sequenza di cifre.
    • Il pattern per il delimitatore : è la stringa esatta “:”.

3.2 L’analizzatore lessicale e il parser

L’analizzatore lessicale (o lexer) è un componente del compilatore o interprete che prende in input il codice sorgente del programma e lo divide in lessemi. Esso confronta ciascun lessema con i pattern definiti per il linguaggio di programmazione e genera una sequenza di token. Questi token sono poi passati al parser.

Ad esempio, il codice if x == 10: di Python, viene trasformato in una sequenza di token: IF, NAME(x), EQEQUAL, NUMBER(10), COLON.

Il parser è un altro componente del compilatore o interprete che prende in input la sequenza di token generata dall’analizzatore lessicale e verifica che la sequenza rispetti le regole sintattiche del linguaggio di programmazione. Il parser analizza i token per formare una struttura gerarchica che rappresenti le relazioni grammaticali tra di essi. Questa struttura interna è spesso un albero di sintassi (parse tree o syntax tree), che riflette la struttura grammaticale del codice sorgente, solitamente descritta usando una forma standard di notazione come la BNF (Backus-Naur Form) o varianti di essa 1. L’albero di sintassi ottenuto viene utilizzato per le successive fasi di compilazione o interpretazione, come quella di analisi semantica e di generazione del codice eseguibile. Ad esempio, il parser può verificare che le espressioni aritmetiche siano ben formate, che le istruzioni siano correttamente annidate e che le dichiarazioni di variabili siano valide.

1 La BNF (Backus-Naur form o Backus normal form) è una metasintassi, ovvero un formalismo attraverso cui è possibile descrivere la sintassi di linguaggi formali (il prefisso meta ha proprio a che vedere con la natura circolare di questa definizione). Si tratta di uno strumento molto usato per descrivere in modo preciso e non ambiguo la sintassi dei linguaggi di programmazione, dei protocolli di rete e così via, benché non manchino in letteratura esempi di sue applicazioni a contesti anche non informatici e addirittura non tecnologici. Un esempio è la grammatica di Python.

3.3 L’espressione

Un’espressione è un costrutto fondamentale nei linguaggi di programmazione che presenta sia aspetti sintattici che semantici. Dal punto di vista sintattico, un’espressione può essere definita come una sequenza finita di lessemi che rispetta le regole grammaticali del linguaggio e che può essere valutata per produrre un valore, cioè un dato concreto che rappresenta un’informazione specifica all’interno di un programma.

Un identificatore è un’espressione, così come un letterale. Inoltre, un’espressione più complessa è perlopiù una combinazione di identificatori, letterali e operatori, anche se parole chiave e delimitatori possono essere presenti (ad esempio in Python True è una parola chiave corrispondente al valore logico di verità). Componendo opportunamente espressioni per mezzo di operatori, si ottiene ancora un’espressione.

Ecco alcune tipologie di espressioni (notazioni in Python, non molto dissimili da altri linguaggi):

  • Espressioni aritmetiche: Combinano letterali numerici, identificatori valorizzabili in numeri e operatori aritmetici per eseguire calcoli matematici. Sono estese ad altre tipologie di letterali quando gli operatori sono parimenti reinterpretati. Esempi:

    • 5 + 3: Due letterali interi, 5 e 3, con un operatore binario applicato, +.
    • y / 4.0: Un identificatore di variabile e un letterale a virgola mobile, rispettivamente y e 4.0, con un operatore binario applicato, /.
    • "Hello, " + "world!": Due letterali stringa, "Hello, " e "world! ", con un operatore binario di concatenazione, +.
  • Espressioni logiche: Qui gli operatori sono logici e gli operandi qualsiasi letterale o identificatore. Il risultato è un valore booleano, cioè vero o falso. Esempi:

    • x or 5: x è un identificatore, or un operatore logico binario e 5 un letterale intero (che ha un valore di verità).
    • not y: not è un operatore logico unario e y un identificatore.
    • a and "Hello": a è un identificatore, and un operatore logico binario e "Hello" un letterale stringa (che ha un valore di verità).
  • Espressioni di confronto: Sono caratterizzate da operatori di confronto e restituiscono valori booleani, sempre a partire da letterali e identificatori. Esempi:

    • x < y.
    • x != 42.
    • "Hello" >= "world!": L’ordinamento dei letterali stringa predefinto in Python è quello lessicografico.
  • Espressioni di chiamata a funzione: Invocano identificatori particolari, funzioni e metodi di classi o oggetti, con un elenco opzionale di argomenti definiti da identificatori e letterali. I delimitatori sono usati sia per definire l’inizio e la fine dell’elenco di argomenti, sia per separare gli argomenti. Esempi:

    • max(a, b): La funzione con identificatore max riceve due argomenti, a e b.
    • sin(30, "gradi").
    • obj.my_function(x, 42): Si invoca il metodo individuato dall’identificatore my_function, dell’oggetto con identificatore obj per mezzo dell’operatore di accesso a membro ., passando i due argomenti x e 42.
  • Espressioni di manipolazione di contenitori di dati: Creano e manipolano strutture dati presenti in molti linguaggi di programmazione come liste, dizionari, tuple e insiemi. Delimitatori individuano l’inizio e la fine dell’elenco di elementi e sono utilizzati anche per definire il tipo di struttura. Ogni elemento può essere un identificatore o un letterale. Esempi:

    • [1, x, 3]: Elenco di letterali e identificatori separati dal delimitatore ,, racchiuso da una coppia delimitatori, [ e ], per creare una lista.
    • ('y', 42, True): I delimitatori ( e ) creano una tupla.
    • {0: "Vero", 1: "Falso"}: Elenco di coppie di identificatori o letterali, con un delimitatore : a separarli, generante un dizionario. Un analogo con parole chiave potrebbe essere {True: "Vero", False: "Falso"}, dato che True e False sono parole chiave.
    • contenitore[3], contenitore[indice], "Hello"[4]: Un identificatore di un contenitore di dati seguito da una coppia di parentesi quadre che fungono da operatore di accesso all’elemento dell’elenco la cui posizione è determinata dal letterale o altro identificatore contenuto. Il letterale stringa crea un contenitore di caratteri e come tale può essere utilizzato per estrarne uno coll’operatore di accesso.
  • Espressioni condizionali (ternarie): È una espressione in cui identificatori, letterali o espressioni più semplici sono separati da parole chiave, if e else. In pratica è un modo compatto di scrivere una istruzione condizionale. Esempi:

    • 1 if x else 0: Se l’identificatore x è associato ad un dato con valore di verità allora l’espressione restituisce 1, 0 in caso contrario.
    • True if x > 0.5 else False: Abbiamo una espressione aritmetica x > 0.5 è in mezzo alle parole chiave if ed else, quindi è il valore di verità ottenuto dalla sua valutazione che determina se ad essere restituita dall’espressione ternaria, sarà la parola chiave che precede if o quella che segue else.
  • Espressioni di creazione di contenitori di dati per comprensione: Utilizzano una sintassi compatta per generare nuovi contenitori di dati a partire da elementi esistenti, applicando una funzione e una condizione. Esempi:

    • [x**2 for x in range(10) if x % 2 == 0]: Genera una lista dei quadrati dei numeri pari da 0 a 9.
    • {x: x**2 for x in range(10) if x % 2 == 0}: Genera un dizionario dove le chiavi sono numeri pari da 0 a 9 e i valori sono i loro quadrati.
    • {x**2 for x in range(10) if x % 2 == 0}: Genera un set dei quadrati dei numeri pari da 0 a 9.
  • Espressioni composte:

    • (x + y) * (a - b): Combinazione di espressioni aritmetiche.
    • max(a, b) if a > b else min(a, b): Espressione condizionale con confronto e chiamate a funzione.
    • [x**2 for x in range(10) if x % 2 == 0]:

3.4 L’istruzione

Anche l’istruzione è sia un concetto sintattico che semantico, al pari dell’espressione. Nel primo caso, un’istruzione può essere definita come una unità sintattica completa che specifica un’azione da eseguire all’interno di un programma.

I linguaggi di programmazione presentano delle istruzioni condivise, nel senso di fondamentali, che presentano solo minime differenze sintattiche tra l’uno e l’altro e costrutti più particolari, perché dipendenti da specificità dalla progettazione del linguaggio particolare. Ciò potrebbe essere anche solo questione di sintassi, più che effetto della introduzione di nuovi concetti.

3.4.1 L’istruzione semplice

L’istruzione semplice è un’operazione atomica che consiste in una combinazione di lessemi. A seconda del linguaggio di programmazione, un’istruzione semplice può essere terminata da un delimitatore specifico, come un punto e virgola ;. Ciò vale per Python e JavaScript, anche se è poco comune, mentre per Java, C e C++ è obbligatorio. In tutti questi linguaggi, è possibile mettere più istruzioni su una singola riga separandole con un punto e virgola ;.

Di seguito un elenco di istruzioni semplici, alcune standard cioè presenti in tutti o la gran parte dei linguaggi più diffusi, altre specifiche ma che mettono in evidenza aspetti rilevanti di progettazione:

  • Espressioni: In alcuni linguaggi di programmazione un’espressione è anche una delle istruzioni semplici più importanti quando a sé stante, anche se, in generale, è presente all’interno di istruzioni semplici o composte. Alcuni esempi in vari linguaggi:

    • In Python, l’uso principale dell’espressione come istruzione, è la produzione di effetti collaterali, cioè effetti diversi dalla restituzione di valore:

      • Chiamate di funzione: print("Hello") e list.append(5), ove le espressioni producono effetti collaterali come la stampa su schermo e la modifica di una lista.
      • Valutazioni condizionali: x > 5 and fai_qualcosa(), ove la funzione fai_qualcosa() è eseguita solo se x > 5.
    • In Java e C++ le chiamate a funzioni o metodi di classi o oggetti sono espressioni e istruzioni.

    • In C le chiamate a funzioni.

    • In Java, C e C++ l’applicazione dell’operatore unario di incremento e quello di decremento sono anch’esse istruzioni valide: x++; e x--;, ove, rispettivamente, x è incrementato e decrementato di una unità.

    • Javascript ha anche il concetto di funzione anonima auto-invocata (inglese: immediately invoked function expression, IIFE), cioè espressioni a sé stanti che vengono invocate immediatamente dopo la loro definizione:

    (function() {
      console.log('Hello, World!');
    })();
    • In C++ sono definiti gli operatori di flusso << e >> che permettono, rispettivamente, di inviare dati al flusso di output e ricevere dati dal flusso di input:
    #include <iostream>
    #include <string>
    
    int main() {
      std::string name;
    
      std::cout << "Enter your name: "; /* <1> */
      std::getline(std::cin, name); /* <2> */
      std::cout << "Hello, " << name << "!" << std::endl;  /* <3> */
    
      std::cout << "Enter your name: "; /* <4> */
      std::cin >> name; /* <5> */
      std::cout << "Hello, " << name << "!" << std::endl; /* <6> */
    
      return 0;
    }
    1. std::cout << "Enter your name: ": Stampa il messaggio “Enter your name:” su console.
    2. std::getline(std::cin, name);: Legge l’input dell’utente e lo memorizza nella variabile name.
    3. std::cout << "Hello, " << name << "!" << std::endl;: Stampa un messaggio concatenato su console.
    4. std::cout << "Enter your name: ": Stampa il messaggio “Enter your name:” su console.
    5. std::cin >> name;: Legge l’input dell’utente e lo memorizza nella variabile name.
    6. std::cout << "Hello, " << name << "!" << std::endl;: Stampa un messaggio concatenato su console.

    Entrambi gli operatori << e >> supportano la concatenazione (inglese: chaining), permettendo di inserire più operazioni in una singola istruzione. Ad esempio:

    #include <iostream>
    #include <string>
    
    int main() {
      std::string nome, cognome; /* <1> */
      int età; /* <2> */
    
      std::cout << "Inserisci il tuo nome, cognome ed età: "; /* <3> */
      std::cin >> nome >> cognome >> età; /* <4> */
    
      std::cout << "Nome: " << nome << " " << cognome /* <5> */
                << ", Età: " << età << std::endl; /* <6> */
    
      return 0; 
    }
    1. Dichiarazione delle variabili nome e cognome di tipo std::string.
    2. Dichiarazione della variabile età di tipo int.
    3. Richiesta di input all’utente per inserire il nome, il cognome e l’età.
    4. Lettura dell’input dell’utente e memorizzazione nelle variabili nome, cognome e età.
    5. Inizio della stampa del nome e del cognome dell’utente.
    6. Continuazione della stampa con l’età dell’utente e terminazione della riga.
  • Dichiarazione di variabili: La dichiarazione di una variabile introduce una nuova variabile nel programma e specifica il suo tipo2. La dichiarazione non assegna necessariamente un valore iniziale alla variabile. Esempi:

    • In Java, C e C++, la dichiarazione delle variabili richiede la specifica del tipo e può essere combinata con l’assegnazione: int x; oppure int x = 5;.
    • In Python la dichiarazione avviene automaticamente con l’assegnazione, anche se è possibile annotare il tipo di una variabile, ad esempio x: int = 5, anche se ciò non costringe il programmatore a utilizzare x con interi.
    • In Javascript la dichiarazione è combinata all’assegnazione e definisce anche l’ambito, cioè la regione di codice dove è accessibile, e la modificabilità della variabile:
    1let x = 5;
    2var y = 10;
    3const z = 15;
    1
    Ambito di blocco di codice, variabile riassegnabile.
    2
    Ambito di funzione, variabile riassegnabile.
    3
    Ambito di blocco di codice, variabile non riassegnabile.
  • Assegnazione: Utilizza un operatore di assegnazione (ad esempio, =) per attribuire un valore rappresentato da un letterale, una espressione o un identificatore, ad un identificatore di variabile, che possiamo pensare come un nome simbolico rappresentante una posizione dove è memorizzato un valore. In alcuni linguaggi deve essere preceduta dalla dichiarazione.

    • In Python:

      z = (x * 2) + (y / 2)
      • z: Identificatore della variabile.
      • =: Operatore di assegnazione.
      • (x * 2): Espressione che moltiplica x per 2.
      • (y / 2): Espressione che divide y per 2.
      • +: Operatore aritmetico che somma i risultati delle due espressioni in una più complessa. L’esecuzione dell’istruzione produce un risultato valido solo se x e y sono associate a valori numerici e ciò perché non tutte le istruzioni sintatticamente corrette sono semanticamente corrette. D’altronde ciò non deve essere preso come regola, perché se * fosse un operatore che ripete quanto a sinistra un numero di volte definito dal valore di destra e / la divisione del valore di sinistra in parti di numero pari a quanto a destra, allora x e y potrebbero essere stringhe.
    • In Java, C e C++ l’assegnazione è preceduta dalla dichiarazione di tipo oppure è contestuale a quest’ultima: z = (x * 2) + (y / 2);.

  • Assegnazione aumentata: Combina un’operazione e un’assegnazione in un’unica istruzione.

    • In Python, l’assegnazione aumentata è l’unico modo per incrementare o decrementare il valore di una variabile in modo conciso:
    1x += 1
    2x -= 1
    1
    Incrementa x di 1 (x = x + 1)
    2
    Decrementa x di 1 (x = x - 1)
    • In Java, C e C++ è possibile utilizzare sia l’assegnazione aumentata che gli operatori di incremento e decremento in modalità prefisso e suffisso:
    // Assegnazione aumentata
    1x += 1;
    2x -= 1;
    
    // Incremento e decremento prefisso
    3++x;
    4--x;
    
    // Incremento e decremento suffisso
    5x++;
    6x--;
    1
    Equivalente a x = x + 1.
    2
    Equivalente a x = x - 1.
    3
    Incrementa x di 1 e restituisce il nuovo valore di x.
    4
    Decrementa x di 1 e restituisce il nuovo valore di x.
    5
    Incrementa x di 1 e restituisce il valore precedente di x.
    6
    Decrementa x di 1 e restituisce il valore precedente di x.
  • Istruzioni di controllo del flusso: Permettono di interrompere o continuare l’esecuzione di cicli o di saltare a una specifica etichetta nel codice. Esempi:

    • In Python: break, continue.
    • In Java: break;, continue;.
    • In C: break;, continue;, goto label;.
    • In C++: break;, continue;, goto label;.
  • Gestione della vita degli oggetti: Include la creazione, l’utilizzo e la distruzione dei dati presenti nella memoria del computer. In alcuni linguaggi ciò è parzialmente o totalmente a carico del programmatore, mentre, all’altro estremo, è completamente gestito dal linguaggio. Esempi:

    • Creazione di oggetti:

      • In C++: int* ptr = new int;.
      • In Java: String str = new String("Hello, world!");
      • In Python: Non è presente una istruzione specifica giacché l’espressione MyClass() crea un oggetto di tipo MyClass.
      • In C: La creazione di oggetti è spesso gestita manualmente attraverso l’allocazione di memoria dinamica con funzioni come malloc.
    • Distruzione di oggetti:

      • In Python: La gestione della memoria è automatica tramite il garbage collector.
      • In Java: In Java, la gestione della memoria è affidata al garbage collector.
      • In C++: delete ptr;.
      • In C: free(ptr); (richiede #include <stdlib.h>).
    • Eliminazione di variabili:

      • In Python: del x.
  • Ritorno di valori: Utilizzata all’interno di funzioni per restituire un valore. Esempi:

    • In Python: return x.
    • In Java: return x;.
    • In C: return x;.
    • In C++: return x;.
  • Generazione di eccezioni: Utilizzata per generare e inviare un’eccezione, cioè una interruzione della sequenza ordinaria delle istruzione per gestire gli effetti di una anomalia occorsa durante l’esecuzione. Esempi:

    • In Python: raise ValueError("Invalid input").
    • In Java: throw new IllegalArgumentException("Invalid input");.
    • In C++: throw std::invalid_argument("Invalid input");.
    • In C: Non esiste un equivalente diretto, ma si possono utilizzare meccanismi come setjmp e longjmp per la gestione degli errori.
  • Importazione di moduli: Permettono di importare moduli o parti di essi, cioè di utilizzare funzioni, classi, variabili e altri identificatori definiti in altri file o librerie. Esempi:

    • In Python: import math, from math import sqrt
    • In Java: import java.util.List;
    • In C: #include <stdio.h>
    • In C++: #include <iostream>
  • Dichiarazioni globali e non locali: Permettono di dichiarare variabili che esistono nell’ambito globale o non locale. Esempi:

    • In Python: global x, nonlocal y.
    • In Java: Le variabili globali non sono supportate direttamente; si utilizzano campi statici delle classi.
    • In C: Le variabili globali sono dichiarate al di fuori di qualsiasi funzione.
    • In C++: Le variabili globali sono dichiarate al di fuori di qualsiasi funzione.
  • Assert: Utilizzata per verificare se una condizione è vera e, in caso contrario, sollevare un’eccezione. Esempi:

    • In Python: assert x > 0, "x deve essere positivo".
    • In Java: assert x > 0 : "x deve essere positivo";.
    • In C: assert(x > 0); (richiede #include <assert.h>).
    • In C++: assert(x > 0); (richiede #include <cassert>).

2 Spiegheremo il concetto di tipo a breve, per ora si può pensare ad esso come l’insieme dei possibili valori e operazioni che si possono effettuare su di essi.

3.5 Le istruzioni composte e i blocchi di codice

Le istruzioni composte sono costituite da più istruzioni semplici e possono includere strutture di controllo del flusso, come condizioni (if), cicli (for, while) ed eccezioni (try, catch). Queste istruzioni sono utilizzate per organizzare il flusso di esecuzione del programma e possono contenere altre istruzioni semplici o composte al loro interno.

Un blocco di codice è una sezione del codice che raggruppa una serie di istruzioni che devono essere eseguite insieme. I blocchi di codice sono spesso utilizzati all’interno delle istruzioni composte per delimitare il gruppo di istruzioni che devono essere eseguite in determinate condizioni o iterazioni.

In molti linguaggi di programmazione, i blocchi di codice sono delimitati da parentesi graffe ({}), mentre in altri linguaggi, come Python, l’indentazione è utilizzata per indicare l’inizio e la fine di un blocco di codice.

Alcuni esempi di istruzione e blocco di codice:

  • Esempio in C:

    if (x > 0) {
      printf("x è positivo\n");
    
      y = x * 2;
    }

    In questo esempio:

    1. if (x > 0) e quanto nelle parentesi graffe è un’istruzione composta. if è una parola chiave seguita da una espressione tra delimitatori.
    2. { printf("x è positivo\n"); y = x * 2; } è un blocco di codice che viene eseguito se la condizione dell’istruzione if è vera. Sono presenti diversi delimitatori, una espressione e una istruzione di assegnamento.
  • Esempio in Python:

    if x > 0:
      print("x è positivo")
    
      y = x * 2

    In questo esempio:

    1. if x > 0: è un’istruzione composta.
    2. Le righe indentate sotto l’istruzione if, cioè print("x è positivo") e y = x * 2, costituiscono un blocco di codice che viene eseguito se la condizione dell’istruzione if è vera.

Alcuni esempi di istruzione e blocco di codice:

  • Esempio in C:

    if (x > 0) {
      printf("x è positivo\n");
    
      y = x * 2;
    }

    In questo esempio:

    1. if (x > 0) e quanto nelle parentesi graffe è un’istruzione composta.
    2. { printf("x è positivo\n"); y = x * 2; } è un blocco di codice che viene eseguito se la condizione dell’istruzione if è vera.
  • Esempio in Python:

    if x > 0:
      print("x è positivo")
    
      y = x * 2

    In questo esempio:

    1. if x > 0: è un’istruzione composta.
    2. Le righe indentate sotto l’istruzione if (print("x è positivo") e y = x * 2) costituiscono un blocco di codice che viene eseguito se la condizione dell’istruzione if è vera.

Di seguito sono elencate le istruzioni composte principali, con spiegazioni e semplici esempi di sintassi per Python, Java, C e C++:

  • Condizionali (if, else if, else): Le istruzioni condizionali permettono l’esecuzione di blocchi di codice basati su espressioni logiche.

    • Python:

      if x > 0:
        print("x è positivo")
      
      elif x == 0:
        print("x è zero")
      
      else:
        print("x è negativo")
    • Java:

      if (x > 0) {
        System.out.println("x è positivo");
      } else if (x == 0) {
        System.out.println("x è zero");
      } else {
        System.out.println("x è negativo");
      }
    • C:

      if (x > 0) {
        printf("x è positivo\n");
      
      } else if (x == 0) {
        printf("x è zero\n");
      
      } else {
        printf("x è negativo\n");
      
      }
    • C++:

      if (x > 0) {
        std::cout << "x è positivo" << std::endl;
      
      } else if (x == 0) {
        std::cout << "x è zero" << std::endl;
      
      } else {
        std::cout << "x è negativo" << std::endl;
      
      }
  • Cicli (for): I cicli for permettono di iterare su un insieme di valori o di ripetere l’esecuzione di un blocco di codice per un numero specificato di volte.

    • Python:

      for i in range(5):
        print(i)
    • Java:

      for (int i = 0; i < 5; i++) {
        System.out.println(i);
      }
    • C:

      for (int i = 0; i < 5; i++) {
        printf("%d\n", i);
      }
    • C++:

      for (int i = 0; i < 5; i++) {
        std::cout << i << std::endl;
      }

Cicli (while): I cicli while ripetono l’esecuzione di un blocco di codice finché una condizione specificata rimane vera.

  • Python:

    while x > 0:
      print(x)
    
      x -= 1
  • Java:

    while (x > 0) {
      System.out.println(x);
    
      x--;
    }
  • C:

    while (x > 0) {
      printf("%d\n", x);
    
      x--;
    }
  • C++:

    while (x > 0) {
      std::cout << x << std::endl;
    
      x--;
    }
  • Gestione delle eccezioni (try, catch): Le istruzioni di gestione delle eccezioni permettono di gestire errori o condizioni anomale che possono verificarsi durante l’esecuzione del programma.

    • Python:

      try:
        value = int(input("Inserisci un numero: "))
      
      except ValueError:
        print("Input non valido")
    • Java:

      try {
        int value = Integer.parseInt(input);
      
      } catch (NumberFormatException e) {
        System.out.println("Input non valido");
      }
    • C++:

      try {
        int value = std::stoi(input);
      
      } catch (const std::invalid_argument& e) {
        std::cout << "Input non valido" << std::endl;
      }
    • C: C non ha un supporto nativo per la gestione delle eccezioni, ma si possono usare meccanismi come setjmp e longjmp.3

      #include <setjmp.h>
      
      1jmp_buf buf;
      
      void error() {
      2  longjmp(buf, 1);
      }
      
      int main() {
      3  if (setjmp(buf)) {
      4    printf("Errore rilevato\n");
      
        } else {
      5    error();
        }
        return 0;
      }
      1
      Dichiarazione di una variabile di tipo jmp_buf.
      2
      Salta al punto salvato in buf con valore di ritorno 1.
      3
      Salva il contesto di esecuzione attuale in buf.
      4
      Esegue se longjmp viene chiamato.
      5
      Chiama la funzione error, che salta indietro al punto setjmp.
  • Selezione multipla (switch, case, default): Le istruzioni di selezione multipla permettono di eseguire uno tra diversi blocchi di codice basati sul valore di un’espressione.

    • Python (a partire da Python 3.10 con match):

      match x:
        case 0:
          print("x è zero")
      
        case 1:
          print("x è uno")
      
        case _:
          print("x è un altro numero")
    • Java:

      switch (x) {
        case 0:
          System.out.println("x è zero");
      
          break;
      
        case 1:
          System.out.println("x è uno");
      
          break;
      
        default:
          System.out.println("x è un altro numero");
      
          break;
      }
    • C:

      switch (x) {
        case 0:
          printf("x è zero\n");
      
          break;
      
        case 1:
          printf("x è uno\n");
      
          break;
      
        default:
          printf("x è un altro numero\n");
      
          break;
      }
    • C++:

      switch (x) {
        case 0:
          std::cout << "x è zero" << std::endl;
      
          break;
      
        case 1:
          std::cout << "x è uno" << std::endl;
      
          break;
      
        default:
          std::cout << "x è un altro numero" << std::endl;
      
          break;
      }

3 setjmp e longjmp sono funzioni della libreria standard del C utilizzate per implementare il salto non locale, consentendo a un programma di salvare e ripristinare l’ambiente di esecuzione. La funzione setjmp salva lo stato corrente del programma (inclusi registri e stack) in una struttura jmp_buf, mentre longjmp ripristina questo stato, causando un salto indietro nel flusso di controllo fino al punto in cui setjmp è stato chiamato. Queste funzioni permettono di uscire da contesti di funzioni annidate senza dover tornare manualmente attraverso ogni chiamata di funzione.

3.6 L’organizzazione delle istruzioni in un programma

Il programma è solitamente salvato in un file di testo in righe. Queste righe possono essere classificate in righe fisiche e righe logiche.

Una riga fisica è una linea di testo nel file sorgente del programma, terminata da un carattere di a capo.

Esempio:

1int x = 10;
1
Questa è una riga fisica.

Una riga logica è una singola istruzione, che può estendersi su una o più righe fisiche.

Esempio di riga logica con più righe fisiche:

1int y = (10 + 20 + 30 +
2         40 + 50);
1
Prima riga fisica della riga logica.
2
Seconda riga fisica della riga logica.

Il concetto di righe fisiche e logiche esiste perché le istruzioni (o righe logiche) possono essere lunghe e composte, richiedendo più righe fisiche per migliorare la leggibilità e la gestione del codice.

In molti linguaggi di programmazione, l’uso di righe fisiche e logiche facilita l’organizzazione e la formattazione del codice. Ad esempio:

  • Python utilizza l’indentazione per definire i blocchi di codice, quindi una riga logica che si estende su più righe fisiche deve continuare con una corretta indentazione. Inoltre, è possibile usare il carattere di continuazione (\) per indicare che una riga logica prosegue sulla riga successiva:

    result = (10 + 20 + 30 + \
              40 + 50)
  • Java e C utilizzano le parentesi graffe ({}) per delimitare i blocchi di codice, e le istruzioni possono estendersi su più righe fisiche senza il bisogno di un carattere di continuazione, grazie al punto e virgola (;) che termina le istruzioni:

    int y = (10 + 20 + 30 + 
             40 + 50);
    int y = (10 + 20 + 30 + 
             40 + 50);

L’uso corretto di righe fisiche e logiche migliora la leggibilità del codice, rendendolo più facile da capire e mantenere. Inoltre, una buona formattazione del codice facilita il lavoro di squadra, poiché gli sviluppatori possono facilmente seguire e comprendere la logica implementata da altri.