15.7 VIRTUAL MEMBER FUNCTIONS IN C


15.7 VIRTUAL MEMBER FUNCTIONS IN C++

In Chapter 3, we mentioned that in Java every member function defined for a class exhibits polymorphic behavior.[4] But, in C++, a member function must be declared virtual in order for it to behave polymorphically. So a C++ class can have member functions that are just that-functions-and member functions that behave polymorphically and are therefore methods.[5] When a C++ member function is declared virtual in a base class, and then when it is subsequently invoked on an object of a derived class, the system automatically chooses that definition of the function that is most applicable to the object provided the objects are manipulated through pointers or references. A virtual member function defined in a base class stays virtual in a derived class even if it is not explicitly declared virtual in the derived class.

To demonstrate the polymorphism achieved by declaring a member function virtual, we will consider a base class Employee and a derived class Manager, as in Figure 15.7. We will consider the following two situations for this two-class hierarchy:

  • An Employee base class with a print () member function, and a Manager derived class with a print () member function of its own.

  • Same as above, except that we will now declare the base-class print () member function virtual.

We will show that the print () function will not behave polymorphically in the first case, whereas it will in the second case. In the first case, when we invoke print () on a collection of Employee objects some of whom may actually be Manager objects, the print () function used will be the one for the Employee class. By contrast, when we do the same in the second case, the system will automatically figure out at run time the true identity of each Employee object and, when the object is actually a Manager, invoke the print () defined for the Manager class.


Figure 15.7

Case 1

We show an Employee base class and a Manager derived class, each with its own print () member function in lines (A) and (B), respectively:

 
class Employee { // BASE string firstName, lastName; //..... public: Employee(string fnam, string lnam) { firstName = fnam; lastName = lnam; } void print() const { //(A) cout << firstName << " " << lastName << " "; } }; class Manager : public Employee { // DERIVED short level; //. . public: Manager( string fnam, string lnam, short lvl) : Employee(fnam, lnam), level(lvl) {} void print() const { //(B) Employee::print(); cout << " works at level: " << level << end1; } }

Now consider the following declaration that constructs a vector of pointers to Employee objects:

    vector<Employee*> empList; 

and let's say we have the following Employees in an organization:

     Employee* e1 = new Employee( "john", "doe");     Employee* e2 = new Employee( "jane", "doe");     Manager* e3 = new Manager( "mister", "bigshot", 2);     Manager76* e4 = new   Manager("ms", "importante", 10); 

Note that for the last two Employees, we could also have said[6]

     Employee* e3 = new Manager( "mister", "bigshot", 2);     Employee* e4 = new Manager("ms", "importante", 3); 

We will now insert the four Employees into the vector declared earlier:

     empList.push_back(e1);     empList.push_back(e2);     empList.push_back(e3);     empList.push_back(e4); 

Note that regardless of whether we declare e3 and e4 as Manager* or Employee*, these will be stored in the vector empList as Employee* because we declared empList to be a vector whose elements are of type Employee*. Keeping in mind that all items in empList will be stored as Employee*, we now want to be able to write a print loop like

      vector<Employee*>::iterator p = empList.begin();                                          //(C)      while ( p < empList.end() )        (*p++)->print();                                                                               //(D) 

and, in line (D), we want that print () function to be invoked for each item in the vector which is most applicable to the item. So for e1 and e2, we want the function Employee::print () to be used, whereas for e3 and e4 we want the system to use Manager::print () automatically.

As things stand now, that is not what's going to happen. As the system goes through each item in the vector, it will apply the Employee::print () to each item. As a result, the output on the terminal will look like

     john doe     jane doe     mister bigshot     ms importante 

Case 2

If we want the system to automatically use Manager::print () for items of type Manager*, we have to declare the function print () as a virtual function in the base class. So the definition of the base class will have to change to look like

