Pass by Value, Reference, and Pointer
When you call a function and provide an argument, C++ has three fundamental mechanisms for how the argument is delivered: by value (the function gets a copy), by reference (the function gets an alias to the original), or by pointer(the function gets the memory address of the original). Each mechanism has a different cost, different safety properties, and different appropriate use cases. Choosing the right one is one of the most important decisions you make when writing a function signature.
Pass by value
When you pass by value, the function receives a copyof the argument. The function can modify its local copy freely — those modifications never affect the caller's variable. Pass by value is the default in C++ and is appropriate for small, cheap-to-copy types like int, double, bool, and char. For larger objects like strings or vectors, making a copy on every call is wasteful.
void increment(int n) // n is a copy
{
n++; // modifies the copy, not the original
}
int x = 5;
increment(x);
std::cout << x; // still 5 — x was not modified
// Pass by value IS appropriate for small types:
double square(double x) { return x * x; }
// Pass by value is EXPENSIVE for large types:
void process(std::vector<int> v) { ... } // copies entire vector!Pass by reference
When you pass by reference, the function receives an aliasfor the original variable. Any modification to the parameter directly modifies the caller's variable. Adding constin front gives you a read-only alias — you get the efficiency of not copying without the ability to accidentally modify the caller's data. The const T& idiom is the most common parameter style in C++ for anything larger than a pointer.
// Mutable reference — CAN modify the caller's variable
void increment(int& n) // & makes this a reference
{
n++; // modifies the original
}
int x = 5;
increment(x);
std::cout << x; // 6 — x was modified through the reference
// const reference — CANNOT modify, but avoids copying
void printVector(const std::vector<int>& v)
{
for (int x : v) std::cout << x << " ";
}
std::vector<int> data = {1, 2, 3, 4, 5};
printVector(data); // no copy made — fastA const T& parameter also accepts temporaries and literals, which a plain (non-const) reference does not:
void greet(const std::string& name) {
std::cout << "Hello, " << name;
}
greet("Alice"); // const ref accepts a string literal
greet(std::string("Bob")); // const ref accepts a temporary
// void greet(std::string& name) would reject both of the above
// because you cannot bind a non-const reference to a temporaryPass by pointer
When you pass by pointer, the function receives the memory address of the original variable. The function must dereference the pointer with * to access the value. Passing a pointer has the same performance characteristics as passing a reference, but with added capabilities: the pointer can be null (expressing “no argument provided”) and can be rebound to point at different objects. The caller must explicitly take the address with &.
void increment(int* p) // pointer parameter
{
if (p != nullptr) { // always check before dereferencing
(*p)++;
}
}
int x = 5;
increment(&x); // caller takes address explicitly
std::cout << x; // 6
increment(nullptr); // valid — pointer can be null; function guards against itThe old C-style approach was to use pointers for all non-value parameters. Modern C++ prefers references for required parameters (callers can't pass null by accident) and pointers only when nullability is intentional (optional parameters, optional results).
Comparison
| Mechanism | Signature | Can modify caller? | Can be null? | When to use |
|---|---|---|---|---|
| By value | int f(int x) | No (copy) | No | Small cheap types: int, double, char |
| By const ref | int f(const T& x) | No | No | Large read-only objects: string, vector, struct |
| By mutable ref | void f(T& x) | Yes | No | Output parameter or in-out modification |
| By pointer | void f(T* p) | Yes (via *) | Yes | Optional parameter; C APIs; rebinding needed |
Putting it together
// By value — function doesn't need to change x
double fahrenheitToCelsius(double f) {
return (f - 32.0) * 5.0 / 9.0;
}
// By const reference — large string, read only
void logMessage(const std::string& msg) {
std::cout << "[LOG] " << msg << "\n";
}
// By mutable reference — swap two values
void swapInts(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// By pointer — optional logging
void compute(int x, Logger* log = nullptr) {
if (log) log->write("computing...");
// ...
}
int a = 3, b = 7;
swapInts(a, b);
std::cout << a << " " << b; // 7 3Key rules to remember
Default to const T& for large objects you only read
const T& is the most common parameter style in C++ for strings, vectors, and structs. It avoids copying and prevents accidental modification.
Use T& (non-const reference) when the function intentionally modifies the caller's variable
The & in the signature signals to the caller that their variable will be changed. This makes the interface self-documenting.
Use pass-by-value for small built-in types and when the function needs its own copy
Passing int, double, bool, char by value is idiomatic and efficient — the copy is just a register. For objects, prefer references.
Use a pointer when null is a valid input — check before dereferencing
A null pointer passed to a function that expects a non-null pointer causes a crash. References cannot be null, which makes them safer for required arguments.