Learn C Programming

Lesson 2 of 8 · Operators and Type Conversion

Module 4 · Operators and Type Conversion

Relational and Logical Operators, and Short Circuit

FreeReading

In this lesson

  • Compare two values with <, <=, >, >=, == and !=.
  • Join two answers with && and ||, and predict which side never runs.
  • Pick one of two values with the ternary operator, in a single expression.

Zara wants to know whether a mark sits between 1 and 10. She writes 1 < mark < 10, the way the range is written on paper.

It compiles. The Playground says nothing at all. And it answers 1 for a mark of 42, for a mark of 900, and for every other number she tries.

The line is not broken. It is a perfectly ordinary expression that means something other than what she read.

A comparison is an int, worth 1 or 0

Module 2 lesson 4 said this once. This lesson depends on it, so here it is again with the six operators.

The six relational operators

a <  b     is a less than b
a <= b     is a less than or equal to b
a >  b     is a greater than b
a >= b     is a greater than or equal to b
a == b     is a equal to b            two equals signs, always
a != b     is a not equal to b
  • Every one of them produces an int, and that int is 1 or 0. Never anything else.
  • = is assignment. == is a question. They are different operators that look alike.
  • The answer is a value, so you can store it, print it and do arithmetic on it.
#include <stdio.h>

int main(void)
{
    int mark = 42;

    printf("mark < 50   %d\n", mark < 50);
    printf("mark >= 40  %d\n", mark >= 40);
    printf("mark == 42  %d\n", mark == 42);
    printf("mark != 42  %d\n", mark != 42);
    return 0;
}
mark < 50   1
mark >= 40  1
mark == 42  1
mark != 42  0

Nothing here decides anything. Four questions were asked and four numbers came back.

Why 1 < mark < 10 compiles and lies

Relational operators group from the left. So C reads Zara's line as (1 < mark) < 10.

The bracketed part is a comparison, so its value is 1 or 0. Then C asks whether that 1 or 0 is less than 10. It always is.

#include <stdio.h>

int main(void)
{
    int mark = 900;

    printf("1 < mark        %d\n", 1 < mark);
    printf("that, < 10      %d\n", (1 < mark) < 10);
    printf("written chained %d\n", 1 < mark < 10);
    return 0;
}
1 < mark        1
that, < 10      1
written chained 1

The last two lines are the same expression written twice. The brackets only make the reading visible.

On the Playground's command line this is completely silent, because the check lives behind -Wall. A local gcc -Wall gives two warnings: comparisons like 'X<=Y<=Z' do not have their mathematical meaning and comparison of constant '10' with boolean expression is always true.

So a range needs two separate comparisons, joined by the operator in the next section.

&& and ||, and the truth table a program can print

&& is "and": 1 when both sides are true. || is "or": 1 when at least one side is true.

"True" here means "not zero", the Module 2 rule. The result is again a plain int worth 1 or 0.

#include <stdio.h>

int main(void)
{
    printf("p q   p&&q  p||q  !p\n");
    printf("0 0    %d     %d    %d\n", 0 && 0, 0 || 0, !0);
    printf("0 1    %d     %d    %d\n", 0 && 1, 0 || 1, !0);
    printf("1 0    %d     %d    %d\n", 1 && 0, 1 || 0, !1);
    printf("1 1    %d     %d    %d\n", 1 && 1, 1 || 1, !1);
    return 0;
}
p q   p&&q  p||q  !p
0 0    0     0    1
0 1    0     1    1
1 0    0     1    0
1 1    1     1    0

Zara's range check now writes itself: mark > 1 && mark < 10. Two questions, one joining word.

So the truth table is not something to memorise. It is four lines a program will print for you in ten seconds.

Short circuit: the right side may never run

C promises more than the table. It promises an order, and a chance to stop early.

For a && b, C evaluates a first. If a is 0 the answer is already 0, so b is never evaluated at all.

For a || b, C evaluates a first. If a is not zero the answer is already 1, so b is skipped.

Short circuit, drawn as two gates The left operand is always evaluated; the right one has to earn its turn a && b evaluate a a is 0 answer 0, b skipped a is not 0 evaluate b, answer b a || b evaluate a not 0 answer 1 a is 0 evaluate b, answer b This is a rule of the language, not an optimisation a compiler may skip.

You can watch it happen. printf returns the number of characters it printed, so a printf works as the right operand.

#include <stdio.h>

int main(void)
{
    int stock = 0;

    printf("start\n");
    int a = stock && printf("  the right side of && ran\n");
    int b = 1 || printf("  the right side of || ran\n");
    printf("a %d, b %d\n", a, b);
    return 0;
}
start
a 0, b 1

