3.4 Communicating Objects by Reference and By Pointer


Three major structures are built using objects communicated by reference or by pointer::
  • result parameters: to allow the sender to see the changes made to the object by the execution of the receiver's method,

  • managers: to allow an object of one class to create, distribute, or otherwise manage objects of another class, and

  • associations: to establish connections among objects that allow the objects to interact beyond the duration of a single method invocation.

All of these uses will be explored in this section. Additional features of associations are presented in the next two sections.

The difference between communicating objects by reference or by pointer is largely syntactic, as they each accomplish the important goal of passing original, not copied, objects. In C++, the ampersand symbol (&) is used to denote communicating an object by reference and the asterisk symbol (*) is used to denote communicating an object by pointer. Remeber, though, that these two symbols have several meanings in C++ depending on the context in which they are used, so to indicate communicating objects by reference or by pointer, the symbols "&" and "*" must appear after a class name in a parameter list or in a return type.

Result Parameters

Passing an object as a parameter by reference or by pointer allows the sender to see changes made to the object by the receiver. In some cases the object is used purely in an output form, that is, the object's initial value is not used by the receiver. In other cases the receiver uses the object in an input-output form, that is, the receiver uses the initial value of the parameter object and the receiver possibly changes the object in some way. This latter use will be illustrated below.

A simple information-retrieval example illustrates how objects are passed by reference and by pointer. In this example, a simple text file will be searched for the first line containing a given search string. Various applications can be built on this model, among them:

  • Searching a library catalog file for a given author's name
  • Searching a bookmark file for a URL of a given organization
  • Searching a phone-book file for the phone number of a given person

Many other examples follow the same basic organization.

The simple information-retrieval example extends the File class by adding a method to search a file object for a given search string.

An information retrieval query is represented by the following class:

The Query Class
       class Query {
         private:
                                // encapsulated implementation
         public:

          Query (char* searchText);
          Query();
          void   SetSearch(char* searchText);
          char*  GetSearch();
          void   AskUser();
          void   SetResult(char* resultText);
          char*  GetResult();
          ~Query();
        };

The Query class contains two text strings, a search string, and a result string, each of which has an associated pair of accessor and mutator methods to set and get the string's value. The two "get" methods return a null pointer if their respective strings are undefined. The AskUser method initiates a dialogue that allows the user to provide a search string. This design anticipates that the sender defines the search string (via the constructor, the SetSearch method, or the AskUser method), that the receiver doing the search defines the result string (via the SetResult method), and that the sender retrieves the result string (via the GetResult method).

The File class is extended by adding a method to search a File object for a given query, modifying the Query object if the search is successful. The additions to the File class are:

       class File {
          private:

          public:
                ...
                void SearchFor (Query& q);              // by reference
                void SearchFor (Query* q);              // by pointer
                ...
            
        };

The specification "Query&" denotes passing a Query object by reference. The specification "Query*" denotes passing a Query object by pointer. In neither case is the Query object copied.

The Query class and the extended File class can be used as shown below. This code also illustrates the syntactic differences between by reference and by pointer objects.

       Query query1("object"), query2("oriented");
        Query *query3;
        Query *query4;

        query3 = new Query("programming");
        query4 = new Query("C++");

        File booklist("booklist");

        booklist.SearchFor( query1);            // by reference
        booklist.SearchFor(&query2);            // by pointer
        booklist.SearchFor( query3);            // by pointer
        booklist.SearchFor(*query4);            // by reference

        char* result1 = query1.GetResult();
        char* result2 = query2.GetResult();
        char* result3 = query3->GetResult();
        char* result4 = query4->GetResult();

In this example, query1 and query2 are names for objects, while query3 and query4 are pointers to objects. The first use of the SearchFor method, using query1, is by reference because query1 is an object. Passing the query1 object matches with the by reference overloading of the SearchFor method. The second use of the SearchFor method, using query2, is by pointer because the parameter, "&query2" is a pointer. The "&" operator in this context is the "address of" operator. Thus, what is passed is the "address of query2" (i.e., a pointer to the query2 object) which matches the by pointer overloading of the SearchFor method. The third use of the SearchFor method, using query3, is also by pointer because query3 is, by its declaration, a pointer to a Query object. The fourth use of the SearchFor method, using query4, is by reference. While query4 is a pointer to a Query object, the actual parameter is "*query4." The "*" operator is the dereference operator yielding the object that is being pointed to. Thus, the actual parameter is an object. When the actual parameter is an object, it will match with the by reference overloading of the SearchFor method.

The following table summarizes the syntactic aspects of passing arguments by reference and by pointer. The first line of the table indicates that, moving left to right,

  • The receiver declares the formal parameter, named "y" by the receiver, to be a reference to an object of class T
  • The sender declares the actual parameter, named "x" by the sender, as an object of the class T
  • The sender would invoke the receiver's method using the name of the object, "x"
  • The sender accesses the methods of the actual parameter using the "dot" notation
  • The receiver accesses the methods of the formal parameter using the "dot" notation.

The first row corresponds to the first use of the SearchFor method using the query1 object in the example code above. The subsequent rows in the table correspond to query2, query3, and query4, respectively.

By Reference and By Pointer:
Matching the Sender and Receiver
Receiver's
Declaration
Sender's
Declaration
Actual
Parameter
Sender
Access
Receiver
Access
T& y T x x x.f() y.f()
T* y T x &x x.f() y->f()
T* y T *x x x->f() y->f()
T& y T *x *x x->f() y.f()

Managers

Manager objects commonly return objects by reference or by pointer. In some cases the manager object is responsible for creating and distributing objects of another class. In this role, managers are also called "factory" objects. In other cases the manager object is responsible for maintaining a set of managed objects, organizing them according to some attribute (e.g, alphabetically by a string attribute), or finding a managed object with a certain attribute value (e.g., a given name). In this role, managers are also called "collections." Manager objects are able to simplify a system by concealing the design decisions surrounding the construction, duplication, indexing, storage, and distribution of the managed objects. Other parts of the system are able to use the managed objects with less concern for the low-level management details. To fulfill its role as a manager, pointers and references are essential because maintaining a collection of copies of the managed objects is usually pointless; it is the original objects that are to be managed.

Two examples are given to illustrate the use of pointers and references in defining managers. The first example extends the File class so that a file object can return a file stream object for the underlying disk file or a text window in which the file can be viewed. In this example, the File object acts as a factory that produces a particular kind of file stream or text window object. The second example, illustrating the concept of a collection, defines a Frame manager that maintains a set of Frame objects. The Frame manager is similar in concept to the "window manager" found on most systems.

File Class Example

The File class is extended to illustrate returning objects by reference and by pointer. One extension of the File class is a new method GetStream() that returns a reference to a file stream object (an object in the class fstream), through which the disk file represented by the File object can be manipulated. The second extension to the File class is a change in the View() method that will make it return a pointer to a TextWindow object. The TextWindow pointed to is the window in which the File object is currently displayed. Returning a pointer to this window allows the program to manipulate the window (i.e., to move it or resize it). The two extensions to the File class are shown below.

Extended File Class
  class File {
    private:

    public:
      ...
       fstream&    GetStream(); // return stream for this file
       TextWindow* View();      // return pointer to window for this file
      ...
   };

The design of the GetStream method allows the File class to make available the full functionality of the stream I/O model without duplicating all of the stream I/O methods in its own interface. For example, the following code uses the extended File class to output to a file selected interactively by the user:

  FileNavigator nav;
   File aFile = nav.AskUser();

   fstream& fileStream = file.GetStream();
   fileStream << "add new data to file" << endl;

The GetStream method also enables more complicated uses, such as enabling different parts of a system to obtain a reference to the same stream object. This allows different parts of the system to share access to the same underlying disk file. One other advantage of the GetStream method is that the code using the File object need not be aware of the File's name or how the name of the file was obtained.

The redefinition of the View method allows the window containing the contents of the disk file to be manipulated under program control. An example of this usage is shown in the following code:

  FileNavigator nav;
   File aFile = nav.AskUser();

   TextWindow* tw = aFile.View();  // present file for viewing, return
                                   // pointer to viewing window

   tw->MoveTo(Location(10,10));    // move viewing window
   tw->Resize(Shape(200,500));     // resize viewing window

As illustrated in this code, the View method creates a TextWindow and returns a pointer to this TextWindow. The text contents of the file are viewable in this window. The viewing window is then moved and resized by the program.

Frame Manager Example

A simple FrameManager class will be designed to illustrate how objects can be returned by pointer. The FrameManger is responsible for maintaining a collection of Frame objects and returning that Frame object in the collection with a given name. Recall that the construction of a Frame object allows the object to be given a name. It is this name that is used by the FrameManager to identify a given Frame object among all those currently in the FrameManger's collection. Not considered are small details such as how the Manager behaves if there are two Frame objects with the same name.

The definition of the FrameManager is shown below. The constructor of the FrameManager allows the maximum number of Frames that the manger will have at any one time to be specified. The default value limits the FrameManager to hold at most ten Frame objects at any one time. The Add and Remove methods are both overloaded to allow the argument to be given by either a pointer or a reference to a Frame object. Also, the Remove method has an additional overloading that allows the Frame object to be identified by its name. The FindByName method returns a pointer to the Frame object whose name matches the input argument of the method; if no such Frame object can be found by the FrameManager, the method returns a null pointer.

FrameManager Class
class FrameManager {
private:
public:
         FrameManager(int maxFrames = 10);
  void   Add(Frame& frame);
  void   Add(Frame* frame);
  Frame& FindByName(char* frameName);
  void   Remove(char* frameName);
  void   Remove(Frame& frame);
  void   Remove(Frame* frame);
        ~FrameManager();
};

