65. Branches and loops
Branches choose what to do; loops repeat. That is enough to
write down any algorithm — and this is exactly where most of the errors
the compiler lets through live: mixed-up precedence, a stray
semicolon, a forgotten break, a condition that never becomes false.
The good news: the compiler warns about half of these errors on its own. Along the way, the section shows which ones — and which are caught only by a separate option.
Everything said about warnings was taken from armclang (Keil). GCC without options
warns about none of these errors: it catches assignment in a condition,
x & 1 == 0 and the dangling else with -Wall, an empty if body and fall-through
in switch with -Wextra. CubeIDE puts -Wall into the project by itself (section 4б.3);
-Wextra is added by hand: a project starts with -Wall -Wextra (section 77.4).
On a microcontroller, loops also have a role of their own: the whole program is one infinite loop, and in memory it looks very recognisable.
Terms used in the documentation
Term Meaning relational, equality operators operators that compare two values logical operators NOT, AND, OR on truth values short-circuit evaluation the right operand is evaluated only if needed operator precedence, associativity which operator binds first, and in which direction they group conditional operator the ternary operator ? :dangling else an elsethat belongs to the nearestiffall-through execution running on into the next caseloop body, iteration the repeated statements, one pass through them pre-test, post-test loop a loop that checks its condition before, after the body infinite loop a loop that never ends busy-wait waiting by spinning in a loop timeout a limit on how long to wait
65.1 Comparisons give 0 or 1
==, !=, <, >, <=, >= return a number of type int: 1 if
the relation is true, and 0 if it is false. There is no separate boolean value
here — so the result of a comparison can be added, multiplied or
stored in a variable:
int a = (5 == 3); /* 0 */
int b = (100 >= 1); /* 1 */
int n_positive = (x > 0) + (y > 0) + (z > 0);
And the other way round: the condition of an if or a loop treats only zero as false,
and everything else as true. if (count) means if (count != 0).
== and = differ by one character and mean completely different things. About
if (a = 5) — below, in 65.4.
65.2 Logical operators and short-circuit evaluation
!, &&, || also give 0 or 1. Their main property is short-circuit
evaluation: the right operand is evaluated only if the result depends on it.
- in
a && b, whenais false the result is already known — false, andbis not evaluated; - in
a || b, whenais true the result is true, andbis not evaluated.
Protection is built on this:
int n = 0, b = 5;
int r = (n != 0 && b / n); /* 0 — и никакого деления на ноль */
The division never happened at all. The order here is guaranteed: after the left
operand of && and || there is a sequence point (section 64.10), so
"check first, then use" is a reliable technique.
Bitwise & and | do not short-circuit: both operands are always
evaluated. Replacing && with & "for speed" means losing the protection.
⚠️ Integer division by zero on Cortex-M3, M4, M7 and M33 does not stop the program by default — it quietly gives zero. On a PC such a program would crash at once. On the chip the error goes unnoticed until the trap is enabled — bit 4
DIV_0_TRPin theSCB->CCRregister (section 7):SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;. When it fires, it raises a UsageFault, and if that handler is not enabled inSCB->SHCSR, a HardFault (section 35.2). Enabling it in a debug build is a good habit. Cortex-M0 and M0+ (F0, G0, L0, C0) have no hardware divider and no such bit: the line above will not build there, and what a division by zero yields is decided by the library division function.
65.3 Precedence: where parentheses are mandatory
Operators are applied in order of precedence — from highest to lowest:
| Precedence | Operators | Grouping |
|---|---|---|
| 1 | () [] -> ., postfix ++ -- |
→ |
| 2 | ! ~, prefix ++ --, unary + - * & sizeof (type) |
← |
| 3 | * / % |
→ |
| 4 | + - |
→ |
| 5 | << >> |
→ |
| 6 | < <= > >= |
→ |
| 7 | == != |
→ |
| 8 | & |
→ |
| 9 | ^ |
→ |
| 10 | \| |
→ |
| 11 | && |
→ |
| 12 | \|\| |
→ |
| 13 | ? : |
← |
| 14 | = += -= and the other assignments |
← |
| 15 | , |
→ |
That is why *p++ is *(p++): the postfix ++ from row 1 binds tighter than
the asterisk from row 2 (section 67.4).
You do not need to remember most of the table — it all works as in arithmetic. But two rows go against intuition, and both hit code for microcontrollers, where bitwise operations are at every step.
Comparison binds tighter than bitwise operators (rows 7 and 8):
if (x & 1 == 0) /* на деле x & (1 == 0), то есть x & 0 — всегда ноль */
if ((x & 1) == 0) /* правильно: младший бит равен нулю */
For an even x = 4 the first line gives 0, the second gives 1. The parity check
"never fires", and it is hard to spot by eye.
Addition binds tighter than shift (rows 4 and 5): 1 << 2 + 1 is 1 << 3,
that is 8, not 5.
Fortunately, armclang warns about x & 1 == 0 without any
options: "& has lower precedence than ==; == will be evaluated first";
GCC does so with -Wall: "suggest parentheses around comparison in operand of '&'".
A rule from practice: a bitwise operator next to a comparison, or a
shift next to arithmetic, always goes in parentheses — even when the precedence
happens to match what you intended.
65.4 if and else
if (условие)
действие;
else if (другое условие)
другое действие;
else
действие по умолчанию;
Braces are needed when a branch has more than one statement. In
practice they are always written: otherwise a second line added a month later
quietly ends up outside the if.
A dangling else belongs to the nearest if, not to the one
it was aligned under:
if (a)
if (b)
return 1;
else /* относится к if (b), хотя выровнен под if (a) */
return 2;
armclang warns about this on its own: "add explicit braces to avoid
dangling else"; GCC does with -Wall.
Assignment instead of comparison is the error discussed in section 64.9:
if (a = 5) /* a становится 5, условие всегда истинно */
For this one armclang also has a warning without options: "using the
result of an assignment as a condition without parentheses"; GCC has it
with -Wall. If an assignment in a
condition really is intended, it is put in double parentheses — a sign
to both the compiler and the reader.
A semicolon after the condition is the third classic:
if (error); /* пустой оператор — условие ни на что не влияет */
stop_motor(); /* выполняется всегда */
Here the compiler warns too — "if statement has empty body", and
the same for while — but only when the semicolon is on the same
line as the condition. An empty statement on a separate line it considers
intentional and stays silent. GCC catches an empty if body with -Wextra — on a
separate line too, but says nothing about while; -Wall, in turn, notices the misleading
indentation of stop_motor() under such an if.
65.5 The ternary operator
condition ? value1 : value2 is an if-else that returns a
value and therefore fits inside an expression:
int max = a > b ? a : b;
printf("LED %s\n", led_on ? "ON" : "OFF");
Chains group from the right: a ? b : c ? d : e reads as
a ? b : (c ? d : e).
A frequent question — is it faster than if? No. Functions that compute a maximum
with if-else and with the ternary operator are turned by the compiler into
the same machine code, instruction for instruction. Choose by
readability: a short choice of a value — the ternary operator; everything else — if.
Nested ternary operators read badly; it is better to avoid them.
65.6 switch
switch picks a branch by an integer value; the case labels must be
constants:
switch (a) {
case 1: printf("one "); break;
case 2: printf("two "); /* break забыт */
case 3:
case 4: printf("three "); break;
default: printf("default ");
}
break is not a formality. Without it, execution falls through into
the next branch. Here is what this fragment prints:
a |
Output |
|---|---|
| 1 | one |
| 2 | two three — fall-through |
| 3 | three |
| 4 | three |
| 5 | default |
Sometimes fall-through is needed: case 3: with no body sits right above case 4:, and both
labels lead to the same branch — this is a common technique. It is bad when the fall-through
is accidental, as with case 2.
And here is an unpleasant detail: armclang is silent about accidental fall-through —
even with -Wall -Wextra. Only the separate option
-Wimplicit-fallthrough catches it. In GCC this option is part of -Wextra, and fall-through
is visible with that already. To keep an intended fall-through from causing a
warning, it is marked:
case 1:
prepare();
__attribute__((fallthrough)); /* провал задуман */
case 2:
run();
break;
default can stand anywhere, but it is usually placed last. There
break is formally not needed — but it is written anyway: if a new label is later
added below, there will be no fall-through.
65.7 Three loops
| Loop | Condition checked | Runs at least once? |
|---|---|---|
while (condition) { … } |
before the body | no |
do { … } while (condition); |
after the body | yes |
for (init; condition; step) { … } |
before the body | no |
for is a while gathered into one line:
for (i = 1; i <= 5; i++) { i = 1;
действие; while (i <= 5) {
} действие;
i++;
}
Any of the three parts of the for header can be empty; for (;;) is
a loop without a condition, that is, an infinite one.
A few checks of understanding — what is a after each loop:
int i, a = 1;
for (i = 1; i < 4; i++) a++; /* 3 шага: a = 4 */
int a2 = 1;
for (i = 1; i < 4; i++) a2 = a2 + i; /* 1 + 1 + 2 + 3: a2 = 7 */
int a3 = 1, b = 2;
for (i = 3; i >= 1; i--) a3 += b; /* 3 шага по 2: a3 = 7 */
i++ or ++i in the header? For an integer counter there is no difference:
the compiler produces the same code. Nobody uses the value of the expression here;
only the increment itself remains.
The exit condition is written as an inequality, not an equality. The loop
n = 2;
while (n != 5) { …; n += 2; } /* 2, 4, 6, 8, 10, … — бесконечно */
never ends: stepping by 2 from two, you cannot land on five. With
n < 5 it would stop after 4.
A floating-point counter is even worse. Adding 0.1f to zero ten times
gives not 1 but 1.000000119: 0.1 is an infinite fraction in binary
(section 63.15), and the error accumulates. The loop
for (x = 0; x != 1.0f; x += 0.1f) will not stop. Count with an integer
counter and compute the floating-point value from it.
An unsigned counter counting down — for (uint8_t i = 10; i >= 0; i--) —
is also endless: an unsigned value is always greater than or equal to zero. This case
is covered in section 3.5.
65.8 break, continue, goto
continue moves to the next iteration, skipping the rest of the body; the for
step is still executed:
for (i = 1; i < 5; i++) {
if (i == 3) continue;
printf("%d ", i); /* 1 2 4 */
}
break leaves the nearest loop or switch — only one
level. You cannot leave two nested loops with one break.
goto exists in C, and it has one generally accepted use — leaving
deep nesting for common cleanup code:
for (...) {
for (...) {
if (fatal) goto cleanup;
}
}
cleanup:
disable_peripheral();
Everything else — jumps backwards, into the middle of a loop, "to avoid writing a
loop" — makes the program unreadable. The rule is simple: goto only
forwards and only to the cleanup.
65.9 The infinite loop — the heart of a microcontroller program
On a PC a program ends by returning control to the system.
On a microcontroller there is nowhere to return to: main never finishes.
int main(void)
{
init_hardware();
for (;;) { /* или while (1) — это одно и то же */
poll_buttons();
update_display();
}
}
The compiler turns an empty infinite loop for (;;) { } or while (1) { }
into one two-byte instruction — a branch to itself. In
machine code this is e7fe, and it is one of the few combinations that
are worth recognising on sight.
Because you will meet it in the debugger. The default interrupt
handler in the F103 and F407 startup files is built exactly the same way: one
label for all unhandled interrupts, followed by a branch to itself. If you
enable an interrupt and forget to write a handler — or misspell its
name by one letter — the program ends up there at the very first interrupt and
"hangs". Halt it with the debugger and you will see exactly e7fe
(sections 7 and 35).
65.10 Wait loops
A delay loop without volatile disappears. The function
void delay(void) { for (uint32_t i = 0; i < 100000; i++) { } }
turns into a single instruction with optimisation — a return. The compiler saw
that the loop does nothing and threw it away. With volatile on the counter the loop
stays: the variable is honestly read from and written to memory at every step.
But even such a delay is bad: its duration depends on the core clock, the optimisation level and even the alignment of the code in Flash. Time is measured with a timer — the system one (section 7.4) or an ordinary one (section 13).
Waiting for a flag must have a limit. The loop
while (!(USART1->SR & USART_SR_TXE)) { } /* а если флаг не поднимется? */
hangs forever if the peripheral fails. In production code the wait is limited in time:
uint32_t start = ticks;
while (!(USART1->SR & USART_SR_TXE)) {
if (ticks - start > TIMEOUT_MS) return ERR_TIMEOUT;
}
And in case something unforeseen hangs, there is the watchdog timer: it restarts the chip if the program stops checking in (section 12). This is the answer to the frequent question "is there protection against an endless loop" — there is, and on a microcontroller it is in hardware.
Why the flag is declared volatile and what happens without it — section 3.4.
65.11 Loop exercises
Digits of a number — the remainder of division by 10 gives the last digit, the division drops it:
int n = 12345, count = 0, sum = 0;
while (n != 0) {
count++;
sum += n % 10;
n /= 10;
} /* count = 5, sum = 15 */
Greatest common divisor. By subtraction: subtract the smaller from the larger until the numbers are equal. By remainder: replace the larger with the remainder of dividing it by the smaller until the remainder becomes zero.
while (b != 0) {
int t = b;
b = a % b;
a = t;
} /* в a — НОД */
For 14 and 35 both methods give 7 in three steps. The difference shows on awkward numbers: for 1000000 and 3, subtraction needs 333335 steps, the remainder method — two. The same algorithm, a different way of writing the step — and a difference of hundreds of thousands of times. On estimating algorithms — section 76.
Brute force. How many numbers from 102 to 987 are written with three different
digits? A loop over all of them, three digits via / and %, three comparisons — 648.
Reasoning gives the same answer: the first digit can be chosen in nine
ways, the second in nine, the third in eight.
65.12 A side-effects exercise
What do you get after each line if you start with a = 5, b = 7?
(a += 5) * (b -= 3); /* 10 * 4 = 40; a = 10, b = 4 */
--b / (a++ - 3); /* 3 / 7 = 0; a = 11, b = 3 */
(a -= 2) || 47 / (b - 7); /* 1; a = 9, b = 3 */
(a *= b) + (b *= a); /* НЕЛЬЗЯ */
In the third line the right operand of || was not evaluated: the left one is already true.
The fourth is undefined behaviour: a and b are read and modified in
one expression without a sequence point (section 64.10). Different compilers
give different results, and it has no correct answer.
65.13 Pitfalls
A bit check never fires. x & 1 == 0 is
x & (1 == 0). You need parentheses: (x & 1) == 0.
An if is always true. if (a = 5) — assignment instead of comparison.
An else fired under the wrong condition. A dangling else belongs to the
nearest if.
An action is always executed. A semicolon after if (…) or
while (…).
Two branches run in a switch. A forgotten break. The option
-Wimplicit-fallthrough finds such places.
A loop does not end. A != condition with a step that jumps over the
boundary; a floating-point counter; an unsigned counter counting down.
break did not leave the outer loop. It leaves only one
level.
A delay vanished after optimisation was turned on. A counter without
volatile; better still — a timer.
A delay "drifts" when the clock changes. The loop counts instructions, not time.
The program hung at e7fe. It landed in the default handler:
the interrupt is enabled, but there is no handler or its name is misspelled.
The program hangs waiting for a flag. There is no limit on the waiting time.
Division by zero caused no error. On Cortex-M3, M4, M7 and M33 the result
is zero by default; the trap is enabled with the DIV_0_TRP bit. M0 and M0+ do not have it:
division is done by a library function.
65.14 Summary
- Comparisons and logical operators give an
int: 1 or 0. A condition treats only zero as false. &&and||evaluate the right operand only when needed — checks of the form "make sure first, then use" are built on this.&and|evaluate both operands.- Comparison binds tighter than bitwise operators:
x & 1 == 0is always zero. Addition binds tighter than shift:1 << 2 + 1is 8. armclangwithout options warns aboutif (a = 5), aboutx & 1 == 0, about a danglingelseand about a semicolon right after a condition. About fall-through inswitch— only with-Wimplicit-fallthrough. GCC without options is silent about all five:-Wallcatches three, the empty body and fall-through —-Wextra.- The ternary operator and
if-elsegive the same code; so doi++and++iin aforheader. - The loop exit condition is written as an inequality; the counter is an integer.
- An empty infinite loop is
e7fe, and the default interrupt handler looks the same. - A delay loop without
volatileis thrown away by the optimiser; time is measured with a timer, and a wait for a flag is limited in time. - GCD by remainder for 1000000 and 3 takes two steps; by subtraction — 333335.
Next: section 66 "Functions, scope, recursion".