ZetCode

C integer limits and stdint.h

last modified November 14, 2026

An integer type has a limited range, and the compiler is allowed to choose the width of most of them. The headers <limits.h> and <stdint.h> publish those limits and provide types with an exact width, so a program can state what it needs instead of guessing. This tutorial covers the macros in limits.h, the difference between unsigned wraparound and signed overflow, the fixed-width types, and the related types size_t and ptrdiff_t.

Limits in limits.h

<limits.h> defines one pair of macros for each integer type: the largest and the smallest value it can represent. The first program prints the limits of the common types on this platform.

limits.c
#include <stdio.h>
#include <limits.h>

int main(void) {

    printf("CHAR_BIT:  %d\n", CHAR_BIT);
    printf("CHAR_MIN:  %d\n", CHAR_MIN);
    printf("CHAR_MAX:  %d\n", CHAR_MAX);
    printf("SCHAR_MIN: %d\n", SCHAR_MIN);
    printf("UCHAR_MAX: %d\n", UCHAR_MAX);
    printf("INT_MIN:   %d\n", INT_MIN);
    printf("INT_MAX:   %d\n", INT_MAX);
    printf("UINT_MAX:  %u\n", UINT_MAX);
    printf("LONG_MAX:  %ld\n", LONG_MAX);
    printf("LLONG_MAX: %lld\n", LLONG_MAX);

    return 0;
}

CHAR_BIT is 8, so a byte has eight bits. CHAR_MIN is -128, which shows that plain char is signed on this platform; on a machine where it is unsigned the macro would be 0 and CHAR_MAX would be 255. The unsigned maximum is one less than two to the width, and INT_MIN is one less in magnitude than INT_MAX, a consequence of two's complement.

$ gcc -std=c17 -Wall -Wextra -o limits limits.c
$ ./limits
CHAR_BIT:  8
CHAR_MIN:  -128
CHAR_MAX:  127
SCHAR_MIN: -128
UCHAR_MAX: 255
INT_MIN:   -2147483648
INT_MAX:   2147483647
UINT_MAX:  4294967295
LONG_MAX:  9223372036854775807
LLONG_MAX: 9223372036854775807

Unsigned arithmetic wraps

Unsigned integers are defined to wrap around modulo one more than their maximum, so the result of every unsigned operation is well defined. Adding 1 to UINT_MAX gives 0, and subtracting 1 from 0 gives UINT_MAX.

unsigned_wrap.c
#include <stdio.h>
#include <limits.h>

int main(void) {

    unsigned int max = UINT_MAX;
    unsigned int zero = max + 1;
    unsigned int minus_one = 0u - 1u;

    printf("UINT_MAX:     %u\n", max);
    printf("UINT_MAX + 1: %u\n", zero);
    printf("0u - 1u:      %u\n", minus_one);

    return 0;
}

This behavior is relied upon in hashing, checksums and bit manipulation, where wrapping is the desired result rather than an error. It also explains why a loop that counts down with an unsigned variable never terminates: the condition i >= 0 is always true, because an unsigned value cannot be negative.

$ gcc -std=c17 -Wall -Wextra -o unsigned_wrap unsigned_wrap.c
$ ./unsigned_wrap
UINT_MAX:     4294967295
UINT_MAX + 1: 0
0u - 1u:      4294967295

Signed overflow is undefined

Signed integers are different: the standard says that an operation whose result does not fit in the type has undefined behavior. The program is not required to wrap, to trap or to do anything in particular, and the compiler may assume that the overflow never happens. The next program computes INT_MAX + 1, and a sanitizer reports the violation when it runs.

signed_overflow.c
#include <stdio.h>
#include <limits.h>

int main(void) {

    int max = INT_MAX;
    int next = max + 1;

    printf("%d\n", next);

    return 0;
}

The option -fsanitize=undefined adds run-time checks for undefined behavior. Here it prints a diagnostic, but the program keeps running and prints -2147483648, the value that two's complement produces on this machine, and exits with status 0. That silence is the danger: the same operation may be optimized away in a different build, so a signed overflow must be prevented in the source rather than tested afterwards.

$ gcc -std=c17 -Wall -Wextra -fsanitize=undefined -o signed_overflow signed_overflow.c
$ ./signed_overflow
signed_overflow.c:7:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
-2147483648

