Corning Community College
C/C++ Programming
Task 7: Inheritance
~~TOC~~
To explore the concepts of Inheritance, its importance to Object- Oriented design, and how to implement it in your programs.
In “C++ Programming: Program Design Including Data Structures” by D.S. Malik, please reference:
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.
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.
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.
Can many objects be classified / simplified in this way? Look at the following objects, and tell me what they have in common:
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.
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.
Taking into account the properties of a storage device:
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.
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.
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:
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.
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.
The following are some examples where Inheritance can be used:
In each case, the left-hand item is derived from the right-hand item, just like derived classes from base classes
With inheritance, we find ourselves using the “:” operator, along with the “::” scoping operator. Just like “=” vs. “==”, it is important not to mix them up.
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.
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.
Specifically, I'd like for you to identify the following:
Using the class declaration given in tape.h:
When you finish with the tape class, we'll need to test it:
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$
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.
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" ...
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:
As always, the class mailing list and class IRC channel are available for assistance, but not answers.