Day 9. Exploiting References

Yesterday, you learned how to use pointers to manipulate objects on the free store and how to refer to those objects indirectly. References, the topic of today’s lesson, give you almost all the power of pointers but with a much easier syntax.

Today, you will learn

• What references are

• How references differ from pointers

• How to create references and use them

• What the limitations of references are

• How to pass values and objects into and out of functions by reference

What Is a Reference?

A reference is an alias; when you create a reference, you initialize it with the name of another object, the target. From that moment on, the reference acts as an alternative name for the target, and anything you do to the reference is really done to the target.

You create a reference by writing the type of the target object, followed by the reference operator (&), followed by the name of the reference, followed by an equal sign, followed by the name of the target object.

References can have any legal variable name, but some programmers prefer to prefix reference names with the letter “r.” Thus, if you have an integer variable named someInt, you can make a reference to that variable by writing the following:


int &rSomeRef = someInt;

This statement is read as “rSomeRef is a reference to an integer. The reference is initialized to refer to someInt.” References differ from other variables that you can declare in that they must be initialized when they are declared. If you try to create a reference variable without assigning, you receive a compiler error. Listing 9.1 shows how references are created and used.

Note

Note that the reference operator (&) is the same symbol as the one used for the address-of operator. These are not the same operators, however, although clearly they are related.

The space before the reference operator is required; the space between the reference operator and the name of the reference variable is optional. Thus

int &rSomeRef = someInt;  // ok
int & rSomeRef = someInt; // ok

Listing 9.1. Creating and Using References


1:  //Listing 9.1 - Demonstrating the use of references
2:
3:  #include <iostream>
4:
5:  int main()
6:  {
7:      using namespace std;
8:      int  intOne;
9:      int &rSomeRef = intOne;
10:
11:      intOne = 5;
12:      cout << "intOne: " << intOne << endl;
13:      cout << "rSomeRef: " << rSomeRef << endl;
14:
15:      rSomeRef = 7;
16:      cout << "intOne: " << intOne << endl;
17:      cout << "rSomeRef: " << rSomeRef << endl;
18:
19:      return 0;
20:  }

Image


intOne: 5
rSomeRef: 5
intOne: 7
rSomeRef: 7

Image

On line 8, a local integer variable, intOne, is declared. On line 9, a reference to an integer (int), rSomeRef, is declared and initialized to refer to intOne. As already stated, if you declare a reference but don’t initialize it, you receive a compile-time error. References must be initialized.

On line 11, intOne is assigned the value 5. On lines 12 and 13, the values in intOne and rSomeRef are printed, and are, of course, the same.

On line 15, 7 is assigned to rSomeRef. Because this is a reference, it is an alias for intOne, and thus the 7 is really assigned to intOne, as is shown by the printouts on lines 16 and 17.

Using the Address-Of Operator (&) on References

You have now seen that the & symbol is used for both the address of a variable and to declare a reference. But what if you take the address of a reference variable? If you ask a reference for its address, it returns the address of its target. That is the nature of references. They are aliases for the target. Listing 9.2 demonstrates taking the address of a reference variable called rSomeRef.

Listing 9.2. Taking the Address of a Reference


1:  //Listing 9.2 - Demonstrating the use of references
2:
3:  #include <iostream>
4:
5:  int main()
6:  {
7:      using namespace std;
8:      int  intOne;
9:      int &rSomeRef = intOne;
10:
11:      intOne = 5;
12:      cout << "intOne: " << intOne << endl;
13:      cout << "rSomeRef: " << rSomeRef << endl;
14:
15:      cout << "&intOne: "  << &intOne << endl;
16:      cout << "&rSomeRef: " << &rSomeRef << endl;
17:
18:      return 0;
19:  }

Image


intOne: 5
rSomeRef: 5
&intOne:  0x3500
&rSomeRef: 0x3500

Caution

Because the final two lines print memory addresses that might be unique to your computer or to a specific run of the program, your output might differ.

Image

Once again, rSomeRef is initialized as a reference to intOne. This time, the addresses of the two variables are printed in lines 15 and 16, and they are identical.

C++ gives you no way to access the address of the reference itself because it is not meaningful as it would be if you were using a pointer or other variable. References are initialized when created, and they always act as a synonym for their target, even when the address-of operator is applied.

For example, if you have a class called President, you might declare an instance of that class as follows:


President George_Washington;

You might then declare a reference to President and initialize it with this object:


President &FatherOfOurCountry = George_Washington;

Only one President exists; both identifiers refer to the same object of the same class. Any action you take on FatherOfOurCountry is taken on George_Washington as well.

