Why this matters
Programs make decisions: is the input valid, is the file open, has the loop reached the end, is the user allowed in. Every decision is an expression that is true or false. C's way of representing truth is unusually bare (it is just an int), and its logical operators have a property, short-circuit evaluation, that is used everywhere in real code to guard against crashes. Getting comfortable with these expressions now makes if and loops, in the next module, straightforward.
Relational and equality operators
| operator | meaning |
|---|---|
< | less than |
> | greater than |
<= | less than or equal |
>= | greater than or equal |
== | equal (two equals signs) |
!= | not equal |
Each produces an int: 1 if the comparison holds, 0 if not. There is no separate boolean value underneath; the CPU compares, sets a flag, and the compiler turns that flag into 0 or 1.
int a = 5, b = 3;
printf("%d %d %d %d %d %d\n", a < b, a > b, a == b, a != b, a <= 5, b >= 4);
0 1 0 1 1 0
The usual arithmetic conversions from the previous lesson apply before comparing, which is how -1 < 0u comes out false. Comparing doubles with == has the rounding problems you met in the floating-point lesson.
Truth in C
Where C needs a condition (in if, while, &&, !...), it treats zero as false and any nonzero value as true. 1, -1, 42 and 'A' are all true. This is why you will see code like if (count) meaning "if count is not zero".
Since C99 there is also a real boolean type. Include <stdbool.h> to get bool, true and false:
#include <stdbool.h>
bool done = true;
printf("done = %d, sizeof(bool) = %zu\n", done, sizeof(bool));
done = 1, sizeof(bool) = 1
A bool can only hold 0 or 1: storing any nonzero value into it gives 1. Use it for variables and function results that are conceptually yes/no; it documents intent. (C23 makes bool, true and false keywords, so the include becomes unnecessary.)
Logical operators
a && b(AND) is 1 if both operands are true (nonzero), else 0.a || b(OR) is 1 if at least one operand is true, else 0.!a(NOT) is 1 ifais 0, else 0.
printf("%d %d %d\n", !0, !7, !!7); /* 1 0 1 */
printf("%d %d\n", 3 && -1, 0 || 0); /* 1 0 */
!!x is an idiom that turns any value into exactly 0 or 1.
Testing whether a value lies in a range needs two comparisons joined with &&:
int age = 15;
int teen = 13 <= age && age <= 19; /* 1 */
Short-circuit evaluation
&& and || evaluate their left operand first, and evaluate the right operand only if needed:
- For
a && b: ifais false, the result is 0 without evaluatingb. - For
a || b: ifais true, the result is 1 without evaluatingb.
x != 0 && 10 / x > 1
------- ----------
evaluated evaluated only if the left side was true (nonzero)
first
This guarantee is what makes guard expressions safe:
int x = 0;
int ok = x != 0 && 10 / x > 1;
printf("ok = %d\n", ok);
ok = 0
No division by zero happens; the left side was false so the right side never ran. You will use this pattern constantly: "the pointer is not null and the thing it points to is valid", "the index is in range and the element matches", "the file opened or we report an error".
Most other C operators do not promise an evaluation order for their operands (in f() + g(), either function may be called first). &&, ||, the comma operator and ?: are the exceptions.
Precedence: where comparisons and logic fit
From tighter to looser binding:
! unary - (casts) highest
* / %
+ -
< <= > >=
== !=
&&
||
= (assignment) lowest
So a + 1 < b * 2 && c != 0 reads naturally as ((a + 1) < (b * 2)) && (c != 0), and a || b && c is a || (b && c). The trap is !: !x == 5 is (!x) == 5, not !(x == 5). Parenthesize mixtures of && and ||; gcc even suggests it (-Wparentheses).
De Morgan's laws
Negating a compound condition flips AND and OR and negates each part:
!(A && B) == !A || !B
!(A || B) == !A && !B
"Not (adult and has ticket)" is "not adult, or no ticket". Remember to negate the comparisons properly: the opposite of age >= 18 is age < 18, not age <= 18.
Common mistakes
=instead of==.if (x = 5)assigns 5 to x and is always true. gcc warns: This is one of the most common C bugs of all time; never ignore that warning.warning: suggest parentheses around assignment used as truth value [-Wparentheses]- Chained comparisons.
0 <= x <= 10compiles but means(0 <= x) <= 10, which is always true:warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses] &and|instead of&&and||. The single-character versions are bitwise operators (module 13). They evaluate both sides, so guards stop working, and2 & 1is 0 even though both are "true".- Comparing floating-point results with
==. Use a tolerance. - Guard on the wrong side.
10 / x > 1 && x != 0divides first. The guard must come first.
Recap
- Comparison operators produce the int 1 or 0. In conditions, 0 is false and anything else true.
<stdbool.h>providesbool,true,falsefor clarity.&&,||,!combine conditions;&&and||evaluate left to right and short-circuit, which makes guards likex != 0 && 10 / x > 1safe.- Comparisons bind looser than arithmetic,
&&tighter than||; parenthesize mixtures. - De Morgan:
!(A && B)is!A || !B. - Classic bugs:
=for==, chained comparisons,&for&&.
Going further: rewrite the leap-year rule in two different but equivalent ways (for example starting with year % 400 == 0 ||), and use the exercise check to confirm they agree.