     class Employee {         string firstName, lastName;         // ..... public:     Employee(string fnam, string lnam) {         firstName = fnam;         lastName = lnam;     }     virtual void print() const {                                                                             //(E)         cout << firstName << " " << lastName << " ";     } }; 

Pay attention to the keyword virtual that appears in the header of the base class function print() in line (E).[7]. Declaring print() virtual in the base class causes the print loop of line (C) to output

      john doe      jane joe      mister bigshot works at level: 2      ms importante works at level: 3 

A virtual function in a base class acts like an interface to the corresponding functions defined for the derived classes. If there exists in the derived class a definition for the same function, the runtime will ensure that the correct definition of the function is used for each object.

Here is the source code for this example:

 
//VirtualPrint1.cc #include <iostream> #include <string> #include <vector> using namespace std; class Employee { // BASE string firstName, lastName; public: Employee(string fnam, string lnam) { firstName = fnam; lastName = lnam; } virtual void print() const { //(F) cout << firstName << " " << lastName << " "; } virtual ~Employee(){} //(G) }; class Manager : public Employee { // DERIVED short level; public: Manager(string fnam, string lnam, short lvl) : Employee(fnam, lnam), level(lvl) {} void print() const { //(H) Employee::print(); cout << " works at level: " << level; } ~Manager(){} }; int main() { vector<Employee*> empList; Employee* e1 = new Employee("john", "doe"); Employee* e2 = new Employee("jane", "joe"); Employee* e3 = new Manager( "mister", "bigshot", 2 ); Employee* e4 = new Manager( "ms", "importante", 3 ); empList.push_back( e1 ); empList.push_back( e2 ); empList.push_back( e3 ); empList.push_back( e4 ); vector<Employee*>::iterator p = empList.begin(); while ( p < empList.end() ) { (*p++)->print(); cout << end1; } delete e1; delete e2; delete e3; delete e4; return 0; }

Line (F) of the program above defines a virtual print() function in the base class Employee. This function is overridden in the derived class Manager in line (H).

The reason for why the base class destructor in line (G) has been declared virtual will be made clear in Section 15.10.

15.7.1 Restrictions on Virtual Function Declarations

A requirement on a virtual function is that such a function must be defined for the class in which it is first declared, unless it is declared to be a pure virtual function (a concept defined in Section 15.12).

Additionally, although a virtual function will typically be in the protected or the public sections of a class, since only those members are visible in a derived class, it is legal and sometimes very useful to define a virtual function in the private section of a class. Later in Section 15.9 we will show an example of a private virtual function.

15.7.2 Virtual Functions in Multilevel Hierarchies

Consider the 3-level hierarchy of Figure 15.8. Suppose we define a print() function to be virtual in class Person and then provide override definitions for this function in Employee and Manager classes. Now suppose we create a vector of Employee objects by

     vector<Employee*> empList;     Employee* e1 = new Employee( "mister", "bigshot", 2 );     Employee* e2 = new Employee( "ms", "importante", 3 );     Manager* m3 = new Manager( "mister", "biggun", 5, 2 );     Manager* m4 = new Manager( "ms", "shiningstar", 5, 2 );     empList.push_back( e1 );     empList.push_back( e2 );     empList.push_back( m3 );     empList.push_back( m4 ); 

Will the print() function exhibit polymorphic behavior on this list of Employee objects even though the virtual declaration was made for the Person class?


Figure 15.8

The answer is yes. Once a function is declared virtual at any depth in a hierarchy, it will exhibit polymorphic behavior with respect to all classes at and below that level in the hierarchy. The following source code can be used to exercise this idea:

 
//VirtualPrint2.cc #include <iostream> #include <string> #include <vector> using namespace std; class Person { // BASE string firstName, lastName; public: Person( string fnam, string lnam ) : firstName( fnam ), lastName( lnam ) {} virtual void print() const { cout << firstName //(A) << " " << lastName << " "; } virtual ~Person(){} //(B) }; class Employee : public Person { string companyName; public: Employee( string fnam, string lnam, string cnam ) : Person( fnam, lnam ), companyName( cnam ) {} void print() const { Person::print(); cout << companyName << " "; } ~Employee(){} //(C) }; class Manager : public Employee { // DERIVED short level; public: Manager( string fnam, string lnam, string cnam, short lvl ) : Employee( fnam, lnam, cnam ), level( lvl ) {} void print() const { Employee::print(); cout << level; } ~Manager(){} //(D) }; int main() { vector<Employee*> empList; Employee* e1 = new Employee( "mister", "bigshot", "megaCorp" ); Employee* e2 = new Employee( "ms", "importante", "devourCorp" ); Employee* m3 = new Manager("mister", "biggun", "plunderCorp" , 2); Employee* m4 = new Manager("ms", "shiningstar", "globalCorp", 2); empList.push_back( e1 ); empList.push_back( e2 ); empList.push_back( m3 ); empList.push_back( m4 ); vector<Employee*>::iterator p = empList.begin(); while ( p < empList. end() ) { //(E) (*p++)->print(); //(F) cout << end1; } delete e1; delete e2; delete m3; delete m4; return 0; }

This program produces the following output:

