Choose a chacter: (Dog) Class Hierarchies, (Hat) Example Class Hierarchy, (Iron) Assignment Operators, (Racing car) Overriding and Polymorphism, (Ship) Static and Dynamic binding in OOP Languages or (Shoe) Client Classes
The OOP class mechanism has many similarities with Abstract Data Types (ADTs) as used in some imperative programming languages. The distinction between an ADT and a class is that the latter can incorporate inheritance. Inheritance allows classes to "inherit" class members from other classes. The advantage is that related classes can be declared more succinctly, thus avoiding cluttering of programs through re-use of code. The use of inheritance also serves to ease understranding of a progarm (especially when programs become large).
Some definitions:
Derived classes are specified as follows:
'class' NAME ':' <CLASS-ACCESS> SUPER-CLASS-NAME { ... }
The access can be private, public or protected. Consequently the following inheritance access restrictions may result:
Super-Class Member | Sub-Class Access | Sub-Class Member | |
---|---|---|---|
private | private | inaccessible | |
protected | private | private | |
public | private | private | |
private | public | inaccessible | |
protected | public | protected | |
public | public | public | |
private | protected | inaccessible | |
protected | protected | protected | |
public | protected | protected |
Thus, given a class X with a class-access of private, the protected and public members of its supper class become private members of the class X. Alternatively, if the class access was public the status of the protected and public members of the supper class would remain unchanged, and if the class access was protected the protected and public members of the supper class would become protected members of the class X. Note that whatever the case a sub-class cannot access a private member of a super-class.
NAME | DATA | FUNCTIONS |
---|---|---|
CIRCLE | Radius | Calculation of Area |
CYLINDER | Radius | Calculation of Volume |
SPHERE | Radius & Height | Calculation of Volume |
Note that the calculation of the volume of a cylinder/sphere may involve the equation to calculate the area of a circle of the same radius. Consequently the above can be arranged into a class hierarchy as follows:
The class Circle is the base class which has one data member (Radius), a constructor and a member function (CalcArea). The classes Cylinder and Sphere are then derived classes from this base class such that they inherit the member of the base class. The above can b e encoded in C++ as follows:
class Circle { protected: float Radius; public: Circle(float ra = 0.0); float CalcArea(void); }; class Cylinder : public Circle { protected: float Height; public: Cylinder(float ra = 0.0, float ht = 0.0); float CalcVolume(void); }; class Sphere : public Circle { public: Sphere(float ra = 0.0); float CalcVolume(void); };
There are six functions here (class methods), if we include a constructor for each class of object. These can be encoded as follows:
#include <math.h> #include <iostream.h> #include "example8.h" #define PI 3.1428571; Circle::Circle(float ra) { Radius = ra; } float Circle::CalcArea(void) { return(PI*Radius*Radius); } Cylinder::Cylinder(float ra, float ht) { Radius = ra; Height = ht; } float Cylinder::CalcVolume() { return(Height*Circle::CalcArea()); } Sphere::Sphere(float ra) { Radius = ra; } float Sphere::CalcVolume() { return(Radius*(4/3)*Circle::CalcArea()); }
The classes and associated member function can now be incorporated into some application code. For example:
main() { Circle x = Circle(1.0); Cylinder y = Cylinder(2.0, 3.0); Sphere z = Sphere(4.0); cout << "Area circle x = " << x.CalcArea() << endl; cout << "Area cylinder y = " << y.CalcArea() << endl; cout << "Volume cylinder y = " << y.CalcVolume() << endl; cout << "Area sphere z = " << z.CalcArea() << endl; cout << "Volume sphere z = " << z.CalcVolume() << endl; }
In C++ the compiler builds a default memberwise assignment operator for each class by overloading the assignment operator. However, programmers are free to write their own explicit assignment operators. To define an operator (any operator) the keyword operator must be included in the declaration thus:
void operator=(className &);
What the above statement essentially states is that we are overloading the = operator with a user defined version.
void Circle::operator=(const Circle &oldCircle) { Radius = oldCircle.Radius; }
Note the use of a reference argument (an address). oldCircle is defined as a reference of the type Circle, i.e. as a reference to the Circle class. The use of the const declaration prevents any inadvertent alteration to the right hand side of the assignment operator. We would include the above function in our class definition as follows:
class Circle { protected: float Radius; public: Circle(float ra = 0.0); float CalcArea(void); void operator=(const Circle &); };Remember that initialisation and assignment are two entirely different operations:
Although all sub-classes inherit class member functions from their super-classes they can also override such functions. For example, in the above code, we could add another CalcArea in the definition for the cylinder class to override that given in the Circle base class. Overriding may be desirable where:
In imperative languages, the declaration of a procedure specifies the type of each formal parameter. For a polymorphic procedure, on the other hand, the type of a parameter is not fixed. As a result the formal parameters can correspond to actual parameters of different types and hence the same function name can be used in conjunction with both base and derived classes. Thus overriding a base member function using an overloaded derived member function is an example of polymorphism (the term poly morphic literally means "many structured").
Consider the following:
#include <math.h> #include <iostream.h> class One { protected: float a; public: One(float val = 2); float F1(float); float F2(float); }; class Two : public One { public: float F1(float); }; One::One(float val) { a = val; } float One::F1(float num) { return(num/2); } float One::F2(float num) { return(pow(F1(num),2)); } float Two::F1(float num) { return(num/3); } main() { int n; One obj1 = One(); Two obj2 = Two(); cin << n; cout << "obj1 applied to F1 = " << obj1.F1(n) << endl; cout << "obj2 applied to F1= " << obj2.F1(n) << endl; cout << "obj1 applied to F2 = " << obj1.F2(n) << endl; cout << "obj2 applied to F2= " << obj2.F2(n6) << endl; }
From the above the output
produced
by function F2will be the same regradless of which object calls the function.
This is because the derived class does not have an override for the base class F2 function.
Once invoked, the base class F2 function will always call the base class version of F1 rather than the derived class override version. The reason for this is because of a process known as function binding.
In normal function calls (in C++) static binding is used.
Here the determination of which function should be called is made at compile time.
Thus, in the example, when the compiler first encounters the F1 function in the base class it makes the
determination that whenever F2 is called it will subsequently call the base class function F1.
To rectify the above situation we must use dynamic binding. Here the determination of which function should be called is not made until run-time. In C++ this is achieved using virtual functions. A virtual function tells the compiler to create a pointer to a function, but not fill in the value of the pointer until the function is actually called. At run time the pointer is then instantiated with the appropriate function address according to the object making the call.
#include <math.h> #include <iostream.h> class One { protected: float a; public: One(float val = 2); virtual float F1(float); float F2(float); }; class Two : public One { public: virtual float F1(float); }; One::One(float val) { a = val; } float One::F1(float num) { return(num/2); } float One::F2(float num) { return(pow(F1(num),2)); } float Two::F1(float num) { return(num/3); } main() { int n; One obj1 = One(); Two obj2 = Two(); cin << n; cout << "obj1 applied to F1 = " << obj1.F1(n) << endl; cout << "obj2 applied to F1= " << obj2.F1(n) << endl; cout << "obj1 applied to F2 = " << obj1.F2(n) << endl; cout << "obj2 applied to F2= " << obj2.F2(n6) << endl; }
Although dynamic binding gives greater flexibility (e.g. it allows more extensive overriding of functions) there is an execution overhead associated with it. All OOP languages support dynamic binding, but because of the execution time costs they do not all enforce it. From the above we have seen that C++ uses a hybrid approach: static binding is used by default, dynamic binding is only applied when directed by the programmer. (Note C++ uses static type checking.) Smalltalk (the original stereotype OOP language) uses a purely dynamic approach without static type checking.
We have seen that a class can access members of some other class through the inheritance mechanism. An alternative approach is to declare a "variable" (i.e. an entity) of a given class as a data member of another class. For example given a class circle and another class sphere we can declare a variable of the class (the type) circle as a data member of the class sphere. Consequently the class sphere is said to be a client class of the class circle. Note that on declaration the variable will not be attached to any specific instance of the class circle, i.e. the variable is void. To associate the variable with an object we must use a creation instruction. Once a client has attached (assigned) the variable to an object, the client may access the public class features associate with the created object.
Not all oop languages support the client class concept, C++ does not. However, the class concept is a significant feature of the oop language Eiffel. In languages where then client concept is supported the programmer is usually free to define features of a class which are not accessible to any client classes (secret class features), or which are only accessible to a limitted number of client classes.
Return to oop home page
.Created and maintained by Frans Coenen. Last updated 03 July 2001