The C Path — learn C, visually

📚 The Standard Library

limits.h, float.h & stdint.h: know your sizes

⏱ 14 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

A file your program saves on Linux can come back garbled when read on Windows — the two systems literally disagree about how many bytes some numbers take. Today you get number types that are exactly the same size on every machine, which is how image files, save games, and internet messages stay readable on every device on Earth.

"How big is an int?" The honest C answer: it depends. The standard only guarantees minimums — int ≥ 16 bits, long ≥ 32, long long ≥ 64. Real machines disagree: on 64-bit Linux long is 64 bits, on 64-bit Windows it's 32! Today's headers turn that chaos into certainty.

limits.h: the actual numbers on this machine

mylimits.c
#include <stdio.h>
#include <limits.h>
#include <float.h>

int main(void) {
    printf("CHAR_BIT   = %d bits per byte\n", CHAR_BIT);
    printf("char       : %d .. %d\n", CHAR_MIN, CHAR_MAX);
    printf("short      : %d .. %d\n", SHRT_MIN, SHRT_MAX);
    printf("int        : %d .. %d\n", INT_MIN, INT_MAX);
    printf("long       : %ld .. %ld\n", LONG_MIN, LONG_MAX);
    printf("uint max   : %u\n", UINT_MAX);
    printf("int bits   = %zu\n", sizeof(int) * CHAR_BIT);
    printf("DBL_EPSILON= %g, DBL_DIG=%d\n", DBL_EPSILON, DBL_DIG);
    return 0;
}
terminal
$ gcc mylimits.c -o mylimits && ./mylimits
CHAR_BIT   = 8 bits per byte
char       : -128 .. 127
short      : -32768 .. 32767
int        : -2147483648 .. 2147483647
long       : -9223372036854775808 .. 9223372036854775807
uint max   : 4294967295
int bits   = 32
DBL_EPSILON= 2.22045e-16, DBL_DIG=15
# note: char is signed here — that's implementation-defined!

CHAR_BIT is the number of bits in a byte — 8 everywhere you'll likely ever work, but the standard permits more (some DSPs use 16 or 32). The pattern sizeof(type) * CHAR_BIT gives a type's bit width portably.

🧠 Checkpoint: What does the C standard guarantee about int?

  • Exactly 32 bits
  • At least 16 bits
  • The same size as long
  • At least 32 bits
Show answer

At least 16 bits — Only a minimum: int must span at least −32767..32767 (16 bits). It happens to be 32 bits on modern desktops, but 16-bit ints are alive and well on microcontrollers — which is exactly why stdint.h exists.

float.h: precision limits

macrotypical valuemeaning
FLT_MAX / DBL_MAX3.4×10³⁸ / 1.8×10³⁰⁸largest finite float / double
FLT_EPSILON1.19×10⁻⁷smallest x where 1.0f + x ≠ 1.0f
DBL_EPSILON2.22×10⁻¹⁶same for double — the yardstick for relative comparisons
FLT_DIG / DBL_DIG6 / 15decimal digits that survive a round trip

These are the numbers behind Part 0's "float ≈ 7 digits, double ≈ 15–16" — now you know where to look them up.

The fix: stdint.h fixed-width types

C99 ended the guessing game. When the exact width matters — file formats, network packets, embedded registers, overflow-sensitive math — say what you mean:

typemeaning
int8_t … int64_t, uint8_t … uint64_texactly N bits, two's complement (mandatory since C23!) — optional only on exotic hardware
int_least8_tsmallest type with at least N bits — always exists
int_fast8_tfastest type with at least N bits (often plain int under the hood)
intptr_t / uintptr_tinteger wide enough to round-trip a pointer — the only sanctioned way to store an address as an integer
intmax_t / INT32_MAX …the widest integer type, plus a MAX/MIN macro for every type above

Printing them: inttypes.h

