Part 1 · The C Language30 min read
Operators, Expressions, and Implicit Conversions
Most C bugs that survive code review are not logic errors. They are expressions that do not mean what they look like: a mask test that compares before it masks, a byte shifted into the sign bit, a negative number that becomes four billion when compared with an unsigned length. C silently converts operands behind your back, and its precedence rules were designed in 1972 for reasons that no longer apply. In firmware these bugs hit hardest, because register manipulation is wall-to-wall bitwise operators on mixed-width unsigned types. This lesson teaches you to evaluate any C expression the way the compiler does.
Precedence and associativity
Section titled “Precedence and associativity”Precedence decides how operators group when you write no parentheses: a + b * c is a + (b * c). Associativity decides how operators of the same precedence group: a - b - c is (a - b) - c (left to right), while a = b = c is a = (b = c) (right to left).
Here is the table, highest precedence first. The full version with every operator is on the C quick reference page.
| Level | Operators | Associativity |
|---|---|---|
| 1 | () call, [], ->, ., postfix x++ x-- | left to right |
| 2 | prefix ++x --x, unary + - ! ~, cast (type), * dereference, & address, sizeof | right to left |
| 3 | * / % | left to right |
| 4 | + - | left to right |
| 5 | << >> | left to right |
| 6 | < <= > >= | left to right |
| 7 | == != | left to right |
| 8 | & (bitwise AND) | left to right |
| 9 | ^ | left to right |
| 10 | | | left to right |
| 11 | && | left to right |
| 12 | || | left to right |
| 13 | ?: | right to left |
| 14 | = += -= *= /= %= <<= >>= &= ^= |= | right to left |
| 15 | , (comma operator) | left to right |
You do not need to memorize all fifteen levels. You need to recognize the handful of cases where the table disagrees with intuition.
The bitwise operators bind more loosely than comparisons
Section titled “The bitwise operators bind more loosely than comparisons”This is the most expensive design mistake in C. &, ^ and | sit below == and !=:
#define FLAG_READY 0x04u
if (status & FLAG_READY == FLAG_READY) { /* parsed as status & (FLAG_READY == FLAG_READY) */ start_transfer();}FLAG_READY == FLAG_READY is 1, so the condition tests bit 0, not bit 2. The code works by coincidence whenever bit 0 and bit 2 happen to agree. GCC’s -Wall (via -Wparentheses) reports suggest parentheses around comparison in operand of '&'. Write it as (status & FLAG_READY) != 0u, or just (status & FLAG_READY) in a condition.
Shifts bind more loosely than arithmetic
Section titled “Shifts bind more loosely than arithmetic”uint16_t word = hi << 8 + lo; /* parsed as hi << (8 + lo) */uint32_t mask = 1u << n - 1; /* parsed as 1u << (n - 1), which may be what you meant, or not */The first line is a bug: it shifts hi by 8 plus the value of lo. Since + binds tighter than <<, combining bytes must be written (hi << 8) | lo or (hi << 8) + lo. -Wparentheses warns about + inside <<. Parenthesize every shift that appears next to arithmetic, even when you know the rule, because the next reader may not.
Unary operators bind tightest
Section titled “Unary operators bind tightest”!x & y is (!x) & y. If you meant “the y bit of x is clear”, that is !(x & y). And the postfix operators bind tighter than the prefix ones, which gives the most common idiom in C its meaning:
uint8_t value = *p++; /* *(p++): read the byte p points to, then advance p */(*p)++; /* increment the byte p points to; p does not move */*p++ uses the old value of p for the dereference (postfix ++ yields the value before incrementing) and advances p as a side effect. You will see it in every copy loop. Pointers get their own lesson in Pointers I: addresses, arithmetic, and const; for now, remember that *p++ moves the pointer.
Chained comparisons do not chain
Section titled “Chained comparisons do not chain”if (0 < x < 10) is (0 < x) < 10. The left part is 0 or 1, which is always less than 10, so the condition is always true. Write if (x > 0 && x < 10). GCC warns that such comparisons “do not have their mathematical meaning”.
Integer promotion
Section titled “Integer promotion”Here is the rule that explains half of the surprises in C: arithmetic is never performed in a type narrower than int. Before any arithmetic, unary +, - or ~, or a shift, every operand of a type smaller than int is converted (promoted) to int, provided int can represent all its values. If it cannot, it is promoted to unsigned int. On the Cortex-M, where int is 32 bits, char, signed char, unsigned char, short, unsigned short, int8_t, uint8_t, int16_t, uint16_t and bool all promote to int.
#include <stdio.h>#include <stdint.h>
int main(void){ uint8_t a = 200; uint8_t b = 100; uint8_t x = 0x0F; int neg = -7;
int sum = a + b; /* computed in int: 300 */ uint8_t sum8 = (uint8_t)(a + b); /* 300 converted to uint8_t: 44 */
printf("a + b as int: %d\n", sum); printf("a + b as uint8_t: %u\n", (unsigned)sum8); printf("sizeof(a + b): %zu\n", sizeof(a + b)); printf("~x: 0x%X\n", (unsigned)~x); printf("(uint8_t)~x: 0x%X\n", (unsigned)(uint8_t)~x); printf("-7 / 2 = %d, -7 %% 2 = %d\n", neg / 2, neg % 2); return 0;}a + b as int: 300a + b as uint8_t: 44sizeof(a + b): 4~x: 0xFFFFFFF0(uint8_t)~x: 0xF0-7 / 2 = -3, -7 % 2 = -1sizeof(a + b) is 4: the sum of two uint8_t values is an int. Mostly this protects you, because a + b does not wrap at 255 in the middle of an expression. But it bites in three classic ways.
The complement trap. ~x on a uint8_t complements all 32 bits of the promoted int:
uint8_t x = 0x0F;if (~x == 0xF0) { /* never true: ~x is 0xFFFFFFF0 (as int, -16) */ /* ... */}GCC’s -Wextra warns about comparing a promoted bitwise complement with a constant. Cast back to the narrow type before comparing: (uint8_t)~x == 0xF0.
The shift-into-the-sign-bit trap. Assembling a 32-bit value from received bytes:
#include <stdint.h>
uint32_t read_u32_le(const uint8_t b[4]){ return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24); /* bug */}b[3] promotes to int. If it is 0x80 or larger, b[3] << 24 produces a value that does not fit in a signed 32-bit int, which is undefined behavior. Most of the time you get the bits you expected, until the optimizer uses the assumption. The fix is to widen before shifting:
#include <stdint.h>
uint32_t read_u32_le(const uint8_t b[4]){ return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24);}Endianness and byte serialization are covered properly in Bit manipulation.
The 16-bit multiply trap. This one is truly sneaky:
uint16_t a = 65535;uint16_t b = 65535;uint32_t product = a * b; /* undefined behavior on a 32-bit int machine */Both operands are promoted to signed int. The product, 4294836225, exceeds INT_MAX, so this is signed overflow: undefined, even though every type you wrote is unsigned. On an AVR with 16-bit int, uint16_t promotes to unsigned int instead and the multiply wraps harmlessly. Same source, different semantics. Write (uint32_t)a * b.
What promotion costs on Arm
Section titled “What promotion costs on Arm”The Cortex-M has only 32-bit registers and 32-bit arithmetic instructions. When the result must be stored back into a narrow type, the compiler adds an instruction to truncate it:
#include <stdint.h>
uint8_t add8(uint8_t a, uint8_t b) { return a + b; }uint32_t add32(uint32_t a, uint32_t b) { return a + b; }add8: add r0, r0, r1 uxtb r0, r0 @ zero-extend byte: keep only the low 8 bits bx lradd32: add r0, r0, r1 bx lrThe uxtb is the conversion back to uint8_t. That is why on a 32-bit MCU, local variables and loop counters should be uint32_t or int rather than uint8_t: a uint8_t loop counter can cost an extra instruction per iteration to emulate wrapping at 256, and saves nothing, since it lives in a 32-bit register anyway. Use narrow types for storage (arrays, structs, packets), and natural-width types for computation.
The usual arithmetic conversions
Section titled “The usual arithmetic conversions”When a binary operator (+, *, <, ==, & and the rest) gets two operands of different types, C converts them to a common type using the “usual arithmetic conversions”:
- If either operand is floating point, the other is converted to the widest floating type involved (
long double, thendouble, thenfloat). - Otherwise, apply integer promotion to both operands.
- If both now have the same type, done.
- If both are signed, or both are unsigned, convert the narrower one to the wider one.
- If the unsigned operand’s type is at least as wide as the signed one’s, convert the signed operand to unsigned.
- Otherwise, if the signed type can represent every value of the unsigned type, convert the unsigned operand to the signed type.
- Otherwise, convert both to the unsigned version of the signed type.
Rule 5 is the dangerous one. Some examples, with 32-bit int (both on Cortex-M and x86-64):
| Expression types | Common type on Cortex-M (ILP32) | On x86-64 Linux (LP64) | Why |
|---|---|---|---|
int op unsigned int | unsigned int | unsigned int | rule 5 |
int32_t op uint8_t | int | int | uint8_t promotes to int first |
int16_t op uint16_t | int | int | both promote to int |
long op unsigned int | unsigned long | long | rule 7 on ILP32, rule 6 on LP64 |
int64_t op uint32_t | int64_t | int64_t | rule 6 |
int op float | float | float | rule 1 |
float op double | double | double | rule 1 |
The long row shows why code tested on a PC can break on the target: the same expression has a signed type on one machine and an unsigned type on the other.
The signed/unsigned comparison trap
Section titled “The signed/unsigned comparison trap”Put rule 5 together with a comparison:
#include <stdint.h>#include <stdbool.h>
bool over_limit(int32_t temperature, uint32_t limit){ return temperature > limit; /* bug when temperature is negative */}If temperature is -5 and limit is 80, the comparison converts -5 to uint32_t, giving 4294967291, which is greater than 80. A sensor reading below freezing triggers the over-temperature shutdown. The general form of the trap is memorable: -1 < 1u is false.
-Wextra enables -Wsign-compare for C, which catches this:
warning: comparison of integer expressions of different signedness: 'int32_t' {aka 'int'} and 'uint32_t' {aka 'unsigned int'} [-Wsign-compare](The aka types say long int and long unsigned int when you cross-compile for Arm, as you learned in Types, sizes, and how numbers are stored.) The fixes, in order of preference:
- Make the types agree by design. If a limit can be compared with a negative temperature, the limit should be signed too.
- Check the sign first:
return temperature >= 0 && (uint32_t)temperature > limit;. - Compare in a wider signed type that holds both ranges:
(int64_t)temperature > (int64_t)limit.
Two related traps. A loop for (int i = 0; i < len; i++) with size_t len compares signed with unsigned; use size_t i. And unsigned subtraction never goes negative: if a and b are unsigned, if (a - b < 0) is never true (GCC’s -Wextra says the comparison is “always false”), and strlen(s) - 1 on an empty string is SIZE_MAX. Compare a < b instead of subtracting.
Note that comparing int8_t with uint8_t, or int16_t with uint16_t, is safe on a 32-bit machine: both promote to int before the comparison and no value changes.
Division and remainder with negative numbers
Section titled “Division and remainder with negative numbers”Since C99, integer division truncates toward zero, and the remainder has the sign of the dividend. The identity (a / b) * b + a % b == a always holds when the division is defined.
| Expression | Result |
|---|---|
7 / 2 | 3 |
-7 / 2 | -3 (not -4) |
7 / -2 | -3 |
7 % 2 | 1 |
-7 % 2 | -1 (not 1) |
7 % -2 | 1 |
The negative remainder breaks a common idiom. Stepping backwards through a circular buffer with idx = (idx - 1) % SIZE; gives -1 when idx is 0 (if idx is signed), and indexing with -1 reads before the buffer. Write idx = (idx + SIZE - 1) % SIZE;, or use unsigned indexes with a power-of-two size and a mask, which Memory: where everything lives builds into a ring buffer.
Division by zero is undefined behavior, and so is INT_MIN / -1 (the true result, 2147483648, does not fit). What happens in hardware varies:
- The Cortex-M3 and M4 have hardware divide instructions (
SDIV,UDIV, 2 to 12 cycles). By default, dividing by zero returns 0 without any fault. Setting theDIV_0_TRPbit (bit 4) of the SCBCCRregister (0xE000ED14) makes it raise a UsageFault instead, which is far easier to debug. Watchdogs, faults, and recovering from them uses this. - The Cortex-M0 and M0+ have no divide instruction at all. Division calls a library routine (
__aeabi_idiv,__aeabi_uidiv) that takes dozens of cycles.
Division by a constant power of two is cheap, but only free for unsigned values. Unsigned x / 4 becomes a single right shift. Signed x / 2 needs a correction for negative numbers, because an arithmetic shift rounds toward minus infinity (-7 shifted right by 1 is -4) while division truncates toward zero (-3):
div2_signed: add r0, r0, r0, lsr #31 @ add 1 if x is negative asrs r0, r0, #1 @ arithmetic shift right bx lrIf a quantity cannot be negative, make it unsigned and the compiler can use the plain shift.
Shift rules
Section titled “Shift rules”Shifts are everywhere in firmware, and their rules are strict:
- The left operand is promoted first, and the result has the promoted type.
- If the right operand is negative, or greater than or equal to the width of the promoted left operand, the behavior is undefined.
x << 32on a 32-bitxis undefined, not zero. - For a signed left operand,
x << nis undefined ifxis negative or if the result is not representable. So1 << 31is undefined with a 32-bitint, because 2147483648 does not fit. - Right-shifting a negative signed value is implementation-defined. GCC and Clang perform an arithmetic shift (copying the sign bit), but do not rely on it in portable code.
The firmware conclusion: shift unsigned values, and write unsigned constants:
#include <stdint.h>
#define PIN_MASK(n) (1u << (n)) /* unsigned int: fine for n from 0 to 31 */#define BIT64(n) (UINT64_C(1) << (n)) /* needed for n from 32 to 63 */
uint32_t odr_bit_15 = PIN_MASK(15); /* 0x00008000 */uint32_t top_bit = PIN_MASK(31); /* 0x80000000, well defined */1u << 31 is fine: unsigned int is 32 bits and the result fits. 1u << 40 is still undefined because 1u is only 32 bits wide; you need a 64-bit left operand.
Why is an oversized shift undefined rather than zero? Because CPUs disagree. On x86, the hardware masks the shift count to 5 bits, so x << 32 returns x unchanged. On Arm, a register-specified shift uses the bottom byte of the count, so shifting by 32 to 255 yields 0. C refused to pick one and made both behaviors (and any other) legal. With constant operands the compiler may do something else again. Undefined behavior is not “whatever the CPU does”.
Compound assignment and increment
Section titled “Compound assignment and increment”x op= y means x = x op y, except that x is evaluated only once. That matters when x has side effects or is expensive, as in regs[i++] |= mask; (evaluated once, so i advances once).
The conversion back to the left-hand type happens silently:
uint8_t level = 200;level += 100; /* computed as int 300, stored as 44 */Neither -Wall nor -Wextra comments on this. That is the nature of C: narrowing on assignment is legal and silent unless you ask for -Wconversion (below).
Prefix ++i increments and yields the new value; postfix i++ increments and yields the old value. As statements on their own, i++; and ++i; are identical and generate identical code.
What neither form is, is atomic. On the Cortex-M4, counter++ on a global compiles to three instructions:
ldr r2, [r3] @ load counter from RAM adds r2, r2, #1 @ increment in a register str r2, [r3] @ store backIf an interrupt fires after the ldr and also increments counter, the str overwrites the interrupt’s update with a stale value: one increment is lost. Marking counter volatile does not change this. Interrupts done right shows how to make such updates safe.
The comma operator
Section titled “The comma operator”The comma operator evaluates its left operand, discards the result, then evaluates its right operand, which gives the value and type of the whole expression. There is a sequence point between the two. Its one common legitimate use is in for loops:
for (uint32_t i = 0, j = n - 1; i < j; i++, j--) { /* swap elements i and j */}Here uint32_t i = 0, j = n - 1 is a declaration list (a separator, not the operator) and i++, j-- is the comma operator. The commas between function arguments are also separators, not the operator. Because the comma operator has the lowest precedence of all, x = 1, 2; assigns 1 to x and then evaluates and discards 2. Outside for headers and macros, avoid it.
The conditional operator
Section titled “The conditional operator”cond ? a : b evaluates cond, then evaluates exactly one of a or b. It is the only way to choose between values inside an expression, for example in an initializer:
const uint32_t timeout_ms = fast_mode ? 10u : 100u;On Arm it usually compiles without a branch, using a conditional move inside an IT block. Two subtleties:
- The result type comes from the usual arithmetic conversions applied to
aandb. Soint64_t r = flag ? -1 : 1u;stores 4294967295 whenflagis true, not -1, because the common type ofintandunsigned intisunsigned int, and -1 is converted before it ever reaches theint64_t. - It is right-associative, so
a ? b : c ? d : eisa ? b : (c ? d : e). One level of nesting is readable when formatted on separate lines; more than that belongs in anifchain.
sizeof is evaluated at compile time
Section titled “sizeof is evaluated at compile time”sizeof is an operator, not a function. Applied to a type it needs parentheses (sizeof(uint32_t)); applied to an expression it does not (sizeof buf). Its result is an integer constant expression of type size_t, computed by the compiler from the type of the operand. The operand is not evaluated:
uint32_t i = 0;size_t n = sizeof(i++); /* n is 4; i is still 0 */No code is generated for i++. The single exception is a variable-length array, whose size is only known at run time, so sizeof on a VLA is computed at run time. One more surprise: sizeof('a') is sizeof(int), 4, because character constants are int in C.
Because sizeof is a compile-time constant, it is free: use it instead of hard-coded sizes everywhere, as in memset(&cfg, 0, sizeof cfg).
Sequencing and unsequenced modifications
Section titled “Sequencing and unsequenced modifications”C does not specify the order in which most operands are evaluated. In f() + g(), either function may run first. In uart_send(read_status(), read_data()), the two arguments may be evaluated in either order. This is unspecified behavior: the compiler must pick some order, need not document it, and may pick differently in different places.
That matters for hardware. Reading a register can have side effects: on the STM32, reading USART2->SR and then USART2->DR is the documented sequence that clears certain error flags, and reading a 32-bit timer as two 16-bit halves requires a particular order. Two volatile reads in one expression are unsequenced relative to each other:
uint32_t v = (TIM_HIGH << 16) | TIM_LOW; /* which register is read first? unspecified */When the order of hardware accesses matters, put each access in its own statement. The semicolon guarantees the order.
The stronger rule: if an object is modified twice without an intervening sequence point, or modified and also read for another purpose, the behavior is undefined:
i = i++; /* undefined */a[i] = i++; /* undefined: i is read for the index and modified */x = ++i + i++; /* undefined */f(i++, i++); /* undefined */The points that do order evaluation (C11 calls it “sequenced before”) are: the end of a full expression (the ;, or the condition of an if, while or switch); the &&, || and comma operators, after their left operand; the ?: operator, after the condition; and a function call, after all arguments are evaluated and before the body runs. -Wall includes -Wsequence-point, which reports operation on 'i' may be undefined for the obvious cases. The cure is always the same: one side effect per object per statement.
Short-circuit evaluation
Section titled “Short-circuit evaluation”&& and || are different from every other binary operator: they evaluate the left operand first, and evaluate the right operand only if needed. For a && b, if a is false, b is never evaluated. For a || b, if a is true, b is never evaluated. The result is always int 0 or 1.
This makes guard expressions safe:
if (len > 0u && buf[len - 1u] == '\n') { /* buf[len - 1] is never read when len is 0 */ len--;}
if (idx < TABLE_SIZE && table[idx] == key) { /* bounds check before the access */ found = true;}Reverse either condition and you read out of bounds. Replace && with & and you lose the guarantee: & evaluates both sides, in unspecified order. This is a real bug class in code where someone “optimized” a condition by using bitwise operators.
Short-circuit evaluation is control flow. if (a && b) compiles to a test of a and a conditional branch past the test of b. That also means side effects in the right operand may not happen: in if (tx_ready() && uart_send(byte)), uart_send is not called when the transmitter is busy. That is usually the intent, but put the side effect on the left, or in its own statement, if it must always run.
A cast (type)expression converts a value explicitly. It is the same conversion that would happen implicitly in an assignment, but written out. Casts are needed in a few specific situations:
- To widen before an operation, so the operation happens in the wider type:
(uint32_t)a * b,(uint32_t)byte << 24,(int64_t)x * y. - To force floating-point division:
(float)sum / countinstead of the integer divisionsum / count. - To document an intentional narrowing:
uint8_t lo = (uint8_t)(word & 0xFFu);tells the reader, and-Wconversion, that losing bits is the point. - To pass the exact type a variadic function expects:
printf("%u", (unsigned)byte),printf("%p", (void *)ptr). - To turn an integer address into a pointer, as in register access, covered in Memory-mapped registers in C.
Every cast is also a statement to the compiler: “I know better, do not warn me”. That is exactly why casts hide bugs:
(uint32_t)(a * b)withuint16_toperands casts after the signed overflow has already happened. The cast must be on an operand, not on the result.(uint8_t)adc_valuesilences the warning but still discards the top bits of a 12-bit reading.- Casting away
constlets you write to read-only data, which may be in flash. - Casting between pointer types can violate alignment and aliasing rules, as Pointers I: addresses, arithmetic, and
constexplains.
Treat every cast in a code review as a claim that must be verified. A codebase with few casts is a codebase whose types are designed correctly.
-Wconversion: making narrowing visible
Section titled “-Wconversion: making narrowing visible”-Wall -Wextra stay silent about implicit conversions that lose information. -Wconversion reports them, and in C it also enables -Wsign-conversion:
#include <stdint.h>
uint8_t low_byte(uint32_t word){ return word; /* narrowing: 32 bits to 8 */}
int32_t as_signed(uint32_t raw){ return raw; /* sign change: values above INT32_MAX become negative */}warning: conversion from 'uint32_t' {aka 'unsigned int'} to 'uint8_t' {aka 'unsigned char'} may change value [-Wconversion]warning: conversion to 'int32_t' {aka 'int'} from 'uint32_t' {aka 'unsigned int'} may change the sign of the result [-Wsign-conversion]It also covers floating point: storing a double into a float, or a float into an integer, is flagged. The fixes fall into two groups. Either the type was wrong (the function should return uint32_t, or the variable should be signed), and you change the type; or the narrowing is intended, and you write the cast together with the masking or range check that justifies it.
-Wconversion is noisy on existing code, and vendor headers often trigger it. On new firmware, enable it from the first commit, where it costs little and catches the truncation bugs that otherwise surface as wrong sensor values months later.
Common mistakes
Section titled “Common mistakes”if (reg & MASK == MASK): compares first, then masks. Writeif ((reg & MASK) != 0u).hi << 8 + lo: adds before shifting. Write(hi << 8) | lo.~on a narrow type compared with a constant: the complement is 32 bits wide. Cast back to the narrow type first.- Shifting a byte into bit 31 without widening:
byte << 24overflowsint. Write(uint32_t)byte << 24. 1 << 31: undefined with 32-bitint. Write1u << 31.- Comparing signed with unsigned: -1 becomes
UINT_MAX. Keep types consistent and enable-Wextra. (idx - 1) % Nwith signedidx: goes negative at 0. Write(idx + N - 1) % N.- Multiplying two
uint16_tvalues into auint32_t: overflows signedint. Cast one operand touint32_tfirst. i = i++and friends: undefined. One modification per object per statement.- Two register reads in one expression: order unspecified. Separate statements.
&where&&was meant: both sides are evaluated, guards stop guarding.- Casting the result instead of an operand: the overflow already happened. Cast the input.
Exercises
Section titled “Exercises”- Without running anything, write down the value and type of each expression on a Cortex-M:
(uint8_t)250 + 10,-1 < 1u,-7 / 2,-7 % 2,sizeof(char) + sizeof 'x',(uint8_t)~(uint8_t)0x0F,true + true. Then print them on your host (casting to a suitable type forprintf) and explain any you got wrong. - Build the buggy
read_u32_leon your host withgcc -std=c11 -O1 -g -fsanitize=undefinedand call it with the bytes0x00, 0x00, 0x00, 0x80. Read the UBSan report (it says the left shift “cannot be represented in type ‘int’”). Apply the fix and confirm the report disappears and the result is0x80000000. - Compile
add8andadd32from this lesson witharm-none-eabi-gcc -std=c11 -O2 -mcpu=cortex-m4 -mthumb -Sand find theuxtb. Then writeuint32_t sum_u8_index(const uint32_t *v)that sumsv[0]tov[199]using auint8_tloop counter, and a second version usinguint32_t. Compare the loops in the assembly and explain any difference. - Take the
over_limitfunction and the two-Wconversionexamples. Compile with-Wall -Wextra -Wconversionand fix every warning. For each one, write a sentence saying whether you changed a type or added a cast, and why that was the right choice. - Write
int32_t div_round(int32_t a, int32_t b)that divides and rounds to the nearest integer (halves away from zero) for all signs ofaandb, assumingbis nonzero and the result fits. Test it with(7, 2),(-7, 2),(7, -2),(-7, -2)and(5, 10). Expected: 4, -4, -4, 4, 1.
Check yourself
Section titled “Check yourself”Question 1:How does C parse if (status & READY == READY)?
== has higher precedence than bitwise &, so the comparison happens first and yields 1. The condition then tests bit 0 of status.
Question 2:What is the value of the expression -1 < 1u?
By the usual arithmetic conversions, int meets unsigned int and the signed operand is converted to unsigned. -1 becomes 4294967295, which is not less than 1.
Question 3:uint8_t x = 0x0F; Is ~x == 0xF0 true?
Integer promotion converts x to a 32-bit int before ~ is applied, so all 32 bits are complemented.
Question 4:What is -7 % 3 in C11?
Division truncates toward zero (-7 / 3 is -2), and the remainder satisfies (a / b) * b + a % b == a, so it is -7 - (-6) = -1.
Question 5:Which of these is well defined on a Cortex-M with 32-bit int?
1u is unsigned int, and 2147483648 fits in it. The others are a signed result that does not fit, a shift count equal to the width, and a promoted int shifted into the sign bit.
Question 6:Which statement has undefined behavior?
In a[i] = i++, i is modified by i++ and read for the index with no sequence point between them. The comma operator in the last option provides a sequence point.
Key takeaways
Section titled “Key takeaways”- Bitwise
&,^,|bind more loosely than==, and shifts more loosely than+. Parenthesize every mixed expression and keep-Wallon. - All arithmetic happens in at least
int: narrow types are promoted first. That explains~xsurprises, shifts into the sign bit, anduint16_t * uint16_toverflowing a signedint. - When signed meets unsigned of the same width, the signed value becomes unsigned:
-1 < 1uis false.-Wextrawarns; design types so it does not happen. - Integer division truncates toward zero and the remainder takes the dividend’s sign. Division by zero is undefined; on Cortex-M3/M4 it silently returns 0 unless you enable the trap.
- Shift unsigned values by less than their width:
1u << 31,(uint32_t)b << 24,UINT64_C(1) << 40. - Operand evaluation order is mostly unspecified, and modifying an object twice without a sequence point is undefined.
&&,||,?:, the comma operator and the semicolon impose order. - A cast silences the compiler; it does not make the code correct. Cast operands before the operation, and use
-Wconversionto find silent narrowing.
Next: Control flow that holds up in production, where these expressions start making decisions, and you see what if, switch and loops become in machine code.