Data, operators, and control flow recap

Chapter 1 ended with a program that ran straight down the page and did the same thing whatever arrived on its input. This chapter gave you the rest of the whole-number family, the operators that combine values and the bits underneath them, and the statements that let a program choose a path and go round again. One idea runs through all five lessons: a type is an interpretation of bytes, and most of the trouble in C comes from an interpretation changing when you did not ask it to. Let's review each lesson before you test yourself.

Integers and Conversions

int is not the whole-number type but one member of a family, and the members differ in the two ways that matter: how many bytes they span and how those bytes are read. C guarantees only minimum ranges, at least 8 bits for char, 16 for short and int, 32 for long and 64 for long long, so any width a sizeof run reports is a fact about this platform rather than about the language. When a width is genuinely part of the requirement, as with a file format or a hardware register, <stdint.h> supplies exact ones such as int32_t, uint8_t and int64_t; plain int is right the rest of the time. char is the member whose name misleads, because it is a small integer holding a character code, so 'A' is a character constant whose value is 65 and %c and %d are two readings of the one byte. Whether plain char is signed or unsigned is implementation-defined, and that word earns its precision only against its neighbours: undefined means the standard requires nothing at all, unspecified means the implementation chooses among allowed behaviours without having to tell you, and implementation-defined means it chooses and must document the choice. Arithmetic brings its own conversions. Integer promotion widens any operand narrower than int to int before the operator sees it, so an unsigned char holding 200 plus 100 is honestly 300 and the truncation to 44 happens later, on the assignment back into one byte: narrow types are storage, not arithmetic. Signed and unsigned are the same bytes read two ways, with <limits.h> naming the boundaries and %u printing an unsigned int, which is why -1 read as unsigned is 4294967295 and why comparing the two signednesses converts the signed side and makes owed < budget false for -1 against 1, reported by -Wextra as -Wsign-compare. Integer division truncates toward zero rather than rounding, so 7 / 2 is 3 while 7 / 2.0 is 3.5, and a double stored into an int loses its fraction the same silent way. A cast such as (double)total / parts converts one operand explicitly and the other follows, but (double)(total / parts) converts the 3 and arrives too late, which is the whole lesson about where parentheses stop. Last comes the asymmetry with the widest gap in the chapter: unsigned overflow wraps and the standard defines it, while signed overflow is undefined behaviour that draws no warning, runs to completion and passes straight through AddressSanitizer, because ASan detects memory errors and not arithmetic ones. Compute in a type wide enough to hold the result.

Operators, Expressions, and Constants

An expression is any piece of code that produces a value, and C's arithmetic set is five characters wide. Four read on sight; the fifth, %, is the remainder, and it hands back exactly what integer division discarded, so 17 / 5 is 3 and 17 % 5 is 2 and the pair splits one number into a quantity and a leftover. It is integer only, and dividing or taking a remainder by zero is undefined behaviour, which is why the exercises kept initializing a divisor to 1 until there was a way to inspect one. The remainder takes the sign of the dividend, so -7 % 2 is -1 rather than the 1 of mathematical modulo, and you never have to memorize that because it falls out of C's guarantee that (a / b) * b + a % b equals a. Precedence decides which operator claims its operands first, *, / and % binding tighter than + and -, which is why first + second / 2.0 is not an average of anything; the table runs to fifteen levels and the habit beats the table, so parenthesize whenever an expression would make a reader pause. Compound assignment updates a variable in place, score += 5 saying once what score = score + 5 says twice, and -=, *=, /= and %= match it. Prefix ++i yields the new value and postfix i++ the old, they are identical as standalone statements, and the expressions where the difference shows are the ones not to write: modifying an object twice between sequence points, as in i = i++, is undefined behaviour, and the order in which a function's arguments are evaluated is unspecified, so printf("%d %d\n", i++, i) has no answer either. gcc reports both as -Wsequence-point, and the fix for both is one side effect per statement. Finally, const names a fixed value instead of scattering a magic number, assigning to it is a compile error rather than a warning, and a C const object is still an ordinary object rather than a compile-time constant.

Bits and Bitwise Operators

Under every interpretation are the bits. A byte is 8 bits with place values doubling from the right, so 00101001 is 32 plus 8 plus 1, or 41. Hexadecimal belongs with them rather than in the trivia, because sixteen digits is exactly what 4 bits hold: split that byte in half, read each half, and it is 0x29. C17 has no binary specifier, but %x prints an unsigned int in hex and %#x adds the prefix, and reading 0x16 tells you which bits are set where the decimal 22 tells you nothing. Four operators address bits directly: & gives 1 where both operands have a 1, | where either does, ^ where exactly one does, and unary ~ flips every bit. A value naming the positions an operation should act on is a mask, and it yields four idioms worth knowing by name: set with flags | mask, test with (flags & mask) != 0, clear with flags & ~mask, and toggle with flags ^ mask, with the compound forms |=, &= and ^= writing the first three as one statement. Promotion reaches down here too, exactly as it did in arithmetic, so ~ on a uint8_t holding 15 produces the int -16 and the expected 240 appears only once the result is stored back into the narrow type. Shifts move bits sideways, and although value << n multiplies by a power of two, the shift you will actually write is 1u << n, the mask for bit n, paired with (flags >> n) & 1u to extract one bit as a plain 0 or 1. Three rules explain why every value in that lesson was unsigned: shifting by a negative amount or by at least the width of the promoted operand is undefined behaviour, left-shifting a signed value into or past its sign bit is undefined too, and right-shifting a negative value is implementation-defined. Do bit work on unsigned types and not one of those questions can arise. One pair of parentheses is not optional: == and != bind tighter than &, | and ^, so flags & mask == 0 quietly means flags & (mask == 0) and always yields 0. Always write (flags & mask) == 0.

