User Tools

Site Tools


haas:spring2011:cprog:tasks:task7


Corning Community College

C/C++ Programming


Task 7: Inheritance

~~TOC~~

Objective

To explore the concepts of Inheritance, its importance to Object- Oriented design, and how to implement it in your programs.

Reading

In “C++ Programming: Program Design Including Data Structures” by D.S. Malik, please reference:

  • Chapter 12 (“Inheritance and Composition”, pages 675-744).

Definition

In Object-Oriented Programming, inheritance is the ability to derive new classes from existing classes. A derived class (or “subclass”) inherits the instance variables and methods of the “base class” (or “superclass”), and may add new instance variables and methods.

Terminology

  • Base Class: a class from which one or more classes are derived. It often contains the fundamental properties from which a class hierarchy is created. (Also referred to as a parent- or super-class)
  • Derived Class: a class that inherits members from a parent class, and also defines new functionality of its own. (Also referred to as a child- or sub-class)

Background

Classes have proved to be an effective way to organize common data and functions (ie “members”) in a single unit container. We can create units of Rectangles, Quizzes, Circles; any assortment of objects. This allows us to further extend our problem solving abilities and focus on the manipulation of top-level matters.

With organization better managed, we can begin to abstract away the technical details of the computer-processed code. By doing this, we can apply some mild personalizations to our programs- members of the Rectangle class, friends of the Quiz class; in a sense, our classes have behaviors.

So what are we doing personalizing our programs? The computer still has to process it in the end, so whatever we do has to ultimately simplify to the realm of computation. The primary reason is to open the door to allow us more possibilities to solve problems. Imperative/structured problem solving is and will forever be an important methodology for solving certain problems, as will logical problem solving. Object-Oriented problem solving gives us yet another outlet for creating solutions.

If there are so many different methodologies, which one is best? The answer: all of them, each in their own place.

For example, if you have a mystery to solve, chock full of clues that logically interconnect, or can be deductively simplified to find out “who dunnit?”, the problem best lends itself to a local solution, in a language such as Prolog or Haskell. Sure, you could code an imperative solution in C, or Pascal, or even assembly language, or an Object-Oriented solution in C++ or Java. But you know what? It would be overkill. You'd spend considerably more time implementing the details (like the deductive logic engine) that by the time you got to your desired goal, you'd be exhausted, and glad to be finished with the problem. But if you code in a language better suited for the task at hand, many details will naturally be abstracted away, allowing you to better focus on the problem at hand.

Object-Orientation gives us the tools to solve problems in a more human manner- that is, through relationships that we ourselves may naturally ponder. Get rid of the details of how to do it, and solve the problem based on the “black-box” building blocks we create for ourselves. (This is in fact almost reverse of what you've been exposed to so far– in programming classes you've been told to train yourself to think of each step to tell the computer what to do, unlike how some people “just do it”… ignoring the steps, glossing over the details. And it isn't that the details don't exist, they are just being abstracted away from that perspective of looking at the problem. OOP also tries to do just that- ignoring the steps, and gets right to the results). A great advantage for tackling certain problems, but again- not all of them. Be sure to use the right tool for the right job. You are here to learn about Object-Oriented Programming so you can have it as an option when evaluating how best to go about solving a problem.

The subject at hand: Inheritance

When we look at objects in the real world, we can see various relationships among them. A dog is a dog, and a cat is a cat. We could design classes to describe these two objects, or we could save ourselves even more work and further simplify the situation. Are there any traits in common between a cat and a dog? They both have 4 legs, two eyes… they're both mammals!

We could create a general “mammals” class, then have the “dog” and “cat” classes derive characteristics from the “mammals” class.

Questions

Can many objects be classified / simplified in this way? Look at the following objects, and tell me what they have in common:

  1. banana, strawberry, grape, watermelon
  2. fly, mosquito, grasshopper, cricket
  3. spring, summer, fall, winter
  4. Australia, Antarctica, Africa, Eurasia

Just as in structured / imperative problem solving, or logical problem solving, we must have the ability to find, comprehend, and create patterns. Pattern Matching is an important skill that has value across all disciplines, so it is doubly important as a programmer that these abilities are sharp, as it will greatly help you in solving problems.

Looking at the above items, we can see the common traits each group of objects share. We can come up with a common base from which we can derive and customize to the particular case.

Peripheral Organization

Let's go through another example:

tape, disk, compact disc, RAM

What do all these objects have in common?

