Hour 13. Developing Advanced References and Pointers

Passing by Reference for Efficiency

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.

With larger, user-created objects, the cost of these copies is substantial. You’ll use more memory than you need to, and your program will run more slowly.

The size of a user-created object on the stack is the sum of each of its member variables. These, in turn, can each become user-created objects. Passing such a massive structure by copying it onto the stack can be expensive in terms of performance and memory consumption.

There is another cost, too. With the classes you create, each of these temporary copies is created when the compiler calls a special constructor: the copy constructor.

In Hour 14, “Calling Advanced Functions,” you learn how copy constructors work and how you can make your own. 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 value, a copy of that object must also be made and destroyed.

With large objects, these constructor and destructor calls can be expensive in speed and use of memory. To illustrate this idea, the ObjectRef program in Listing 13.1 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.

The program creates the SimpleCat object and then calls two functions. The first function receives the SimpleCat by value and then returns it by value. The second one takes its argument by reference, meaning it receives a pointer to the object, rather than the object itself, and returns a pointer to the object.

Passing by reference avoids creating the copy and calling the copy constructor, and is therefore generally more efficient. On the other hand, it also passes the object itself, and thus exposes that object to change in the called function.

Listing 13.1 The Full Text of ObjectRef.cpp


 1: #include <iostream>
 2:
 3: class SimpleCat
 4: {
 5: public:
 6:     SimpleCat();               // constructor
 7:     SimpleCat(SimpleCat&);     // copy constructor
 8:     ~SimpleCat();              // destructor
 9: };
10:
11: SimpleCat::SimpleCat()
12: {
13:     std::cout << "Simple Cat Constructor ... ";
14: }
15:
16: SimpleCat::SimpleCat(SimpleCat&)
17: {
18:     std::cout << "Simple Cat Copy Constructor ... ";
19: }
20:
21: SimpleCat::~SimpleCat()
22: {
23:     std::cout << "Simple Cat Destructor ... ";
24: }
25:
26: SimpleCat FunctionOne(SimpleCat theCat);
27: SimpleCat* FunctionTwo(SimpleCat *theCat);
28:
29: int main()
30: {
31:     std::cout << "Making a cat ... ";
32:     SimpleCat Frisky;
33:     std::cout << "Calling FunctionOne ... ";
34:     FunctionOne(Frisky);
35:     std::cout << "Calling FunctionTwo ... ";
36:     FunctionTwo(&Frisky);
37:     return 0;
38: }
39:
40: // FunctionOne, passes by value
41: SimpleCat FunctionOne(SimpleCat theCat)
42: {
43:     std::cout << "Function One. Returning ... ";
44:     return theCat;
45: }
46:
47: // functionTwo, passes by reference
48: SimpleCat* FunctionTwo (SimpleCat *theCat)
49: {
50:     std::cout << "Function Two. Returning ... ";
51:     return theCat;
52: }


The following output is displayed:

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 ...


By the Way

The line numbers shown here do not display. They are added to aid in the analysis in the text only.


A very simplified SimpleCat class is declared on lines 3–9. The constructor, copy constructor, and destructor all print an informative message so that you can tell when they’ve been called.

On line 31, main() prints out a message; you can see it on output line 1. On line 32, a SimpleCat object is instantiated. This causes the constructor to be called, and the output from the constructor is shown on output line 2.

On line 33, 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 43 in the called function, which prints an informative message (output line 5). The function then returns, returning 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, so the temporary object created for the return is thrown away, calling the destructor, which produces output line 7. Because 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 call to FunctionOne(), because it passed the cat by value, produced two calls to the copy constructor and two to the destructor, although the call to FunctionTwo() produced none.

Passing a const Pointer

Though passing a pointer to FunctionTwo() is more efficient, it is dangerous. FunctionTwo() is not supposed to change the SimpleCat object it is passed, yet it is given the address of the SimpleCat. This exposes the object to impermissible change and defeats the protection offered in passing by value.

