Salta ai contenuti

Lezione 2: La Logica dei Numeri e l'Input/Output

Copertina Lezione 2

Nelle lezioni precedenti abbiamo visto come un programma minimale possa restituire un valore numerico al sistema operativo o visualizzare messaggi statici sullo schermo. In questa lezione faremo un importante passo avanti: introdurremo il concetto di variabile, scopriremo come interagire dinamicamente con l’utente chiedendo dati da tastiera tramite scanf, e come scrivere espressioni logiche ed algebriche per guidare le scelte fondamentali della CPU tramite il costrutto if.


Per un programmatore alle prime armi, una variabile assomiglia a una lettera algebrica (come la xx o la yy delle equazioni scolastiche). Sotto il cofano del computer, tuttavia, una variabile è una denominazione simbolica associata a un’area fisica della memoria RAM.

Quando scrivete nel vostro codice C:

int age = 19;

State ordinando al compilatore di:

  1. Riservare una cella fisica di memoria (dello Stack) di dimensione fissa (di norma 4 byte per il tipo int sui sistemi moderni a 64 bit).
  2. Associare il nome simbolico age all’indirizzo di partenza di quella cella.
  3. Scrivere dentro quella cella la sequenza binaria corrispondente al numero intero 19 (00000000 00000000 00000000 00010011).

Notiamo che i nomi delle variabili sono un’astrazione creata esclusivamente per agevolare lo sviluppatore umano. Nel momento in cui il compilatore traduce il codice in linguaggio Assembly e in codice macchina, tutti i nomi simbolici (age, raggio, etc.) scompaiono del tutto, venendo sostituiti da indirizzi di memoria fisici o scostamenti relativi a registri di base (es. [rbp-4]).

Il tipo di dato indica al compilatore due informazioni cruciali:

  1. La dimensione in byte della scatola di memoria da riservare.
  2. La codifica (la regola di traduzione) con cui interpretare i bit salvati all’interno. Ad esempio, la sequenza di bit che rappresenta l’intero 65 è identica a quella che rappresenta il carattere 'A' in codifica ASCII; solo il tipo ci dice come interpretarla.

Il linguaggio C impone la dichiarazione esplicita del tipo per ragioni di efficienza compilativa ed esecutiva: conoscendo in anticipo il tipo, il compilatore genera direttamente le istruzioni macchina native più corrette ed efficienti (la somma tra interi add usa circuiti della CPU radicalmente diversi rispetto alla somma in virgola mobile per numeri reali).

All’inizio del nostro viaggio ci concentreremo su due soli tipi fondamentali:

  • int: Rappresenta i numeri interi con segno (es. 0, 42, -1053). Di norma occupa 4 byte (32 bit), consentendo di rappresentare numeri nell’intervallo tra circa −2-2 miliardi e +2+2 miliardi.
  • double: Rappresenta i numeri reali o decimali (es. 3.14, -0.005, 2.0). Il nome deriva da double precision (doppia precisione): occupa 8 byte (64 bit) e segue lo standard internazionale IEEE 754 per la codifica in virgola mobile. È il tipo di default utilizzato dal C quando scrivete una costante decimale nel codice.

Dichiarazioni con Inizializzazione e Dichiarazioni Disgiunte

Sezione intitolata “Dichiarazioni con Inizializzazione e Dichiarazioni Disgiunte”

In C potete decidere di dichiarare e inizializzare una variabile contemporaneamente sulla stessa riga:

int age = 19; // Alloca la memoria e vi scrive subito 19

Oppure potete scegliere la strada della dichiarazione disgiunta dall’assegnamento:

int score; // Dichiarazione: lo spazio viene riservato, ma il valore è indeterminato
score = 100; // Assegnamento successivo: viene scritto 100

Quando leggete un errore di compilazione che cita le parole lvalue o rvalue, fate riferimento a questa distinzione:

  • Lvalue (Left-value): Identifica una locazione di memoria fisica persistente e modificabile (scrivibile). Deve stare a sinistra dell’operatore di assegnamento =. Esempio: il nome di una variabile (x).
  • Rvalue (Right-value): Rappresenta un valore temporaneo, costante o calcolato che non possiede una locazione di memoria persistente a cui fare riferimento. Può stare solo a destra dell’operatore =. Esempi: un letterale numerico (5), il risultato di un calcolo (x + 3).

Cosa non potete fare:

5 = x; // ERRORE: 5 è un rvalue, non potete assegnargli una cella di memoria!
x + y = 10; // ERRORE: l'espressione a sinistra non è un lvalue scrivibile.

