Menu

Booleani in C: stdbool.h, _Bool e valori di verità

C non ha avuto un tipo booleano fino a C99. Zero è falso, tutto il resto è vero, i confronti restituiscono int e stdbool.h aggiunge bool, true e false. In più il bug = vs == che questa scelta rende possibile.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

C ha passato i suoi primi 27 anni senza un tipo booleano. Non è una svista: è una scelta di progettazione con conseguenze che incontri fin dal primo giorno, e da cui deriva direttamente un bug famoso.

Zero è falso, tutto il resto è vero

Le regole delle condizioni in C non prevedono alcun valore speciale vero/falso. if, while, for, &&, || e ! fanno tutti una sola domanda: questo valore è zero?

I numeri negativi sono veri. Le frazioni sono vere. Un puntatore non nullo è vero. Solo lo zero, di qualsiasi tipo, è falso.

Ecco perché il codice C è pieno di idiomi che sembrano criptici finché non conosci la regola:

if (count)            /* se count non è zero */
if (!count)           /* se count è zero */
if (p)                /* se p non è NULL */
if (!strcmp(a, b))    /* se le stringhe sono uguali: strcmp restituisce 0 se coincidono */

L'ultimo trae in inganno molte persone. strcmp restituisce 0 quando le stringhe coincidono, quindi in un if "uguali" si legge come una negazione. Scrivere if (strcmp(a, b) == 0) è più chiaro e fa esattamente la stessa cosa.

I confronti producono int

Un confronto in C non è un tipo speciale di espressione. Viene valutato come un normale int con valore 1 o 0, e puoi stamparlo, memorizzarlo o usarlo nei calcoli:

L'operatore ! funziona allo stesso modo: produce 1 se il suo operando è zero e 0 altrimenti. Quindi !!x è un idioma classico che normalizza qualsiasi valore a esattamente 0 o 1.

bool, true e false (C99)

C99 ha aggiunto un vero tipo booleano chiamato _Bool e un header che gli dà un nome leggibile:

<stdbool.h> è minuscolo: definisce bool come _Bool, true come 1 e false come 0. Tutto l'header è questo. Ma usarlo rende visibile l'intenzione in un modo in cui int flag = 1; non riesce mai a fare.

_Bool ha un comportamento davvero distinto: memorizza solo 0 o 1. Qualsiasi valore diverso da zero assegnato viene convertito esattamente in 1:

In printf non esiste ancora un %b per i booleani: stampali con %d oppure convertili tu in testo:

In C23 è diventato ancora più semplice: bool, true e false sono diventate vere parole chiave, quindi l'include non è più necessario. La maggior parte del codice oggi punta ancora a C17, quindi tieni #include <stdbool.h>.

Restituire un booleano da una funzione

È qui che bool si guadagna il suo posto nel modo più evidente:

Un tipo di ritorno bool dice a chi chiama cosa significa il valore. Un tipo di ritorno int di una funzione chiamata check_file potrebbe essere un booleano, un conteggio o un codice di errore, e secondo le convenzioni delle librerie C spesso è il terzo, dove 0 significa successo. Dare un nome al tipo elimina l'incertezza.

Il bug = vs ==

Poiché l'assegnazione è un'espressione che restituisce il valore assegnato, e poiché qualsiasi valore diverso da zero è vero, questo compila:

Sono andate storte due cose insieme. La condizione ha assegnato 5 a x, distruggendone il valore, e poi ha verificato 5, che è diverso da zero, quindi il ramo è stato eseguito. Il programma non ha errori e, di default, nemmeno avvisi.

Peggio ancora, la versione con 0 non esegue mai il suo ramo:

if (found = 0) { /* mai eseguito, e ora found vale 0 */ }

Tre difese, in ordine di utilità:

Compila con gli avvisi attivi. -Wall lo individua:

warning: suggest parentheses around assignment used as truth value

Arrivare a zero avvisi è la vera soluzione, e costa un solo flag.

Scrivi prima la costante. La "condizione Yoda" trasforma il refuso in un errore di compilazione:

if (5 == x)   /* corretto */
if (5 = x)    /* errore: non è un lvalue, il compilatore ti ferma */

Alcuni team la adorano e altri la trovano illeggibile; entrambe le posizioni sono difendibili, ma funziona.

Sii intenzionale quando lo vuoi davvero. Un'assegnazione dentro una condizione è davvero utile per leggere l'input:

int c;
while ((c = getchar()) != EOF) { ... }

Le parentesi in più attorno a c = getchar() sono necessarie per la precedenza e segnalano anche che l'assegnazione è voluta. Questo è l'idioma: se la tua assegnazione in una condizione non ha questo aspetto, probabilmente è un refuso.

Errori comuni con i booleani

Confrontare con true. if (flag == true) funziona con stdbool.h, ma si rompe appena flag è un int che contiene 42, perché 42 == 1 è falso. Scrivi if (flag).

Confronti concatenati. if (0 < x < 10) è sempre vero: 0 < x restituisce 0 o 1, ed entrambi sono minori di 10. Scrivi if (x > 0 && x < 10).

Operatori bit a bit al posto di quelli logici. & e | non sono && e ||. Non fanno valutazione a cortocircuito e combinano i bit:

Entrambi gli operandi sono "veri", eppure a & b vale 0. Usare & dove intendevi && produce una condizione sbagliata solo per certi valori, il tipo di bug più difficile da trovare.

Confrontare float per uguaglianza. if (0.1 + 0.2 == 0.3) è falso. Confronta invece con una tolleranza: vedi i tipi di dati per capire perché.

Domande frequenti

C ha un tipo booleano?

Da C99 sì: _Bool è un tipo predefinito e includendo <stdbool.h> ottieni i nomi più leggibili bool, true e false. Prima di C99 non esisteva alcun booleano: i programmi usavano int con 0 per falso e 1 per vero, cosa che funziona ancora ed è ancora comune.

Cosa conta come vero in C?

Qualsiasi valore diverso da zero. 1, -5, 0.01 e un puntatore non nullo sono tutti veri in una condizione; solo 0, 0.0 e un puntatore nullo sono falsi. Non esiste un controllo booleano separato: if (x) verifica semplicemente se x è diverso da zero.

Come si usa bool in C?

Aggiungi #include <stdbool.h> e poi bool ready = true; funziona come ti aspetti. Dietro le quinte bool è _Bool, che memorizza solo 0 o 1: assegnargli qualsiasi valore diverso da zero memorizza esattamente 1.

Qual è la differenza tra = e == in C?

= assegna, == confronta. if (x = 5) assegna 5 a x e poi verifica 5, che è diverso da zero, quindi il ramo viene eseguito sempre. if (x == 5) verifica l'uguaglianza. La prima forma compila senza errori, ed è per questo che esistono sia -Wall sia l'abitudine di scrivere if (5 == x).

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA