Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Modern C++ Basics: Types, Containers, Functions and Classes
Values, References, Pointers and const Correctness
Understand how C++ passes values, use references to avoid copies, apply const correctness, and know when a pointer is the right tool.
What you'll build
A score-updating program that shows the difference between passing by value, by reference and by const reference, and how const protects data.
The idea
By default C++ copies values: passing an object to a function makes a copy, and changes to the copy are invisible to the caller. That is why C++ gives you more options:
- Pass by value
void f(int x): a copy. Good for small types (int,double,bool). - Pass by reference
void f(int& x): the function works on the caller's variable and can change it. - Pass by const reference
void f(const std::string& s): no copy and no change. The default for large read-only arguments (std::string,std::vector). - Pointers
int*hold an address and may be null. Use them when "no object" is a valid state or you need to reseat the target. Prefer references otherwise.
A reference is another name for an existing object. It must be initialized and cannot be rebound.
const correctness: mark everything that shouldn't change as const: variables, parameters, and member functions that don't modify the object. The compiler then catches accidental changes, and callers know what's safe to pass.
Never return a reference or pointer to a local variable. The local is destroyed when the function ends, leaving a dangling reference. Returning by value is almost always fine and fast, thanks to optimizations.
The code
#include <iostream>
#include <string>
#include <vector>
void bump_by_value(int score) { score += 10; } // changes a copy
void bump_by_reference(int& score) { score += 10; } // changes the caller's variable
// const reference: no copy, no modification
double average(const std::vector<int>& scores) {
if (scores.empty()) return 0.0;
double total = 0;
for (int s : scores) total += s;
return total / static_cast<double>(scores.size());
}
// Pointers may be null
const std::string* find_name(const std::vector<std::string>& names, char first_letter) {
for (const auto& name : names) {
if (!name.empty() && name[0] == first_letter) return &name;
}
return nullptr;
}
int main() {
int score = 50;
bump_by_value(score);
std::cout << "after by value: " << score << '\n';
bump_by_reference(score);
std::cout << "after by reference: " << score << '\n';
const std::vector<int> scores{70, 85, 90};
std::cout << "average: " << average(scores) << '\n';
// scores.push_back(100); // error: scores is const
// A reference is another name for the same object
int& alias = score;
alias = 99;
std::cout << "score via alias: " << score << '\n';
std::vector<std::string> names{"Ada", "Grace", "Linus"};
if (const auto* found = find_name(names, 'G')) {
std::cout << "found " << *found << '\n';
}
if (find_name(names, 'Z') == nullptr) {
std::cout << "no name starts with Z\n";
}
// Looping with a reference avoids copying each element, and lets us modify them
for (auto& name : names) name += "!";
for (const auto& name : names) std::cout << name << ' ';
std::cout << '\n';
}
Output
after by value: 50 after by reference: 60 average: 81.6667 score via alias: 99 found Grace no name starts with Z Ada! Grace! Linus!
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Passing big objects by value when a const reference would do; copies of strings and vectors cost time.
- Returning a reference to a local. The object is gone when the function returns; return by value instead.
- Using raw pointers for ownership. Pointers like
int*don't say who frees the memory; modern C++ usesstd::unique_ptr(see the smart pointers lesson). - Forgetting
conston member functions that don't change the object, which blocks use with const objects. - Dereferencing a null pointer. Check pointers returned from lookups before using
*or->.
Key points
- Values are copied; use
T&to modify the caller's object andconst T&to read without copying. - Pass small types by value and large read-only types by const reference.
- A reference must be initialized and always refers to the same object; a pointer may be null.
- Make things
constby default; never return a reference to a local variable.
Further reading: C++ Core Guidelines: Functions (F.15 how to pass parameters).
Practice questions
When should a parameter be `const std::string&` rather than `std::string`?
Answer
When the function only reads the string. A const reference avoids copying a potentially large object while still guaranteeing the function can't modify it.
Why is returning a reference to a local variable a bug?
Answer
The local variable is destroyed when the function returns, so the reference points to memory that no longer holds a valid object. Using it is undefined behavior. Return by value instead.
Open in the app to mark lessons complete, take notes and track your progress.