References. References as Arguments of Functions

1. Introduction to references

We use names such as x and y to access the locations in memory. The command int x; is a request issued to the computer to give us a piece of RAM memory sufficient to hold one integer. We also inform the computer that we will use the name x for the content of this memory.

If after the above command we issue the command int &y=x; we are asking the computer to allow us to use name y as well as name x for the content of the memory location allocated by the command int x;. This may look strange and useless at the moment. We will see later some amazing consequences of this ability to use different names for memory locations.

Let us now summarize the meanings of the instructions that are used to declare variables.

\begin{eqnarray*} \begin{array}{ll|ll} \mbox{Instruction} & & & \mbox{Meaning}\\ \hline \mbox{int x;} &&& \mbox{Please give me memory for one integer}\\ &&& \mbox{and let me call it }x.\\ \hline &&& \mbox{Don't give me any new memory.}\\ \mbox{int& y=x;} &&&\mbox{Just let me use the name }y\mbox{ in}\\ &&&\mbox{addition to the name }x\mbox{ for }\\ &&&\mbox{the old memory you already gave me.}\\ \hline \end{array} \end{eqnarray*}

Let us now analyze the memory diagram for the following code that consists of four instructions.

int x=87;
int z=x;
int& y=x;
y=977;

The picture below shows how the memory diagram is updated after each of the instructions.

Problem 1. What does the following code print?
#include<iostream>
int main(){
   int x=87;
   int z=x;
   int &y=x;
   y=977;
   std::cout<<1000000*x+1000*y+z<<"\n";
   return 0;
}

2. Analysis of addresses

We will now see that the variable and its reference share the same address. The variable y will be defined as a reference to x. We will print the addresses of x and y and see that they are the same.

 #include <iostream>
int main(){
   int x;
   int& y=x;
   x=27;
   std::cout<< "x="<< x<< ". y="<< y<< ". &x=";
   std::cout<< (long)(&x)<< ". &y=";
   std::cout<< (long)(&y)<< "."<< std::endl;
   std::cout<< "Modification of y"<< std::endl;
   y=37;
   std::cout<< "x="<< x<< ". y="<< y<< ". &x=";
   std::cout<< (long)(&x)<< ". &y=";
   std::cout<< (long)(&y)<< "."<< std::endl;  
   return 0;
}

The output of the program indicates that x and y have the same value and that their addresses in the memory are the same. The command x=27 updates the memory location whose name is x. The memory location with the name y is the very same as the one with the name x. Thus, when we print y we see the same number \(27\). Later on, when we make the change y=37, we observe that x is changed as well.

Let us point out that reference must be initialized upon declaration. The command int myNumber=37; can be replaced by two commands

int myNumber; 
myNumber=37;
However, the same cannot be done with int& y=x;. Also, we cannot initialize the reference using numbers int& y=37. During the initialization we must provide a variable that corresponds to the physical location in the memory (also known as L-value because it can be on the left side of the assignment operator).

3. Arguments of functions

We have learned how to use pointers to create mutator functions. Those are the functions capable of changing their arguments. References also offer a way to accomplish this task.

Before we can do that, we need to fully clarify the mechanism of function calls.

Let us first identify the following four categories of functions:

  • Category 1: Functions with no arguments and no return value
  • Category 2: Functions with arguments but no return value
  • Category 3: Functions with no arguments but with return value
  • Category 4: Functions with arguments and with return value

In this document we will analyze in great details the functions in categories 1 and 2. We have used the functions in categories 3 and 4, but we will fully understand the mechanics of return statement once we learn about classes and move semantics.

3.1. Functions without arguments and without return values

We will analyze a code in which a fairly simple function is called from the main.

#include<iostream>
void simplestFunction(){
   std::cout<< "Executing easy function"<< std::endl;
}
int main(){
   std::cout<< "Printing before the function."<< std::endl;
   simplestFunction();
   std::cout<< "Printing after the function."<< std::endl;
   return 0;
}

The previous code is equivalent to the one in which the command simpleFunction(); is replaced by the block of code

{
   std::cout<< "Executing easy function"<< std::endl;
}

Any local variable defined inside the function would behave as a local variable defined inside a block of code surrounded by { and }: it would cease to exist after the block.

3.2. Function with argument(s) and no return value

Our next example is the function that has one argument x of type int.

#include<iostream>
void lessSimpleFunction(int x){
   std::cout<< "Executing function with x="<< x<< std::endl;
}
int main(){
   std::cout<< "Printing before the function."<< std::endl;
   lessSimpleFunction(7);
   std::cout<< "Printing after the function."<< std::endl;
   return 0;
}

The line lessSimpleFunction(7) is replaced by the code from the declaration of the function (i.e. the code between { and }). However, one line is added at the beginning of the code: int x=7;. The line is obtained by copying the statement between ( and ) of the declaration and adding =7 to it. Thus the above code is equivalent to

#include<iostream>
int main(){
   std::cout<< "Printing before the function."<< std::endl;
   {
      int x=7;
      std::cout<< "Executing function with x="<< x<< std::endl;
   }
   std::cout<< "Printing after the function."<< std::endl;
   return 0;
}

Therefore the line with arguments of functions is copied to the very beginning of the block of the code and the arguments are initialized with the values provided in the function call.

3.3. Function with references in argument line

We could have placed int& x in the argument line of the function. The same rule of replacement from previous section would apply in this situation.

#include<iostream>
void lessSimpleFunction(int& x){
   std::cout<< "Executing function with x="<< x<< std::endl;
   x=20;
   std::cout<< "I changed x to x="<< x<< std::endl;
}
int main(){
   int z=7;
   std::cout<< "Printing before the function."<< std::endl;
   lessSimpleFunction(z);
   std::cout<< "Printing after the function. ";
   std::cout<<"The variable z became z="<< z<< std::endl;
   return 0;
}

The previous code is equivalent to the following one:

#include<iostream>
int main(){
   int z=7;
   std::cout<< "Printing before the function."<< std::endl;
   {
      int& x=z; //This line is due to the argument line lessSimpleFunction(int& x)
      std::cout<< "Executing function with x="<< x<< std::endl;
      x=20;
      std::cout<< "I changed x to x="<< x<< std::endl;
   }
   std::cout<< "Printing after the function. ";
   std::cout<<"The variable z became z="<< z<< std::endl;
   return 0;
}

The function lessSimpleFunction is a mutator. It changes the argument x.