CIS Logo SVC Logo

   Computing & Information Systems
   Department

 

Schoology Facebook        Search CIS Site      Tutorials

Software Design Using C++



More Complex Object-Oriented Features

Look at the following files. They rewrite our Product class, used in the Objects and Classes section of these Web pages, to include a number of more advanced object-oriented features.

Copy Constructor

Let's start by introducing the so-called copy constructor. In the header file you will see that there are two constructor functions, the default constructor that we used before, as well as the following:


Product(const Product & OldProduct);

The copy constructor takes an existing Product object and creates a new object with a copy of the data from the existing one. In essence it let's you easily "clone" an object. Here is the code for this function:


/* Given:  OldProduct  An existing Product object.
   Task:   This is a copy constructor.  It creates a new Product
           object containing a copy of the data from OldProduct.
   Return: Nothing is directly returned, but the implicit object is created.
*/
Product::Product(const Product & OldProduct)
   {
   strcpy(Name, OldProduct.Name);
   Year = OldProduct.Year;
   Price = OldProduct.Price;
   }

Of course, to copy a string field we need to use strcpy, whereas the numeric fields can be copied with an assignment statement. Recall that when we give the field names with no object name in front (as in Name, Year, and Price) these refer to fields of the implicit object, the object that we are using this function on. In this case, that means the object that we are creating with this constructor.

Recall that strcpy does not check to see if the destination string has enough room for the data. This can open up one's software to a buffer overflow attack, a well-known type of security flaw. When writing professional software use a copy function that does proper bounds checking instead. The strncpy function is one possibility. Using the string class is an even better approach. Supposedly it is not possible to have a buffer overflow if you use objects of the string class. Follow the above link for further information on this important topic.

Now look at the test program to see how the copy constructor is called. There are two equivalent ways of calling it:


Product ProductB(ProductA);   // one way to call the copy constructor
Product ProductC = ProductA;  // another way to call the copy constructor

The first one creates a new object ProductB that is a clone of ProductA. The second creates ProductC as another clone of ProductA.

Overloaded Operator

In the header file you will see that there is a boolean operator function with the following prototype. It is listed as a public function and can thus be applied to any Product object whether used in an application program, in the code for another class's functions, etc.


bool operator<(const Product & RHS) const;

Operators are seen as a special type of function that uses a function name consisting of operator followed by the operator symbol(s). There are some operator symbols that cannot be overloaded to work on your own objects. See a good reference book for details. In this case we are trying to set up an overloaded less than operator that compares two Product objects. You might wonder where the two objects are. One is the implicit object, and the other is the RHS parameter (short for right-hand side). Note that the parameter is a constant reference parameter for efficiency reasons: We do not want a call to this function to have to copy the entire object that is used as the parameter. The const at the end of the above line indicates that the function itself is a constant function. In other words, it does not change the object to which it is applied (the "left-hand side" object, if you will).

Using the overloaded boolean operator is easy. If you look at the test program, you will see the following call. It is as easy as testing something like 2 < 3. In this case, ProductA matches up with the implicit object; it is the left-hand side of the comparison. ProductD matches up with the RHS parameter; it is the right-hand side of the comparison.


if (ProductA < ProductD)
   cout << endl << "ProductA is less than ProductD in price" << endl;
else
   cout << endl << "ProductA is NOT less than ProductD in price" << endl;

The following is the complete code for this overloaded operator. It is pretty easy to follow. The hard part is getting used to the notation in the function prototype and figuring out which object is the left-hand side and which is the right-hand side.


/* Given:  RHS   A Product object (the right-hand side of the < ).
   Task:   To compare the price of the implicit Product object to that
           of the RHS.
   Return: In the function name return true if the implicit object is
           less than RHS in price, false otherwise.
*/
bool Product::operator<(const Product & RHS) const
   {
   if (Price < RHS.Price)
      return true;
   else
      return false;
   }

Friend Function