Passing by value is like giving a museum a photograph of your masterpiece rather than the real thing. If vandals mark it up, no harm occurs 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.

If you want to provide the security of pass by value and the efficiency of pass by reference, the solution is to pass a const pointer to SimpleCat. Doing so prevents calling any non-const member function on SimpleCat, and thus protects the object from change. The ConstPasser program in Listing 13.2 demonstrates this idea.

Listing 13.2 The Full Text of ConstPasser.cpp


 1: #include <iostream>
 2:
 3: class SimpleCat
 4: {
 5: public:
 6:     SimpleCat();
 7:     SimpleCat(SimpleCat&);
 8:     ~SimpleCat();
 9:
10:     int GetAge() const { return itsAge; }
11:     void SetAge(int age) { itsAge = age; }
12:
13: private:
14:     int itsAge;
15: };
16:
17: SimpleCat::SimpleCat()
18: {
19:     std::cout << "Simple Cat Constructor ... ";
20:     itsAge = 1;
21: }
22:
23: SimpleCat::SimpleCat(SimpleCat&)
24: {
25:     std::cout << "Simple Cat Copy Constructor ... ";
26: }
27:
28: SimpleCat::~SimpleCat()
29: {
30:     std::cout << "Simple Cat Destructor ... ";
31: }
32:
33: const SimpleCat * const
34: FunctionTwo (const SimpleCat *const theCat);
35:
36: int main()
37: {
38:     std::cout << "Making a cat ... ";
39:     SimpleCat Frisky;
40:     std::cout << "Frisky is ";
41:     std::cout << Frisky.GetAge() << " years old ";
42:     int age = 5;
43:     Frisky.SetAge(age);
44:     std::cout << "Frisky is ";
45:     std::cout << Frisky.GetAge() << " years old ";
46:     std::cout << "Calling FunctionTwo ... ";
47:     FunctionTwo(&Frisky);
48:     std::cout << "Frisky is ";
49:     std::cout << Frisky.GetAge() << " years old ";
50:     return 0;
51: }
52:
53: // functionTwo, passes a const pointer
54: const SimpleCat * const
55: FunctionTwo (const SimpleCat * const theCat)
56: {
57:     std::cout << "Function Two. Returning ... ";
58:     std::cout << "Frisky is now " << theCat->GetAge();
59:     std::cout << " years old ";
60:     // theCat->SetAge(8); const!
61:     return theCat;
62: }


Here’s the output:

Making a cat...
Simple Cat Constructor...
Frisky is 1 years old
Frisky is 5 years old
Calling FunctionTwo...
Function Two. Returning...
Frisky is now 5 years old
Frisky is 5 years old
Simple Cat Destructor...

SimpleCat has added two accessor functions: GetAge() on line 10, which is a const function; and SetAge() on line 11, which is not. It has also added the member variable itsAge on line 14.

The constructor, copy constructor, and destructor are still defined to display their messages. The copy constructor is never called, however, because the object is passed by reference and no copies are made. On line 39, an object is created, and its default age is printed on lines 40 and 41.

On line 43, itsAge is set using the accessor SetAge(), and the result is displayed on lines 43 and 44. FunctionOne() is not used in this program, but FunctionTwo() is called.

FunctionTwo() has changed slightly; the parameter and return value are now declared, on lines 33 and 34, 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, therefore, cannot call the non-const member function, SetAge(). If the call to SetAge() on line 60 were 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 nonconstant object is passed to FunctionTwo(), but because the FunctionTwo() declaration declares the pointer to be a constant pointer, the object is treated as if it were constant.

References as an Alternative to Pointers

The ConstPasser program solves the problem of making extra copies, saving the calls to the copy constructor and destructor. It uses constant pointers to constant objects, thereby solving the problem of the called function making impermissible changes to the objects passed in as parameters. The method is still somewhat cumbersome, however, because the objects passed to the function are pointers.

