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