La funzione printf non serve solo a stampare stringhe statiche. Funziona come una stampante di modelli o template. Per stampare un numero a schermo, la CPU deve effettuare una conversione: deve prendere la rappresentazione binaria in RAM (es. il numero 5) e tradurla nel carattere testuale corrispondente (il codice ASCII del carattere '5').

Per farlo, forniamo a printf una Stringa di Formato (Format String) che funge da modello con dei segnaposto (specificatori di formato), seguita dall’elenco degli argomenti che andranno a riempire ordinatamente tali segnaposto:

printf("L'utente %s ha ottenuto %d punti.\n", "Alice", 150);

In questo esempio, %s (segnaposto per stringhe) riceve "Alice", mentre %d (segnaposto per interi decimali) riceve 150.

Per questa prima fase del corso utilizzeremo quasi esclusivamente questi tre segnaposto fondamentali:

  • %d: Stampa o legge un intero (int) in base 10 con segno.
  • %f: Stampa un numero reale (double o float) in virgola fissa. Mostra di default 6 cifre decimali (es. 3.141593).

Mentre printf scrive verso l’esterno, la funzione scanf legge i dati digitati dall’utente sulla tastiera (dallo standard input stdin) e li memorizza nelle variabili del programma.

int age;
scanf("%d", &age);

Per utilizzare correttamente scanf dovete seguire tre regole ferree:

  1. Fornire il formato corretto: %d per interi, %lf per i double (attenzione: mentre in printf potete usare %f per i double, in scanf dovete usare obbligatoriamente %lf - long float - altrimenti la memoria verrà corrotta scrivendo 4 byte al posto di 8. Parleremo meglio di questi specificatori in un’altra lezione.).
  2. Fornire l’indirizzo con l’operatore &: scanf deve scrivere fisicamente nella memoria della variabile. Per farlo, ha bisogno dell’indirizzo fisico della cella RAM, ottenuto anteponendo l’operatore unario & (“indirizzo di”) al nome della variabile. Vedremo questi dettagli meglio in seguito. Per ora, possiamo pensare a & come ad un operatore che ci permette di accedere alla variabile in scrittura, mentre di norma l’accesso sarebbe solo in lettura.
  3. Consumo del Whitespace: scanf ignorerà automaticamente tutti gli spazi, tabulazioni e caratteri di invio (\n) iniziali fino a quando non incontrerà il primo carattere utile del tipo richiesto.

Esempio Pratico Completo: Calcolo dell’Area del Cerchio

Sezione intitolata “Esempio Pratico Completo: Calcolo dell’Area del Cerchio”

Ecco un programma C autosufficiente che potete compilare per vedere l’I/O in azione:

#include <stdio.h>
int main(void) {
double raggio = 0.0;
double area = 0.0;
double PI = 3.1415926535;
printf("Inserisci il raggio del cerchio (numero reale): ");
scanf("%lf", &raggio); // Chiede il raggio all'utente
area = PI * raggio * raggio;
printf("\n--- Risultati ---\n");
printf("Raggio inserito: %f cm\n", raggio);
printf("Area calcolata: %f cm²\n", area);
return 0;
}

Espressioni Aritmetiche, Precedenza ed Associatività

Sezione intitolata “Espressioni Aritmetiche, Precedenza ed Associatività”

Gli operatori si dividono in:

  • Unari: Lavorano su un solo operando (es. - unario per invertire il segno, come in -x).
  • Binari: Richiedono due operandi (es. +, -, *, /, %).

Per valutare espressioni complesse senza ambiguità, il compilatore C applica regole rigide di Precedenza (l’ordine di importanza degli operatori) e Associatività (la direzione in cui valutare gli operatori con la stessa priorità).

Un concetto cardine del C, spesso trascurato, è che qualsiasi espressione (inclusa una singola istruzione di assegnamento) produce e restituisce sempre un valore di output.

Ad esempio, l’espressione di assegnamento x = 5 non si limita a scrivere 5 nella memoria della variabile x, ma restituisce essa stessa il valore 5 come output. Questo consente ad esempio di concatenare gli assegnamenti come in y = x = 5 (che valuta x = 5 restituendo 5, che viene poi assegnato a y).

Ecco la tabella completa degli operatori principali con l’indicazione del loro valore restituito:

PrioritàOperatoreDescrizioneValore Restituito in OutputAssociatività
1 (Massima)+ - !Unari (Segno, NOT)Il valore numerico invertito o lo stato invertito (1/0)Da destra a sinistra
2 (Media)* / %Moltplicazione, Divisione, ModuloIl risultato matematico dell’operazioneDa sinistra a destra
3 (Bassa)+ -Somma, SottrazioneIl risultato matematico dell’operazioneDa sinistra a destra
4 (Minima)=Assegnamento sempliceIl valore che è stato scritto nella variabile a sinistraDa destra a sinistra

