Do You Get My Reference?

Edited By: Prof. Garth Santor

Introduction

For the purpose of explanation, we will imagine that our RAM is a library, in which books (memory spaces) are located(allocated) in specific aisles and shelves (memory addresses). There’s also a few points that need to get established:

First, is the idea that every variable (and function, but we’ll get to that later) created by our program has a memory address that indicates where it exists in RAM. You can read my Heap & Stack post to learn more.

Second, is that data types signify different memory sizes, as well as different representations of the data that has been stored. For example, an int is typically 4 bytes, whereas a char is 1 byte. On the other hand, both a float and an int are 4 bytes, but they are represented very differently in memory because of the floating point. It’s like the difference between a one-bedroom apartment, and a one-bedroom house. They might be the same size or room number, but their representation is very different.

Let’s first look at a visual representation of this before moving on:

int vs float with notes.png

As you can see, i and f both take up 4 bytes on the stack. The evidence of that is that at their memory addresses, only 8 hex values are taken up (each hex digit is a nibble - 4 bits). The cc bytes in between are debugging memory guards that Visual Studio adds in, to minimize the risks of overflow. But in release mode, the variables will exist back to back on the stack. And even though we have stored the same value to them: 2, they are represented very differently in RAM because of their different types.

Every variable has a memory location that can be accessed by prefixing the variable with the '&' operator.

With that said, we can safely say that calling i or f will give us the value that is stored in that memory location (in this case: 2), but calling &i or &f will give us the actual memory location in which these values reside. It’s like asking who lives in a house versus what the address of the house is.

So why should we know this, and when does it come in handy?

Using the reference of a variable instead of its actual value is typically used when passing it in to a function, especially when we have a large block of memory. Think of this: you want to refer a friend to a book at the library, and the plausible way to do that would be to tell them the address of that book at the library.

Assuming we have the following program:

int triple_num(int num) {    
    num *= 3;
    return num;
}

int main() {
    int i = 2;
    i = triple_num(i);
}

In this case, passing in i to triple_num will copy over that value, and allocate a new int with the value of 2. So in order to retrieve our result, we will need to assign the return value of triple_num to i. Not only is that inconvenient, but we will also get a performance hit if the parameter we are passing in by value is significantly larger than an int. What this is doing is the equivalent of the following: you want to replace the cover on a book, so you copy it, send it to the printing company, who will then send you yet another book with the new cover on it, which you will replace with your old book.

Let’s look a little closer at what’s going on:

debugging pass by value edit.png

We can clearly see where i was allocated on the stack: 0x0098FA59, with the value of 2. After that, our function was allocated with its own stack that included yet another value of 2 which was copied over to 0x0098F984. So we now have two instances of our object, and any manipulations that triple_num does on num will not affect i in any way:

debugging pass by value 2 edit.png

What we can do to avoid this resource-heavy operation, is to simply pass in the address of our variable, and the function will go at that memory location and manipulate it accordingly. We also wouldn’t have to reassign it after the function has done its work. Follow this piece of code:

void triple_num (int& num) {
    num *= 3;
}

int main() {
    int i = 2;
    triple_num(i);
}

The C++ Committee has decided to do this in a very unconventional way. Basically, the caller of the function can’t really know if they’re passing in the value of the variable, or its address. It’s up to the function to decide what it’s going to accept. So in this case, you show the printing company your book, and they decide to take the location of it, instead of copying it. The only change we did in code to achieve that, is to add an & to the parameter type of the function.

Let’s also look a little closer at what is happening here:

debugging pass by reference edit.png

In this case, num is actually the memory location of i. So when we pass over line 6, watch what happens:

debugging pass by reference 2 edit.png

Seeing that, you will be tempted to say references are awesome! Less variables on the stack, changes to variables are made without reassigning, so our code will be faster! Right? Well, there’s one small thing to note here: memory addresses on an x86 machine are 4 bytes long, so as big as an int. On an x64 machine, memory addresses are 8 bytes long. Which means that passing in a reference to an int in an x64 machine will actually take up more space on the stack than the value itself. Adding to that, passing in a reference to a char will always be more resource-heavy than the value itself, since a char is only 1 byte. However, passing in a reference to a custom class instance that’s maybe 50 bytes, or even a reference to a vector, will always be more efficient than copying over that object. So be wary of these details when attempting to enhance the performance of your code.

Java will automagically pass a POD by value, and a class object by reference. Developers have no control over that.

Moral of the story, POD (Plain Old Data) variables that take up one slot in memory (i.e. not a data structure) should typically be passed in by value, unless the need arises for a reference. Constructed objects and containers, however, should always be passed by reference.

One last thing to note about this, is that the reference of i: 0x00BAFC5C was allocated on the call stack of triple_num, and was accessed by manipulating its value, which is the reference of i. There was no distinction in the method of access for a variable versus a reference to a variable.
This will be different for pointers, as is discussed in the next post.