Pointers

1. Address of a location in the memory

The command

int x;
in C++ code is the way for a programmer to give the following request to the computer:

Please give me the memory that can hold one integer. This memory will be called x. When in future I ask to store or read from this memory, I will refer to it as x.

If in the future we issue the command x=30; we are essentially giving the following request to the computer:

Place the value 30 in the memory location that we agreed to call x.

When we issue the command

x=x+27;
we are asking for the following 3 actions:

  • 1) Dear computer, read the content of the memory location that we call x;
  • 2) Add number 27 to the content that you found in x.
  • 3) Place the obtained sum to the memory location that we call x.

We like to call the memory locations using beautiful names such as x. The computer is willing to help the programmers. The programmers can use custom names for memory locations. However, internally, each memory location has a numerical addresses assigned to it. We may think of the addresses as license plates, or street names and numbers. In C++ it is possible to see the address of a memory location. We can see the address if we ask the computer to print the value &x (this can be read as address of x).

 std::cout << &x << std::endl;
You will notice that it prints non-sensical word containing both letters and numbers. It actually prints in hexadecimal system. If you really want to see the address as a number in decimal system, you can convert it to long and type
 std::cout << (long)(&x) << std::endl;
The above program can be summarized as
// addressPrinting.cpp 
// compile with 
// c++ addressPrinting.cpp -o addPrinting 
// execute with 
// ./addPrinting 
#include <iostream>
int main(){
 int x;
 std::cout <<  (long)(&x) << std::endl;
}
You will notice that every time you execute the program it will print a different address. Every time the program is loaded to and executed from a different place in memory.

2. Mutator problem

We will now analyze one example in which the addresses are very useful. Our goal is to write a function that modifies variables located outside of the function. We want to create a function that modifies its arguments. Such functions are called mutators. Let us assume that we want to write the function that accepts the integers x and y and is required to add the number 1 to x and the number 2 to y.

2.1. First attempt

Our first attempt to create the described function will fail. The code that we will write will be very intuitive, but it won't work. It is very important to write it down, and then analyze and see the reasons behind the failure. Then we will see how the addresses can be used to create mutator functions.

 
#include <iostream>
void addOneToXAndTwoToY(int x, int y){
 x=x+1;
 y=y+2;
}
int main(){
 int x,y;
 x=5; y=7;
 addOneToXAndTwoToY(x,y); 
 std::cout <<  "x=" << x << " and y=" << y << std::endl;
}

After executing the code, you will receive the output

x=5 and y=7

which means that x and y are not changed by the function. In order to see what happened behind the scene we will analyze the code starting with the function main().

  • Step 1. The line int x,y; requests two memory locations from the computer. It also asks the computer to accept internal names x and y for these two memory locations.
  • Step 2. The line addOneToXAndTwoToY(x,y); calls the function addOneToXAndTwoToY and passes the value 5 instead of the first argument and the value 7 instead of the second.
  • Step 3. When the function is called, the function can be thought of as a separate new program within the program. The declaration specifies that it will require two memory locations for two integers. The function will want to call them x and y. These happens to be the same names as the names used by the main function but these will be brand new memory locations. They will contain the values 5 and 7.
  • Step 4. The value inside the location x within the function gets increased to \(6\), while the value x inside the main program stays 5.
  • Step 5. Similarly, the value inside the location y within the function gets increased to \(9\), while the value y from main() stays 7.
  • Step 6. The function finishes its execution and deletes its variables x and y. We wished to see the values 6 and 9. However, these two numbers were actually wiped out.
  • Step 7. The main function ends its job by printing 5 and 7 to the standard output.

2.2. Using pointers to solve the mutator problem

C++ copies the arguments to the functions. Hence, if we want to create functions that are mutators, we need to use a trick: We will not use variables as arguments of the functions. The arguments of the functions will be addresses of variables. In other words, we will call the function in the following way

 
addOneToXAndTwoToY(&x,&y); 

Once the function is executed, it will receive the address and then the function will access the locations of these address and modify their contents.

If we just make the above modification to our code, it won‘t compile. The reason is that &x and &y are not of type int. The declaration

void addOneToXAndTwoToY(int x, int y)
expects integers. The quantities &x and &y are called pointers to int. Beginner-friendly programming languages would spell out this variable type and introduce a descriptive names for such variable types, and in friendly languages the function declaration would look something like
void addOneToXAndTwoToY(pointer_to_int pX, pointer_to_int pY) // incorrect 
However, C++ and many other related languages use int* to denote the pointer to integer. The declaration must be
void addOneToXAndTwoToY(int* pX, int* pY) // correct declaration
This creates a confusion with novices because the same symbol * is used for different purposes. When we write 7*8 we are multiplying the numbers \(7\) and \(8\). However when we write int* pX; we are issuing a command

Dear computer, please give me enough memory to store an address of an integer. We will call the content of this memory by the name pX.

Another unfortunate feature of the language is that * does not have to be affixed to int. Each of the following three commands is correct: int * pX; int* pX; int *pX;.

We need to modify the code inside the function so that it now accesses the location with the address pX and increases its content by 1. The function also must access the location with the address pY and increase its value by 2. A gentle language would use a syntax that looks something like

void addOneToXAndTwoToY(int* pX, int* pY){
 content(pX)=content(pX)+1; // not C++, unfortunately 
 content(pY)=content(pY)+2; // not C++, unfortunately 
}

The designers of the C-family of languages thought that content is too long to type. They wanted something shorter. They decided to use * and now instead of content(pX) we write *pX. The updated correct code is given below