They are all types of storage: each object can store data, and provides the user with a method of accessing it.

So, what if we classified all of these objects as “storage” devices? tape IS A storage medium. disk IS A storage medium… we could then create a general purpose “storage” class that defines the basic properties of a storage device.

Questions

Taking into account the properties of a storage device:

  • What fundamental actions must be present to make a storage device useful?
  • If designing a class, sketch out what the class declaration might look like.

Implementation: declaring a derived class

Derived classes are still classes, so everything you know about declaring classes still applies. The only difference is the addition of new information- namely, the specification of the base class(es) from which we are deriving.

The format for declaring a derived class is as follows:

Format for declaring a derived class:
 
class derived_class: public base_class
{
    public:
        derived_class_constructor();
        derived_class_function1();
 
    protected:
        derived_class_functionM();
        derived_class_variable1;
 
    private:
        derived_class_variable2;
};

For the most part, only 2 changes are present.

First, and foremost, the only real distinction between a regular (base) class and a derived class is that “: public base_class_name” part we add at the very top. This indicates we are deriving from that specified class- or using it as a base from which to expand upon.

For another example of class inheritance, using a relationship between Mars and planets (after all, Mars IS A planet), we could do the following:

class mars : public planet
{
    ...
};

Where “planet” is the base class, defining some fundamental characteristic available to all planets, and “Mars” is the derivation of planet: it inherits all the fundamental properties of a planet, and adds its own to make it unique.

Based on the above, we just repeat for any additional instances of derived classes we need… for example, Neptune IS A planet as well:

Expanding upon base classes is easy, first was mars, now how about neptune:
 
class neptune : public planet
{
    ...
};

Creating new class derivatives is easy, plus we don't have to maintain multiple copies of code- it can be in a central location that we can reference when needed.

The other change you may have noticed is the presence of the “protected” keyword. As described in the “Access Control” section in this task, the “protected” keyword allows us to share information among the class hierarchy. It is accessible within the hierarchy (just as private data is available from WITHIN the class), but not accessible outside of the hierarchy or class. If you've got member variables that are used in derived classes, you should designate them as protected.

If you've got data that should ONLY be handled within a particular class, designate it as private. This change must also be present in the base class, so that the derived classes can access the data.

Note that “protected” does not replace “private”, it is merely another functionality of the C++ access control mechanisms. You can use any combination of “public”, “protected”, or “private” in your classes- whatever best fits the problem at hand.

Class Hierarchy

Perhaps one of the most important relationships formed with Inheritance is the class hierarchy. This is referring to the base class / derived class structure formed when implementing a solution using Inheritance. With the base class above the derived class(es), we can create a diagram that illustrates the connection and therefore the flow of characteristics / traits in the class family.

For an example of a diagram representing a Class Hierarchy, we need to look no further than our Mars and Neptune class derivation example. Note how the base or parent class is at the top of the chain, with derived classes beneath.

Access Control

One of the holy grails of Object-Oriented Programming (OOP) is Data Hiding, where the class shields access to certain pieces of information for operational or security purposes.

Access control is the method of controlling access (accessing / modifying) to information through some sort of standardized interface.

On UNIX systems, files have modes of access control defining what each echelon of the user hierarchy can do to the file (in terms of read, write, execute).

In C++, there are also facilities available for controlling access to your data. These “access levels” are special keywords that can be prepended to a declaration (or at the start of an entire section), and consist of the following:

  • public: accessible anywhere, both inside and outside the class, and class hierarchy.
  • protected: data or function is accessible from inside the class, as well as inside derived classes.
  • private: directly accessible ONLY within the declared class.

Of course, other facilities exist which further allow you to manipulate access to your data- friends, as discussed last week, allow for interclass data accessibility.

We make use of member functions known as “accessor methods” (typically any of the get or set member functions) to allow you indirect access to private class data.

IS-A relationships

Syntactically, we've covered the hows, whos, whats, and wheres of Inheritance. However, that leaves two more important questions: Why and When to use Inheritance?

One of the standpoints I've been taking is to make sure we use the right tool for the right job. Some tasks may or may not lend themselves well to an Object-Oriented solution.

For this reason, you should always check for the need to Inherit in your particular problem before you go about implementing it. The need for Inheritance indicates the presence of the “IS-A” relationship. Important in database design, among other places, the IS-A relationship illustrates the relationship of the derived class to the base class.

For example: A checking account IS A bank account. A square IS A rectangle.

It is important to figure out this relationship so you can more effectively design your solution and take better advantage of the Object-Oriented concepts.

Examples

The following are some examples where Inheritance can be used:

  • A tree IS-A plant.
  • A triangle IS-A polygon.
  • A car IS-A vehicle.
  • A sedan IS-A type of car.
  • A checking account IS-A bank account.
  • Mars IS-A planet.

In each case, the left-hand item is derived from the right-hand item, just like derived classes from base classes

":" vs. "::"

With inheritance, we find ourselves using the “:” operator, along with the “::” scoping operator. Just like “=” vs. “==”, it is important not to mix them up.

  • :” is used to start a list of base/superclasses from which you are deriving.
  • ::” is the C++ scoping operator. It is used to associate a class with its member functions.

Multiple Inheritance

In all the examples so far we've been dealing with a single base class where one or more derived classes are inherited from. This helps to get the general idea of Inheritance across, but is not necessarily adequate for describing all relationships.

Deriving from more than one base is referred to as “Multiple Inheritance”, and is accomplished as simply as comma-delimiting the base classes you are deriving.

Program

Let's expand upon our storage device example as a demonstration of using Inheritance in Object-Oriented Programming.

Storage devices have played an important role in computing over the years. Allowing us to save our work and load it back up later, we can manage data that doesn't have to be in the scarce few registers of the CPU at all times.

One of the earliest forms of storage that is still in widespread use today is that of the magnetic tape. Tape drives provide us the ability to read and write data in a sequential or linear fashion.

Think of a tape as one really long connected line:

<html><center></html>

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 N
'h' 'e' 'l' 'l' 'o' ',' ' ' 'w' 'o' 'r' 'l' 'd' '!' '\0'

<html></center></html>

It starts at an index of 0 and goes until the end (defined by its capacity). There exists a tape head such that we have the ability to position it over any of the storage cells of the tape and either write some value or read some value.

Note that tape is not cyclical- when we get to the very end, it doesn't just “roll-over” to the beginning. Instead, we must reposition the tape head manually to the beginning, using a process commonly referred to as “rewinding”.

Also, due to the nature of the tape, we cannot just “jump” from some position to some other random position. Instead, we must traverse all the cells from point A to point B. For this reason, tape storage is referred to as “sequentially accessed”, as opposed to “randomly accessed” as we'd have with disks or RAM.

Because we're using the tape as a computerized storage device, we must assign various actions to it that we can call upon to operate the device:

action description
read reads a numeric value stored beneath the tape head. Increment head by 1 after a successful read
write stores a numeric value to the position beneath the tape head. Increment head by 1 cell afterwards
forward re-position the tape head one cell ahead of its current position
backward re-position the tape head one cell behind its current position
rewind move tape head back to the absolute beginning of the tape (index 0)

Tapes typically have labels, so we can “name” them to properly distinguish one tape from another. The actions of “setting” and “getting” the label come to mind for this.

In the inheritance/ subdirectory of the CPROG Public Directory, you will find the following files:

file description
storage.h class declaration for storage class
storage.cc class definition of storage class
tape.h class declaration for tape class (derived from storage class)
disk.h class declaration for disk class (derived from storage class)
disk.cc class definition of disk class
main.cc sample implementation using derived disk class

There is also a sample Makefile for your compiling pleasure.

To copy this into your ~/src/cprog directory, type the following:

lab46:~$ cd ~/src/prog
lab46:~/src/cprog$ cp -a /var/public/cprog/inheritance task7
lab46:~/src/cprog$ 

Compile and run the sample program, observe the results.

Questions

Specifically, I'd like for you to identify the following:

  • How is disk.cc using load(), store(), and pos()?
  • These functions aren't defined in the disk class. Where are they defined?
  • Why are they working in a class they aren't directly defined in?
  • What is the relationship between these two classes?

Implementation

Using the class declaration given in tape.h:

  • Complete the implementation of the tape class, as tape.cc
  • Make sure all member functions work as specified
  • Be sure to make appropriate use of the functions you are inheriting
  • Don't recreate the functionality when it already exists within the class hierarchy
  • When rewinding or fast-forwarding your tape, display some animation or whirlygig to simulate the “authenticity” of tape

