Five Operators, and the One You Have Not Met

Every program so far has computed something: total / parts, a + 100, at_top + 1. An expression is any piece of code that produces a value. The smallest ones are a literal like 60 or a variable name, and operators build larger expressions out of smaller ones. C's arithmetic set is five characters wide.

#include <stdio.h>

int main(void)
{
    int apples = 17;
    int per_bag = 5;
    int owed = -7;

    printf("sum %d, difference %d, product %d\n",
           apples + per_bag, apples - per_bag, apples * per_bag);
    printf("%d apples fill %d bags with %d left over\n",
           apples, apples / per_bag, apples % per_bag);
    printf("-7 / 2 is %d, -7 %% 2 is %d, and (-7 / 2) * 2 + (-7 %% 2) is %d\n",
           owed / 2, owed % 2, (owed / 2) * 2 + owed % 2);

    return 0;
}
sum 22, difference 12, product 85
17 apples fill 3 bags with 2 left over
-7 / 2 is -3, -7 % 2 is -1, and (-7 / 2) * 2 + (-7 % 2) is -7

Four of those read on sight. The fifth, %, is the remainder operator, and it answers the question integer division leaves behind. Last lesson established that 17 / 5 is 3 because integer division truncates toward zero and throws the fraction away. 17 % 5 is 2, and 2 is precisely what was thrown away, counted back in whole apples. The two operators are a pair, and together they split one number into a quantity and a leftover. That is the job you will keep reaching for: a count of seconds becomes minutes and seconds with one / and one %, and no other machinery at all. (%% in a format string is how you ask printf for a literal percent sign, since a bare % starts a conversion specification.)

Two facts to fix before you use it. % is integer only. There is no % for double, and asking for one is a compile error rather than a rounding surprise. And dividing or taking a remainder by zero is undefined behaviour, in the full sense the last lesson gave that phrase: not an error you catch afterwards, not a strange number, simply no requirement on the program at all. That is why the previous exercise initialized its divisor to 1 rather than 0. Actually inspecting a divisor before you use it needs a way to send the program down one path or another, which is still a couple of lessons away.

The third output line is where people get hurt, because they arrive expecting the modulo of mathematics, in which -7 mod 2 is 1 and a remainder is never negative. C does something else: the remainder takes the sign of the dividend, the left operand. So -7 % 2 is -1, and a negative second count would give you -7 minutes and -8 seconds rather than anything you would say out loud. The rest of that line shows why it must be so. C guarantees that (a / b) * b + a % b equals a wherever the division is defined, and you can watch it hold: -3 times 2 is -6, plus -1 is -7. Division truncates toward zero, so the remainder has no choice but to lean the same way. Remember the invariant and you never have to memorize the sign rule, because the sign rule falls out of it.

Precedence, and the Parentheses Habit

When one expression contains several operators, precedence decides which of them claims its operands first, and C's ranking is the one you learned in arithmetic class: multiplication, division and remainder bind tighter than addition and subtraction. Averaging two numbers is where that bites first.

#include <stdio.h>

int main(void)
{
    int first = 7;
    int second = 10;

    printf("first + second / 2.0 is %.2f\n", first + second / 2.0);
    printf("(first + second) / 2.0 is %.2f\n", (first + second) / 2.0);

    return 0;
}
first + second / 2.0 is 12.00
(first + second) / 2.0 is 8.50

The first line is not an average of anything. / went first, so it halved second alone and then added the whole of first. Parentheses are the override: they group what you meant, and the second line is the average you asked for.

C's full precedence table runs to fifteen or so levels and there is nothing to gain from memorizing it. Two ranks are worth knowing outright: the arithmetic one you just used, and the fact that assignment is itself an expression sitting almost at the bottom, which is why int minutes = total / 60; needs no parentheses to divide before it stores. Unary minus binds tighter than any arithmetic pair, so -second + 3 negates and then adds. Past that, adopt the habit rather than the table: when an expression would make a reader pause, parenthesize it. Redundant parentheses cost nothing at run time and the compiler discards them, while a wrong guess about precedence costs you an afternoon.

Updating a Variable in Place

Most arithmetic in real programs updates a variable using its own current value, and C has shorter spellings for that.

#include <stdio.h>

int main(void)
{
    int score = 40;
    int count = 5;

    score = score + 5;
    score += 5;
    score *= 2;

    int from_prefix = ++count;
    int from_postfix = count++;

    printf("score is %d, count is %d\n", score, count);
    printf("prefix gave %d, postfix gave %d\n", from_prefix, from_postfix);

    return 0;
}
score is 100, count is 7
prefix gave 6, postfix gave 6

score = score + 5 names score twice to say one thing. score += 5 reads the value, adds 5, and writes the result back into the same bytes, which is exactly the overwrite-in-place picture chapter 1 drew for assignment. -=, *=, /= and %= follow the identical pattern. Prefer the compound form when the variable on the left is the one being updated: it says "change this" instead of "compute something and happen to store it here", and it cannot go wrong by naming a different variable on the two sides. 40 became 45, then 50, then 100.

