|

Each
time you pass an object into a function by value,
a copy of the object is made. Each time you return
an object from a function by value, another copy
is made.
In
the "Extra Credit" section at the end of Unit 5,
you learned that these objects are copied onto the
stack. Doing so takes time and memory. For small
objects, such as the built-in integer values, this
is a trivial cost.
However,
with larger, user-created objects, the cost is greater.
The size of a user-created object on the stack is
the sum of each of its member variables. These,
in turn, can each be user-created objects, and passing
such a massive structure by copying it onto the
stack can be very expensive in performance and memory
consumption.
There
is another cost as well. With the classes you create,
each of these temporary copies is created when the
compiler calls a special constructor: the copy constructor.
Tomorrow you will learn how copy constructors work
and how you can make your own, but for now it is
enough to know that the copy constructor is called
each time a temporary copy of the object is put
on the stack.
When
the temporary object is destroyed, which happens
when the function returns, the object’s destructor
is called. If an object is returned by the function
by value, a copy of that object must be made and
destroyed as well.
With
large objects, these constructor and destructor
calls can be expensive in speed and use of memory.
To illustrate this idea, Listing 9.9 creates a stripped-down
user-created object: SimpleCat. A real object would
be larger and more expensive, but this is sufficient
to show how often the copy constructor and destructor
are called.
Listing
9.10 creates the SimpleCat object and then calls
two functions. The first function receives the Cat
by value and then returns it by value. The second
one receives a pointer to the object, rather than
the object itself, and returns a pointer to the
object.
Listing
9.10. Passing objects by reference.
1:
//Listing 9.10
2:
// Passing pointers to objects
3:
4:
#include <iostream.h>
5:
6:
class SimpleCat
7:
{
8:
public:
9:
SimpleCat (); // constructor
10:
SimpleCat(SimpleCat&); // copy constructor
11:
~SimpleCat(); // destructor
12:
};
13:
14:
SimpleCat::SimpleCat()
15:
{
16:
cout << "Simple Cat Constructor...\n";
17:
}
18:
19:
SimpleCat::SimpleCat(SimpleCat&)
20:
{
21:
cout << "Simple Cat Copy Constructor...\n";
22:
}
23:
24:
SimpleCat::~SimpleCat()
25:
{
26:
cout << "Simple Cat Destructor...\n";
27:
}
28:
29:
SimpleCat FunctionOne (SimpleCat theCat);
30:
SimpleCat* FunctionTwo (SimpleCat *theCat);
31:
32:
int main()
33:
{
34:
cout << "Making a cat...\n";
35:
SimpleCat Frisky;
36:
cout << "Calling FunctionOne...\n";
37:
FunctionOne(Frisky);
38:
cout << "Calling FunctionTwo...\n";
39:
FunctionTwo(&Frisky);
40:
return 0;
41:
}
42:
43:
// FunctionOne, passes by value
44:
SimpleCat FunctionOne(SimpleCat theCat)
45:
{
46:
cout << "Function One. Returning...\n";
47:
return theCat;
48:
}
49:
50:
// functionTwo, passes by reference
51:
SimpleCat* FunctionTwo (SimpleCat *theCat)
52:
{
53:
cout << "Function Two. Returning...\n";
54:
return theCat;
55:
}
OUTPUT:
1: Making a cat...
2: Simple Cat Constructor...
3: Calling FunctionOne...
4: Simple Cat Copy Constructor...
5: Function One. Returning...
6: Simple Cat Copy Constructor...
7: Simple Cat Destructor...
8: Simple Cat Destructor...
9: Calling FunctionTwo...
10: Function Two. Returning...
11: Simple Cat Destructor...
NOTE: Line numbers will not print.
They were added to aid in the analysis.
ANALYSIS: A very simplified SimpleCat
class is declared on lines 6-12. The constructor,
copy constructor, and destructor all print an informative
message so that you can tell when they’ve been called.
On line 34, main() prints out a message, and that
is seen on output line 1. On line 35, a SimpleCat
object is instantiated. This causes the constructor
to be called, and the output from the constructor
is seen on output line 2.
On
line 36, main() reports that it is calling FunctionOne,
which creates output line 3. Because FunctionOne()
is called passing the SimpleCat object by value,
a copy of the SimpleCat object is made on the stack
as an object local to the called function. This
causes the copy constructor to be called, which
creates output line 4.
Program
execution jumps to line 46 in the called function,
which prints an informative message, output line
5. The function then returns, and returns the SimpleCat
object by value. This creates yet another copy of
the object, calling the copy constructor and producing
line 6.
The
return value from FunctionOne() is not assigned
to any object, and so the temporary created for
the return is thrown away, calling the destructor,
which produces output line 7. Since FunctionOne()
has ended, its local copy goes out of scope and
is destroyed, calling the destructor and producing
line 8.
Program
execution returns to main(), and FunctionTwo() is
called, but the parameter is passed by reference.
No copy is produced, so there’s no output. FunctionTwo()
prints the message that appears as output line 10
and then returns the SimpleCat object, again by
reference, and so again produces no calls to the
constructor or destructor.
Finally,
the program ends and Frisky goes out of scope, causing
one final call to the destructor and printing output
line 11.
The
net effect of this is that the call to FunctionOne(),
because it passed the cat by value, produced two
calls to the copy constructor and two to the destructor,
while the call to FunctionTwo() produced none.
Passing
a const Pointer
Although
passing a pointer to FunctionTwo() is more efficient,
it is dangerous. FunctionTwo() is not allowed to
change the SimpleCat object it is passed, yet it
is given the address of the SimpleCat. This seriously
exposes the object to change and defeats the protection
offered in passing by value.
Passing
by value is like giving a museum a photograph of
your masterpiece instead of the real thing. If vandals
mark it up, there is no harm done to the original.
Passing by reference is like sending your home address
to the museum and inviting guests to come over and
look at the real thing.
The
solution is to pass a const pointer to SimpleCat.
Doing so prevents calling any non-const method on
SimpleCat, and thus protects the object from change.
Listing 9.11 demonstrates this idea.
Listing
9.11. Passing const pointers.
1:
//Listing 9.11
2:
// Passing pointers to objects
3:
4:
#include <iostream.h>
5:
6:
class SimpleCat
7:
{
8:
public:
9:
SimpleCat();
10:
SimpleCat(SimpleCat&);
11:
~SimpleCat();
12:
13:
int GetAge() const { return itsAge; }
14:
void SetAge(int age) { itsAge = age; }
15:
16:
private:
17:
int itsAge;
18:
};
19:
20:
SimpleCat::SimpleCat()
21:
{
22:
cout << "Simple Cat Constructor...\n";
23:
itsAge = 1;
24:
}
25:
26:
SimpleCat::SimpleCat(SimpleCat&)
27:
{
28:
cout << "Simple Cat Copy Constructor...\n";
29:
}
30:
31:
SimpleCat::~SimpleCat()
32:
{
33:
cout << "Simple Cat Destructor...\n";
34:
}
35:
36:const
SimpleCat * const FunctionTwo (const SimpleCat *
const theCat);
37:
38:
int main()
39:
{
40:
cout << "Making a cat...\n";
41:
SimpleCat Frisky;
42:
cout << "Frisky is " ;
43
cout << Frisky.GetAge();
44:
cout << " years _old\n";
45:
int age = 5;
46:
Frisky.SetAge(age);
47:
cout << "Frisky is " ;
48
cout << Frisky.GetAge();
49:
cout << " years _old\n";
50:
cout << "Calling FunctionTwo...\n";
51:
FunctionTwo(&Frisky);
52:
cout << "Frisky is " ;
53
cout << Frisky.GetAge();
54:
cout << " years _old\n";
55:
return 0;
56:
}
57:
58:
// functionTwo, passes a const pointer
59:
const SimpleCat * const FunctionTwo (const SimpleCat
* const theCat)
60:
{
61:
cout << "Function Two. Returning...\n";
62:
cout << "Frisky is now " << theCat->GetAge();
63:
cout << " years old \n";
64:
// theCat->SetAge(8); const!
65:
return theCat;
66:
}
OUTPUT:
Making a cat...
Simple Cat constructor...
Frisky is 1 years old
Frisky is 5 years old
Calling FunctionTwo...
FunctionTwo. Returning...
Frisky is now 5 years old
Frisky is 5 years old
Simple Cat Destructor...
ANALYSIS: SimpleCat has added two accessor
functions, GetAge() on line 13, which is a const
function, and SetAge() on line 14, which is not
a const function. It has also added the member variable
itsAge on line 17. The constructor, copy constructor,
and destructor are still defined to print their
messages. The copy constructor is never called,
however, because the object is passed by reference
and so no copies are made. On line 41, an object
is created, and its default age is printed, starting
on line 42.
On
line 46, itsAge is set using the accessor SetAge,
and the result is printed on line 47. FunctionOne
is not used in this program, but FunctionTwo() is
called. FunctionTwo() has changed slightly; the
parameter and return value are now declared, on
line 36, to take a constant pointer to a constant
object and to return a constant pointer to a constant
object.
Because
the parameter and return value are still passed
by reference, no copies are made and the copy constructor
is not called. The pointer in FunctionTwo(), however,
is now constant, and thus cannot call the non-const
method, SetAge(). If the call to SetAge() on line
64 was not commented out, the program would not
compile.
Note
that the object created in main() is not constant,
and Frisky can call SetAge(). The address of this
non-constant object is passed to FunctionTwo(),
but because FunctionTwo()’s declaration declares
the pointer to be a constant pointer, the object
is treated as if it were constant!
References
as an Alternative
Listing
9.11 solves the problem of making extra copies,
and thus saves the calls to the copy constructor
and destructor. It uses constant pointers to constant
objects, and thereby solves the problem of the function
changing the object. It is still somewhat cumbersome,
however, because the objects passed to the function
are pointers.
Since
you know the object will never be null, it would
be easier to work with in the function if a reference
were passed in, rather than a pointer. Listing 9.12
illustrates this.
Listing
9.12. Passing references to objects.
1:
//Listing 9.12
2:
// Passing references to objects
3:
4:
#include <iostream.h>
5:
6:
class SimpleCat
7:
{
8:
public:
9:
SimpleCat();
10:
SimpleCat(SimpleCat&);
11:
~SimpleCat();
12:
13:
int GetAge() const { return itsAge; }
14:
void SetAge(int age) { itsAge = age; }
15:
16:
private:
17:
int itsAge;
18:
};
19:
20:
SimpleCat::SimpleCat()
21:
{
22:
cout << "Simple Cat Constructor...\n";
23:
itsAge = 1;
24:
}
25:
26:
SimpleCat::SimpleCat(SimpleCat&)
27:
{
28:
cout << "Simple Cat Copy Constructor...\n";
29:
}
30:
31:
SimpleCat::~SimpleCat()
32:
{
33:
cout << "Simple Cat Destructor...\n";
34:
}
35:
36:
const SimpleCat & FunctionTwo (const SimpleCat
& theCat);
37:
38:
int main()
39:
{
40:
cout << "Making a cat...\n";
41:
SimpleCat Frisky;
42:
cout << "Frisky is " << Frisky.GetAge()
<< " years old\n";
43:
int age = 5;
44:
Frisky.SetAge(age);
45:
cout << "Frisky is " << Frisky.GetAge()
<< " years old\n";
46:
cout << "Calling FunctionTwo...\n";
47:
FunctionTwo(Frisky);
48:
cout << "Frisky is " << Frisky.GetAge()
<< " years old\n";
49:
return 0;
50:
}
51:
52:
// functionTwo, passes a ref to a const object
53:
const SimpleCat & FunctionTwo (const SimpleCat
& theCat)
54:
{
55:
cout << "Function Two. Returning...\n";
56:
cout << "Frisky is now " << theCat.GetAge();
57:
cout << " years old \n";
58:
// theCat.SetAge(8); const!
59:
return theCat;
60:
}
OUTPUT:
Making a cat...
Simple Cat constructor...
Frisky is 1 years old
Frisky is 5 years old
Calling FunctionTwo...
FunctionTwo. Returning...
Frisky is now 5 years old
Frisky is 5 years old
Simple Cat Destructor...
ANALYSIS: The output is identical to
that produced by Listing 9.11. The only significant
change is that FunctionTwo() now takes and returns
a reference to a constant object. Once again, working
with references is somewhat simpler than working
with pointers, and the same savings and efficiency
are achieved, as well as the safety provided by
using const.
const
References
C++
programmers do not usually differentiate between
"constant reference to a SimpleCat object" and "reference
to a constant SimpleCat object." References themselves
can never be reassigned to refer to another object,
and so are always constant. If the keyword const
is applied to a reference, it is to make the object
referred to constant.
When
to Use References and When to Use Pointers
C++
programmers strongly prefer references to pointers.
References are cleaner and easier to use, and they
do a better job of hiding information, as we saw
in the previous example.
References
cannot be reassigned, however. If you need to point
first to one object and then another, you must use
a pointer. References cannot be null, so if there
is any chance that the object in question may be
null, you must not use a reference. You must use
a pointer.
An
example of the latter concern is the operator new.
If new cannot allocate memory on the free store,
it returns a null pointer. Since a reference can’t
be null, you must not initialize a reference to
this memory until you’ve checked that it is not
null. The following example shows how to handle
this:
int
*pInt = new int;
if
(pInt != NULL)
int
&rInt = *pInt;
In
this example a pointer to int, pInt, is declared
and initialized with the memory
returned by the operator new. The address in pInt
is tested, and if it is not null, pInt is dereferenced.
The result of dereferencing an int variable is an
int object, and rInt is initialized to refer to
that object. Thus, rInt becomes an alias to the
int returned by the operator new.
DO
pass parameters by reference whenever possible.
DO return by reference whenever possible. DON’T
use pointers if references will work. DO
use const to protect references and pointers whenever
possible. DON’T return a reference to a local
object.
|