Fundamentals 64 min read

C Pointers Deep Dive: CPU, Compiler, Memory & Common Pitfalls

This article explains C pointers through four layers—C language semantics, compiler transformations, CPU execution, and memory mapping—covering pointer types, arithmetic, arrays vs pointers, dynamic allocation, embedded hardware access, and common errors like null and dangling pointers with assembly examples.

Deepin Linux
Deepin Linux
Deepin Linux
C Pointers Deep Dive: CPU, Compiler, Memory & Common Pitfalls

1. Prerequisites: How the CPU Accesses Data

1.1 CPU Does Not Execute C Code Directly

The CPU cannot run C code directly; it must be compiled into machine instructions. For example, a simple C program adding two integers is translated into assembly that varies by architecture (x86, ARM), compiler, and optimization level. C describes program semantics; assembly describes how a specific machine implements those semantics.

1.2 Two Key CPU Storage Locations

Data resides primarily in registers (fast, few) or memory (large, slower). Stack, global data, dynamic allocations, memory-mapped files, and device registers are all addressable memory. Caches (L1/L2/L3) affect speed but not the basic addressable-memory model.

1.3 How the CPU Accesses Memory

The CPU computes an address, places it on the address bus, and performs a load or store via the data bus. Reading variable a means calculating its address, sending it to memory, and loading the returned value into a register.

1.4 Where Can an Ordinary Variable Live?

A local variable may be placed on the stack, kept entirely in registers, or optimized away. If its address is taken (e.g., int *p = &x;), the compiler typically gives it a computable address, though aggressive optimizations may still avoid a stack slot. Global/static variables go into sections like .data or .bss. Dynamically allocated objects come from the heap (or heap-like regions), though the C standard does not mandate the name "heap".

1.5 Variable, Object, Value, and Address

A variable is an identifier denoting a storage location. An object is the actual data in memory (e.g., an int occupying 4 bytes). The value is the stored data (e.g., 10). The address is the object's memory location, obtained with &a; this address is a pointer value pointing to a. A code example demonstrates these relationships.

2. What Exactly Is a C Pointer?

2.1 Pointer, Pointer Variable, and Pointer Value Are Distinct

int *

is a pointer type (semantics: points to an int object). p is a pointer variable (an object of type int *). &a produces a pointer value (the address of a). Analogy: int * defines what kind of arrow fits in the box; p is the box; &a is the arrow placed inside.

2.2 What Does p in int *p Hold?

int *p = &a;

declares a pointer variable p of type int * and stores the address of a in it. *p then accesses the value of a (10).

2.3 Difference Between p , &p , and *p

p

: Pointer value stored in p; typical result: address of a. &p: Address of the pointer variable p itself; typical result: points to the p object. *p: Object pointed to by p; typical result: represents the int object a. p and &p differ numerically. *p yields an lvalue or rvalue referring to the target object, enabling both read ( int v = *p;) and write ( *p = 20;).

2.4 Core Capability: Indirect Access

A pointer lets the program decide at runtime which object to operate on. A function void set_value(int *p, int v) { *p = v; } can modify x or y depending on the passed address.

2.5 Why Do Pointers Need Types?

Type determines access width ( int * dereferences 4 bytes, char * 1 byte), pointer arithmetic step size ( p+1 advances by sizeof(int)), and alignment requirements. void * is a generic pointer but cannot be dereferenced or used in arithmetic without a cast.

2.6 The CPU Does Not Know "int Pointer"

Type information exists only in C and the compiler. The compiler emits instructions like mov eax, DWORD PTR [rdi] (read 4 bytes from address in rdi). The CPU sees only bits, addresses, and load/store operations. Types guide code generation and compile-time checks.

3. Where Does a Pointer Variable Actually Reside?

3.1 Local Pointer Variables

Typically placed on the stack. Example assembly ( -O0) shows a at [rbp-4], p at [rbp-16], with lea rax, [rbp-4] computing &a and mov [rbp-16], rax storing it. This is a common implementation, not a language guarantee.

3.2 At Runtime: Pointer Values Often Live in Registers

For short-lived pointers, the compiler keeps the value in a register (e.g., rdi for the first argument on x86-64), avoiding memory traffic. Debuggers may show such variables as "unavailable" or with unstable values because the source-level variable has been optimized into a register.

3.3 Global and Static Pointers

Objects with static storage duration (global int *global_ptr, static int *static_ptr) typically reside in .data (initialized) or .bss (zero-initialized). Section names and layout are toolchain-specific. Global pointer values may require linker relocation.

3.4 With Optimization: The Pointer Variable May Vanish

