munotes®

Relational and Logical Operators

Chapter Sixteen

Syllabus topic 2, "Type of operators: Arithmetic operators, relational and logical operators, Increment and Decrement operators, assignment operators, the conditional operator, Assignment operators and expression, Precedence and order of Evaluation Block Structure, Initialization, C Preprocessor"

Pages 74 to 79 of 222

In one line

Relational operators compare two values and produce 1 or 0, and logical operators combine such results, with && and || stopping as soon as the answer is certain.

What a condition actually is in C

C has no separate truth value in the way later languages do. A condition is just an integer expression, and the rule is one line:

Zero is false. Every other value is true.

That is all. if (x) runs its body whenever x is not zero, and if (x - 5) runs whenever x is not 5. The relational operators produce 1 for true and 0 for false, so they fit the rule, but nothing requires a condition to come from one.

C99 added _Bool and the header <stdbool.h> with bool, true and false. They are worth using for clarity, and they change nothing underneath: true is 1 and false is 0.

The six relational operators

OperatorMeansa = 5, b = 3
<less thana < b is 0
>greater thana > b is 1
<=less than or equala <= b is 0
>=greater than or equala >= b is 1
==equal toa == b is 0
!=not equal toa != b is 1
#include <stdio.h>

int main(void)
{
    int a = 5, b = 3;

    printf("a < b  is %d\n", a < b);
    printf("a > b  is %d\n", a > b);
    printf("a == b is %d\n", a == b);
    printf("a != b is %d\n", a != b);
    printf("and the result really is an int: sizeof(a > b) is %zu\n",
           sizeof(a > b));
    return 0;
}
a < b  is 0
a > b  is 1
a == b is 0
a != b is 1
and the result really is an int: sizeof(a > b) is 4

= is assignment and == is comparison. if (x = 5) assigns 5 to x and then tests 5, which is not zero, so the body always runs. It compiles, because an assignment is an expression with a value. This is the single most expensive typing error in C, and -Wall warns about it when it looks suspicious.

The habit that prevents it: when comparing a variable with a constant, some programmers write the constant first, if (5 == x), because if (5 = x) will not compile. Use it if it helps you; the more reliable protection is to compile with warnings on and read them.

The three logical operators

&&    logical AND    true when both operands are true
||    logical OR     true when at least one operand is true
!     logical NOT    true when the operand is false
munotes.in74

Relational and Logical Operators

&& and || are binary and produce 1 or 0. ! is unary and also produces 1 or 0, so !0 is 1 and !5 is 0, not -5.

#include <stdio.h>

int main(void)
{
    int age = 20;
    int marks = 65;

    printf("age >= 18 && marks >= 60 is %d\n", age >= 18 && marks >= 60);
    printf("age <  18 || marks >= 60 is %d\n", age <  18 || marks >= 60);
    printf("!(age >= 18)             is %d\n", !(age >= 18));
    printf("!0 is %d and !5 is %d\n", !0, !5);
    return 0;
}
age >= 18 && marks >= 60 is 1
age <  18 || marks >= 60 is 1
!(age >= 18)             is 0
!0 is 1 and !5 is 0

Short circuiting, which is a guarantee and not an optimisation

&& evaluates its left operand first. If that is false, the answer must be false, so the right operand is not evaluated at all. || does the same when the left operand is true. The standard guarantees this, which means you may rely on it, and you will need to.

#include <stdio.h>

int calls = 0;

int noisy(int value)
{
    calls = calls + 1;
    return value;
}

int main(void)
{
    calls = 0;
    printf("0 && noisy(1) is %d, and noisy was called %d time(s)\n",
           0 && noisy(1), calls);

    calls = 0;
    printf("1 || noisy(1) is %d, and noisy was called %d time(s)\n",
           1 || noisy(1), calls);

    calls = 0;
    printf("1 && noisy(1) is %d, and noisy was called %d time(s)\n",
           1 && noisy(1), calls);
    return 0;
}
0 && noisy(1) is 0, and noisy was called 0 time(s)
1 || noisy(1) is 1, and noisy was called 0 time(s)
1 && noisy(1) is 1, and noisy was called 1 time(s)

