Integers, widths and limits

Integers come in several widths, and each width has a range. Going past that range is either a well-defined wrap or a serious bug, depending on whether the type is signed. This lesson prints the ranges of the machine, shows both behaviours and introduces the headers that make the width explicit.

Widths and limits

limits.h defines one macro per limit, named after the type. CHAR_BIT is the number of bits in a byte. The values below are the ones on x86_64 with 32-bit int; a program must use the macros, not the numbers, if it has to run elsewhere.

#include <limits.h> #include <stdio.h> int main(void) { printf("CHAR_BIT %d\n", CHAR_BIT); printf("INT_MAX %d\n", INT_MAX); printf("INT_MIN %d\n", INT_MIN); printf("UINT_MAX %u\n", UINT_MAX); printf("LONG_MAX %ld\n", LONG_MAX); printf("ULLONG_MAX %llu\n", ULLONG_MAX); return 0; }

gcc -Wall -Wextra limits.c -o limits
./limits

CHAR_BIT 8
INT_MAX 2147483647
INT_MIN -2147483648
UINT_MAX 4294967295
LONG_MAX 9223372036854775807
ULLONG_MAX 18446744073709551615

The negative range of a signed type is one larger than its positive range, because zero takes one of the bit patterns. C23 requires two's complement representation, which is what these values show.

Unsigned wrap-around

Unsigned arithmetic is defined modulo one more than the maximum value, so adding one to UINT_MAX gives zero. That is not an error in C; it is how counters, hashes and checksums are written. A division by two plus one lands exactly on the top bit.

#include <limits.h> #include <stdio.h> int main(void) { unsigned int u = UINT_MAX; u = u + 1; printf("after wrap: %u\n", u); printf("UINT_MAX / 2 + 1: %u\n", UINT_MAX / 2 + 1); return 0; }

gcc -Wall -Wextra wrap.c -o wrap
./wrap

after wrap: 0
UINT_MAX / 2 + 1: 2147483648

The second line sets the top bit: 231 is representable as unsigned int but not as int, which is the first hint of the next section.

Signed overflow

Signed overflow is different: the standard says the behaviour is undefined, so the compiler may assume it never happens. The program below overflows an int on purpose. Built with UndefinedBehaviorSanitizer, it reports the exact expression and operand values, then continues with a wrapped result that is not guaranteed.

#include <limits.h> #include <stdio.h> int main(void) { int x = INT_MAX; x = x + 1; printf("%d\n", x); return 0; }

gcc -fsanitize=undefined -g -Wall -Wextra ub.c -o ub
./ub

ub.c:8:7: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
-2147483648

The printed value looks like a wrap, and on this compiler it is, but the standard promises nothing: an optimiser is allowed to remove the whole check around the expression. Lesson 34 collects the other forms of undefined behaviour and lesson 35 explains the sanitizers.

Fixed-width types

When a value must have an exact width, use stdint.h: int32_t is exactly 32 bits, uint64_t exactly 64. To print them, include inttypes.h and use the macros it defines, because the underlying type differs between systems. PRIu64 expands to the right conversion.

#include <inttypes.h> #include <stdint.h> #include <stdio.h> int main(void) { int32_t views = 2000000000; int64_t people = 9000000000LL; uint64_t mask = 0xFFFFFFFFFFFFFFFFULL; printf("views: %" PRId32 "\n", views); printf("people: %" PRId64 "\n", people); printf("mask: %" PRIu64 "\n", mask); return 0; }

gcc -Wall -Wextra fixed.c -o fixed
./fixed

views: 2000000000
people: 9000000000
mask: 18446744073709551615

The format string is assembled by the preprocessor: % followed by the expansion of PRId64. Writing %ld instead works on this machine but breaks where int64_t is a long long.

Signed and unsigned together

Mixing signed and unsigned values in one expression converts the signed operand to unsigned, so -1 becomes the largest unsigned value. The comparison below therefore finds the count larger than ten, and GCC flags the line:

#include <stdio.h> int main(void) { int count = -1; unsigned int limit = 10; if (count < limit) { printf("count is smaller\n"); } else { printf("count is not smaller\n"); } return 0; }

gcc -Wall -Wextra signcmp.c -o signcmp
./signcmp

signcmp.c: In function 'main':
signcmp.c:8:15: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
8 | if (count < limit) {
| ^
count is not smaller

A constant that does not fit is the second trap. This one needs -Wconversion, because the value is converted quietly:

#include <stdio.h> int main(void) { int x = 3000000000; printf("%d\n", x); return 0; }

gcc -Wall -Wextra -Wconversion bigconst.c -o bigconst

bigconst.c: In function 'main':
bigconst.c:5:13: warning: conversion from 'long int' to 'int' changes value from '3000000000' to '-1294967296' [-Wconversion]
5 | int x = 3000000000;
| ^~~~~~~~~~

The literal has type long on this machine, and the initialisation cuts it to 32 bits. Declaring the variable as long or int64_t keeps the value; the warning is the cheapest way to notice.

Exercises

  1. Print SHRT_MAX, SCHAR_MIN and LLONG_MIN and check each against the range the standard guarantees.
  2. Write a loop that counts up in an unsigned char and stops after the wrap. How many iterations does it run?
  3. Store the value 4 294 967 296 in an unsigned int and in a uint64_t. Compile with -Wconversion and explain the difference.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why is the negative range of a signed integer one value larger than its positive range?

    Answer

    Because zero takes one of the non-negative bit patterns

    Zero is counted among the non-negative values, so the positive side loses one pattern to it and the negative side keeps the rest.

  2. What does CHAR_BIT report on the machine used in this course?

    Answer

    8

    CHAR_BIT is the number of bits in a byte, which is 8 on this platform.

  3. What happens when an unsigned int holding UINT_MAX is incremented?

    Answer

    The value becomes zero, defined by the standard

    Unsigned arithmetic is defined modulo one more than the maximum value, so adding one to UINT_MAX gives zero.

  4. What does the type int32_t guarantee?

    Answer

    Exactly 32 bits

    The fixed-width types from stdint.h name an exact number of bits, unlike int, whose size depends on the platform.

  5. What does this program print? #include <limits.h> #include <stdio.h> int main(void) { unsigned int u = UINT_MAX; u = u + 1; printf("%u\n", u); return 0; }

    Answer

    0

    Unsigned arithmetic wraps modulo UINT_MAX + 1, so one more than the maximum becomes zero.

  6. What does UINT_MAX / 2 + 1 evaluate to?

    Answer

    2147483648

    Half of the maximum plus one sets the top bit, which is representable as unsigned but not as a signed int.

  7. What does this program print? #include <stdio.h> int main(void) { int count = -1; unsigned int limit = 10; printf("%d\n", count < limit); return 0; }

    Answer

    0

    The signed -1 is converted to unsigned, becomes the largest unsigned value, and is not smaller than 10.

  8. This program is built with gcc -std=c17 -Wconversion bigconst.c -o bigconst. What does it print when run? #include <stdio.h> int main(void) { int x = 3000000000; printf("%d\n", x); return 0; }

    Answer

    -1294967296

    The literal is a long, and cutting it to 32 bits keeps the low bits, which print as -1294967296.

  9. Which warning flag reports a comparison between an int and an unsigned int?

    Answer

    -Wsign-compare

    -Wsign-compare, part of -Wextra, names the two signednesses and the line of the comparison.

  10. What does the standard say about signed integer overflow?

    Answer

    It is undefined behaviour

    Signed overflow is undefined, so an optimiser may assume it never happens even when a particular run appears to wrap.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev