C Variables, Types & Memory Layout: A Complete Guide (C23)

C Variables, Types & Memory Layout: A Complete Guide (C23)
Table of Contents
- Variables as Memory Regions
- C's Fundamental Types and Their Sizes
- Fixed-Width Types with stdint.h
- C23: Automatic Type Inference with auto
- The sizeof Operator
- Variable Scope and Storage Duration
- Endianness: How Multi-Byte Values are Stored
- Integer Overflow: The Silent Killer
- Constants: const vs #define vs constexpr
- Frequently Asked Questions
- Key Takeaway
Variables as Memory Regions
When you declare a variable in C, the compiler allocates a specific block of bytes in memory and associates a name with it. This is fundamentally different from dynamic languages.
What actually happens here:
- The compiler reserves 4 bytes on the stack (on a typical 64-bit system).
- The binary representation of
25(0x00000019) is written to those 4 bytes. - The name
ageis a compile-time alias for the address of that memory region.
After compilation, the name age literally disappears; the generated machine code refers only to the memory address. This is why C compilers can generate code so efficiently — by the time the binary runs, there's no symbol table to look up, just raw memory addresses.
C's Fundamental Types and Their Sizes
C provides several built-in types, each with platform-dependent sizes. This is both a source of flexibility and a common source of bugs:
| Type | Typical Size | Range |
|---|---|---|
char | 1 byte | -128 to 127 |
unsigned char | 1 byte | 0 to 255 |
short | 2 bytes | -32,768 to 32,767 |
int | 4 bytes | ~±2.1 billion |
long | 4 or 8 bytes* | Platform-dependent |
long long | 8 bytes | ~±9.2 × 10¹⁸ |
float | 4 bytes | IEEE 754 single precision |
double | 8 bytes | IEEE 754 double precision |
[!WARNING] Platform Dependency is Real. On a 32-bit Linux,
longis 4 bytes. On a 64-bit Linux,longis 8 bytes. On Windows 64-bit,longis still 4 bytes (LLP64 vs LP64 data model). Code that assumeslongis a specific size will silently break when ported. This is whystdint.hexists.
Why int is 4 Bytes
Modern CPUs are optimized to work with data that matches their register width or common word boundaries. On a 32-bit architecture, int was the "natural" register size. On 64-bit systems, int remains 4 bytes by convention — partly for historical ABI compatibility and partly because 32-bit integers are still the most common use case for counters and loop indices.
Fixed-Width Types with stdint.h
The <stdint.h> header defines types with guaranteed, explicit sizes on any platform. This is what you should use for all systems-level code:
The naming convention: intN_t for signed, uintN_t for unsigned, where N is the bit count (8, 16, 32, 64).
Also useful from <stdint.h>:
intptr_t/uintptr_t: An integer guaranteed to be large enough to hold any pointer (4 bytes on 32-bit, 8 bytes on 64-bit).int_fast32_t: The fastest available type at least 32 bits wide on the current platform.int_least32_t: The smallest available type at least 32 bits wide.
C23: Automatic Type Inference with auto
One of the most significant additions to C23 is the auto keyword for type inference (similar to C++'s auto, introduced in C++11). The compiler deduces the type from the initializer expression at compile time — there is zero runtime cost and no loss of type safety.
[!IMPORTANT] C23's
autois not the same asvarin JavaScript. The type is resolved at compile time and is permanent. Once the compiler deducesscoreasint, it is anintforever. This is type inference, not dynamic typing.
Use auto when the type is obvious from context (e.g., iterator variables, literals). Use explicit types when the type is an important part of the semantics (e.g., uint32_t packet_len makes the constraint visible to the reader).
The sizeof Operator
sizeof is not a function — it is a compile-time operator that returns the size of a type or expression in bytes. The return type is size_t (an unsigned integer type, typically 8 bytes on 64-bit systems).
A critical pattern — safe array sizing:
Variable Scope and Storage Duration
C variables have two orthogonal properties: scope (where the name is visible) and storage duration (how long the memory exists):
| Storage Class | Scope | Lifetime | Memory Location |
|---|---|---|---|
auto (default) | Block | Until block exits | Stack |
static (local) | Block | Entire program | BSS/Data |
static (global) | File | Entire program | BSS/Data |
register (hint) | Block | Until block exits | Register (maybe) |
extern | File/global | Entire program | Defined elsewhere |
Understanding static is critical for writing reentrant-safe code in multi-threaded programs, which we cover in Module 12.
Endianness: How Multi-Byte Values are Stored
When a multi-byte integer (like int32_t) is stored in memory, the CPU must decide the order of bytes. This is called endianness:
- Little-Endian (x86-64, ARM by default): The least significant byte is at the lowest memory address.
- Big-Endian (network byte order, some embedded systems): The most significant byte is at the lowest address.
For the value 0x12345678:
This matters enormously when you write binary data to a file or network socket on one machine and read it on another. The <arpa/inet.h> functions htonl() and ntohl() (host-to-network long and network-to-host long) handle this conversion explicitly — we cover them in detail in Module 15 (Sockets & TCP/IP).
Integer Overflow: The Silent Killer
C integers do not automatically detect overflow. When you exceed the maximum value of a type, the value wraps around — silently and without any error.
[!CAUTION] Signed integer overflow is undefined behavior in C, meaning the compiler is free to do anything — optimize away the branch, crash the program, or silently corrupt data. Always check bounds before arithmetic on signed integers, or use the
__builtin_add_overflow()GCC/Clang intrinsic for safety-critical code.
Constants: const vs #define vs constexpr
C23 gives us three ways to define constants, each with different semantics:
Best practice in C23: Prefer constexpr for named compile-time constants and const for values that should not be modified at runtime. Use #define only for feature flags and conditional compilation.
Frequently Asked Questions
What is the difference between signed and unsigned integers? A signed integer uses one bit to represent the sign (positive or negative), giving it a range of approximately -2^(N-1) to 2^(N-1)-1. An unsigned integer uses all bits for magnitude, giving it a range of 0 to 2^N-1 — twice as large on the positive side. Arithmetic on unsigned values wraps modulo 2^N (well-defined), while signed overflow is undefined behavior.
Why does memory alignment matter for performance?
Modern CPUs read data from memory in aligned chunks matching their bus width (typically 8 bytes on x86-64). If a 4-byte int starts at address 0x003 (not divisible by 4), the CPU must perform two separate memory reads and combine the result — which is 2× slower. The compiler handles alignment automatically for individual variables, but you must manage it carefully when designing struct layouts (covered in Module 8).
Can a pointer be larger than an int on 64-bit systems?
Yes. On 64-bit systems (LP64/LLP64), a pointer is 8 bytes while int remains 4 bytes. This is why you should never cast a pointer to int — use uintptr_t from <stdint.h> to store a pointer value as an integer safely.
What does volatile do?
volatile tells the compiler that a variable's value can change at any time — from an interrupt handler, DMA controller, or memory-mapped hardware register. The compiler must not optimize away reads/writes to volatile variables or cache them in registers. It is essential for embedded systems programming and multi-threaded signal handlers.
Is _Bool the same as bool?
_Bool is the C native Boolean type (since C99). The <stdbool.h> header defines bool as a macro expanding to _Bool, and true/false as macros for 1/0. In C23, bool, true, and false are now keywords — you no longer need #include <stdbool.h>.
Key Takeaway
C variables are physical memory regions with a precise size, type, and address. By using <stdint.h> fixed-width types, you write code that behaves identically on any platform. By understanding auto, static, const, and constexpr, you control not just what a variable stores, but how long it lives and how much the compiler is allowed to optimize it.
This physical understanding of data is what separates systems programmers from scripting language users. It is the foundation upon which memory management, pointer arithmetic, and all of the advanced topics in this course are built.
Read next: Control Flow & Deterministic Logic in C →
This post is part of the C Mastery Course — 30 modules from fundamentals to production-grade systems engineering.