There is another way to create an overloaded operator. In general an overloaded operator is a standard operator (like +, =, <) that is extended to allow it to be used on objects (including user-defined objects) with which the operator otherwise could not be used. The usual way to set up such an operator is as a method of the class like we did above. However, it is also possible to create the operator as a friend function. A friend function does not belong to the class, but the class does contain a notice like the following to tell the class to give the friend function direct access to any of its private fields. Many people say that this defeats the purpose of information hiding and so avoid using friend functions. Nevertheless, we will take a look at how an operator can be set up as a friend function.


friend bool operator==(const Product & LHS, const Product & RHS);

For a boolean operator created as a friend function, the two objects that it applies to are passed as parameters. Thus you see the parameters LHS and RHS above. (These are short for left-hand side and right-hand side, of course.) The operator is used in the obvious way. Here is a call of this operator taken from our test program:


if (ProductA == ProductB)
   cout << "ProductA and ProductB have the same price" << endl << endl;
else
   cout << "ProductA and ProductB do NOT have the same price" << endl << endl;

The code for this operator is shown below. Note that as a non-class function it has no implicit object. When comparing the two Price fields you must specify which object is meant in each case.


/* Given:  LHS   A Product object (the left-hand side of the == ).
           RHS   A Product object (the right-hand side of the == ).
   Task:   To compare the prices of the LHS and RHS objects.
   Return: In the function name return true if the LHS object is equal
           to the RHS object in price, false otherwise.
*/
bool operator==(const Product & LHS, const Product & RHS)
   {
   if (LHS.Price == RHS.Price)
      return true;
   else
      return false;
   }

Comparison operators such as == and < can be implemented either as class functions or as friend functions. Because of the principle of information hiding, that it is best not to allow any more access to data than necessary, the class function route is usually the best to use.

Overloaded Stream Operator

As another example of an overloaded operator implemented as a friend function, see the overloaded << stream operator. In the header file you will find the following which lets the Product class know that this operator should be allowed direct access to the private fields of objects of this class.


friend ostream & operator<<(ostream & OutputStream, const Product & Prod);

The following use of this overloaded operator is similar to those in our test program. Note how nicely it lets us output a Product object. The operator is seen as a function with operator appended with two less than symbols, all as the function name. There are two parameters to the function: The first is the output stream (cout in the example below), and the second is the Product object to output (ProductC below). After each field of the product has been output, a reference to the stream is returned so that as many items as desired can be chained together in an output statement. For example, after ProductC is output below, it is as if we then did cout << endl at that point. It takes a little getting used to, but once you see the pattern, it is easy.


cout << ProductC << endl;

The code for our overloaded operator is shown below. Note the use of the return to return the modified file stream (modified since we have written to it).


/* Given:  OutputStream  An output file stream.
           Prod          A Product object.
   Task:   To print the contents of Prod onto the OutputStream.
   Return: The modified file stream is returned in the function name.
           This allows one to chain together more than one << operator
           as in:   cout << ProductA << endl << ProductB << endl;
*/
ostream & operator<<(ostream & OutputStream, const Product & Prod)
   {
   OutputStream << "Product name: " << Prod.Name << endl
      << "Year made: " << Prod.Year << endl
      << "Price: " << Prod.Price << endl << endl;

   return OutputStream;
   }

The test program also shows that it is possible to use our overloaded stream operator to output a Product object to a text file. It works just as well as cout.


// Let's try writing to a text file:
OutFile.open("try3.txt", ios::out);
if (OutFile.fail())
   {
   cerr << "Could not create file try3.txt" << endl;
   exit(1);
   }

OutFile << "This is ProductD:" << endl << ProductD;
OutFile.close();

Overloaded Assignment Operator

Finally, let's look at the overloaded = (assignment) operator found in our Product class. It has the following prototype in the header file. Note that the right-hand side of the assignment is seen as the parameter to the operator= function. The function returns a reference to a Product object so that assignment statements can be chained together.


Product & operator=(const Product & RHS);

The complete code for this overloaded assignment operator is given below. Note that the RHS parameter gets copied into the implicit object, the one that the operator= function is applied to.


