Unit 1 Inheritance, Virtual Functions, and
Polymorphism – Revisited
Structure
1.1 Introduction
1.2 Derivation and Inheritance
1.2.1 Derivation
1.2.2 Inheritance
Self Assessment Questions
1.3 Multiple Inheritance
1.4 Virtual Functions
Self Assessment Questions
1.5 Polymorphism
1.6 Summary
1.7 Terminal Questions
References and Further Reading
1.1 Introduction
This
unit revisits some of the C++ concepts of Inheritance, Virtual
Functions, and Polymorphism. Readers are suggested not to consider this
to be An extra effort is demanded of the readers to build upon what is
presented in this unit.
Objectives:
At the end of this unit you should be able to:
· Recollect the Inheritance concept of C++
· Recollect the Virtual Functions and Polymorphism concepts of C++
· Build upon the concepts presented in this chapter to learn more and incorporate the concepts into your programs.
1.2 Derivation and Inheritance
1.2.1 Derivation
Inheritance
is implemented in C++ through the mechanism of derivation. Derivation
allows you to derive a class, called a derived class, from another
class, called a base class.
Derived class syntax:
In
the declaration of a derived class, you list the base classes of the
derived class. The derived class inherits its members from these base
classes.
The
qualified_class_specifier must be a class that has been previously declared in a class declaration.
An
access specifier is one of
public,
private, or
protected.
The
virtual keyword can be used to declare virtual base classes.
The following example shows the declaration of the derived class
D and the base classes
V,
B1, and
B2. The class
B1 is both a base class and a derived class because it is derived from
class
V and is a base class for
D:
class V { /* ... */ };
class B1 : virtual public V { /* ... */ };
class B2 { /* ... */ };
class D : public B1, private B2 { /* ... */ };
Classes that are declared but not defined are not allowed in base lists.
For example:
class X;
// error
class Y: public X { };
The compiler will not allow the declaration of class
Y because
X has not been defined.
When
you derive a class, the derived class inherits class members of the
base class. You can refer to inherited members (base class members) as
if they were members of the derived class. For example:
class Base {
public:
int a,b;
};
class Derived : public Base {
public:
int c;
};
int main() {
Derived d;
d.a = 1; // Base::a
d.b = 2; // Base::b
d.c = 3; // Derived::c
}
The
derived class can also add new class members and redefine existing base
class members. In the above example, the two inherited members,
a and
b, of the derived class
d, in addition to the derived class member
c,
are assigned values. If you redefine base class members in the derived
class, you can still refer to the base class members by using the
:: (scope resolution) operator. For example:
#include <iostream>
using namespace std;
class Base {
public:
char* name;
void display() {
cout << name << endl;
}
};
class Derived: public Base {
public:
char* name;
void display() {
cout << name << ", " << Base::name << endl;
}
};
int main() {
Derived d;
d.name = "Derived Class";
d.Base::name = "Base Class";
// call Derived::display()
d.display();
// call Base::display()
d.Base::display();
}
Output:
Derived Class, Base Class
Base Class
You
can manipulate a derived class object as if it were a base class
object. You can use a pointer or a reference to a derived class object
in place of a pointer or reference to its base class. For example, you
can pass a pointer or reference to a derived class object
D to a function expecting a pointer or reference to the base class of
D.
You do not need to use an explicit cast to achieve this; a standard
conversion is performed. You can implicitly convert a pointer to a
derived class to point to an accessible unambiguous base class. You can
also implicitly convert a reference to a derived class to a reference to
a base class.
The following example demonstrates a standard conversion from a pointer to a derived class to a pointer to a base class:
#include <iostream>
using namespace std;
class Base {
public:
char* name;
void display() {
cout << name << endl;
}
};
class Derived: public Base {
public:
char* name;
void display() {
cout << name << ", " << Base::name << endl;
}
};
int main() {
Derived d;
d.name = "Derived Class";
d.Base::name = "Base Class";
Derived* dptr = &d;
// standard conversion from Derived* to Base*
Base* bptr = dptr;
// call Base::display()
bptr->display();
}
Output:
Base Class
The statement
Base* bptr = dptr converts a pointer of type
Derived to a pointer of type
Base.
The
reverse case is not allowed. You cannot implicitly convert a pointer or
a reference to a base class object to a pointer or reference to a
derived class. For example, the compiler will not allow the following
code if the classes
Base and
Class are defined as in the above
example:
int main() {
Base b;
b.name = "Base class";
Derived* dptr = &b;
}
The compiler will not allow the statement
Derived* dptr = &b because the statement is trying to implicitly convert a pointer of type
Base to a pointer of type
Derived.
If
a member of a derived class and a member of a base class have the same
name, the base class member is hidden in the derived class. If a member
of a derived class has the same name as a base class, the base class
name is hidden in the derived class.
1.2.2 Inheritance
Inheritance
is a mechanism of reusing and extending existing classes without
modifying them, thus producing hierarchical relationships between them.
Inheritance is almost like embedding an object into a class. Suppose that you declare an object
x of class
A in the class definition of
B. As a result, class
B will have access to all the public data members and member functions of class
A. However, in class
B, you have to access the data members and member functions of class
A through object
x. The following example demonstrates this:
#include <iostream>
using namespace std;
class A {
int data;
public:
void f(int arg) { data = arg; }
int g() { return data; }
};
class B {
public:
A x;
};
int main() {
B obj;
obj.x.f(20);
cout << obj.x.g() << endl;
// cout << obj.g() << endl;
}
In the main function, object
obj accesses function
A::f() through its data member
B::x with the statement
obj.x.f(20). Object
obj accesses
A::g() in a similar manner with the statement
obj.x.g(). The compiler would not allow the statement
obj.g() because
g() is a member function of class
A, not
class
B.
The inheritance mechanism lets you use a statement like
obj.g() in the above example. In order for that statement to be legal,
g() must be a member function of class
B.
Inheritance
lets you include the names and definitions of another class's members
as part of a new class. The class whose members you want to include in
your new class is called a base class. Your new class is derived from
the base class. The new class contains a subobject of the type of the
base class. The following example is the same as the previous example
except it uses the inheritance mechanism to give class
B access to the members of class
A:
#include <iostream>
using namespace std;
class A {
int data;
public:
void f(int arg) { data = arg; }
int g() { return data; }
};
class B : public A { };
int main() {
B obj;
obj.f(20);
cout << obj.g() << endl;
}
Class
A is a base class of class
B. The names and definitions of the members of class
A are included in the definition of class
B; class
B inherits the members of class
A. Class
B is derived from class
A. Class
B contains a subobject of type
A.
You
can also add new data members and member functions to the derived
class. You can modify the implementation of existing member functions or
data by overriding base class member functions or data in the newly
derived class.
You may derive classes from other derived
classes, thereby creating another level of inheritance. The following
example demonstrates this:
struct A { };
struct B : A { };
struct C : B { };
Class
B is a derived class of
A, but is also a base class of
C. The number of levels of inheritance is only limited by resources.
Multiple
inheritance allows you to create a derived class that inherits
properties from more than one base class. Because a derived class
inherits members from all its base classes, ambiguities can result. For
example, if two base classes have a member with the same name, the
derived class cannot implicitly differentiate between the two members.
Note that, when you are using multiple inheritance, the access to names
of base classes may be ambiguous. See Multiple inheritance section for
more detailed information.
A direct base class is a base class that appears directly as a base specifier in the declaration of its derived class.
An
indirect base class is a base class that does not appear directly in
the declaration of the derived class but is available to the derived
class through one of its base classes. For a given class, all base
classes that are not direct base classes are indirect base classes. The
following example demonstrates direct and indirect base classes:
class A {
public:
int x;
};
class B : public A {
public:
int y;
};
class C : public B { };
Class
B is a direct base class of
C. Class
A is a direct base class of
B. Class
A is an indirect base class of
C. (Class
C has
x and
y as its data members.)
Polymorphic
functions are functions that can be applied to objects of more than one
type.
In C++, polymorphic functions are implemented in two ways:
· Overloaded functions are statically bound at compile time.
·
C++ provides virtual functions. A virtual function is a function that
can be called for a number of different user-defined types that are
related through derivation. Virtual functions are bound dynamically at
run time. They are described in more detail a little further in the
chapter.
Self Assessment Questions
1. Explain the derived class syntax.
2. What do you mean by direct and indirect base classes?
1.3 Multiple Inheritance
You
can derive a class from any number of base classes. Deriving a class
from more than one direct base class is called multiple inheritance.
In the following example, classes
A,
B, and
C are direct base classes for the derived class
X:
class A { /* ... */ };
class B { /* ... */ };
class C { /* ... */ };
class X : public A, private B, public C { /* ... */ };
The
following inheritance graph describes the inheritance relationships of
the above example. An arrow points to the direct base class of the class
at the tail of the arrow:
The
order of derivation is relevant only to determine the order of default
initialization by constructors and cleanup by destructors.
A direct base class cannot appear in the base list of a derived class more than once:
class B1 { /* ... */ }; // direct base class
class D : public B1, private B1 { /* ... */ }; // error
However, a derived class can inherit an indirect base class more than once, as shown in the following example:
class L { /* ... */ }; // indirect base class
class B2 : public L { /* ... */ };
class B3 : public L { /* ... */ };
class D : public B2, public B3 { /* ... */ }; // valid
In the above example, class
D inherits the indirect base class
L once through class
B2 and once through class
B3. However, this may lead to ambiguities because two subobjects of class
L exist, and both are accessible through class
D. You can avoid this ambiguity by referring to class
L using a qualified class name. For example:
B2::L
or
B3::L.
You can also avoid this ambiguity by using the base specifier
virtual to declare a base class, as described in Section 1.2.1.
1.4 Virtual Functions
By default, C++ matches a function call with the correct function definition at compile time. This is called
static
binding. You can specify that the compiler match a function call with
the correct function definition at run time; this is called
dynamic binding. You declare a function with the keyword
virtual if you want the compiler to use dynamic binding for that specific function.
The
following examples demonstrate the differences between static and
dynamic binding. The first example demonstrates static binding:
#include <iostream>
using namespace std;
struct A {
void f() { cout << "Class A" << endl; }
};
struct B: A {
void f() { cout << "Class B" << endl; }
};
void g(A& arg) {
arg.f();
}
int main() {
B x;
g(x);
}
The following is the output of the above example:
Class A
When function
g() is called, function
A::f() is called, although the argument refers to an object of type
B. At compile time, the compiler knows only that the argument of function
g() will be a reference to an object derived from
A; it cannot determine whether the argument will be a reference to an object of type
A or type
B. However, this can be determined at run time. The following example is the same as the previous example, except that
A::f() is declared with the
virtual keyword:
#include <iostream>
using namespace std;
struct A {
virtual void f() { cout << "Class A" << endl; }
};
struct B: A {
void f() { cout << "Class B" << endl; }
};
void g(A& arg) {
arg.f();
}
int main() {
B x;
g(x);
}
The following is the output of the above example:
Class B
The
virtual keyword indicates to the compiler that it should choose the appropriate definition of
f() not by the type of reference, but by the type of object that the reference refers to.
Therefore,
a virtual function is a member function you may redefine for other
derived classes, and can ensure that the compiler will call the
redefined virtual function for an object of the corresponding derived
class, even if you call that function with a pointer or reference to a
base class of the object.
A class that declares or inherits a virtual function is called a polymorphic class.
You
redefine a virtual member function, like any member function, in any
derived class. Suppose you declare a virtual function named
f in a class
A, and you derive directly or indirectly from
A a class named
B. If you declare a function named
f in class
B with the same name and same parameter list as
A::f, then
B::f is also virtual (regardless whether or not you declare
B::f with the
virtual keyword) and it overrides
A::f. However, if the parameter lists of
A::f and
B::f are different,
A::f and
B::f are considered different,
B::f does not override
A::f, and
B::f is not virtual (unless you have declared it with the
virtual keyword). Instead
B::f hides
A::f. The following example demonstrates this:
#include <iostream>
using namespace std;
struct A {
virtual void f() { cout << "Class A" << endl; }
};
struct B: A {
void f(int) { cout << "Class B" << endl; }
};
struct C: B {
void f() { cout << "Class C" << endl; }
};
int main() {
B b; C c;
A* pa1 = &b;
A* pa2 = &c;
// b.f();
pa1->f();
pa2->f();
}
The following is the output of the above example:
Class A
Class C
The function
B::f is not virtual. It hides
A::f. Thus the compiler will not allow the function call
b.f(). The function
C::f is virtual; it overrides
A::f even though
A::f is not visible in
C.
If
you declare a base class destructor as virtual, a derived class
destructor will override that base class destructor, even though
destructors are not inherited.
The return type of an overriding
virtual function may differ from the return type of the overridden
virtual function. This overriding function would then be called a
covariant virtual function. Suppose that
B::f overrides the virtual function
A::f. The return types of
A::f and
B::f may differ if all the following conditions are met:
· The function
B::f returns a reference or pointer to a class of type
T, and
A::f returns a pointer or a reference to an unambiguous direct or indirect base class of
T.
· The const or volatile qualification of the pointer or reference returned by
B::f has the same or less const or volatile qualification of the pointer or reference returned by
A::f.
· The return type of
B::f must be complete at the point of declaration of
B::f, or it can be of type
B.
The following example demonstrates this:
#include <iostream>
using namespace std;
struct A { };
class B : private A {
friend class D;
friend class F;
};
A global_A;
B global_B;
struct C {
virtual A* f() {
cout << "A* C::f()" << endl;
return &global_A;
}
};
struct D : C {
B* f() {
cout << "B* D::f()" << endl;
return &global_B;
}
};
struct E;
struct F : C {
// Error:
// E is incomplete
// E* f();
};
struct G : C {
// Error:
// A is an inaccessible base class of B
// B* f();
};
int main() {
D d;
C* cp = &d;
D* dp = &d;
A* ap = cp->f();
B* bp = dp->f();
};
The following is the output of the above example:
B* D::f()
B* D::f()
The statement
A* ap = cp->f() calls
D::f() and converts the pointer returned to type
A*. The statement
B* bp = dp->f() calls
D::f() as well but does not convert the pointer returned; the type returned is
B*. The compiler would not allow the declaration of the virtual function
F::f() because
E is not a complete class. The compiler would not allow the declaration of the virtual function
G::f() because class
A is not an accessible base class of
B (unlike friend classes
D and
F, the definition of
B does not give access to its members for class
G).
A
virtual function cannot be global or static because, by definition, a
virtual function is a member function of a base class and relies on a
specific object to determine which implementation of the function is
called. You can declare a virtual function to be a friend of another
class.
If a function is declared virtual in its base class, you can still access it directly using the scope resolution (
::)
operator. In this case, the virtual function call mechanism is
suppressed and the function implementation defined in the base class is
used. In addition, if you do not override a virtual member function in a
derived class, a call to that function uses the function implementation
defined in the base class.
A virtual function must be one of the following:
· Defined
· Declared pure
· Defined and declared pure
A base class containing one or more pure virtual member functions is called an
abstract class.
Self Assessment Questions
1. What is a covariant virtual function?
2. What is a polymorphic class?
1.5 Polymorphism
In simple terms,
polymorphism lets you treat derived class members just like their parent class's members.
More
precisely, polymorphism is the ability of objects belonging to
different types to respond to method calls of methods of the same name,
each one according to an appropriate type-specific behavior. The
programmer (and the program) does not have to know the exact type of the
object in advance, so this behavior can be implemented at run time
(this is called
late binding or
dynamic binding).
The
different objects involved only need to present a compatible interface
to the clients (the calling routines). That is, there must be public
methods with the same name and the same parameter sets in all the
objects. In principle, the object types may be unrelated, but since they
share a common interface, they are often implemented as subclasses of
the same parent class. Though it is not required, it is understood that
the different methods will also produce similar results (for example,
returning values of the same type).
In practical terms,
polymorphism means that if class B inherits from class A, it doesn’t
have to inherit everything about class A; it can do some of the things
that class A does differently. This means that the same “verb” can
result in different actions as appropriate for a specific class, so
controlling code can issue the same command to a series of objects and
get appropriately different results from each one.
Polymorphism
allows client programs to be written based only on the abstract
interfaces of the objects which will be manipulated (interface
inheritance). This means that future extension in the form of new types
of objects is easy, if the new objects conform to the original
interface. In particular, with object-oriented polymorphism, the
original client program does not even need to be recompiled (only
relinked) in order to make use of new types exhibiting new (but
interface-conformant) behavior.
(In C++, for instance, this is
possible because the interface definition for a class defines a memory
layout, the virtual function table describing where pointers to
functions can be found. Future, new classes can work with old,
precompiled code because the new classes must conform to the abstract
class interface, meaning that the layout of the new class's virtual
function table is the same as before; the old, precompiled code can
still look at the same memory offsets relative to the start of the
object's memory in order to find a pointer to the new function. It is
only that the new virtual function table points to a new implementation
of the functions in the table, thus allowing new, interface-compliant
behavior with old, precompiled code.)
Consider the following example:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Animal {
public:
Animal(const string & name) : name(name) { }
virtual ~Animal() { }
virtual const string talk() = 0;
const string name;
};
class Cat : public Animal {
public:
Cat(const string & name) : Animal(name) { }
virtual ~Cat() { }
virtual const string talk() { return "Meow!"; }
};
class Dog : public Animal {
public:
Dog(const string & name) : Animal(name) { }
virtual ~Dog() { }
virtual const string talk() { return "Arf! Arf!"; }
};
// prints the following:
//
// Missy: Meow!
// Mr. Bojangles: Meow!
// Lassie: Arf! Arf!
int main() {
Animal * animals [] = {
new Cat("Missy"),
new Cat("Mr. Bojangles"),
new Dog("Lassie")
};
for(int i = 0; i < 3; i++)
cout << animals[i]->name << ": " << animals[i]->talk() << endl;
for(int i = 0; i < 3; i++)
delete animals[i];
return 0;
}
Note that the
talk()
method is explicitly declared as
virtual
.
This is because polymorphic method calls have relatively high overhead
in C++. This overhead is lessened by treating all method calls as
non-polymorphic, unless explicitly marked as
virtual
by the developer.
1.6 Summary
This
chapter has provided brief and concise description of C++ concepts on
Inheritance, Virtual Functions and Polymorphism. The focus of this
chapter was to revisit and not describe the concepts in detail. The
reader is expected to fallback on prior knowledge or go through concrete
resource materials to gain more insights and to strengthen his/her
knowledge.
1.7 Terminal Questions
1. Explain in brief the concept of Inheritance, Virtual Functions, and Polymorphism.
2. What are polymorphic functions? How are they implemented?
3. Explain multiple inheritance.
4. What do you mean by static and dynamic binding?
5. What is an abstract class?
6. What is an interface definition?
7. What are virtual function tables?
No comments:
Post a Comment