Consideriamo questa espressione complessa:

int x;
int y;
y = (x = 2 * 3) + 4;

Sotto il cofano, la CPU eseguirà le operazioni in questo preciso ordine rispettando le precedenze e le parentesi:

  1. Risolve l’espressione tra parentesi tonde più interna: x = 2 * 3.
  2. Esegue la moltiplicazione 2 * 3 che restituisce 6.
  3. Esegue l’assegnamento x = 6. Questa espressione scrive 6 in x e restituisce in output 6.
  4. Sostituisce l’intera parentesi (x = 2 * 3) con il suo valore di output 6. L’espressione ora diventa: y = 6 + 4.
  5. Esegue la somma 6 + 4 che restituisce 10.
  6. Esegue l’assegnamento finale scrivendo 10 in y.

Per scrivere codice più compatto ed efficiente, il C offre degli operatori di assegnamento composto:

  • x += 5; equivale a scrivere x = x + 5;
  • x *= 2; equivale a scrivere x = x * 2;
  • x -= 1; equivale a scrivere x = x - 1;

In C, il comportamento dell’operatore di divisione / è determinato unicamente dal tipo degli operandi:

  • Divisione Intera: Se entrambi gli operandi sono interi (int), C esegue un calcolo intero e tronca completamente la parte decimale senza effettuare alcun arrotondamento. Esempio: 5 / 2 restituisce 2 (la frazione .5 viene persa). Possiamo vedere questa divisione anche come il quoziente tra due numeri.
  • Divisione Reale: Se almeno uno degli operandi è un numero reale (ad esempio double), C esegue una divisione decimale completa. Esempio: 5.0 / 2 promuove implicitamente l’intero 2 a 2.0 ed esegue 5.0 / 2.0, restituendo 2.5.

L’operatore Modulo (%) restituisce invece il resto della divisione intera e può essere utilizzato solo tra interi. Esempio: 5 % 2 restituisce 1. Viene comunemente usato per verificare se un numero è pari (n % 2 == 0).


Nello standard C originale non esiste un tipo booleano puro nativo (sebbene standard moderni abbiano introdotto estensioni). Il C rappresenta i valori logici tramite interi:

  • 0 rappresenta Falso (False).
  • Qualsiasi valore diverso da 0 (solitamente 1) rappresenta Vero (True).

Gli operatori di confronto (relazionali) permettono di stabilire relazioni d’ordine o di uguaglianza tra espressioni aritmetiche, producendo come output 1 se la relazione è verificata (Vero) e 0 in caso contrario (Falso):

OperatoreSignificatoEsempioRisultato (se a=5, b=10)Nota di Attenzione
==Uguale aa == b0 (Falso)Da non confondere con l’assegnamento =!
!=Diverso daa != b1 (Vero)Restituisce vero se i valori non coincidono
>Maggiore dia > b0 (Falso)Confronto stretto
<Minore dia < b1 (Vero)Confronto stretto
>=Maggiore o uguale ab >= 101 (Vero)Vero se maggiore o esattamente identico
<=Minore o uguale aa <= 51 (Vero)Vero se minore o esattamente identico

Per combinare più relazioni e creare espressioni logiche complesse, si utilizzano gli operatori logici:

OperatoreNome LogicoDescrizioneSintassi CRegola di Output
&&AND (Congiunzione)Entrambe vereA && BRestituisce 1 solo se sia A che B sono diversi da zero.
``OR (Disgiunzione)Almeno una vera
!NOT (Negazione)Inversione logica!ARestituisce 1 se A vale zero, e 0 se A è diverso da zero.

Per comprendere appieno l’output prodotto dagli operatori logici, si fa riferimento alle tabelle di verità (dove 0 è Falso e 1 è Vero):

Operando AOperando BRisultato A && BRisultato A || BRisultato !A
0 (Falso)0 (Falso)001
0 (Falso)1 (Vero)011
1 (Vero)0 (Falso)010
1 (Vero)1 (Vero)110

Per deviare il flusso lineare del programma a seconda delle decisioni logiche, utilizziamo il costrutto if:

if (condizione) {
// Questo blocco viene eseguito solo se la condizione restituisce un valore Vero (diverso da 0)
}

Ecco un esempio pratico in cui usiamo variabili intere per memorizzare stati logici:

#include <stdio.h>
int main(void) {
int haPatente = 1; // 1 rappresenta Vero, 0 rappresenta Falso
int eta = 20;
// L'operatore logico && combina i due controlli
if (eta >= 18 && haPatente) {
printf("Autorizzato alla guida dell'autovettura.\n");
}
// Il NOT logico inverte la condizione
int pioggia = 1; // 1 rappresenta Vero (Piove)
if (!pioggia) {
printf("Puoi fare una passeggiata all'aperto.\n");
} else {
printf("Prendi l'ombrello!\n");
}
return 0;
}

La Logica Cortocircuitata (Short-Circuit Evaluation)

Sezione intitolata “La Logica Cortocircuitata (Short-Circuit Evaluation)”

Il compilatore C valuta le espressioni logiche composte da sinistra a destra ed arresta immediatamente la valutazione non appena il risultato finale è matematicamente certo:

  • Cortocircuito dell’AND (&&): Nell’espressione A && B, se A è Falso (0), l’intera espressione sarà sicuramente falsa indipendentemente da B. Pertanto, B non viene valutato.
  • Cortocircuito dell’OR (||): Nell’espressione A || B, se A è Vero (1), l’intera espressione sarà sicuramente vera indipendentemente da B. Pertanto, B non viene valutato.

Questo comportamento pigro è di vitale importanza pratica per proteggere il programma da crash a runtime. Permette infatti di effettuare un controllo di sicurezza preliminare prima di eseguire operazioni potenzialmente fatali:

int divisore = 0;
int dividendo = 10;
// Grazie al cortocircuito, poichè "divisore != 0" è Falso (0),
// la divisione per zero non viene eseguita e il programma non crasha.
if (divisore != 0 && (dividendo / divisore) > 2) {
printf("Condizione superata in sicurezza!\n");
}

Lo stesso principio si applica per evitare errori usando l’operatore OR (||): se la prima condizione è Vera (1), l’intera espressione è sicuramente vera, quindi la seconda parte non viene valutata. Questo è utile ad esempio per impostare un bypass o una scorciatoia di sicurezza:

int bypassaControlli = 1; // 1 rappresenta Vero
int divisore = 0;
int dividendo = 10;
// Grazie al cortocircuito, poichè "bypassaControlli" è Vero (1),
// l'intera condizione dell'if è già soddisfatta.
// La CPU non esegue la seconda parte, evitando la divisione per zero e il crash!
if (bypassaControlli || (dividendo / divisore) > 2) {
printf("Accesso consentito (controlli bypassati o superati).\n");
}

Se invertissimo l’ordine della condizione (if ((dividendo / divisore) > 2 || bypassaControlli)), la CPU proverebbe immediatamente ad eseguire la divisione per zero, provocando l’arresto immediato del programma con un errore di tipo Floating point exception.


Mettetevi alla prova con questo test rapido di 10 domande per verificare la vostra comprensione dei concetti di questa lezione. Cliccate su ciascuna domanda per verificare la risposta corretta e la spiegazione.

1. Sotto il cofano del calcolatore, che cos’è fisicamente una variabile?

  • A) Una lettera algebrica salvata nel compilatore.
  • B) Una denominazione simbolica associata a un indirizzo e ad un’area fisica della memoria RAM.
  • C) Un file temporaneo memorizzato sull’hard disk.
  • D) Un’istruzione in linguaggio macchina per eseguire una somma.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Le variabili sono astrazioni create per lo sviluppatore umano; durante la compilazione vengono tradotte direttamente in indirizzi fisici o scostamenti della memoria RAM (solitamente nello Stack).

2. Quale delle seguenti affermazioni su una variabile locale non inizializzata è corretta?

  • A) C assegna automaticamente il valore 0 ad essa.
  • B) La compilazione fallisce immediatamente.
  • C) Contiene un valore spazzatura (garbage value) indeterminato dovuto allo stato precedente dei bit in RAM.
  • D) Viene eliminata automaticamente dal sistema operativo all’avvio.
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: La dichiarazione riserva l’area di memoria ma non la azzera. Leggere una variabile non inizializzata produce comportamenti imprecisati basati sui bit residui in quella cella.

3. Perché il linguaggio C richiede la dichiarazione esplicita del tipo per ogni variabile?

  • A) Per consentire al compilatore di allocare la corretta dimensione in byte e generare istruzioni CPU specifiche ed efficienti.
  • B) Per rendere il codice leggibile solo da programmatori esperti.
  • C) Perché la memoria RAM gestisce solo tipi interi.
  • D) Per consentire al linker di unire i file sorgente in un unico file eseguibile.
▶ Mostra Risposta Corretta

Risposta corretta: A

Spiegazione: Conoscere il tipo permette al compilatore di allocare lo spazio esatto (es. 4 byte per int, 8 byte per double) e di usare i registri e le istruzioni CPU appropriate (somma intera vs somma reale).

