Understanding the Address-of Operator in C

The address-of operator (&) is a fundamental unary operator in the C programming language designed to retrieve the memory location of an object. This article explores the primary purposes of the address-of operator—including pointer initialization, simulated pass-by-reference semantics, and data structure manipulation—alongside its low-level internal behavior, compiler constraints, and assembly-level implementation.

Purpose of the Address-of Operator

In C, variables represent named storage locations in system memory. The primary purpose of the address-of operator (&) is to evaluate to the memory address of its operand rather than the value stored within that operand.

Key applications include:

  • Pointer Initialization: Pointers store memory addresses. The & operator provides the exact address needed to initialize or assign a pointer variable (e.g., int *ptr = &var;).
  • Pass-by-Reference Emulation: C is strictly a pass-by-value language. Functions cannot directly modify caller arguments unless the caller explicitly passes the memory address of the variable using &, allowing the callee to dereference the pointer and alter the original value.
  • Direct Hardware and Buffer Access: Low-level operations, such as memory-mapped I/O, serialization, and interaction with APIs like scanf or fread, require direct access to the memory buffer where data should be written or read.

Syntactic Rules and Operand Constraints

The operand of the & operator must be an lvalue—an expression that designates an object residing in memory.

You can apply the operator to:

  • Named scalar, array, or structure variables (e.g., &x, &my_struct).
  • Array elements (e.g., &arr[0]).
  • Structure and union member selections (e.g., &node.next).
  • Dereferenced pointers (e.g., &*ptr, which cancels out to the value of ptr).

The operator cannot be applied to:

  • Rvalues: Expressions without a permanent memory location, such as literals (&42) or temporary computation results (&(a + b)).
  • Bit-fields: Individual bits in a struct cannot be addressed directly because the smallest addressable unit of memory in C is a byte.
  • Register Variables: Variables declared with the register storage class hint to the compiler that the value should reside entirely within a CPU register, which does not have a conventional memory address.

Internal Behavior and Compilation Mechanics

At the architectural level, the address-of operator does not produce runtime code that "fetches" an address; rather, it instructs the compiler how to resolve memory references during code generation.

For variables with static or global storage duration, the address is deterministic relative to the binary's memory layout. The compiler emits a symbol reference. During the linking phase, the linker resolves this symbol to a concrete virtual memory address in the data or BSS segment. In position-independent executables (PIE), this translates to an instruction pointer-relative offset resolved at runtime.

2. Stack Frame Offsets

For local (automatic) variables, the variable lives on the execution stack. The compiler tracks each local variable as a fixed byte offset from the stack base pointer (such as RBP in x86-64 architectures). When & is used on a local variable, the compiler generates instructions to calculate the effective address by adding or subtracting that offset from the current frame pointer.

3. Machine Code Generation

In modern assembly architectures, the address-of operation typically translates into an instruction that calculates an address without accessing memory. In x86-64 assembly, this is executed via the lea (Load Effective Address) instruction:

lea rax, [rbp - 4]   ; Computes the address of a local variable and stores it in RAX

Unlike instructions such as mov, which load the value stored at the computed address, lea directly loads the calculated address itself, making the operation computationally cheap.

4. Impact on Compiler Optimizations

Using the address-of operator on a variable forces the compiler to guarantee that the variable has a physical address in memory. Normally, optimizing compilers prefer to keep frequently used local variables entirely within CPU registers to maximize performance.

Once an address is taken using &, the variable is marked as "address-taken." This forces the compiler to allocate space for it on the stack frame (spilling it to memory) because CPU registers cannot be referenced via pointers. Furthermore, the compiler must assume pointer aliasing may occur, restricting certain aggressive reordering and caching optimizations for that variable.