Class inheritance |
So far, we have looked at single, standalone classes, ie. classes without a parent, classes without children - such as row, vector, ComplexNumber (and the MIDI classes).
Another HUGE benefit of OOP (in addition to encapsulation) is that of INHERITANCE - formation of class 'hierarchies', ie. using an existing class to create new children classes, and then creating _their_ children, and so on. This allows for the creation of massively complex programs (eg. Facebook server, MacOS, World of Warcraft..), with thousands or even tens of thousands (!) of classes in the application.
A class hierarchy is a tree with a single root that can have multiple children - each child can have only one parent, but can itself have multiple children. Here is a quick example (from a rather complex UI system called Qt):
base==general/common, derived==specialized/detailed.
The idea is this: a 'base' class (type) is created, with common members and methods - common to several children classes.
Each child (derived, or 'sub')class **is** a base class, AND MORE: it can **add** extra (new) members and methods, or can **modify** ("override") base class methods, but **cannot** delete base functionality. That way, we REUSE base class capability in all the derived classes.
Our shape2D class looks like this:
class shape2D
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
shape2D(int wd=1, int ht=1) {
width = wd;
height = ht;
}
void prtShape() {
cout << width << "," << height << endl;
}
protected:
int width;
int height;
};// class shape2D
Here is a program that creates a couple of shape2D instances.
Next, let us DERIVE (subclass) a 'Rectangle' class, from shape2D:
class Rectangle: public shape2D
{
public:
Rectangle(int wd, int ht) {
width=wd;
height=ht;
}
int getArea()
{
return (width * height);
}
};/
Example with shapes..
Notes:
* we use class Rectangle: public shape2D to DERIVE the Rectangle subclass from the shape2D parent class
* for now, we'll derive our classes using ': public' - it implies that the base class' public methods will CONTINUE to be public for us (in the derived class)
* note that width and height have to be 'protected', in shape2D - if they are private, they will NOT be visible (usable) in derived classes!
* ': public' ensures that in derived classes, width and height continue to be 'protected'
* 'protected' means the members are visible only to self and derived classes, NOT to the outside world; in that sense, 'protected' is like "inheritance-friendly private" :)
Notes (cont'd):
* in the Rectangle class, our constructor was able to initialize width and height, which were declared in shape2D (not in Rectangle) - THAT IS WHAT INHERITANCE MEANS - we inherit the base class' public and protected members and methods. shape2D gets constructed first, and, "on top of it", Rectangle.
* in Rectangle, we ADD a getArea() method, and use the rectangle area calculation in it; inherited classes could contain extra functionality (methods, members) that might not be necessary/relevant/runnable in the base class
Here is a program that exercises Rectangle construction. Note the order of object construction..
Let us now add a Triangle shape, also derived from Rectangle:
class Triangle: public shape2D
{
public:
Triangle(int wd, int ht) {
width=wd;
height=ht;
cout << "Triangle constructed, with width,height as " << width << "," << height << endl;
}
int getArea()
{
return 0.5*(width * height);
}
};// class Triangle
This program creates a Triangle shape (and a Rectangle one).
So our inheritance tree (aka hierarchy) looks like this:
Side note: UML is a modeling language used to describe class hierarchies (and MORE). Benefits: standardized notation, AUTOMATIC code generation [from diagrams]!
We can now inherit from Rectangle to create a Square:
class Square: public Rectangle
{
public:
Square (int side):Rectangle(side,side)
{
cout << "Square constructed, with side " << side << endl;
}
};// class Square
Note the alternate syntax for constructor initialization - the ':' means we are calling our parent's constructor. The 'usual' syntax would have been:
{
width = side;
height = side;
}
But if you try the 'usual' syntax above, it wouldn't work! Why? Because a class constructor can only initialize its own members and its immediate superclass' members (which means we can't directly set shape2D's width and height members). So, Square MUST initialize Rectangle, and can ONLY do so using the ':' notation, as shown above - there is no alternative syntax. You can read more here.
Square inherits from Rectangle (including getArea()), which inherits from shape2D (including width, height, prtShape()):
Here we add a diagonal() method to Square.
Exercise: implement a Circle class as a subclass of shape2D (user will initialize a circle with an int radius). Make radius be a member of Circle. Re-implement ("override") prtShape(). Implement area() and perimeter() (both should return a double).
Think: is shape2D even necessary, as a base class?
This is an API for the driver for MATRIXVISION's mvBlueCOUGAR industrial camera: http://www.matrix-vision.com/manuals/mvBlueCOUGAR-F/cpp/index.html
The code might appear 'daunting', but it merely reflects a systematic application of ideas and syntax you are learning in this course..
Here is the API for Maya: http://download.autodesk.com/global/docs/mayasdk2012/en_us/index.html
Again, there are a LOT of classes and hierarchies - again, all this was not written overnight (!) or even by a single developer.
We can do something interesting with our hierarchy - what if we created a pointer to shape2D (which is our base class), BUT ASSIGNED IT TO A NEW SQUARE INSTEAD?! After all, a Square "isA" shape2D, so this should be possible (it is):
shape2D *sh = new Square(4);
Also, we override prtShape() in Square, ie. create a Square-specfic one:
void prtShape() {
cout << "square: " << width << endl;
}
BIG QUESTION: what happens, what do WE want to happen, with this:
sh->prtShape();
Will shape2D's prtShape() get called, or would Square's? Find out..
Even though we expected (and got) shape2D's prtShape(), WE WOULD LIKE Square's version to be called!!
WHY?
"Plugins" - in a drawing program, if Square, Circle, Triangle, Polygon etc are all plugins THAT ARE WRITTEN (YEARS!) **AFTER** the main program was released, we'd like to write a new plugin (eg. Star), install it (copy the compiled plugin file into a 'Plugins' folder), restart our program, click on our new 'Star' icon, AND HAVE Star's methods (eg. draw()) invoked!
shape2D *sh = new Star(); is how the main program can hope to make this happen!
In other words, at "runtime" (not compile time), "late binding" (not early binding) happens - a new derived class Star() object gets created, and is assigned to a base class shape2D pointer! 'Late" because this assignment does not, CANNOT (Star wasn't even BORN then!!) happen when main was being written..
Very cool idea :) But, it doesn't work :( In other words, sh->prtShape() ended up calling the shape2D version, not the Square version.
How do we fix this? There was just ONE keyword missing, in the base class: 'virtual' - in other words, we need to say:
virtual void prtShape()
{
cout << "shape2D: " << width << "," << height << endl;
}
If a base class function is marked 'virtual', it means that at runtime, a derived class' object can be assigned to a base class pointer, in which case, if the derived class overides the virtual function, THAT DERIVED VERSION WILL GET CALLED!! PURE **MAGIC**. Take a look.
So to recap: now we have neat way of getting an overriding derived class method invoked at runtime, if we assign a derived class object to a base class pointer:
shape2D *sh = new Square(4);
// Square's prtShape() gets run, if shape2D's is virtual
sh->prtShape();
New problem: WHAT IF Square() does not (re)implement prtShape()?!
In the above, since Square() did not provide prtShape(), Rectangle was searched (to see if it had a prtShape, it didn't), and finally, shape2D did have a prtShape(), that was called.. **With a virtual function, the 'search' order goes from derived to base.**
Here's why, the parent's prtShape() being called [if the child doesn't provide one] might be a problem - the parent function might be inadequate. What if we *ALWAYS* want the derived class' version, *NEVER* the parent's? If a plugin base class is 'Blur' and I implement 'RadialBlur', then my RadialBlur's 'run()' is what should be called (that is where I have implemented my cool radblur algorithm), not a generic run() from Blur. So, there needs to a way to FORCE derived classes to override base classes' virual functions! Answer: "pure".
If we make a base class function 'pure virtual' (the class becomes an 'abstract base class'), every derived class **does** need to implement that function! That is how we can ensure that at runtime, the derived class version **will** get called (because we were 'forced' to provide one).
How to make a virtual function 'pure'?
= 0; // :)
In other words, we need to use the function prototype mechanism to declare this in the parent class:
virtual void prtShape() = 0; // =0 means 'PURE' virtual!
...
virtual void shape2D::prtShape()
{
cout << "shape2D: " << width << "," << height << endl;
}
With the above in place, we're forced to provide a prtShape() for Square - uncomment Square's prtShape() to get the code to compile and run..
So make sure you understand what happens in the three cases, of the superclass method being:
Note - just for completing the rest of the possibilities with shape2D and Square:
// will work, will call shape2D's prtShape()
shape2D *sh = new shape2D();
sh->prtShape();
// will work, will call Square's prtShape()
Square *sh = new Square(4);
sh->prtShape();
// will NOT work, will result in the following message:
// error: invalid conversion from 'shape2D*' to 'Square*' [-fpermissive]
Square *sh = new shape2D();
sh->prtShape();
We covered a lot of ground..
* inheritance (subclassing) is a great form of REUSE
* derived classes inherit base classes' non-private methods and members
* derived classes can add extra members and methods, override methods
* a derived class object can be assigned to a base class pointer
* using a virtual function, a derived class object's method can be invoked for above (if implemented in the derived class)
* using a pure virtual function, a derived class object's method WILL be invoked (so, a definition does need to exist - no option to leave it out)
* if a base class' methods are ALL PURE VIRTUAL, such a class can be called an 'interface' - it provides signatures (but NOT implementations) for a collection of useful methods that subclasses are 'contractually obligated' to provide. This is a useful design principle [so useful that Java actually provides a keyword for this!].