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).