4. Nell’istruzione x = y + 3;, quali elementi rappresentano rispettivamente un lvalue e un rvalue?

  • A) x è un lvalue; y + 3 è un rvalue.
  • B) x è un rvalue; y + 3 è un lvalue.
  • C) Entrambi sono lvalue perché contengono variabili.
  • D) Entrambi sono rvalue perché fanno parte di un calcolo aritmetico.
▶ Mostra Risposta Corretta

Risposta corretta: A

Spiegazione: Un lvalue indica una locazione di memoria fisica persistente in cui è possibile scrivere (la variabile x), mentre un rvalue è un valore calcolato temporaneo (y + 3) che non ha un indirizzo persistente accessibile.

5. Cosa succede se si usa lo specificatore %f all’interno della funzione scanf per leggere un numero di tipo double?

  • A) Il numero viene letto correttamente ma approssimato all’intero più vicino.
  • B) Si verifica una corruzione di memoria, poiché scanf si aspetta %lf per scrivere 8 byte anziché 4.
  • C) La compilazione fallisce con un errore di sintassi.
  • D) La variabile viene allocata nello Heap invece che sullo Stack.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: In scanf, lo specificatore %f è destinato ai dati di tipo float (4 byte). Per memorizzare correttamente un double (8 byte) è obbligatorio usare %lf.

6. Quanto restituisce l’espressione aritmetica 5 / 2 in C?

  • A) 2.5
  • B) 2
  • C) 3
  • D) 0
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Poiché entrambi gli operandi (5 e 2) sono interi (int), C esegue una divisione intera e tronca completamente la parte frazionaria.

7. Come si può ottenere una divisione decimale corretta (che restituisca 2.5) a partire dai numeri 5 e 2?

  • A) Scrivendo 5.0 / 2 o 5 / 2.0.
  • B) Utilizzando l’operatore modulo %.
  • C) Memorizzando il risultato in una variabile double senza modificare i due operandi interi.
  • D) Modificando il codice di ritorno del programma main con return 2.5;.
▶ Mostra Risposta Corretta

Risposta corretta: A

Spiegazione: Se almeno uno degli operandi è un letterale reale (5.0), l’altro intero viene promosso implicitamente a reale prima dell’operazione, forzando una divisione in virgola mobile.

8. In C, come viene rappresentato internamente il valore logico Falso (False)?

  • A) Tramite la parola chiave false inclusa di default nel compilatore.
  • B) Esclusivamente dal valore intero 0.
  • C) Da qualsiasi valore minore di 0.
  • D) Dalla presenza di un valore spazzatura (garbage value).
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Per il linguaggio C nativo, lo 0 logico rappresenta il Falso, mentre qualsiasi valore diverso da zero (positivo o negativo) è interpretato come Vero.

9. Nel frammento if (a != 0 && b / a > 1), cosa succede se la variabile a è uguale a 0?

  • A) Il programma crasha immediatamente a causa di una divisione per zero.
  • B) La condizione restituisce Vero perché la seconda parte non è controllata.
  • C) La seconda parte della condizione non viene valutata grazie alla logica di cortocircuito dell’operatore &&.
  • D) Il valore di b viene azzerato in automatico dalla CPU.
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Poiché il primo termine a != 0 valuta a Falso (0), il risultato globale di un AND (&&) è matematicamente certo di essere Falso. La CPU arresta la valutazione evitando il crash dovuto alla divisione b / a.

10. Qual è il comportamento dell’operatore logico di cortocircuito OR (||)?

  • A) Valuta sempre entrambi gli operandi per sicurezza.
  • B) Se il primo operando è Vero (1), non valuta il secondo perché l’espressione è sicuramente Vera.
  • C) Inverte sempre il risultato finale.
  • D) Restituisce errore se uno dei due operandi è uguale a zero.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Per l’operatore || basta che un solo operando sia vero affinché l’espressione totale sia vera. Se il primo termine è non-nullo, la CPU ottimizza saltando la valutazione del secondo.


Mettete in pratica i concetti studiati provando a risolvere questi esercizi. Le soluzioni in codice C sono nascoste all’interno dei relativi pannelli.

