UNIT 02 · LESSON 2 OF 6

Integer Types, Signedness, and Overflow

What is the contract, and where is it written down?

INTERACTIVEWhere the numbers run out
Fixed-width integer drawn as a ring with an addition travelling around it064128192255uint8_t250 + 10= 4crossed the edge: wrapped modulo 256
Fixed-width integer drawn as a ring with an addition travelling around it064128192255uint8_t250 + 10= 4crossed the edge: wrapped modulo 256

Try this

uint8_t: 250 + (10) = 260 exactly. Outside 0…255: unsigned arithmetic wraps modulo 256, giving 4.

A fixed-width integer is a ring, not a line. Adding past the top comes back around at the bottom. For unsigned types C defines this wrap exactly (modulo 2ⁿ). For signed types the same bit pattern appears on real two’s-complement hardware, but the C standard calls signed overflow undefined behaviour, so the compiler may assume it never happens.

What you will be able to do
  • Choose a fixed-width type from <stdint.h> for a given range and say what its wrap-around limit is.
  • Read a two’s-complement bit pattern as a signed value and explain why the top bit weighs −2ⁿ⁻¹.
  • State the difference between unsigned wrap (defined, modulo 2ⁿ) and signed overflow (undefined behaviour) and give one consequence of each.
  • Predict the result of mixing signed and unsigned operands, including the −1 < 1u trap, using the usual arithmetic conversions.
  • Use unsigned subtraction to compute an elapsed time correctly across a counter wrap.
Before you start
  • Binary and hexadecimal notation, and the weights of bits (lesson 1).
  • C variable declarations and arithmetic operators.
Steps in this lesson
  1. Fixed widths and the ring
  2. Name the width you mean
  3. Two’s complement
  4. Promotions and the −1 < 1u trap
  5. Worked example: elapsed time across a wrap
  6. Common misconceptions

The puzzle

Two counters, both eight bits wide, both holding 255. Add one to each. The first reads 0, exactly as the hardware clock that feeds it intends. The second is a signed variable, and the C standard says the result is not 0, not −256, not anything: the program has no defined meaning from that point on, and the compiler is entitled to have assumed it could never happen. Same bits, same adder, different contract. What is the contract, and where is it written down?

STEP 1

Fixed widths and the ring

A microcontroller register, a memory cell and a C integer variable all have a fixed number of bits. An nn-bit unsigned value can represent 00 to 2n−12^n - 1; an 8-bit uint8_t holds 0 to 255, a 16-bit uint16_t holds 0 to 65 535, a 32-bit uint32_t holds 0 to 4 294 967 295. There is no ninth bit to carry into. The natural picture is not a number line but a ring: one step past the top lands on the bottom.

↑ This step uses the figure at the top of the page.

For unsigned types C makes this exact. §6.3.1.3 of the standard says a value that does not fit is reduced modulo 2n2^n, so

(255+1) mod 256=0,(255 + 1) \bmod 256 = 0, (0−1) mod 256=255.(0 - 1) \bmod 256 = 255.

That is a definition, not an accident, and firmware relies on it. For signed types the same standard (§6.5 ¶5) says an unrepresentable result is undefined behaviour. The processor will still produce a bit pattern, and on every current architecture it is the two’s-complement wrap you would expect, but the language refuses to promise it. Lesson 6 explains why that refusal has teeth: the optimiser is allowed to delete code that could only matter if overflow occurred.

STEP 2

Name the width you mean

int is at least 16 bits; on 32-bit Cortex-M it is 32, on an 8-bit AVR it is 16. long is at least 32. char may be signed or unsigned depending on the compiler and target: it is signed on x86 GCC and unsigned on Arm GCC by default. Firmware that talks to hardware cannot live with “at least”. The header <stdint.h> (C11 §7.20) provides types whose width is part of the name:

TypeBitsMinMax
uint8_t80255
int8_t8−128127
uint16_t16065 535
int16_t16−32 76832 767
uint32_t3204 294 967 295
int32_t32−2 147 483 6482 147 483 647

Use these for anything that maps onto hardware: register contents, buffer bytes, sample values, protocol fields. Keep plain int for loop counters and arithmetic where the exact width does not matter. size_t is the unsigned type for sizes and array indices.

STEP 3

Two’s complement

Signed integers reuse the same nn bits by making the most significant bit negative: in an 8-bit signed value bit 7 weighs −128-128 instead of +128+128, and the other seven weigh 64,32,…,164, 32, \dots, 1 as before.

INTERACTIVETwo’s complement: the top bit is negative
Eight bits with two’s-complement weights summed to a signed value1716051403121100-1286432168421-128 + 64 + 16 + 4 + 2 = -42same bits read as uint8_t: 214 · hex 0xD6negate: invert all bits, add 1 → 00101010 = 42
Eight bits with two’s-complement weights summed to a signed value1716051403121100-1286432168421-128 + 64 + 16 + 4 + 2 = -42same bits read as uint8_t: 214 · hex 0xD6negate: invert all bits, add 1 → 00101010 = 42
int8_t -42 is stored as 11010110 (0xD6). Read as uint8_t the same byte is 214. Bit 7 contributes −128 when set.

In an 8-bit two’s-complement number the most significant bit weighs −128 instead of +128; the other seven weigh what they always did. That single change gives one unambiguous zero, a range of −128 to +127, and lets the same adder hardware handle signed and unsigned values. Slide the value to watch the weights combine.

