Chapter 5 : Pointers

Chapter 5 : Pointers

Contents

5.1. Introduction

This chapter introduces the concept of Pointers in C++. The use of pointers allows programmers to directly control the allocation of memory within a program. This makes possible the implementation of much more flexible ADTs which overcome the "fixed size" limitations inherent in types such as arrays. For example, pointers can be used to build dynamically expandable arrays or classes of "linked" objects such as lists, queues, stacks or trees.

The great flexibility of Pointers comes at some cost. First, the software engineer needs to be explicitly aware of how memory is allocated to the program. Secondly, the syntax of pointers, at least in C++, can be rather formidable. For these reasons, this entire chapter is devoted to the basics of using pointers. The early examples may often seem a bit trivial, overly complex or, dare I say, "pointless". However, it is worth establishing a thorough grounding before progressing to the really useful examples in later chapters.

This chapter covers the following topics:

First, let's go back to basics and consider the properties of variables. A variable can be viewed as " a place where data is stored in a program" and has three key properties - its name (an identifier chosen by the programmer) its type (what kind of data it stores) and its current value.

The computer stores the value somewhere in its memory. This will typically be in a set of memory locations starting at some memory address. Thus, each variable also has an address property describing where its value is stored in computer memory. Whenever the programmer uses the name of the variable, the computer maps this into its address. However, the details of how this is done are hidden away from the programmer. In particular:

  • The computer automatically allocates memory to store the value when the variable is first defined.
  • The computer automatically frees this memory when the variable is destroyed (e.g. at the end of a program or function).

    For simple programs, it is beneficial for programmers to be shielded from these details. However, sometimes this becomes a bit restrictive. For example, the fixed size of arrays results from the computer allocating a fixed number of memory locations to store the array when it is first declared. More advanced programming often requires the programmer to take control of their own memory allocation in order to build more flexible data types (e.g. dynamically expandable). This is achieved through the use of pointers.

    A Pointer is a new kind of variable whose value is a memory address. Thus, a pointer can store the location of some data in memory. We say that it "points" at other values.

    Like normal variables, pointers are "strongly typed". This means that an "integer pointer" can only store the addresses of integer values and a "char pointer" can only contain the addresses of char values.

    The following program declares and uses a pointer named p. The program achieves nothing useful apart from illustrating the syntax for dealing with pointers.

    The statement int *p declares a variable called p whose type is "pointer to integer". Initially, p doesn't point at anything. We can draw this as the following:

    The statement p = &i stores the address of the variable i in p. In other words, it makes p "point at" i. We draw this in the following way.

    The operator & is called the "address of" operator. It returns the address of whichever variable it is applied to. Now the value of i is 1 and the value of p is &i (address of i).

    The statement cout << "contents of i are: " << *p << "\\n" prints out the current value of i. It does this by following the pointer p. The code *p means "get the value of whatever p is pointing at". We call this "dereferencing a pointer" and say that * is the dereferencing operator.

    The statement p = &j resets the pointer p to point at the value of the variable j.

    We can now display this value by cout << *p. Notice that at this moment in time j and *p are synonymous.

    To summarise:

  • a pointer can point at values by storing their addresses.
  • A pointer is made to point at something by the "address of" operator &.
  • A value can be accessed through a pointer via the "dereferencing operator" *.

    Now let us look at a second, more complex, example.

    This example uses two pointers, p and q, and demonstrates the difference between assigning the values of pointers and the values of the things to which they point. We can draw the situation at the end of the statement *q = 2 as the following.

    The statement *p = *q copies "the value of the thing q points at" (the value of j) to "the thing that p points at" (i). The situation is now:

    Contrast this with the statement p = q. This copies the value of the pointer q - i.e. the address of j, into pointer p. In other words, we have reset p to point at j. Both pointers now point at the same value. The situation is now:

    It is vital to understand the difference between the statements *p = *q and p = q. The first copies the values being pointed at. The second actually resets the pointers.

    The statement *q = 3 now makes the following change:

    The final statement of the program prints out this value by accessing it through the pointer p.

    5.3. Allocating and freeing memory with new and delete

    So far we have used pointers to point at existing variables which had their memory automatically allocated. In this section we see how to allocate and free our own memory through the operations new and delete. Consider the following program:

    The statement new int allocates enough memory to store an integer value and returns its address as a result. The statement int *p = new(int); declares a pointer and makes it point at this new memory.

    Notice that we are not pointing at an existing variable. The program then stores the value 3 in this memory and then adds 1 to its value before printing it out. The final statement delete(p) tidies up by freeing the memory currently pointed at by p.

    In general, new allocates enough memory to store a value of the specified type and returns its address. Its argument can be any C++ type, including user defined classes. Delete frees the memory pointed at by the given pointer. For those interested in details, the available memory is actually stored in an area called the heap. New grabs some memory from the heap and delete returns it back to the heap. It should be stressed that all memory is automatically freed at the end of the program - it is not allocated for all time. However, it is a very good idea to make sure that there is an explicit call to delete for each call to new. This style of tidy programming reduces the chances of bugs occurring. In particular watch out for things like:

    The first call to new allocates some memory and sets p to point at it. The second allocates some more and sets p to point at this. This is shown below:

    The first piece of memory is no longer pointed at by anything. If there was a value stored there, it would no longer be accessible. We call this a hanging memory problem and it is to be avoided.

    In order to avoid this problem, we need to be able to test whether a pointer already points at some memory before we allocate any new memory. When we first declare a pointer, in points no-where.

    We can explicitly set it to point at the null address by a statement such as the following.

    In this case, 0 represents the null address. This is often drawn as the electrical earth symbol.

    We can explicitly test for the null address. The following fragment of code allocates new memory for the pointer f. However, it checks to see whether it first needs to delete any old memory.

    This is a particularly important piece of code to understand and will come up in many examples.

    5.4. Pointers and objects

    Pointers can point at user defined classes.

    This fragment of code constructs a new Charset and sets the pointer ps to point at it.

    The statement *ps.print() invokes the print method on the object that ps points at (i.e. on s).

    We can also use new to initialise an object. In this case new automatically calls the constructor for the object as well as allocating memory. We need to give new any arguments needed by the constructor. The general syntax is pointer = new type( arguments ). For example,

    The operator delete frees the memory and calls a special function called the destructor to tidy up. Destructors are optional in a class and we shall not come across them until a bit later. Note that the brackets around new and delete arguments are optional.

    The cumbersome notation *a.method() can be written as a->method(). Thus we can write:

    This tends to be a bit easier to read. We can also access data members this way - thus *a.X = 3 becomes a->X = 3.

    In general, the operator -> is extremely useful for accessing the members of objects through pointers and we will use it many times throughout the rest of the course.

    5.5. Pointers as function arguments

    Pointers can be passed as function arguments and returned as function results. Consider the following function which takes a pointer to a character as its argument.

    This might be called in the following ways:

    Once inside the function call example(b) we have the following situation.

    The formal parameter c takes the value &a and therefore points at the variable a. Any change to *c in the function would change the value of a. In other words, the function can cause "side effects" by changing the value of a variable in the main program. This is the same effect as "pass by reference". In fact, the computer implements pass by reference by passing addresses behind the scenes. We call passing pointers as function arguments "pass by address".

    We can also return results this way. For example:

    However, NEVER RETURN THE ADDRESS OF A LOCAL VARIABLE THAT HAS BEEN AUTOMATICALLY ALLOCATED INSIDE A FUNCTION!. This is because the variable will be automatically freed at the end of the function and the resulting address will no longer be valid. Thus, although the above function is valid, the following is incorrect:

    You can only safely return the address of memory that you have allocated yourself.

    We have now seen three ways of passing arguments to functions:

  • by value - a formal argument is created and the value copied in. This does not cause side effects in the calling program.
  • by reference - the formal parameter and calling program refer to the same memory so side-effects may occur. However, the user program looks the same as for call by value.
  • by address - addresses are passed and so side-effects can occur. The calling program needs to be aware that addresses are being used and the syntax is more difficult to follow.

    We can see that pass by reference and pass by address are similar in effect. The main difference is in the syntax - pass by reference appears more "natural" for the user of a function. In effect, the computer is adding a layer of gloss to addresses to make them appear as references.

    5.6. Pointers and Arrays

    There is a very close relationship between pointers and arrays in C++. In fact, we shall see that they can almost be used interchangeably. Before we embark on this discussion, I should point out that the syntax you are about to see often becomes very terse and confusing. Furthermore, these aspects of C++ are not really found in other languages to the same degree (except for C). However, even if you don't use them yourself, you will need to be able to understand them in other peoples programs.

    Pointers can point at array elements. For example, the following code uses a pointer to read values into an array.

    Pointers are C++ types and some of the standard operators have been overloaded to apply to them. These include:

    The idea of "pointer arithmetic" makes most sense when pointers are combined with arrays. If the pointer p currently points at array element a[i], then:

  • p+1 is a pointer to a[i+1].
  • p-3 is a pointer to a[i-3].
  • p++ moves the pointer to point at the next element.
  • p = p + 3 moves the pointer to point 3 places on down the array.

    Also, if p points to a[i] and q points to a[i+1], then we can assert that p < q evaluates to true.

    We have already seen that if p points to the null address then p == 0 evaluates to true. Of course !p evaluates to false (this could be written p != 0).

    The following for loops are equivalent

    Notice the use of parentheses in *(p + i). This is because operator * (pointer dereference) has higher priority than operator + (addition). The statement *p + i means "get the value pointed at and add i to it". In other words, increase the value of an element of the array. The statement *(p + i) means "increment the pointer and get what it now points at". In other words, get the value of the next array element.

    Now we can go one step further. The name of an array is the same as a pointer to its first element!

    Thus, the statement p = &a[0] could more simply be written as p = a. Also, a[10] can be written as *(a + 10). The following for loop is a valid way to print out the values of the array a.

    In essence, subscript and pointer array access are interchangeable - a[i] <=> *(a + i).

    All of this may seem confusing and pointless because it introduces no new functionality into C++. However, we will see later examples where it is more convenient to process arrays through pointers than through subscripts. The following program defines a function to search an array for a specified value. It uses pointers, not subscripts, to access the array elements.

    Notice also that because the name of an array is a pointer to its first value, arrays are always passed by address and never by value.

    5.7. Character arrays

    We finish this chapter with a brief discussion of character arrays, or "character strings" as they are often called. Character arrays have a special representation in programs as literal strings. For example,

    Of course, character strings may be of different lengths. As a result, the computer appends an additional null character to them in order to show where they end. This can be quoted as the literal character '\\0'. Thus the statement:

    Defines a new character array (pointer) with the following initial value.

    The length of this array is 8 characters because the computer has automatically appended the additional \\0. We can display this array with the statement cout << message.

    In general, the type char * should be interpreted as a null terminated character array.

    As a final example, the following function returns the length (number of characters) in an arbitrary sized character array. Note that it doesn't count the null character at the end.


    Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!