Be careful to distinguish between the & symbol on line 9 of Listing 9.2, which declares a reference to an integer named rSomeRef, and the & symbols on lines 15 and 16, which return the addresses of the integer variable intOne and the reference rSomeRef. The compiler knows how to distinguish these two uses by the context in which they are being used.

Note

Normally, when you use a reference, you do not use the address-of operator. You simply use the reference as you would use the target variable.

Attempting to Reassign References (Not!)

Reference variables cannot be reassigned. Even experienced C++ programmers can be confused by what happens when you try to reassign a reference. Reference variables are always aliases for their target. What appears to be a reassignment turns out to be the assignment of a new value to the target. Listing 9.3 illustrates this fact.

Listing 9.3. Assigning to a Reference


1:  //Listing 9.3 - //Reassigning a reference
2:
3:  #include <iostream>
4:
5:  int main()
6:  {
7:      using namespace std;
8:      int  intOne;
9:      int &rSomeRef = intOne;
10:
11:      intOne = 5;
12:       cout << "intOne:       " << intOne << endl;
13:      cout << "rSomeRef:   " << rSomeRef << endl;
14:      cout << "&intOne:      "  << &intOne << endl;
15:      cout << "&rSomeRef: " << &rSomeRef << endl;
16:
17:      int intTwo = 8;
18:      rSomeRef = intTwo;   // not what you think!
19:      cout << " intOne:       " << intOne << endl;
20:      cout << "intTwo:        " << intTwo << endl;
21:      cout << "rSomeRef:   " << rSomeRef << endl;
22:      cout << "&intOne:      "  << &intOne << endl;
23:      cout << "&intTwo:      "  << &intTwo << endl;
24:      cout << "&rSomeRef: " << &rSomeRef << endl;
25:      return 0;
26:  }

Image


intOne:         5
rSomeRef:    5
&intOne:      0012FEDC
&rSomeRef: 0012FEDC

intOne:        8
intTwo:        8
rSomeRef:   8
&intOne:      0012FEDC
&intTwo:      0012FEE0
&rSomeRef: 0012FEDC

Image

Once again, on lines 8 and 9, an integer variable and a reference to an integer are declared. The integer is assigned the value 5 on line 11, and the values and their addresses are printed on lines 12–15.

On line 17, a new variable, intTwo, is created and initialized with the value 8. On line 18, the programmer tries to reassign rSomeRef to now be an alias to the variable intTwo, but that is not what happens. What actually happens is that rSomeRef continues to act as an alias for intOne, so this assignment is equivalent to the following:


intOne = intTwo;

Sure enough, when the values of intOne and rSomeRef are printed (lines 19–21), they are the same as intTwo. In fact, when the addresses are printed on lines 22–24, you see that rSomeRef continues to refer to intOne and not intTwo.

Image

Referencing Objects

Any object can be referenced, including user-defined objects. Note that you create a reference to an object, but not to a class. For instance, your compiler will not allow this:

Image

You must initialize rIntRef to a particular integer, such as this:


int howBig = 200;
int & rIntRef = howBig;

In the same way, you don’t initialize a reference to a Cat:

Image

You must initialize a reference to a particular Cat object:


Cat Frisky;
Cat & rCatRef = Frisky;

References to objects are used just like the object itself. Member data and methods are accessed using the normal class member access operator (.), and just as with the built-in types, the reference acts as an alias to the object. Listing 9.4 illustrates this.

Listing 9.4. References to Objects


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

Image


Frisky is: 5 years old.
And Frisky weighs 8 pounds.

Image

On line 25, Frisky is declared to be a SimpleCat object. On line 26, a SimpleCat reference, rCat, is declared and initialized to refer to Frisky. On lines 29 and 31, the SimpleCat accessor methods are accessed by using first the SimpleCat object and then the SimpleCat reference. Note that the access is identical. Again, the reference is an alias for the actual object.

References

References act as an alias to another variable. Declare a reference by writing the type, followed by the reference operator (&), followed by the reference name. References must be initialized at the time of creation.

Example 1


int hisAge;
int &rAge = hisAge;

Example 2


Cat Boots;
Cat &rCatRef = Boots;

Null Pointers and Null References

When pointers are not initialized or when they are deleted, they ought to be assigned to null (0). This is not true for references because they must be initialized to what they reference when they are declared.

However, because C++ needs to be usable for device drivers, embedded systems, and real-time systems that can reach directly into the hardware, the ability to reference specific addresses is valuable and required. For this reason, most compilers support a null or numeric initialization of a reference without much complaint, crashing only if you try to use the object in some way when that reference would be invalid.

Taking advantage of this in normal programming, however, is still not a good idea. When you move your program to another machine or compiler, mysterious bugs might develop if you have null references.

Passing Function Arguments by Reference

On Day 5, “Organizing into Functions,” you learned that functions have two limitations: Arguments are passed by value, and the return statement can return only one value.

Passing values to a function by reference can overcome both of these limitations. In C++, passing a variable by reference is accomplished in two ways: using pointers and using references. Note the difference: You pass by reference using a pointer, or you pass a reference using a reference.

The syntax of using a pointer is different from that of using a reference, but the net effect is the same. Rather than a copy being created within the scope of the function, the actual original object is (effectively) made directly available to the function.

Passing an object by reference enables the function to change the object being referred to. On Day 5, you learned that functions are passed their parameters on the stack. When a function is passed a value by reference (using either pointers or references), the address of the original object is put on the stack, not the entire object. In fact, on some computers, the address is actually held in a register and nothing is put on the stack. In either case, because an address is being passed, the compiler now knows how to get to the original object, and changes are made there and not in a copy.

Recall that Listing 5.5 on Day 5 demonstrated that a call to the swap() function did not affect the values in the calling function. Listing 5.5 is reproduced here as Listing 9.5, for your convenience.

Listing 9.5. Demonstrating Passing by Value


1:  //Listing 9.5 - Demonstrates passing by value
2:  #include <iostream>
3:
4:  using namespace std;
5:  void swap(int x, int y);
6:
7:  int main()
8:  {
9:     int x = 5, y = 10;
10:
11:     cout << "Main. Before swap, x: " << x << " y: " << y << endl;
12:     swap(x,y);
13:     cout << "Main. After swap, x: " << x << " y: " << y << endl;
14:     return 0;
15:  }
16:
17:  void swap (int x, int y)
18:  {
19:     int temp;
20:
21:     cout << "Swap. Before swap, x: " << x << " y: " << y << endl;
22:
23:     temp = x;
24:     x = y;
25:     y = temp;
26:
27:     cout << "Swap. After swap, x: " << x << " y: " << y << endl;
28:  }

Image


Main. Before swap, x: 5 y: 10
Swap. Before swap, x: 5 y: 10
Swap. After swap, x: 10 y: 5
Main. After swap, x: 5 y: 10

Image

This program initializes two variables in main() and then passes them to the swap() function, which appears to swap them. When they are examined again in main(), they are unchanged!

The problem here is that x and y are being passed to swap() by value. That is, local copies were made in the function. These local copies were changed and then thrown away when the function returned and its local storage was deallocated. What is preferable is to pass x and y by reference, which changes the source values of the variable rather than a local copy.

Two ways to solve this problem are possible in C++: You can make the parameters of swap() pointers to the original values, or you can pass in references to the original values.

Making swap() Work with Pointers

When you pass in a pointer, you pass in the address of the object, and thus the function can manipulate the value at that address. To make swap() change the actual values of x and y by using pointers, the function, swap(), should be declared to accept two int pointers. Then, by dereferencing the pointers, the values of x and y will actually be accessed and, in fact, be swapped. Listing 9.6 demonstrates this idea.

Listing 9.6. Passing by Reference Using Pointers


1:  //Listing 9.6 Demonstrates passing by reference
2:  #include <iostream>
3:
4:  using namespace std;
5:  void swap(int *x, int *y);
6:
7:  int main()
8:  {
9:      int x = 5, y = 10;
10:
11:      cout << "Main. Before swap, x: " << x << " y: " << y << endl;
12:      swap(&x,&y);
13:      cout << "Main. After swap, x: " << x << " y: " << y << endl;
14:      return 0;
15:  }
16:
17:  void swap (int *px, int *py)
18:  {
19:      int temp;
20:
21:      cout << "Swap. Before swap, *px: " << *px <<
22:           " *py: " << *py << endl;
23:
24:      temp = *px;
25:      *px = *py;
26:      *py = temp;
27:
28:      cout << "Swap. After swap, *px: " << *px <<
29:           " *py: " << *py << endl;
30:
31:  }

Image


Main. Before swap, x: 5 y: 10
Swap. Before swap, *px: 5 *py: 10
Swap. After swap, *px: 10 *py: 5
Main. After swap, x: 10 y: 5

Image

Success! On line 5, the prototype of swap() is changed to indicate that its two parameters will be pointers to int rather than int variables. When swap() is called on line 12, the addresses of x and y are passed as the arguments. You can see that the addresses are passed because the address-of operator (&) is being used.

On line 19, a local variable, temp, is declared in the swap() function. temp need not be a pointer; it will just hold the value of *px (that is, the value of x in the calling function) for the life of the function. After the function returns, temp is no longer needed.

On line 24, temp is assigned the value at px. On line 25, the value at px is assigned to the value at py. On line 26, the value stashed in temp (that is, the original value at px) is put into py.

The net effect of this is that the values in the calling function, whose address was passed to swap(), are, in fact, swapped.

Implementing swap() with References

The preceding program works, but the syntax of the swap() function is cumbersome in two ways. First, the repeated need to dereference the pointers within the swap() function makes it error-prone—for instance, if you fail to dereference the pointer, the compiler still lets you assign an integer to the pointer, and a subsequent user experiences an addressing error. This is also hard to read. Finally, the need to pass the address of the variables in the calling function makes the inner workings of swap() overly apparent to its users.

It is a goal of an object-oriented language such as C++ to prevent the user of a function from worrying about how it works. Passing by pointers puts the burden on the calling function rather than where it belongs—on the function being called. Listing 9.7 rewrites the swap() function, using references.

Listing 9.7. swap() Rewritten with References


1:  //Listing 9.7 Demonstrates passing by reference
2:  // using references!
3:  #include <iostream>
4:
5:  using namespace std;
6:  void swap(int &x, int &y);
7:
8:  int main()
9:  {
10:       int x = 5, y = 10;
11:
12:       cout << "Main. Before swap, x: " << x << " y: "
13:            << y << endl;
14:
15:       swap(x,y);
16:
17:       cout << "Main. After swap, x: " << x << " y: "
18:            << y << endl;
19:
20:       return 0;
21:  }
22:
23:  void swap (int &rx, int &ry)
24:  {
25:        int temp;
26:
27:        cout << "Swap. Before swap, rx: " << rx << " ry: "
28:             << ry << endl;
29:
30:        temp = rx;
31:        rx = ry;
32:        ry = temp;
33:
34:
35:        cout << "Swap. After swap, rx: " << rx << " ry: "
36:             << ry << endl;
37:
38:  }

Image


Main. Before swap, x:5 y: 10
Swap. Before swap, rx:5 ry:10
Swap. After swap, rx:10 ry:5
Main. After swap, x:10, y:5

Image

Just as in the example with pointers, two variables are declared on line 10, and their values are printed on line 12. On line 15, the function swap() is called, but note that x and y, not their addresses, are passed. The calling function simply passes the variables.

When swap() is called, program execution jumps to line 23, where the variables are identified as references. The values from the variables are printed on line 27, but note that no special operators are required. These variables are aliases for the original variables and can be used as such.

On lines 30–32, the values are swapped, and then they’re printed on line 35. Program execution jumps back to the calling function, and on line 17, the values are printed in main(). Because the parameters to swap() are declared to be references, the variables from main() are passed by reference, and thus their changed values are what is seen in main() as well.

As you can see from this listing, references provide the convenience and ease of use of normal variables, with the power and pass-by-reference capability of pointers!

Understanding Function Headers and Prototypes

Listing 9.6 shows swap() using pointers, and Listing 9.7 shows it using references. Using the function that takes references is easier, and the code is easier to read, but how does the calling function know if the values are passed by reference or by value? As a client (or user) of swap(), the programmer must ensure that swap() will, in fact, change the parameters.

This is another use for the function prototype. By examining the parameters declared in the prototype, which is typically in a header file along with all the other prototypes, the programmer knows that the values passed into swap() are passed by reference, and thus will be swapped properly. On line 6 of Listing 9.7, you can see the prototype for swap()—you can see that the two parameters are passed as references.

If swap() had been a member function of a class, the class declaration, also available in a header file, would have supplied this information.

In C++, clients of classes and functions can rely on the header file to tell all that is needed; it acts as the interface to the class or function. The actual implementation is hidden from the client. This enables the programmer to focus on the problem at hand and to use the class or function without concern for how it works.

When Colonel John Roebling designed the Brooklyn Bridge, he worried in detail about how the concrete was poured and how the wire for the bridge was manufactured. He was intimately involved in the mechanical and chemical processes required to create his materials. Today, however, engineers make more efficient use of their time by using well-understood building materials, without regard to how their manufacturer produced them.

It is the goal of C++ to enable programmers to rely on well-understood classes and functions without regard to their internal workings. These “component parts” can be assembled to produce a program, much the same way wires, pipes, clamps, and other parts are assembled to produce buildings and bridges.

