Pointers and References
by Dhruv MehtaDescription
This is a simple explanation of pointers and references going into their basics, how they are used and the important differences between them. We also go over important concepts like pass by value, pass by pointer, pass by reference, all of which will be important to understand for Exam 0.
How is data stored in the computer?
To be able to understand how Pointers and References work we first need to understand how data is stored in the computer. Every time we instruct the computer to store some data, the computer finds a memory location and stores the data there. So, any data stored in the computer has 3 essential properties:
- Name: The variable name used to store the data.
- Value: The data itself i.e. the information to be stored.
- Memory Location: The location in memory in the computer where the data has been stored. The location is usually denoted in some sort of hexadecimal format like
0x0010
or0x0210
.
Let us try to look at this with an example:
Look at a simple line where we are telling the computer that it should store the value 2
in a variable called a
:
int a = 2;
So, the memory block looks like this:
Name | Value | Memory Location |
---|---|---|
a | 2 | 0x0010 |
Note that I am not talking about stack vs heap memory here. For simplicity, I will just be talking about memory in general.
References
The simplest way to think about a reference is as a nickname or an alias. All a reference does is tell the computer that a particular memory block can be referred to by multiple names. The way we denote a Reference is using the &
sign. When we see the &
on the left-hand side of the =
sign of an assignment statement, we know that a reference is being used.
Let us take a look at an example:
int a = 2;
int& x = a;
Here, x
is a reference to a
which means that data of a
can also be referred to by x
. This is what the memory block looks like after this code has been executed:
Name | Value | Memory Location |
---|---|---|
a or x | 2 | 0x0010 |
So, creating a reference does not create new memory blocks or modify the data in any way except for just telling the computer that there is now another name with which we can use the memory block.
So, a statement like a = a + 2
will be the exact same as a statement saying x = x + 2
since it is the exact same memory block with just different names. For example, some people call me Dhruv and some people call me Mr. Mehta but it is still me that they are talking about. So the data (in this case, me) is the same in both cases.
Basic Examples using References:
Example 1
#include <iostream>
int main() {
int number = 5;
int& ref = number;
ref = 10; // Modifying 'number' through the reference 'ref'
std::cout << "Number: " << number << std::endl;
return 0;
}
Output:
Number: 10
Explanation: In this example, a variable is being created called “number” with the value 5. So, a memory block is created with the name “number” and value as 5. In the next line, we are creating a reference to called ref. So, we are basically telling the computer that “ref” is another name that we can use for the same memory block. So, when we use “ref” or “number” we are referring to the same memory block. So any changes to ref change the value of the memory block.
Example 2:
#include <iostream>
int main() {
int number = 5;
int& ref = number;
int x = number;
ref = 10; // Modifying 'number' through the reference 'ref'
std::cout << "Value in number: " << number << std::endl;
std::cout << "Value in x:" << x << std::endl;
return 0;
}
Output:
Value in number: 10
Value in x: 5
Explanation: Here, ref and number are referring to the same memory block. But, when we assign x
, since x
is not a reference, x
is not a name for the same memory block but it creates a copy of the original one, so any changes in the original one will not reflect in x
since it is a completely different memory block with its own value, location, and name.
Pointers
A pointer is a variable that stores the address or location of a memory block. So, when we create a pointer another memory block is created which contains, as its data, the memory location of another block.
Let us look at these statements:
int a = 2;
int* x = &a;
(Note that here the &
is the “Address of” operator and not a Reference. The two are completely different and independent operations.)
So, here the memory is stored in the following way:
Name | Value | Memory Location |
---|---|---|
a | 2 | 0x0010 |
x | 0x0010 | 0x2180 |
So, the value (data stored) in memory block x
is the memory location of a
.
To simplify this even further, instead of using hexadecimal codes to denote memory locations, I am going to be using actual physical addresses.
Let us consider the following code:
int a = 2;
int* x = &a;
Name | Value | Memory Location |
---|---|---|
a | 2 | Siebel Center for Computer Science |
x | Siebel Center for Computer Science | Illini Union |
So, this means that x
is sitting at the Illini Union. However, x
knows that it can find a
at Siebel Center for Computer Science. So, since x
knows the location of a
, we can use x
to get to a
.
That way we can get to a
using x
is using the *
operator (Dereferencing). If we write some code like:
*x = *x + 3;
- The
*
operator here is used to access the memory block ofA
. - So what
*x
does is that it tells the computer to use the address thatX
is storing and use the memory block at that address (which is the memory block ofA
). - Let us look at the statement
*x = *x + 3
. - The
*x
on the left side of the=
sign is telling the computer to access the memory block ofA
and store in it the result of the statement on the right side of the=
sign. - On the right side of the
=
, the*x + 3
is telling the computer to find get the value stored in*X
which is theA
block and then add 3 to it. - So, what the statement
*X = *X + 3
is telling the computer to access theA
block, add 3 to it, and then store the result in theA
block.
Difference Between Reference and AddressOf Operators
I would highly recommend reading this section carefully because a lot of people get confused between the two and make many mistakes as a result. Before we go into this explanation, I just want to say that this is one of the things I hate most about C++. Why did they have to use the &
sign for both these operations when these operations do completely unrelated things?
The main difference between the usage of the &
in the Reference and AddressOf operations is that:
- If the
&
is being used on the left side of the=
sign then it is being used for Referencing. In this case, the operator is used to create a nickname for the existing memory block. - If the
&
is being used on the right side of the=
sign then it is being used as an AddressOf operator. In this case, the operator is used to get the address or the location of the memory block.
Again, the two are completely different and do very different things.
Let’s just recap with the earlier examples we were using:
int a = 2;
This line of code creates a memory block like this:
Name | Value | Memory Location |
---|---|---|
a | 2 | 0x0010 |
int& r = a;
int * p = &a;
After these two lines of code, the memory blocks look like this:
Name | Value | Memory Location |
---|---|---|
a or r | 2 | 0x0010 |
p | 0x0010 | 0x2180 |
Reference to Pointers
Okay, I know you are getting angry at me with how weird stuff
is getting but I guarantee this is as weird as it gets and once you read the explanation you’ll realize that this is not as bad as it sounds. Again, let’s look at some code and the corresponding memory blocks:
int a = 2;
int* p = &a;
Name | Value | Memory Location |
---|---|---|
a | 2 | 0x0010 |
p | 0x0010 | 0x2180 |
Now understand that p
is the name of the memory block and so, we can also give the memory block a nickname other than p
. That is all a reference to a pointer is. It is giving a nickname to a pointer variable.
It is written this way:
int* & pref = p;
This is implying that pref
is a nickname for the memory block of p
and the data type of memory block is an int*
.
The resultant memory blocks are:
Name | Value | Memory Location |
---|---|---|
a | 2 | 0x0010 |
p or pref | 0x0010 | 0x2180 |
Basic Examples using Pointers
Example 1
#include <iostream>
int main() {
int number = 5;
int* ptr = &number; // Declare a pointer and initialize it with the address of 'number'
std::cout << "Value of number: " << *ptr << std::endl;
std::cout << "Address of number: " << &number << std::endl;
std::cout << "Value stored in ptr: " << ptr << std::endl;
*ptr = 10;
std::cout << "Updated value of number: " << *ptr << std::endl;
return 0;
}
Output:
Value of number: 5
Address of number: 0x7fffe4c1aabc
Value stored in ptr: 0x7fffe4c1aabc
Updated value of number: 10
Explanation: Here we have a memory block with the name “number” which has the value 5 and it is stored at the memory address 0x7fffe4c1aabc
. We then create a pointer. This pointer is just another memory block with the name “ptr” which has the value 0x7fffe4c1aabc
and some other memory location. We then use the *
operator to use ptr
to access the memory block of “number” and so 5 gets printed out. Then we are printing out the memory location of the “number” memory block using the &
which is the AddressOf operator. In the next line, we are printing out what ptr
itself stores, so here we see that ptr
stores the memory location of “number” and so 0x7fffe4c1aabc
is printed out. We also see that we can update the value of a memory block using a pointer to it. So, here we do *ptr
to access the memory block and then assign a new value to it so it gets updated.
Example 2
#include <iostream>
int main() {
int number = 5;
int* ptr1 = &number; // Declare a pointer and initialize it with the address of 'number'
int* ptr2 = &number;
std::cout << "Value of block that ptr1 points to: " << *ptr1 << std::endl;
std::cout << "Value of block that ptr2 points to: " << *ptr2 << std::endl;
std::cout << "Value stored in ptr1: " << ptr1 << std::endl;
std::cout << "Value stored in ptr2: " << ptr2 << std::endl;
*ptr1 = 10;
std::cout << "Made an Update" << std::endl;
std::cout << "Updated Value of block that ptr1 points to: " << *ptr1 << std::endl;
std::cout << "Updated Value of block that ptr2 points to: " << *ptr2 << std::endl;
std::cout << "Value stored in ptr1: " << ptr1 << std::endl;
std::cout << "Value stored in ptr2: " << ptr2 << std::endl;
return 0;
}
Output:
Value of block that ptr1 points to: 5
Value of block that ptr2 points to: 5
Value stored in ptr1: 0x7FFFEA350A8B
Value stored in ptr2: 0x7FFFEA350A8B
Made an Update
Updated Value of block that ptr1 points to: 10
Updated Value of block that ptr2 points to: 10
Value stored in ptr1: 0x7FFFEA350A8B
Value stored in ptr2: 0x7FFFEA350A8B
Explanation: This is very similar to the previous example. Here, we create a memory block called number and assign it a value of 5. We then create 2 points ptr1
and ptr2
which point to the memory block of number. You can see that both the pointers have the same value since they are storing the location of the same memory block. Then we update the value of the memory block using ptr1
. We can then see using the print statements that the value of the memory block has been updated. We can see that both the pointers are still pointing to the same block. *ptr2
will print the updated value since ptr2
points to the same number block which has been updated.
Example 3
#include <iostream>
int main() {
int num1 = 5;
int num2 = 20;
int* ptr = &num1;
std::cout << "Value stored in num1: " << num1 << std::endl;
std::cout << "Value stored in num2: " << num2 << std::endl;
std::cout << "Value stored in ptr: " << ptr << std::endl;
ptr = &num2;
*ptr = 50;
std::cout << "Updated" << std::endl;
std::cout << "Value of num1: " << num1 << std::endl;
std::cout << "Value of num2: " << num2 << std::endl;
std::cout << "Value stored in ptr: " << ptr << std::endl;
return 0;
}
Output:
Value stored in num1: 5
Value stored in num2: 20
Updated
Value stored in num1: 5
Value stored in num2: 50
Explanation: Here, we are creating 2 memory blocks num1
and num2
which have the values 5 and 20 respectively. Then we create a pointer ptr
which points to the memory block num1
. We can see this in the first 3 lines of the output. Then, with the line ptr = &num2
, we are changing the value stored in ptr
and redirecting it to num2
. Now, ptr
stores the location of the memory block of num2
. So, when we dereference ptr
, we are given the num2
block. So, now, *ptr2 = 50
changes the value stored in the num2
block. We can see in the output that the value of num2
has been changed and ptr
now stores the location of num2
.
More Complex Examples using Pointers and References
Example 1
#include <iostream>
void modifyValue(int& ref) {
ref = 50; // Modify the value using the reference
}
int main() {
int num = 20;
std::cout << "Value of num: " << num << std::endl;
modifyValue(num); // Modify the value of 'num' using the reference
std::cout << "New value of num: " << num << std::endl;
return 0;
}
Output:
Value of num: 20
New Value of num: 50
Explanation: Here, we create a memory block called num
having a value of 20. We then pass num
to the modifyValue
. As you can see from the &
sign, the parameters of modifyValue
are references. What this essentially does is:
int& ref = num;
So, ref
is a reference to num
which means that it is just another name of the same memory block as num
. So, any changes made to ref
in modifyValue
change the memory block that num
and ref
refer to. So, when we print num
, it now shows the updated value. This is known as “pass by reference” since the parameters are references to the variables passed to the functions.
Example 2
#include <iostream>
void modifyValue(int* p) {
*p = 50; // Modify the value using the reference
}
int main () {
int num = 10;
std::
cout << "Old Value of Num: " << num << std::endl;
modifyValue(&num);
std::cout << "New Value of Num: " << num << std::endl;
}
Output:
Old Value of Num: 10
New Value of Num: 50
Explanation: Here, we create a memory block called num
having a value of 2. Then we pass the address of this memory block to the function modifyValue
which creates a pointer p
storing the location of the memory block num
. We then dereference p
to get access to the memory block of num
and then we change the value of that which means that num
memory block is now containing the new value 50. This is known as ‘pass by pointer’ when we pass the address of our memory block to a function.
Example 3
#include <iostream>
void modifyValue(int x) {
x = 50; // Modify the value using the reference
}
int main () {
int num = 10;
std::cout << "Old Value of Num: " << num << std::endl;
modifyValue(num);
std::cout << "New Value of Num: " << num << std::endl;
}
Output:
Old Value of Num: 10
New Value of Num: 10
Explanation: When we do not use pointers or references while passing a variable to a function, then what the function does is creates a duplicate/copy of the memory block which was passed to it. This new memory block contains the same data/value but it is separate from the old memory block. So, any changes made to the data in this new memory block do not affect the old memory block. This is known as ‘pass by value’.
Example 4
#include <iostream>
int main() {
int num = 5;
int* ptr1 = # // Pointer to an integer
int** ptr2 = &ptr1; // Pointer to a pointer to an integer
std::cout << "Value of num: " << num << std::endl;
std::cout << "Deref of ptr1: " << *ptr1 << std::endl; // Dereference ptr1
std::cout << "Double Deref of ptr2: " << **ptr2 << std::endl; // Dereference ptr2 twice
return 0;
}
Output:
Value of num: 5
Deref of ptr1: 5
Double Deref of ptr2: 5
Explanation: Here, we first create a memory block with the name num
and the value of 5. Then we create a pointer called ptr1
which points to the memory block. Then we create another pointer ptr2
which is pointing to the memory block containing ptr1
. So, we have ptr2
pointing to ptr1
which is pointing to num
. When we dereference ptr2
as (*ptr2)
we get the value stored in it i.e. ptr1
, and when we dereference this, we get the num
block. So, a **ptr2
is a double dereference which gets us from ptr2
to num
.
Example 5
#include <iostream>
int main () {
int num1 = 3;
int num2 = 5;
int* p = &num1;
int* & pref = p;
*p = 20;
pref = &num2;
*p = 10;
std::cout << "Num1: " << num1 << std::endl;
std::cout << "Num2: " << num2 << std::endl;
}
Output:
Num1: 20
Num2: 10
Explanation: Here, we first create 2 blocks of memory. First a block with the name num1
and value 3 and second block with a name num2
and value 5. Then we create another memory block by the name p
, which is a pointer which means that it stores the address of num1
. So, we can use the block p
to access num1
. Now, we create a reference “pref” of the block p
. So, now the block which had the name p
has another name pref
(note that p
and pref
are names of the same block of memory). Now we dereference p
. So, we get access to the block that it is pointing to which is num1
and we change the value of num1
to 20. Now we change pref
(which is the same block as p
) to point to num2
instead of num1
. So, now the block (which has the names p
and pref
) points to num2
. Now, we dereference pref
and get access to the num2
block and change the value to 10. So, num1
has a value 20 and num2
has a value 10.
Example 6
#include <iostream>
int main() {
int num1 = 3;
int num2 = 5;
int* p = &num1;
int* & pref = p;
pref = &num2;
*p = 10;
std::cout << "Num1: " << num1 << std::endl;
std::cout << "Num2: " << num2 << std::endl;
}
Output:
Num1: 3
Num2: 10
Explanation: This is a lot like the previous example with a small change to further clarify how references and pointers work. Here, we first create 2 blocks of memory. First a block with the name num1
and value 3 and second block with a name num2
and value 5. Then we create another memory block by the name p
, which is a pointer which means that it stores the address of num1
. So, we can use the block p
to access num1
. Now, we create a reference “pref” of the block p
. So, now the block which had the name p
has another name pref
(note that p
and pref
are names of the same block of memory). Now we say that the block of pref
will store the address of num2
. So, earlier this block was storing the address of num1
and now it will instead store the address of num2
. So, to be clear, the block (which has the name p
and pref
) stores the address of num2
now. Then we dereference p
which means we get access to the block that it is pointing to which is num2
and then we change the value of num2
to 10. So, num1
remains unchanged and num2
is now 10.