Branching with if and switch

The six comparison operators ask C's questions, and each produces an int that is 1 when the relation holds and 0 when it does not. if runs its block when that value is nonzero, and two habits come with it: brace the body even for one statement, and write the comparison out rather than leaning on a value being nonzero. This is where the chapter's two outstanding debts were paid, because checking scanf's conversion count and checking a divisor before dividing are the same move, and undefined behaviour cannot be detected after the fact: you check before, or not at all. A return in main ends the program then and there, so return 1; is both an exit code saying the run failed and the way out of an error branch. Conditions combine with &&, || and !, and && and || short-circuit, evaluating the left operand first and the right one only if the answer is still in doubt. That ordering is guaranteed by the standard, which is precisely what makes divisor != 0 && dividend % divisor == 0 a safe guard, and it is the exception to the unspecified argument order two lessons earlier. Two ways of writing an if that lies both compile: if (converted = 1) assigns rather than compares and is therefore true on every run, and a stray semicolon in if (score > 100); makes the empty statement the body while the block below it runs unconditionally. gcc names both under -Wall -Wextra and neither warning is on by default, which is the case for those flags in a sentence. switch replaces a chain of else if when one integer is checked against fixed constants, and "runs from there" is the whole of it: a case is a label rather than a box, so control enters at the match and carries on until a break stops it. Deliberate fallthrough is expressed with a /* falls through */ comment that gcc reads as a claim, the labels must be integer constant expressions, and always write a default.

Repeating with Loops

while evaluates its condition before each pass and may run its body zero times, for gathers the three parts every counted loop needs onto one line where a reader checks them in a glance, and do ... while moves the test to the bottom so the body always runs at least once, with a semicolon after the closing while (...). Declare the counter in the for initializer so it exists only inside the loop, and brace every body. A loop whose body never changes what its condition tests never ends, which is the cheapest loop bug to find. The fencepost is the expensive one: i < n starting from 0 runs exactly n times with the counter taking every value up to n - 1, so the bound you typed is the count, while <= runs n + 1 times and looks no different, which is why count from 0 and use < is a default rather than an option. The countdown written with an unsigned counter is the chapter's sharpest illustration that defined is not the same as correct: i >= 0 on an unsigned type is not a question but the constant 1, --i at zero wraps to a huge value exactly as the standard promises it must, and the result is a perfectly well-formed loop that never ends, caught only by -Wextra as -Wtype-limits. Count down in a signed counter. break leaves a loop and continue starts its next pass, reading best as a guard at the top of the body, though a continue still runs a for header's update and skips one written at the bottom of a while body. The chapter's closing payoff is that the condition can be the read: scanf returns the number of conversions it completed, so while (scanf("%d", &value) == 1) ends on non-numeric text and on EOF alike with no sentinel and no count announced in advance. Here is the chapter in one program, guarding before it operates in three separate places.

#include <stdio.h>

int main(void)
{
    unsigned int seen = 0u;
    int value = 0;
    int count = 0;

    while (scanf("%d", &value) == 1)
    {
        ++count;
        if (value >= 0 && value < 32)
        {
            seen |= 1u << value;
        }
    }

    if (count == 0)
    {
        printf("no numbers read\n");
        return 1;
    }

    printf("read %d numbers, %d full triples with %d left over, bits seen %#x\n",
           count, count / 3, count % 3, seen);

    return 0;
}

Given the input 4 7 12 7 9 3 30 stop:

read 7 numbers, 2 full triples with 1 left over, bits seen 0x40001298

The conversion count ends the loop, the short-circuit && keeps the shift count in range so the mask is never undefined, / and % split the count into whole triples and a leftover, and the empty-input branch runs before anything divides by zero.

Key Terminology

  • Integer promotion: The widening of any operand narrower than int to int before an arithmetic or bitwise operator sees it
  • Undefined, unspecified, implementation-defined: No requirement at all; a choice among allowed behaviours that need not be documented; a choice that must be
  • Truncation toward zero: What integer division and a double stored into an int both do to a fraction, discarding it rather than rounding
  • Cast: A type name in parentheses converting a value explicitly, as in (double)total / parts, and a last resort rather than a habit
  • Wrap: The defined reduction of an unsigned result modulo one more than the maximum, with no signed equivalent
  • Remainder: The % operator, returning what integer division discarded and taking the sign of the dividend
  • Precedence: Which operator claims its operands first, overridden by parentheses
  • Compound assignment: +=, |= and their family, reading a variable, applying an operator and writing back in one statement
  • Sequence point: An ordering point in evaluation; modifying an object twice between two of them is undefined behaviour
  • Mask: A value naming the bit positions an operation should act on, usually built as 1u << n
  • Short-circuit: The guarantee that && and || evaluate their left operand first and the right one only if it can still change the answer
  • Guard: A check placed before a dangerous operation, since undefined behaviour cannot be detected afterwards
  • Fallthrough: Control carrying on from one switch case into the next, deliberate only when a /* falls through */ comment says so
  • Fencepost: The off-by-one family, from 0 to 3 inclusive being 4 numbers rather than 3
  • Conversion count loop: while (scanf("%d", &value) == 1), reading until the input stops converting

Looking Forward

You can now pick a type that holds what you need, combine values with operators that will not surprise you, reach the bits underneath, and steer a program with if, switch and the three loops. Everything so far has lived inside a single main, in the single stack frame chapter 1 described. Chapter 3 gives you more than one: writing your own functions, watching the call stack grow and shrink through recursion, and passing addresses so that a function can write into memory it does not own, which is also where a value can outlive or fail to outlive the frame that held it.