Neither message appeared. stock was 0, so the && stopped; the 1 was already true, so the || stopped.

So short circuit is how you guard a dangerous operation. Put the check on the left of &&, and the dangerous part runs only when the check passed.

! turns a number into an answer, and !! flattens one

! is "not". It gives 0 for any nonzero value, and 1 for zero.

Apply it twice and you get a value that is 1 or 0 and nothing else. !!7 is 1, !!0 is 0.

#include <stdio.h>

int main(void)
{
    int credits = 7;

    printf("credits      %d\n", credits);
    printf("!credits     %d\n", !credits);
    printf("!!credits    %d\n", !!credits);
    return 0;
}
credits      7
!credits     0
!!credits    1

The !! pair is a known idiom for "reduce this to exactly 1 or 0". It is not a special operator; it is ! used twice.

So "has credits" and "how many credits" become two different values, and only one of them is safe to compare against 1.

The ternary: one expression, two possible values

Every operator so far took two operands. This one takes three, and it is the only such operator in C.

The conditional operator

condition ? value_if_true : value_if_false
  • condition is evaluated first. Zero counts as false, anything else as true.
  • Exactly one of the two values is evaluated, the same short-circuit promise.
  • The whole thing is a value, so it can sit on the right of an = or inside a printf.
#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);

    int larger = (a > b) ? a : b;

    printf("%d\n", larger);
    return 0;
}
91

That output is for the input 91 47. Module 5 will write the same thing with if over four lines; here it is one.

The track's advice is one line long. Use the ternary when both results are values of the same kind. Reach for if the moment a branch does work instead of producing a value.

Example 1: the smallest program that stores an answer

A comparison goes into a box, and the box is printed with %d.

#include <stdio.h>

int main(void)
{
    int age = 0;
    scanf("%d", &age);

    int adult = age >= 18;

    printf("%d\n", adult);
    return 0;
}
1

That output is for the input 18. The boundary is in the operator: >= includes 18, > would not.

Run in Compiler
Example 2: the range check Zara wanted

Two comparisons, joined with &&, printed as one number.

#include <stdio.h>

int main(void)
{
    int mark = 0;
    scanf("%d", &mark);

    int in_range = mark >= 1 && mark <= 10;

    printf("%d\n", in_range);
    return 0;
}
0

That output is for the input 42. Try 1, 10 and 11: the two ends are the tests worth running, and the operator you chose decides them.

Run in Compiler
Example 3: a division that cannot divide by zero

Short circuit used on purpose. The divisor is checked on the left, so the division on the right is unreachable when it would be undefined.

#include <stdio.h>

int main(void)
{
    int n = 0;
    int d = 0;
    scanf("%d %d", &n, &d);

    int big = (d != 0) && (n / d > 10);

    printf("%d\n", big);
    return 0;
}
0

That output is for the input 500 0. The division never happened, so there was nothing to go wrong. Swap the two sides and the program becomes undefined for that same input.

Run in Compiler

Where this is used

  • Every form you have filled in. A signup form checks a length and a character range with the same && chain, on the server, before the value reaches a database.
  • Leap years. The rule is three comparisons joined by && and ||. The same expression sits in the Linux date utility, in a spreadsheet and in Exercise 2 below.
  • Feature flags. A release check reads flag_on && user_is_beta. Short circuit means the second, slower lookup happens only for the small group where the flag is on at all.
  • Guarding a pointer. Real C is full of p != NULL && p->ready. Module 11 explains the arrow; the shape is exactly Example 3, and the crash it prevents is worse.

Common mistakes

1. Writing a range the way mathematics writes it.

int ok = 1 < mark < 10;
printf("%d\n", ok);

Silent on the Playground, because both checks for it live behind -Wall; a local gcc -Wall reports comparisons like 'X<=Y<=Z' do not have their mathematical meaning. It prints 1 for every value. Write mark > 1 && mark < 10.

2. One equals sign where two were meant.

int matched = code = 5;
printf("matched %d\n", matched);

No message at either command line. The inner = is an assignment whose value is 5, so matched is always 5 and code has been overwritten. Two equals signs ask; one equals sign tells.

3. Using & where && was needed.

int big = (d != 0) & (n / d > 10);

No message at either command line, and for most inputs the answer is even correct. But & has no short circuit, so both sides are evaluated and the division runs with d at zero, which is undefined behaviour. One character decides whether the program is safe.

4. Comparing two doubles with ==.

printf("%d\n", 0.1 + 0.2 == 0.3);

