JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Making Decisions: Operators and Control Flow

A program that only runs straight down the page is just a long receipt. Here you teach C to compute, compare, and choose — so it can do different things for different inputs.

From a straight line to a branching road

In the last guide your first real program ran top to bottom: declare a variable, print it, stop. That is a straight road. But the interesting programs in the world ask questions — *is this number even?*, *did the file open?*, *is the password right?* — and then take a different turn depending on the answer. The machinery that lets a program take a turn is called control flow, and the questions it asks are built out of operators. This guide is where C stops reciting and starts deciding.

Everything here rests on a single quiet fact about C: a decision is just a number. C has no separate 'truth' substance hiding under the hood — when it asks whether something is true, it computes a value, and the rule is simply that zero is false and anything non-zero is true. So `if (3)` always runs, `if (0)` never does, and `if (x - x)` never does either, because x - x is 0. Once you internalise that, the operators below stop being magic punctuation and become ordinary arithmetic whose answer happens to steer the road.

Operators: the verbs that build a question

Operators come in families. The arithmetic ones (+, -, *, /, %) compute; note that for two ints, / truncates (7 / 2 is 3, not 3.5) and % gives the remainder (7 % 2 is 1). The relational ones (<, <=, >, >=) and the equality ones (== and !=) compare two values and hand back 1 for true or 0 for false — they are how you turn 'is x bigger?' into a number. The logical ones (&&, ||, !) glue those answers together: && is 'and', || is 'or', ! flips true and false.

Two refinements matter. First, precedence decides what binds first, the same way * binds before + in maths: `2 + 3 * 4` is 14, and `x < 10 && y > 0` reads as `(x < 10) && (y > 0)`. You do not have to memorise the whole table — when in doubt, add parentheses; clear beats clever. Second, && and || short-circuit: in `a && b`, if a is false, b is never evaluated, because the answer is already settled. That is not a quirk to tolerate but a tool to use — `if (p != NULL && p->ready)` safely checks p before ever touching what it points at.

if, else, and the truth value underneath

The first branch you reach for is `if`. It takes a value in parentheses, and if that value is non-zero it runs the block that follows; an optional `else` catches the zero case. You can chain `else if` to test several conditions in order, top to bottom, taking the first that matches. Because the test is 'just a number', you can write `if (count)` to mean 'if count is non-zero' — common and idiomatic, though `if (count != 0)` says the same thing more loudly, and loud is often kinder to the next reader.

There is a quiet trap in the truth value, though, and it is worth naming honestly. C's `int` truth works fine for whole numbers, but floating-point comparison can surprise you: 0.1 + 0.2 is not exactly 0.3 in the machine's number system, so `if (a == b)` on floats can be false even when the maths says equal. The cure is to compare within a small tolerance, not for exact equality. And C99 did add a real boolean — include stdbool.h and you get `bool`, `true`, `false` — which makes intent clearer even though under the hood it is still that same 0-or-1 integer.

Loops, and the ternary shortcut

A branch lets a program go one of two ways once; a loop lets it do something many times. C offers three. `while (cond) { ... }` checks first, then repeats while the condition holds — good when you might run zero times. `do { ... } while (cond);` runs the body once before checking — good when you must run at least once. And `for (init; cond; step) { ... }` packs the three ceremonies of counting — start, test, advance — onto one line, which is why it is the natural choice for 'do this N times'. All three lean on the same non-zero-is-true rule from the start of this guide.

for (int i = 0; i < 3; i++) {
    printf("%d\n", i);
}
// 1. init:  i = 0
// 2. test:  i < 3 ?  -> if false, stop
// 3. body:  printf
// 4. step:  i++   -> back to test
// prints 0, 1, 2  (NOT 3 -- the test fails at i == 3)
The four beats of a for-loop, and why it stops at 2: the test 'i < 3' is checked before each body, so when i reaches 3 the loop ends.

Two keywords steer from inside a loop: `break` jumps out of the loop entirely, and `continue` skips the rest of this round and goes straight to the next iteration. Both are great for early exits — 'stop as soon as you find it' — but a loop riddled with them gets hard to follow, so reach for them deliberately. C also has `switch`, a tidy multi-way branch on a single integer value; just remember each `case` falls through into the next unless you end it with `break`, which is itself a classic source of bugs.

When a choice is small enough to fit in an expression, the ternary operator `cond ? a : b` earns its keep: it evaluates cond and yields a if true, b if false. So `int max = (x > y) ? x : y;` reads almost like English and avoids a four-line `if`/`else`. Use it for choosing a value; do not abuse it to choose between actions with side effects, where a plain `if` is clearer.

Honest hazards: conversions and undefined behaviour

Operators look innocent, but they sit on top of how C handles types, and that is where decisions quietly go wrong. When you mix types in a comparison or arithmetic, C performs implicit conversions first. The sharpest edge: comparing a signed int against an unsigned value promotes the signed one to unsigned, so `-1 < 1u` is false, because -1 becomes a huge positive number. A loop written `for (unsigned i = n; i >= 0; i--)` never ends, because an unsigned value is always >= 0. These are not exotic — they are everyday slips, and they are why you read your own conditions twice.

The deeper hazard is undefined behaviour (UB), and it deserves a straight word now rather than a shock later. Some operations have no defined meaning in C: signed integer overflow, dividing by zero, or shifting by a width past the type's size. The crucial thing — and the part beginners are usually told wrong — is that UB does not mean 'whatever the platform happens to do'. The standard lets the optimizer assume UB never happens, and reason from there. That is why a bug can hide at `gcc -O0` and then corrupt your data at `gcc -O2`: the optimizer, trusting that overflow can't occur, deleted the very check you wrote to catch it.

None of this should scare you off — it should make you tidy. Turn warnings on (`gcc -Wall -Wextra`), brace every branch, parenthesise when unsure, prefer == not = in tests, and pick unsigned types only when you truly mean 'cannot be negative'. With those habits, control flow becomes the most reliable part of your program. And the same yes/no thinking returns at the very top: a C program ends by handing a number back to the operating system as its exit status — 0 for success, non-zero for failure — so even your program's final decision is, once again, just a number.