The counter proves it. And this is what makes the following safe, which is the reason the guarantee matters:

if (count != 0 && total / count > 50)

If count is zero the division never happens. Write the two tests in the other order and the program divides by zero. Chapter 15 said the only correct handling of division by zero is to test first; short circuiting is what lets you do it in one expression.

The leap year, in full

MU's Practical 1(c). The Gregorian rule:

  1. A year divisible by 4 is a leap year,
  2. except that a year divisible by 100 is not,
  3. except that a year divisible by 400 is.

So 2024 is a leap year, 1900 is not, and 2000 is.

Written as one expression:

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

The brackets are not optional. && binds tighter than ||, so without them the expression would group as (year % 4 == 0 && year % 100 != 0) || year % 400 == 0, which is a different test. It happens to give the right answer for every year, which is exactly why the mistake survives: it is wrong reasoning that produces right answers, and a viva question about 1900 will expose it.

munotes.in75

Relational and Logical Operators

#include <stdio.h>

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

int main(void)
{
    int years[] = {1900, 1996, 2000, 2023, 2024, 2100, 2400};

    for (int i = 0; i < 7; i++) {
        printf("%d is %sa leap year\n",
               years[i], is_leap(years[i]) ? "" : "not ");
    }
    return 0;
}
1900 is not a leap year
1996 is a leap year
2000 is a leap year
2023 is not a leap year
2024 is a leap year
2100 is not a leap year
2400 is a leap year

And reading the year from the user, which is what the practical asks:

#include <stdio.h>

int main(void)
{
    int year;

    printf("Enter a year: ");
    if (scanf("%d", &year) != 1) {
        printf("\nThat was not a year.\n");
        return 1;
    }
    if (year < 1) {
        printf("\nThere is no year %d in this calendar.\n", year);
        return 1;
    }

    if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) {
        printf("\n%d is a leap year.\n", year);
    } else {
        printf("\n%d is not a leap year.\n", year);
    }
    return 0;
}
1900
Enter a year:
1900 is not a leap year.

For the algorithm and flowchart the same practical asks for, see chapter 2, which draws this test as three decision diamonds.

Two traps worth a mark each

1. a < b < c does not mean what it says in mathematics. It groups as (a < b) < c, and (a < b) is 0 or 1, so the whole thing compares 0 or 1 against c. Write a < b && b < c.

The listing below is wrong on purpose, and gcc says so before it is even run:

#include <stdio.h>

int main(void)
{
    int a = 10, b = 5, c = 1;

    printf("mathematically 10 < 5 < 1 is false\n");
    printf("in C, a < b < c gives %d, because (a < b) is %d and %d < c is %d\n",
           a < b < c, a < b, a < b, (a < b) < c);
    printf("written correctly, a < b && b < c gives %d\n", a < b && b < c);
    return 0;
}
munotes.in76

Relational and Logical Operators

chained.c: In function ‘main’:
chained.c:9:14: warning: comparisons like ‘X<=Y<=Z’ do not have their mathematical meaning [-Wparentheses]
    9 |            a < b < c, a < b, a < b, (a < b) < c);
      |            ~~^~~

It runs anyway, because a warning is not an error, and this is what it prints:

mathematically 10 < 5 < 1 is false
in C, a < b < c gives 1, because (a < b) is 0 and 0 < c is 1
written correctly, a < b && b < c gives 0

2. Never compare floating-point values with ==. Chapter 9 said why: the stored value is the nearest representable one, and arithmetic can land on a different neighbour.

#include <stdio.h>
#include <math.h>

int main(void)
{
    double x = 0.1 + 0.2;

    printf("x == 0.3 is %d\n", x == 0.3);
    printf("fabs(x - 0.3) < 1e-9 is %d\n", fabs(x - 0.3) < 1e-9);
    printf("x is actually %.20f\n", x);
    return 0;
}
x == 0.3 is 0
fabs(x - 0.3) < 1e-9 is 1
x is actually 0.30000000000000004441

