First Next Previous Last Glossary About

Pointers and Memory - An introduction


Introduction

At the very heart of the computers we use is the notion of the "stored program machine". It means that any computer program occupies memory space and all of the elements of the program occupy distinctive parts of the memory space. Each function and variable has an identifier attached to it which "points" to the location in memory where the element can be found.

The majority of the programs we have used so far have depended on program elements the location of which was determined when the program was built. If we plan to use an integer we make a declaration that names the integer (gives it an identifier) and the compiler then allocates a particular memory location to that variable.

In many situations this is all we need but there are also many other situations when, at compile time, we don't know what variables and functions we will need to access. We need to create a program element "dynamically", that is when the program runs. For example we might need to use an array but don't know what the array must be until the program runs. Most programming languages support the dynamic allocation of memory and provide access to memory "pointers", variables which are memory addresses. The pointer variable gives the address of some element that was created dynamically.

There are other situations where using pointers is advantageous. Pointers can greatly improve the speed of access to certain kinds of program elements. For example it is much quicker to use a pointer to iterate through an array than it is to use an index.

Languages like C and C++ rely heavily on pointers and this means that you need to understand and be able to effectively use pointers. Before looking at pointers you should review what you know about arrays. This is because there is a strong relationship between arrays and pointers.

Return to top of page

A review of arrays

An array is declared like this:


 int ia[50]; //single dimension array

The array name is ia and it is size 50 elements. It is indexed from 0 to 49, all arrays in C++ are based at 0.

We can declare arrays of more than a single dimension:


 int ia2[5][10];   //two dimensional array

 int ia3[5][5][5]; //three dimensional array

The array operators [ ] associate left to right, ia2 is a 5 element array and each element of ia2 is a 10 element array.

Here is a sample program which uses three integer arrays:

Code Explanation

#include <iostream>


main (void)

{ int

   ia [3]       = {1,2,3},

   ia2[3][2]    = {{2,1},{2,2},{2,3}},

   ia3[3][2][2] = {{ {3,1},{3,2} },

                   { {3,3},{3,4} },

                   { {3,5},{3,6} },

                  },

   i,j,k;

There are three arrays:
  1. ia is a single dimension array,
  2. ia2 is a two-dimension array and,
  3. ia3 is a three-dimension array.
Each array is initialised and you can see from the way each array is declared that ia2 can be viewed as a single-dimension array of size 3 and each cell is a single-dimension array of size 2.

  cout << "New run ... "

       << endl;

  for (i = 0; i < 3; i++)

   cout << "ia["

        << i << "] is "

        << ia[i] << endl;

  cout << endl;

Now we just display the one-dimension array,

  for (i = 0; i < 3; i++)

   { for (j = 0; j < 2; j++)

      cout << "ia2["

           << i << "]["

           << j << "] is "

           << ia2[i][j] << endl;

     cout << endl;

   }

  cout << endl;

and the two-dimension array,

  for (i = 0; i < 3; i++)

   { for (j = 0; j < 2; j++)

      for (k = 0; k < 2; k++)

       cout << "ia3[" << i

            << "]["

            << j << "]["

            << k

            << "] is "

            << ia3[i][j][k]

            << endl;

      cout << endl;

   }

and the three-dimension array.

  ia3[2][1][1] += 4;

  cout << endl << endl

       << "ia3[2][1][1] is now "

       << ia3[2][1][1]

       << endl;



  return 0;

}

The combined assignment operator += is useful here for ia3[2][1][1] += 4; otherwise we would have to write ia3[2][1][1] = ia3[2][1][1] + 4;

Return to top of page

A start with pointers

