Quick Answer

A reference is another name for an existing object. It must be initialised, can never be null, and can never be made to refer to something else. A pointer is a variable holding an address, so it can be null, reassigned and stored in containers. Pass small values such as int and double by value, pass large objects by const reference to avoid copying, and use a pointer when the argument is genuinely optional or when you need to move it along.

The copy you did not ask for

Start with the version of this topic that actually costs marks in a coding round, because it is not a syntax question at all.

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

void printAll(std::vector<std::string> names) {   // copies the whole vector
    for (auto n : names) {                        // copies each string again
        std::cout << n << "\n";
    }
}

This compiles without a warning and prints the right answer. It also copies the entire vector into the function, performing a heap allocation for every string in it, and then copies each string a second time on every iteration of the loop. For a vector of city names read from input, that is two rounds of allocation for work that should have touched nothing.

The corrected version adds nothing but a const and an &, in two places:

void printAll(const std::vector<std::string>& names) {
    for (const auto& n : names) {
        std::cout << n << "\n";
    }
}

Now nothing is copied. The function receives a reference to the caller's vector, and each loop iteration binds a reference to an existing string. The const promises you will not modify anything, so a caller can pass a temporary and a reader can see at a glance that this function only observes.

This is the single highest-value habit in C++: by default, pass and loop over anything larger than a machine word by const reference. C++ copies by default because copying is the safe choice, not the cheap one. Python and Java hand you a handle to the object, so the same code there costs nothing, and people arriving from those languages write the slow version without realising a copy happened at all.

What a reference actually is

A reference is an alias. After int& r = a; the names r and a refer to the same object, and there is no way to tell them apart afterwards. That leads directly to the behaviour most beginners get wrong:

int a = 5, b = 9;
int& r = a;

r = b;               // this ASSIGNS 9 into a. It does not rebind r.
std::cout << a;      // prints 9
std::cout << &r;     // the address of a, forever

People expect r = b to make r refer to b, the way reassigning a pointer works. It cannot. Once a reference is bound it is bound for its whole life, and every operation on it is an operation on the original object.

Everything else follows from that one rule:

  • A reference must be initialised. int& r; does not compile, because an alias for nothing is meaningless.
  • A reference cannot be null. There is no nullptr equivalent, so a function taking int& never needs a check for absence.
  • There is no reference arithmetic. ++r increments the value, not an address.
  • You cannot make an array of references or store references directly in a std::vector; use std::reference_wrapper or pointers for that.
  • sizeof(r) reports the size of the referred-to type, not the size of a pointer.

Under the bonnet a reference is usually implemented as a pointer that is dereferenced automatically, but the standard does not require it to occupy any storage at all, and for a simple local alias the compiler normally emits nothing. Treat that as an implementation detail. At the language level a reference is a name, and a pointer is a value.

Pass by value, by reference or by pointer

Three ways to hand data to a function, with a fairly mechanical rule for choosing between them.

By value when the type is small and cheap to copy: int, double, char, an enum, a pointer, or a small struct of a few such members. The function gets its own copy, so the caller's variable is untouched. Passing an int by const reference is usually a small pessimisation, since the reference adds an indirection where the value would have fitted in a register.

By const reference when the object is large and the function only reads it: std::string, std::vector, maps, and your own classes. No copy, and const documents the intent.

By non-const reference when the function must modify the caller's object and that object always exists.

void addBonus(int& marks) { marks += 5; }              // must exist
void addBonus(int* marks) { if (marks) *marks += 5; }  // may be absent

int main() {
    int score = 40;
    addBonus(score);    // reference version: no visible clue it changes score
    addBonus(&score);   // pointer version: the & announces it at the call site
    return 0;           // score is now 50
}

By pointer when the argument is genuinely optional, when the function needs to reseat what it points at, or when you are talking to a C API. Some house styles use a pointer for every output parameter precisely so that the & at the call site warns the reader that the variable is about to change. Others prefer non-const references and rely on the function name. Both are defensible; pick one and be consistent within a codebase.

There is a fourth case people miss. If the function is going to keep a copy, take the parameter by value and std::move it into place. That way a caller passing a temporary gets a move instead of a copy, and a caller passing a named object pays one copy either way.

const references, temporaries and dangling