Under -O2, int a=10; int *p=&a; *p=20; return a; can become mov eax, 20; ret. Neither a nor p occupies storage; the whole sequence is constant-folded. volatile forces observable accesses but does not disable all optimizations.

3.5 Fixed Addresses Are a Teaching Model

Variable addresses are not fixed: stack addresses change per call, malloc returns different addresses. Crucially, the pointer variable's location and the address it holds are two separate questions.

4. Step-by-Step: int a=10; int *p=&a; to Machine Execution

4.1 int a = 10

Creates an int object a with value 10. Unoptimized: mov DWORD PTR [rbp-4], 10. Optimized: may stay in a register with no memory write.

4.2 int *p = &a — The &a Part

&a

produces a pointer value (address of a) without reading a 's content. If a is at [rbp-4], lea rax, [rbp-4] computes the address (no memory load). Then mov [rbp-16], rax stores it into p.

4.3 Expression p Reads the Pointer Value

When p appears in a value context, the compiler emits a load from its storage (e.g., mov rax, [rbp-16]) or uses the register already holding it.

4.4 *p Produces the Target Object

*p

means "form an expression designating the object pointed to by p ". As an rvalue it reads ( int v = *p;); as an lvalue it denotes the write location ( *p = 20;).

4.5 *p = 20 Performs an Indirect Write

Typical sequence: load pointer value into register, then store through it: mov rax, [rbp-16]; mov DWORD PTR [rax], 20. The key is "get address, then write via that address" — indirect addressing.

4.6 & Is Not Simply Reading a Memory Number

Addresses are computed by the compiler from layout, stack frames, linker results, or allocator returns — not fetched from memory as data.

4.7 * Is Not a Simple Value-Fetch Operator

int x = *p;

reads; *p = 20; writes. int *q = &*p; can simplify to q = p because &*p forms the target object then takes its address back.

4.8 Observing Address Calculation and Indirect Access via Assembly

Use gcc -O0 -S -masm=intel demo.c for stepwise code; -O2 shows optimizer's transformations (may reduce to a single ret).

4.9 After Optimization Steps May Look Completely Different

Constant propagation, dead-code elimination, register allocation, etc., mean the generated machine code need not mirror source line-by-line. C requires observable behavior equivalence, not instruction-for-statement correspondence.

5. Why Does C Need Pointers?

5.1 Indirect Access Lets Runtime Decide the Target Object

Analogy: ask a librarian (pointer) for a book instead of finding it yourself. Function update(int *target, int val) operates on whichever object the caller passes.

5.2 Function Parameters: Accessing Caller's Data via Pointer

C passes by value; a pointer parameter receives a copy of the address. Modifying the pointer copy ( p = NULL;) doesn't affect the caller's pointer. Modifying the pointed-to object ( *p = 100;) does. To change the caller's pointer, pass a pointer to pointer ( int **pp).

5.3 Avoiding Large Object Copies

Passing a large struct by value copies kilobytes; passing a pointer copies 4/8 bytes. Example: void process(const Packet *p) vs void process(Packet p). However, small objects may be faster passed by value (registers). Benchmark to decide.

5.4 Dynamic Storage Must Be Accessed Through Pointers

malloc

returns a pointer to anonymous storage with no variable name. The program can only reach it via that pointer value. Example shows allocation, use, and free.

5.5 Linked Lists, Trees, and Graphs Rely on Pointers

Nodes contain pointers to other nodes ( struct Node { int value; struct Node *next; }). The structure is defined by pointer relationships, not by where nodes are allocated (heap, static array, pool).

5.6 Function Pointers Enable Indirect Calls

int (*op)(int,int) = add;

stores a callable entry address. The compiler emits an indirect call. Function pointers and object pointers are distinct types; sizes may differ.

5.7 Embedded Systems Use Pointers for Hardware Access

Memory-mapped I/O: #define UART_DR (*(volatile unsigned int *)0x40000000u). Casting an integer address to a pointer and dereferencing accesses device registers. Embedded developers must understand pointers beyond "address of a variable".

6. Most Common Pointer Mistakes

6.1 Null Pointer Is Not Simply Physical Address Zero

NULL

is a macro (often 0 or (void*)0) meaning "points to no valid object". C does not require all-zero representation or mapping to physical address 0. memset(&p, 0, sizeof p) is not guaranteed to produce a null pointer.

6.2 Dereferencing Null Pointer: Undefined Behavior

int *p = NULL; int x = *p;

is UB. OS may map low pages as inaccessible to catch it early, but that's OS protection, not C semantics.

6.3 Wild Pointers: Sometimes Crash, Sometimes "Seem Fine"

Uninitialized pointer ( int *p; *p = 10;) holds an indeterminate value. It may accidentally point to writable memory and appear to work, but later stack/layout changes cause crashes. "Runs for weeks then fails" is typical.