Adding or subtracting 1 is common enough to get its own operators, and each has two spellings that differ in what the expression hands back. Prefix ++count yields the new value; postfix count++ yields the old one. Both changed count, which is why it ended at 7. The prefix gave 6, the value after its increment; the postfix also gave 6, but that was the value before its increment. As a standalone statement the two are identical: on a line of their own ++count; and count++; do the same thing, and neither is more correct. The difference only surfaces inside a larger expression, which is exactly where you should not be putting them.

Two Questions With No Answer

The next program is wrong. It looks like it increments i and stores the result.

#include <stdio.h>

int main(void)
{
    int i = 5;

    i = i++;
    printf("i is %d\n", i);

    return 0;
}

That statement modifies i twice, once through the assignment and once through the ++, with nothing between them to say which happens first. C calls those ordering points sequence points, and modifying an object more than once between them is undefined behaviour. Not 5, not 6, not "whichever your compiler picks": there is no answer to look up, so the output is deliberately not shown. gcc says as much:

increment.c: In function 'main':
increment.c:7:7: warning: operation on 'i' may be undefined [-Wsequence-point]
    7 |     i = i++;
      |     ~~^~~~~

The next program is wrong too, in a way that reads far more innocently.

#include <stdio.h>

int main(void)
{
    int i = 5;

    printf("%d %d\n", i++, i);

    return 0;
}

The order in which a function's arguments are evaluated is unspecified, the term last lesson defined as the implementation choosing among allowed behaviours without having to document its choice. No rule says i++ runs before the plain i or after it. This is worse than a coin flip, though: one argument modifies i while the other reads it, nothing sequences the two, and so the program lands back in undefined behaviour. gcc reports it with the same warning.

arguments.c: In function 'main':
arguments.c:7:24: warning: operation on 'i' may be undefined [-Wsequence-point]
    7 |     printf("%d %d\n", i++, i);
      |                       ~^~

Both fixes are the same fix, and it is not a cleverer expression. Give each side effect a statement of its own, and this prints old 5, new 6:

#include <stdio.h>

int main(void)
{
    int i = 5;
    int old = i;

    i += 1;
    printf("old %d, new %d\n", old, i);

    return 0;
}

That is the rule to carry: one side effect per statement, and keep ++ and -- out of any expression that also reads the same variable. Written plainly there is nothing left for the reader or the compiler to guess at.

Naming Fixed Values With const

A bare 60 in a seconds calculation is a magic number: a literal whose meaning lives only in the head of whoever typed it, and written twice it is also two places to get wrong. The const qualifier gives such a value a name and tells the compiler it must never change.

#include <stdio.h>

int main(void)
{
    const int seconds_per_minute = 60;
    const double vat_rate = 0.10;
    int total_seconds = 428;
    double price = 24.50;

    printf("%d minutes and %d seconds\n",
           total_seconds / seconds_per_minute, total_seconds % seconds_per_minute);
    printf("%.2f plus VAT is %.2f\n", price, price + price * vat_rate);

    return 0;
}
7 minutes and 8 seconds
24.50 plus VAT is 26.95

const says the object must not be modified through that name, and the compiler enforces it as an error rather than a warning, so there is no ignoring it and carrying on. The next program does not compile:

#include <stdio.h>

int main(void)
{
    const double vat_rate = 0.10;

    vat_rate = 0.20;
    printf("rate is %.2f\n", vat_rate);

    return 0;
}
vat.c: In function 'main':
vat.c:7:14: error: assignment of read-only variable 'vat_rate'
    7 |     vat_rate = 0.20;
      |              ^

Name a value with const whenever it is fixed and appears more than once, or whenever the bare number would make a reader ask "why that one?". snake_case is the usual spelling, the same as for any other variable. One honest caveat to note and move past: a const variable in C is an ordinary object you are forbidden to assign to, not a compile-time constant, so a few contexts later in the course will not accept one. The preprocessor offers a different mechanism for those, and the course reaches it when you need it.

Key Takeaways

  • An expression produces a value, and C's arithmetic operators are +, -, *, / and %. The remainder % returns what integer division discarded, so 17 / 5 is 3 and 17 % 5 is 2, and the pair splits a number into a quantity and a leftover.
  • % is integer only, there is no % for double, and dividing or taking a remainder by zero is undefined behaviour.
  • The remainder takes the sign of the dividend, so -7 % 2 is -1, not the 1 that mathematical modulo gives. It follows from C's guarantee that (a / b) * b + a % b equals a.
  • Precedence decides which operator claims its operands first, and *, / and % bind tighter than + and -. Do not memorize the table; parenthesize whenever an expression would make a reader pause.
  • Compound assignment updates a variable in place: score += 5 is score = score + 5 said once, and -=, *=, /= and %= match it.
  • Prefix ++i yields the new value and postfix i++ the old. Standalone they are identical, and the expressions where the difference matters are the ones not to write.
  • Modifying an object twice between sequence points, as in i = i++, is undefined behaviour, and the order of function arguments is unspecified, so printf("%d %d\n", i++, i) has no answer either. gcc reports both as -Wsequence-point. One side effect per statement.
  • const means the object must not be modified through that name, and assigning to it is a compile error. Use it to name fixed values instead of scattering magic numbers, remembering that a C const object is not a compile-time constant.