Fundamental Data Types
Introduction
Data types define what values you can store, how much memory they use, which operations apply, and how the compiler interprets bits. In C, sizes of some types are implementation-defined (within standard minimums), so portable code measures with sizeof and uses limits headers or fixed-width types when exact width matters.
This chapter covers integer and floating types, char and bool, full sizeof / limits.h / float.h demos, signed overflow undefined behavior and safer patterns, and a strong emphasis on stdint.h fixed-width types—with full programs you can run on Linux.
Compile convention:
gcc -std=c17 -Wall -Wextra -o program program.c
./programBasic Data Types in C
- Integer types — whole numbers
- Floating-point types — real numbers
- Character types —
char(integer family)
- Boolean —
_Bool/bool(C99+)
Integer Types
Typical Sizes (LP64 Linux, e.g. x86_64)
These are common on 64-bit Linux with GCC—not guarantees for every platform.
| Type | Size (typical) | Range (typical, signed) |
|---|---|---|
char |
1 byte | −128..127 or 0..255 if unsigned char |
short |
2 bytes | −32 768..32 767 |
int |
4 bytes | −2 147 483 648..2 147 483 647 |
long |
8 bytes (LP64) | full 64-bit signed range |
long long |
8 bytes | full 64-bit signed range |
Unsigned Variants
| Type | Typical max |
|---|---|
unsigned char |
255 |
unsigned short |
65 535 |
unsigned int |
4 294 967 295 |
unsigned long |
2⁶⁴−1 on LP64 |
unsigned long long |
2⁶⁴−1 |
Unsigned arithmetic wraps modulo 2ⁿ (well-defined). Signed overflow is not well-defined (see below).
Worked Demo: sizeof of Major Types
Run this on your machine and keep the output—it is your ABI cheatsheet.
/* sizeof_tour.c */
#include <stdio.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
int main(void) {
printf("=== sizeof tour (this machine) ===\n");
printf("char: %zu\n", sizeof(char));
printf("signed char: %zu\n", sizeof(signed char));
printf("unsigned char: %zu\n", sizeof(unsigned char));
printf("short: %zu\n", sizeof(short));
printf("unsigned short:%zu\n", sizeof(unsigned short));
printf("int: %zu\n", sizeof(int));
printf("unsigned int: %zu\n", sizeof(unsigned int));
printf("long: %zu\n", sizeof(long));
printf("unsigned long: %zu\n", sizeof(unsigned long));
printf("long long: %zu\n", sizeof(long long));
printf("unsigned long long: %zu\n", sizeof(unsigned long long));
printf("float: %zu\n", sizeof(float));
printf("double: %zu\n", sizeof(double));
printf("long double: %zu\n", sizeof(long double));
printf("bool / _Bool: %zu\n", sizeof(bool));
printf("void *: %zu\n", sizeof(void *));
printf("size_t: %zu\n", sizeof(size_t));
printf("ptrdiff_t: %zu\n", sizeof(ptrdiff_t));
printf("int8_t: %zu\n", sizeof(int8_t));
printf("int16_t: %zu\n", sizeof(int16_t));
printf("int32_t: %zu\n", sizeof(int32_t));
printf("int64_t: %zu\n", sizeof(int64_t));
printf("uint64_t: %zu\n", sizeof(uint64_t));
printf("intptr_t: %zu\n", sizeof(intptr_t));
printf("uintptr_t: %zu\n", sizeof(uintptr_t));
printf("intmax_t: %zu\n", sizeof(intmax_t));
int x = 0;
printf("sizeof x: %zu (parens optional for expressions)\n", sizeof x);
return 0;
}gcc -std=c17 -Wall -Wextra -o sizeof_tour sizeof_tour.c
./sizeof_tourSample-shaped output on a typical x86_64 Linux GCC (yours may match):
char: 1, short: 2, int: 4, long: 8, long long: 8
float: 4, double: 8, long double: 16
void *: 8
On 32-bit ILP32 Linux, long and pointers are often 4 bytes—another reason not to assume.
Limits from limits.h
/* limits_demo.c */
#include <stdio.h>
#include <limits.h>
int main(void) {
printf("=== limits.h (signed) ===\n");
printf("CHAR_BIT = %d bits in a byte\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("SCHAR_MAX = %d\n", SCHAR_MAX);
printf("SHRT_MIN = %d\n", SHRT_MIN);
printf("SHRT_MAX = %d\n", SHRT_MAX);
printf("INT_MIN = %d\n", INT_MIN);
printf("INT_MAX = %d\n", INT_MAX);
printf("LONG_MIN = %ld\n", LONG_MIN);
printf("LONG_MAX = %ld\n", LONG_MAX);
printf("LLONG_MIN = %lld\n", LLONG_MIN);
printf("LLONG_MAX = %lld\n", LLONG_MAX);
printf("\n=== limits.h (unsigned maxima) ===\n");
printf("UCHAR_MAX = %u\n", UCHAR_MAX);
printf("USHRT_MAX = %u\n", USHRT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
printf("ULONG_MAX = %lu\n", ULONG_MAX);
printf("ULLONG_MAX = %llu\n", ULLONG_MAX);
return 0;
}gcc -std=c17 -Wall -Wextra -o limits_demo limits_demo.c
./limits_demoUse these macros for portable range checks instead of hard-coded 2147483647.
Floating-Point Types and float.h
| Type | Typical size | Approx. decimal digits |
|---|---|---|
float |
4 | ~6–7 (FLT_DIG) |
double |
8 | ~15–16 (DBL_DIG) |
long double |
8–16 | extended (LDBL_DIG) |
/* float_limits_demo.c */
#include <stdio.h>
#include <float.h>
int main(void) {
printf("=== sizes ===\n");
printf("float: %zu\n", sizeof(float));
printf("double: %zu\n", sizeof(double));
printf("long double: %zu\n", sizeof(long double));
printf("\n=== decimal digits of precision ===\n");
printf("FLT_DIG = %d\n", FLT_DIG);
printf("DBL_DIG = %d\n", DBL_DIG);
printf("LDBL_DIG = %d\n", LDBL_DIG);
printf("\n=== min positive normalized / max finite ===\n");
printf("FLT_MIN = %e FLT_MAX = %e\n", FLT_MIN, FLT_MAX);
printf("DBL_MIN = %e DBL_MAX = %e\n", DBL_MIN, DBL_MAX);
printf("LDBL_MIN = %Le LDBL_MAX = %Le\n", LDBL_MIN, LDBL_MAX);
printf("\n=== epsilon (1 + eps != 1) ===\n");
printf("FLT_EPSILON = %e\n", FLT_EPSILON);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);
printf("LDBL_EPSILON = %Le\n", LDBL_EPSILON);
printf("\n=== radix and mantissa bits ===\n");
printf("FLT_RADIX = %d\n", FLT_RADIX);
printf("FLT_MANT_DIG = %d DBL_MANT_DIG = %d LDBL_MANT_DIG = %d\n",
FLT_MANT_DIG, DBL_MANT_DIG, LDBL_MANT_DIG);
return 0;
}gcc -std=c17 -Wall -Wextra -o float_limits_demo float_limits_demo.c
./float_limits_demoClassic Precision Surprise
#include <stdio.h>
int main(void) {
double a = 0.1 + 0.2;
printf("0.1 + 0.2 = %.20f\n", a);
printf("equals 0.3? %s\n", (a == 0.3) ? "yes" : "no");
return 0;
}gcc -std=c17 -Wall -Wextra -o fp_surprise fp_surprise.c
./fp_surpriseCharacter Types
char is an integer type holding a character code (often ASCII/UTF-8 unit). Whether plain char is signed is implementation-defined.
#include <stdio.h>
int main(void) {
char letter = 'A';
char from_code = 65;
printf("Letter: %c\n", letter);
printf("As int: %d\n", letter);
printf("from_code: %c\n", from_code);
signed char sc = -1;
unsigned char uc = 255;
printf("signed char -1 -> %d\n", sc);
printf("unsigned char 255 -> %u\n", uc);
return 0;
}gcc -std=c17 -Wall -Wextra -o char_demo char_demo.c
./char_demoPrefer unsigned char for raw byte buffers; prefer signed char when you need negative small integers explicitly.
Boolean Type (C99+)
#include <stdio.h>
#include <stdbool.h>
int main(void) {
_Bool flag = 1;
bool ok = true;
bool bad = false;
printf("flag=%d ok=%d bad=%d\n", flag, ok, bad);
if (ok) {
printf("Task complete path\n");
}
return 0;
}Any scalar converts to _Bool: zero → 0, nonzero → 1.
Fixed-Width Types (stdint.h) — Prefer These When Width Matters
int and long change meaning across ABIs. For wire formats, file headers, embedded registers, cryptography, and cross-platform protocols, use exact-width types when the implementation provides them (all modern hosted Linux toolchains do for 8/16/32/64).
Exact-Width
| Type | Width | Signed range (two’s complement) |
|---|---|---|
int8_t / uint8_t |
8 | −128..127 / 0..255 |
int16_t / uint16_t |
16 | ±2¹⁵ / 0..2¹⁶−1 |
int32_t / uint32_t |
32 | ±2³¹ / 0..2³²−1 |
int64_t / uint64_t |
64 | ±2⁶³ / 0..2⁶⁴−1 |
Also: int_leastN_t, int_fastN_t, intmax_t, intptr_t, uintptr_t.
Printing Portably: inttypes.h
/* stdint_demo.c */
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
int main(void) {
int8_t a = -5;
uint8_t b = 200;
int16_t c = -1000;
uint16_t d = 50000;
int32_t e = 123456789;
uint32_t f = 4000000000u;
int64_t g = -123456789012345LL;
uint64_t h = 123456789012345ULL;
printf("int8: %" PRId8 "\n", a);
printf("uint8: %" PRIu8 "\n", b);
printf("int16: %" PRId16 "\n", c);
printf("uint16: %" PRIu16 "\n", d);
printf("int32: %" PRId32 "\n", e);
printf("uint32: %" PRIu32 "\n", f);
printf("int64: %" PRId64 "\n", g);
printf("uint64: %" PRIu64 "\n", h);
/* Hex */
printf("uint32 hex: 0x%08" PRIX32 "\n", f);
/* Pointer-sized integers */
void *p = &e;
uintptr_t raw = (uintptr_t)p;
printf("pointer as uintptr_t: 0x%" PRIxPTR "\n", raw);
/* Limits from stdint.h */
printf("INT32_MIN=%" PRId32 " INT32_MAX=%" PRId32 "\n",
INT32_MIN, INT32_MAX);
printf("UINT64_MAX=%" PRIu64 "\n", UINT64_MAX);
return 0;
}gcc -std=c17 -Wall -Wextra -o stdint_demo stdint_demo.c
./stdint_demoWhen to Use What
| Need | Choose |
|---|---|
| Exact layout / protocol field | intN_t / uintN_t |
| “At least N bits”, speed OK | int_fastN_t |
| “At least N bits”, small size | int_leastN_t |
| Object sizes, array indices | size_t |
| Pointer difference | ptrdiff_t |
| Generic local counter on host | int is fine |
| File offsets (POSIX) | often off_t (with feature macros) |
Example: Portable Binary Header
/* packet_header.c */
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
struct PacketHeader {
uint32_t magic; /* 0x4B313947 'K19G' style demo */
uint16_t version;
uint16_t length;
uint32_t crc32;
};
int main(void) {
struct PacketHeader h = {
.magic = 0x4B313947u,
.version = 1,
.length = 42,
.crc32 = 0xDEADBEEFu
};
printf("sizeof header = %zu (expect 12 if no padding)\n", sizeof h);
printf("magic=%08" PRIX32 " ver=%" PRIu16 " len=%" PRIu16 " crc=%08" PRIX32 "\n",
h.magic, h.version, h.length, h.crc32);
/* Serialize explicitly (endianness still your responsibility) */
uint8_t buf[12];
memcpy(buf + 0, &h.magic, 4);
memcpy(buf + 4, &h.version, 2);
memcpy(buf + 6, &h.length, 2);
memcpy(buf + 8, &h.crc32, 4);
printf("bytes:");
for (size_t i = 0; i < sizeof buf; i++) {
printf(" %02X", buf[i]);
}
printf("\n");
return 0;
}gcc -std=c17 -Wall -Wextra -o packet_header packet_header.c
./packet_headerNote: memcpy of multi-byte integers uses host endianness. Network protocols often need explicit big-endian packing.
Signed Overflow Is Undefined Behavior
For signed integer types, overflow (and some underflows) are undefined behavior (UB) in C. The compiler may assume signed overflow never happens and optimize accordingly—including deleting “impossible” checks.
/* overflow_ub_note.c — educational; do not rely on wraparound of signed int */
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
int x = INT_MAX;
printf("INT_MAX = %d\n", x);
/* The expression (x + 1) is UB if it overflows.
* In practice you might see wrap, a trap, or surprising optimization.
* Do NOT write production code that expects wrap on signed overflow.
*/
printf("Naive signed x+1 is UB if evaluated at INT_MAX.\n");
/* Well-defined wrap: use unsigned */
unsigned int ux = (unsigned int)INT_MAX;
printf("unsigned wrap: %u + 1 = %u\n", ux, ux + 1u);
/* Or use wider type for the math */
int64_t wide = (int64_t)x + 1;
printf("widened: %" PRId64 "\n", wide);
return 0;
}gcc -std=c17 -Wall -Wextra -o overflow_ub_note overflow_ub_note.c
./overflow_ub_noteSafer patterns:
/* overflow_safe_patterns.c */
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>
/* Return false if a + b would overflow int */
static bool safe_add_int(int a, int b, int *out) {
if (b > 0 && a > INT_MAX - b) {
return false;
}
if (b < 0 && a < INT_MIN - b) {
return false;
}
*out = a + b;
return true;
}
/* Unsigned: wrap is defined; detect wrap if you need saturation/error */
static bool safe_add_u32(uint32_t a, uint32_t b, uint32_t *out) {
if (a > UINT32_MAX - b) {
return false; /* would wrap */
}
*out = a + b;
return true;
}
static bool safe_mul_int(int a, int b, int *out) {
if (a == 0 || b == 0) {
*out = 0;
return true;
}
/* Use int64_t intermediate when int is 32-bit — common on Linux */
int64_t r = (int64_t)a * (int64_t)b;
if (r > INT_MAX || r < INT_MIN) {
return false;
}
*out = (int)r;
return true;
}
int main(void) {
int sum;
if (!safe_add_int(INT_MAX, 1, &sum)) {
printf("add INT_MAX+1: overflow detected (good)\n");
}
if (safe_add_int(100, 23, &sum)) {
printf("100+23 = %d\n", sum);
}
uint32_t u;
if (!safe_add_u32(UINT32_MAX, 1u, &u)) {
printf("u32 MAX+1: overflow detected\n");
}
int prod;
if (!safe_mul_int(100000, 100000, &prod)) {
printf("100000*100000 does not fit in int\n");
}
/* Compiler helpers (GCC/Clang) — practical on Linux */
int r;
if (__builtin_add_overflow(INT_MAX, 1, &r)) {
printf("__builtin_add_overflow caught INT_MAX+1\n");
}
if (!__builtin_add_overflow(20, 22, &r)) {
printf("__builtin_add_overflow 20+22 = %d\n", r);
}
return 0;
}gcc -std=c17 -Wall -Wextra -o overflow_safe_patterns overflow_safe_patterns.c
./overflow_safe_patternsSafer Patterns Summary
| Pattern | Notes |
|---|---|
| Check before op | if (a > INT_MAX - b) style for add |
| Wider intermediate | int64_t for 32-bit int multiply/add |
| Unsigned modular math | Defined wrap; still check if wrap is an error |
__builtin_*_overflow |
GCC/Clang; clean and efficient |
| Sanitizers | gcc -fsanitize=undefined catches overflow at runtime in tests |
gcc -std=c17 -Wall -Wextra -fsanitize=undefined -o ov_san ov.c
./ov_sanType Specifiers and Qualifiers
| Specifier / qualifier | Role |
|---|---|
signed / unsigned |
Signedness |
short / long / long long |
Rank / width hints |
const |
Not assignable through this lvalue |
volatile |
May change outside program’s knowledge |
restrict |
Pointer aliasing promise (C99) |
#include <stdio.h>
int main(void) {
const int max_size = 100;
long long big = 123456789012345LL;
unsigned short port = 8080;
printf("max_size=%d big=%lld port=%hu\n", max_size, big, port);
return 0;
}Type Promotion (Preview)
char and short promote to int (or unsigned int) in expressions. Mixed int/float/double follow usual arithmetic conversions. Details belong in the type-conversion chapter; for now, know that sizeof(char + char) is often sizeof(int).
#include <stdio.h>
int main(void) {
char a = 1, b = 2;
printf("sizeof(char)=%zu sizeof(a+b)=%zu\n", sizeof(char), sizeof(a + b));
return 0;
}gcc -std=c17 -Wall -Wextra -o promote promote.c
./promotePractical Combined Explorer
/* type_explorer.c */
#include <stdio.h>
#include <limits.h>
#include <float.h>
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
int main(void) {
printf("=== Memory Requirements ===\n");
printf("char %zu | int %zu | long %zu | float %zu | double %zu | void* %zu\n",
sizeof(char), sizeof(int), sizeof(long),
sizeof(float), sizeof(double), sizeof(void *));
int n = 1000;
printf("\nArray of %d ints: %zu bytes\n", n, (size_t)n * sizeof(int));
printf("Array of %d int64_t: %zu bytes\n", n, (size_t)n * sizeof(int64_t));
printf("\n=== Critical limits ===\n");
printf("INT_MAX=%d UINT_MAX=%u\n", INT_MAX, UINT_MAX);
printf("LONG_MAX=%ld\n", LONG_MAX);
printf("SIZE_MAX=%zu\n", (size_t)SIZE_MAX);
printf("DBL_DIG=%d DBL_EPSILON=%e\n", DBL_DIG, DBL_EPSILON);
printf("\n=== Fixed width sanity ===\n");
printf("sizeof(int32_t)=%zu sizeof(int64_t)=%zu\n",
sizeof(int32_t), sizeof(int64_t));
return 0;
}gcc -std=c17 -Wall -Wextra -o type_explorer type_explorer.c
./type_explorerBest Practices
- Measure with
sizeof; don’t hard-code type sizes.
- Use
stdint.hfor on-disk, on-wire, and register-facing fields.
- Print fixed-width values with
inttypes.hmacros (PRId32, …).
- Treat signed overflow as a bug, not as wraparound.
- Prefer
size_tfor sizes and indices into objects.
- Compare floats with tolerances, not
==, unless you need exact bit identity.
- Enable
-Wall -Wextra; add-fsanitize=undefinedin debug builds.
Exercises
Exercise 1 — ABI report
Extend sizeof_tour.c to also print CHAR_BIT, whether plain char is signed ((char)-1 < 0), and sizeof(struct { char c; int i; }) vs sum of members (padding).
gcc -std=c17 -Wall -Wextra -o abi_report abi_report.c
./abi_reportExercise 2 — limits-driven clamp
Write int clamp_to_int(long long x) that returns INT_MIN/INT_MAX if out of range, else (int)x. Test with values from limits_demo output.
Exercise 3 — safe multiply
Implement bool safe_mul_i64(int64_t a, int64_t b, int64_t *out) without invoking UB. Compare against __builtin_mul_overflow.
Exercise 4 — protocol fields
Define a message with uint8_t type, uint16_t id, uint32_t payload_len using fixed-width types. Pack into a byte buffer in little-endian explicitly (shifts and masks), then unpack and verify round-trip.
Exercise 5 — float.h scavenger
Print FLT_ROUNDS and explain what each value means (look up in the standard or man pages). Measure how many times you can add FLT_EPSILON to 1.0f before the sum changes (loop with a counter).
Exercise 6 — UB sanitizer lab
Intentionally evaluate int x = INT_MAX; x = x + 1; in a tiny program under:
gcc -std=c17 -Wall -Wextra -fsanitize=undefined -O0 -o ub ub.c
./ubThen rewrite using a safe helper and confirm the sanitizer is silent.
Summary
- Integer and floating types — families, typical Linux sizes
sizeoftour — measure your ABI
limits.h/float.h— portable ranges and FP characteristics
char/bool— integer nature and boolean conversion
stdint.h+inttypes.h— fixed-width types and portable I/O
- Signed overflow UB — detection patterns and builtins
- Qualifiers —
const,volatile,restrict(preview)
- Exercises — ABI report, clamp, safe mul, packing, sanitizers
Next: variables, constants, and storage duration—how these types appear in real programs.