Scrivete un programma in C che richieda all’utente l’importo totale di un conto e la percentuale di mancia da lasciare. Calcolate e stampate l’importo della mancia e il conto totale (conto + mancia) formattati con due cifre decimali.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
double conto = 0.0;
double percentualeMancia = 0.0;
double mancia = 0.0;
double totale = 0.0;
printf("Inserisci l'importo del conto (€): ");
scanf("%lf", &conto);
printf("Inserisci la percentuale di mancia (es. 15): ");
scanf("%lf", &percentualeMancia);
mancia = conto * (percentualeMancia / 100.0);
totale = conto + mancia;
printf("\n--- Riepilogo Spesa ---\n");
printf("Conto originale: %.2f €\n", conto);
printf("Mancia calcolata (%.1f%%): %.2f €\n", percentualeMancia, mancia);
printf("Conto totale da pagare: %.2f €\n", totale);
return 0;
}

Spiegazione dell’algoritmo:

  1. Dichiarazione delle variabili di tipo double per mantenere la precisione decimale dell’importo.
  2. Input tramite scanf con il formato %lf.
  3. Calcolo aritmetico dividendo la percentuale per 100.0 (forzando calcolo reale) e sommandolo al totale.
  4. Visualizzazione formattata tramite %.2f per mostrare solo i centesimi.

Scrivete un programma in C che legga due numeri interi (chiamati numeratore e denominatore) e determini se il primo è divisibile per il secondo (ovvero se il resto è nullo). Stampate a schermo 1 se è divisibile, 0 altrimenti. Vincolo: non usate l’istruzione if o l’operatore ternario.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
int numeratore = 0;
int denominatore = 0;
printf("Inserisci il numeratore (intero): ");
scanf("%d", &numeratore);
printf("Inserisci il denominatore (intero): ");
scanf("%d", &denominatore);
// Verifichiamo prima se il denominatore è diverso da zero per evitare crash.
// Sfruttiamo il cortocircuito logico: se il denominatore è 0,
// l'espressione si ferma restituendo 0 (Falso).
int divisibile = (denominatore != 0) && (numeratore % denominatore == 0);
printf("Risultato divisibilita': %d\n", divisibile);
return 0;
}

Spiegazione dell’algoritmo:

  1. Leggiamo i due interi con %d.
  2. L’espressione logica (denominatore != 0) && (numeratore % denominatore == 0) garantisce la sicurezza: se denominatore è zero, il programma non valuta l’operatore modulo % (che su divisione per zero provocherebbe crash).
  3. L’operatore logico AND && restituisce direttamente 1 (Vero) se la condizione è rispettata, altrimenti restituisce 0 (Falso).

Esercizio 3: Calcolo dell’Idoneità (Soglia di Punteggio)

Sezione intitolata “Esercizio 3: Calcolo dell’Idoneità (Soglia di Punteggio)”

Scrivete un programma in C che legga due punteggi interi (es. prova scritta e prova pratica, ciascuna con valore da 0 a 15). Il programma deve determinare se il candidato ha ottenuto l’idoneità. L’idoneità si ottiene solo se la somma dei due punteggi è maggiore o uguale a 18. Stampate 1 in caso di successo (idoneo), 0 altrimenti. Vincolo: non utilizzate il costrutto else o else if (non ancora formalmente introdotti).

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
int scritto = 0;
int pratico = 0;
int somma = 0;
int idoneo = 0;
printf("Inserisci il punteggio dello scritto (0-15): ");
scanf("%d", &scritto);
printf("Inserisci il punteggio del pratico (0-15): ");
scanf("%d", &pratico);
somma = scritto + pratico;
// Utilizziamo solo istruzioni if semplici basate su logica di confronto.
// Inizializziamo idoneo a 0, e lo cambiamo a 1 solo se la soglia è raggiunta.
if (somma >= 18) {
idoneo = 1;
}
printf("Stato idoneita': %d\n", idoneo);
return 0;
}

Spiegazione dell’algoritmo:

  1. Leggiamo i due punteggi interi tramite scanf.
  2. Calcoliamo la somma algebrica delle prove.
  3. Utilizziamo una variabile di stato idoneo inizializzata a 0 (Falso). Un singolo controllo if lineare verifica se somma >= 18: in caso affermativo, la variabile viene aggiornata a 1.

Esercizio 4: Scambio di Valori Senza Variabile Temporanea

Sezione intitolata “Esercizio 4: Scambio di Valori Senza Variabile Temporanea”

Scrivete un programma in C che legga due numeri interi da tastiera e li memorizzi nelle variabili a e b. Successivamente, scambiate i valori contenuti nelle due variabili senza utilizzare una terza variabile di supporto (temporanea). Stampate a schermo i valori delle variabili prima e dopo lo scambio. Suggerimento: utilizzate operazioni algebriche di addizione e sottrazione.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
int a = 0;
int b = 0;
printf("Inserisci il valore di a: ");
scanf("%d", &a);
printf("Inserisci il valore di b: ");
scanf("%d", &b);
printf("\nPrima dello scambio: a = %d, b = %d\n", a, b);
// Algoritmo di scambio algebrico senza variabile temporanea
a = a + b; // a ora contiene la somma dei due valori
b = a - b; // b diventa la differenza (somma - b iniziale) -> assume il valore originale di a
a = a - b; // a diventa la differenza (somma - a iniziale) -> assume il valore originale di b
printf("Dopo lo scambio: a = %d, b = %d\n", a, b);
return 0;
}

Spiegazione dell’algoritmo:

  1. La prima istruzione memorizza la somma dei due numeri in a.
  2. Sottraendo il valore iniziale b dalla nuova somma a (eseguito con a - b), si ottiene esattamente il valore originario di a, che viene memorizzato in b.
  3. Infine, sottraendo il nuovo valore di b (che è il vecchio a) dalla somma a (eseguito con a - b), si isola il valore originario di b, che viene memorizzato in a.

Esercizio 5: Calcolatore di Tariffe Orarie (Bassa/Alta Stagione)

Sezione intitolata “Esercizio 5: Calcolatore di Tariffe Orarie (Bassa/Alta Stagione)”

Un parcheggio applica una tariffa base di 2.50 € all’ora. Tuttavia, se l’utente ha diritto ad una tariffa promozionale (rappresentata da una variabile intera haPromozione che vale 1) ed è un giorno feriale (rappresentato da una variabile giornoFeriale che vale 1), la tariffa viene ridotta del 20%. Scrivete un programma che calcoli e stampi il costo totale del parcheggio in base al numero di ore inserite dall’utente. Vincolo: non utilizzate il costrutto else o else if.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
double ore = 0.0;
int haPromozione = 0;
int giornoFeriale = 0;
double tariffaOraria = 2.50; // Tariffa base di partenza
double totale = 0.0;
printf("Inserisci il numero di ore di sosta: ");
scanf("%lf", &ore);
printf("Hai diritto alla promozione? (1 per Si, 0 per No): ");
scanf("%d", &haPromozione);
printf("E' un giorno feriale? (1 per Si, 0 per No): ");
scanf("%d", &giornoFeriale);
// Applichiamo lo sconto del 20% solo se entrambe le condizioni sono vere.
// L'operatore logico && valuta la contemporaneità degli eventi.
if (haPromozione && giornoFeriale) {
tariffaOraria = tariffaOraria * 0.80; // Sconto del 20%
}
totale = ore * tariffaOraria;
printf("\nTariffa oraria applicata: %.2f €/h\n", tariffaOraria);
printf("Totale da pagare: %.2f €\n", totale);
return 0;
}

Spiegazione dell’algoritmo:

  1. Dichiarazione ed inizializzazione delle variabili. La tariffa di base è impostata su 2.50 a runtime.
  2. Acquisizione degli stati logicizzati dell’utente tramite interi (1/0).
  3. L’espressione logica haPromozione && giornoFeriale controlla se entrambe le condizioni sono verificate. In caso positivo, il blocco if aggiorna la tariffa applicando lo sconto (tariffaOraria * 0.80).
  4. Calcolo finale moltiplicando le ore per la tariffa calcolata in precedenza.

Scrivete un programma in C che legga un numero intero da tastiera, lo moltiplichi per 3, vi sommi 10 ed infine stampi il risultato. Vincolo: per eseguire le operazioni utilizzate esclusivamente gli operatori di assegnamento composto (*=, +=).

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
int numero = 0;
printf("Inserisci un numero intero: ");
scanf("%d", &numero);
// Eseguiamo i calcoli usando solo operatori composti
numero *= 3; // Equivale a: numero = numero * 3;
numero += 10; // Equivale a: numero = numero + 10;
printf("Risultato finale: %d\n", numero);
return 0;
}

Spiegazione dell’algoritmo:

  1. Acquisiamo l’input con %d.
  2. Applichiamo la moltiplicazione per 3 e la memorizziamo direttamente su numero con l’operatore composto *=.
  3. Sommiamo 10 a numero memorizzando il risultato su se stessa usando +=.

Scrivete un programma in C che legga una temperatura espressa in gradi Celsius (numero reale) e la converta in gradi Fahrenheit usando la formula: F=C×95+32F = C \times \frac{9}{5} + 32 Vincolo: assicuratevi che la divisione 95\frac{9}{5} non venga troncata a causa della divisione intera.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
double celsius = 0.0;
double fahrenheit = 0.0;
printf("Inserisci la temperatura in gradi Celsius: ");
scanf("%lf", &celsius);
// Usiamo 9.0 / 5.0 (divisione reale) anziché 9 / 5 (divisione intera)
fahrenheit = celsius * (9.0 / 5.0) + 32.0;
printf("Temperatura in Fahrenheit: %.2f °F\n", fahrenheit);
return 0;
}

