Chapter 17

Pointing the Way to C++ Pointers

In This Chapter

arrow Introducing the concept of pointer variables

arrow Declaring and initializing a pointer

arrow Using pointers to pass arguments by reference

arrow Allocating variable-sized arrays from the heap

This chapter introduces the powerful concept of pointers. By that I don’t mean specially trained dogs that point at birds but rather variables that point at other variables in memory. I start with an explanation of computer addressing, before getting into the details of declaring and using pointer variables. This chapter wraps up with a discussion of something known as the heap and how we can use it to solve a problem that I slyly introduced in the last chapter.

But don’t think the fun is over when this chapter ends. The next chapter takes the concept of pointers one step further. In fact, in one way or another, pointers will reappear in almost every remaining chapter of this book.

warning.eps It may take you a while before you get comfortable with the concept of pointer variables. Don’t get discouraged. You may have to read through this chapter and the next a few times before you grasp all the subtleties.

What’s a Pointer?

A pointer is a variable that contains the address of another variable in the computer’s internal memory. Before you can get a handle on that statement, you need to understand how computers address memory.

warning.eps The details of computer addressing on the Intel processor in your PC or Macintosh are quite complicated and much more involved than you need to worry about in this book. I use a very simplified memory model in these discussions.

Every piece of random-access memory (RAM) has its own, unique address. For most computers, including Macintoshes and PCs, the smallest addressable piece of memory is a byte.

remember.eps A byte is 8 bits and corresponds to a variable of type char.

An address in memory is exactly like an address of a house, or would be if the following conditions were true:

  • Every house is numbered in order.
  • There are no skipped or duplicated numbers.
  • The entire city consists of one long street.

So, for example, the address of a particular byte of memory might be 0x1000. The next byte after that would have an address of 0x1001. The byte before would be at 0x0FFF.

tip.eps I don’t know why, but, by convention, memory addresses are always expressed in hexadecimal. Maybe it’s so that non-programmers will think that computer addressing is really complicated.

Declaring a Pointer

A char variable is designed to hold an ASCII character, an int an integer number, and a double a floating-point number. Similarly, a pointer variable is designed to hold a memory address. You declare a pointer variable by adding an asterisk (*) to the end of the type of the object that the pointer points at, as in the following example:

  char* pChar;    // pointer to a character
int*  pInt;     // pointer to an int

A pointer variable that has not otherwise been initialized contains an unknown value. Using the ampersand (&) operator, you can initialize a pointer variable with the address of a variable of the same type:

  char cSomeChar = 'a';
char* pChar;
pChar = &cSomeChar;

In this snippet, the variable cSomeChar has some address. For argument’s sake, let’s say that C++ assigned it the address 0x1000. (C++ also initialized that location with the character 'a'.) The variable pChar also has a location of its own, perhaps 0x1004. The value of the expression &cSomeChar is 0x1000, and its type is char* (read “pointer to char”). So the assignment on the third line of the snippet example stores the value 0x1000 in the variable pChar. This is shown graphically in Figure 17-1.

9781118823873-fg1701.tif

Figure 17-1: The layout of cSomeChar and pChar in memory after their declaration and initialization, as described in the text.

Take a minute to really understand the relationship between the figure and the three lines of C++ code in the snippet. The first declaration says, “go out and find a 1-byte location in memory, assign it the name cSomeChar, and initialize it to 'a'.” In this example, C++ picked the location 0x1000.

The next line says, “go out and find a location large enough to hold the address of a char variable and assign it the name pChar.” In this example, C++ assigned pChar to the location 0x1004.

tip.eps In Code::Blocks, all addresses are 4 bytes in length irrespective of the size of the object being pointed at — a pointer to a char is the same size as a pointer to a double. The real world is similar — the address of a house looks the same no matter how large the house is.

The third line says, “assign the address of cSomeChar (0x1000) to the variable pChar.” Figure 17-1 represents the state of the program after these three statements.

“So what?” you say. Here comes the really cool part, as demonstrated in the following expression:

  *pChar = 'b';

