Inheritance is an object-oriented programming technique that allows
us to model relationships between objects. A nice example for this
relationship is fruit. Imagine you have a class Fruit already
defined. Perhaps it has some methods such as seedsRemaining()
and numCalories(). Here's what a header file
Fruit.hh might look like:
class Fruit
{
private:
Color *color;
int calories;
int seeds;
public:
Fruit(); // no arg constructor
Fruit(Color *c, int cal, int s);
~Fruit();
int seedsRemaining() const;
int numCalories() const;
}
What about a banana? Are there "methods" intrinsic to a banana that do
not apply to every Fruit? Sure. Maybe I want to check to see
if my banana stillHasPeel() (that would be an appealing method
to write). You can likely come up with others. In C++, we can represent the
fact that our class Banana inherits from the class
Fruit like so:
class Banana : public Fruit
{
private:
bool peelExists;
public:
Banana(); // no arg constructor
Banana(Color *c, int cal, int s, bool peel);
~Banana();
bool stillHasPeel() const;
}
Notice the single colon, followed by the keyword
public (which means "public inheritance" in this context
--- there are other types of inheritance which are used less often, such as
private and protected inheritance),
followed by the name of the class to inherit from. Note that we must
#include the declaration of Fruit via its header
file for this code to work.
What you have done here is to say that Bananas are
Fruits and can thus do anything a Fruit can do, but
can also perhaps do more (and perhaps do the same thing in a different way).
So even though you haven't specified that a Banana has a
numCalories method, it does have one because it inherits the
method from the Fruit class. Therefore, if I instantiate a
Banana, I can call any public method belonging to the class as
well as any public method belonging to its parent class:
Banana t();
if (t.stillHasPeel() == true)
{
cout << "t still has a peel!" << endl;
}
if (t.numCalories() > 200)
{
cout << "This is a big honkin' banana!" << endl;
}
Of course, Banana is just one of the many "subclasses" or
"child classes" we could "derive" from Fruit (which is known as
the "superclass" or "base class" or "parent class" of Banana).
One could imagine having Apples, Oranges, heck,
maybe a whole cornucopia of delicious seed-bearing treats. If we had enough
sub-classes of Fruit which all had peels, we might decide that
the stillHasPeel() method is intrinsic to all those, so we could
add another layer of functionality into our inheritance tree, if you
will, like so:

