CIS Logo SVC Logo

   Computing & Information Systems
   Department

 

Schoology Facebook        Search CIS Site      Tutorials

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.

[inheritance drawing]

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.

[box drawing of application and objects]

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.

[box drawing of application and objects]

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.

[box diagram of a Car 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++

Author: Br. David Carlson with contributions by Br. Isidore Minerd
Last updated: April 03, 2021
Disclaimer