      mister bigshot megaCorp      ms importante devourCorp      mister biggun plunderCorp 2      ms shiningstar globalCorp 2 

With regard to the destructor statements in lines (B), (C), (D), especially with regard to the need for the virtual destructor in line (B), see the discussion in Section 15.10.

15.7.3 Can Operators Be Made to Behave Polymorphically?

While C++ functions can be made to behave polymorphically by declaring them virtual, what about the operators? Suppose we provide overload definitions for the output operator ‘<<' (as was shown in Section 15.5) for both the Employee class and the Manager class in the Employee-Manager example at the beginning of this section. Now suppose we include the following statements in main():

      Employee* e_ptr = new Manager( "ms", "importante", 3);      cout << *e_ptr;        // Employee def for '<<' invoked 

In light of our previous discussions on polymorphism, the true identity of the object pointed to by e_ptr is not known at compile time. It could either be an Employee object or it could be a Manager object. So which definition of the ‘<<' operator will be invoked for the above cout statement?

The answer is: The definition of the ‘<<' operator for the base Employee class. Polymorphism cannot be exhibited with respect to operators. All that is known at compile time is that e_ptr is of type Employee* and therefore the compiler will decide to use that definition of the output operator that applies to Employee objects. The only way to invoke the Manager definition is by using a cast operator, as below:

     Employee* e_ptr = new Manager( "ms", "importante", 3);     Manager* m_ptr = static_cast<Manager*>(e_ptr);     cout << *m_ptr;         // Manager def for '<<' invoked 

15.7.4 Polymorphic Types

A class that declares or inherits at least one virtual function defines a polymorphic type. If for some reason no other functions can be declared virtual, you can still create a polymorphic type by making the destructor virtual.[8] The reason for why you may want to declare a polymorphic type even when the mainstream functions are not polymorphic has to do with Run-Time Type Identification (RTTI) that we will discuss in the next chapter. RTTI works only for polymorphic types.

[4]We also mentioned in Chapter 3 that a member function must behave polymorphically in order to be called a method. Therefore, all member functions in Java are methods.

[5]However, as we mentioned earlier, this usage distinction between the words function and method is not followed strictly in the literature. It is common to refer to both functions and methods as just functions.

[6]This works because we declared Employee to be a public base of Manager, meaning that a Manager* can be assigned to an Employee* without explicit type conversion.

[7]We could also have written this function as void virtual print(){ …… }

[8]As we will see in Section 15.10, there can be other important reasons for declaring a destructor to be virtual. A nonvirtual destructor can become a source of memory leaks for polymorphic types.




Programming With Objects[c] A Comparative Presentation of Object-Oriented Programming With C++ and Java
Programming with Objects: A Comparative Presentation of Object Oriented Programming with C++ and Java
ISBN: 0471268526
EAN: 2147483647
Year: 2005
Pages: 273
Authors: Avinash Kak

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net