A pointer is an address, ie a value that "points" to something else. A pointer is declared by placing the pointer operator * (in this case it doesn't mean multiply) in front of an identifier, eg:

char *ch; 
This means ch is a pointer (will hold the address of) to a character variable. It helps to read the declaration "in reverse", ch is a pointer to a char.
int *intp;
intp points to an integer, or intp is a pointer to an int.


To access what a pointer points at use the indirection operator * (I suppose we could call it the pointer operator again), eg


 cout << *ch;

The pointer operator is often used with the address-of operator &, ie given:

char *ch, fred = 'A';
Declare one pointer ch and one char variable fred.
ch = &fred;
Assign the address of the char variable fred to ch. Now *ch is whatever fred contains. ie 'A'.


Code Explanation

#include <iostream>


main (void)

{int ia [3] = {1,2,3},

     i = 100,

     *intp;



  intp = &i;

I said earlier that there is a relationship between arrays and pointers and it is this: the array name is a pointer.

Since intp is a pointer we can assign the address of i to intp by using the address-of (or reference) operator.


  cout << "The value pointed "

       << "to by intp is "

       << *intp

       << endl;



  cout << "i is "

       <<  i

       << endl;

  intp = ia;

To show the value in the location pointed to by intp we write *intp. This is called de-referencing the pointer.


The line intp = ia; assigns one pointer to another. Since ia (the name of the array) is a pointer we can do that.


  for (i = 0; i < 3; i++)

   cout << "intp now points at "

        << *intp++

        << endl;

  cout << endl;

The statement *intp++ looks a bit daunting but it shows two operations on intp. The first (*) de-references intp, ie gets what intp points at, the second (++) increments the pointer. It is important to realise that it is the pointer that is incremented, not what the pointer points to.


  *intp = 20;

  cout << "*intp is now "

       << *intp

       << endl;

The next step just assigns a new value to *intp, ie the location pointed to.

  *intp += 1;

  cout << "*intp is now "

       << *intp

       << endl;

This is another operation on the location pointed to by intp;

  (*intp)++;

  cout << "*intp is now "

       << *intp

       << endl;



  return 0;

}

This too is another operation on the location pointed to by intp. If *intp is 20 then (*intp)++ results in location intp containing 21. This is quite different from *intp++ - see above.


Arithmetic with pointers

Before taking a look at some pointer arithmetic I want to show you a couple of programs which do the same thing and which differ in just one line:


//ptr_2.cc

#include <iostream>

#include <cstdlib>



main ()

{ char name[] = "Fred Smith";



  cout << name

       << " "

       << strlen(name)

       << endl;



  for (int i = 0;

       i <= strlen(name); i++)

   { cout << i << " ";

     if (name[i] == NULL)

      cout << "NULL";

     else

      cout << name[i];

     cout << endl;

   }

  return 0;

}


//ptr_2a.cc

#include <iostream>

#include <cstdlib>



main ()

{ char *name = "Fred Smith";



  cout << name

       << " "

       << strlen(name)

       << endl;



  for (int i = 0;

       i <= strlen(name); i++)

   { cout << i << " ";

     if (name[i] == NULL)

      cout << "NULL";

     else

      cout << name[i];

     cout << endl;

   }

  return 0;

}



You can see that the programs differ only in the line where a character variable is declared. Since the two programs do the same thing they demonstrate that an array name is a pointer. We will use these programs to do some pointer arithmetic but first I will step through them to explain what they do.

We now take both the sample programs and modify them to use pointer arithmetic.


//ptr_2b.cc

#include <iostream>

#include <cstdlib>



main ()

{ char name[] = "Fred Smith",

       *n;



  n = name;

  while (*n != NULL)

   { cout << *n << endl;

     n++;

   }



  return 0;

}

//ptr_2c.cc

#include <iostream>

#include <cstdlib>



main ()

{ char *name = "Jack Smith";



  while (*name != NULL)

   cout << *name++ << endl;



  return 0;

}




//ptr_2d.cc

#include <iostream<

#include <cstdlib<



main ()

{ char *name = "Bert Smith";

  int  vals[4] = {23,45,56,75};



  cout << "A char is "

       << sizeof(char)

       << " byte."

       << endl;



  cout << "An int is "

       << sizeof(int)

       << " bytes."

       << endl;



  for (int i = 0;

       i < strlen(name);

       i++)

   cout << *(name + i);



  cout << endl;



  for (int i = 0;

           i < 4;

           i++)

   cout << *(vals + i)

        << endl;



  return 0;

}


A char is 1 byte.

An int is 4 bytes.

Bert Smith

23

45

56

75

Output

This example does much the same as the pair prt_2b and prt2_c except that it shows the addition of an integer value (i) to the pointer name.

The statement *(name + i); says "add i to name then get the value pointed to by that address".

It is important to understand that, for pointer arithmetic, different kinds of pointers are handled differently for different kinds of variables. The reason for this is that the variables pointed to are different sizes. For instance an integer is bigger than a character and a float is bigger than both an integer and a character. When you code something like name + i even though i may have a value of 1 for instance it doesn't necessarily mean that just 1 is added to name. The addition for pointer arithmetic is done in units of the element size. Assume name is 126787, ie it is addressing location 126787 in memory. If the element size is 4, as for an integer, and i is 1 then name + i results in 126791.



Here is a summary of some important points:

Return to top of page

Dynamic variables

In the lesson on Scope and storage specifiers I wrote that there were several storage specifiers and that some kinds of storage were created dynamically. Each time you make a "normal" variable declaration you are instructing the compiler to set aside an area of memory for the variable. The variable name is an identifier which associates the allocated memory with the name. For example:




You can make variables like x and a exist for the duration of the program by declaring them as static or as external. Normally though variables are local to their scope, that is, they don't exist until they come into scope. For instance a variable declared inside a function will not exist until the function is executed.

There is another way to use variables and it is a way that is very much under your control. You can create your own dynamic variables. These are variables that don't exist until you, the programmer, explicitly create them. The steps are quite simple:

  1. declare a pointer variable and,
  2. allocate the appropriate amount of memory to the variable.

Dynamic variables and the new and delete operators

If we create an instance of a dynamic variable then we use the new operator:


  char * x;

  x = new char;

Here a character pointer variable is declared then memory is allocated to it.


  delete x;

The memory can be returned to whence it came with the delete operator:


#include <iostream>



main ()

{ char * x;

  x = new char;



  *x = 'A';

  cout << *x << endl;

  delete x;

  return 0;

}

Here is a complete program which uses the new and delete operators. The pointer variable x is declared, memory is allocated to it, the pointer is dereferenced so a value ('A') can be assigned to that memory location, the pointer is dereferenced again so that the contents can be displayed and, finally, the memory is freed. When allocating memory with new you don't need to know how much is required, the compiler will take care of that for you.



#include <iostream>

#include <string>

#include <cstdlib>



main ()

{

  string * str;

  char   * s;



  str = new string;

  s = new char[50];



  if ((str == NULL) || (s == NULL))

   { cerr <<  "ERROR: no space."

          << endl;

     exit(1);

   }

  *str = "Allocation successful.";



  strcpy(s,"Allocation successful 2.");

  cout << *str << endl;

  cout << s << endl;



  delete str;

  delete[] s;



  return 0;

}

When you create dynamic variables the required memory comes from an area known as the heap. In effect this is all memory that is not currently in use by other programs and data structures. There is always the possibility that there may be no available memory. This example shows how you can test to see if new was able to allocate memory.

When new is used it will return a pointer to the memory that was allocated or it will return the NULL pointer if no memory was available. You should always test the pointer returned by new because using an invalid pointer can have disastrous results for your program and worse, can have disastrous results for the operating system. You have probably seen, many times, Windows report a GPF fault (General Protection Fault). This usually results from a program using an invalid pointer and attempting to access memory to which it has no legitimate claim.




Tutorial

Exercise 1

Try each of the example programs first and make sure they work.

Exercise 2

Write a program which declares an array of 10 floats then use a for loop to display each value in the array by array index.

Now use pointer arithmetic in a for loop to display each cell in the array.

Return to top of page


First Next Previous Last Glossary About


Copyright © 1999 - 2001 David Beech