Software Design Using C++
Objects and Classes
Object-Oriented Programming
Object-oriented programming places more emphasis on the data items (objects)
than on the operations (functions) that manipulate them. The operations
are closely bundled with the data. Languages such as Smalltalk
that are object-oriented have been around for years, whereas others (such as
C++ and Java ) are newer. However, until recent years
most people used procedural programming, which emphasizes functions over data.
Consider our old friend the
abstract data type. It consists of some kind of data (perhaps fractions)
and operations on that data (such as adding or subtracting two fractions
and so on). If we used procedural C++ programming to implement fractions
we would probably make a fraction to be a structure with numerator and
denominator fields. Then we would have separate functions for add, subtract,
etc. With object-oriented C++ programming we can create fraction
objects, where each object contains the numerator and denominator fields as
well as the functions (add, subtract, etc.). Each fraction object is said
to belong to the fraction class. A class is essentially a user-defined
data type that implements the abstract data type (ADT). If
fraction is the name of our class of fractions, then a variable of
type fraction is called an object of the class. Classes
of objects are heavily used in C++ programming. The following sections
briefly explain five typical features of the object-oriented approach.
Encapsulation
As just mentioned, a fraction object can encapsulate not just the data
fields needed (such as numerator and denominator), but the functions
that manipulate the data as well. Packaging may seem to be rather trivial,
but good packaging makes the job of building an application program
much easier by providing easy-to-use classes of objects. In addition,
objects provide information hiding in that an application program
using a fraction object can only access the data via the public functions;
there is normally no direct access to private data (which is how the
numerator and denominator fields would probably be set up). (In C++
one can make either a data field or a function to be either public or private.)
Inheritance
With inheritance one can easily develop a new class that contains all of the
data fields and functions of an existing class (or classes). Inheriting all
of the features of a single class is called single inheritance. Multiple
inheritance (where a new class inherits all of the features of two or more
classes) will not be covered here.
Where might one use inheritance? For example, suppose that we have a class
named Vehicle . Objects of type Vehicle could
represent your car, your neighbor's tractor, your sister's bicycle, etc.
However, maybe you need to keep additional information or provide other
functions for certain types of vehicles, such as boats. You could then create
a class Boat that is a subclass of Vehicle ,
that is, it inherits all of the features of the Vehicle class.
You just need to add the new data fields and functions; the rest is there
automatically. That can save time: your time. In addition it is possible
to override any of the inherited function code with new code if an operation
has to change somehow for objects of the subclass. For example, we might
want a Print function that prints out all of the data about a
Vehicle object. The Print function for a Boat
object should print the same information as well as any added information kept for boats.
In fact, we might want to have various subclasses of vehicles, such as ones
for land vehicles, boats, and planes. Then we might want to have subclasses
of some of these. For example, we could have classes for cars and trucks
that are subclasses of the land vehicles class. All of this can be pictured
in an inheritance diagram as shown below. Note that an arrow from one class
to a second means that the first class inherits from the second, that is,
the first class is a subclass of the second.
Polymorphism
This refers to the fact that an operation, such as printing, can be
implemented in different ways for different classes in a hierarchy of
classes and subclasses. In the example mentioned above, printing for
boats might be somewhat different than printing for a general vehicle, etc.
The notation used would be something like this:
Vehicle MyVehicle;
Boat YourBoat;
// how to place data into MyVehicle and YourBoat not shown here
MyVehicle.Print();
YourBoat.Print();
|
Note that the Print function has the same name in both cases.
However, the code that gets executed is different, depending on the class
of object to which the call of Print is attached. This is
polymorphism (which literally means "many forms"). You can think of it as
allowing a function to come in many "flavors", just like ice cream. Some
people think of the above example as sending print messages to the two
objects, which carry out the printing based on their own individualized print code.
By the way, calling a function that belongs to a class always follows the
syntax shown above. A function of a class is often called a class
method. The call takes the form:
object.function(parameter-list)
|
Run-time Binding
This is sometimes called late binding. In the above example the
compiler can figure out ahead of time which version of the Print
function to use for each of the two calls of Print . However, it is
sometimes not possible to figure this kind of thing out until the program
is actually running. To handle this situation C++ uses virtual
member functions. The usual way that this problem arises is when we
access objects via pointers and don't know the class of object pointed
to until runtime. This can happen because pointers can sometimes be
dynamically changed to point to various types of objects. (For example,
we could have a pointer to an object of a class and change it to point to
an object of a subclass.) We will not look further at run-time binding here
as it is a fairly advanced topic.
Reusability
Code reuse is great! There is no sense in coding again something that you
already have. Well-designed classes
of objects make reuse easier as objects of these classes can be incorporated
into other application programs. In addition, inheritance can be used to
derive new classes that are based on existing classes, again allowing us to
reuse a lot of existing code. All of this can allow us to design new
software faster and more easily, by building upon what we already have.
A Simple Object-Oriented Example
Let's write a program that creates a few simple objects and then prints
out the contents of each. Perhaps these objects could belong to a
Product class of products that our company has on hand to sell.
The first task is to design the class. Let's say that the data for a product
will consist of a name, a year of manufacture, and the price. The only
function that we said we needed above was a print function, but we will also
need a special function, called a constructor, to create each new
object of the class. The constructor function always has the same name
as the class itself. Look at the following class declaration:
const int StringMax = 48;
typedef char ShortString[StringMax];
class Product
{
private:
ShortString Name;
int Year;
float Price;
public:
Product(ShortString InitialName = "", int InitialYear = 0, float InitialPrice = 0.0);
void Print(void) const;
};
|
The three data fields are fairly obvious. Note that they are in a section
that is labelled private. This means that an application program has no
direct access to them and neither do objects of other classes. The only
way to access these private fields is via the public functions that class
Product provides. This provides protection for the
data in that programmers cannot directly change (and possibly mess up)
the data fields. Forcing programs to access the data via well-written
public methods is much safer. This is the most common way to design classes
and is recommended for your use. It even allows one to replace one
class implementation with another, without having to rewrite the application
program itself. As long as the application only accesses the objects via
the public functions everything still works, but if the application directly
accessed some data field (which was labelled public) there could be a problem
as that data field might not even be present in the new implementation.
Use a clean class design so that one implementation can be pulled out and
another easily plugged in to replace it!
The Print function is the simpler of the two. It has no
parameters and returns no value. The const label indicates
that this is a constant function, one that is guaranteed not to
change the object to which it is applied. Always label your member functions
const if you know that they do not change the object. Clearly,
printing the data contained in an object does not change the object itself.
The Product function is the constructor as it has the same name
as the class itself. Constructors can have parameters if needed. This one
has three parameters, which will contain the initial values that we want to
copy into the three data fields of the newly-constructed object. This
constructor also uses default values with the parameters. The numeric
parameters use zero as the default values and the string parameter uses the
empty string as the default value. This means that when the constructor is
called, if no actual parameters are provided in the call, the default values
are to be used.
It is not always necessary to write your own constructor function for a class
as there is an automatic constructor that simply allocates memory
space for a new object of the class. If you don't need to initialize anything inside
a new object, this automatic constructor may work just fine. If you do write
your own constructor, note that the automatic constructor still gets called first,
to allocate space for the object, and then the code for your constructor is executed.
How do we write a little test program that creates a few objects of this class and
calls the Print function on each? The following illustrates this.
int main(void)
{
Product ProductA("toaster, 4 slice", 1997, 91.98);
Product ProductB; // use default values
Product ProductC("mixer, 5 speed", 1998, 32.95);
cout << "ProductA:" << endl;
ProductA.Print();
cout << "ProductB:" << endl;
ProductB.Print();
cout << "ProductC:" << endl;
ProductC.Print();
return 0;
}
|
The first three lines of this main function show how to use the
Product constructor to create three objects of this class. The
class name comes first, followed by the variable (object) name and any
parameter list. The middle one has no parameter list, so the constructor
will use the default values mentioned above. Calling the Print
function is even easier and follows the syntax discussed earlier: use the
object name, a dot, the function name, and the parameter list.
How can one picture our little application program with its three objects?
The following drawing is one way to do this. The entire application is
drawn as a large box, with an opening for the user interface. The user
interface is where the user interacts with the program (by viewing
information that the program prints, by entering data that the program
prompts for, etc.). Inside the application box are three boxes, one for
each of the three Product objects. Only one of the objects
is drawn large enough to label its internals, which consist of the three
private data fields hidden away in the back, and the public functions at
the front whereby the application can access the object. Note how the
boxes for the public functions have open ends to indicate that this is
how one has access to the object. There are more sophisticated ways to
make drawings of objects, but this simple box diagram will suffice for now.
Now, surely we are missing something. Nowhere have we written any code for the
two class functions (methods). Here it is, complete with comments:
/* 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(ShortString InitialName, int InitialYear, float InitialPrice)
{
// Recall that you cannot simply assign one string into another.
strcpy(Name, InitialName);
Year = InitialYear;
Price = InitialPrice;
}
|
/* Given: Nothing (other than the implicit object).
Task: To print the data contained in this object.
Return: Nothing.
*/
void Product::Print(void) const
{
cout << "Product name: " << Name << endl
<< "Year made: " << Year << endl
<< "Price: " << Price << endl << endl;
}
|
The syntax for a function definition is the return type, the class name,
two colons, the function name, the parameter list, an optional const
label, followed by the code for the function. The constructor is
unique in that it does not have a return type. Look at the Print
function first as it is simpler. Note how it has direct access to
the data fields (Name , Year , and
Price ) of the object. The only question
is this: what object are we talking about? There is no object given as
a parameter. The answer is that we are referring to the three data fields
of the object specified when Print gets called, in our case out
in the main function. This is sometimes called the
implicit object. There is no way to tell when we write the code
for the Print function what object we will apply it to. That
is determined when we do the function call(s) in our test
program.
The constructor simply copies its three parameters into the three data
fields of the implicit object. The comments call it a default constructor.
This means that this constructor can be used with no parameters,
in our case because default values are provided for all three parameters.
(Note that the default values are not listed again in the function definition.)
One can have several constructors in a given class, but only one can be a
default constructor. It is the one that says what to do when no parameters are given.
Now, how do we put all of his together into one program? Although everything
could be shoved into one file, it is common to put the class declaration
(as well as constants and typedefs) into a header file, the code for
the class functions into a .cpp file, and the application program (in this
case consisting just of a main function) into another .cpp file.
Look at the following three files to see how this division has been done.
Adding to Our Example
It would be nice if our Product class provided more functionality.
Then we could do more in our application program. Well, let's add some more class
functions. It is common to use set and get functions,
often with names that start with set and get. A get function is used to get
information out of an object. A set function is used to place data into
the object. Thus the flow of data is reversed.
Since we have three data fields in any object of our class, we will write
three get functions, one to get a copy of each data item. We could also
write three set functions, but let's just write one to set a value into
the Price field. It could be named SetPrice ,
but we will name it ChangePrice . Take a look at out revised
project in the following files:
Note that our three get functions are constant functions. This is always the
case as looking up data in an object does not change the object. Two of
our get functions return their values in the function name, but the
GetName function needs to return a string (a character array),
which cannot be returned in a function name. Thus we use a parameter instead.
The code for the function uses strcpy to copy the string from
the Name field into this parameter.
Note well 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. Follow the above link for further information
on this important topic.
The test program adds the code shown below in order to try out the new class
functions. Note the familiar syntax for each call: object name, dot, function
name, and parameter list. The functions that return values in the function
name are embedded in output statements as a quick way to print these numbers.
Refer to GetYear and GetPrice below.
ProductA.ChangePrice(87.95);
cout << "After changing the price on ProductA we have:" << endl;
ProductA.Print();
// Let's get the values of the three fields individually:
ProductA.GetName(Result);
cout << "Name of ProductA: " << Result << endl;
cout << "Year made for ProductA: " << ProductA.GetYear() << endl;
cout << "Price of ProductA: " << ProductA.GetPrice() << endl << endl;
|
The following drawing shows our application as the large outer box, and
the three objects that it manipulates as the three inner boxes. The details
are only shown for one of the three objects, but in reality all three would
look the same. Note that the picture is the same as the picture for the
previous program except that each object has more public functions that
can be used to access the object.
A Program Using Inheritance
A Software Engineering Example
Suppose that we want a program that allows the user to interactively look up
information about cars that we are selling that fit any price range that the
user cares to specify. Let's again use the
software development life cycle
to assist us with this problem.
Analysis
In this step we need to get a clear handle on what the program should do.
This is usually done by specifying the inputs, processing, and outputs.
Let's think about input data first. Suppose that the car data isn't supplied
as input at all, but is hard-coded into the program. This isn't very
flexible, but is fine for a simple program. (If we wanted to design a
better program, we could have it read the car data from a file. This
would be a good exercise for the reader to try!) Suppose that we want the
following data for each car: a description (make and model), the year the car was
made, whether it is a 2-door or 4-door, the horsepower of the engine, and the price.
The processing is simple enough. We want the user to be allowed to repeatedly
look up the data on all cars that fit a price range that the user supplies.
That means input: we need to input the price range, probably by having the
user enter a minimum and a maximum price. The processing is a type of search,
but has to produce all cars that fit the price range, not just the first car
that can be found to fit the range. The output is also clear: for each car
found that fits the range, print its data on the screen.
Design
Consider the data that we must deal with. We have similar data about several
cars. We might first think of using an array of records to hold this data.
That would work, but then we remember that we have a Product
class already written that holds some of the data that we need. We could
use inheritance to derive a subclass Car that adds the extra
data and any new functions that we might need. Then we could use an array
of Car objects. Reusing existing code should allow us to
make fast progress on this problem. The only data fields that we need to
add would be ones for the horsepower and the number of doors on the car.
Now let's think about the class functions that are needed. Inheritance
will give us a function to print the data for a general Product
object, but we will want to override that with a Print
function that will also print the specialized Car data.
We might also want get functions to get the horsepower value from an
object and to get the number of doors from an object. We might not need
them to solve this problem, but it cannot hurt to provide them.
Certainly, we will need a constructor for the Car subclass
that will copy five initial values into the five data fields of the
object being constructed.
If we had a more complicated hierarchy of classes and subclasses, we might
draw an inheritance diagram at this point. In our case the diagram would
just consist of the word Car with an arrow to the word
Product , showing that the former is derived by inheritance from the
latter. That diagram is too simple to be worth drawing, so let's skip it here.
At this point we might sketch out the class declaration. Let's use
a box diagram much like we used earlier. This gives a picture of a
Car object, showing both the private data fields hidden
inside as well as the public functions at the interface to the object.
In the drawing above we could also give return types and parameter lists for each
of the functions, but that would rather clutter up the diagram. Instead, at this
stage in the design, let's write out the class declaration as shown below:
class Car:public Product
{
private:
int Doors;
int Horsepower;
public:
Car(ShortString InitialName = "", int InitialYear = 0,
float InitialPrice = 0.0, int InitialDoors = 0,
int InitialHP = 0): // initialization list follows:
Product(InitialName, InitialYear, InitialPrice),
Doors(InitialDoors), Horsepower(InitialHP)
{
}
void Print(void) const;
int GetDoors(void) const;
int GetHorsepower(void) const;
};
|
Since this is our first example of how to derive one class
from another via inheritance, examine this class declaration carefully.
The first line says that the class Car is being derived by
public inheritance from our existing Product class. This
means that it will inherit all of the fields and functions of the
Product class so as to keep them as public as possible. Essentially
this means that public fields and functions come in as public in the subclass,
but private ones become inaccessible. The latter might seem to be surprising
at first glance, but makes sense because private fields of a Product
object are not supposed to be accessible to anything outside of the
object. Thus a Car object will have no direct access to these
inherited fields; its only access will be via the inherited public functions
such as GetPrice . There are other types of inheritance besides
public inheritance, but they are not covered here.
In the class declaration above, the added private fields Doors
and Horsepower are obvious. Then we have four added public
functions. The last two are pretty clear as they are the "get" functions
that were mentioned above. The Print function overrides
the one that is automatically inherited from the Product class.
(Notice that this is an example of polymorphism.)
The code for the new Print function will be written later.
The first added function is obviously a constructor since it has the same
name as the class name Car . Then we see five parameters
with default values. The default values are there in case the user should
call the constructor without supplying actual parameters. What is new is
the colon symbol followed by an initialization list. This initialization list
is repeated below for convenience:
Product(InitialName, InitialYear, InitialPrice),
Doors(InitialDoors), Horsepower(InitialHP)
|
The first line says that when the Car constructor gets called,
the old Product constructor should be called first, with the
three parameters shown, to do some of the initialization. The second line
is an easy way to specify that InitialDoors should be copied
into the Doors field and InitialHP into the
Horsepower field. The empty curly braces that follow show
that there is no other code for the function; everything is taken care of
in the two lines preceding the braces.
Next, we turn to designing the new class functions in detail. One way to
do this is to write out the comment section for the new functions. The
constructor's job is pretty obvious as it just copies its five parameters
into the five fields of the newly constructed object.
Thus we move to the comment sections for the other new functions:
/* Given: Nothing (other than the implicit Car object).
Task: To print the data contained in this object.
Return: Nothing.
*/
void Car::Print(void) const
|
/* Given: Nothing.
Task: To look up the number of doors of the implicit Car object.
Return: This number of doors in the function name.
*/
int Car::GetDoors(void) const
|
/* Given: Nothing.
Task: To look up the horsepower of the implicit Car object.
Return: This horsepower number in the function name.
*/
int Car::GetHorsepower(void) const
|
That about completes the design of the Car class, but there
is more to our problem than that. We also indicated that the application would
use an array of objects of this class. We might thus decide to use the following:
const int MaxCars = 6;
typedef Car CarArrayType[MaxCars];
|
We also need to design the application program itself. We already said
that we needed a kind of search function to go through the array and print
the data on all cars that fit a given range. This leads to the following
stand-alone function (that is, a function which is not a member of a class):
/* Given: CarArray An array of Car objects.
NumCars The number of cars in CarArray, from index 0 onward.
Task: To ask the user for the minimum and maximum price to consider
and then to print the data on all cars in the array that fit
that price range.
Return: Nothing.
*/
void Lookup(CarArrayType CarArray, int NumCars)
|
Prototyping
Next we would probably build a prototype, rather than attempt to code the
whole project. One likely way to do this would be to write the code for
the class functions, since that looks to be fairly simple, but to use a
stub for the Lookup function. For example, this stub could
just print a "function incomplete" message and not
really search through the array at all. The main function
would be written out. Then the prototype could be tested and debugged.
These details are not shown here in the interest of saving space. The
reader is encouraged to create this prototype as a practice exercise.
Coding
We need to code the class functions as well as the stand-alone
Lookup function. Let's do the Lookup function
first. Assuming that the local variables have been suitably declared, the
code would look something like this:
cout << "Enter lower bound on price of cars to look at: ";
cin >> LowerBound;
cout << "Enter upper bound on price of cars to look at: ";
cin >> UpperBound;
cin.get(); // get the newline that followed the UpperBound number
cout << endl;
for (k = 0; k < NumCars; k++)
{
Price = CarArray[k].GetPrice();
if ((LowerBound <= Price) && (Price <= UpperBound))
{
CarArray[k].Print();
cout << "Press ENTER to continue";
cin.get();
cout << endl;
}
}
|
The for loop above methodically goes through each Car
object in the array. For each object we use GetPrice to obtain
the car's price. The if is obviously used to call
Print only on those objects whose price fits the desired range.
The main function was supposedly coded in the prototyping
section, but since we did not show it there, here it is now:
CarArrayType CarArray;
char Reply;
// One can assign one object into another of the same type PROVIDED
// all of the data resides inside of the object:
CarArray[0] = Car("Ford Mustang", 1995, 3456.99, 2, 150);
CarArray[1] = Car("Chevy Nova", 1977, 1020.88, 4, 120);
CarArray[2] = Car("Chevy Blazer", 1994, 3350.99, 2, 140);
CarArray[3] = Car("Toyota Camry", 1996, 6380.99, 2, 100);
CarArray[4] = Car("Honda Civic", 1993, 2895.55, 2, 100);
CarArray[5] = Car("Ford Bronco", 1998, 12569.99, 2, 120);
do
{
Lookup(CarArray, MaxCars);
cout << endl << "Do another lookup (y/n)? ";
cin >> Reply;
cout << endl;
}
while (tolower(Reply) == 'y');
|
We start with CarArray as an array of empty Car
objects. (The data fields will contain the default values specified in
our constructor.) We then use a series of assignment statements to copy
Car objects that contain interesting data into the array.
Unless the = operator is overloaded (which it is not here), the assignment
is a simple bitwise copy. This works fine when copying an object into another
of the same type as long as all of the data for the first object is contained
within that object. However, if the first object contains a pointer to
some data (such as a character-array type of string) that is outside of the object, then the
assignment will probably not give what you want. In the case of a pointer
to a string, you will end up with two objects with pointers to the same
array of characters. If the first object happens to change some of those
characters, then the string for the second object appears to change as well!
The two objects are intertwined, are not really completely separate.
Finally, we code the new class functions. This is fairly easy here. The
code for the new Print function and one of the get functions
is shown below. Note that Print is allowed to call the
inherited GetName function. The Product:: in
front is to show that it belongs to the Product class. In this
case it is unnecessary since the derived class has no GetName
function. If both classes have a function by the same name, then the
class name and two colons would be necessary.
void Car::Print(void) const
{
ShortString CarName;
Product::GetName(CarName);
cout << "Type of car: " << CarName << endl
<< "Year: " << Product::GetYear() << endl
<< "Price: " << Product::GetPrice() << endl
<< "Number of doors: " << Doors << endl
<< "Horsepower: " << Horsepower << endl << endl;
}
|
int Car::GetDoors(void) const
{
return Doors;
}
|
For a look at the complete project, check through the following files. All
five of these files would be part of the C++ project.
Testing and Debugging
Since this program uses hard-coded data, it is pretty straightforward to
test it. The list is not long. We can try various price ranges and
check with our own eyes that the correct cars are printed. Also try
specifying a range that no cars fit to see that no cars are printed.
It appears that everything is working OK.
Maintenance
However, it is possible that we have missed something above. After the
program is put in use, someone might notice an error. Then we would
have to fix it. A user might also request that a new feature be added
to the program, such as allowing it to look up all cars that match a
particular make and model, or all cars that match a particular year.
However, if we were really going to do a lot of this type of thing
what we really would want to do would be to set up a database using
relational database software, as it already has built into it the code to
do any type of query (lookup) imaginable.
Documentation
We have created a large amount of documentation above.
We have written descriptions for most of the stages, a drawing of a
Car object, the internal documentation (program comments),
probably a record of testing, etc. A user's manual might even be written
although this program is pretty simple to use.
Related Items
Back to the main page for Software Design Using C++
|