Skip to content
C++

Pass by Value, Reference, and Pointer

When you call a function and provide an argument, C++ has three fundamental mechanisms for how the argument is delivered: by value (the function gets a copy), by reference (the function gets an alias to the original), or by pointer(the function gets the memory address of the original). Each mechanism has a different cost, different safety properties, and different appropriate use cases. Choosing the right one is one of the most important decisions you make when writing a function signature.

Pass by value

When you pass by value, the function receives a copyof the argument. The function can modify its local copy freely — those modifications never affect the caller's variable. Pass by value is the default in C++ and is appropriate for small, cheap-to-copy types like int, double, bool, and char. For larger objects like strings or vectors, making a copy on every call is wasteful.

void increment(int n)   // n is a copy
{
    n++;                // modifies the copy, not the original
}

int x = 5;
increment(x);
std::cout << x;   // still 5 — x was not modified

// Pass by value IS appropriate for small types:
double square(double x) { return x * x; }

// Pass by value is EXPENSIVE for large types:
void process(std::vector<int> v) { ... }  // copies entire vector!

Pass by reference

When you pass by reference, the function receives an aliasfor the original variable. Any modification to the parameter directly modifies the caller's variable. Adding constin front gives you a read-only alias — you get the efficiency of not copying without the ability to accidentally modify the caller's data. The const T& idiom is the most common parameter style in C++ for anything larger than a pointer.

// Mutable reference — CAN modify the caller's variable
void increment(int& n)  // & makes this a reference
{
    n++;   // modifies the original
}

int x = 5;
increment(x);
std::cout << x;  // 6 — x was modified through the reference

// const reference — CANNOT modify, but avoids copying
void printVector(const std::vector<int>& v)
{
    for (int x : v) std::cout << x << " ";
}

std::vector<int> data = {1, 2, 3, 4, 5};
printVector(data);   // no copy made — fast

A const T& parameter also accepts temporaries and literals, which a plain (non-const) reference does not:

void greet(const std::string& name) {
    std::cout << "Hello, " << name;
}

greet("Alice");           // const ref accepts a string literal
greet(std::string("Bob")); // const ref accepts a temporary

// void greet(std::string& name) would reject both of the above
// because you cannot bind a non-const reference to a temporary

Pass by pointer

When you pass by pointer, the function receives the memory address of the original variable. The function must dereference the pointer with * to access the value. Passing a pointer has the same performance characteristics as passing a reference, but with added capabilities: the pointer can be null (expressing “no argument provided”) and can be rebound to point at different objects. The caller must explicitly take the address with &.

void increment(int* p)   // pointer parameter
{
    if (p != nullptr) {   // always check before dereferencing
        (*p)++;
    }
}

int x = 5;
increment(&x);        // caller takes address explicitly
std::cout << x;       // 6

increment(nullptr);   // valid — pointer can be null; function guards against it

The old C-style approach was to use pointers for all non-value parameters. Modern C++ prefers references for required parameters (callers can't pass null by accident) and pointers only when nullability is intentional (optional parameters, optional results).

Comparison

MechanismSignatureCan modify caller?Can be null?When to use
By valueint f(int x)No (copy)NoSmall cheap types: int, double, char
By const refint f(const T& x)NoNoLarge read-only objects: string, vector, struct
By mutable refvoid f(T& x)YesNoOutput parameter or in-out modification
By pointervoid f(T* p)Yes (via *)YesOptional parameter; C APIs; rebinding needed

Putting it together

// By value — function doesn't need to change x
double fahrenheitToCelsius(double f) {
    return (f - 32.0) * 5.0 / 9.0;
}

// By const reference — large string, read only
void logMessage(const std::string& msg) {
    std::cout << "[LOG] " << msg << "\n";
}

// By mutable reference — swap two values
void swapInts(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// By pointer — optional logging
void compute(int x, Logger* log = nullptr) {
    if (log) log->write("computing...");
    // ...
}

int a = 3, b = 7;
swapInts(a, b);
std::cout << a << " " << b;  // 7 3

Key rules to remember

Default to const T& for large objects you only read

const T& is the most common parameter style in C++ for strings, vectors, and structs. It avoids copying and prevents accidental modification.

Use T& (non-const reference) when the function intentionally modifies the caller's variable

The & in the signature signals to the caller that their variable will be changed. This makes the interface self-documenting.

Use pass-by-value for small built-in types and when the function needs its own copy

Passing int, double, bool, char by value is idiomatic and efficient — the copy is just a register. For objects, prefer references.

Use a pointer when null is a valid input — check before dereferencing

A null pointer passed to a function that expects a non-null pointer causes a crash. References cannot be null, which makes them safer for required arguments.

Sign in to track progress