xSKILLby Infirals
Register

C++ for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Pointers and references compared

Functions and library types12 min readLesson 3 of 5

3.1 Two ways to reach an object

A reference gives an existing object a second name. Once a reference is bound, every use of it is a use of the original object. A pointer is a separate object that holds the address of another object; reading or writing the target requires the * operator.

Both mechanisms avoid copying, and they serve different contracts. A reference says that an object must exist and will stay in place for the reference's lifetime. A pointer can be empty, can be redirected to another object, and can be tested before use. Many C++ interfaces use references for required arguments and pointers for optional ones.

cpp
#include <iostream>

int main() {
    int score = 40;
    int& bonus = score;
    int* ptr = &score;

    bonus += 2;
    *ptr += 3;

    std::cout << "Score: " << score << "\n";
    std::cout << "Bonus: " << bonus << "\n";
    return 0;
}

Output

Score: 45
Bonus: 45

3.1.1 Binding rules

A reference must be initialized when it is declared, and it cannot be rebound afterward. int& r = score; is valid. int& r; and int& r = nullptr; are not. After r = other; the value of score changes; r still names score.

A pointer can be initialized to nullptr, left uninitialized (a dangerous choice), and pointed at a different object later. Dereferencing a null pointer or an uninitialized pointer is undefined behavior, so test a pointer before reading through it when it may not be set. Both references and pointers can dangle once the object they name is destroyed, and the compiler will not always warn you.

cpp
#include <iostream>
#include <string>

int main() {
    std::string first = "first";
    std::string second = "second";
    std::string* pick = &first;
    pick = &second;
    std::cout << "Picked: " << *pick << "\n";

    pick = nullptr;
    if (pick != nullptr) {
        std::cout << "Value: " << *pick << "\n";
    } else {
        std::cout << "Nothing selected.\n";
    }
    return 0;
}

Output

Picked: second
Nothing selected.

3.1.2 Const with pointers and references

The position of const changes what is protected. const int* p is a pointer to a const int: *p cannot be assigned, and p can point elsewhere. int* const p is a const pointer: p cannot change, and *p can. Reading the declaration from right to left keeps the meaning clear.

References follow a simpler rule. const int& r binds to a value the code will only read, and the compiler rejects assignment through r. A plain int& cannot bind to a literal or a temporary, because changing a temporary would be pointless. A const reference can bind to a temporary, and the temporary lives until the end of the full expression.

cpp
#include <iostream>

int main() {
    int low = 10;
    int high = 20;
    const int* view = &low;
    int* const anchor = &high;

    view = &high;
    *anchor = 25;

    std::cout << "View: " << *view << "\n";
    std::cout << "High: " << high << "\n";
    return 0;
}

Output

View: 25
High: 25

3.1.3 References in loops

A range-based for loop copies each element into its loop variable unless the variable is a reference. for (std::string name : names) copies every string. for (std::string& name : names) binds to each element and can modify it. for (const std::string& name : names) reads without copying and blocks modification.

The same choice appears with auto. auto drops the reference and copies; auto& keeps a reference; const auto& reads. For small integers the copy costs little, and for strings or structures the reference is usually the better default.

cpp
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names = {"Ada", "Ravi", "Mei"};
    for (std::string& name : names) {
        name += "!";
    }
    for (const std::string& name : names) {
        std::cout << name << "\n";
    }
    return 0;
}

Output

Ada!
Ravi!
Mei!

3.1.4 Lifetime and dangling references

A reference or pointer is only usable while its target is alive. If a function returns a reference to a local variable, that variable is destroyed when the function returns, and the caller holds a dangling reference. The same problem appears when you keep a reference into a std::vector and then add enough elements to force reallocation.

References stored as class members must be initialized in the constructor, and the referenced object must outlive the class object. When those conditions are hard to guarantee, store a value or a smart pointer instead. Compilers rarely diagnose a dangling reference, so the lifetime rule has to come from your design.

3.1.5 Choosing between them

Use a reference when the argument is required, the object exists, and the function should work with the caller's object. Use a const reference for read-only access to a large object. Use a pointer when the argument can be omitted, when the code needs to walk an array, or when an older C API requires one.

Return a reference only when the returned object already exists and will outlive the call: a member, a container element, or a static object. Return by value when the function creates something new. A reference parameter also removes the need for a null check at the call site.

A function that returns a reference to a container element depends on the element existing. Check for an empty container before the call, and keep the container alive while the reference is in use.

cpp
#include <cstddef>
#include <iostream>
#include <vector>

int& highest(std::vector<int>& values) {
    std::size_t best = 0;
    for (std::size_t i = 1; i < values.size(); ++i) {
        if (values[i] > values[best]) {
            best = i;
        }
    }
    return values[best];
}

int main() {
    std::vector<int> scores = {72, 91, 64};
    highest(scores) += 5;
    std::cout << "Top score: " << scores[1] << "\n";
    return 0;
}

Output

Top score: 96

3.1.6 Practice

  1. Bind a reference to an integer, change the value through the reference, and print both names.
  2. Point a pointer at two different integers in turn and print the target after each assignment.
  3. Write a function that takes a vector by const reference and returns the largest value.
  4. Modify a vector through a range-based loop, then print it with a const reference loop.
  5. Explain why int& r = 5; is rejected while const int& r = 5; compiles.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01When must a reference be initialized, and why can it not be rebound?

    A reference is bound when it is declared and stays bound to that object for its lifetime. There is no syntax to make it refer to another object, so code that needs to switch targets uses a pointer.

  • 02What is the difference between `const int*` and `int* const`?

    `const int*` is a pointer to a const int: the value cannot be changed through it, but the pointer can move. `int* const` is a const pointer: the address cannot change, but the value at that address can.

  • 03Why is returning a reference to a local variable dangerous?

    The local object is destroyed when the function returns, so the reference names storage that no longer holds a live object. Using it afterward is undefined behavior.