#include <iostream>
void addOneToXAndTwoToY(int* pX, int* pY){
 (*pX)=(*pX)+1;
 (*pY)=(*pY)+2;
}
int main(){
 int x,y;
 x=5; y=7;
 addOneToXAndTwoToY(&x,&y);
 std::cout <<  "x=" << x << " and y=" << y << std::endl;
}

3. Accessing the content from the pointer

As we have seen in the previous section, *pX is used to access the content of the memory location whose address is pX. The following code is another example of accessing the location of the variable x using its address.

#include <iostream>
int main(){
   int x;
   int* pX;
   x=30;
   pX=&x;
   std::cout << "pX="  <<  pX  << std::endl;
   std::cout << "*pX=" <<  *pX  << std::endl;
   *pX=45;
   std::cout << "x=" << x << std::endl;
   return 0;
}

4. Allocating and freeing memory

In previous examples the pointers were used as addresses to the memory locations already occupied by other variables. For instance, pX=&x; results in pX pointing to the memory location occupied by x.

It is possible for a pointer to contain an address that is not assigned to the other variable that the programmer has requested from the computer.

Consider the following code, that you should not try to compile and execute.

// DANGEROUS CODE - DO NOT COMPILE/EXECUTE
#include <iostream>
int main(){
   int* pX;
   pX=(int*)(100);
   std::cout << *pX << std::endl;
   return 0;
}

In the lines above, we assigned the memory address \(100\) to pX. Then we attempted to print the content of this address. Usually, the operating system will return an error, or the program may crash. Namely, the address \(100\) is likely given to some other program and we are not allowed to access it. In contrast, when we wrote pX=&x; then the address &x is an address for which we had the right to access. This was the address we received after issuing a command int x;. The command was a request to the computer to give us a memory location to store an integer.

We may use the command new to request a memory location. The command pX=new int; requests a location for one integer. The address of the obtained location is then stored in pX. After allocating the memory with new we are allowed to access it with *pX. When we no longer need the memory we return it to the computer using the command delete pX;. The computer can later assign this returned memory to other program or to our own program if we ask again.

#include <iostream>
int main(){
   int* pX;
   pX=new int;
   *pX=37;
   (*pX)+=17;
   std::cout << *pX << std::endl;
   delete pX;
   return 0;
}

5. Memory leak

Memory leak is a dangerous bug that computer programs can have. It occurs when a programmer fails to return the memory allocated using the command new. The following few lines of code show an example of a memory leak.

int* pX;
pX=new int;
*pX=37;
pX=new int;
*pX=73;
delete pX;

There are two commands of the form pX=new int;. After the first one, the computer gave us a memory to store one integer. We chose to store \(37\). However, when we called the command pX=new int; for the second time, then the computer issued us a new memory. The variable pX stores now the address of this new memory, while nobody else points to our old number 37. The memory location that holds the number 37 is lost forever. Nobody points to that memory and the computer still thinks that our program needs it. The computer will not allocate this memory to some other process, and since we don‘t have a way to access it again, until our program is turned off (or the computer restarted), we have permanently occupied a piece of RAM memory that nobody else is allowed to use.

Memory leaks are especially dangerous when they occur within recursion or a loop. The entire RAM memory of the computer can go missing within a few seconds.

The following youtube video is an interesting introduction to pointers in C. When watching the video keep in mind that pointers in C are slightly different that instead of new they use malloc. Here is the link to the video: link

6. Practice problems

Problem 1.

The function a_capital modifies the string by turning each character a into A. The code below has the implementation of the function. It is missing only one line in the main function. The main function declares the string w and reads the string from the user input. The missing line should call the function a_capital in such a way that every character a in w is turned into A. Write the command that should be placed instead of // ??? // to achieve the described outcome.

#include<iostream>
void a_capital(std::string* s){
   long len=(*s).length();
   for(long i=0;i<len;++i){
      if((*s)[i]=='a'){
         (*s)[i]='A';
      }
   }
}
int main(){
   std::string w;
   std::cin>>w;
   // ??? //
   std::cout<<w<<"\n";
   return 0;
}

Problem 2.

The function sumAndProduct accepts three arguments. The first two are pointers aC and aB that contain the addresses of two integers c and b. The third argument is the integer a. The function should calculate the sum of a and b and the product of a and b. The sum should be placed in the memory location c whose address is aC. The product should be placed in the memory location b, whose address is aB. Your code should replace the text // ??? // below.

#include<iostream>
void sumAndProduct(long* aC, long* aB, long a){
   // ??? //
}
int main(){
   long numberA, numberB, numberC;
   std::cin>>numberA>>numberB;
   sumAndProduct(&numberC, &numberB, numberA);
   std::cout<<numberC<<" "<<numberB<<"\n";
   return 0;
}

Problem 3.

Create a function swap that changes the contents of two variables a and b. The content of a should be stored in b and the content of b should be stored in a. The function receives two arguments aA and aB that are addresses of variables a and b. Your code should replace the text // ??? // below.

#include<iostream>
void swap(long* aA, long* aB){
  // ??? //
}
int main(){
  long a, b;
  std::cin>>a>>b;
  swap(&a,&b);
  std::cout<<a<<" "<<b<<"\n";
  return 0;
}

Problem 4. The variables pointerA and pointerB are two pointers in the main() function declared with the commands long* pointerA; long* pointerB;. The variable pointerA contains the address of the memory location a of type long. This was achieved with the commands long a; pointerA=&a; Similarly, the variable pointerB is assigned the memory location of the variable b with the commands long b; pointerB=&b;. Create a function swap that swaps the contents of two variables pointerA and pointerB.