This line says, “store a 'b' at the char location pointed at by pChar.” This is demonstrated in Figure 17-2. To execute this expression, C++ first retrieves the value stored in pChar (that would be 0x1000). It then stores the character 'b' at that location.

9781118823873-fg1702.tif

Figure 17-2: The steps involved in executing *pChar = 'b'.

tip.eps The * when used as a binary operator means “multiply”; when used as a unary operator, * means “find the thing pointed at by.” Similarly & has a meaning as a binary operator (though I didn’t discuss it), but as a unary operator, it means “take the address of.”

So what’s so exciting about that? After all, I could achieve the same effect by simply assigning a 'b' to cSomeChar directly:

  cSomeChar = 'b';

Why go through the intermediate step of retrieving its address in memory? Because there are several problems that can be solved only with pointers. I discuss two common ones in this chapter. Subsequent chapters describe a number of problems that are solved most easily with pointers.

Passing Arguments to a Function

There are two ways to pass arguments to a function: either by value or by reference. Now, consider both in turn.

Passing arguments by value

In Chapter 11, I write that arguments are passed to functions by value, meaning that it is the value of the variable that gets passed to the function and not the variable itself.

The implications of this become clear in the following snippet (taken from the PassByReference example program in the online material):

  void fn(int nArg1, int nArg2)
{
    // modify the value of the arguments
    nArg1 = 10;
    nArg2 = 20;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // initialize two variables and display their values
    int nValue1 = 1;
    int nValue2 = 2;

    // now try to modify them by calling a function
    fn(nValue1, nValue2);

    // what is the value of nValue1 and nValue2 now?
    cout << "nValue1 = " << nValue1 << endl;
    cout << "nValue2 = " << nValue2 << endl;

    return 0;
}

This program declares two variables, nValue1 and nValue2, initializes them to some known value, and then passes their value to a function fn(). This function changes the value of its arguments and simply returns 0.

  • Question: What is the value of nValue1 and nValue2 in main() after the control returns from fn()?
  • Answer: The value of nValue1 and nValue2 remain unchanged at 1 and 2, respectively.

To understand why, examine carefully how C++ handles memory in the call to fn(). C++ stores local variables (such as nValue1 and nValue2) in a special area of memory known as the stack. Upon entry into the function, C++ figures out how much stack memory the function will require, and then reserves that amount. Say, for argument’s sake, that in this example, the stack memory carved out for main() starts at location 0x1000 and extends to 0x101F. In this case, nValue1 might be at location 0x1000 and nValue2 at location 0x1004.

remember.eps An int takes up 4 bytes in Code::Blocks. See Chapter 14 for details.

As part of making the call to fn(), C++ first stores the values of each argument on the stack — starting at the rightmost argument and working its way to the left.

technicalstuff.eps The last thing that C++ stores as part of making the call is the return address so that the function knows where to return to after it is complete.

For reasons that have more to do with the internal workings of the CPU, the stack “grows downward,” meaning that the memory used by fn() will have addresses smaller than 0x1000. Figure 17-3 shows the state of memory at the point that the computer processor reaches the first statement in fn(). C++ stored the second argument to the function at location 0x0FF4 and the first argument at 0x0FF0.

remember.eps Remember that this is just a possible layout of memory. I don’t know (or care) that any of these are in fact the actual addresses used by C++ in this or any other function call.

9781118823873-fg1703.tif

Figure 17-3: A possible layout of memory immediately after entering the function fn(int, int).

The function fn(int, int) contains two statements:

  nArg1 = 10;
nArg2 = 20;

Figure 17-4 shows the contents of memory immediately after these two statements are executed. Pretty simple, really — the value of nArg1 has changed to 10 and nArg2 to 20, just as you would expect. The main point of this demonstration, however, is to show that changing the value of nArg1 and nArg2 has no effect on the original variables back at nValue1 and nValue2.

9781118823873-fg1704.tif

Figure 17-4: The same memory locations immediately prior to returning from fn(int, int).

Passing arguments by reference

So what if I wanted the changes made by fn() to be permanent? I could do this by passing not the value of the variables but their address. This is demonstrated by the following snippet (also taken from the PassByReference example program):

  // fn(int*, int*) - this function takes its arguments
