C / OPERATORS AND EXPRESSIONS
Logical operators and short-circuit evaluation
Combine tests with &&, || and ! in C, order the operands so guards protect what follows, and predict which side effects short-circuiting skips.
What you will learn
- Read a && b as an if statement: b is evaluated only when a is nonzero.
- Put null and bounds checks on the left so the right operand is safe to evaluate.
- Expect 0 or 1 back from &&, || and !, never the operand's own value.
- Never substitute & or | for && or ||: both sides run and the bits get combined.
Understanding Logical operators and short-circuit evaluation
C has three logical operators: && (and), || (or) and ! (not). Each one asks a yes/no question about its operands by comparing them against zero, so 42, -1, 0.5 and any non-null pointer are all true, while 0, 0.0 and a null pointer are false. The result is not the operand you passed in; it is always the int value 1 or 0. That is why `5 && 3` is 1, and why the trick from other languages, `char *name = arg || "anon";`, is a type error in C rather than a default value.
The part worth internalising is that && and || evaluate their left operand first, then decide whether the right operand runs at all. If the left side of && is zero the answer is already 0, so the right side is skipped; if the left side of || is nonzero the answer is already 1, so the right side is skipped. This is not a compiler optimisation you hope for, it is a rule of the language, and there is a sequence point after the left operand, meaning everything the left side does is complete before the right side is considered. The useful mental model is that && and || are if statements written as expressions: the operands are an ordered sequence, not an unordered set.
Two practical consequences follow. First, operand order is part of the correctness of the expression: `i < n && a[i] == target` is safe while `a[i] == target && i < n` reads out of bounds, because in the second form nothing stops the subscript from being evaluated. Second, the right operand is conditional code, so anything with a side effect placed there, a function call or an increment, may silently never happen. The bitwise cousins & and | look interchangeable but are not: they evaluate both operands unconditionally and combine bit patterns, so the guard disappears and `2 & 1` is 0 where `2 && 1` is 1.
<stdio.h>
static int calls = 0;
static int probe(const char *tag, int value)
{
calls++;
printf(" probe %s returns %d\n", tag, value);
return value;
}
int main(void)
{
printf("A: left is 0\n");
int a = probe("L", 0) && probe("R", 1);
printf("A = %d\n\n", a);
printf("B: left is nonzero\n");
int b = probe("L", 1) || probe("R", 0);
printf("B = %d\n\n", b);
printf("C: both sides needed\n");
int c = probe("L", 1) && probe("R", 7);
printf("C = %d\n\n", c);
printf("probe was called %d times\n", calls);
return 0;
}
&& and || are control flow disguised as operators: the left operand decides whether the right one is evaluated at all, and the result is always the int 0 or 1.
Worked examples
The bounds check has to come first
A scan loop that stops on a condition without ever subscripting past the last element.
<stdio.h>
int main(void)
{
int a[5] = { 3, 8, 12, 20, 33 };
int i;
i = 0;
while (i < 5 && a[i] < 15)
i++;
if (i < 5)
printf("first element >= 15 is a[%d] = %d\n", i, a[i]);
else
printf("no element >= 15\n");
i = 0;
while (i < 5 && a[i] < 100)
i++;
printf("scan stopped at i = %d\n", i);
return 0;
}
Example explained
Line 1`i < 5 && a[i] < 15` puts the range test on the left, so `a[i]` is only evaluated for an index that is known to be valid.
Line 2In the second loop i reaches 5, `i < 5` is 0, and short-circuiting means `a[5]` is never read.
Line 3Written the other way round, `a[i] < 100 && i < 5`, the subscript happens before the guard: undefined behaviour, which often returns a plausible-looking value instead of crashing.
Line 4The loop leaves i at the index that stopped it, so a plain `i < 5` afterwards distinguishes found from ran off the end.
&& is not &, and the result is only ever 0 or 1
Shows that logical operators normalise their result while bitwise & combines bit patterns.
<stdio.h>
int main(void)
{
int x = 2, y = 1;
printf("x && y = %d\n", x && y);
printf("x & y = %d\n", x & y);
printf("5 || 0 = %d\n", 5 || 0);
printf("!5 = %d\n", !5);
printf("!!42 = %d\n", !!42);
return 0;
}
Example explained
Line 1`x && y` asks whether both operands are nonzero, so it yields 1 even though neither operand equals 1.
Line 2`x & y` lines the bits up: 0b10 & 0b01 is 0, so an `if` written with & takes the opposite branch here.
Line 3`!5` is 0 and `!!42` is 1, which is why `!!v` is the idiomatic way to squash any value down to exactly 0 or 1.
Line 4& also evaluates both operands unconditionally, so `p != NULL & p->count` dereferences a null pointer instead of guarding against one.
Important notes
Short-circuiting is guaranteed by the language, not an optimisation you hope for: there is a sequence point after the left operand, so the left side's effects are finished before the right side is even considered, and `i < n && a[i]` is portable across every conforming compiler.
&& binds tighter than ||, so `a || b && c` means `a || (b && c)`; both bind looser than the comparison operators, which is why `x > 0 && x < 10` needs no inner parentheses.
Common mistakes
Typing & or | where && or || was meant. It compiles silently, both operands are always evaluated so the guard is gone, and the result is a bit pattern: `if (2 & 1)` is false where `if (2 && 1)` is true.
Hiding required work in the right operand, as in `if (have_cached || fetch(&v))`. Once have_cached is nonzero fetch never runs, so v keeps its stale value and the bug only appears on the second call.
Expecting `a || b` to hand back a or b the way Python and JavaScript do. In C it is 1, so `char *name = arg || "anon";` assigns an int to a pointer instead of choosing a default.
Try it yourself
Change, predict, then run
Write `int p(int n)` that prints `p(n)` and returns n, then predict, run and compare the printed lines and the final value of `p(0) && p(1)`, `p(1) || p(2)` and `p(0) || p(3) && p(4)`.
Open the C workspaceCheck your understanding
A function receives `const char *s` that may be NULL. Which condition tests "s is non-null and its first character is a digit" without risking undefined behaviour?
- if (s[0] >= '0' && s[0] <= '9' && s != NULL)
- if (s != NULL && s[0] >= '0' && s[0] <= '9')
- if (s != NULL & s[0] >= '0' & s[0] <= '9')
- if (s != NULL || (s[0] >= '0' && s[0] <= '9'))
Show answer
&& evaluates left to right and skips everything to the right of an operand that is 0, so putting `s != NULL` first means the subscript is only reached for a valid pointer. Option 0 is the tempting one because it contains exactly the same three tests, but order is part of the semantics: it reads s[0] before anything has checked s. Option 2 uses bitwise &, which has no short-circuit and evaluates all three operands, so it dereferences NULL as well; option 4 both dereferences NULL when s is null and is always true when it is not.