Chapter 23
In This Chapter
Defining pointers to objects
Invoking a member function through a pointer
Passing pointers to objects to functions
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.
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;
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.
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 pointers to functions is just one of the many ways to entertain yourself with pointers.
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:
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.
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);
}
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.
This addresses both of the problems with passing a copy:
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.
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.
3.141.28.107