References & Pointers

C++ · Lesson 10 of 32 7 min read Updated

In the last lesson you saw that a function receives a copy of each argument. Often that is not what you want: a function that adds bonus marks should change the real marks, and a function that prints a long list should not waste time copying it. C++ gives you two tools to refer to an existing object instead of copying it: references and pointers.

References are simpler and safer, so you will use them most. Pointers are more flexible and appear everywhere in older code, in C libraries and in low-level work, so you must understand them too.

References: another name for a variable

A reference is an alias, a second name for an existing variable. You create one by adding & after the type. Whatever you do through the reference happens to the original.

C++
#include <print>

int main() {
    int score = 50;
    int& alias = score;          // alias refers to score

    alias += 10;
    std::println("score = {}, alias = {}", score, alias);

    score = 99;
    std::println("score = {}, alias = {}", score, alias);
}
Output
score = 60, alias = 60
score = 99, alias = 99

There is only one integer here, with two names. A reference must be initialised when it is created, and it can never be changed to refer to a different variable later.

Passing by reference

The main use of references is in function parameters. A reference parameter lets the function work on the caller's variable directly.

C++
#include <print>
#include <string>

void add_bonus(int& marks, int bonus) {
    marks += bonus;                       // changes the caller's variable
}

void swap_values(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

void show(const std::string& name, int marks) {   // no copy, read-only
    std::println("{}: {}", name, marks);
}

int main() {
    int maths = 72;
    add_bonus(maths, 5);
    show("Ananya", maths);

    int x = 3;
    int y = 8;
    swap_values(x, y);
    std::println("x = {}, y = {}", x, y);
}
Output
Ananya: 77
x = 8, y = 3

Look at show: the parameter is const std::string&. The & avoids copying the string, and const promises the function will not change it. This pass by const reference is the standard way to pass large objects such as strings, vectors and class objects that a function only reads. For small types like int, double and char, passing by value is just as fast and simpler. (The standard library already has std::swap, so in real code you would not write swap_values yourself.)

Pointers: variables that hold addresses

Every variable lives somewhere in memory, and each location has an address, a number. A pointer is a variable whose value is an address. Two operators work with pointers:

  • &x (address-of) gives the address of x.
  • *p (dereference) gives the object that pointer p points to.
C++
#include <print>

int main() {
    int runs = 45;
    int* p = &runs;              // p holds the address of runs

    std::println("runs = {}", runs);
    std::println("*p = {}", *p); // follow the pointer

    *p = 100;                    // change runs through the pointer
    std::println("runs is now {}", runs);

    int wickets = 3;
    p = &wickets;                // a pointer CAN be re-pointed
    std::println("*p is now {}", *p);
}
Output
runs = 45
*p = 45
runs is now 100
*p is now 3

The type int* means "pointer to int". The * has two jobs: in a declaration it makes a pointer type, and in an expression it dereferences. The address itself is a large number that changes every time you run the program, so lessons rarely print it.

nullptr: pointing at nothing

Unlike a reference, a pointer can be empty. The special value nullptr means "points to nothing". This makes pointers useful for answers that may not exist, such as "the first negative balance, if there is one". Always check before dereferencing: dereferencing nullptr is undefined behaviour and usually crashes the program.

C++
#include <print>
#include <vector>

int* find_first_negative(std::vector<int>& values) {
    for (auto& v : values) {
        if (v < 0) {
            return &v;               // address of the item inside the vector
        }
    }
    return nullptr;                  // nothing found
}

int main() {
    std::vector<int> balances = {500, 1200, -300, 800};

    if (int* p = find_first_negative(balances); p != nullptr) {
        std::println("Found a negative balance: {}", *p);
        *p = 0;                      // fix it in place
    }
    std::println("Third balance is now {}", balances[2]);
}
Output
Found a negative balance: -300
Third balance is now 0

In modern C++, std::optional (see Exceptions, optional & expected) is often a clearer way to say "maybe a value", but pointers like this are still common.

References or pointers?

ReferencePointer
Can be emptyNoYes, nullptr
Can be re-pointedNoYes
Must be initialisedYesNo, but always should be
Syntax to useLike a normal variableNeeds * and ->
Typical useFunction parametersOptional objects, arrays, low-level code

Rule of thumb: use a reference when something must always exist, and a pointer only when "nothing" is a valid answer or you need to re-point. Never use a raw pointer to own memory you created with new; Smart Pointers vs new & delete shows the modern way.

Dangling references and pointers

A reference or pointer to an object that no longer exists is dangling. Local variables are destroyed when their function returns, so never return a reference or pointer to one:

C++
const std::string& bad_name() {
    std::string name = "Kabir";
    return name;          // name is destroyed here: the reference dangles
}

GCC warns "reference to local variable 'name' returned". Using the result is undefined behaviour: it may print garbage, crash, or seem to work today and fail tomorrow. Return by value instead (std::string bad_name()); modern compilers make that cheap.

Common mistakes

  • Dereferencing nullptr or an uninitialised pointer. Initialise every pointer, and check for nullptr before using *.
  • Forgetting & in a parameter. void add_bonus(int marks, int bonus) compiles but changes only a copy.
  • Returning references to locals. The object dies when the function ends.
  • Keeping a pointer into a vector while adding items. When a vector grows it may move its items to new memory, and old pointers and references into it dangle.

Key points

  • A reference (int& r = x;) is another name for an existing object; it cannot be empty or re-pointed.
  • Pass by reference to let a function change the caller's variable; pass by const& to avoid copying large objects.
  • A pointer (int* p = &x;) stores an address; *p reaches the object, and nullptr means "nothing".
  • Prefer references; use pointers when "no object" is valid or re-pointing is needed.
  • Never return a reference or pointer to a local variable: it would dangle.

Exercise

Write a function apply_discount(double& price, double percent) that reduces a price in place, and a function const std::string* find_city(const std::vector<std::string>& cities, char first_letter) that returns a pointer to the first city starting with that letter, or nullptr. In main, discount a price of 1299 by 10 percent and print it with two decimals, then look up a city starting with 'K' and one starting with 'Z' in {"Jaipur", "Kochi", "Pune"}.

Show solution
C++
#include <print>
#include <string>
#include <vector>

void apply_discount(double& price, double percent) {
    price -= price * percent / 100;
}

const std::string* find_city(const std::vector<std::string>& cities, char first_letter) {
    for (const auto& city : cities) {
        if (!city.empty() && city[0] == first_letter) {
            return &city;
        }
    }
    return nullptr;
}

int main() {
    double price = 1299;
    apply_discount(price, 10);
    std::println("Price after discount: {:.2f}", price);

    std::vector<std::string> cities = {"Jaipur", "Kochi", "Pune"};
    for (char letter : {'K', 'Z'}) {
        if (const std::string* city = find_city(cities, letter); city != nullptr) {
            std::println("{}: {}", letter, *city);
        } else {
            std::println("{}: no city found", letter);
        }
    }
}
Output
Price after discount: 1169.10
K: Kochi
Z: no city found

The function returns the address of the string inside the vector, which stays valid as long as the vector is alive and unchanged.

Quick Quiz

1. After int a = 5; int& r = a; r = 9;, what is a?

2. What is the best parameter type for a function that only reads a long std::string?

3. What does *p mean when p is an int*?

4. Which is a valid reason to use a pointer instead of a reference?

Learn C++ with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call