Pass by Value and Pass by Reference

Hi everyone ✋

In the previous post we learned the very basics of pointers ─ the address-of operator, the dereference operator, and the asterisk operator. If you haven’t read that one yet, go through it first because we will be using all of those concepts here.

Today we will talk about something that confuses a lot of beginners ─ pass by value and pass by reference.

So, what does “passing” even mean?

Whenever we call a function and hand it some data, we are passing that data to the function. Now the real question is ─ when we pass data to a function, does the function get the actual variable, or does it get a copy of it?

That single question is the whole topic of today.

Let’s take a deep dive 🦴

The story of two friends

To understand this properly, let’s imagine a small story.

Suppose soudha has a notebook with the number 24 written in it. Her friend imran asks her, “Hey, can I borrow your number?”

Now soudha has two choices ─

  • She can photocopy the page and give imran the photocopy. If imran scribbles all over it, soudha’s original notebook stays untouched. This is pass by value.
  • She can hand imran the original notebook itself. Now if imran scribbles in it, soudha’s notebook is changed forever. This is pass by reference.

Keep this story in mind. Everything below is just this story written in C++.

Pass by Value

When we pass a variable by value, the function receives a copy of the variable, not the original one. Whatever the function does to that copy, the original variable stays completely unaffected.

Let’s look at a classic example ─ a function that tries to swap two numbers.

#include <iostream>
using namespace std;

// here a and b are receiving copies of the original variables
void swapNumbers(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;

    cout << "inside function a: " << a << endl;
    cout << "inside function b: " << b << endl;
}

int main()
{
    int soudha = 24;
    int imran = 121;

    cout << "before swap soudha: " << soudha << endl;
    cout << "before swap imran: " << imran << endl;

    // passing the variables by value
    swapNumbers(soudha, imran);

    cout << "after swap soudha: " << soudha << endl;
    cout << "after swap imran: " << imran << endl;

    return 0;
}

The output will be ─

before swap soudha: 24
before swap imran: 121
inside function a: 121
inside function b: 24
after swap soudha: 24
after swap imran: 121

Wait 🤔 the swap worked inside the function but soudha and imran are still the same outside?

Yes, exactly. And this is the photocopy situation.

When we called swapNumbers(soudha, imran), the compiler made fresh copies of soudha and imran and named them a and b. The function happily swapped a and b, but those were just copies living in different memory. The original soudha and imran never knew anything happened.

Let’s see it in memory. Each column represents one variable’s memory.

soudha imran a (copy) b (copy)
24 121 24 121

After the swap inside the function ─

soudha imran a (copy) b (copy)
24 121 121 24

See? Only the copies a and b changed. The originals are untouched. The moment the function ends, a and b are destroyed and all that work is thrown away.

So pass by value is safe but sometimes useless ─ when we actually want the function to change the original, it simply can’t.

Pass by Reference

Now let’s hand over the original notebook.

In C++ there are two ways to pass the original variable to a function ─ using pointers and using references. Since we just learned pointers, let’s do the pointer way first. This is where our previous post pays off 🥳

Pass by reference using pointers

Instead of passing the variables, we pass their addresses. And to receive addresses, the function parameters must be pointer variables.

#include <iostream>
using namespace std;