In this manner, large amounts of complexity in object hierarchies can be managed at any level of granularity you wish.
protected fields and methodsWhen inheritance enters the picture, some things that were simple to
understand before become more complicated. Previously, we understood that
most fields of an object should be private because they
represented details of the implementation, and most methods should be
public so that they could be used by other code (although
private methods are also useful on occasion). With inheritance,
if a private field is declared in a superclass, then instances
of its subclass will not have direct access to that field!
private really means private! Sometimes this is what you
want, but often you want fields in a class to be directly accessible to
instances of subclasses, but not directly accessible to any other code. This
kind of field with "selective access" is called a
protected field (note that you can have
protected methods too). Whether you should usually use
private or protected fields is a controversial
topic in the object-oriented programming community, but the best answer is
that it depends on the situation.
Sometimes you want to change what a method does in an instance of a subclass as compared to what it does in an instance of a class. You know that if a method is defined in a superclass and you don't define the method in the subclass, the version of the method that will be used is the version in the superclass (that's how object-oriented programming encourages code reuse). But you can also redefine the method in the subclass, as long as it has the same type of arguments and returns the same type of values. This is called overriding a method.
However, it's not quite that simple (is it ever?). One of the neat things
about object-oriented programming is that you can write functions that take
arguments which are instances of a given class, and then pass those functions
instances of a subclass of the original class and they will still work. For
example, let's say we add a new method to our class Fruit that
returns a string containing information about the fruit; we'll call it
getInfo(). We might use it like this:
void print_fruit_info(Fruit *f)
{
std::cout << f->getInfo() << endl;
}
However, the information for a Banana might be different from
that for a Fruit, so we'd want to override the
getInfo() method in the Banana class. Fine and
dandy. But C++ guarantees us that we can pass a pointer to a
Banana object to the function print_fruit_info(),
because a Banana is also a Fruit. But if we define
the getInfo() methods in the Fruit and
Banana classes as usual, we'll find, to our surprise, that
print_fruit_info() will call the Fruit version of
getInfo() even if we pass it a pointer to a Banana
object. That's not what we want. We want the system to be smart enough so
that it "does the right thing" with a Banana object, which in
this case means calling the Banana object's
getInfo() method if a pointer to a Banana object
was passed to print_fruit_info(). This "smart" behavior is
called polymorphism, and it's the only really clever thing that
object-oriented programming gives you (as well as being the hardest thing to
understand!). To make the getInfo() method polymorphic, it has
to be declared virtual in the Fruit class
(you can also declare it virtual in the Banana
class as well or not; it won't matter either way). It will look like
this:
class Fruit
{
protected: // Might as well change this too...
Color *color;
int calories;
int seeds;
public:
Fruit();
Fruit(Color *c, int cal, int s);
~Fruit();
int seedsRemaining() const;
int numCalories() const;
virtual std::string getInfo();
}
If you leave out the virtual keyword, the
print_fruit_info() will always use the Fruit
version of getInfo(), which is not what you want. Adding the
virtual keyword means that the "right" method for the object
will always be called, even if it's passed to a function that expects an
instance of a superclass of the object. Adding the virtual
keyword totally changes the meaning of the function!
There's one other complicating point (aren't there a lot of them?). Polymorphism works as long as the argument to the function is a pointer or a reference, but if it's just a plain object, like this:
void print_fruit_info(Fruit f) // Note: type of f is "Fruit" and not "Fruit *"
{
std::cout << f->getInfo() << endl;
}
then it won't work (the superclass method will always be called). That's
because the original object will get copied into a brand new
Fruit object when the function print_fruit_info() is
called. Watch out for this pitfall!
Another issue is that when you have virtual methods, your
destructor should also be virtual. This makes sense; if you
have a collection of Fruit * pointers, of which some point to
Fruits and some point to Bananas, and you want to
call delete on all of them, you want to make sure that the
correct destructor for each object is called. You can accomplish this by
making the destructor virtual. If you forget to do this (a
very common bug!) then the wrong destructor may get called on an
object, which could lead to memory leaks or a core dump.
Sometimes you are overriding a method of a subclass of a particular class, and you want to call the original class' version of the method (and then do something else in addition to that). How do you do that? In the example above, you might have this:
// In Fruit.cc:
Fruit::getInfo()
{
std::string s = "I'm a big, juicy, delicious fruit!";
return s;
}
// In Banana.cc:
Banana::getInfo()
{
// Call the superclass method:
std::string s = Fruit::getInfo();
// Now do something else in addition to that.
s += "\n"; // Add a newline.
s += "And not only that, I'm a sweet, tasty banana!";
return s;
}
In other words, if you want to call a superclass method you have to
qualify it explicitly with the name of the class. In this case, if you just
called getInfo() and not Fruit::getInfo() you'd end
up in an infinite loop (can you see why?).
Another thing you may want to do is to call a superclass constructor inside the constructor of a subclass. The way to do this is to put the superclass constructor call inside the member initialization list of the constructor. For instance, we might have:
// In Fruit.cc:
Fruit::Fruit(Color *c, int cal, int s)
: color(c), calories(cal), seeds(s) // initialize fields
{ } // nothing else to do
// In Banana.cc:
Banana::Banana(Color *c, int cal, int s, bool peel)
: peelExists(peel), // initialize fields
Fruit(c, cal, s), // call superclass constructor
{ } // nothing else to do
There are certain classes that are so generally useful that they are provided as a standard feature of C++. Since many of them use templates, the template-using classes are collected into a library called the "Standard Template Library" or STL for short. In fact, the STL also contains templated functions as well as classes, but we'll leave that discussion for the advanced C++ track. For our purposes, what you need to know is that there are a lot of collection classes in the STL. Collection classes are classes that implement various kinds of collections of objects. These include:
vector (a resizable array)list (a doubly-linked list -- don't you wish you had known
about this when you did lab 4?)map (a collection that establishes a mapping between two
types of objectSTL classes are quite easy to use. First, you #include the
appropriate header file e.g.
#include <vector> // for vectors #include <list> // for lists #include <map> // for maps
Then, when you need a particular kind of templated object, you create it as you'd expect:
vector<int> v; // Creates a vector of integers. list<std::string> ls; // Creates a linked list of strings. map<std::string, double> m; // Creates a map between strings and doubles.
Note that some templates (like map) take multiple template
parameters; this is completely legal and often useful. Also note that STL
template classes often overload operators to behave in reasonable ways. So
you can do things like this:
v[0] = 42; std::cout << "The first vector element is: " << v[0] << endl; m["Joe"] = 1001; std::cout << "Joe's ID number is: " << m["Joe"] << endl;
Both the vector and the map class overload the
square brackets (indexing) operator so that it behaves the way that you would
expect it to. So vectors can be treated like arrays (even
though internally they're very different), and although we haven't seen
anything quite like maps before, they behave like arrays that
use arbitrary objects as indexes (here, string objects).
One subtle point about STL container classes: if the object stored in the
container is a pointer, then deleteing the container will not
delete the pointer. Managing the memory corresponding to the
pointer is the user's responsibility.
There is much, much, much more to say about the STL. It's covered in much more detail in the advanced C++ track (as well as in many fine books), so if you're interested you should take that track (and look for those books).
Given the following base class Employee (in Employee.hh and Employee.cc):
#include <string>
class Employee {
protected:
double payRate;
std::string name;
public:
Employee(std::string empName, double empRate);
double pay() const;
void print() const;
};
Do the following:
Derive a new class Hourly from Employee,
where Hourly meets the following criteria:
addHours which lets the
user increase the number of hours this employee has worked so far.pay method and returns the
payRate times the number of hours worked as the amount of
pay.print method but then calls the
parent print method first. It then appends an extra line of
information displaying the total hours worked so far.Derive another new class Executive from
Employee, where Executive meets the following
criteria:
awardBonus which lets the
user set the bonus pay for this employee.pay method and:
pay method,Executive has been paid!), andprint method but then calls the
parent print method first. It then appends an extra line of
information displaying the bonus accumulated thus far.#ifndef/#define/#endif technique in
each of your header files to avoid multiple inclusion of header
files.lab7.cc that:
map<int, Employee *> to map Social Security
Numbers to Employee objects,Hourly employee, liberally gives them some
hours worked, and then adds them to the map with SSN 250124332, which
is that employee's social security number, of course,Executive employee, liberally applies a bonus to
their salary for this term, and adds them with SSN 007324232,Employee *'s from the map by
using the bracket-operator, andprint() method and then displays the return of the
pay() method for each of these objects in turn.Employee.hh class and
making a couple of very simple changes. Do that.
That's all, folks!