Three consequences follow from that one change of weight:

  1. The range is −2n−1-2^{n-1} to 2n−1−12^{n-1} - 1: −128 to 127 for eight bits. There is one more negative number than positive.
  2. Zero is unique (0x00), and to negate a value you invert every bit and add one: −42=∼42+1-42 = \sim 42 + 1.
  3. The adder does not care. Adding the bit patterns for −42-42 and 5050 produces the bit pattern for 88, with a carry out of the top that is simply discarded. Signed and unsigned addition are the same instruction; only the interpretation of the result, and the overflow condition, differ.

C11 allowed two other signed representations in principle, but C23 (N3096 §6.2.6.2) finally requires two’s complement, matching every processor you will meet. What C23 did not change is the rule that signed overflow is undefined.

STEP 4

Promotions and the −1 < 1u trap

C never does arithmetic in types narrower than int. Before any operation, a uint8_t or int16_t operand is promoted to int (§6.3.1.1). This is why (uint8_t)0x80 << 1 is 0x100 and not 0: the shift happens in int. Only when the result is stored back into a uint8_t is it truncated to 0x00.

When a signed and an unsigned operand of the same rank meet, the usual arithmetic conversions (§6.3.1.8) convert the signed one to unsigned. Hence the classic trap:

int      a = -1;
unsigned b =  1;
if (a < b) { … }   /* never taken: -1 becomes 4294967295u */

The comparison is done in unsigned int, where −1 wraps to the maximum value. Most compilers warn about signed/unsigned comparison; the fix is to keep both operands the same signedness, and the habit is to keep quantities that can never be negative unsigned from the start.

STEP 5

Worked example: elapsed time across a wrap

A free-running 16-bit hardware timer counts up at 1 MHz and wraps from 65 535 to 0. Firmware records start = 65 000, waits, and later reads now = 1 200. How long elapsed?

Naively 1200−65000=−638001200 - 65000 = -63800: wrong, and in a signed 16-bit type it would also overflow. In uint16_t arithmetic the subtraction wraps by definition:

(1200−65000) mod 65536=1736(1200 - 65000) \bmod 65536 = 1736

So 1 736 µs elapsed, which is correct: 536 ticks to reach the wrap, then 1 200 more. The single rule that makes this work is subtract in the same unsigned width as the counter. In C the subtraction happens in int after promotion, so cast the result back: (uint16_t)(now - start). This works for any elapsed time shorter than one full period (65.536 ms here); longer intervals are ambiguous, and you need a wider counter or an overflow interrupt that counts the wraps.

The same reasoning tells you when a sum will not fit. Averaging 100 ADC samples of up to 4 095 each gives a maximum sum of 409 500, which exceeds 65 535: accumulate in uint32_t, not uint16_t, or the average will be silently wrong for bright inputs.

MYTHS AND FACTS

Common misconceptions

Overflow always wraps around

Unsigned wrap is defined. Signed overflow is undefined behaviour; wrap is what the hardware does, not what the program is guaranteed.

int is 32 bits

It is at least 16. On many 8-bit and 16-bit targets it is 16; use <stdint.h> when the width matters.

char is signed

It is whichever the compiler chooses for the target; Arm GCC picks unsigned. Use int8_t or uint8_t for small numbers and char only for text.

Arithmetic on uint8_t happens in 8 bits

It happens in int; the result is truncated only when stored back.

Comparing a signed and an unsigned value is fine if both are small

Both operands are converted to unsigned first; negative values become huge.

Two’s complement is a trick for storing negatives

It is the representation that lets one adder serve both signed and unsigned arithmetic; that is why it won and why C23 mandates it.

Check yourself

Answer in your head, then open the card.

What does uint8_t x = 200; x += 100; leave in x?

300 does not fit. Unsigned wrap: 300 mod 256=44300 \bmod 256 = 44. Defined, and probably not what was intended.

The byte 0xF6 is read as an int8_t. What value is that?

1111 0110: bit 7 contributes −128, the rest 64 + 32 + 16 + 4 + 2 = 118. Total −10. Check: invert 0xF6 → 0x09, add 1 → 0x0A = 10. So −10.

A 32-bit millisecond counter started at 4 294 967 000 and now reads 500. How many milliseconds passed, and what C expression gives it?

(500−4294967000) mod 232=796(500 - 4294967000) \bmod 2^{32} = 796. Expression: (uint32_t)(now - start); both operands are already 32-bit unsigned, so the subtraction wraps correctly without a cast, but the cast documents the intent.

Why is if (-1 < 1u) false?

The signed −1 is converted to unsigned for the comparison and becomes 4 294 967 295, which is not less than 1.

Sources (5)
  1. ISO/IEC 9899:2011 (C11), committee draft N1570, §6.2.5 types, §6.2.6.2 integer types (representations), §6.3.1.1 integer promotions, §6.3.1.3 signed and unsigned integers, §6.3.1.8 usual arithmetic conversions, §6.5 ¶5 exceptional conditions, §7.20 <stdint.h> — §6.3.1.3 ¶2 defines unsigned wrap as reduction modulo one more than the maximum value; §6.5 ¶5 makes an unrepresentable signed result undefined behaviour
  2. ISO/IEC 9899:2024 (C23), working draft N3096, §6.2.6.2 integer types — C23 requires signed integers to use two’s complement; C11 still allowed sign-magnitude and ones’ complement
  3. SEI CERT C Coding Standard, INT30-C “Ensure that unsigned integer operations do not wrap” — defined wrap is still a bug when it is not intended; shows precondition and postcondition tests
  4. SEI CERT C Coding Standard, INT32-C “Ensure that operations on signed integers do not result in overflow” — lists which operations can overflow and how to check before performing them
  5. GCC manual, “Options That Control Optimization”: -fwrapv, -ftrapv, -fstrict-overflow — compiler switches that change signed-overflow behaviour from undefined to wrapping or trapping, and the default assumption that overflow does not occur