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.
#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.
#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.
#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.
#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.
#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.
#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.
- Check the limit macros instead of assuming that
intis 32 bits. - Use the fixed-width types when the layout of data matters, such as in file formats and network protocols.
- Never let a signed value overflow; test before adding, or use a wider type.
- Do not mix signed and unsigned values in comparisons; compile with
-Wsign-compare, which-Wextraenables. - Use
size_tfor counts and indices, andptrdiff_tfor pointer differences. - Use
-fsanitize=undefinedwhile testing to catch overflow and other undefined behavior.
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
List all C tutorials.