Data Types in C β The Part the Textbook Table Leaves Out
int is not always 4 bytes. long is not the same size on Windows and Linux. Signed overflow is undefined behaviour, not a wraparound guarantee. Here's what actually matters once you leave the classroom.
Last Updated
March 2026
Read Time
15 min
Level
BeginnerβIntermediate
What a Data Type Actually Controls in C
A data type in C decides three things at once, and most tutorials only mention the first: how many bytes the compiler reserves, how those bytes are interpreted (two's-complement integer, IEEE-754 float, raw character), and what arithmetic rules apply when that value is used in an expression. The third one β the rules β is where most real bugs live, not the storage size.
The C standard does not fix exact sizes for most types. It only guarantees minimums: char is at least 8 bits, short and int at least 16, long at least 32, long long at least 64. Everything beyond that minimum is left to the compiler and target platform β which is exactly why the same source file can behave differently on two machines without a single line changing.
The Built-In Types, One Category at a Time
C ships with a small set of primitives, and everything else β arrays, structs, pointers, unions β is built out of these. Here's what each one is actually for, not just what it's called.
Guaranteed at least 8 bits, used for both character storage and, in practice, as the smallest addressable unit of memory β sizeof(char) is always exactly 1 by definition, which is why buffer-size math is usually done in char units.
The 'natural' word size the CPU works with most efficiently. On every mainstream desktop and server platform in 2026 this is 32 bits, even on 64-bit machines β a fact that surprises people expecting int to grow with the architecture.
At least 16 bits, rarely used deliberately today except to save memory in large arrays β a struct with a million short fields instead of int fields is a real 2MB-vs-4MB difference at scale.
At least 32 bits β but this is the type where platform differences bite hardest. 8 bytes on Linux/macOS x86-64, only 4 bytes on 64-bit Windows. Covered in detail below.
Introduced in C99 specifically to guarantee at least 64 bits everywhere, precisely because long's size wasn't consistent enough to rely on for large counters across platforms.
Modifiers, not standalone types, that decide whether the top bit represents sign (two's complement) or extends the positive range. unsigned int on a 32-bit width doubles your positive ceiling to 4,294,967,295 β at the cost of never going negative.
Single-precision IEEE-754, 32 bits, roughly 7 significant decimal digits of precision. Fine for graphics coordinates and sensor readings; risky for anything involving money.
Double-precision IEEE-754, 64 bits, roughly 15-17 significant decimal digits. The default choice for scientific and financial calculations in C β though even this isn't exact for decimal fractions, as shown further down.
_Bool arrived in C99 as a true boolean type stored in at least 1 byte; <stdbool.h> gives you the bool, true, false spelling most people actually use. Before C99, developers just used int with 0/1 by convention.
Not a storage type at all β it represents the absence of a value (for function return types) or, as void*, a pointer with no associated type, used for generic memory handling in functions like malloc().
Actual Sizes You'll Measure β Not Just Standard Minimums
The standard's minimums are a floor, not a promise. Here's what sizeof() actually returns on the platforms most developers hit in practice β measured, not guessed.
The column that causes real pain is long. This is called the LP64 vs LLP64 split β Linux and macOS on 64-bit x86 grow long to 8 bytes (LP64: Long and Pointer are 64-bit), while 64-bit Windows deliberately keeps long at 4 bytes for backward compatibility with existing Win32 code (LLP64: only Long Long and Pointer are 64-bit). I've reviewed a cross-platform video-processing tool where a developer used long to store a frame timestamp in microseconds β it worked fine on the Linux build server and silently truncated on the Windows build the day frame counts crossed roughly 35 minutes of runtime, because a 4-byte long overflowed while the exact same source line on Linux had 4 billion times more headroom.
Signed Integer Overflow β Undefined, Not Wraparound
This is the single most misunderstood fact about C integers: signed integer overflow is undefined behaviour, not a guaranteed wraparound to the negative range. Unsigned overflow, by contrast, IS well-defined β it wraps modulo 2^n exactly as the standard specifies. Mixing up which of these two you're relying on is where real production bugs come from.
I hit this myself on a billing-reconciliation job for a telecom client: a daily call-detail-record counter was declared int, and on a promotional day the count crossed 2,147,483,647 β the maximum value for a 32-bit signed int. The count didn't wrap to a small negative number and get caught by a sanity check like I expected. Instead, with optimisations on (-O2), GCC assumed signed overflow could never happen at all and eliminated the very overflow-check branch I'd written, because from the compiler's point of view that branch was provably unreachable.
#include <stdio.h>
#include <limits.h>
int countCallRecords(int currentCount) {
int next = currentCount + 1;
if (next < currentCount) { // intended overflow check
printf("Counter overflowed!\n");
return -1;
}
return next;
}
int main() {
// GCC -O2 may optimise the overflow check away entirely,
// because it assumes signed overflow (INT_MAX + 1) never happens
printf("%d\n", countCallRecords(INT_MAX));
return 0;
}Compiling with -fsanitize=undefined catches this instantly at runtime: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'. The actual fix was switching the counter to unsigned long (well-defined wraparound, and 8 bytes of headroom on Linux) and adding an explicit ceiling check well before the type's real limit, not relying on wraparound detection at all.
Integer Promotion β Why char + char Isn't a char
C promotes any operand smaller than int β char, short, and their unsigned versions β up to int (or unsigned int) before doing arithmetic. This is called the usual arithmetic conversions, and it's why the following looks wrong until you know the rule:
#include <stdio.h>
int main() {
char a = 100;
char b = 100;
char sum = a + b; // a + b is computed as INT (200), THEN truncated back to char
printf("%d\n", sum); // prints -56 on a platform with signed 8-bit char
return 0;
}a + b is computed in int arithmetic as 200 β no overflow there, since int easily holds 200. The problem is the assignment back into char sum, which on a platform where char is signed 8-bit can only represent -128 to 127. 200 doesn't fit, so it wraps to -56. This isn't a compiler bug; it's the promotion-then-truncation rule doing exactly what the standard says, and it's exactly the kind of thing that shows up in embedded checksum code that quietly produces wrong values only for specific input combinations.
Float and Double Aren't Exact β Here's the Actual Bit Pattern
Everyone eventually discovers that 0.1 + 0.2 != 0.3 in C, but few people check what the actual stored value is. It's not random β it's IEEE-754 double precision doing exactly what it's specified to do with a binary fraction that can't represent 0.1 exactly, the same way 1/3 can't be written exactly in decimal.
#include <stdio.h>
int main() {
double a = 0.1;
double b = 0.2;
double sum = a + b;
printf("%.1f\n", sum); // 0.3 β looks fine at low precision
printf("%.20f\n", sum); // 0.30000000000000004441 β the real stored value
printf("%d\n", sum == 0.3); // 0 β this comparison is FALSE
return 0;
}In my opinion, the right takeaway isn't 'avoid floating point' β it's never compare floats with ==, and never use float/double for money. A GST-invoicing module I reviewed at a retail client stored per-line tax amounts as float and summed hundreds of line items; the accumulated rounding error was small enough per line (fractions of a paisa) to pass casual testing, but large enough across a 400-line wholesale invoice to fail a chartered accountant's manual total-matching check by βΉ0.02 β enough to bounce the invoice in their reconciliation system. The fix was switching to integer paise (a long long counting the smallest currency unit) for anything that had to match to the last decimal.
stdint.h β The Fix for All of the Above
C99 added <stdint.h> specifically because relying on int/long sizes across platforms had already caused enough of exactly the bugs described above. If a size actually matters to your logic β a file format, a network protocol, a hardware register β use a fixed-width type and stop guessing.
- βΆ
int8_t / uint8_tβ exactly 8 bits, signed or unsigned. Common for raw byte buffers and protocol fields. - βΆ
int32_t / uint32_tβ exactly 32 bits everywhere, unlike plainlong, which is the fix for the LP64/LLP64 timestamp bug described earlier. - βΆ
int64_t / uint64_tβ exactly 64 bits everywhere, the safe choice for counters that might genuinely exceed 2 billion β UPI transaction IDs, log-line sequence numbers, file offsets past 2GB. - βΆ
size_tβ the unsigned type returned bysizeof, guaranteed large enough to represent the size of any object; on 64-bit platforms this is typically 8 bytes, which is why comparing a signed loop counter against asize_tarray length triggers-Wsign-compare. - βΆ
intptr_t / uintptr_tβ integer types guaranteed wide enough to hold a pointer value, useful when you genuinely need to do arithmetic on an address as a number, such as in custom memory allocators.
My honest stance after years of cross-platform C: default to int32_t/int64_t for anything that leaves a single translation unit β file formats, network messages, struct layouts shared across a DLL boundary. Reserve plain int for loop counters and other purely local, throwaway arithmetic where the exact width genuinely doesn't matter.
printf Format Specifiers β Where the Type System Stops Protecting You
printf is a variadic function β it has no way to check at compile time (without compiler-specific help) that the format string matches the arguments you actually passed. Get the specifier wrong for a fixed-width type and the compiler, if it's paying attention, tells you exactly what's mismatched:
#include <stdio.h>
#include <stdint.h>
int main() {
int64_t transactionId = 9821345067123;
printf("Transaction: %d\n", transactionId); // wrong specifier for int64_t
return 0;
}$ gcc -Wall -Wformat txn.c -o txn
txn.c:6:29: warning: format '%d' expects argument of type 'int', but argument 2
has type 'int64_t' {aka 'long int'} [-Wformat=]
6 | printf("Transaction: %d\n", transactionId);
| ~^ ~~~~~~~~~~~~~
| | |
| int int64_t {aka long int}The correct fix isn't guessing whether int64_t is long or long long on your platform β that's the exact platform-dependence this whole article is warning about. Use the portable macros from <inttypes.h> instead: printf("Transaction: %" PRId64 "\n", transactionId);. PRId64 expands to whatever the correct specifier actually is on the compiling platform, so the same source line is correct everywhere without an #ifdef in sight.
How I Actually Choose Types in Production C
- βΆ
Use fixed-width types for anything crossing a boundary β file formats, network protocols, struct layouts shared between modules compiled by different toolchains. Plain int/long are fine only for purely local, single-function arithmetic.
- βΆ
Never store money in float or double β use an integer count of the smallest currency unit (paise, cents) or a fixed-point/decimal library. The GST-invoice rounding bug above is not a rare edge case; it's the default outcome of accumulating float sums.
- βΆ
Compile with -Wall -Wextra -Wconversion at minimum β -Wconversion specifically catches implicit narrowing conversions, which is exactly the class of bug in the char-overflow example.
- βΆ
Add -fsanitize=undefined to your test builds β signed overflow, in particular, produces no warning at compile time in most configurations; the sanitizer catches it at the exact line and input that triggers it.
- βΆ
Prefer unsigned only when the value can genuinely never be negative β array indices and sizes, yes. Loop counters that might legitimately go negative in an edge case, no β comparing a signed counter against an unsigned length is a classic -Wsign-compare trap.
- βΆ
Match printf specifiers to inttypes.h macros for fixed-width types β don't assume int64_t is 'long' or 'long long'; let PRId64/PRIu32/etc. resolve it correctly per platform.
Mistakes I See Repeatedly in Code Review
- βΆ
Assuming int is always 4 bytes β true on every mainstream platform in 2026, but code that hardcodes this assumption (instead of using sizeof or stdint.h) breaks the moment it targets a 16-bit microcontroller toolchain where int is still 2 bytes.
- βΆ
Comparing signed and unsigned in the same expression β `if (signedCount < unsignedLimit)` silently converts the signed value to unsigned first; a negative signedCount becomes an enormous positive number, and the comparison does the opposite of what was intended.
- βΆ
Using %d for size_t β `printf("%d", sizeof(arr))` is technically wrong since sizeof returns size_t, not int; on a 64-bit platform this is a real type mismatch, correctly flagged by `-Wformat` as `expects type 'int', but argument has type 'long unsigned int'`. Use `%zu` instead.
- βΆ
Relying on char being signed or unsigned without checking β plain char's signedness is implementation-defined; ARM platforms commonly default it to unsigned while x86 GCC defaults it to signed, which matters the moment you compare a char against a negative sentinel like EOF.
Data Types β Interview Questions
Practice Questions
1. What is the output, assuming a signed 8-bit char? char a = 120; char b = 10; char sum = a + b; printf("%d", sum);
Medium2. On 64-bit Linux, what does sizeof(long) return, and how does that differ on 64-bit Windows?
Easy3. Why might this comparison behave unexpectedly? int index = -1; size_t length = 10; if (index < length) { printf("safe"); }
Hard4. What compiler flag would catch a signed integer overflow at runtime that produces no warning at compile time?
Medium5. What type from stdint.h guarantees exactly 32 bits on every platform, unlike plain long?
Easy6. Given double sum = 0.1 + 0.2;, why does 'if (sum == 0.3)' evaluate to false?
Medium7. What's wrong with printf("%ld", someInt32Value); if someInt32Value is declared as int32_t?
HardClosing Thoughts
The textbook version of C data types β 'int is 4 bytes, float is 4 bytes, double is 8 bytes, done' β is accurate often enough to pass a first course and wrong often enough to break production code the day it touches a second platform, a large enough number, or a currency calculation. Every bug in this article compiled without a single error under default flags; every one of them was caught the moment stricter warnings or a sanitizer were switched on.
If your C code will ever run on more than one platform, hold a value larger than a few hundred thousand, or touch money, don't reach for int and long out of habit β reach for <stdint.h>, know exactly which arithmetic rule you're relying on, and compile with the warnings turned all the way up before you trust the result.