fabs(x - y) < tolerance is the form to write, and the tolerance is chosen for the problem.

What this does NOT mean

A relational expression does not produce a special truth type. It produces an int, 1 or 0, which is why it can be printed with %d and added up.

"True" is not 1. Any non-zero value is true as a condition. if (7) runs, and !7 is 0. Only the operators produce 1.

& and | are not && and ||. The single-character forms are bitwise operators: they combine the bits of their operands and they always evaluate both sides. 1 & 2 is 0 while 1 && 2 is 1.

! is not negation. !x is 1 or 0. Arithmetic negation is -x.

Short circuiting is not an optimisation the compiler may skip. It is required by the standard, and programs depend on it for safety.

if (x = 5) is not a syntax error. It is a legal assignment used as a condition, and it is nearly always a typing mistake for ==.

Quick revision

  • Six relational operators: < > <= >= == !=. Each yields int 1 or 0.
  • Zero is false; any non-zero value is true.
  • = assigns, == compares. if (x = 5) is legal and always true.
  • Three logical operators: &&, ||, !.
  • && and || short-circuit, guaranteed by the standard, left operand first.
  • count != 0 && total / count > 50 is safe because of it.
  • && binds tighter than ||, so an OR inside an AND needs brackets.
  • Leap year: y % 4 == 0 && (y % 100 != 0 || y % 400 == 0).
  • a < b < c is a bug. Write a < b && b < c.
  • Never compare floating-point values with ==; use fabs(x - y) < tolerance.
  • & and | are bitwise and evaluate both operands.
munotes.in77

Relational and Logical Operators

Test yourself

1. What is the value of 5 > 3 and what is its type?

1, of type int.

2. What does if (n = 0) do?

It assigns 0 to n and then tests 0, which is false, so the body never runs. It was almost certainly meant to be if (n == 0).

3. Write the leap-year condition and say why the brackets are needed.

year % 4 == 0 && (year % 100 != 0 || year % 400 == 0). Without them, && binding tighter than || would group the expression as (y % 4 == 0 && y % 100 != 0) || y % 400 == 0, which is a different test built on wrong reasoning.

4. Why is if (n != 0 && 100 / n > 5) safe while if (100 / n > 5 && n != 0) is not?

&& evaluates the left operand first and skips the right if the left is false. In the first form the division is reached only when n is non-zero. In the second the division happens first and divides by zero.

5. What is !(-5)?

  1. Minus five is non-zero, so it is true, and the logical NOT of true is 0.

6. Give the difference between && and &.

&& is logical AND: it produces 1 or 0 and does not evaluate its right operand if the left is false. & is bitwise AND: it combines the bits of its operands and always evaluates both.

7. Is 2100 a leap year?

No. It is divisible by 4 and by 100 and not by 400, so the century exception applies.

What can be asked on this, and how to answer it

"Explain the relational and logical operators in C." Give the six relational with a value each, then the three logical, and then the two things the examiner is looking for: that the result is an int of 1 or 0, and that && and || short-circuit. Give the safe-division example for the second.

"What is short-circuit evaluation? Give an example where it matters." Define it, say the standard guarantees it, and give count != 0 && total / count > 50. Say that reversing the order makes the program divide by zero, which is what turns it from a nicety into a rule.

munotes.in78

Relational and Logical Operators

"Distinguish between = and ==." = assigns and yields the value assigned; == compares and yields 1 or 0. if (x = 5) is always true, and if (x == 5) tests. This is a favourite one-mark question.

"Write a program to check whether a year is a leap year." Give this chapter's program with the full three-part rule, and state the three test cases that show you understand it: 2024 yes, 1900 no, 2000 yes.

"Distinguish between logical and bitwise operators." Logical operators treat their operands as true or false, produce 1 or 0, and short-circuit. Bitwise operators work on the individual bits, produce a bit pattern, and always evaluate both operands. 1 && 2 is 1; 1 & 2 is 0.

munotes.in79

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Report or request
Done!