C / BRANCHING AND SWITCH
Boolean logic with stdbool.h
Use bool, true and false from <stdbool.h>, understand why assigning 42 to a bool stores 1, and rely on && / || short-circuiting to guard unsafe operations.
What you will learn
- Use bool, true and false from <stdbool.h> in place of int flags holding 0 and 1
- Predict that bool b = v; stores (v != 0), so 42, -1 and 0.5 all become 1
- Test truth with if (flag) or !flag instead of the fragile flag == true
- Use && and || short-circuiting to check a pointer before dereferencing it
Understanding Boolean logic with stdbool.h
Before C23, <stdbool.h> is a very thin header: it defines bool as a macro for the built-in type _Bool, true as 1 and false as 0. The payoff is not new syntax but a conversion rule, because a _Bool object can only represent 0 and 1, so bool ready = 42; stores 1 while int ready = 42; keeps 42. The mental model for any assignment or initialization of a bool is x = (v != 0), and that applies to pointers and floating values too. That normalization is what makes two bools safely comparable, whereas two int flags carrying different nonzero truths compare unequal to each other.
C's operators were already boolean before the header existed: ==, <, &&, || and ! each produce an int that is exactly 0 or 1, which is why sizeof(1 == 1) is sizeof(int) rather than sizeof(bool). Conditions work in the opposite direction: if, while and ?: do not demand a bool, they test whether the controlling expression compares unequal to zero, so an int, a char or a pointer is a legal condition. That asymmetry is the reason x == true is a bug magnet: it narrows the accepted truths from "anything nonzero" down to "exactly 1", while if (x) accepts every truthy value.
&& and || are control-flow operators rather than arithmetic ones. Each guarantees left-to-right evaluation with a sequence point after the left operand, and skips the right operand entirely once the outcome is fixed, so s != NULL && s[0] == 'x' can never read through a null pointer. Their bitwise lookalikes & and | do neither: they always evaluate both operands and combine bits, so 6 & 1 is 0 even though both operands are truthy. Choosing & in a condition therefore loses the guard and can silently change the truth value.
Printing is the one place where bool offers nothing special. There is no printf conversion for it, so a bool is promoted to int and printed with %d, or converted to a word with flag ? "true" : "false".
<stdbool.h>
<stdio.h>
static int noisy(void)
{
puts("noisy() ran");
return 1;
}
int main(void)
{
bool flag = 42; /* stores (42 != 0), i.e. 1 */
int plain = 42; /* stores 42 */
printf("flag = %d\n", flag);
printf("plain = %d\n", plain);
printf("true = %d, false = %d\n", true, false);
printf("sizeof(bool) = %zu, sizeof(1 == 1) = %zu\n",
sizeof(bool), sizeof(1 == 1));
bool ok = true;
if (ok || noisy())
puts("noisy() was skipped: || already knew the answer");
printf("2 && 1 = %d, 2 & 1 = %d\n", 2 && 1, 2 & 1);
return 0;
}
bool is a type whose assignment converts any value to exactly 0 or 1, while every C condition only ever asks whether an expression is nonzero.
Worked examples
Guarding a dereference with &&
Shows how short-circuit evaluation lets one condition validate a pointer before a later operand uses it.
<stdbool.h>
<stdio.h>
bool starts_with_digit(const char *s)
{
return s != NULL && s[0] >= '0' && s[0] <= '9';
}
int main(void)
{
const char *cases[] = { "7 apples", "apples", NULL };
for (int i = 0; i < 3; i++) {
bool hit = starts_with_digit(cases[i]);
printf("%-10s -> %s\n",
cases[i] ? cases[i] : "(null)",
hit ? "true" : "false");
}
return 0;
}
Example explained
Line 1s != NULL && s[0] >= '0' is safe because && finishes the left operand first and skips everything to its right when that operand is false, so s[0] is never read for a null pointer.
Line 2The return type is bool, so the three chained comparisons are collapsed into a single stored 0 or 1 before the caller ever sees them.
Line 3cases[i] ? cases[i] : "(null)" is needed because passing a null pointer to %s is undefined behaviour, not a guaranteed "(null)".
Line 4hit ? "true" : "false" is required because printf has no conversion specifier that prints a bool as a word.
Why == true breaks on nonzero returns
Demonstrates that a truthy value is not necessarily 1, and shows the two ways to normalize it.
<stdbool.h>
<stdio.h>
/* a "nonzero means yes" function, in the style of isdigit() */
int has_flag(int mask)
{
return mask & 0x04;
}
int main(void)
{
int mask = 0x06;
printf("has_flag(0x06) = %d\n", has_flag(mask));
printf("has_flag(mask) == true = %d\n", has_flag(mask) == true);
printf("!!has_flag(mask) = %d\n", !!has_flag(mask));
bool normalized = has_flag(mask);
printf("normalized == true = %d\n", normalized == true);
return 0;
}
Example explained
Line 1has_flag returns mask & 0x04, which is 4 for 0x06: a perfectly truthy value that is not 1.
Line 2has_flag(mask) == true expands to a comparison with 1, so it yields 0 even though the flag is set.
Line 3!!has_flag(mask) applies ! twice, and since ! yields int 0 or 1, the pair maps any nonzero value onto 1.
Line 4Storing the result in a bool performs the same conversion at assignment time, which is why normalized == true is 1.
Important notes
sizeof(bool) is 1 and sizeof(int) is 4 on mainstream platforms, but the standard only requires that a _Bool can store 0 and 1, so treat the printed sizes as typical rather than guaranteed.
From C23 onward bool, true and false are keywords available with no include at all; in C99 through C17 they are macros, which is why older code often writes _Bool directly.
Common mistakes
Using bool without #include <stdbool.h> under C17 or earlier: compilation stops with "unknown type name 'bool'", because bool is only a macro that this header supplies.
Writing if (isdigit(c) == true): functions documented as returning nonzero may return a bitmask (glibc's isdigit does), so the comparison against 1 fails and the branch silently never runs.
Typing & instead of && in a condition, as in if (p != NULL & p->n > 0): both sides are always evaluated so the null check no longer protects the dereference, and bitwise combining can flip the result, since 6 & 1 is 0 while 6 && 1 is 1.
Try it yourself
Change, predict, then run
Write bool is_leap(int y) that returns (y % 4 == 0 && y % 100 != 0) || y % 400 == 0, then print the answer for 1900, 2000, 2024 and 2025 as the words true and false. Also print the raw return value with %d to confirm it is only ever 1 or 0.
Open the C workspaceCheck your understanding
Given int n = 6; bool a = n & 4; int b = n & 4; what do a and b hold, and why?
- Both hold 4, because & returns the bit value and a bool stores whatever it is given
- Both hold 1, because & normalizes its result to 0 or 1
- a holds 1 and b holds 4, because converting a value to bool stores whether it is nonzero
- a holds 0 and b holds 4, because 4 is not equal to true
Show answer
6 & 4 is 4. Assigning that to an int keeps 4, but conversion to _Bool is defined as the result of comparing the value with zero, so the bool becomes 1. Option 2 is tempting because && really does yield 0 or 1, but the bitwise & performs no normalization at all: here the 1 comes from the assignment's type conversion, not from the operator.