// here a and b are pointer variables
// they receive the addresses of the original variables
void swapNumbers(int *a, int *b)
{
    // dereferencing to access and change the actual values
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main()
{
    int soudha = 24;
    int imran = 121;

    cout << "before swap soudha: " << soudha << endl;
    cout << "before swap imran: " << imran << endl;

    // passing the addresses of the variables
    swapNumbers(&soudha, &imran);

    cout << "after swap soudha: " << soudha << endl;
    cout << "after swap imran: " << imran << endl;

    return 0;
}

The output will be ─

before swap soudha: 24
before swap imran: 121
after swap soudha: 121
after swap imran: 24

🥳🥳 Now the swap actually worked outside the function too.

Let’s understand why. When we called swapNumbers(&soudha, &imran), we did not pass copies of the values. We passed the addresses of soudha and imran. The pointer parameters a and b are now pointing directly at the original variables.

soudha imran a (pointer) b (pointer)
24 121 address of soudha address of imran

So when we wrote *a inside the function, we were not touching a copy ─ we were reaching through the pointer and changing soudha’s actual value. Same for *b and imran.

This is exactly the dereferencing trick we learned in the previous post. *a = *b means “put the value stored at b’s address into the location a is pointing to.”

After the swap ─

soudha imran a (pointer) b (pointer)
121 24 address of soudha address of imran

The pointers themselves never changed where they point. We only changed the values at those addresses. And since those addresses belong to soudha and imran, the change is permanent.

Pass by reference using references

C++ gives us a cleaner way that doesn’t make us write & and * everywhere ─ the reference.

A reference is basically a nickname for an existing variable. If soudha also goes by the nickname a, then doing something to a is doing something to soudha. There is no copy, no address juggling.

We create a reference parameter using the & symbol in the parameter list.

#include <iostream>
using namespace std;

// here a and b are references
// a is just another name for the variable passed in, same for b
void swapNumbers(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

int main()
{
    int soudha = 24;
    int imran = 121;

    cout << "before swap soudha: " << soudha << endl;
    cout << "before swap imran: " << imran << endl;

    // we pass the variables normally, no & needed here
    swapNumbers(soudha, imran);

    cout << "after swap soudha: " << soudha << endl;
    cout << "after swap imran: " << imran << endl;

    return 0;
}

The output will be ─

before swap soudha: 24
before swap imran: 121
after swap soudha: 121
after swap imran: 24

Same result as the pointer version, but notice how clean the code looks 🥳

Inside the function we wrote plain a and b ─ no asterisks, no dereferencing. And when calling it, we wrote swapNumbers(soudha, imran) ─ no address-of operator. That’s because a and b are not separate variables at all. They are just another name for soudha and imran.

One thing that confuses beginners here ─ this & is not the address-of operator. When & is used in a parameter declaration like int &a, it means “a is a reference.” When & is used in front of a variable like &soudha, it means “address of soudha.” Same symbol, different jobs ─ exactly like the three meanings of * we discussed last time.

Quick comparison

Let’s put all three side by side so the difference is crystal clear.

Pass by Value Pass by Reference (pointer) Pass by Reference (reference)
What is passed a copy of the value the address of the variable the variable itself (as a nickname)
Parameter syntax int a int *a int &a
Call syntax func(soudha) func(&soudha) func(soudha)
Inside function use a directly dereference with *a use a directly
Changes the original? No Yes Yes
Extra memory for a copy? Yes No No

Okay but where do we actually use this? 🤔

So far the swap example is great for learning, but you might be thinking ─ “when am I ever going to swap two numbers in real life?”

Fair question. Let’s look at three small real-world situations where pass by value vs pass by reference is not just theory ─ it actually decides whether your program works or breaks, and whether it is fast or slow.

Real-world example 1 ─ Updating a player’s score in a game 🎮

The problem

Imagine soudha is building a small game. Every time the player collects a coin, the score should go up by 10. So she writes a helper function ─

#include <iostream>
using namespace std;

// score is passed by value ─ this is a copy!
void collectCoin(int score)
{
    score = score + 10;
}

int main()
{
    int playerScore = 0;

    collectCoin(playerScore);
    collectCoin(playerScore);
    collectCoin(playerScore);

    cout << "final score: " << playerScore << endl;

    return 0;
}

She expects the score to be 30 after collecting three coins. But the output is ─

final score: 0

😱 The score never moved. The player collected three coins and got nothing!

This is the photocopy problem again. Each call to collectCoin got a copy of playerScore. The function added 10 to the copy, then the copy was destroyed when the function ended. The real playerScore back in main never changed.

The solution

We need the function to modify the actual score, not a copy. So we pass by reference.

#include <iostream>
using namespace std;

// score is now a reference ─ another name for the real variable
void collectCoin(int &score)
{
    score = score + 10;
}

int main()
{
    int playerScore = 0;

    collectCoin(playerScore);
    collectCoin(playerScore);
    collectCoin(playerScore);

    cout << "final score: " << playerScore << endl;

    return 0;
}

Now the output is ─

final score: 30

🥳 Because score is now just a nickname for playerScore, every + 10 lands directly on the original variable. The coins finally count.

Real-world example 2 ─ Returning more than one value 📦

The problem

A C++ function can only return one value. But real problems often need more than one answer.

Suppose imran wants a function that takes a total number of seconds and breaks it into minutes and seconds. For example, 150 seconds → 2 minutes and 30 seconds. That’s two answers, but return only gives us one. How do we get both out of a single function?

The solution

We pass minutes and seconds by reference and let the function fill them in. The function doesn’t return anything ─ instead it writes the results directly into the caller’s variables.

#include <iostream>
using namespace std;

// minutes and seconds are references
// the function writes its answers into them
void breakTime(int totalSeconds, int &minutes, int &seconds)
{
    minutes = totalSeconds / 60;
    seconds = totalSeconds % 60;
}

int main()
{
    int total = 150;
    int mins = 0;
    int secs = 0;

    // mins and secs will be filled by the function
    breakTime(total, mins, secs);

    cout << total << " seconds = " << mins << " minutes and " << secs << " seconds" << endl;

    return 0;
}

The output will be ─

150 seconds = 2 minutes and 30 seconds

🥳 Notice that totalSeconds was passed by value ─ because the function only needs to read it, not change it. But minutes and seconds were passed by reference, because those are the variables we want the function to fill in.

This pattern ─ some parameters by value (inputs), some by reference (outputs) ─ is extremely common in real C++ code.

Real-world example 3 ─ Processing a big list of students 🐶➡️🚀

The problem

Now let’s talk about speed, because this is where pass by value can silently hurt you.

Suppose anik has a list of one million student names stored in a vector<string>, and he just wants to count how many of them passed. He writes ─

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// the whole vector is passed by value ─ a full copy!
int countStudents(vector<string> students)
{
    return students.size();
}

int main()
{
    vector<string> students(1000000, "passed");

    cout << "total students: " << countStudents(students) << endl;

    return 0;
}

This code works and prints the right answer. But there is a hidden disaster ─ when we passed students by value, C++ copied all one million strings into the function. That’s a huge amount of memory and time wasted, just to count them. The function didn’t even change anything!

This is like photocopying an entire 1000-page book just so someone can count the pages. Pure waste.

The solution

We pass the vector by reference so no copy is made. But since we only want to read the list and not change it, we also add const to promise we won’t touch it.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// const reference ─ no copy is made, and we promise not to modify it
int countStudents(const vector<string> &students)
{
    return students.size();
}

int main()
{
    vector<string> students(1000000, "passed");

    cout << "total students: " << countStudents(students) << endl;

    return 0;
}

Same correct output ─ but now zero copies are made. The function looks straight at the original list, counts it, and finishes. Fast and memory-friendly 🚀

This is the golden rule for big objects ─ pass by const reference when you only need to read. You get the speed of pass by reference (no copy) and the safety of pass by value (can’t accidentally modify it).

So which one should we use? 🤔

A few simple rules of thumb ─

  • If the function only needs to read small data (like an int or a char) and should not change the original, pass by value. It’s safe and cheap.
  • If the function needs to modify the original variable (like the game score, or the minutes and seconds), pass by reference (pointer or reference).
  • If the variable is big (like a large object, a vector, or a string), passing by value copies the whole thing which is slow and wasteful. In that case we pass by const reference ─ even just for reading ─ to skip the copy while still protecting the original.
// passing a big object by reference but promising not to change it
void printData(const vector<int> &numbers)
{
    for (int x : numbers)
    {
        cout << x << " ";
    }
}

This way we get the speed of pass by reference (no copy) and the safety of pass by value (can’t modify). Best of both worlds 🥳

A common mistake

Beginners often mix up the syntax between the pointer version and the reference version.

// pointer version ─ parameter is a pointer, so we must dereference
void swapNumbers(int *a, int *b)
{
    int temp = *a;   // correct, dereferencing
    a = b;           // WRONG ─ this just swaps the pointers, not the values
}

If we forget the * and write a = b, we are only changing where the pointers point, not the actual values. The original variables stay untouched and the swap silently fails. So in the pointer version, always remember ─ to touch the value, dereference with *.

In the reference version this mistake can’t happen, because there is no pointer to accidentally reassign. That’s one more reason references are loved in C++.

Congratulations 🥳🥳🥳
We now understand the difference between pass by value and pass by reference, the two different ways to pass by reference, and ─ more importantly ─ when to use each one in real code.

In the next post we will talk about pointers and arrays ─ how they are deeply connected and why arrays “decay” into pointers.

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *