Przez pierwsze 27 lat C nie miało typu logicznego. To nie przeoczenie, tylko decyzja projektowa z konsekwencjami, na które trafiasz pierwszego dnia, oraz jeden słynny błąd, który z niej bezpośrednio wynika.
Zero to fałsz, wszystko inne to prawda
W regułach warunków w C nie ma specjalnej wartości prawda/fałsz. if, while, for, &&, || i ! zadają dokładnie jedno pytanie: czy ta wartość jest zerem?
Liczby ujemne są prawdziwe. Ułamki są prawdziwe. Niepusty wskaźnik jest prawdziwy. Fałszywe jest tylko zero, w dowolnym typie.
Dlatego kod w C jest pełen idiomów, które wyglądają zwięźle, dopóki nie znasz tej reguły:
if (count) /* jeśli count nie jest zerem */
if (!count) /* jeśli count jest zerem */
if (p) /* jeśli p nie jest NULL */
if (!strcmp(a, b)) /* jeśli stringi są równe: strcmp zwraca 0 przy zgodności */
Ten ostatni łapie wiele osób. strcmp zwraca 0, gdy stringi są równe, więc w if "równe" czyta się jak "nie". Zapis if (strcmp(a, b) == 0) jest czytelniejszy i robi dokładnie to samo.
Porównania zwracają int
Porównanie w C nie jest specjalnym rodzajem wyrażenia. Daje zwykły int o wartości 1 albo 0, który możesz wypisać, zapisać albo użyć w obliczeniach:
Operator ! działa tak samo: zwraca 1, jeśli jego argument jest zerem, a 0 w przeciwnym razie. Dlatego !!x to klasyczny idiom, który sprowadza dowolną wartość do dokładnie 0 albo 1.
bool, true i false (C99)
C99 dodało prawdziwy typ logiczny o nazwie _Bool oraz nagłówek, który nadaje mu czytelną nazwę:
<stdbool.h> jest malutki: definiuje bool jako _Bool, true jako 1 i false jako 0. To cały nagłówek. Ale jego użycie pokazuje intencję w sposób, w jaki int flag = 1; nigdy tego nie zrobi.
_Bool ma jedno naprawdę odrębne zachowanie: przechowuje tylko 0 albo 1. Przypisanie dowolnej niezerowej wartości zamienia ją na dokładnie 1:
W printf nadal nie ma %b dla wartości logicznych: wypisuj je przez %d albo samodzielnie zamieniaj je na tekst:
W C23 jest jeszcze prościej: bool, true i false stały się prawdziwymi słowami kluczowymi, więc dołączanie nagłówka nie jest już potrzebne. Większość dzisiejszego kodu wciąż celuje w C17, więc zostaw #include <stdbool.h>.
Zwracanie wartości logicznej z funkcji
Tu bool najwyraźniej zasługuje na swoje miejsce:
Typ zwracany bool mówi wywołującemu, co oznacza wartość. Typ int zwracany przez funkcję o nazwie check_file może być wartością logiczną, licznikiem albo kodem błędu, a w konwencjach biblioteki C często jest tym trzecim, gdzie 0 oznacza sukces. Nazwanie typu usuwa zgadywanie.
Błąd = zamiast ==
Ponieważ przypisanie jest wyrażeniem, które zwraca przypisaną wartość, a każda wartość niezerowa jest prawdą, to się kompiluje:
Dwie rzeczy poszły źle jednocześnie. Warunek przypisał 5 do x, niszcząc jego wartość, a potem sprawdził 5, które jest niezerowe, więc gałąź się wykonała. Domyślnie program nie ma ani błędu, ani ostrzeżenia.
Co gorsza, wersja z 0 nigdy nie wykonuje swojej gałęzi:
if (found = 0) { /* nigdy nie wykonana, a found ma teraz wartość 0 */ }
Trzy sposoby obrony, w kolejności przydatności:
Kompiluj z ostrzeżeniami. -Wall to wyłapie:
warning: suggest parentheses around assignment used as truth value
Dojście do zera ostrzeżeń to tutaj prawdziwe rozwiązanie, a kosztuje jedną flagę.
Pisz stałą jako pierwszą. "Warunek Yody" zamienia literówkę w błąd kompilacji:
if (5 == x) /* poprawnie */
if (5 = x) /* błąd: to nie lvalue, kompilator cię zatrzyma */
Niektóre zespoły to uwielbiają, a inne uważają za nieczytelne; obie postawy da się obronić, ale to działa.
Rób to świadomie, gdy naprawdę tego chcesz. Przypisanie w warunku jest naprawdę przydatne przy czytaniu wejścia:
int c;
while ((c = getchar()) != EOF) { ... }
Dodatkowe nawiasy wokół c = getchar() są wymagane ze względu na priorytet operatorów, a przy okazji sygnalizują, że przypisanie jest zamierzone. To jest ten idiom: jeśli twoje przypisanie w warunku tak nie wygląda, to prawdopodobnie literówka.
Częste błędy z wartościami logicznymi
Porównywanie z true. if (flag == true) działa ze stdbool.h, ale psuje się w chwili, gdy flag jest int o wartości 42, bo 42 == 1 jest fałszem. Pisz if (flag).
Łańcuchowe porównania. if (0 < x < 10) jest zawsze prawdziwe: 0 < x daje 0 albo 1, a oba są mniejsze od 10. Pisz if (x > 0 && x < 10).
Operatory bitowe zamiast logicznych. & i | to nie && i ||. Nie działają na zasadzie krótkiego spięcia i łączą bity:
Oba argumenty są "prawdziwe", a mimo to a & b wynosi 0. Użycie & tam, gdzie chodziło o &&, daje warunek, który jest błędny tylko dla niektórych wartości, a to najtrudniejszy do znalezienia rodzaj błędu.
Porównywanie liczb zmiennoprzecinkowych na równość. if (0.1 + 0.2 == 0.3) jest fałszem. Zamiast tego porównuj z tolerancją; dlaczego, wyjaśnia artykuł o typach danych.
Najczęściej zadawane pytania
Czy C ma typ logiczny?
Od C99 tak: _Bool jest typem wbudowanym, a dołączenie <stdbool.h> daje przyjaźniejsze nazwy bool, true i false. Przed C99 nie było żadnego typu logicznego: programy używały int z 0 jako fałszem i 1 jako prawdą, co nadal działa i wciąż jest popularne.
Co jest prawdą w C?
Każda wartość różna od zera. 1, -5, 0.01 i wskaźnik różny od NULL są w warunku prawdziwe; fałszywe są tylko 0, 0.0 i wskaźnik pusty. Nie ma osobnego sprawdzenia logicznego: if (x) po prostu sprawdza, czy x jest różne od zera.
Jak używać bool w C?
Dodaj #include <stdbool.h> i wtedy bool ready = true; działa tak, jak się spodziewasz. Pod spodem bool to _Bool, który przechowuje tylko 0 albo 1: przypisanie dowolnej niezerowej wartości zapisuje dokładnie 1.
Jaka jest różnica między = a == w C?
= przypisuje, == porównuje. if (x = 5) przypisuje 5 do x, a potem sprawdza 5, które jest niezerowe, więc gałąź wykonuje się zawsze. if (x == 5) sprawdza równość. Pierwsza wersja kompiluje się bez błędu, dlatego istnieje zarówno -Wall, jak i nawyk pisania if (5 == x).