//                  by reference
void fn(int* pnArg1, int* pnArg2)
{
    // modify the value of the arguments
    *pnArg1 = 10;
    *pnArg2 = 20;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // initialize two variables and display their values
    int nValue1 = 1;
    int nValue2 = 2;

    fn(&nValue1, &nValue2);

    return 0;
}

Notice first that the arguments to fn() are now declared not to be integers but pointers to integers. The call to fn(int*, int*) passes not the values of the variables nValue1 and nValue2 but their addresses.

remember.eps In this example, the value of the expression &nValue1 is 0x1000, and the type is int* (which is pronounced “pointer to int”).

The state of memory upon entry into this function is shown in Figure 17-5.

9781118823873-fg1705.tif

Figure 17-5: The content of memory after the call to fn(int*, int*).

The function fn(int*, int*) now stores its values at the locations to which its arguments point:

  *pnArg1 = 10;
*pnArg2 = 20;

This first statement says “store the value 10 at the int location passed to me in the argument pnArg1.” This stores a 10 at location 0x1000, which happens to be the variable nValue1. This is demonstrated graphically in Figure 17-6.

9781118823873-fg1706.tif

Figure 17-6: The content memory immediately prior to returning from fn(int*, int*).

Putting it together

The complete PassByReference program appears as follows:

  //
//  PassByReference - demonstrate passing arguments to a
//                    function both by value and by
//                    reference.
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

// fn(int, int) - demonstrate a function that takes two
//                arguments and modifies their value
void fn(int nArg1, int nArg2)
{
    // modify the value of the arguments
    nArg1 = 10;
    nArg2 = 20;
}

