Copy or alias
Every function call must answer a question Python never asks you. Does the callee receive the caller's actual variable, or its own copy?
Getting it wrong in one direction gives you a function that mysteriously changes nothing. Getting it wrong in the other direction copies a million-element container on every call. C++ makes the choice explicit in the parameter's type, and this lesson is the decision rule professionals use.
By default, C++ copies arguments
void tryToDouble(int x) { // x is a COPY of the caller's value x = x * 2; // modifies only the copy } int main() { int score = 50; tryToDouble(score); std::cout << score; // still 50 }
This is pass by value. The function gets its own copy, and nothing it does can touch the caller's variable.
Add & to pass by reference
void doubleIt(int& x) { // x is an ALIAS for the caller's variable x = x * 2; // modifies the original }
The & after the type makes the parameter a reference, meaning another name for the caller's variable rather than a copy. Now doubleIt(score) really does change score. Unit 5 explains how references work underneath, and for now the two behaviors are what matter.
The same body, one ampersand apart
Two functions with identical bodies, differing only in whether the parameter is a copy or a reference. Both are called on the same variable, in order.
#include <iostream> void tryToDouble(int x) { x = x * 2; } // copy void doubleIt(int& x) { x = x * 2; } // alias int main() { int a = 50; tryToDouble(a); std::cout << "after tryToDouble: " << a << "\n"; doubleIt(a); std::cout << "after doubleIt: " << a << "\n"; return 0; }
Output
after tryToDouble: 50 after doubleIt: 100
The call sites are identical. Both read f(a), with no marker at the point of the call to say which one modifies a, so the only way to know is to look at the function's declaration. That is a genuine readability cost of references, and it is why some codebases prefer to return a new value instead of modifying a parameter in place.
tryToDouble is not merely useless, it is the kind of function a compiler will warn about, since it computes a value into a copy that is discarded the moment the function returns.
const reference: fast and safe
Copying an int is instant, but copying a std::string of a million characters is real work. Passing big objects by reference avoids the copy, and adding const promises the function will only read:
void printTwice(const std::string& s) { std::cout << s << s; // s += "!"; would be a COMPILE ERROR, s is const here }
The rule of thumb professional C++ uses everywhere:
- Small things (int, double, char, bool): pass by value.
- Big things you only read: pass by
const&. - Things the function must modify: pass by
&.
Reading a large object without copying it
A function that must read a huge std::string without copying it and without being able to change it takes a const std::string& parameter.
Each half of that type does one job. The & binds the parameter to the caller's string so no copy is made, and the const forbids modification, turning any attempted write into a compile error rather than a bug.
| Parameter type | Copies? | Can modify caller's data? |
|---|---|---|
std::string s | yes, the whole thing | no |
std::string& s | no | yes |
const std::string& s | no | no |
Plain std::string copies every character, which is wasted work for a read-only function. std::string& avoids the copy but allows the function to change the caller's data, which the requirement rules out and which readers of the declaration would reasonably expect it to do.
Swapping two variables through references
The classic reference exercise. Both parameters are references, so the assignments inside the function land on the caller's variables.
#include <iostream> void swap_ints(int& a, int& b) { int tmp = a; a = b; b = tmp; } int main() { int x = 3, y = 7; swap_ints(x, y); std::cout << x << " " << y << "\n"; return 0; }
Output
7 3
The temporary is not optional. Assigning a = b first destroys the old value of a, so without tmp holding it, the following b = a would just copy b onto itself and both variables would end up as 7.
Because a and b are references, every assignment in the body writes into x and y directly. Drop both ampersands and the function still compiles and still swaps something, just two local copies that vanish on return, leaving the output as 3 7. The standard library ships std::swap from <utility>, which does exactly this for any type, so hand-written swaps belong in exercises rather than production code.