Spiegazione dell’algoritmo:

  1. Dichiarazione delle variabili di tipo double per gestire i decimali.
  2. Calcolo della conversione. Usare 9.0 / 5.0 (o semplicemente 1.8) previene l’errore classico di divisione intera 9 / 5, che in C verrebbe troncato a 1.

Esercizio 8: La trappola dell’assegnamento nel confronto

Sezione intitolata “Esercizio 8: La trappola dell’assegnamento nel confronto”

Scrivete un programma che inizializzi una variabile intera stato a 0. Scrivete una istruzione if che controlli se lo stato è attivo (uguale a 1). Nel blocco della condizione inserite erroneamente l’operatore di assegnamento = al posto di == (es. if (stato = 1)). Eseguite il programma e spiegate perché il blocco condizionale viene eseguito nonostante il valore iniziale fosse 0.

💻 Mostra Soluzione e Spiegazione

Codice sorgente assegnamento_errato.c:

#include <stdio.h>
int main(void) {
int stato = 0;
// Errore comune: usiamo '=' invece di '=='
if (stato = 1) {
printf("Il blocco viene eseguito! Stato attuale: %d\n", stato);
}
return 0;
}

Spiegazione:

  1. L’espressione stato = 1 non effettua un confronto, ma assegna il valore 1 alla variabile stato.
  2. In C, il risultato di un’operazione di assegnamento corrisponde al valore stesso assegnato (in questo caso 1).
  3. Essendo 1 un valore diverso da zero, viene interpretato logicamente come Vero (True). Pertanto, la condizione dell’if è soddisfatta ed il blocco viene eseguito.

Scrivete un programma in C che legga da tastiera un numero intero positivo che rappresenta una quantità di secondi. Calcolate e stampate a schermo il numero equivalente espresso in Ore, Minuti e Secondi. Suggerimento: utilizzate gli operatori divisione intera / e modulo %.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
int secondiTotali = 0;
int ore = 0;
int minuti = 0;
int secondiResidui = 0;
printf("Inserisci la quantita' di secondi: ");
scanf("%d", &secondiTotali);
// Calcoliamo le ore dividendo i secondi per 3600 (secondi in un'ora)
ore = secondiTotali / 3600;
// Calcoliamo quanti secondi rimangono dopo aver tolto le ore intere
secondiResidui = secondiTotali % 3600;
// Calcoliamo i minuti dai secondi residui dividendo per 60
minuti = secondiResidui / 60;
// I secondi finali sono il resto dell'operazione precedente
secondiResidui = secondiResidui % 60;
printf("%d secondi equivalgono a: %d h, %d m, %d s\n", secondiTotali, ore, minuti, secondiResidui);
return 0;
}

Spiegazione dell’algoritmo:

  1. Dividiamo per 3600 (numero di secondi in 1 ora) ottenendo le ore intere per troncamento.
  2. Calcoliamo il resto dei secondi esclusi con % 3600.
  3. Dividiamo la quantità restante per 60 per ottenere i minuti ed usiamo l’operatore modulo per isolare i secondi finali.

Esercizio 10: Calcolo di Sicurezza del Logaritmo (Bozza Logica)

Sezione intitolata “Esercizio 10: Calcolo di Sicurezza del Logaritmo (Bozza Logica)”

Il calcolo del logaritmo naturale di un numero reale xx (in matematica espresso come ln⁡(x)\ln(x)) è definito solo per numeri strettamente maggiori di zero (x>0x > 0). Scrivete un programma in C che acquisisca un numero double x da tastiera e, utilizzando gli operatori logici e il cortocircuito, memorizzi in una variabile valido il valore 1 se è possibile calcolare il logaritmo in sicurezza, 0 altrimenti. Vincolo: non utilizzate il costrutto if o else.

💻 Mostra Soluzione e Codice C
#include <stdio.h>
int main(void) {
double x = 0.0;
printf("Inserisci il valore reale x: ");
scanf("%lf", &x);
// L'espressione relazionale restituisce 1 se vero, 0 se falso.
// Viene valutata in sicurezza.
int valido = (x > 0.0);
printf("E' possibile calcolare il logaritmo? (1=Si, 0=No): %d\n", valido);
return 0;
}

Spiegazione dell’algoritmo: In C le espressioni di confronto (come x > 0.0) producono un valore numerico pari a 1 se la relazione d’ordine è verificata o 0 in caso contrario. Assegniamo questo valore direttamente alla variabile valido, senza usare blocchi condizionali.