Wrox Press C++ Tutorial
Using pointers, and particularly references to class objects, is very important to object-oriented programming. Class objects can involve considerable amounts of data, so using a pass-by-value mechanism by specifying objects as parameters can be very time consuming and inefficient. There are also some techniques involving the use of references which are essential to some operations with classes. As we shall see, you can't write a copy constructor without using a reference parameter.
You declare a pointer to a class object in the same way that you declare other pointers. For example, a pointer to objects of the Box class is declared in this statement:
Box* pBox = 0; // Declare a pointer to Box
You can now use this to store the address of a Box object in an assignment in the usual way, using the address operator:
pBox = &Cigar; // Store address of Box object Cigar in pBox
As we saw when we used the this pointer in the definition of the member function compare(), you can call a function using a pointer to an object. We can call the function Volume() for the pointer pBox in this statement:
// Display volume of object pointed to by pBox
cout << pBox->Volume();
Again, this uses the indirect member access operator. This is the typical notation used by most programmers for this kind of operation, so from now on we'll use it universally.
Let's try exercising the indirect member access operator a little more. We will use the example Ex6_09.cpp as a base, but change it a little.
// Ex6_12.cpp
// Exercising the indirect member access operator
#include <iostream>
using namespace std;
class Box // Class definition at global scope
{
public:
// Constructor definition
Box(double lv=1.0, double bv=1.0, double hv=1.0)
{
cout << endl << "Constructor called.";
length = lv; // Set values of
breadth = bv; // data members
height = hv;
}
// Function to calculate the volume of a box
double Volume()
{
return length * breadth * height;
}
// Function to compare two boxes which
// returns true if the first is greater
// than the second, and false otherwise
int compare(Box* pBox)
{
return this->Volume() > pBox->Volume();
}
private:
double length; // Length of a box in inches
double breadth; // Breadth of a box in inches
double height; // Height of a box in inches
};
int main(void)
{
Box Boxes[5]; // Array of Box objects declared
Box Match(2.2, 1.1, 0.5); // Declare Match box
Box Cigar(8.0, 5.0,1.0); // Declare Cigar Box
// Initialize pointer to Cigar object address
Box* pB1 = &Cigar;
Box* pB2 = 0; // Pointer to Box initialized to null
cout << endl
<< "Address of Cigar is " << pB1 // Display address
<< endl
<< "Volume of Cigar is " << pB1->Volume(); // Volume of
// object pointed to
pB2 = &Match;
if(pB2->compare(pB1)) // Compare via pointers
cout << endl
<< "Match is greater than Cigar";
else
cout << endl
<< "Match is less than or equal to Cigar";
pB1 = Boxes; // Set to address of array
Boxes[2] = Match; // Set 3rd element to Match
cout << endl // Now access thru pointer
<< "Volume of Boxes[2] is " << (pB1 + 2)->Volume();
cout << endl;
return 0;
}
The change to the class definition is not one of great substance. We've only modified the compare() function to accept a pointer to a Box object as an argument. The function main() merely exercises pointers to Box type objects in various, rather arbitrary, ways.
Within the function main(), after declaring an array Boxes, and the Box objects Cigar and Match, we declare two pointers to Box objects. The first, pB1, is initialized with the address of the object Cigar, and the second, pB2, is initialized to NULL. All of this uses the pointer in exactly the same way you would when you're applying a pointer to a basic type. The fact that we're using a pointer to a type that we have defined ourselves makes no difference.
We use pB1 with the indirect member access operator to generate the volume of the object pointed to, and the result is displayed. We then assign the address of Match to pB2 and use both pointers in calling the compare function. Because the argument of the function compare() is a pointer to a Box object, the function uses the indirect member access operator in calling the Volume() function for the object.
To demonstrate that we can use address arithmetic on the pointer pB1 when using it to select the member function, we set pB1 to the address of the first element of the array of type Box, Boxes. In this case, we select the third element of the array and calculate its volume. This is the same as the volume of Match.
If you run the example, the output window will look something like that shown below:

Of course, the value of the address for the Cigar object may well be different on your PC.
You can see that there were seven calls of the constructor for Box objects: five due to the array Boxes, plus one each for the objects Cigar and Match.
Overall, there's virtually no difference between using a pointer to a class object and using a pointer to a basic type, such as double.
References really come into their own when they are used with classes. As with pointers, there is virtually no difference between the way you declare and use references to class objects and the way in which we've already declared and used references to variables of basic types. To declare a reference to the object Cigar, for instance, we would write this:
Box& rCigar = Cigar; // Define reference to object Cigar
To use a reference to calculate the volume of the object Cigar, you would just use the reference name where the object name would otherwise appear:
// Output volume of Cigar thru a reference
cout << rCigar.Volume();
As you may remember, a reference acts as an alias for the object it refers to, so the usage is exactly the same as using the original object name.
The importance of references is really in the context of arguments and return values in functions, particularly class member functions. Let's return to the question of the copy constructor as a first toe in the water. For the moment, we'll sidestep the question of when you need to write your own copy constructor and concentrate on the problem of how you can write one. We'll use the class Box just to make the discussion more concrete.
The copy constructor is a constructor which creates an object by initializing it with an object of the same class, which has been created previously. It therefore needs to accept such an object as an argument. We might consider writing the prototype like this:
Box(Box initB);
Let's now consider what happens when this constructor is called. If we write this declaration,
Box myBox = Cigar;
this will generate a call of the copy constructor as follows:
Box::Box(Cigar);
This seems to be no problem, until you realize that the argument is passed by value. So, before the object Cigar can be passed, the compiler needs to arrange to make a copy of it. Therefore, it calls the copy constructor to make a copy of the argument for the call of the copy constructor. Unfortunately, since it is passed by value, this call also needs a copy of its argument to be made, so the copy constructor is called... and so on and so on. We end up with an infinite number of calls to the copy constructor.
The solution, as I'm sure you have guessed, is to use a const reference parameter. We could write the prototype of the copy constructor like this:
Box(const Box& initB);
Now the argument to the copy constructor doesn't need to be copied. It will be used to initialize the reference parameter, so no copying takes place. As you will remember from our discussion on references, if a parameter to a function is a reference, no copying of the argument occurs when the function is called. The function accesses the argument variable in the caller function directly. The const qualifier ensures that the argument can't be modified in the function.
This is an important use of the qualifier const. You should always declare a parameter of a function as const unless the function is intended to modify it.
We could implement the copy constructor as follows:
Box::Box(const Box& initB)
{
length = initB.length;
breadth = initB.breadth;
height = initB.height;
}
This definition of the copy constructor assumes that it appears outside of the class definition. The name is therefore qualified with the class name using the scope resolution operator. Each data member of the object being created is initialized with the corresponding member of the object passed as an argument. Of course, we could equally well use the initialization list to set the values of the object.
This case is not an example of when we need to write a copy constructor. As we have seen, the default copy constructor works perfectly well with Box objects. We will get to why and when we need to write our own copy constructor in the next chapter.