Because you know the parameters will never be NULL, it is easier to work with the function if references are passed in rather than pointers. The RefPasser program in Listing 13.3 rewrites the previous project to use references rather than pointers.

Listing 13.3 The Full Text of RefPasser.cpp


 1: #include <iostream>
 2:
 3: class SimpleCat
 4: {
 5: public:
 6:     SimpleCat();
 7:     SimpleCat(SimpleCat&);
 8:     ~SimpleCat();
 9:
10:     int GetAge() const { return itsAge; }
11:     void SetAge(int age) { itsAge = age; }
12:
13: private:
14:     int itsAge;
15: };
16:
17: SimpleCat::SimpleCat()
18: {
19:     std::cout << "Simple Cat Constructor... ";
20:     itsAge = 1;
21: }
22:
23: SimpleCat::SimpleCat(SimpleCat&)
24: {
25:     std::cout << "Simple Cat Copy Constructor... ";
26: }
27:
28: SimpleCat::~SimpleCat()
29: {
30:     std::cout << "Simple Cat Destructor... ";
31: }
32:
33: const SimpleCat & FunctionTwo (const SimpleCat & theCat);
34:
35: int main()
36: {
37:     std::cout << "Making a cat... ";
38:     SimpleCat Frisky;
39:     std::cout << "Frisky is " << Frisky.GetAge()
40:               << " years old ";
41:
42:     int age = 5;
43:     Frisky.SetAge(age);
44:     std::cout << "Frisky is " << Frisky.GetAge()
45:               << " years old ";
46:
47:     std::cout << "Calling FunctionTwo... ";
48:     FunctionTwo(Frisky);
49:     std::cout << "Frisky is " << Frisky.GetAge()
50:               << " years old ";
51:     return 0;
52: }
53:
54: // functionTwo passes a ref to a const object
55: const SimpleCat & FunctionTwo (const SimpleCat & theCat)
56: {
57:     std::cout << "Function Two. Returning... ";
58:     std::cout << "Frisky is now " << theCat.GetAge()
59:               << " years old ";
60:     // theCat.SetAge(8);   const!
61:     return theCat;
62: }


This program has the following 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 ...

The output is identical to that produced by the previous program. 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, and the safety provided by using const, are achieved.

When to Use References and When to Use Pointers

Generally, C++ programmers strongly prefer references to pointers because they are cleaner and easier to use. References cannot be reassigned, however. If you need to point first to one object and then to another, you must use a pointer. References cannot be NULL, so if there is any chance that the object in question might be, you must use a pointer rather than a reference. If you want to allocate dynamic memory from the heap, you have to use pointers as discussed in previous hours.

Don’t Return a Reference to an Object That Isn’t in Scope!

After C++ programmers learn to pass by reference, they have a tendency to go hog-wild. It is possible, however, to overdo it. Remember that a reference always is an alias that refers to some other object. If you pass a reference into or out of a function, be sure to ask yourself, “What is the object I’m aliasing, and will it still exist every time it’s used?”

The ReturnRef program in Listing 13.4 illustrates the danger of returning a reference to an object that no longer exists.

Listing 13.4 The Full Text of ReturnRef.cpp


 1: #include <iostream>
 2:
 3: class SimpleCat
 4: {
 5: public:
 6:     SimpleCat(int age, int weight);
 7:     ~SimpleCat() {}
 8:     int GetAge() { return itsAge; }
 9:     int GetWeight() { return itsWeight; }
10: private:
11:     int itsAge;
12:     int itsWeight;
13: };
14:
15: SimpleCat::SimpleCat(int age, int weight):
16: itsAge(age), itsWeight(weight) {}
17:
18: SimpleCat &TheFunction();
19:
20: int main()
21: {
22:     SimpleCat &rCat = TheFunction();
23:     int age = rCat.GetAge();
24:     std::cout << "rCat is " << age << " years old! ";
25:     return 0;
26: }
27:
28: SimpleCat &TheFunction()
29: {
30:     SimpleCat Frisky(5,9);
31:     return Frisky;
32: }