In much the same way that an engineer examines the spec sheet for a pipe to determine its load-bearing capacity, volume, fitting size, and so forth, a C++ programmer reads the declaration of a function or class to determine what services it provides, what parameters it takes, and what values it returns.

Returning Multiple Values

As discussed, functions can only return one value. What if you need to get two values back from a function? One way to solve this problem is to pass two objects into the function, by reference. The function can then fill the objects with the correct values. Because passing by reference allows a function to change the original objects, this effectively enables the function to return two pieces of information. This approach bypasses the return value of the function, which can then be reserved for reporting errors.

Once again, this can be done with references or pointers. Listing 9.8 demonstrates a function that returns three values: two as pointer parameters and one as the return value of the function.

Listing 9.8. Returning Values with Pointers


1:  //Listing 9.8 - Returning multiple values from a function
2:
3:  #include <iostream>
4:
5:  using namespace std;
6:  short Factor(int n, int* pSquared, int* pCubed);
7:
8:  int main()
9:  {
10:     int number, squared, cubed;
11:     short error;
12:
13:     cout << "Enter a number (0 - 20): ";
14:     cin >> number;
15:
16:     error = Factor(number, &squared, &cubed);
17:
18:     if (!error)
19:     {
20:        cout << "number: " << number << endl;
21:        cout << "square: " << squared << endl;
22:        cout << "cubed: "  << cubed   << endl;
23:     }
24:     else
25:        cout << "Error encountered!!" << endl;
26:     return 0;
27:  }
28:
29:  short Factor(int n, int *pSquared, int *pCubed)
30:  {
31:     short Value = 0;
32:     if (n > 20)
33:        Value = 1;
34:     else
35:     {
36:        *pSquared = n*n;
37:        *pCubed = n*n*n;
38:        Value = 0;
39:     }
40:     return Value;
41:  }

Image


Enter a number (0-20): 3
number: 3
square: 9
cubed: 27

Image

On line 10, number, squared, and cubed are defined as short integers. number is assigned a value based on user input on line 14. On line 16, this number and the addresses of squared and cubed are passed to the function Factor().

On line 32, Factor() examines the first parameter, which is passed by value. If it is greater than 20 (the maximum value this function can handle), it sets the return value, Value, to a simple error value. Note that the return value from Function() is reserved for either this error value or the value 0, indicating all went well, and note that the function returns this value on line 40.

The actual values needed, the square and cube of number, are not returned by using the return mechanism; rather, they are returned by changing the pointers that were passed into the function.

On lines 36 and 37, the pointers are assigned their return values. These values are assigned to the original variables by the use of indirection. You know this by the use of the dereference operator (*) with the pointer names. On line 38, Value is assigned a success value and then on line 40 it is returned.

Tip

Because passing by reference or by pointer allows uncontrolled access to object attributes and methods, you should pass the minimum required for the function to do its job. This helps to ensure that the function is safer to use and more easily understandable.

Returning Values by Reference

Although Listing 9.8 works, it can be made easier to read and maintain by using references rather than pointers. Listing 9.9 shows the same program rewritten to use references.

Listing 9.9 also includes a second improvement. An enum has been added to make the return value easier to understand. Rather than returning 0 or 1, using an enum, the program can return SUCCESS or FAILURE.

Listing 9.9. Rewritten Using References

Image

Image


Enter a number (0 - 20): 3
number: 3
square: 9
cubed: 27

Image

Listing 9.9 is identical to 9.8, with two exceptions. The ERR_CODE enumeration makes the error reporting a bit more explicit on lines 36 and 41, as well as the error handling on line 22.

The larger change, however, is that Factor() is now declared to take references to squared and cubed rather than to pointers. This makes the manipulation of these parameters far simpler and easier to understand.

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.

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

Another cost occurs 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.10 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. Passing Objects by Reference

Image

Image

Image


Making a cat...
Simple Cat Constructor...
Calling FunctionOne...
Simple Cat Copy Constructor...
Function One. Returning...
Simple Cat Copy Constructor...
Simple Cat Destructor...
Simple Cat Destructor...
Calling FunctionTwo...
Function Two. Returning...
Simple Cat Destructor...

Image

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.

The 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 the first line of the output. On line 35, a SimpleCat object is instantiated. This causes the constructor to be called, and the output from the constructor is seen on the second line of output.

On line 36, main() reports that it is calling FunctionOne, which creates the third line of output. 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 the fourth line of output.

Program execution jumps to line 46 in the called function, which prints an informative message, the fifth line of output. 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 the sixth line of output.

