Intuition
A house and its address. Writing the address on two sticky notes doesn’t build a second house: anyone who follows either note arrives at the same door, and if one of them paints the door red, the other sees red too. But if you cross out one note and write a different address on it, the other note and the house are unchanged.
A variable that holds a reference is the sticky note. The object on the heap is the house.
How it works
Some values are stored directly in a variable: numbers, booleans, and in some languages small structs. Copying one gives you an independent copy.
Lists, objects, maps and strings usually live on the heap, and the variable holds a reference to them. Copying that variable copies the reference, so both variables point at the same object.
In the figure:
count = 5stores the number in demo’s frame.a = [1, 2]creates a list on the heap;arefers to it.c = a.copy()creates a second list with the same values.update(a, count)copies the reference intonumsand the number inton.n = n + 1changes only update’s copy of the number.nums[0] = nfollows the shared reference, so demo’sasees the 6.nums = [0, 0]pointsnumsat a new list.ais unaffected, and once update returns, nothing refers to the new list.
In each language
| Language | How references work |
|---|---|
| Python | Every variable holds a reference. Numbers and strings can’t be changed in place, so sharing them never surprises you; lists, dicts and objects can. is compares references, == compares values. |
| JavaScript | Numbers, strings and booleans behave like values; objects and arrays are shared by reference. === on two objects is true only if they are the same object. |
| Go | Structs and arrays are copied on assignment. Slices, maps and channels hold a pointer inside, so copies share data. Explicit pointers: p := &x, then *p = 3. |
| Rust | &x borrows to read and &mut x borrows to change. The compiler allows many readers or one writer at a time, never both, and a reference can’t outlive its value. |
| C | Pointers are addresses you can do arithmetic on: int *p = &x; *p = 3;. Nothing stops a pointer to freed memory or NULL being used, so mistakes crash or corrupt memory. |
| Java | Primitives (int, double) are values; every object variable is a reference. Arguments are always passed by value, and for objects the value is the reference. |
Common mistakes
- Expecting
b = ato copy a list. It copies the reference. Copy explicitly:a.copy()orlist(a)(Python),[...a](JavaScript),slices.Clone(a)(Go),a.clone()(Rust). - Shallow copies of nested data. Copying a list of lists copies the outer list only; the inner lists are still shared. Use
copy.deepcopy(Python) orstructuredClone(JavaScript) when you need a full copy. - Python’s mutable default argument.
def add(x, items=[])creates the list once, when the function is defined, so every call shares it. Useitems=Noneand make a new list inside. - Null and dangling pointers. Following a null reference crashes (
NullPointerException,TypeError: Cannot read properties of null); in C, using a pointer afterfreeis undefined behaviour.
Check yourself
Check yourself
Inside a function, nums[0] = 6 and then nums = [0, 0]. What does the caller's list look like?
The write followed the shared reference into the caller’s list. The reassignment only pointed the local name nums at a new list.
Check yourself
a = [1, 2], b = a, c = a.copy(), then a.append(3). Which lists have three items?
a and b are two names for one list. c is a separate copy made before the append.
Sources
- Python documentation, “Programming FAQ: How do I write a function with output parameters (call by reference)?”
- The Go Programming Language Specification, sections “Slice types” and “Address operators”.
- The Rust Programming Language, chapter 4.2, “References and Borrowing”.
Finished this page?
Your list