When you build this program, you are confronted with an error about how a reference to the local variable Frisky is being returned.


Watch Out!

Some compilers are smart enough to see the reference to a null object and report a compile error. Other compilers will compile and even run; however, noted that this is a bad coding practice and that you should not take advantage of it when using a compiler that will allow you to do this.


On lines 3–13, SimpleCat is declared. On line 22, a reference to SimpleCat is initialized with the results of calling TheFunction(), which is declared on line 18 to return a reference to a SimpleCat.

The body of TheFunction() declares a local object of type SimpleCat and initializes its age and weight. It then returns that local object by reference. Some compilers are smart enough to catch this error and don’t let you run the program. Others let you run the program, but with unpredictable results. When TheFunction() returns, the local object, Frisky, is destroyed (painlessly, I assure you). The reference returned by this function is to a nonexistent object, and this is a bad thing.

Returning a Reference to an Object on the Heap

You might be tempted to solve the problem in RefReturn by having TheFunction() create Frisky on the heap. That way, when you return from TheFunction(), Frisky still exists.

The problem with this approach is this: What do you do with the memory allocated for Frisky when you have finished with it? The Leak program in Listing 13.5 illustrates this problem.

Listing 13.5 The Full Text of Leak.cpp


 1: #include <iostream>
 2:
 3: class SimpleCat
 4: {
 5: public:
 6:     SimpleCat (int age, int weight);
 7:     ~SimpleCat() {}
 8:     int GetAge() { return itsAge; }
 9:     int GetWeight() { return itsWeight; }
10:
11: private:
12:     int itsAge;
13:     int itsWeight;
14: };
15:
16: SimpleCat::SimpleCat(int age, int weight):
17: itsAge(age), itsWeight(weight) {}
18:
19: SimpleCat & TheFunction();
20:
21: int main()
22: {
23:     SimpleCat &rCat = TheFunction();
24:     int age = rCat.GetAge();
25:     std::cout << "rCat is " << age << " years old! ";
26:     std::cout << "&rCat: " << &rCat << " ";
27:     // How do you get rid of that memory?
28:     SimpleCat *pCat = &rCat;
29:     delete pCat;
30:     // Uh oh, rCat now refers to ??
31:     return 0;
32: }
33:
34: SimpleCat &TheFunction()
35: {
36:     SimpleCat *pFrisky = new SimpleCat(5,9);
37:     std::cout << "pFrisky: " << pFrisky << " ";
38:     return *pFrisky;
39: }


Here’s the output:

pFrisky: 8861880
rCat is 5 years old!
&rCat: 8861880

This compiles, links, and appears to work. But it is a time bomb waiting to go off.

The function TheFunction() has been changed so that it no longer returns a reference to a local variable. Memory is allocated on the heap and assigned to a pointer on line 36. The address that the pointer holds is printed, and then the pointer is dereferenced and the SimpleCat object is returned by reference.

On line 23, the return of TheFunction() is assigned to a reference to a SimpleCat, and that object is used to obtain the cat’s age, which is displayed on line 25.

To prove that the reference declared in main() is referring to the object put on the heap in TheFunction(), the address of operator is applied to rCat. Sure enough, it displays the address of the object it refers to, and this matches the address of the object on the heap.

So far, so good. But how will that memory be freed? You can’t call delete on the reference. One clever solution is to create another pointer and initialize it with the address obtained from rCat. This does delete the memory and plugs the memory leak. One small problem, though: What is rCat referring to after line 30? As stated earlier, a reference must always be an alias for an actual object. If it references a null object (as this does now), the program is invalid.


Watch Out!

It cannot be overemphasized that a program with a reference to a null object might compile, but it is invalid and its performance is unpredictable.


