
Dynamic Binding
In the example above, it really is no problem to create static objects of the classes. When we call Print
on each object, the corresponding version of Print
will be called. It becomes a bit more complicated when we introduce a pointer to a Person
object and let it point at an object of one of the subclasses. As Print
in Person
is virtual, dynamic-binding comes into force. This means that the version of Print
in the object the pointer actually points at during the execution will be called. Had it not been virtual, Print
in Person
would always have been called. To access a member of an object given a pointer to the object, we could use the dot notation together with the dereferring operator. However, the situation is so common that an arrow notation equivalent to those operations has been introduced. The following two lines are by definition interchangeable:
pPerson->Print(); (*pPerson).Print();
Main.cpp
#include <iostream> using namespace std; #include "Person.h" #include "Student.h" #include "Employee.h" void main() { Person person("John Smith"); person.Print(); cout << endl; Student student("Mark Jones", "MIT"); student.Print(); cout << endl; Employee employee("Adam Brown", "Microsoft"); employee.Print(); cout << endl; Person* pPerson = &person; pPerson->Print(); // Calls Print in Person. cout << endl; pPerson = &student; pPerson->Print(); // Calls Print in Student. cout << endl; pPerson = &employee; pPerson->Print(); // Calls Print in Employee. }
Had we omitted the word virtual
in the class Person
above, we would not have dynamic-binding, but rather static-binding. In that case, Print
in Person
would always be called. As mentioned above, static-binding is present for performance reasons only and I suggest that you always mark every method of the baseclass in the class hierarchy as virtual.
Let us take the next logical step and continue with abstract baseclasses and pure virtual methods. An abstract baseclass cannot be instantiated into an object, but can be used as a baseclass in a class hierarchy. In the example above, we became acquainted with virtual methods. In this section, we look into pure virtual methods.
A pure virtual method does not have a definition, just a prototype. A class becomes abstract if it has at least one pure virtual method, which implies that a class cannot be abstract without a pure virtual method. A subclass to an abstract class can choose between defining all pure virtual methods of all its baseclasses, or become abstract itself. In this manner, it is guaranteed that a concrete (not abstract) subclass always has definitions of all its methods.
The next example is a slightly different version of the hierarchy of the previous section. This time, Person
is an abstract baseclass because it has the pure virtual method Print
. Its prototype is virtual and succeeded with = 0
.
The field m_stName
is now protected
, which means that it is accessible by methods in subclasses, but not by methods of other classes or by freestanding functions. Another difference in these classes is the use of constant references in the constructor. Instead of sending the object itself as an actual parameter, which might be time and memory consuming, we can send a reference to the object. To make sure that the fields of the object are not changed by the method, we mark the reference as constant. Compare the constructor of Person
in this case with the previous case. The change does not really affect the program, it is just a way to make the program execute faster and use less memory.
Person.h
class Person { public: Person(const string& stName); virtual void Print() const = 0; protected: string m_stName; };
Person.cpp
#include <string> using namespace std; #include "Person.h" Person::Person(const string& stName) :m_stName(stName) { // Empty. }
As Person
is an abstract class, Student
must define Print
in order not to become abstract itself.
Student.h
class Student : public Person { public: Student(const string& stName, const string& stUniversity); void Print() const; private: string m_stUniversity; };
In the Student
class of the previous example, Print
called Print
in Person
. This time, Person
does not have a definition of Print
, so there is no method to call. Instead, we access the Person
field m_stName
directly; this is allowed because in this version it is protected in Person
.
Student.cpp
#include <string> #include <iostream> using namespace std; #include "Person.h" #include "Student.h" Student::Student(const string& stName, const string& stUniversity) :Person(stName), m_stUniversity(stUniversity) { // Empty. } void Student::Print() const { cout << "Name: " << m_stName << endl; cout << "University: " << m_stUniversity << endl; }
Employee
works in the same way as Student
.
Employee.h
class Employee : public Person { public: Employee(const string& stName, const string& stEmployer); void Print() const; private: string m_stEmployer; };
Employee.cpp
#include <string> #include <iostream> using namespace std; #include "Person.h" #include "Employee.h" Employee::Employee(const string& stName, const string& stEmployer) :Person(stName), m_stEmployer(stEmployer) { // Empty. } void Employee::Print() const { cout << "Name: " << m_stName << endl; cout << "Company: " << m_stEmployer << endl; }
In the main
function, we cannot create an object of the Person
class as it is an abstract class. Neither can we let the pointer pPerson
point at such an object. However, we can let it point at an object of the class Student
or Employee
. The condition for Student
and Employee
being concrete classes was that they defined every pure virtual method, so we can be sure that there always exists a definition of Print
to call.
Main.cpp
#include <string> #include <iostream> using namespace std; #include "Person.h" #include "Student.h" #include "Employee.h" void main() { // Does not work as Person is an abstract class. // Person person("John Smith"); // person.Print(); // cout << endl; Student student("Mark Jones", "Berkeley"); student.Print(); cout << endl; Employee employee("Adam Brown", "Microsoft"); employee.Print(); cout << endl; // In this version, there is no object person to point at. // Person* pPerson = &person; // pPerson->Print(); // cout << endl; Person* pPerson = &student; pPerson->Print(); // Calls Print in Student. cout << endl; pPerson = &employee; pPerson->Print();// Calls Print in Employee. }