The return value from FunctionOne() is not assigned to any object, and so the temporary object created for the return is thrown away, calling the destructor, which produces the seventh line of output. Because FunctionOne() has ended, its local copy goes out of scope and is destroyed, calling the destructor and producing the eighth line of output.

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 the tenth line of output 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 the final line of output.

The net effect of this is that the call to FunctionOne(), because it passed the Frisky 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 meant to be allowed to change the SimpleCat object it is passed, yet it is given the address of the SimpleCat. This seriously exposes the original 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 pointer to a constant SimpleCat. Doing so prevents calling any non-const method on SimpleCat, and thus protects the object from change.

Passing a constant reference allows your guests to see the original painting, but not to alter it in any way. Listing 9.11 demonstrates this idea.

Listing 9.11. Passing Pointer to a Constant Object


1:  //Listing 9.11 - Passing pointers to objects
2:
3:  #include <iostream>
4:
5:  using namespace std;
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..." << endl;
23:       itsAge = 1;
24:  }
25:
26:  SimpleCat::SimpleCat(SimpleCat&)
27:  {
28:       cout << "Simple Cat Copy Constructor..." << endl;
29:  }
30:
31:  SimpleCat::~SimpleCat()
32:  {
33:       cout << "Simple Cat Destructor..." << endl;
34:  }
35:
36:  const SimpleCat * const FunctionTwo
37:       (const SimpleCat * const theCat);
38:
39:  int main()
40:  {
41:       cout << "Making a cat..." << endl;
42:       SimpleCat Frisky;
43:       cout << "Frisky is " ;
44:       cout << Frisky.GetAge();
45:       cout << " years old" << endl;
46:       int age = 5;
47:       Frisky.SetAge(age);
48:       cout << "Frisky is " ;
49:       cout << Frisky.GetAge();
50:       cout << " years old" << endl;
51:       cout << "Calling FunctionTwo..." << endl;
52:       FunctionTwo(&Frisky);
53:       cout << "Frisky is " ;
54:       cout << Frisky.GetAge();
55:       cout << " years old" << endl;
56:       return 0;
57:  }
58:
59:  // functionTwo, passes a const pointer
60:  const SimpleCat * const FunctionTwo
61:       (const SimpleCat * const theCat)
62:  {
63:       cout << "Function Two. Returning..." << endl;
64:       cout << "Frisky is now " << theCat->GetAge();
65:       cout << " years old " << endl;
66:       // theCat->SetAge(8);   const!
67:       return theCat;
68:  }

Image


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

Image

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 42, an object is created, and its default age is printed, starting on line 43.

On line 47, itsAge is set using the accessor SetAge, and the result is printed on line 48. 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 object being pointed to in FunctionTwo(), however, is now constant, and thus cannot call the non-const method, SetAge(). If the call to SetAge() on line 66 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 nonconstant object is passed to FunctionTwo(), but because FunctionTwo()’s declaration declares the pointer to be a constant pointer to a constant object, 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.

Because you know the object will never be null, it would be easier to work within 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 - Passing references to objects
2:
3:  #include <iostream>
4:
5:  using namespace std;
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..." << endl;
23:       itsAge = 1;
24:  }
25:
26:  SimpleCat::SimpleCat(SimpleCat&)
27:  {
28:       cout << "Simple Cat Copy Constructor..." << endl;
29:  }
30:
31:  SimpleCat::~SimpleCat()
32:  {
33:       cout << "Simple Cat Destructor..." << endl;
34:  }
35:
36:  const     SimpleCat & FunctionTwo (const SimpleCat & theCat);
37:
38:  int main()
39:  {
40:       cout << "Making a cat..." << endl;
41:       SimpleCat Frisky;
42:       cout << "Frisky is " << Frisky.GetAge() << " years old" << endl;
43:       int age = 5;
44:       Frisky.SetAge(age);
45:       cout << "Frisky is " << Frisky.GetAge() << " years old" << endl;
46:       cout << "Calling FunctionTwo..." << endl;
47:       FunctionTwo(Frisky);
48:       cout << "Frisky is " << Frisky.GetAge() << " years old" << endl;
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..." << endl;
56:       cout << "Frisky is now " << theCat.GetAge();
57:       cout << " years old " << endl;
58:       // theCat.SetAge(8);   const!
59:       return theCat;
60:  }

Image


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

Image

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 they are always constant. If the keyword const is applied to a reference, it is to make the object referred to constant.

Knowing When to Use References Versus Pointers