There are actually two solutions to this problem. The first is to return a pointer to the memory created on line 36. Then the calling function can delete the pointer when it is done. To do this, change the return value of TheFunction to a pointer (rather than reference) and return the pointer, rather than the dereferenced pointer:

SimpleCat *TheFunction()
{
    SimpleCat *pFrisky = new SimpleCat(5,9);
    std::cout << "pFrisky: " << pFrisky << " ";
    return pFrisky; // return the pointer
}

A more desirable solution is to declare the object in the calling function and then pass it to TheFunction() by reference. The advantage of this alternative is that the function that allocates the memory (the calling function) is also the function responsible for deallocating it, which, as discussed in the next section, is preferable.

Pointer, Pointer, Who Has the Pointer?

When your program allocates memory on the heap, a pointer is returned. It is imperative that you keep a pointer to that memory, because after the pointer is lost, the memory cannot be deleted and becomes a memory leak.

As you pass this block of memory between functions, one of the functions “owns” the pointer. Typically, the value in the block is passed using references, and the function that created the memory block is the one that deletes it. But this is a general rule, not an ironclad one.

It is dangerous for one function to create space in memory and another to free it, however. Ambiguity about which owns the pointer can lead to one of two problems: forgetting to delete a pointer or deleting it twice. Either one can cause serious problems in your program. It is safer to build your functions so that they delete the memory spaces they created.

If you write a function that needs to create a block of memory and then pass it back to the calling function, consider changing your interface. Have the calling function allocate the memory and then pass it into your function by reference. This moves all memory management out of your program and back to the function that is prepared to delete it.

Summary

With the completion of this hour, you should now be comfortable with how pointers and references are created in C++ and understand their strengths and weaknesses.

A pointer is a variable that holds a memory address, whereas a reference is an alias.

Both provide forms of indirection that enable functions to be more, well, functional. In many cases, however, you’ll find that references are the better choice.

Q&A

Q. Why have pointers if references are easier?

A. References cannot be NULL, and they cannot be reassigned. Pointers offer greater flexibility, but are slightly more difficult to use.

Q. Why would you ever return by value from a function?

A. If the object being returned is local, you must return by value or you will be returning a reference to a nonexistent object.

Q. Given the danger in returning by reference, why not always return by value?

A. There is much greater efficiency in returning by reference. Memory is saved, and the program runs faster.

Q. Where can I go to college to become a clown?

A. One place to go is the Ohio College of Clowning Arts, an institution of higher learning “dedicated to teaching students the ancient and honorable art of the clown.” The school, located in Louisville, Ohio, offers 30 weeks of instruction.

Courses include Clown Characterization, Physical Clowning, and Balloons and Magic for the Clown.

The college costs from $1,295 to $1,495. Visit www.ohiocollegeclowningarts.com for more information.

Workshop

Now that you’ve had the chance to learn about advanced pointer and reference topics, you can answer a few questions and complete a couple of exercises to firm up your knowledge.

Quiz

1. When dealing with large amounts of data, which approach is better?

A. Pass by reference

B. Pass by value

C. Both approaches are equally valid

2. What keyword prevents a called function from changing the value of a pointer?

A. static

B. enum

C. const

3. Can you create a reference to a pointer variable?

A. Yes

B. No

C. None of your business

Answers

1. A. Pass by reference, because pass by value causes a copy to be made of the variables being passed. For a large object, this could take considerable time and memory.

2. C. Using the const keyword tells the compiler to prevent the called function from changing the value of the pointer. You get the protection of pass by value without paying the cost of making copies.

3. A. Yes, but you might want to be careful in doing so though because it can get to be confusing—especially considering that pointers are confusing to begin with.

Activities

1. Modify the Leak program to use pointers in the call to TheFunction() and use the proper deletion method to prevent memory leaks.

2. Modify the ObjectRef and RefPasser programs to display the addresses of the variables before the function calls and afterward. This will give insight into the mechanism involved.

To see solutions to these activities, visit this book’s website at http://cplusplus.cadenhead.org.

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

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