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
- Print SHRT_MAX, SCHAR_MIN and LLONG_MIN and check each against the range the standard guarantees.
- Write a loop that counts up in an unsigned char and stops after the wrap. How many iterations does it run?
- 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.
-
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.
-
What does
CHAR_BITreport on the machine used in this course?Answer
8
CHAR_BITis the number of bits in a byte, which is 8 on this platform. -
What happens when an
unsigned intholdingUINT_MAXis 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_MAXgives zero. -
What does the type
int32_tguarantee?Answer
Exactly 32 bits
The fixed-width types from
stdint.hname an exact number of bits, unlikeint, whose size depends on the platform. -
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. -
What does
UINT_MAX / 2 + 1evaluate to?Answer
2147483648
Half of the maximum plus one sets the top bit, which is representable as unsigned but not as a signed
int. -
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.
-
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. -
Which warning flag reports a comparison between an
intand anunsigned int?Answer
-Wsign-compare-Wsign-compare, part of-Wextra, names the two signednesses and the line of the comparison. -
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