6.4 Dangling Pointers: After free , Pointer Still Holds Old Address

free(p)

ends the allocation's lifetime; p retains its bit pattern but becomes invalid. Use-after-free ( *p = 20; or printf("%d", *p)) is UB. Best practice: free(p); p = NULL;.

6.5 Out-of-Bounds Access: Computed Address ≠ Permission to Access

int a[3]; int *p = a + 3;

(one-past) is valid for comparison but not dereference. a + 4 exceeds allowed pointer arithmetic. Even if CPU computes a number, C forbids its use.

6.6 Pointer Type Mismatches Affect Width, Alignment, and Interpretation

int a = 0x12345678; char *p = (char*)&a; *p

reads only 1 byte (0x78 on little-endian), not the full int. Wrong type causes wrong access size, potential alignment faults, and misinterpretation.

6.7 Multi-Level Pointers: int **pp Adds a Layer of Indirection

int a=10; int *p=&a; int **pp=&p;

. **pp = 20 requires two loads: first p from pp, then a from p. It's not "larger address" but more indirection levels.

6.8 What const Actually Restricts

const int *p

: cannot modify *p via p; p can change. int *const p: p cannot change; *p can. const int *const p: both fixed. Read left-to-right from variable name: const binds to left token (or right if none).

7. Pointer Arithmetic: Why p + 1 Doesn't Just Add 1

7.1 Pointers Are Not Ordinary Integers

p + 1

means "advance by one element of the pointed-to type", not "add 1 to the numeric address".

7.2 int *p + 1 Typically Steps Over One int

If sizeof(int)==4 and p==0x1000, p+1==0x1004. But C does not fix int at 4 bytes; it's implementation-defined.

7.3 char *p + 1 Steps One Byte

sizeof(char)==1

by definition. char * often used for byte-wise object inspection: unsigned char *bytes = (unsigned char*)&value;.

7.4 Scaling Is Done by the Compiler

For int *q = p + i;, compiler emits lea rax, [rdi + rsi*4] (scale factor from sizeof(int)). CPU only executes the address arithmetic; type-based scaling is compile-time.

7.5 When Can Two Pointers Be Subtracted?

Only when both point into the same array (or one-past). Result is element count difference ( ptrdiff_t), not byte difference. Unrelated pointers cannot be meaningfully subtracted.

7.6 Pointer Arithmetic Is Constrained to the Object's Bounds

Standard loops use one-past pointer as sentinel: while (p != end) { ...; ++p; }. Staying within the array ensures defined behavior.

7.7 Casting to Integer for Arithmetic Is Risky

Use uintptr_t if integer representation is needed. Integer arithmetic loses type information, may break alignment, produce invalid addresses, and defeat compiler alias analysis. Conversion back to pointer is implementation-defined.

8. Arrays and Pointers: The Classic C Confusion

8.1 Arrays Are Not Pointers

int a[10]

is a contiguous block of 10 int s (40 bytes on 32-bit). int *p is a separate pointer object (4 bytes) holding an address. Different type, size, lifetime.

8.2 Why Array Names Often Decay to First-Element Pointers

In most expressions, a converts to &a[0] (type int *). Exceptions: sizeof(a) yields array size; &a yields pointer to whole array ( int (*)[10]).

8.3 a , &a[0] , and &a Have Different Types

All may share the same numeric address, but a and &a[0] are int *; &a is int (*)[10]. Pointer arithmetic immediately reveals the difference.

8.4 Why sizeof(a) Isn't Pointer Size

sizeof(a)

gives total array bytes (e.g., 40). As a function parameter, int a[10] adjusts to int *a, so sizeof(a) inside the function returns pointer size. Pass length separately.

8.5 a + 1 vs &a + 1 Span Different Distances

a + 1

advances by sizeof(int); &a + 1 advances by 10*sizeof(int). Compiler applies different scaling based on pointer type.

8.6 Why p[i] Equals *(p + i)

By definition, p[i] is *(p + i). Even i[p] works (same as *(i + p)), but don't write it for readability.

8.7 Two-Dimensional Arrays Are Not Double Pointers

int matrix[2][3]

is a contiguous block of 6 int s. In expressions it decays to int (*)[3] (pointer to row). Casting to int ** is wrong — there is no array of row pointers in memory.

9. Functions and Dynamic Memory: Pointers in Real Projects

9.1 What Pointer Parameter Passing Actually Passes

Still pass-by-value: the address value is copied. void set_null(int *p) { p = NULL; } only changes the local copy. To modify caller's pointer, use int **out.

9.2 Why Pointers Can Modify Caller's Objects

