CFoundations

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

TT
TopicTrick Team
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

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.

c

What actually happens here:

  1. The compiler reserves 4 bytes on the stack (on a typical 64-bit system).
  2. The binary representation of 25 (0x00000019) is written to those 4 bytes.
  3. The name age is a compile-time alias for the address of that memory region.
mermaid

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:

TypeTypical SizeRange
char1 byte-128 to 127
unsigned char1 byte0 to 255
short2 bytes-32,768 to 32,767
int4 bytes~±2.1 billion
long4 or 8 bytes*Platform-dependent
long long8 bytes~±9.2 × 10¹⁸
float4 bytesIEEE 754 single precision
double8 bytesIEEE 754 double precision

[!WARNING] Platform Dependency is Real. On a 32-bit Linux, long is 4 bytes. On a 64-bit Linux, long is 8 bytes. On Windows 64-bit, long is still 4 bytes (LLP64 vs LP64 data model). Code that assumes long is a specific size will silently break when ported. This is why stdint.h exists.

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:

c

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.

c

[!IMPORTANT] C23's auto is not the same as var in JavaScript. The type is resolved at compile time and is permanent. Once the compiler deduces score as int, it is an int forever. 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).

c

A critical pattern — safe array sizing:

c

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 ClassScopeLifetimeMemory Location
auto (default)BlockUntil block exitsStack
static (local)BlockEntire programBSS/Data
static (global)FileEntire programBSS/Data
register (hint)BlockUntil block exitsRegister (maybe)
externFile/globalEntire programDefined elsewhere
c

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:

text

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.

c

[!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:

c

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.