The idea: what gets copied when you pass a value
A call f(a) always copies something into the new stack frame of f. The
question is what. It can be the whole list, nothing at all (the parameter becomes another name for the
variable a), or just the pointer that a holds. Those three answers are
pass by value, pass by reference and the rule Java, Python and JavaScript use:
pass the reference by value.
The canvas runs the same program in three panels at once, one hop per step. The left column of each panel
is the stack: frame main with a (and b), and frame
f with x while f runs. The right part is the heap: list
objects numbered #1, #2, … in the order they were made. A blue arrow is a pointer to a list; a
purple arrow is a reference to a variable. A grey box carries the value being passed or written.
Pass by value
In C++, void f(vector<int> x) takes its argument by value. The call runs the copy
constructor: it reads every element of a's buffer #1 and writes them into a new
buffer #2 that belongs to x. Whatever f then does to x stays
in #2, and when f returns, x's destructor frees it. The caller's
a prints [1, 2, 3] in every demo. The price is the copy: a million elements are a
million writes, which is why C++ code passes big objects as const vector<int>&.
Pass by reference
void f(vector<int>& x) copies nothing. x becomes another name for the
variable a itself; the compiler passes a's address and follows it on every use. So
x[0] = 9 is a[0] = 9, and x = {7, 7} is a = {7, 7}: both
change what the caller sees (Demo 1, Demo 2). C#'s ref, Pascal's
var and PHP's &$x parameters work the same way.
Passing a reference by value (Java, Python, JS)
In Python, Java (for objects and arrays) and JavaScript (for objects), a variable never holds a list. It
holds a reference: a pointer to an object on the heap. The call copies that pointer, so
a and x are two names for one list #1. The language is still pass by
value; the value just happens to be a pointer. That is why people argue about what to call it. Python's
docs call it call by object reference, others say call by sharing.
Mutate vs reassign
The difference shows when f assigns to its parameter. Mutating goes through the pointer and
reaches the shared list; assigning only re-aims the callee's copy of the pointer. In C++, = means
something else again: it copies new contents into the object the name stands for.
| f does | By value | By reference | Reference by value |
|---|---|---|---|
x[0] = 9 (Demo 1) | a = [1, 2, 3] | a = [9, 2, 3] | a = [9, 2, 3] |
x = [7, 7] (Demo 2) | a = [1, 2, 3] | a = [7, 7] | a = [1, 2, 3] |
x = [7, 7]; x[0] = 9 (Demo 3) | a = [1, 2, 3] | a = [9, 7] | a = [1, 2, 3] |
Demo 3 is the classic trap. In Python, after x = [7, 7] the name x points at a new
list #2, so the next x[0] = 9 lands there, not in a. When f
returns, nothing points at #2 any more: its reference count drops to 0 and it is freed. The same
reason explains why a Java swap(a, b) method can never swap the caller's variables.
Shallow vs deep copy
Copying a list of lists has the same three answers one level down. Pick b = copy of a and the
panels switch to a = [[1, 2], [3, 4]]: an outer list #1 whose two cells point at the
inner lists #2 and #3.
b = acopies nothing but the pointer. Every change throughbis a change toa.- A shallow copy (
a.copy(),list(a),a[:],copy.copy(a), JavaScript's[...a], Java'sclone()on an array) makes one new outer list and copies its cells, which are pointers. Sob[0][0] = 9still changesa(Demo 4), whileb[1] = [5, 6]only rewritesb's own outer list (Demo 5). - A deep copy (
copy.deepcopy(a), JavaScript'sstructuredClone(a)) copies every list it reaches, so nothing is shared. It costs a copy of the whole structure and keeps a memo table so shared or cyclic parts are copied once.
| Then | b = a | Shallow copy | Deep copy |
|---|---|---|---|
b[0][0] = 9 (Demo 4) | a = [[9, 2], [3, 4]] | a = [[9, 2], [3, 4]] | a = [[1, 2], [3, 4]] |
b[1] = [5, 6] (Demo 5) | a = [[1, 2], [5, 6]] | a = [[1, 2], [3, 4]] | a = [[1, 2], [3, 4]] |
Which language does what
| Language | Default | Real pass by reference |
|---|---|---|
| C | By value (pass a pointer to share) | No |
| C++ | By value, copy constructor runs | T& |
| Java | By value: primitives, and references to objects | No |
| Python | Reference by value (call by object reference) | No |
| JavaScript | By value: primitives, and references to objects | No |
| C# | By value (references for classes, copies for structs) | ref, out |
| Go | By value (slices and maps carry a pointer inside) | No |
See also Stack vs Heap in Java for frames and references in the JVM,
Stack vs Heap in C for what a stack frame and malloc look like
underneath, and Python Garbage Collection for the reference counts that free
#2 here.
What the page leaves out
Immutable values (Python ints, strings and tuples can't be mutated, so for them the three ways look the
same), C++ move semantics and copy elision, const&, Rust's borrows, the vector's size and
capacity fields, small-int caching, the cycle collector, real addresses, and objects with their own
__copy__ / __deepcopy__ or copy constructors.