A const reference does something a non-const one cannot: it can bind to a temporary, and binding extends that temporary's life to the life of the reference. That is why void greet(const std::string& name) accepts greet("Anita"), even though the string literal has to be turned into a temporary std::string first. A plain std::string& parameter would reject the same call.

Lifetime extension has limits, and both of them bite in real code. It does not survive being returned, and it does not apply to reference members initialised in a constructor.

const std::string& bad() {
    std::string s = "Pune";
    return s;            // s is destroyed here; the caller gets a dangling ref
}

Compilers usually warn about that exact shape. They cannot warn about the more common one, where the reference outlives its target because a container moved underneath it:

std::vector<int> v{1, 2, 3};
int& first = v[0];

v.push_back(4);      // may reallocate and free the old buffer
first = 99;          // writing to memory the vector no longer owns

Nothing here is null, so no null check would have caught it. This is the fundamental difference between a reference and a copy: a reference is only as valid as the object behind it. If you hold one across anything that can destroy, resize or reallocate the owner, you have a bug that will show up as corrupted data long before it shows up as a crash.

The practical rule is that references are for the duration of a call. Take them as parameters, bind them to loop elements, use them and let them go. When you need to store a link to something for later, store a pointer, an index, or a weak_ptr, so that the possibility of the target disappearing is visible in the type.

When a pointer is the right answer

References are the better default for parameters, but there are jobs they cannot do at all.

Optional values. A reference must refer to something, so absence has to be expressed some other way. A pointer with nullptr is the traditional answer; std::optional is the modern one when you are returning a value rather than referring to one.

Walking a structure. A traversal reassigns its cursor on every step, which a reference cannot do:

struct Node { int value; Node* next; };

int length(const Node* head) {
    int n = 0;
    for (const Node* p = head; p != nullptr; p = p->next) {
        ++n;
    }
    return n;
}

Try to write that with a reference and you are stuck at the first element, because p = p->next would assign a whole node over the head instead of moving on. Linked lists, tree traversal and iterator-style code all need pointers or iterators for this reason.

Storing links. You cannot put references in a std::vector. Collections of polymorphic objects hold pointers, and in modern code that usually means std::vector<std::unique_ptr<Shape>> so the ownership is explicit.

Talking to C. Every C library function takes pointers, including buffers and out-parameters, so anything below the C++ layer is pointer territory.

The convention that ties it together in modern C++ is about ownership rather than syntax. A unique_ptr or shared_ptr says this code owns the object and will destroy it. A raw pointer says this code is only looking, and might be looking at nothing. A reference says this code is only looking, and there is definitely something there. If you pick the one whose promise matches what your function actually does, readers get the answer without opening the implementation.

Frequently Asked Questions

Is a reference just a pointer that is automatically dereferenced? That is close to how compilers usually implement it, but not what the language guarantees. A reference has no independent identity: you cannot take the address of the reference itself, reseat it, or make an array of references, and the standard does not require it to occupy any storage. Treating it as a name for an object rather than a variable holding an address explains its behaviour far better.
Should I always pass by const reference instead of by value? No. For types that fit in a register, such as int, double, char, bool and pointers, passing by value is at least as fast and often faster, because a reference adds an indirection the copy avoided. Use const reference for strings, vectors, maps and user-defined classes of any real size. A rough guide is to pass by value anything the size of two pointers or smaller that is cheap to copy.
Why does my reference become invalid after push_back? Because a vector stores its elements in one contiguous buffer, and when that buffer is full push_back allocates a larger one, moves the elements and frees the old memory. Any reference, pointer or iterator that pointed into the old buffer now refers to freed memory. Either re-fetch the reference after the insertion or call reserve first so no reallocation happens.
Can a reference be null in C++? Not in valid code. There is no way to create a null reference directly, which is exactly why a function taking a reference does not need a null check. You can construct one indirectly by dereferencing a null pointer and binding the result, but that is already undefined behaviour at the point of the dereference, so it proves nothing about references.
Which one do interviewers expect me to use? They mostly want to see that you can justify the choice. Use const reference for read-only parameters of non-trivial types, non-const reference for in-out parameters that always exist, and pointers where absence or reseating is part of the design. If you say you passed a vector by const reference to avoid a copy, you have shown the understanding the question was aimed at.