Function receives a copy of the address, but both pointers designate the same object. *p = 123 modifies that shared object.

9.3 Passing Large Structs: Pointers Reduce Copying

void parse_frame(const Frame *f)

avoids copying an 8KB struct. const documents read-only intent. But pointer indirection may hurt cache locality; benchmark for small structs.

9.4 What malloc Returns

A pointer to suitably aligned dynamic storage of requested size, or NULL on failure. No cast needed in C: int *p = malloc(10 * sizeof *p);. Contents are uninitialized.

9.5 Dynamic Storage ≠ Standard "Heap"

C specifies dynamic storage duration and allocator behavior; implementation may use traditional heap, pools, arenas, mmap, etc. Say "dynamic storage", not "heap".

9.6 free Releases Memory, Not the Pointer Variable

free(p)

ends the allocation's lifetime; the pointer variable p (if local) still exists in scope. Debugger may show stale address. Always free(p); p = NULL;.

9.7 Leaks, Double-Free, Use-After-Free

Leak: storage alive but no pointer reaches it.

Double-free: calling free twice on same allocation.

Use-after-free: accessing storage after free. free cannot nullify caller's pointer (receives copy). Programmer must do it.

10. Pointers in Embedded Development: Why Low-Level Understanding Matters

10.1 Fixed-Address Access Often Involves Integer-to-Pointer Casts

#define GPIO_ODR (*(volatile unsigned int *)0x48000014u)

. Cast integer to pointer, dereference. Depends on MCU, compiler, linker script — not portable to desktop C.

10.2 MMIO: Accessing Peripheral Registers via Pointers

Memory-mapped I/O places device registers in CPU address space. UART_STATUS, UART_DATA macros. CPU sees normal load/store; hardware responds instead of RAM.

10.3 What volatile Pointers Solve

volatile unsigned int *status_reg

tells compiler the target may change externally (hardware, interrupt). Prevents read/write merging or deletion. Does not guarantee atomicity, thread sync, or memory barriers. volatile int *p vs int *volatile p restrict different entities.

10.4 DMA Uses Addresses and Pointers Extensively

DMA controller needs source, destination, length. Addresses may be CPU virtual, bus, physical, or device addresses. Cache maintenance (clean/invalidate) required on cached systems. DMA "pointers" may span multiple address spaces.

10.5 Pointers, RAM, Flash, and Peripheral Address Spaces

Embedded memory map: Flash (code/constants), RAM (variables/stack), peripheral space (MMIO). Pointers traverse all three.

10.6 Address 0x00000000 May Be Valid on MCUs

Some MCUs map Flash or vector table at 0. Hardware allows reads, but C null-pointer semantics remain. int *p = 0; *p is still UB. Use vendor-defined macros for hardware zero-address access.

10.7 Bare-Metal vs OS Environments Differ

With MMU: invalid access → page fault → SIGSEGV. Bare-metal: invalid access → BusFault/HardFault, silent corruption, or delayed failure. Debugger, map files, and datasheets are essential.

11. Summary

Understand pointers in four layers:

C Language : Pointer types, values, objects, lifetimes, valid access.

Compiler : Variable allocation, type-driven instruction selection, optimizations.

CPU : Registers, address calculation, load/store, indirect addressing.

Memory : Address mapping, permissions, hardware response.

At C level, a pointer is a typed value with referential semantics. Compiler translates pointer expressions into address arithmetic, register ops, and memory instructions. CPU sees only bit patterns and final addresses — it knows nothing of int *. Memory layer requires the address to map to an existing, type-compatible, accessible object or region.

Thus int *p raises two distinct questions: "Where does the pointer variable p live?" (stack, register, static, or optimized away) and "Where does the pointer value in p point?" (stack a, dynamic storage, global, Flash, device register).

To internalize this chain, write small programs and inspect:

gcc -O0 -S -masm=intel demo.c
gcc -O2 -S -masm=intel demo.c
objdump -d demo.o

Use a debugger to watch registers, stack frames, and memory. The chain from code to machine becomes clear:

Source objects
  ↓
Compiler understands types & lifetimes
  ↓
Generates address calc & memory instructions
  ↓
CPU holds addresses in registers
  ↓
Memory system returns or writes data

Once this flow clicks, pointers cease to be "memorized but unusable" syntax. They become a straightforward capability letting programs locate, link, and modify objects at runtime — and giving us direct dialogue with CPU, memory, and hardware. Pointers still cause bugs, but now you can explain exactly why: object missing, lifetime ended, type mismatch, or CPU lacks permission. That's far better than staring at a HardFault and calling it "mystical".

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory Managementcompiler optimizationdynamic allocationCPU architectureembedded systemscommon pitfallspointer arithmeticC pointers
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.