When you finish with the tape class, we'll need to test it:

  • Enhance or re-implement main() to test the tape class
  • Tape head operation. Demonstrate the successful loading and storing of data
  • Tape head movement. Move the tape head around and show that data is in the correct place
  • Store the following values sequentially (one after the other): 2, 4, 6, 8, 10, 12
  • Demonstrate the successful storage of the above sequence by successfully loading it off the tape

Review of Compiling/Executing

Just to review the compilation/execution process for working with your source code, if we had a file, hello.cc, that we wished to compile to a binary called hello, we'd first want to compile the code, as follows:

lab46:~/src/cprog$ g++ -o hello hello.cc
lab46:~/src/cprog$ 

Assuming there are no syntax errors or warnings, and everything compiled correctly, you should just get your prompt back. In the event of problems, the compiler will be sure to tell you about them.

Conceptually, the arrangement is as follows:

g++ -o BINARY_FILE SOURCE_FILE

The BINARY_FILE comes immediately after the -o, and the SOURCE_FILE, must never immediately follow a -o. It can precede, and such is perfectly valid (especially if you feel that way more intuitive).

To execute your binary, we need to specify a path to it, so we use ./, which basically says “in the current directory”:

lab46:~/src/cprog$ ./hello
Hello, World!
lab46:~/src/cprog$ 

Optimization

If you find yourself experiencing “anomalous” behavior in your resulting program that just cannot be explained away (ie no discernable logic errors). You can try enabling some compiler optimizations.

Compiler optimizations invoke additional functionality present in the compiler that can do some alterations of your compiled code, reordering things for more efficiency, and even correcting aberrant behavior (but also having the potential to break otherwise “working” behavior).

To use compiler optimizations, we add the “-O#” option (capital letter Oh) to the compiler command-line. There are a number of compiler optimizations available to us (this was all gleaned from the gcc(1) manual page):

option description
-O0 no optimization (this is the default, it happens if you don't specify anything)
-O reduce code size and execution time, plus some non-expensive optimizations
-O1 same as -O
-O2 optimize more. Compile time increases for the result of better code and execution
-O3 yet more optimizations. Long compile time, perhaps more efficient code
-Os optimize for size. Uses a lot of -O2 optimizations so long as it does not impact code size

So, if you'd like to compile your code with level 1 optimizations:

g++ -O1 -o BINARY_FILE SOURCE_FILE

As your programs get bigger and more complex, the utilization of compiler optimizations can make a significant impact on the resulting performance of your program. For most of the stuff we're doing now, you're not likely to notice many improvements.

Copying files to your submit directory

As you write your code, hopefully you've developed the good habit of storing all your programs in your ~/src/cprog directory (and have added/committed them to your repository).

But, in order to complete your tasks, you've been requested to place it in your ~/src/submit directory instead.

What to do?!

We'll simply make a copy of your code! Assuming we're working with a source file called myprog.cc in our ~/src/cprog directory, we'll copy it into ~/src/submit/ and give it a name of: taskX.cc

To do that we use the cp command, and run it as follows:

lab46:~/src/cprog$ cp myprog.cc ~/src/submit/taskX.cc
lab46:~/src/cprog$ 

We can then hop over to our submit directory and add/commit it:

lab46:~/src/cprog$ cd ~/src/submit
lab46:~/src/submit$ ls
contact.info    taskU.c    taskV.c    taskW.c    taskX.cc
lab46:~/src/submit$ svn add taskX.cc
Added   taskX.cc
lab46:~/src/submit$ svn commit -m "added taskX.cc to the submit directory"
...

Submission

All questions in this assignment require an action or response. Please organize your responses into an easily readable format and submit the final results to your instructor per the appropriate methods.

Your assignment is expected to be performed and submitted in a clear and organized fashion- messy or unorganized assignments may have points deducted. Be sure to adhere to the submission policy.

When complete, questions requiring a response can be electronically submit using the following form:

<html><center><a href=“http://lab46.corning-cc.edu/haas/content/cprog/submit.php?task7”>http://lab46.corning-cc.edu/haas/content/cprog/submit.php?task7</a></center></html>

Additionally, the successful results of the following actions will be considered for evaluation:

  • placement of the code you created to solve this task in your ~/src/submit directory
  • create a directory task7/ in your ~/src/submit directory
  • place your multi-file implementation of task7.c in this task7/ directory
  • addition/commit of the task7/ directory and all its contents to your repository

As always, the class mailing list and class IRC channel are available for assistance, but not answers.

haas/spring2011/cprog/tasks/task7.txt · Last modified: 2011/03/17 19:27 by 127.0.0.1