Programming Foundations

Memory, Pointers, References, and Lifetimes

Understand memory safety through addresses and pointers, shared references, the stack and heap, and object lifetimes.

About 75 min · Updated 2026-10-25

Information checked as of: 2026-10-26

Learning objectives

  • Diagram runtime memory using stacks, heaps, addresses, pointers, and references.
  • Find and fix basic memory defects by reasoning about aliasing, lifetimes, deallocation, and bounds.

When two variables refer to the same data, a change made through one can be visible through the other. If a reference outlives the data it points to, it can instead lead to an invalid access. Keep three questions separate: where is the data, which references can reach it, and how long is it valid?

Addresses and pointers

Memory is read and written by specifying locations with addresses. In C, a pointer is a value that can point to an object of a particular type, among other uses. The & operator obtains an address; the * operator accesses the object through a pointer. A pointer variable itself also occupies storage, so distinguish the pointer’s value from the value of the object it points to.

int x = 10;
int *p = &x;
int *q = p;
*p = 20;
// x == 20, *q == 20

Both p and q point to x, so assigning through *p changes the value read through *q. Assigning q=p does not copy x. Multiple access paths to the same data are called aliasing. When passing a pointer to a mutating function, consider which part of the caller’s state can be affected.

Copies and shared references

Reference sharing matters in high-level languages too. In Python, a=[1,2] followed by b=a shares one list. With b=a.copy(), the outer lists are separate, but if their elements are themselves lists, references to those inner objects are still shared. Shallow and deep copies differ in how far they create separate objects through the nested structure.

a = [[1], [2]]
b = a.copy()
b[0].append(9)
print(a)             # [[1, 9], [2]]
b.append([3])
print(len(a), len(b)) # 2 3

The first change reaches a shared inner list. The second affects only b’s outer list. If “I copied it, but the original changed,” draw the outer containers and their element references separately rather than looking only at variable names. Making every copy deep can consume unnecessary time and memory.

Connect the stack and heap with lifetimes

A typical native implementation places call-related information and some local variables on a stack, and manages dynamically allocated regions on a heap. A stack fits the pattern of returning from the most recently entered call. A heap also supports data whose lifetime does not follow the nesting of calls. A variable’s name being local does not by itself guarantee its physical storage location.

For example, optimization may keep a value in a register or eliminate an allocation. Distinguish a lifetime guaranteed by the language from a physical layout chosen by its implementation. Statements such as “the heap is always slow” or “local variables always live on the stack” cannot explain every optimized program or high-level language.

An address cannot be used after the object’s lifetime

int *bad(void) {
    int x = 42;
    return &x;  // x reaches the end of its lifetime when the function returns
}

After this function returns, the caller must not dereference the returned pointer. Even if an immediate attempt appears to read 42, that observation does not establish correct behavior. The pointer is dangling because the object it referred to has reached the end of its lifetime. Similarly, accessing dynamically allocated storage after freeing it is a use-after-free.

Approaches differ between languages. With manual management, define responsibility for allocation and deallocation and make ownership clear. Garbage collection reclaims unreachable memory. Rust’s ownership and borrowing rules prevent many invalid references at compile time. None of these approaches necessarily manages resources such as open files and network connections through exactly the same mechanism.

Check your understanding

  • If p and q point to the same x, does setting q to a null pointer change x? No. The operation changes q’s pointer value, not the contents of x.

  • Why can changing an inner list after a shallow copy affect the original? The outer container was copied, but references to the inner objects are still shared.

  • Is an access safe if it reads the expected value once from an object whose lifetime has ended? No. Old contents happening to remain in storage is different from the language guaranteeing that an access is valid.

Concepts in this unit: Aliasing · Lifetime · Pointer ·

Read next: Virtual Memory, Page Tables, and the TLB