Operators, expressions and input · about 45 min

Comparisons, logic and short-circuit evaluation

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

operatormeaning
<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

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:

  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

Recap

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.

Exercises

Exercise 1 · quiz

A chained comparison

What does printf("%d\n", 5 < 3 < 2); print?

Answer
  • 1

C has no chained comparisons. 5 < 3 < 2 is (5 < 3) < 2. 5 < 3 is false, which is the int 0, and 0 < 2 is true, which is 1. gcc warns: "comparisons like 'X<=Y<=Z' do not have their mathematical meaning". To test a range, combine two comparisons with &&: lo < x && x < hi.

Exercise 2 · quiz

Which guards are safe?

x is an int that may be 0. Which of these expressions never divide by zero? Select all that apply.

Answer
  • x != 0 && 100 / x > 5

  • x == 0 || 100 / x > 5

&& and || evaluate left to right and stop as soon as the answer is known. In the first, if x != 0 is false the whole && is false and the division is skipped. In the third, if x == 0 is true the whole || is true and the division is skipped. The second divides before it checks. The fourth uses &, the bitwise AND operator, which always evaluates both sides: a classic typo.

Exercise 3 · write code

Is it a digit?

Write a function

int is_digit_char(char c)

that returns 1 if c is one of the characters '0' to '9' and 0 otherwise, as a single return of a comparison expression. Do not use <ctype.h>.

File: main.c

Starting point

#include <stdio.h>

int is_digit_char(char c)
{
    return '0' <= c <= '9';
}

int main(void)
{
    printf("%d %d\n", is_digit_char('7'), is_digit_char('x'));
    return 0;
}
Hint 1

The starter compiles, but read the warning: chained comparisons do not work in C.

Hint 2

Write two comparisons and join them with &&.

Reference solution
#include <stdio.h>

int is_digit_char(char c)
{
    return '0' <= c && c <= '9';
}

int main(void)
{
    printf("%d %d\n", is_digit_char('7'), is_digit_char('x'));
    return 0;
}

Exercise 4 · quiz

De Morgan

Which expression means the same as !(age >= 18 && has_ticket)?

Answer
  • age < 18 || !has_ticket

De Morgan's law: not (A and B) is (not A) or (not B). Not (age >= 18) is age < 18 (careful: not <= 18). The last option is wrong for a precedence reason: ! binds tighter than >=, so it means (!age) >= 18, which is always false because !age is 0 or 1.

Exercise 5 · write code

Leap years

In the Gregorian calendar, a year is a leap year if it is divisible by 4, except that years divisible by 100 are not leap years, except that years divisible by 400 are. So 2024 and 2000 are leap years; 2023 and 1900 are not.

Write a function

int is_leap(int year)

that returns 1 for leap years and 0 otherwise, using a single return with %, comparisons, && and || (no if; that comes in the next module).

File: main.c

Starting point

#include <stdio.h>

int is_leap(int year)
{
    return year % 4 == 0;
}

int main(void)
{
    printf("2024:%d 1900:%d 2000:%d 2023:%d\n",
           is_leap(2024), is_leap(1900), is_leap(2000), is_leap(2023));
    return 0;
}
Hint 1

Divisible by n means year % n == 0.

Hint 2

A year is a leap year if (divisible by 4 and not by 100) or (divisible by 400).

Hint 3

return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;

Reference solution
#include <stdio.h>

int is_leap(int year)
{
    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}

int main(void)
{
    printf("2024:%d 1900:%d 2000:%d 2023:%d\n",
           is_leap(2024), is_leap(1900), is_leap(2000), is_leap(2023));
    return 0;
}