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.

Create a free account to save this page, take notes and track your progress. Sign in

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

Pass by value, reference and const reference
#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++ uses std::unique_ptr (see the smart pointers lesson).
  • Forgetting const on 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 and const 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 const by 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.

Was this page helpful?