Here's the trap: what printf specifier matches int64_t? On 64-bit Linux it's long (%ld), on Windows it's long long (%lld) — hardcode either and the other platform breaks. inttypes.h provides format macros that expand to the right letters via string-literal concatenation:

fixed.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void) {
    int32_t  file_offset = 123456;        /* exact 32 bits    */
    uint8_t  flags       = 0xC3;          /* exact byte       */
    int64_t  big         = INT64_MAX;
    int_fast16_t counter = 0;             /* whatever's quick */

    /* PRId64 expands to "ld" or "lld" as needed —
       string concatenation glues it into the format: */
    printf("big    = %" PRId64 "\n", big);
    printf("offset = %" PRId32 ", flags = 0x%" PRIX8 "\n",
           file_offset, flags);

    uintptr_t addr = (uintptr_t)&counter; /* pointer <-> int  */
    printf("counter lives at 0x%" PRIxPTR "\n", addr);

    printf("sizeof int_fast16_t = %zu\n", sizeof counter);
    return 0;
}
terminal
$ gcc fixed.c -o fixed && ./fixed
big    = 9223372036854775807
offset = 123456, flags = 0xC3
counter lives at 0x7ffc9a2b4c2c
sizeof int_fast16_t = 8
# fast16 chose a full 64-bit register on this machine!

🧠 Checkpoint: Why is printf("%ld", my_int64) unportable for an int64_t?

  • %ld only prints 32 bits
  • int64_t may be long on one platform and long long on another — the specifier must match the underlying type
  • printf can’t print 64-bit values
  • It’s fine — int64_t is always long
Show answer

int64_t may be long on one platform and long long on another — the specifier must match the underlying type — int64_t is a typedef for whichever native type is 64-bit: long on Linux/macOS, long long on Windows. A mismatched specifier is UB. PRId64 from inttypes.h expands to the correct letters on each platform.

stddef.h & stdbool.h: the little glue types

offsetof makes struct padding (Part 2) visible. Recall why: an int wants a 4-aligned address, so the compiler inserts gap bytes:

offsets.c
#include <stdio.h>
#include <stddef.h>

struct packet {
    char     tag;      /* 1 byte              */
    int      value;    /* 4 bytes, wants 4-alignment */
    char     flag;     /* 1 byte              */
};

int main(void) {
    printf("offset of tag   = %zu\n", offsetof(struct packet, tag));
    printf("offset of value = %zu\n", offsetof(struct packet, value));
    printf("offset of flag  = %zu\n", offsetof(struct packet, flag));
    printf("sizeof struct   = %zu\n", sizeof(struct packet));
    return 0;
}
terminal
$ gcc offsets.c -o offsets && ./offsets
offset of tag   = 0
offset of value = 4
offset of flag  = 8
sizeof struct   = 12
# 6 bytes of data, 12 bytes of struct: 6 bytes of padding!

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

🧠 Checkpoint: offsetof(struct packet, value) above returned 4, not 1. Why?

  • offsetof counts from 1
  • int members always come first in memory
  • The compiler inserted 3 padding bytes so the int starts at a 4-aligned offset
  • sizeof(char) is 4 here
Show answer

The compiler inserted 3 padding bytes so the int starts at a 4-aligned offset — Alignment (Part 5!): a 4-byte int is placed at offsets divisible by 4 for efficient access. tag occupies offset 0, offsets 1–3 are padding, value starts at 4. offsetof is how you verify a layout matches a file format or wire protocol.

So which type do I use?

This spot has an interactive flow widget — open the interactive lesson to play with it.

💡

Default to int for small numbers, size_t for sizes/indexes, int64_t when values can get big, exact-width types at any binary boundary (files, network, hardware). Reach for unsigned types for bit manipulation — not just because a value "can't be negative" (unsigned underflow bugs in loop conditions are legion).

This spot has an interactive editor widget — open the interactive lesson to play with it.

Types measured and pinned down — now for the standard library's wildest corner: signals that interrupt your program, and jumps that teleport across functions.

▶ Practice this lesson interactively (with live gcc)