The most common thing people believe about C data types is that int is 32 bits and long is 64. Neither is guaranteed. The standard specifies minimum ranges, not sizes, and the sizes you actually get depend on the platform’s data model — which is why the same program can print 8 for sizeof(long) on Linux and 4 on Windows, on identical 64-bit hardware.
This reference gives you the sizes, ranges, and printf specifiers for every basic type, plus the fixed-width types from stdint.h that exist precisely because the basic ones vary. Everything here was measured rather than quoted: each program was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 under -std=c17 with -Wall -Wextra, and the data-model comparison was produced by compiling the same source twice, once as a 64-bit binary and once with -m32. All output blocks and compiler warnings are captured verbatim. Where a platform could not be tested directly — 64-bit Windows in particular — the article says so rather than presenting documentation as measurement.
Table of Contents
- What Is a Data Type in C?
- Complete Type Reference Table
- The Sizes Are Not Fixed: Data Models
- Fixed-Width Types: <stdint.h>
- Character Types and the Signedness Trap
- The Boolean Type
- Floating-Point Types
- Type Conversions and Integer Promotion
- Format Specifiers: Getting printf Right
- Derived Types
- Key Takeaways
- Frequently Asked Questions
- Conclusion
What Is a Data Type in C?
A data type in C defines how much memory a value occupies, how the bits in that memory are interpreted, and which operations are valid on it. C provides basic types for integers (char, short, int, long, long long), floating-point numbers (float, double, long double), a boolean type (_Bool), and the void type, plus derived types built from them: arrays, pointers, structures, unions, and enumerations.
The critical property, and the one that surprises people moving from Java or C#, is that C does not fix the sizes of its basic types. The standard guarantees minimum ranges — int must hold at least −32,767 to 32,767 — and a size ordering, but the actual widths are chosen by the implementation. Code that assumes otherwise compiles cleanly and breaks on the next platform.
For the normative definitions, cppreference’s arithmetic types page is the reference to keep open alongside this one.
Complete Type Reference Table
Measured on x86-64 Linux (the LP64 model). The Minimum column is what the C standard guarantees everywhere; the Typical column is what this platform actually reports.
| Type | Typical size | Minimum required | Range (typical) | printf |
|---|---|---|---|---|
char | 1 byte | 1 byte | −128 to 127 or 0 to 255 | %c |
signed char | 1 byte | 1 byte | −128 to 127 | %hhd |
unsigned char | 1 byte | 1 byte | 0 to 255 | %hhu |
short | 2 bytes | 2 bytes | −32,768 to 32,767 | %hd |
unsigned short | 2 bytes | 2 bytes | 0 to 65,535 | %hu |
int | 4 bytes | 2 bytes | −2,147,483,648 to 2,147,483,647 | %d |
unsigned int | 4 bytes | 2 bytes | 0 to 4,294,967,295 | %u |
long | 8 bytes | 4 bytes | ±9.22 × 10¹⁸ | %ld |
unsigned long | 8 bytes | 4 bytes | 0 to 18,446,744,073,709,551,615 | %lu |
long long | 8 bytes | 8 bytes | ±9.22 × 10¹⁸ | %lld |
_Bool | 1 byte | — | 0 or 1 | %d |
float | 4 bytes | — | ~1.2E−38 to ~3.4E+38, 6 digits | %f |
double | 8 bytes | — | ~2.3E−308 to ~1.7E+308, 15 digits | %lf |
long double | 16 bytes | — | ~3.4E−4932 to ~1.1E+4932, 18 digits | %Lf |
void * | 8 bytes | — | an address | %p |
size_t | 8 bytes | — | 0 to SIZE_MAX | %zu |
ptrdiff_t | 8 bytes | — | PTRDIFF_MIN to PTRDIFF_MAX | %td |
The measured limits behind that table:
CHAR_BIT (bits per byte) = 8
INT_MIN=-2147483648 INT_MAX=2147483647
LONG_MAX=9223372036854775807 LLONG_MAX=9223372036854775807
UINT_MAX=4294967295 ULONG_MAX=18446744073709551615
FLT_DIG=6 DBL_DIG=15 LDBL_DIG=18
FLT_MAX=3.40282e+38 DBL_MAX=1.79769e+308
Never hard-code these numbers. <limits.h> and <float.h> provide them as macros for whatever platform you are compiling on, and using the macros is the difference between code that ports and code that appears to.
Note sizeof(long double) reporting 16 bytes. That is the storage size including padding for alignment — the x86 80-bit extended format uses 10 of those bytes, and the remaining 6 are padding. LDBL_DIG reporting 18 significant digits, rather than the 33 you would get from a true 128-bit quad type, confirms it.
The Sizes Are Not Fixed: Data Models
Here is the demonstration that matters most, and it needs no second machine. The same source compiled twice:
printf("int=%zu long=%zu long long=%zu pointer=%zu size_t=%zu\n",
sizeof(int), sizeof(long), sizeof(long long),
sizeof(void*), sizeof(size_t));
=== 64-bit build (default) ===
int=4 long=8 long long=8 pointer=8 size_t=8 -> LP64
=== 32-bit build (-m32) ===
int=4 long=4 long long=8 pointer=4 size_t=4 -> ILP32
Same compiler, same machine, same source — long, void * and size_t all halved. int and long long did not move.
There is a third model that catches people porting between operating systems:
| Model | int | long | long long | pointer | Used by |
|---|---|---|---|---|---|
| ILP32 | 4 | 4 | 8 | 4 | 32-bit Linux, Windows, older embedded |
| LP64 | 4 | 8 | 8 | 8 | 64-bit Linux, macOS, BSD |
| LLP64 | 4 | 4 | 8 | 8 | 64-bit Windows |
The LP64/LLP64 split is the classic portability trap: on 64-bit Linux long is wide enough to hold a pointer, and on 64-bit Windows it is not. Code that stores a pointer in a long works perfectly until someone builds it with MSVC. (LLP64 figures come from Microsoft’s documented model, not from a build performed here — this sandbox is Linux-only.)
The correct way to store a pointer as an integer is uintptr_t from <stdint.h>, which is defined to be wide enough on every platform.
Fixed-Width Types: <stdint.h>
Because the basic types vary, C99 added exact-width types. When your file format, network protocol, or hardware register needs exactly 32 bits, say exactly 32 bits:
TYPE BYTES MAX SPECIFIER
int8_t 1 127 PRId8
int16_t 2 32767 PRId16
int32_t 4 2147483647 PRId32
int64_t 8 9223372036854775807 PRId64
uint64_t 8 18446744073709551615 PRIu64
uintptr_t 8 holds any pointer PRIuPTR
Printing them portably needs <inttypes.h>, because the right specifier for int64_t is %ld on LP64 and %lld on LLP64. The PRI macros resolve to whichever is correct:
#include <inttypes.h>
int64_t big = 9000000000;
printf("Portable printing of int64_t: %" PRId64 "\n", big);
Portable printing of int64_t: 9000000000
The syntax looks strange because PRId64 expands to a string literal that the compiler concatenates with the surrounding format string. It is worth the awkwardness on any code that crosses platforms.
Which to use: reach for int for ordinary loop counters and arithmetic, where you want the platform’s natural word size. Reach for int32_t, uint8_t and friends when the width is part of the contract — file formats, wire protocols, memory-mapped registers. Use size_t for anything that is a size or an array index, and ptrdiff_t for the difference between two pointers.
Character Types and the Signedness Trap
char is the only type in C that comes in three distinct flavours: char, signed char, and unsigned char. The first is a separate type from the other two, and whether it is signed is implementation-defined.
Both outcomes, produced on the same machine:
=== x86-64 default ===
CHAR_MIN=-128 CHAR_MAX=127 -> plain char is SIGNED
=== forced unsigned (-funsigned-char, what ARM/PowerPC do by default) ===
CHAR_MIN=0 CHAR_MAX=255 -> plain char is UNSIGNED
This is not a hypothetical. Plain char is signed on x86 and unsigned on most ARM and PowerPC platforms — which is why code that works on a developer’s laptop can misbehave on a Raspberry Pi. The classic failure is a byte value above 127 read into a plain char, which becomes negative on x86 and stays positive on ARM.
The rules that avoid it: use plain char only for actual text characters; use unsigned char for raw bytes and binary data; use signed char when you genuinely want a small signed integer. And note that char is always exactly 1 byte by definition — sizeof(char) is 1 everywhere — but a byte is not guaranteed to be 8 bits. CHAR_BIT reports the true width, and while it is 8 on every mainstream platform, some DSPs use 16 or 32.
Character constants have a further surprise: in C, 'A' has type int, not char. So sizeof('A') is 4 on this platform, not 1. (In C++ it is 1 — one of the genuine incompatibilities between the languages.)
The Boolean Type
C has had a boolean type since C99. _Bool is a built-in type, and including <stdbool.h> provides the friendlier spellings bool, true, and false. Any C tutorial claiming otherwise is describing C89, a standard now more than three decades old.
_Bool is not merely a small int. It normalises every assigned value to exactly 0 or 1:
2. _Bool normalises, int does not
_Bool b = 42 -> 1
_Bool b = 0.5 -> 1
int i = 42 -> 42
3. Consequence for comparisons
(_Bool)2 == (_Bool)1 -> true
(int)2 == (int)1 -> false
That last pair is the practical reason to use it. Two different non-zero “true” values compare equal as _Bool and unequal as int — which is exactly the bug that the TRUE/FALSE macro idiom used to produce.
In C23, bool, true and false become proper keywords and <stdbool.h> is no longer required, though including it remains harmless.
Floating-Point Types
float, double and long double store real numbers in IEEE 754 format on essentially all modern platforms. The measured precision on x86-64: FLT_DIG 6 significant digits, DBL_DIG 15, LDBL_DIG 18.
Use double by default. float exists to save memory when storing many values, and its 6-digit precision runs out faster than people expect. An unsuffixed literal like 3.14 is already a double; write 3.14f for a float or 3.14L for a long double.
The rule that causes the most trouble: never compare floating-point values with ==. Values that are mathematically equal frequently are not equal in binary, because most decimal fractions have no exact binary representation. Compare against a tolerance instead:
#include <math.h>
if (fabs(a - b) < 1e-9) { /* close enough to equal */ }
Choosing that tolerance sensibly depends on the magnitude of the values involved — the guide to the math library covers the functions available for it.
Type Conversions and Integer Promotion
C converts operands automatically before most operations, and the rules are where a lot of quiet bugs live.
Integer promotion converts anything narrower than int — char, short, _Bool, bitfields — to int before arithmetic:
3. Integer promotion
sizeof(char) = 1 but sizeof(x + y) = 4
x + y = 200 (computed as int, no overflow at 127)
Two char values holding 100 each sum to 200, not to an overflowed −56, because the addition happens in int.
The usual arithmetic conversions then bring both operands to a common type. Among integers the wider rank wins, and if the ranks are equal but the signedness differs, unsigned wins — the source of the -1 < 1u surprise covered in the guide to operators in C.
Here is a conversion result that a lot of references get wrong. What type is int + float?
1. What type is (int + float)?
sizeof(a) = 4 (int)
sizeof(b) = 4 (float)
sizeof(a + b) = 4 -> FLOAT, not double
The result is float. The int is converted to float, and the addition happens in single precision — the value is only widened to double afterwards if you assign it to one, by which point any precision already lost is gone. This matters when accumulating: sum a large array in float and the error compounds.
Truncation goes the other way, and it discards information silently:
4. Truncation: int 321 -> char
result = 65 ('A')
5. Float to int conversion truncates toward zero
(int)3.99 = 3
(int)-3.99 = -3 (not -4)
Assigning 321 to a char keeps the low 8 bits, giving 65 — the character 'A'. Converting a floating-point value to an integer truncates toward zero, so −3.99 becomes −3, not −4. Use floor(), ceil() or round() when you want a specific rounding behaviour rather than whatever truncation happens to give.
Format Specifiers: Getting printf Right
A mismatched printf specifier is undefined behaviour, not a cosmetic issue. The two that catch people most often are size_t and long, because %d appears to work on 32-bit platforms and then corrupts output on 64-bit ones.
The compiler will tell you, if you let it:
warning: format '%d' expects argument of type 'int',
but argument 2 has type 'size_t' {aka 'long unsigned int'} [-Wformat=]
warning: format '%d' expects argument of type 'int',
but argument 2 has type 'long int' [-Wformat=]
Both diagnostics come from -Wall. The correct forms:
| Type | Specifier | Note |
|---|---|---|
int | %d | |
unsigned int | %u | |
long | %ld | not %d |
long long | %lld | |
size_t | %zu | the most commonly wrong one |
ptrdiff_t | %td | |
char (as number) | %hhd | |
float / double | %f | float promotes to double in varargs |
long double | %Lf | |
| pointer | %p | cast to void * |
int32_t | PRId32 | from <inttypes.h> |
int64_t | PRId64 | resolves per platform |
Derived Types
The basic types combine into derived ones, each covered in depth elsewhere:
- Arrays — a contiguous block of same-typed elements. An array name decays to a pointer to its first element in most contexts, which is why
sizeofbehaves differently inside a function than outside it. - Pointers — hold addresses.
sizeof(void *)is 8 on LP64 and LLP64, 4 on ILP32; see pointers to pointers in C for the deeper treatment. - Structures — group members of different types, with padding inserted for alignment, which is why
sizeof(struct)is often larger than the sum of its members. Covered in structures in C. - Unions — overlay members in the same memory; the size is that of the largest member.
- Enumerations — named integer constants. The underlying type is implementation-defined in C17; C23 lets you specify it.
void— the incomplete type, used for functions returning nothing and for generic pointers.
Allocating memory for derived types brings its own set of decisions — malloc versus calloc covers the practical differences, including why sizeof *ptr is safer than sizeof(type) in an allocation.
Key Takeaways
- C fixes minimum ranges, not sizes.
sizeof(long)measured 8 in a 64-bit build and 4 in a 32-bit build of the same source on the same machine. - Three data models matter: ILP32, LP64 (Linux/macOS) and LLP64 (Windows).
longis 8 bytes on 64-bit Linux and 4 on 64-bit Windows — the most common porting break. - Plain
charsignedness is implementation-defined — signed on x86, unsigned on most ARM. Useunsigned charfor raw bytes, plaincharonly for text. - C has had
boolsince C99._Boolnormalises to 0 or 1, which is why(_Bool)2 == (_Bool)1is true while theintequivalent is false. int + floatyieldsfloat, notdouble— a widespread error worth checking your own code for.- Use
<stdint.h>when width is part of the contract and<limits.h>rather than hard-coded constants. %zuforsize_t,%ldforlong.-Wallcatches every mismatch; there is no reason to guess.
Frequently Asked Questions
Conclusion
The recurring theme in everything above is that C’s types describe a contract with the machine, not a fixed layout. That is deliberate: it is what let C target everything from 8-bit microcontrollers to 64-bit servers using the same language. The cost is that portable code has to be written with the variability in mind rather than discovered during a port.
Two habits cover most of it. Ask what a value is before you ask what type to give it — a count of things is a size_t, a field in a file format is an int32_t, a character is a char — and compile with -Wall -Wextra so the mismatches you do make get named rather than shipped. From there, the C programming section covers how these types behave once they are inside expressions, structures, and allocated memory.



