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
scanforfread, 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 ofptr).
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
registerstorage 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.
1. Compile-Time and Link-Time Resolution
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.