// fn(int*, int*) - this function takes its arguments
//                  by reference
void fn(int* pnArg1, int* pnArg2)
{
    // modify the value of the arguments
    *pnArg1 = 10;
    *pnArg2 = 20;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // initialize two variables and display their values
    int nValue1 = 1;
    int nValue2 = 2;
    cout << "The value of nArg1 is " << nValue1 << endl;
    cout << "The value of nArg2 is " << nValue2 << endl;

    // now try to modify them by calling a function
    cout << "Calling fn(int, int)" << endl;
    fn(nValue1, nValue2);
    cout << "Returned from fn(int, int)" << endl;
    cout << "The value of nArg1 is " << nValue1 << endl;
    cout << "The value of nArg2 is " << nValue2 << endl;

    // try again by calling a function that takes
    // addresses as arguments
    cout << "Calling fn(int*, int*)" << endl;
    fn(&nValue1, &nValue2);
    cout << "Returned from fn(int*, int*)" << endl;
    cout << "The value of nArg1 is " << nValue1 << endl;
    cout << "The value of nArg2 is " << nValue2 << endl;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The following is the output from this program:

  The value of nArg1 is 1
The value of nArg2 is 2
Calling fn(int, int)
Returned from fn(int, int)
The value of nArg1 is 1
The value of nArg2 is 2
Calling fn(int*, int*)
Returned from fn(int*, int*)
The value of nArg1 is 10
The value of nArg2 is 20
Press Enter to continue …

This program declares the variables nValue1 and nValue2 and initializes them to 1 and 2, respectively. The program then displays their value just to make sure. Next, the program calls the fn(int, int), passing the value of the two variables. That function modifies the value of its arguments, but this has no effect on nValue1 and nValue2 as demonstrated by the fact that their value is unchanged after control returns to main().

The second call passes not the values of nValue1 and nValue2 but their addresses to the function fn(int*, int*). This time, the changes to pnArg1 and pnArg2 are retained even after control returns to main().

Notice that there is no confusion between the overloaded functions fn(int, int) and fn(int*, int*). The types of the arguments are easily distinguished.

Reference argument types

C++ provides a second way to pass arguments by reference: C++ allows variables — including arguments to functions — to be declared referential, as follows:

  void fn2(int& rnArg1, int& rnArg2)
{
    rnArg1 = 10;
    rnArg2 = 20;
}

int nValue1 = 1;
int nValue2 = 2;
fn2(nValue1, nValue2); // called just like fn(int, int)

// the values of nValue1 and nValue2 are now 10 and 20

The int& declares rnArg1 and rnArg2 to be references to int. Calling fn2() actually passes the address of nValue1 and nValue2 but C++ handles the pointer manipulation for you behind the curtains. Changes in fn2(int&, int&) are retained in the calling function.

The program PassByReference2 contained in the online material demonstrates a version of pass by reference, using the reference variable type.

warning.eps The reference is not part of the type. Therefore you cannot overload two functions that differ only insofar as one uses pass by value and the the other uses pass by reference:

  // the following two functions cannot be differentiated
// since they are called the same way
void fn(int  nArg1, int  nArg2);
void fn(int& nArg1, int& nArg2);

So why mess with pointer variables when the referential declaration can handle it all for you? The fact is that I recommend that beginning programmers avoid using referential variable types — C++ may be handling the pointer work for you, but you still have to understand what C++ is doing for you under the covers. Without a firm understanding of pointer types, the beginner has a hard time understanding errors generated by referential declarations.

Playing with Heaps of Memory

One of the problems addressed in Chapter 16 was that of fixed-size arrays. For example, the concatenate() function concatenated two ASCIIZ strings into a single string. However, the function had to be careful not to overrun the target array in case the array didn’t have enough room to hold the combined string. This problem would have gone away if concatenate() could have allocated a new array that was guaranteed to be large enough to hold the concatenated string.

That’s a great idea, but how big should I make this target array — 256 bytes, 512 bytes? There’s no right answer, because there’s no way to know at compile time how to make the target array big enough to hold all possible concatenated strings. You can’t know for sure until runtime how much memory you will need.

Do you really need a new keyword?

C++ provides an extra area in memory just for this purpose, known by the somewhat cryptic name of the heap. A programmer can allocate any amount of memory off of the heap by using the keyword new, as in the following example snippet:

  char* pArray = new char[256];

This example carves a block of memory large enough to hold 256 characters off of the heap. The new keyword returns a pointer to the newly created array. Unlike other variables, heap memory is not allocated until runtime, which means the array size is not limited to constants that are determined at compile time — they can also be variables that are computed at runtime.

warning.eps It may seem odd that the argument to new is an array while what is returned is a pointer. (I have a lot more to say about the relationship between pointers and arrays in the next chapter.) Consider that I could have said something like the following:

  int nSizeOfArray = someFunction();
char* pArray = new char[nSizeOfArray];

Here the size of the array is computed by someFunction(). Obviously this computation can’t occur until the program is actually executing. Whatever value someFunction() returns is used as the size of the array to be allocated in the next statement.

A more practical example is the following code snippet that makes a copy of an ASCIIZ string (assuming you consider copying a string as practical):

  int nLength = strlen(pszString) + 1;
char* pszCopy = new char[nLength];
strncpy(pszCopy, nLength, pszString);

The first statement calls the string function strlen(), which returns the length of the string passed it not including the terminating null character. The + 1 adds room for the terminating null. The next statement allocates room for the copy off of the heap. Finally, the third statement uses the string function strncpy() to copy the contents of pszString into pszCopy. By calculating how big an array you need to store the copy, you are guaranteed that pszCopy is large enough to hold the entire string.

Don’t forget to clean up after yourself

Allocating memory off of the heap is a neat feature, but it has one very big danger in C++: If you allocate memory off of the heap, you must remember to return it to the heap when you’re done using it.

You return memory to the heap by using the delete keyword, as in the following:

  char* pArray = new char[256];

// ...use the memory all you want...

// now return the memory block to the heap
delete[] pArray;
pArray = nullptr;

The delete[] keyword accepts a pointer to an array that has been passed to you from the new keyword and restores that memory to the heap.

remember.eps Use delete[] to return an array. Use delete (without the open and closed brackets) when returning a single object to the heap.

If you don’t return heap memory when you are done with it, your program will slowly consume memory and eventually slow down more and more as the operating system tries to fulfill its apparently insatiable gluttony. Eventually, the program will come to a halt when the OS can no longer satisfy its requests for memory.

Returning the same memory to the heap twice is not quite as bad: Doing so causes the program to crash almost immediately. It’s considered good programming practice to zero out a pointer, using the keyword nullptr, once you’ve deleted the memory block that it points to. You do this for two very good reasons:

  • Deleting a pointer that contains a nullptr has no effect.
  • nullptr is never a valid address. Trying to access memory at the nullptr location will always cause your program to crash immediately, which will tip you off that there’s a problem and make it a lot easier to find.

tip.eps You don’t have to delete memory if your program will exit soon — all heap memory is restored to the operating system when a program terminates. However, returning memory that you allocate off the heap is a very good habit to get into.

tip.eps The keyword nullptr was added by the 2011 C++ standard. If your compiler does not support nullptr, use 0 instead.

Looking at an example

The following ConcatenateHeap program is a version of the concatenate() function that allocates its memory from off the heap:

  //
//  ConcatenateHeap - similar to ConcatenateString except
//                    this version stores the concatenated
//                    string in memory allocated from the
//                    heap so that we are guaranteed
//                    that the target array is always
//                    large enough
//

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

// concatenateString - concatenate two strings together
//                     into an array allocated off of the
//                     heap
char* concatenateString(const char szSrc1[],
                        const char szSrc2[])
{
    // allocate an array of sufficient length
    int nTargetSize = strlen(szSrc1) + strlen(szSrc2) + 1;
    char* pszTarget = new char[nTargetSize];

    // first copy the first string into the target
    int nT;
    for(nT = 0; szSrc1[nT] != ''; nT++)
    {
        pszTarget[nT] = szSrc1[nT];
    }

    // now copy the contents of the second string onto
    // the end of the first
    for(int nS = 0; szSrc2[nS] != ''; nT++, nS++)
    {
        pszTarget[nT] = szSrc2[nS];
    }

    // add the terminator to szTarget
    pszTarget[nT] = '';

    // return the results to the caller
    return pszTarget;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // Prompt user
    cout << "This program accepts two strings "
         << "from the keyboard and outputs them "
         << "concatenated together. " << endl;

    // input two strings
    cout << "Enter first string: ";
    char szString1[256];
    cin.getline(szString1, 256);

    cout << "Enter the second string: ";
    char szString2[256];
    cin.getline(szString2, 256);

    // now concatenate one onto the end of the other
    cout << "Concatentate second string onto the first"
         << endl;
    char* pszT = concatenateString(szString1, szString2);

    // and display the result
    cout << "Result: <"
         << pszT
         << ">" << endl;

    // return the memory to the heap
    delete[] pszT;
    pszT = nullptr;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

This program includes the #include file cstring to gain access to the strlen() function. The concatenateString() function is similar to the earlier versions, except that it returns the address of a block of heap memory containing the concatenated string rather than modify either of the strings passed to it.

remember.eps Declaring the arguments as const means that the function promises not to modify them. This allows the function to be called with a const string as in the following snippet:

  char* pFullName = concatenateString("Mr. ", pszName);

The string "Mr. " is a const character array in the same sense that 1 is a const integer.

The first statement within concatenateString() calculates the size of the target array by calling strlen() on both source strings and adding 1 for the terminating null.

The next statement allocates an array of that size from off the heap, using the new keyword.

The two for loops work exactly like those in the earlier concatenate examples by copying first szSrc1 and then szSrc2 into the pszTarget array before tacking on the final terminating null.

The function then returns the address of the pszTarget array to the caller.

The main() function works the same as in the earlier Concatenate program by prompting the user for two strings and then displaying the concatenated result. The only difference is that this version returns the pointer returned by concatenateString() to the heap before terminating by executing the following snippet:

  delete[] pszT;
pszT = nullptr;

The output from running this program is indistinguishable from its earlier cousins:

  This program accepts two strings
from the keyboard and outputs them
concatenated together.

Enter first string: this is a string
Enter the second string: THIS IS ALSO A STRING
Concatentate second string onto the first
Result: <this is a stringTHIS IS ALSO A STRING>
Press Enter to continue …

The subject of C++ pointers is too vast to be handled in a single chapter. The next chapter examines the relationship between arrays and pointers, a topic I admittedly glossed over in the final example programs in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.206.225