Chapter 23

Pointers to Objects

In This Chapter

arrow Defining pointers to objects

arrow Invoking a member function through a pointer

arrow Passing pointers to objects to functions

arrow Examining this more closely

Chapters 17 and 18 focus on various aspects of the care and feeding of pointers. Surely, you think, nothing more can be said on the subject. But I don’t introduce the concept of classes before those chapters. In this chapter, I describe the intersection of pointer variables and object-oriented programming. This chapter deals with the concept of pointers to class objects. I describe how to create one, how to use it, and how to delete it once you’re finished with it.

Pointers to Objects

A pointer to a programmer-defined type such as a class works essentially the same as a pointer to an intrinsic type:

  int nInt;
int* pInt = &nInt;

class Savings
{
  public:
    int nAccountNumber;
    double dBalance;
};
Savings s;
Savings* pS = &s;

The first pair of declarations defines an integer, nInt, and a pointer to an integer, pInt. The pointer pInt is initialized to point to the integer nInt.

Similarly, the second pair of declarations creates a Savings object s. It then declares a pointer to a Savings object, pS, and initializes it to the address of s.

The type of pS is “pointer to Savings” which is written Savings*.

I feel like the late Billy Mays when I say, “But wait! There’s more!” The similarities continue. The following statement assigns the value 1 to the int pointed at by pInt:

  *pInt = 1;

Similarly, the following assigns values to the account number and balance of the Savings object pointed at by pS.

  (*pS).nAccountNumber = 1234;
(*pS).dBalance = 0.0;

remember.eps The parentheses are required because the precedence of . is higher than that of *. Without the parentheses, *pS.nAccountNumber = 1234 would be interpreted as *(pS.nAccountNumber) = 1234, which means “store 1234 at the location pointed at by pS.nAccountNumber.” This generates a compiler error because nAccountNumber isn’t a pointer (nor is pS a Savings).

Arrow syntax

The only thing that I can figure is that the authors of the C language couldn’t type very well. They wasted no efforts in finding shorthand ways of saying things. Here is another case where they made up a shorthand way to save keystrokes, inventing a new operator -> to stand for *():

  pS->dBalance = 0.0; // same as (*pS).dBalance = 0.0

Even though the two are equivalent, the arrow operator is used almost exclusively because it’s easier to read (and type). Don’t lose sight of the fact, however, that the two forms are completely equivalent.

Calling all member functions

The syntax for invoking a member function with a pointer is similar to accessing a data member:

  class Savings
{
  public:
    int nAccountNumber;
    double dBalance;

    double withdraw(double dAmount);
    double deposit(double dAmount);
};

void fn()
{
    Savings s = {1234, 0.0};
    Savings* pS = &s;

    // deposit money into the account pointed at by pS
    pS->deposit(100.0);
}

The last statement in this snippet says “invoke the deposit() member function on the object pointed at by pS.”

Passing Objects to Functions

Passing pointers to functions is just one of the many ways to entertain yourself with pointers.

Calling a function with an object value

As you know, C++ passes arguments to functions by value by default. If you don’t know that, refer to Chapter 11. Complex, user-defined objects are passed by value as well:

  class Savings
{
  public:
    int nAccountNumber;
    double dBalance;

    double withdraw(double dAmount);
    double deposit(double dAmount);
};

void someOtherFunction(Savings s)
{
    s.deposit(100.0);
}

void someFunction()
{
    Savings s = {1234, 0.0};

    someOtherFunction(s);
}

Here the function someFunction() creates and initializes a Savings object s. It then passes a copy of that object to someOtherFunction(). The fact that it’s a copy is important for two reasons:

  • Making copies of large objects can be very inefficient, causing your program to run slower.
  • Changes made to copies don’t have any effect on the original object in the calling function.

In this case, the second problem is much worse than the former. I can stand a little bit of inefficiency — a Savings object isn’t very big anyway — but the deposit made in someOtherFunction() got booked against a copy of the original account. My Savings account back in someFunction() still has a balance of zero. This is shown graphically in Figure 23-1.

Calling a function with an object pointer

The programmer can pass the address of an object rather than the object itself, as demonstrated in the following example:

  class Savings
{
  public:
    int nAccountNumber;
    double dBalance;

    double withdraw(double dAmount);
    double deposit(double dAmount);
};

void someOtherFunction(Savings* pS)
{
    pS->deposit(100.0);
}

void someFunction()
{
    Savings s = {1234, 0.0};

    someOtherFunction(&s);
}

9781118823873-fg2301.tif

Figure 23-1: By default, C++ passes a copy of the Student object s to someOtherFunction().