/* Given:  RHS   An existing Product object.
   Task:   This is an overloaded assignment operator.  It copies all of
           the data from the RHS object into the implicit object.
   Return: A reference to the implicit object is returned in the function
           name so as to allow one to chain assignments together as
           in:  ProductA = ProductB = ProductC;
           The implicit object is modified.
*/
Product & Product::operator=(const Product & RHS)
   {
   strcpy(Name, RHS.Name);
   Year = RHS.Year;
   Price = RHS.Price;
   return *this;
   }

The one new feature found in the above code is the use of the this pointer. Inside a class function, the this pointer is automatically set up to point to the implicit object, the one the function was applied to. By applying the * operator to the this pointer one gets the implicit object itself. Thus our return statement does indeed return the implicit object (for use in chaining assignments together). (There is a complete section discussing pointers, should you wish to refer to it.)

The test program uses the overloaded assignment operator as shown below. Note that two assignments have been chained together. First, the ProductD object is assigned into ProductC. Second, the return value is assigned into ProductB. Thus, both ProductC and ProductB become copies of the ProductD object. The return value of the leftmost assignment operation is not used; it is simply thrown away.


// Test of the overloaded assignment operator:
ProductB = ProductC = ProductD;

Note the similarity of the overloaded assignment operator to the copy constructor. Both are used to produce copies of an existing object. However, with the copy constructor the copied object is brought into existence, but with the overloaded assignment operator the copied object already existed (though it may have contained different data beforehand).

The Destructor

The destructor class function is more or less the reverse of the constructor function. The constructor is used to create a new object of the class; the destructor is used to destroy it, to wipe it out. The name of the destructor is the class name with a ~ in front of it. For example, the destructor for our Product class is ~Product.

You might claim that our Product class had no such destructor function, but actually it did. There is an automatic destructor function that is used when you don't write any code for your own destructor. All that the automatic destructor function does is to wipe out the object. That is, it reclaims the memory space taken up by the object itself. Note that the automatic destructor is like the automatic constructor that is available to build a new object. The automatic constructor allocates memory space for a new object. If any initialization work needs to be done, then you must write the code for your own constructor function.

Usually you will see no explicit call of the destructor function. Rather, it is called automatically when an object variable goes out of scope. For a local variable this occurs when we exit the function. For a global variable (generally a bad thing to use) this happens when the program finishes. For a variable declared inside of a {...} block, this occurs when we reach the closing }. For an array of objects, the destructor gets called for each object in the array when the array goes out of scope.

To see when the destructor is called in our example using the Product class, add the following destructor function to the product3.cpp file. This destructor just prints out a message when it is called. (Then the automatic destructor takes over and wipes out the memory space taken up by the object.)


Product::~Product(void)
   {
   cout << "Destructor called" << endl;
   } 

To compile the project with the above destructor added, you will of course need to edit the header file product3.h so that it contains a line for the public destructor function. The result should look like this:


class Product
   {
   friend bool operator==(const Product & LHS, const Product & RHS);
   friend ostream & operator<<(ostream & OutputStream, const Product & Prod);
   private:
      ShortString Name;
      int Year;
      float Price;
   public:
      Product(ShortString InitialName = "", int InitialYear = 0, float InitialPrice = 0.0);
      Product(const Product & OldProduct);
      ~Product(void);   //***** Here is the new destructor function.
      void Print(void) const;
      void GetName(ShortString ProductName) const;
      int GetYear(void) const;
      float GetPrice(void) const;
      void ChangePrice(float NewPrice);
      bool operator<(const Product & RHS) const;
      Product & operator=(const Product & RHS);
   };

Remember that no change is needed in the application program try3.cpp, as the destructor is called automatically. Although in this example there was no real need for us to write a destructor function, in later examples we will see cases where we must write our own destructor function. If you wish to glance ahead, look at the example on Dynamically Allocating Objects in the Pointers section of these Web pages.

Related Items

Back to the main page for Software Design Using C++

Author: Br. David Carlson with contributions by Br. Isidore Minerd
Last updated: October 20, 2016
Disclaimer