Fixed-width types from stdint.h

When the width matters, <stdint.h> provides the typedefs int8_t, int16_t, int32_t and int64_t, their unsigned counterparts, and the maximum values as macros. The header <inttypes.h> adds the format macros that match them.

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

int main(void) {

    int8_t small = -128;
    uint8_t byte = 255;
    int32_t word = 2147483647;
    uint64_t big = 18446744073709551615ULL;

    printf("int8_t:   %" PRId8 "\n", small);
    printf("uint8_t:  %" PRIu8 "\n", byte);
    printf("int32_t:  %" PRId32 "\n", word);
    printf("uint64_t: %" PRIu64 "\n", big);

    printf("sizeof(int32_t) = %zu\n", sizeof(int32_t));
    printf("INT32_MAX = %" PRId32 "\n", INT32_MAX);

    return 0;
}

PRId8 expands to the conversion specification for int8_t, and because the macro already contains the percent sign it is written as "%" PRId8 inside the format string. The types are optional: a platform without an exact 8-bit type would not define int8_t, but every platform that runs Linux or Windows does. When a type of at least a given width is enough, use int_least32_t or int_fast32_t instead.

$ gcc -std=c17 -Wall -Wextra -o fixed_width fixed_width.c
$ ./fixed_width
int8_t:   -128
uint8_t:  255
int32_t:  2147483647
uint64_t: 18446744073709551615
sizeof(int32_t) = 4
INT32_MAX = 2147483647

size_t and ptrdiff_t

Two integer types are defined in terms of the machine rather than of a fixed width. size_t is the unsigned type that sizeof returns and that array indices should use, and ptrdiff_t is the signed type that results from subtracting two pointers.

size_types.c
#include <stdio.h>
#include <stddef.h>

int main(void) {

    int vals[10];

    size_t n = sizeof vals / sizeof vals[0];
    ptrdiff_t d = &vals[9] - &vals[0];

    printf("n: %zu\n", n);
    printf("d: %td\n", d);
    printf("sizeof(size_t) = %zu\n", sizeof(size_t));
    printf("sizeof(ptrdiff_t) = %zu\n", sizeof(ptrdiff_t));

    return 0;
}

Both types are 8 bytes here, which is the width of a pointer. The difference of two pointers is measured in elements, not bytes, so d is 9 rather than 36. %zu and %td are the matching conversions; printing a size_t with %d is undefined behavior, because the argument is not an int.

$ gcc -std=c17 -Wall -Wextra -o size_types size_types.c
$ ./size_types
n: 10
d: 9
sizeof(size_t) = 8
sizeof(ptrdiff_t) = 8

Mixing signed and unsigned

When a signed and an unsigned operand meet, the signed value is converted to unsigned before the comparison or the operation. The result is often surprising, and -Wextra reports the comparison that causes it.

sign_compare.c
#include <stdio.h>

int main(void) {

    int i = -1;
    unsigned int u = 1;

    printf("i < u: %d\n", i < u);
    printf("(unsigned) i: %u\n", (unsigned) i);

    return 0;
}

-1 becomes UINT_MAX before the comparison, so i < u is false even though -1 is mathematically smaller than 1. The compiler prints a warning because the conversion is rarely intended. Compare with a cast to the wider signed type, or make both operands the same type, when a negative value is possible.

$ gcc -std=c17 -Wall -Wextra -o sign_compare sign_compare.c
sign_compare.c: In function ‘main’:
sign_compare.c:8:29: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare]
    8 |     printf("i < u: %d\n", i < u);
      |                             ^
$ ./sign_compare
i < u: 0
(unsigned) i: 4294967295

Rules for integer types

The rules below prevent most range errors. They come from the fact that the width of an integer type is a property of the platform, while the requirement of a program is a property of the problem it solves.

Source

cppreference: limits of integer types
cppreference: fixed-width integer types
cppreference: size_t
GCC: instrumentation options

This tutorial covered integer limits in C: the macros of <limits.h>, the defined wraparound of unsigned arithmetic, the undefined behavior of signed overflow and how a sanitizer reveals it, the fixed-width types of <stdint.h>, and the machine-dependent size_t and ptrdiff_t. The types themselves are introduced in C data types, and the operators that combine them in C operators.

Author

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List all C tutorials.