Skip to content

VOL 03 / CH 02 / LESSON 01

2.1 Integer Representation and Overflow ​

Chapter 1 explains that the same bit sequence can have different interpretations. This chapter discusses fixed-width integers; the next will cover floating-point representations and numerical errors.

What Value Does 0xFFFFFFFF Represent? ​

The register window shows only FFFFFFFF, with no type information. Interpreted as a 32-bit unsigned integer, it is 4,294,967,295; interpreted as a common 32-bit two's-complement signed integer, it is -1.

Bit patterns don't have inherent positive or negative signs. The type specifies the bit width and interpretation rules, while the language defines the behavior of operations, conversions, and overflow. When reading system code, you must track all three layers simultaneously.

Unsigned Integer Modulo Operations ​

n bits have 2^n patterns. Unsigned interpretation maps them to 0 through 2^n - 1. In C, addition, subtraction, and multiplication with an unsigned n-value-bit result type wrap modulo 2^n. Narrow operands undergo integer promotions first; unsigned source variables do not guarantee arithmetic at their original width.

c
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void) {
    uint8_t value = UINT8_MAX;

    printf("start = %" PRIu8 "\n", value);
    value = (uint8_t)(value + UINT8_C(1));
    printf("plus one = %" PRIu8 "\n", value);
    value = (uint8_t)(value - UINT8_C(1));
    printf("minus one = %" PRIu8 "\n", value);
    return 0;
}

The output is 255, 0, 255. The uint8_t in the expression undergoes integer promotion, and the modulo 256 result is obtained only when the assignment converts back to uint8_t; an explicit cast makes this boundary clearer.

Wrapping can serve sequence numbers and ring counters, but "predictable results" does not mean "business-correct." If a length calculation wraps from maximum back to minimum, the subsequent buffer allocation might be smaller than the actual amount written. Even when using unsigned types for sizes, range checks must still be performed before any arithmetic.

How two's complement assigns a negative weight to the most significant bit ​

Modern general-purpose computers use two's complement. For an n-bit pattern x[n-1]...x[0], the two's complement value is:

text
-x[n-1] * 2^(n-1) + Σ x[i] * 2^i

8-bit example:

Bit PatternUnsigned InterpretationTwo's Complement Interpretation
0000000000
01111111127127
10000000128-128
11111111255-1

Thus, the n-bit two's complement range is -2^(n-1) to 2^(n-1)-1. The negative side has one extra value, and -128 has no corresponding 8-bit positive number.

From a bit-pattern perspective, flipping bits and adding one yields the two's complement representation of the negative; however, taking the signed negative of the minimum value overflows the range. On a two’s-complement C implementation, -INT_MIN is undefined behavior and you should not rely on hardware typically preserving the same bit pattern.

C23 requires that signed integers use two's complement representation; when reading highly portable code under C17 or earlier standards, one should not assume all implementation details are two's complement. For fixed-format protocols, it's best to use explicitly sized unsigned types and explicitly document the representation.

Signed overflow is not a wrap-around guarantee ​

This C code does not produce portable results:

c
int value = INT_MAX;
value = value + 1;

Signed addition, subtraction, and multiplication results outside the range of the operation’s type after promotions and the usual arithmetic conversions, invoke undefined behavior. Compilers can thus assume that well-formed programs never reach overflow paths, making "compute first, then check if result has decreased" an ineffective overflow detection method.

c
bool add_overflowed(int value) {
    return value + 1 <= value; /* Evaluation already overflows when value == INT_MAX. */
}

Correct manual verification must be completed before the operation begins:

c
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>

static bool add_int_checked(int left, int right, int *result) {
    if (result == NULL) {
        return false;
    }
    if ((right > 0 && left > INT_MAX - right)
            || (right < 0 && left < INT_MIN - right)) {
        return false;
    }
    *result = left + right;
    return true;
}

INT_MIN - right is evaluated only when right < 0 is true, so its result is in [INT_MIN + 1, 0] and is representable. In particular, right == INT_MIN gives zero. Do not rewrite it to negate right first: that intermediate value may be unrepresentable. The caller can use the return value to distinguish success from failure.

Implementations supporting C23 can use <stdckdint.h> with ckd_add, ckd_sub, and ckd_mul; compilers also often provide built-in overflow checking functions. The ckd_* macros return true when the result is unrepresentable, the opposite of this page’s success-returning helpers. Check compiler and standard-library support, not merely whether a -std=c23 option is accepted.

Boundaries Go Beyond Addition ​

Multiplication ​

When checking whether a * b overflow occurs, you should not perform the multiplication first. For non-negative dimensions, a common approach is:

c
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static bool multiply_size_checked(size_t count, size_t width, size_t *result) {
    if (result == NULL) {
        return false;
    }
    if (width != 0 && count > SIZE_MAX / width) {
        return false;
    }
    *result = count * width;
    return true;
}

This pattern directly protects the allocation calculation of "number of elements × size of each element".

Division and Modulus ​

For signed integers, division by zero is always erroneous. On two's complement platforms, there's also a set of easily overlooked boundaries: the mathematical result of INT_MIN / -1 cannot be represented by int, and this is undefined behavior in C. INT_MIN % -1 has the same representable-quotient requirement, even though the mathematical remainder is zero.

Parsing and Narrowing Conversions ​

Clear errno before calling strtol or strtoul. Check for no digits (end == input), forbidden trailing content, and ERANGE. These functions accept leading whitespace and signs; even strtoul accepts a minus sign, so an unsigned return type does not validate a nonnegative input. Strict fields need explicit grammar checks. Before converting a wider integer to a narrower type, the target range must also be checked. The conversion is not a validation step.