No message at either command line, and it prints 0. Module 2 lesson 3 showed why those two values differ in their last bits. Compare a difference against a small tolerance instead.

Brain teaser

Bob is counting how many of two answers were 5, and writes it without a single if.

#include <stdio.h>

int main(void)
{
    int x = 5;
    int y = 3;

    int t = (x == 5) + (y == 5);

    printf("%d\n", t);
    return 0;
}

It prints 1. Say which values of x and y make it print 0, and which make it print 2. Then answer the harder half: replace the + with || and say what the three possible answers become, and why one of them disappears.

Each bracket is worth exactly 1 or 0, so the sum is a count. || does not count; it answers a different question about the same two brackets.

Exercise 1Easy

Amara is validating a mark that must land between two given bounds, both included.

Input. One line with three integers low, high and x.

Output. One line with 1 if x is between low and high, both included, otherwise 0.

Constraints. -1000 <= low <= high <= 1000, and -2000 <= x <= 2000.

Sample. Input 1 10 10 gives 1. Input 1 10 11 gives 0.

#include <stdio.h>

int main(void)
{
    int low = 0;
    int high = 0;
    int x = 0;
    scanf("%d %d %d", &low, &high, &x);

    /* Two comparisons and one &&. Both ends are included. */

    return 0;
}

Not graded in this module. Run it on both bounds before you believe it.

Run in Compiler
Exercise 2Medium

David is writing the calendar check every project eventually needs.

A year is a leap year when it divides by 4 and not by 100, or when it divides by 400.

Input. One line with one integer y.

Output. One line with 1 if y is a leap year, otherwise 0.

Constraints. 1 <= y <= 999999.

Sample. Input 2024 gives 1. Input 1900 gives 0. Input 2000 gives 1.

#include <stdio.h>

int main(void)
{
    int y = 0;
    scanf("%d", &y);

    /* Three remainders, two comparisons joined by &&, then one ||. */

    return 0;
}

Graded as leap-year. The whole rule fits in one expression; 1900 and 2000 are the two years that tell you whether you got it right.

Run in Compiler
Exercise 3Easy

Kenji wants the larger of two scores, without writing a single if.

Input. One line with two integers.

Output. One line with the larger of the two. If they are equal, print that value.

Constraints. Each value is between -1000000 and 1000000.

Sample. Input 91 47 gives 91.

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);

    /* One ternary. Equal values make both branches correct. */

    return 0;
}

Not graded in this module. Then write the smaller one too, by changing exactly one character.

Run in Compiler
Exercise 4Hard

Zara wants to see a whole truth table for one expression, printed by the program itself.

Input. One line with two integers p and q, each 0 or 1.

Output. Four lines: the values of p && q, p || q, !p and p != q, one per line, in that order.

Constraints. Each of p and q is 0 or 1.

Sample. Input 1 0 gives the four lines 0, 1, 0, 1.

#include <stdio.h>

int main(void)
{
    int p = 0;
    int q = 0;
    scanf("%d %d", &p, &q);

    /* Four printf lines, each one expression. No loop, no condition. */

    return 0;
}

Not graded in this module. Run all four input pairs and you have written the table of this lesson's third section.

Run in Compiler

Common doubts

  • Is the answer always exactly 1, or just any nonzero value?

    Exactly 1. C guarantees that <, ==, &&, || and ! all produce 1 or 0. Other people's functions may return any nonzero value for true, which is why !! exists.

  • Why is there both & and &&?

    They do different jobs. && joins two answers and stops early; & works on the bits of two numbers and always evaluates both. Lesson 4 is entirely about the second one.

  • Can I compare a char with a number?

    Yes, because a char is a number. ch >= '0' && ch <= '9' is the standard digit test, and it is comparing 48 and 57.

  • Does the ternary work with different types on the two sides?

    It compiles, and the two values are converted to one common type first. Keep both sides the same type until lesson 6 explains the conversion rules.

  • Is short circuit just an optimisation?

    No. It is written into the standard, so every C compiler must do it. That is what makes it safe to put a guard on the left of &&.

Key takeaways

  • Every comparison produces an int that is exactly 1 or 0.
  • 1 < x < 10 compiles, is always 1, and is silent on the Playground.
  • && and || join answers; zero is false and everything else is true.
  • Short circuit is a rule of the language: the right operand may never be evaluated.
  • ! flips an answer and !! reduces any value to 1 or 0.
  • The ternary is an expression with a value, best used when both sides are values.

Next you stop writing total = total + x and meet the shorter forms, one of which hides undefined behaviour.

End of lesson 2

Mark it done, and your progress moves with you.

Next: Assignment Shortcuts, ++ and --