Experienced C++ programmers strongly prefer references to pointers. References are cleaner and easier to use, and they do a better job of hiding information, as you saw in the previous example.

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 any chance exists that the object in question might 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. Because a reference shouldn’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.

Image

Mixing References and Pointers

It is perfectly legal to declare both pointers and references in the same function parameter list, along with objects passed by value. Here’s an example:


Cat * SomeFunction (Person &theOwner, House *theHouse, int age);

This declaration says that SomeFunction takes three parameters. The first is a reference to a Person object, the second is a pointer to a House object, and the third is an integer. It returns a pointer to a Cat object.

The question of where to put the reference (&) or the indirection operator (*) when declaring these variables is a great controversy. When declaring a reference, you can legally write any of the following:


1:  Cat&  rFrisky;
2:  Cat & rFrisky;
3:  Cat  &rFrisky;

Whitespace is completely ignored, so anywhere you see a space here you can put as many spaces, tabs, and new lines as you want.

Setting aside freedom of expression issues, which is best? Here are the arguments for all three:

The argument for case 1 is that rFrisky is a variable whose name is rFrisky and whose type can be thought of as “reference to Cat object.” Thus, this argument goes, the & should be with the type.

The counterargument is that the type is Cat. The & is part of the “declarator,” which includes the variable name and the ampersand. More important, having the & near the Cat can lead to the following bug:


Cat&  rFrisky, rBoots;

Casual examination of this line would lead you to think that both rFrisky and rBoots are references to Cat objects, but you’d be wrong. This really says that rFrisky is a reference to a Cat, and rBoots (despite its name) is not a reference but a plain old Cat variable. This should be rewritten as follows:


Cat    &rFrisky, rBoots;

The answer to this objection is that declarations of references and variables should never be combined like this. Here’s the right way to declare the reference and nonreference variable:


Cat& rFrisky;
Cat  boots;

Finally, many programmers opt out of the argument and go with the middle position, that of putting the & in the middle of the two, as illustrated in case 2.

Of course, everything said so far about the reference operator (&) applies equally well to the indirection operator (*). The important thing is to recognize that reasonable people differ in their perceptions of the one true way. Choose a style that works for you, and be consistent within any one program; clarity is, and remains, the goal.

Note

Many programmers like the following conventions for declaring references and pointers:

• Put the ampersand and asterisk in the middle, with a space on either side.

• Never declare references, pointers, and variables all on the same line.

Returning Out-of-Scope Object References

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 is always an alias to some other object. If you pass a reference into or out of a function, be certain to ask yourself, “What is the object I’m aliasing, and will it still exist every time it’s used?”

Listing 9.13 illustrates the danger of returning a reference to an object that no longer exists.

Listing 9.13. Returning a Reference to a Nonexistent Object


1:  // Listing 9.13
2:  // Returning a reference to an object
3:  // which no longer exists
4:
5:  #include <iostream>
6:
7:  class SimpleCat
8:  {
9:    public:
10:       SimpleCat (int age, int weight);
11:       ~SimpleCat() {}
12:       int GetAge() { return itsAge; }
13:       int GetWeight() { return itsWeight; }
14:    private:
15:       int itsAge;
16:       int itsWeight;
17:  };
18:
19:  SimpleCat::SimpleCat(int age, int weight)
20:  {
21:       itsAge = age;
22:       itsWeight = weight;
23:  }
24:
25:  SimpleCat &TheFunction();
26:
27:  int main()
28:  {
29:       SimpleCat &rCat = TheFunction();
30:       int age = rCat.GetAge();
31:       std::cout << "rCat is " << age << " years old!" << std::endl;
32:       return 0;
33:  }
34:
35:  SimpleCat &TheFunction()
36:  {
37:       SimpleCat Frisky(5,9);
38:       return Frisky;
39:  }

Image


Compile error: Attempting to return a reference to a local object!

Caution

This program won’t compile on the Borland compiler. It will compile on Microsoft compilers; however, it should be noted that it is a poor coding practice.

Image

On lines 7–17, SimpleCat is declared. On line 29, a reference to a SimpleCat is initialized with the results of calling TheFunction(), which is declared on line 25 to return a reference to a SimpleCat.

The body of TheFunction() in lines 35–39 declares a local object of type SimpleCat and initializes its age and weight. It then returns that local object by reference on line 38. Some compilers are smart enough to catch this error and don’t let you run the program. Others let you run the program, with unpredictable results.

When TheFunction() returns, the local object, Frisky, is destroyed (painlessly, I assure you). The reference returned by this function is an alias 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 Listing 9.13 by having TheFunction() create Frisky on the heap. That way, when you return from TheFunction(), Frisky still exists.

