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 and
to use string objects instead of the old character array strings (C-style strings).
You can see the old version of this presentation on more complex
object-oriented features, if you wish. Note that C++ compilers are making it
more difficult to use the old C-style strings, and especially functions like
strcat and strcpy that can lead to significant security risks, primarily
buffer overflow attacks.
Constructors
Let's start by looking at the so-called constructors.
In the header file you will see that there are two constructor functions. The
default constructor, mentioned first, has the following declaration:
Product(string InitialName = "", int InitialYear = 0, double InitialPrice = 0.0);
|
Notice that we supply simple initial values for the 3 parameters of the object being constructed.
Here is the code for that constructor:
/* Given: InitialName A string containing the product name.
InitialYear The year this product was made.
InitialPrice The product's price.
Task: This is the default constructor. It creates a new Product object
containing the 3 items listed above.
Return: Nothing is directly returned, but the object is created.
*/
Product::Product(string InitialName, int InitialYear, double InitialPrice)
{
Name = InitialName;
Year = InitialYear;
Price = InitialPrice;
}
|
Next, we look at the copy constructor, which has the following declaration. Note that
neither constructor has a return value, not even void. Also recall that the name of
any constructor has to match the name of the class, Product in this case. The two
constructors that we have here can be distinguished by their different parameter lists.
Our default constructor takes 3 parameters, but the copy constructor has only one parameter.
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)
{
Name = OldProduct.Name;
Year = OldProduct.Year;
Price = OldProduct.Price;
}
|
Because we are using string objects, we can copy a string into a string field such as Name
with a simple assignment statement. (That would not work with C-style, character array strings.)
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.
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 concatenated 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;
}
|
Compare the above operator << with the following ordinary Print
function. The code inside is nearly identical. One difference is that this
Print function only prints to cout , the screen, whereas
the overloaded operator << can print to any reasonable output stream, including
cout . Another difference is that << can be used many times in one
command in order to output several Product objects.
void Product::Print(void) const
{
cout << "Product name: " << Prod.Name << endl
<< "Year made: " << Prod.Year << endl
<< "Price: " << Prod.Price << endl << endl;
}
|
The test program shows an example of using our overloaded stream
operator << to output a Product object to a text file. It works
just as well as using the << operator with 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)
{
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 destination 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 .
If we did not write a destructor for our Product class (or any other class), it
would still have 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, a simple destructor function was added
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.)
/* Given: Nothing other than the implicit object.
Task: This is the destructor. It prints a message and the contents of
the implicit object. Then it reclaims the memory space of this
implicit object.
Return: Nothing
*/
Product::~Product(void)
{
cout << "The destructor has been called on an object containing:" << endl;
(*this).Print();
// The automatic destructor runs at this point, reclaiming the memory
// space used by the implicit Product object.
}
|
Remember that the this pointer automatically points to the implicit object.
Thus the Print is being called on the implicit object, the one being destroyed.
This is so that we can see the data right before the object gets wiped out.
Running an Experiment
In order to understand when the destructor wipes out various objects, our test program
contains a function named experiment . This function is called from the
main function and has a reference parameter and a value parameter, both
for Product objects. This function also has a local variable that is a
Product object. Here is the code for this experiment function:
/* Given: ReferenceParamProduct - a reference to an existing product in main.
* ValueParamProduct - a copy of a product object in main.
* Task: Create a local variable, ProductLocal, that is a Product object and
* then simply print the data that is in ReferenceParamProduct,
* ValueParamProduct, and ProductLocal.
*/
void experiment(Product & ReferenceParamProduct, Product ValueParamProduct)
{
Product ProductLocal("Local variable used as a test", 2021, 999.99);
cout << endl << "In function experiment, ReferenceParamProduct prints out as:"
<< endl << ReferenceParamProduct;
cout << "ValueParamProduct prints out as: " << endl << ValueParamProduct;
cout << "Local variable ProductLocal prints out as:" << endl << ProductLocal;
cout << "This is the last line of function experiment" << endl;
}
|
From the output below, we can tell the following:
- When the
experiment function prints its reference parameter, it prints the data from the object that
the reference points to back in the main function.
- Printing the value parameter prints the data that is in the value parameter. Since value
parameters contain a copy of their actual parameters, we print a copy of what the corresponding
actual parameter contained back in the
main function.
- Printing the local variable prints exactly the data that was put into it with the constructor
used by the
experiment function.
- When we reach the last line of the
experiment function, the destructor wipes out the local variable.
- Also, at this same point, the second use of the destructor wipes out the value parameter (but not the actual parameter
back in main that was copied into it).
- After we return to the
main function and reach the end of that function, all 4 objects in main are wiped out
by automatic calls of the destructor for each of these. This includes the dishwasher object in main that the
experiment function had a reference parameter to.
- Essentially, objects are wiped out by the destructor when they go out of scope.
Let's see how object parameters and local variables behave
In function experiment, ReferenceParamProduct prints out as:
Product name: dishwasher
Year made: 2020
Price: 289.95
ValueParamProduct prints out as:
Product name: mixer, 5 speed
Year made: 2019
Price: 22.95
Local variable ProductLocal prints out as:
Product name: Local variable used as a test
Year made: 2021
Price: 999.99
This is the last line of function experiment
The destructor has been called on an object containing:
Product name: Local variable used as a test
Year made: 2021
Price: 999.99
The destructor has been called on an object containing:
Product name: mixer, 5 speed
Year made: 2019
Price: 22.95
This is the first line in main after returning from function experiment
This is the last line before main does its return 0;
The destructor has been called on an object containing:
Product name: mixer, 5 speed
Year made: 2019
Price: 22.95
The destructor has been called on an object containing:
Product name: mixer, 5 speed
Year made: 2019
Price: 22.95
The destructor has been called on an object containing:
Product name: mixer, 5 speed
Year made: 2019
Price: 22.95
The destructor has been called on an object containing:
Product name: dishwasher
Year made: 2020
Price: 289.95
To keep track of parameters and local variables when a function gets called, such as the
experiment function above, a drawing of the run-time stack is quite helpful.
Here is a somewhat simplified picture of the run-time stack after the experiment
function gets called:
Short example data is shown in the picture for the Name , Year , and
Price fields. It is not the same data that was used in our test program above.
Note that the stack frame for function main contains the data for the 4 objects that main
uses, objects ProductA , ProductB , ProductC , and ProductD .
Initially, that is all that is on the stack for this program.
When the experiment function is called, a stack frame for this function is added to the main
memory region above the stack frame for the main function. Recall that an ampersand character & is used in front
of a formal parameter to indicate that it is a reference parameter. Since we have a reference parameter named
ReferenceParamProduct , that parameter contains only a reference, a pointer, to its corresponding actual
parameter, ProductA . This is because ProductA is filled in as the first parameter
(which is the reference parameter) when main calls the experiment function.
Whenever the experiment function refers to any of the 3 fields of ReferenceParamProduct ,
it just follows the pointer and looks at the fields of ProductA in the main function.
The second parameter for the experiment function, ValueParamProduct , does contain data.
This is because it is not a reference parameter. Rather, it is what is called a value parameter, one
that uses no & character. The value (or values) in
the actual parameter get copied into the formal parameter. Thus you see in the picture that ValueParamProduct
contains copies of all of the data from the actual parameter ProductB in the main function.
Next in the stack frame for the experiment function is what is called a return address. This is the location
the computer is to go to when it is done executing the experiment function. Above that in the stack frame
is a Product object named ProductLocal . It is created by the first line of code inside of the
experiment function. Since it is created in this function, it is local to this function. As soon as this
function ends, this variable's lifetime is over. At that point, control returns to the main program, to the command
after the call to function experiment .
Summary of What is in the Product Class
The following shows everything that is in our example class Product .
Besides the items already discussed above, this class contains both a get function and a
set function for each of the 3 fields in any object of this class.
class Product
{
friend bool operator==(const Product & LHS, const Product & RHS);
friend ostream & operator<<(ostream & OutputStream, const Product & Prod);
private:
string Name;
int Year;
double Price;
public:
Product(string InitialName = "", int InitialYear = 0, double InitialPrice = 0.0);
Product(const Product & OldProduct);
~Product(void);
void Print(void) const;
string GetName(void) const;
int GetYear(void) const;
double GetPrice(void) const;
void SetName(string NewName);
void SetPrice(double NewPrice);
void SetYear(int NewYear);
bool operator<(const Product & RHS) const;
Product & operator=(const Product & RHS);
};
|
If you wish to glance ahead at related topics, 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++
|