C++ Pass by Value vs Reference: Complete Guide
Why This Matters
Understanding how data flows into and out of functions is one of the most critical skills in C++. Get it wrong and you will copy megabytes of data needlessly, mutate variables you did not intend to change, or create dangling references that crash your program. C++ gives you fine-grained control over this — more than almost any other language — but that control comes with responsibility.
This lesson covers the three fundamental ways to pass data to functions: by value, by reference, and by const reference. You will also learn when to use each, how references relate to pointers, and get a preview of rvalue references from C++11.
Pass by Value
When you pass by value, the function receives a copy of the argument. Changes inside the function do not affect the original variable.
#include <iostream>
using namespace std;
void increment(int x) {
x++; // Modifies the copy
cout << "Inside function: " << x << endl;
}
int main() {
int num = 10;
increment(num);
cout << "After function: " << num << endl;
return 0;
}
// Output:
// Inside function: 11
// After function: 10
Pass by value is safe — the caller’s data is protected. But copying is expensive for large objects like std::vector, std::string, or custom classes with hundreds of fields.
#include <iostream>
#include <vector>
using namespace std;
// This copies the ENTIRE vector — slow for large vectors!
void print_sum(vector<int> nums) {
int total = 0;
for (int n : nums) total += n;
cout << "Sum: " << total << endl;
}
int main() {
vector<int> big_data(1000000, 1); // 1 million ints
print_sum(big_data); // Copies all 1 million ints!
return 0;
}
Pass by Reference
Adding & after the type makes the parameter a reference — an alias for the original variable. No copy is made, and changes inside the function do affect the original.
#include <iostream>
using namespace std;
void increment(int& x) { // x is a reference to the original
x++;
cout << "Inside function: " << x << endl;
}
int main() {
int num = 10;
increment(num); // num itself is modified
cout << "After function: " << num << endl;
return 0;
}
// Output:
// Inside function: 11
// After function: 11
A reference parameter is not a separate variable — it is another name for the same memory. This is zero-cost in terms of copying.
const References
If you want the performance benefit of references (no copying) but do not want the function to modify the original, use const&.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// No copy, no modification — the ideal way to read large data
void print_sum(const vector<int>& nums) {
int total = 0;
for (int n : nums) total += n;
cout << "Sum: " << total << endl;
// nums.push_back(1); // ERROR: cannot modify const reference
}
void print_upper(const string& text) {
for (char c : text) {
cout << static_cast<char>(toupper(c));
}
cout << endl;
// text[0] = 'X'; // ERROR: const
}
int main() {
vector<int> data = {10, 20, 30, 40, 50};
print_sum(data); // No copy — fast
string msg = "hello";
print_upper(msg); // HELLO — original unchanged
cout << msg << endl; // hello
return 0;
}
The const& pattern is the single most important parameter-passing idiom in C++. Use it by default for any parameter larger than a pointer (strings, vectors, structs, classes). This is what C++ Core Guidelines F.16 recommends.
When to Use Each
// Rule of thumb:
// - Small types (int, double, char, bool, pointers): pass by VALUE
// - Large types you need to READ: pass by CONST REFERENCE
// - Data you need to MODIFY: pass by REFERENCE
void process(int count); // Small — by value
void analyze(const vector<double>& data); // Large, read-only — const ref
void sort_data(vector<double>& data); // Large, modify — ref
void transform(const string& input, string& output); // Mixed
Classic Swap Example
Swapping two variables is the textbook example of why pass by reference exists.
#include <iostream>
using namespace std;
// WRONG: swaps copies, originals unchanged
void bad_swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// CORRECT: swaps the actual variables
void good_swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
bad_swap(x, y);
cout << "After bad_swap: x=" << x << " y=" << y << endl;
good_swap(x, y);
cout << "After good_swap: x=" << x << " y=" << y << endl;
return 0;
}
// Output:
// After bad_swap: x=10 y=20
// After good_swap: x=20 y=10
In practice, use std::swap(x, y) from <algorithm> — it handles move semantics for complex types.
Returning References
Functions can return references to avoid copying. This is common in operator overloading and container access methods.
#include <iostream>
#include <vector>
using namespace std;
// Return reference to the largest element
int& find_max(vector<int>& nums) {
int max_idx = 0;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[max_idx]) max_idx = i;
}
return nums[max_idx]; // Reference to element inside vector
}
int main() {
vector<int> data = {10, 50, 30, 80, 20};
cout << "Max: " << find_max(data) << endl; // 80
// Because it returns a reference, we can MODIFY the max element
find_max(data) = 999;
cout << "Modified max: ";
for (int n : data) cout << n << " ";
cout << endl;
// Output: 10 50 30 999 20
return 0;
}
Dangling References
Never return a reference to a local variable. When the function returns, the local is destroyed, and the reference points to garbage memory — this is undefined behavior.
#include <iostream>
using namespace std;
// DANGEROUS: returns reference to local variable
int& bad_function() {
int local = 42;
return local; // WARNING: local is destroyed after return
}
// SAFE alternatives:
int safe_by_value() {
int local = 42;
return local; // Returns a copy — fine
}
// SAFE: reference to something that outlives the function
int& safe_ref(int& external) {
external += 10;
return external; // external outlives this function
}
int main() {
// int& bad = bad_function(); // UNDEFINED BEHAVIOR
int val = safe_by_value(); // OK: 42
int x = 5;
int& ref = safe_ref(x); // OK: x is still alive
cout << ref << endl; // 15
return 0;
}
Pointers vs References
References and pointers both provide indirect access, but they differ in important ways.
#include <iostream>
using namespace std;
void via_pointer(int* ptr) {
if (ptr == nullptr) return; // Must check for null
*ptr = 100; // Dereference to access value
}
void via_reference(int& ref) {
// No null check needed — references cannot be null
ref = 200;
}
int main() {
int x = 10;
via_pointer(&x); // Must pass address with &
cout << x << endl; // 100
via_reference(x); // Pass directly — cleaner syntax
cout << x << endl; // 200
// Key differences:
// 1. References cannot be null (pointers can)
// 2. References cannot be reassigned (pointers can)
// 3. References have cleaner syntax (no * or &)
// 4. Use references when null is not a valid option
int a = 1, b = 2;
int& ref = a; // ref IS a
ref = b; // This assigns b's VALUE to a, does NOT rebind ref
cout << a << endl; // 2 (a was changed, ref still refers to a)
return 0;
}
Reference to Pointer
You can pass a pointer by reference if you need to modify the pointer itself (not just what it points to).
#include <iostream>
using namespace std;
void allocate(int*& ptr, int value) {
ptr = new int(value); // Modifies the caller's pointer
}
void reset(int*& ptr) {
delete ptr;
ptr = nullptr;
}
int main() {
int* p = nullptr;
allocate(p, 42);
cout << *p << endl; // 42
reset(p);
cout << (p == nullptr ? "null" : "not null") << endl; // null
return 0;
}
Rvalue References (C++11 Preview)
C++11 introduced rvalue references (&&) to enable move semantics. This is an advanced topic (covered fully in later lessons), but here is a taste.
#include <iostream>
#include <string>
using namespace std;
void process(const string& s) {
cout << "Lvalue ref: " << s << endl;
}
void process(string&& s) {
cout << "Rvalue ref: " << s << endl;
// Can "steal" s's resources — s is a temporary
}
int main() {
string name = "Alice";
process(name); // Calls lvalue version
process("Bob"s); // Calls rvalue version (temporary)
process(move(name)); // Calls rvalue version (explicitly moved)
// Warning: name is now in a "moved-from" state
return 0;
}
Rvalue references are the foundation of move semantics, which lets you transfer ownership of resources instead of copying them. This is why modern C++ can be as fast as C while being far more expressive.
Real-World Patterns
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// Pattern 1: Output parameter (modify caller's data)
bool parse_int(const string& input, int& result) {
try {
result = stoi(input);
return true;
} catch (...) {
return false;
}
}
// Pattern 2: Modify and return for chaining
vector<int>& append_range(vector<int>& vec, int start, int end) {
for (int i = start; i <= end; i++) vec.push_back(i);
return vec;
}
// Pattern 3: Multiple outputs via references
void stats(const vector<double>& data, double& mean, double& min_val, double& max_val) {
min_val = max_val = data[0];
double sum = 0;
for (double d : data) {
sum += d;
if (d < min_val) min_val = d;
if (d > max_val) max_val = d;
}
mean = sum / data.size();
}
int main() {
int val;
if (parse_int("42", val)) {
cout << "Parsed: " << val << endl;
}
vector<int> nums;
append_range(append_range(nums, 1, 5), 10, 12);
for (int n : nums) cout << n << " "; // 1 2 3 4 5 10 11 12
cout << endl;
vector<double> readings = {23.5, 19.2, 31.8, 27.1, 15.0};
double avg, lo, hi;
stats(readings, avg, lo, hi);
cout << "Avg=" << avg << " Min=" << lo << " Max=" << hi << endl;
return 0;
}
Common Mistakes
1. Passing a literal to a non-const reference:
void modify(int& x) { x++; }
// modify(5); // ERROR: cannot bind non-const ref to literal
// Fix: use const ref or pass a variable
2. Returning reference to temporary:
const string& bad() {
return "hello"s; // DANGLING: temporary destroyed after return
}
3. Accidentally passing by value when you meant reference:
// Missing & — entire vector is copied every iteration!
void process(vector<int> data) { /* ... */ }
// Fix:
void process(const vector<int>& data) { /* ... */ }
Practice Exercises
Exercise 1: Write a function that takes two int references and sets both to zero.
Exercise 2: Write a normalize function that takes a vector<double>& and divides every element by the maximum value in the vector.
Exercise 3: Write a function split_at that takes a const string& and a char delimiter and returns a vector<string>.
Exercise 4: Benchmark the time difference between passing a large vector by value vs by const reference using <chrono>.
Summary
Pass by value copies data (safe but potentially slow). Pass by reference gives direct access to the original (fast, allows modification). Pass by const reference is the sweet spot for read-only access to large objects (fast, safe). These three patterns cover 99% of parameter-passing in C++. In the next lesson, you will learn recursion — functions that call themselves to solve problems by breaking them into smaller pieces.