Types Part 1: Integer Types and Representations
This chapter covers every integer type in C, how they are represented in memory, what ranges they guarantee, and how conversions between them behave. It is one of the most portability-sensitive chapters in the language.
Why This Matters
Almost every C bug involving overflow, truncation, sign extension, or a surprising comparison result traces back to a misunderstanding of integer types. Systems and embedded programmers cannot write correct code — much less secure code — without knowing exactly what each integer type guarantees and what it does not.
Prerequisites
c.core.2— objects, values, and the abstract machine.
Core Concept
The integer types
C has two broad families of integer types: signed and unsigned. The basic integer types are:
| Type | Signedness | Minimum bits (C99+) | Typical bits (LP64) |
|---|---|---|---|
char | implementation-defined | 8 | 8 |
signed char | signed | 8 | 8 |
unsigned char | unsigned | 8 | 8 |
short / short int | signed | 16 | 16 |
unsigned short | unsigned | 16 | 16 |
int | signed | 16 | 32 |
unsigned int | unsigned | 16 | 32 |
long / long int | signed | 32 | 64 |
unsigned long | unsigned | 32 | 64 |
long long / long long int | signed | 64 | 64 |
unsigned long long | unsigned | 64 | 64 |
_Bool | unsigned | 1 (value range) | 1 |
wchar_t, char16_t, char32_t | implementation-defined | — | varies |
"Minimum bits" is the minimum *number of value bits plus sign bit* required by the standard; the actual size is implementation-defined and is what sizeof reports in bytes.
A subtlety: char is its own type
char, signed char, and unsigned char are three distinct types. char is the same size as signed char and unsigned char (one byte), but it is a separate type, and whether char is signed or unsigned is implementation-defined.
This matters: on x86 GCC/Clang, char is signed by default; on ARM many toolchains, char is unsigned. Code that depends on char being signed is non-portable.
_Bool
_Bool (available as bool via <stdbool.h> since C99) holds only 0 or 1. Any nonzero value converted to _Bool becomes 1. C23 makes bool, true, and false keywords; in C99–C17 they come from the header.
How It Works
Value ranges
For an integer type with N value bits:
- Signed (two's complement, required by C23 and the de facto standard
everywhere): range is -(2^(N-1)) to 2^(N-1) - 1.
- Unsigned: range is
0to2^N - 1.
The standard historically allowed signed integers to be represented in three ways:
1. Two's complement (virtually universal; required by C23). 2. One's complement (obsolete; C17 permitted but rarely used). 3. Sign-and-magnitude (obsolete; C17 permitted but rarely used).
C23 requires two's complement. In C17 and earlier, the representation is implementation-defined but two's complement is the only one you will encounter on real hardware. VERIFIED
Two's complement, precisely
In two's complement with N bits, a bit pattern b represents:
- If the sign bit is 0: the unsigned value of the
N-1low bits. - If the sign bit is 1: the unsigned value of all
Nbits minus2^N.
This makes the most negative value -2^(N-1), which has no positive counterpart. Negating it is undefined behavior because the result is not representable.
Padding bits (C17, not C23)
In C17 and earlier, an integer type can have padding bits that do not participate in the value. C23 removed padding bits for standard integer types. In practice, all mainstream compilers have zero padding bits. PARTIALLY VERIFIED (historically real, practically absent today).
Syntax
Declaring integers
int a;
unsigned int b;
long c;
unsigned long long d;
signed char e;
_Bool f;
Suffixes for literals
| Suffix | Type it selects |
|---|---|
| none | int, else long, else long long (for decimal); also unsigned for octal/hex |
u/U | unsigned variant of the candidate |
l/L | long variant |
ll/LL | long long variant |
ul, lu, etc. | unsigned long |
ull, llu | unsigned long long |
Example:
42 /* int */
42u /* unsigned int */
42L /* long */
42UL /* unsigned long */
42LL /* long long */
42ULL /* unsigned long long */
Examples
Printing integer types portably
Use <inttypes.h> macros for exact-width types:
#include <stdio.h>
#include <inttypes.h>
int main(void)
{
int32_t a = -5;
uint64_t b = 18446744073709551615ULL;
printf("%" PRId32 "\n", a);
printf("%" PRIu64 "\n", b);
return 0;
}
Expected output:
-5
18446744073709551615
The PRId32 and PRIu64 macros expand to the correct format specifier for the platform. This is the only portable way to print exact-width types.
sizeof and limits
#include <stdio.h>
#include <limits.h>
int main(void)
{
printf("int min: %d\n", INT_MIN);
printf("int max: %d\n", INT_MAX);
printf("unsigned int max: %u\n", UINT_MAX);
printf("sizeof(int): %zu\n", sizeof(int));
return 0;
}
Expected output is implementation-defined; on a typical LP64 system:
int min: -2147483648
int max: 2147483647
unsigned int max: 4294967295
sizeof(int): 4
<limits.h> provides INT_MIN, INT_MAX, UINT_MAX, CHAR_MIN, CHAR_MAX, SCHAR_MIN, SCHAR_MAX, UCHAR_MAX, SHRT_MIN, SHRT_MAX, USHRT_MAX, LONG_MIN, LONG_MAX, ULONG_MAX, LLONG_MIN, LLONG_MAX, ULLONG_MAX. CHAR_BIT gives the number of bits in a byte (at least 8).
Variations
Fixed-width types (<stdint.h>)
C99 introduced exact-width types. Use these when the width matters (file formats, network protocols, hardware registers):
| Type | Meaning |
|---|---|
int8_t, uint8_t | exactly 8 bits (if such a type exists) |
int16_t, uint16_t | exactly 16 bits |
int32_t, uint32_t | exactly 32 bits |
int64_t, uint64_t | exactly 64 bits |
intptr_t, uintptr_t | large enough to hold a pointer |
intmax_t, uintmax_t | largest supported integer type |
ptrdiff_t (in <stddef.h>) | signed result of pointer subtraction |
size_t (in <stddef.h>) | unsigned result of sizeof |
The exact-width types are optional — they exist only if the implementation has a type of exactly that width with no padding. In practice, all modern general-purpose systems provide them, but some DSPs with unusual word sizes do not. VERIFIED
Minimum-width and fastest types
<stdint.h> also provides int_leastN_t (at least N bits) and int_fastN_t (fastest type with at least N bits). Prefer these for general code where an exact width is not required but a minimum is.
Common Mistakes
- Assuming
intis 32 bits. It is at least 16 bits. - Assuming
charis signed. It is implementation-defined. - Assuming
longis 64 bits. On 64-bit Windows,longis 32 bits (LLP64);
on 64-bit Linux/macOS, long is 64 bits (LP64).
- Using
%dto print asize_toruint64_t. This is UB if the types do not
match; use %zu, %llu, or <inttypes.h> macros.
- Comparing signed and unsigned without understanding the conversions
(c.types.uac).
Undefined Behavior
- Signed overflow is undefined behavior.
INT_MAX + 1is UB. Unsigned
arithmetic wraps modulo 2^N and is well-defined. VERIFIED
- Negating
INT_MINis UB. - Shifting a signed integer in ways that overflow or produce a negative result
can be UB (c.ops.shift).
- Division by zero or
INT_MIN / -1is UB.
Portability
- The size of each standard integer type is implementation-defined, bounded
only by the minimum ranges.
- The signedness of
charis implementation-defined. - The representation of signed integers was implementation-defined before C23.
- The data model (ILP32, LP64, LLP64) is platform-specific:
| Model | int | long | pointer | Common on |
|---|---|---|---|---|
| ILP32 | 32 | 32 | 32 | 32-bit Unix, Windows |
| LP64 | 32 | 64 | 64 | 64-bit Linux, macOS |
| LLP64 | 32 | 32 | 64 | 64-bit Windows |
Under the Hood
Integer arithmetic maps directly to CPU integer instructions. The compiler chooses the instruction width from the type after the integer promotions and usual arithmetic conversions (c.core.14). A 32-bit add on a 64-bit CPU may be a 32-bit instruction, and the result is truncated to 32 bits.
Two's complement is natural for hardware because addition, subtraction, and multiplication work identically for signed and unsigned at the bit level; only the interpretation of the result differs. This is why unsigned arithmetic is defined to wrap: wrapping is exactly what the hardware does.
Practical Usage
- Use
intfor general-purpose loop counters and small values. - Use
size_tfor sizes, counts, and array indices (it is unsigned and wide
enough for any object).
- Use
ptrdiff_tfor pointer differences. - Use exact-width types when reading/writing binary data, network packets, or
hardware registers.
- Use
uintptr_t/intptr_twhen you must store a pointer in an integer.
Exercises
1. Write a program that prints sizeof and the limits for every basic integer type. Run it on at least two platforms (or cross-compile) and compare. 2. Determine whether char is signed or unsigned on your compiler. Do not rely on the answer in portable code; document how you tested. 3. Write a function that detects whether adding two ints would overflow *without* triggering undefined behavior (check before adding). 4. Demonstrate that INT_MIN negated is UB by writing a program and running it under UBSan (Part 6 covers sanitizers).
Deep Challenge
Implement a small library that safely adds, subtracts, and multiplies signed integers, returning an error on overflow, using only well-defined operations. Explain for each function exactly why your check is correct and does not itself overflow.
Related Concepts
c.core.14— integer promotions and usual arithmetic conversions.c.core.4— floating-point types.c.stdlib.5— stdint.h and stddef.h in depth.c.ops.shift— shift operators.c.sec.2— integer overflow and truncation in security.
References
- ISO/IEC 9899:2018 §6.2.5 (types), §6.2.6 (representations), §5.2.4.2.1
(limits), §7.20 (stdint.h).
- GCC documentation on
-funsigned-char,-fsigned-char. - The 64-bit data model documentation for your target platform.
Verification
- Two's complement is required by C23 and is the universal de facto
representation. VERIFIED
- Signed overflow is UB; unsigned wraps.
VERIFIED char,signed char,unsigned charare three distinct types.VERIFIED_Boolstores 0 or 1.VERIFIED- The LP64/LLP64 table is platform documentation.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Integer type list and sizes
- [ ] char vs. signed char vs. unsigned char
- [ ] _Bool
- [ ] Two's complement representation
- [ ] Value ranges and limits.h
- [ ] Integer literal suffixes
- [ ] stdint.h fixed-width types
- [ ] size_t, ptrdiff_t, intptr_t, uintptr_t
- [ ] Signed overflow UB vs. unsigned wrapping
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.types.int | 0 | 6 |