The problem with this approach is: What do you do with the memory allocated for Frisky when you are done with it? Listing 9.14 illustrates this problem.

Listing 9.14. Memory Leaks


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

Image


pFrisky: 0x00431C60
rCat is 5 years old!
&rCat: 0x00431C60

Caution

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

Image

TheFunction() in lines 39–44 has been changed so that it no longer returns a reference to a local variable. Memory is allocated on the free store and assigned to a pointer on line 41. The address that pointer holds is printed, and then the pointer is dereferenced and the SimpleCat object is returned by reference.

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

To prove that the reference declared in main() is referring to the object put on the free store 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 free store.

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 it plugs the memory leak. One small problem, though: What is rCat referring to after line 34? As stated earlier, a reference must always alias an actual object; if it references a null object (as this does now), the program is invalid.

Note

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.

Three solutions exist to this problem. The first is to declare a SimpleCat object on line 28 and to return that cat from TheFunction() by value. The second is to go ahead and declare the SimpleCat on the free store in TheFunction(), but have TheFunction() return a pointer to that memory. Then, the calling function can delete the pointer when it is done.

The third workable solution, and the right one, is to declare the object in the calling function and then to pass it to TheFunction() by reference.

Pointer, Pointer, Who Has the Pointer?

When your program allocates memory on the free store, 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, someone will “own” the pointer. Typically, the value in the block is passed using references, and the function that created the memory is the one that deletes it. But this is a general rule, not an ironclad one.

It is dangerous for one function to create memory and another to free it, however. Ambiguity about who 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 they create.

If you are writing a function that needs to create 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.

Image

Summary

Today, you learned what references are and how they compare to pointers. You saw that references must be initialized to refer to an existing object and cannot be reassigned to refer to anything else. Any action taken on a reference is in fact taken on the reference’s target object. Proof of this is that taking the address of a reference returns the address of the target.

You saw that passing objects by reference can be more efficient than passing by value. Passing by reference also allows the called function to change the value in the arguments back in the calling function.

You saw that arguments to functions and values returned from functions can be passed by reference, and that this can be implemented with pointers or with references.

You saw how to use pointers to constant objects and constant references to pass values between functions safely while achieving the efficiency of passing by reference.

Q&A

Q   Why have references if pointers can do everything references can?

A   References are easier to use and to understand. The indirection is hidden, and no need exists to repeatedly dereference the variable.

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   Far greater efficiency is achieved in returning by reference. Memory is saved and the program runs faster.

Workshop

The Workshop contains quiz questions to help solidify your understanding of the material covered and exercises to provide you with experience in using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix D, and be certain you understand the answers before going to tomorrow’s lesson.

Quiz

1. What is the difference between a reference and a pointer?

2. When must you use a pointer rather than a reference?

3. What does new return if there is insufficient memory to make your new object?

4. What is a constant reference?

5. What is the difference between passing by reference and passing a reference?

6. When declaring a reference, which is correct:

a. int& myRef = myInt;

b. int & myRef = myInt;

c. int &myRef = myInt;

Exercises

1. Write a program that declares an int, a reference to an int, and a pointer to an int. Use the pointer and the reference to manipulate the value in the int.

2. Write a program that declares a constant pointer to a constant integer. Initialize the pointer to an integer variable, varOne. Assign 6 to varOne. Use the pointer to assign 7 to varOne. Create a second integer variable, varTwo. Reassign the pointer to varTwo. Do not compile this exercise yet.

3. Now compile the program in Exercise 2. What produces errors? What produces warnings?

4. Write a program that produces a stray pointer.

5. Fix the program from Exercise 4.

6. Write a program that produces a memory leak.

7. Fix the program from Exercise 6.

8. BUG BUSTERS: What is wrong with this program?


1:       #include <iostream>
2:       using namespace std;
3:       class CAT
4:       {
5:               public:
6:               CAT(int age) { itsAge = age; }
7:               ~CAT(){}
8:               int GetAge() const { return itsAge;}
9:           private:
10:              int itsAge;
11:    };
12:
13:    CAT & MakeCat(int age);
14:    int main()
15:    {
16:         int age = 7;
17:         CAT Boots = MakeCat(age);
18:         cout << "Boots is " << Boots.GetAge()
19:                  << " years old" << endl;
20:       return 0;
21:    }
22:
23:    CAT & MakeCat(int age)
24:    {
25:           CAT * pCat = new CAT(age);
26:           return *pCat;
27:    }

9. Fix the program from Exercise 8.

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

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