FirstHack Learn
Log in Sign up free
Lessons in this course 0/6 All courses C++ and STL Essentials

CSE

Progress0 / 6 lessons
  1. 1. What C++ adds over C
  2. 2. References vs pointers
  3. 3. vector and why it beats raw arrays
  4. 4. map and set with real complexity numbers
  5. 5. Sorting with comparators
  6. 6. Strings and stringstream

Courses › C++ and STL Essentials

References vs pointers

What a reference really is, when to prefer it, and the three things it cannot do.

10 min read · Lesson 2 of 6 · Free

A reference is another name

C++
#include <iostream>
using namespace std;

int main() {
    int marks = 91;
    int &alias = marks;      // alias IS marks, under a second name

    alias = 100;
    cout << marks << "\n";   // 100

    cout << &marks << "\n";  // same address
    cout << &alias << "\n";  // same address
    return 0;
}

There is no second variable here. alias and marks are two names for one set of bytes. Nothing extra is stored, nothing is copied, and there is no dereference step to write.

Compare with the pointer version:

C++
#include <iostream>
using namespace std;

int main() {
    int marks = 91;
    int *p = &marks;    // p is a separate variable holding an address

    *p = 100;           // must dereference to reach marks
    cout << marks << "\n";   // 100
    return 0;
}

Same result, more syntax, and one extra variable that can be null, can be moved, and can go stale.

The three rules of references

1. A reference must be initialised. int &r; is a compile error. A pointer can sit uninitialised holding garbage — a reference cannot exist without something to refer to.

2. A reference cannot be re-seated. Once bound, it names that variable forever.

C++
#include <iostream>
using namespace std;

int main() {
    int a = 1, b = 2;
    int &r = a;

    r = b;              // this does NOT make r refer to b
                        // it copies b's value INTO a
    cout << a << " " << b << "\n";   // 2 2
    return 0;
}

This surprises people. r = b looks like reassignment of the reference. It is an assignment through the reference. a is now 2.

3. There is no null reference. A reference always refers to a real object, so you never check it before use. That is the main safety benefit.

Passing to functions

C++
#include <iostream>
using namespace std;

void byValue(int x)     { x = 99; }
void byPointer(int *x)  { *x = 99; }
void byReference(int &x){ x = 99; }

int main() {
    int a = 1, b = 1, c = 1;
    byValue(a);
    byPointer(&b);
    byReference(c);
    cout << a << " " << b << " " << c << "\n";   // 1 99 99
    return 0;
}

Look at the call sites. byPointer(&b) shouts that something may change. byReference(c) looks identical to byValue(a). That is the one real cost of references: you cannot tell from the call whether the argument gets modified. You have to read the function signature.

const reference: the workhorse

This is the most useful pattern in everyday C++.

C++
#include <iostream>
#include <vector>
using namespace std;

// copies the whole vector on every call: slow
int sumSlow(vector<int> v) {
    int s = 0;
    for (int x : v) s += x;
    return s;
}

// no copy, and const promises not to modify: fast and safe
int sumFast(const vector<int> &v) {
    int s = 0;
    for (int x : v) s += x;
    return s;
}

int main() {
    vector<int> big(1000000, 1);        // one million ones
    cout << sumSlow(big) << "\n";       // 1000000, after copying 4 MB
    cout << sumFast(big) << "\n";       // 1000000, copying nothing
    return 0;
}

sumSlow copies four megabytes on every call. sumFast passes what is effectively an address. For a vector, string, map or any class, always take a const & unless you need a copy. For an int, double or char, pass by value — copying 4 bytes is cheaper than the indirection.

💡

The rule of thumb: const T& for anything bigger than a pointer that you only read. T& when the function must modify the caller's object. Plain T for small built-in types and when you genuinely want your own copy to modify.

Reference in a range-for loop

C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};

    for (int x : v) x *= 10;            // modifies copies: no effect
    for (int n : v) cout << n << " ";
    cout << "\n";                       // 1 2 3

    for (int &x : v) x *= 10;           // modifies the real elements
    for (int n : v) cout << n << " ";
    cout << "\n";                       // 10 20 30

    for (const int &x : v) cout << x << " ";   // read only, no copy
    cout << "\n";
    return 0;
}

That single & is the difference between a loop that works and a loop that silently does nothing. When a "modify every element" loop appears to have no effect, this is why.

Returning a reference

C++
#include <iostream>
#include <vector>
using namespace std;

// SAFE: the vector outlives the call
int &at(vector<int> &v, int i) { return v[i]; }

// UNSAFE: local dies at the closing brace
// int &bad() { int x = 5; return x; }

int main() {
    vector<int> v = {1, 2, 3};
    at(v, 1) = 50;                   // assigning to a function call
    cout << v[1] << "\n";            // 50
    return 0;
}

Returning a reference to a local variable is the same bug as returning the address of a local in C, with the same consequence: a dangling reference to a destroyed stack frame. The compiler warns; listen to it.

When you still need pointers

References cannot do three things, and each is a real use case.

  • Be null. A function parameter that is optional needs a pointer, so nullptr can mean "not supplied".
  • Be re-pointed. Walking a linked list needs node = node->next. A reference cannot move.
  • Own heap memory. new, delete and smart pointers work with pointers.

Everything else — parameters, loop variables, avoiding copies — should use references.

⚠️

Use nullptr, not NULL, in C++. NULL is the integer 0 in disguise, so with overloaded functions f(NULL) can pick f(int) instead of f(char*). nullptr has its own type and always means "pointer to nothing".

Summary table

Pointer Reference
Can be null yes no
Must be initialised no yes
Can be re-assigned to another object yes no
Syntax to use *p r
Address-of at call site f(&x) f(x)
Arithmetic (p + 1) yes no