The type of the argument to someOtherFunction() is “pointer to Savings.” This is reflected in the way that someFunction() performs the call, passing not the object s but the address of the object, &s. This is shown graphically in Figure 23-2.

9781118823873-fg2302.tif

Figure 23-2: By passing the address of the original Savings object, the programmer can avoid creating a copy of the original object.

This addresses both of the problems with passing a copy:

  • No matter how large and complicated the object might be, the call passes only a single address.
  • Changes made in someOtherFunction() are permanent because they refer to the original object and not a copy.

Looking at an example

The following program demonstrates the difference between passing an object by value versus passing the address of an object:

  //
//  PassObjects - this program demonstrates passing an
//                object by value versus passing the
//                address of the object
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;


// Savings - a simple savings account class
class Savings
{
  public:
    int    nAccountNumber;
    double dBalance;

    // deposit - deposit an amount to the balance;
    //           deposits must be positive number; return
    //           the resulting balance or zero on error
    double deposit(double dAmount)
    {
        // no negative deposits - that's a withdrawal
        if (dAmount < 0)
        {
            return 0.0;
        }

        // okay - add to the balance and return the total
        dBalance += dAmount;
        return dBalance;
    }

    // withdraw - execute a withdrawal if sufficient funds
    //            are available
    double withdraw(double dAmount)
    {
        if (dBalance < dAmount)
        {
            return 0.0;
        }

        dBalance -= dAmount;
        return dBalance;
    }

    // balance - return the balance of the current object
    double balance()
    {
        return dBalance;
    }
};

// someFunction(Savings) - accept object by value
void someFunction(Savings s)
{
    cout << "In someFunction(Savings)" << endl;

    cout << "Depositing $100" << endl;
    s.deposit(100.0);

    cout << "Balance in someFunction(Savings) is "
         << s.balance() << endl;
}

// someFunction(Savings*) - accept address of object
void someFunction(Savings* pS)
{
    cout << "In someFunction(Savings*)" << endl;

    cout << "Depositing $100" << endl;
    pS->deposit(100.0);

    cout << "Balance in someFunction(Savings) is "
         << pS->balance() << endl;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    Savings s = {0, 0.0};

    // first, pass by value
    someFunction(s);
    cout << "Balance back in main() is "
         << s.balance() << endl;

    // now pass the address
    someFunction(&s);
    cout << "Balance back in main() is "
         << s.balance() << 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;
}

This program starts by defining a conventional Savings class with deposit(), withdrawal(), and balance() member functions (the last one just returns the current balance).

The program then defines two overloaded functions someFunction(), one of which accepts as its argument an object of type Savings and the second a pointer to an object of type Savings (written Savings*). Both functions do the same things, first outputting a “Here I am” message and then depositing $100 to the account.

The main() program creates a Savings object s, which it first passes to someFunction(Savings). It then passes the address of the s object to someFunction(Savings*).

The output from this program appears as follows:

  In someFunction(Savings)
Depositing $100
Balance in someFunction(Savings) is 100
Balance back in main() is 0
In someFunction(Savings*)
Depositing $100
Balance in someFunction(Savings) is 100
Balance back in main() is 100
Press Enter to continue …

Notice how both functions deposit $100 into a Savings account object. However, since someFunction(Savings) makes the deposit into a copy, the original s object back in main() is left unchanged as demonstrated by the zero balance.

By passing the address of s to someFunction(Savings*), the program allows that function to modify the original object so the value “stays modified” in main() as demonstrated by the fact that the balance is $100 after control returns.

Allocating Objects off the Heap

You can allocate objects off the heap by using the new keyword, as shown in the following example:

  Savings* newSavings(int nAccountNum)
{
    Savings* pS = new Savings;
    pS->nAccountNumber = nAccountNum;
    pS->dBalance = 0.0;
    return pS;
}

Here the function allocates a new object of class Savings and then initializes it with the account number (passed as an argument) and a zero balance.

This is useful when you don’t know how many objects you are going to need, as in the dynamically sized character arrays described in Chapter 18. In that case, I first count how many characters I need room for, and then allocate an array of appropriate size off the heap.

In this present example, I can determine how many Savings accounts I need in memory at one time and allocate them dynamically off the heap.

technicalstuff.eps Of course, there is the little matter of how to store an unknown quantity of objects. C++ provides several variable-size data structures, in addition to the fixed-size array, as part of the Standard Template Library. A general discussion of the STL is beyond the scope of this book, but feel free to take comfort from the fact that it exists.

warning.eps You must return every object that you allocate off the heap; to do so, you pass the unmodified address of that object to the keyword delete. Otherwise your program will slowly run out of memory and die a horrible death.

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

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