An example of using the FrameManager is shown in the skeleton code below. In this code, an application has several windows open simultaneously. Whenever a mouse event occurs in one of these windows, the OnMouseEvent function is called and the name of the Frame is passed as the first argument. Within the OnMouseEvent function, it is usually necessary to operate on the Frame object in which the mouse event occurred. The FrameManager provides an easy way to discover the needed Frame object given its name.

 Frame *dialogWindow;
  Frame* drawingWindow;
  // .. declare pointers to other windows

  FrameManager windowManager(5);
 void OnStart()
  { dialogWindow = new Frame("Dialogue",...);
    drawingWindow = Frame("Drawing,...);
    //... instantiate other windows
    windowManager.Add(dialogueWindow);
    windowManager.Add(drawingWindow);
    //... add other windows to Manager
  }

  void OnMouseEvent (char* frameName, int x, int y, int buttonState)
  {  Frame* frame = windowManager.FindByName(frameName));
     if (frame != (Frame*)0         // check for null return value
      {     // process mouse event in frame using frame->
      }
     else { // handle case of frame not known to FrameManager
          }
  }


As illustrated in the above code, the FrameManager plays a useful role in simplifying the coding of the OnMouseEvent function - a single, simple call to the FrameManager returns the requested frame if it is known by the FrameManager.

Associations

A simple association will be created using a newly defined class, the Message class, and the existing Frame class. The Message class is an abstraction of a displayable text string. A Message object knows what text should be written to a Frame and where within the Frame the text should appear. In addition, a Message object will be responsible for erasing its textfrom the Frame and for updating the Frame when the Message is changed. The definition of the Message class is given in the table below.

The Message Class
class Message {
  private:
                //encapsulated implementation
  public:
                 
      Message (char *textString, Location whereAt);
      Message (Location whereAt);
void  DisplayIn (Frame&   whichFrame);
void  MoveTo (Location newLocation);
void  setText(char* newText);
char* getText();
void  Clear();
void  Draw ();
     ~Message ();
};

Notice that the DisplayIn method passes the parameter object whichFrame by reference; the & symbol following the class name "Frame" indicates the passing by reference.

To make the idea of an association concrete, a portion of the Message class's implementation is examined. The private data of the Message class contains two pointers, one for the text string that the Message object displays and one for the Frame object in which the string will be displayed. The Message also contains a Location object indicating where the text string is to appear within the Frame. The private data of the Message class is declared as:

       class Message {
          private:
                                //encapsulated implementation

           char     *msgText;           // display this text string 
           Frame    *msgFrame;          // in this Frame 
           Location msgLocation;        // at this Location in the Frame

          public:
                ...
        };

Avoiding some syntactic details, the code for the DisplayIn method is:

       DisplayIn (Frame& whichFrame) {
           msgFrame = &whichFrame;
        }

This method simply takes the address of the whichFrame object and records that address as the value of the Frame pointer msgFrame. To see the effect of this method, consider the following declarations and code:

   // declaration

        Frame window("Message Test", Location(100,100), Shape(200,200));
        Message greeting("Hello World!", Location(20,20));

    // code

        greeting.DisplayIn(window);

This code will create an association between the greeting object and the window object that can be pictured as shown in the figure below. This association is created because the greeting object retains a lasting pointer to the window object. This pointer will remain valid until it is changed by the greeting object (i.e., the DisplayIn method is called to have the Message point to a different Frame object) or the greeting object itself is destroyed.

A Simple Association

Associations must be managed carefully to avoid the problems of dangling pointers and memory leaks. The example above with the greeting object and the window object can be used to illustrate these two pitfalls. If the window object is deleted, then the greeting object has a dangling pointer because the greeting object still points to the memory space previously occupied by the now destructed window object. A memory leak occurs when the greeting object is deleted but not the window object. In this case, there may be no way to refer to the window object. Such objects continue to occupy memory but are inaccessible. In long-running programs, memory leaks can cause total system failures.

The association between the Message object and the Frame object is used by the Clear method in the Message class. The code for this method is:

       Clear() {               // in Message Class

          Shape msgShape = msgFrame->TextSize(msgText);
          msgFrame->Clear(msgLocation, msgShape);

        }

This code uses the private data members msgFrame and msgText to obtain from the Frame object the shape of the text displayed by the Message object. The Clear method of the Frame class is then used to erase the rectangular area containing the Message object's text. Note that msgText, msgFrame, and msgLocation are private data members that are part of the Message object. These data members are visible to the methods of the Message object and the data members exist as long as the object itself exists (though the value of these data members may change). The Shape object msgShape, however, is a local object of the Clear method; this object exists only during the execution of the Clear method itself.

Also notice that both the Message class and the Frame class have a Clear() method. There is no confusion about which Clear method is intended in a given invocation because the class of the object to which the method is applied determines which method is executed. For example, the invocation

       greeting.Clear();

invokes the Clear method in the Message class because greeting is an object of the Message class. Similarly, the invocation

       window.Clear();

invokes the Clear method in the Frame class because window is an object of the Frame class.

A simple association between a Message object and a Frame object is shown in the Hello World example below. In this version, the text "Hello World" can be dragged by the cursor. The Blinking Text example shown in the next section elaborates on the idea of an association.

Hello World Using the Message Class
Frame window("Message Test", Location(100,100), Shape(200,200));
Message greeting("Hello World!", Location(20,20));

void OnStart() 
{ window.Clear();
  greeting.DisplayIn(window);
  greeting.Draw();
}

void OnMouseEvent(char *frameName, int x, int y, int buttonState)
{ if(buttonState & leftIsDown) 
     greeting.MoveTo(Location(x,y));
}

void OnPaint() {
 greeting.Draw(); 
}          

This version of the Hello World problem is considerably simpler than the one that would result without the Message class. The Message class contains the machinery necessary for a Message object to manage itself more completely. Thus, to move the text on the screen it is only necessary to tell the Message object to move itself - it is not necessary to keep track of this information outside of the Message object.

Tasks

  1. Alternating Text: Write a program that implements a simple association using the Message and Frame classes so that "Hello" and "World!" blink alternately. That is, at any one time, only one of the words is visible.
  2. Use the Query class to implement a small information retrieval system that: (1) uses a FileNavigator to get file to search from the user, (2) gets the search string from the user, and (3) displays in a window the query, the file name, and the result of the query.
  3. Revise either of the two Shrinking Window programs created in the exercises for section 2.4 so that the window contains two Message objects that display the current height and width of the window. The window should also contain a Message with your full name.
  4. Revise the Border Walk program created in the exercises for section 2.3 so that the window contains two Messages that display the current location of the window. The window should also contain a Message with your full name.
  5. Write a Falling Text program in which a text string moves from the top of the display to the bottom of the display. On reaching the bottom of the display, the text string should reappear at the top of the display.



©1998 Prentice-Hall, Inc.
A Simon & Schuster Company
Upper Saddle River, New Jersey 07458

Legal Statement

 

 

 

ÿ