Team 2550
Technical Documentation
|
|
Inheritance creates an is-a relationship between classes, while composition creates a has-a relationship. In other words, inheritance says, "I am extend class with these other things," while composition says "This other class is one of my components."
Composition is the term used for placing an object within a class. Say there are two classes, A and B. If you place an object of type A into the declaration of B, that is composition.
You can access values in A
from an object of B
using the dot operator.
B obj; //When the constructor for B is run, it invokes the constructor for A automatically. obj.member.setSomeValue(7); ...
As was said above, inheritance creates an "is-a" relationship between classes.
Inheritance introduces the need for a new encapsulation level: protected
. When a member is private
, it is not accessible to a class that inherits it. Members that are labeled protected
are accessible by derived classes, but not for general use. In other words, protected
members are accessible to methods declared within the derived class, but not when the class is instantiated and used.
When you are developing an application that uses inheritance, you usually start with a Universal Modeling Language (UML) diagram. A UML diagram shows the relationship between classes within a program. In other words, it shows what classes are components (composition) or others and when classes inherit others.
The UML syntax is slightly different from that of C++ due to the fact that it is designed to describe any programming language.
Here is a sample UML diagram...
+
, -
, or #
+
: public-
: private#
: protectedname: type = value
syntax. The type
and = value
parts are optional.method(arg1:type, arg2:type = value): ReturnType
The best way to show how inheritance works is to use it. For this reason, we will implement a class that contains shape properties, and then use inheritance to create classes that correspond to specific shapes. Many graphics libraries (often used to create games) have something similar to this. Note that this example will require basic trigonometry skills (applications of sine, cosine, and tangent).
Here is the declaration and implementation of the Shape
class.
width
and height
are protected because other classes may need access to them.Here is the declaration and implementation of a very simple child class, Rectangle
. The only additional methods it supplies are area()
and perimeter()
.
The encapsulation level before the name of the base class is important. It is the highest level that a member from Shape
can be in a Rectangle
object. In other words, if public Shape
was changed to private Shape
, all of the members that were previously public and protected would be private in the child class. Generally speaking, this is not what you want, and if you want to restrict the access to inherited members, composition is typically the way to go. This does not reduce the protection of any members - inheriting a class as public
means that previously public members are public, protected members are protected, and private members are private.
: Shape(w, h, loc, angle)
bypasses the default constructor. In this case, the arguments of the Rectangle
constructor are passed to the Shape
constructor. This prevents members of the new class from being set twice.
For demonstration purposes, here is a simple test file.
Inheritance also provides a benefit when you are passing a class into a function. You can pass child classes into a function meant for the base class. In other words, you can pass a Rectangle
into a function requiring a Shape
argument. This makes since, a Rectangle will have no less than a Shape (assuming that it inherited Shape as public).
Angle: 0 Layer: 0 Location: 0 0 Width: 1 Height: 1
Note that r
is a Rectangle
and the function's parameter type is a Shape
. This would cause a compiler error if you set the encapsulation level when Rectangle
inherited Shape
were something other than public
.
Unlike a rectangle (or most shapes, for that matter), an equilateral triangle has to have the same width and height. This presents a problem when inheriting from the Shape
class: the user is able to set the width and height individually. Fortunately, we can redefine member functions within the base class in the child. Let's create an equilateral triangle class, called EquilTriangle
, that makes sure the width and height maintain the correct ratio. First, we'll have to use some geometry/trigonometry to determine the ratio between the width and height of an equilateral triangle.
The above diagram gives us one very important piece of information: the ratio between the width and height of an equilateral triangle is: 0.866025403784. In other words:
height = 0.866025403784 * width width = height / 0.866025403784
Note that the above declaration redefines setHeight
and setWidth
.
There is almost nothing new here, so let's run a test program...
Angle: 0 Layer: 0 Location: 1 0 Width: 2 Height: 1.73205 Angle: 0 Layer: 0 Location: 1 0 Width: 3 Height: 2.59808 Angle: 0 Layer: 0 Location: 1 0 Width: 3 Height: 5
As you can see, the program worked. However, running Shape::setHeight
defeats the purpose of this class, to keep the width and height at a constant ratio. This is not always the case, and there are situations where accessing the original function (not the redefined version) can be useful.
It is best to put as many methods as possible in the base class. That way, functions that accept the base class are capable of doing more. For instance, the area
and perimeter
methods are in every child class of Shape
that we declared, and it is safe to say that every shape has both an area and perimeter (circumference in the case of circles, but it is the same thing). The problem we encounter is that the means by which you calculate those values differ from shape to shape. Because of this, it doesn't make since to define the methods in the base class. This is where virtual methods become useful. Basically, they make the base class incomplete. You can still call a virtual function, but unless the user (programmer) inherits the class and defines those functions, the class cannot be used. Let's take another look at the printShape
function.
There is no way to call the area
and perimeter methods
in the above function because they don't exist in the Shape
class. The problem is that the base class cannot define them, and having them return a default value, such as 0, makes no since. In addition, the Shape
class does not implement a specific shape. It is like a rectangle, but it does not define a rectangle any more than it defines any other shape. Let's modify the UML diagram we create earlier in order to deal with this shortcoming.
The declaration of Shape
will now look like this...
= 0
is critical because it makes them pure virtual functions. In other words, it makes the class abstract, meaning that you can't create a Shape
object. In order to use Shape
, you have to inherit it and define the virtual functions. Normal virtual functions behave similar to method overriding (see EquilTriangle).The implementation of Shape
does not change.
In order to demonstrate how (pure) virtual functions work, here is another child class: Ellipse
(which can also be used for circles).
Here is an example main()
using Ellipse
...
Angle: 0 Layer: 0 Location: 0 0 Width: 2 Height: 1 Area: 1.5708 Perimeter: 4.84421
This is different from the override in EquilTriangle in that you cannot access Shape::area()
. If you change the print
function so that it tries to call Shape::area
or Shape::perimeter
, you will get a compiler error.
In function `print(Shape&)': test.cc:(.text+0x77): undefined reference to `Shape::area()' collect2: error: ld returned 1 exit status Compilation exited abnormally with code 1
Although it has limited uses, multiple inheritance can sometimes be helpful. Up until now, any child class inherited a single base class, but you can actually have any number of base classes.
Let's say there is a class called Render
that can display an object on the screen. The members of a Render
object do not matter here, so I will not provide a declaration. Let's say we want to make a rectangle class that is renderable.
There is no real difference between multiple and single inheritance except that there is not a single base class. The members of both Shape
and Render
are callable in the child, and functions that accept either Shape
or Render objects can also operate on the new
Rectangle` class.
Render
is because it requires a variable number of points to be stored, which requires the use of dynamic memory - I have not covered this yet and therefore do not feel comfortable introducing unexplained syntax.