Operators With Side Effects

Week 4 is chapter 3's operator lesson turned into experiments. ++ and -- are the only arithmetic operators that change their operand, and whether they hand back the old value or the new one depends on which side of the variable they sit. The bitwise operators do not change anything, but they work on a number's bits rather than its value, and the lab asks you to compute all six of them on the same inputs and print the results. Each program is short; the marks are for predicting the output before running it, so try that with every input suggested below.

Program 1: The Increment Operator

Show the difference between postfix x++ and prefix ++x. The clean way is two experiments on the same starting value: assign y = x++ and print both, then reset and assign y = ++x and print both.

Algorithm:

  1. Start.
  2. Read n.
  3. Set x to n, then set y to x++ and print y and x.
  4. Set x back to n, then set y to ++x and print y and x.
  5. Stop.

With 5 the first experiment prints y = 5, x = 6 and the second prints y = 6, x = 6. Both forms leave x at 6; they differ only in what the expression is worth. Postfix yields the value x had before, prefix yields the value it has after. When the result is not used, as in a for loop's i++, the two are interchangeable, and this program is the proof that they are not interchangeable the moment the result is assigned.

The INT_MAX check is unusual in a lab program and worth a sentence in the record. Incrementing the largest int overflows a signed integer, and chapter 3 was blunt about that: it is undefined behaviour, not "wraps to negative". INT_MAX comes from <limits.h>, and refusing that one input is what makes every other line of the program defined.

Do not extend this experiment to y = x++ + ++x or x = x++. Chapter 3 named those as undefined behaviour, modifying x twice between sequence points, and a textbook answer key that gives them an output is wrong, whatever number it prints.

Program 2: The Decrement Operator

The same two experiments with --.

Algorithm:

  1. Start.
  2. Read n.
  3. Set x to n, then set y to x-- and print y and x.
  4. Set x back to n, then set y to --x and print y and x.
  5. Stop.

5 gives y = 5, x = 4 and then y = 4, x = 4. Try 0 as well: the postfix line reports y = 0, x = -1, a reminder that nothing stops a count going below zero unless the program checks. The guard is the mirror image of the last one, INT_MIN this time, because decrementing the most negative int overflows in the other direction.

Program 3: The Bitwise Operators

Read a, b, and a shift count n, then compute and print c = a & b, d = a | b, e = ~a, f = a >> n, g = a << n, and h = a ^ b.

Algorithm:

  1. Start.
  2. Read a, b, and n.
  3. If a or b is negative, or n is outside 0 to 15, or a << n would not fit in an int, print a message and stop.
  4. Compute the six results and print each with its expression.
  5. Stop.

Run it with 12 10 2 and check the first, second, and last lines against the bits by hand:

a     = 12 = 1100
b     = 10 = 1010
a & b =  8 = 1000    1 only where both are 1
a | b = 14 = 1110    1 where either is 1
a ^ b =  6 = 0110    1 where they differ

Writing that table in the lab record is the point of the program; the examiner wants to see that &, |, and ^ were worked column by column, not looked up.

The shifts are multiplication and division by powers of two: 12 >> 2 is 3 and 12 << 2 is 48. And ~12 prints -13, which surprises everyone the first time. ~ flips every one of the 32 bits of the int, including the sign bit, and in two's complement, the representation this machine uses, the flipped pattern is the number −(a + 1). C89 does not promise two's complement, so ~a on a signed value is implementation-defined; the relationship ~a == -a - 1 is what you will see on any machine you meet, and the phrase "implementation-defined" is the honest answer to why.

The three guards exist because the shift operators have the sharpest edges in the language. Shifting a negative value left is undefined behaviour, and shifting one right is implementation-defined, so a and b are held non-negative. Shifting by a negative count or by 32 or more, the width of an int here, is undefined, hence the range on n. And a << n is undefined if the result does not fit in an int, which is what the last check computes: a must be at most INT_MAX divided by 2 to the n. Try 100000 1 15 and watch that guard fire. The check for n stops at 15 rather than 31 for a reason from chapter 2: the standard only promises that an int has 16 bits, so a shift of 16 or more is already undefined on the smallest conforming machine, and the guard is written for the standard, not for this laptop.

Key Takeaways

  • Postfix x++ yields the old value and prefix ++x yields the new one; both leave x incremented, so they differ only when the result is used.
  • Incrementing INT_MAX or decrementing INT_MIN is signed overflow, which is undefined behaviour; <limits.h> gives you the names to check against.
  • Modifying a variable twice in one expression is undefined behaviour; no output is the correct output.
  • &, |, and ^ work column by column on the bits; show the binary table in the record.
  • ~a on this platform is -a - 1, a two's complement fact the standard calls implementation-defined.
  • Shifts: negative left operand undefined, negative or too-large count undefined, left-shift overflow undefined. Guard all three before shifting.