When an out-of-range integer is converted to an unsigned type, C specifies that the result is obtained by repeatedly adding or subtracting the modulus within the target range. For example, on a common 32-bit unsigned int, (unsigned int)-1 yields UINT_MAX. This is abstractly a numeric conversion and should not be generally described as "reinterpreting the same memory bytes."

When an integer value exceeds the range and is converted to a signed type, the result is implementation-defined or may generate an implementation-defined signal; do not rely on it for portable wraparound behavior.

Mixing signed and unsigned integers can change comparison results ​

C's usual arithmetic conversions may first convert the signed operands to unsigned. For example, comparing int with its corresponding unsigned int:

c
-1 < 0U

The result is false because -1 became unsigned int. However, the exact rules depend on the rank and representable ranges of the two types, and this example cannot be generalized into a heuristic for all mixed types.

A more reliable engineering practice is to keep types consistent within the same expression:

  • Array indexing and sizeof are commonly used with size_t; reverse looping should avoid unsigned underflow;
  • Don't use index >= 0 to terminate an unsigned reverse loop; it's always true;
  • Before converting for a comparison, verify that the target type can represent the value;
  • Enable -Wall -Wextra -Wconversion and other warnings at compile time, and understand each one thoroughly rather than mechanically suppressing them.

A safe unsigned reverse form is:

c
for (size_t index = length; index-- > 0;) {
    use(items[index]);
}

The condition compares the old value and then decrements. On entering the body, index is in [0, length). The final failed condition still wraps zero to SIZE_MAX, which is defined; the body is skipped and this loop-local variable is not visible afterward. Zero length performs no array access. If code readability is prioritized, using forward order, iterators, or explicit branches is also an option.

Java and Python follow different rules ​

Java’s byte, short, int, and long are fixed-width two’s-complement signed integers. Ordinary addition, subtraction, and multiplication retain the low bits of their promoted int or long result. Arithmetic on byte and short usually promotes to int; narrowing back requires a cast or a language construct such as compound assignment. Math.addExact, multiplyExact, and similar interfaces throw ArithmeticException on overflow. Java does not have a built-in unsigned integer primitive type, but provides Integer.compareUnsigned, toUnsignedLong, and other operations; char is an unsigned 16-bit UTF-16 code unit.

Python's int automatically extends precision within available memory and typically doesn't suffer from fixed-width overflow. When interacting with binary protocols or C interfaces, you must still verify bit widths; value.to_bytes(length, byteorder, signed=...) will raise OverflowError if it cannot accommodate the data.

When porting an algorithm to multiple languages, first specify overflow semantics. Hash codes that rely on Java's wraparound behavior directly translated into C signed arithmetic can shift from well-defined behavior to undefined behavior.

A set of runnable boundary tests ​

The standalone integer_boundaries.c below already includes the helpers; do not paste in duplicate definitions from above. Test both success and failure:

c
#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static bool add_int_checked(int left, int right, int *result) {
    if (result == NULL) {
        return false;
    }
    if ((right > 0 && left > INT_MAX - right)
            || (right < 0 && left < INT_MIN - right)) {
        return false;
    }
    *result = left + right;
    return true;
}

static bool multiply_size_checked(size_t count, size_t width, size_t *result) {
    if (result == NULL) {
        return false;
    }
    if (width != 0 && count > SIZE_MAX / width) {
        return false;
    }
    *result = count * width;
    return true;
}

int main(void) {
    int integer_result = 0;
    size_t size_result = 0;

    assert(add_int_checked(20, 22, &integer_result) && integer_result == 42);
    assert(add_int_checked(INT_MAX, 0, &integer_result));
    assert(!add_int_checked(INT_MAX, 1, &integer_result));
    assert(!add_int_checked(INT_MIN, -1, &integer_result));
    assert(!add_int_checked(1, 2, NULL));

    assert(multiply_size_checked(6, 7, &size_result) && size_result == 42);
    assert(multiply_size_checked(SIZE_MAX, 1, &size_result));
    assert(!multiply_size_checked(SIZE_MAX, 2, &size_result));
    return 0;
}

Compile using ordinary C17 mode without enabling -fwrapv or other semantics-altering options:

bash
cc -std=c17 -Wall -Wextra -Werror integer_boundaries.c -o integer_boundaries
./integer_boundaries

No output and an exit code of 0 indicate that all assertions have passed. Production tests should also cover every integer type, different sign combinations, and the full input parsing chain to the target type.

Hands-on Derivation and Verification ​

  1. Interpret 10000000, 11111111, 01111111, and 11001010 as unsigned and as 8-bit two's complement.
  2. Implement safe subtraction for int, covering INT_MIN - 1 and INT_MAX - (-1).
  3. Explain why abs(INT_MIN) cannot return a positive value within the int range.
  4. Write a safe calloc size check and prove it will not overflow in the check expression.
  5. Compare the semantics of executing "increment the largest integer" in C, Java, and Python.

From Discrete Boundaries to Approximate Representations ​

Each bit pattern of an integer typically corresponds to an exact value, overflow occurring outside that range. Floating-point numbers face a different kind of trade-off: using a finite number of bits to represent vast ranges of magnitude, resulting in gaps between adjacent representable values. The next section continues from IEEE 754 and Rounding Errors.

Built with VitePress | Software Systems Atlas