8Polymorphism

The power of object-oriented programming lies not only in inheritance, but also in its ability to treat a derived-class object like a base-class object. It is polymorphism and dynamic binding that support this mechanism.

8.1An Overview of Polymorphism

Polymorphism means that different objects behave differently when they are given the same message. The call by member functions is the ‘message’ and different behaviors are due to different implementations, i.e., calling different functions. Polymorphism is quite often used in programming. The most common example is the math operator. We use the same plus sign “+” to implement adding operation between integers, floating numbers, and double precision floating numbers. The same message, adding, is accepted by different objects or variables, and different variables carry out the adding operation in different ways. If the adding operation is performed on variables of different types, e.g., a floating and an integer, then the integer will first be converted to a floating point before adding. This is a typical polymorphism.

8.1.1Types of Polymorphism

Object-oriented polymorphism can be divided into four types: overload polymorphism, coercion polymorphism, inclusion polymorphism, and argument polymorphism. The former two fall into the category of special polymorphism, while the latter two, general polymorphism. Overloads of ordinary functions and overloads of class member functions are both overload polymorphism. We will learn operator overloading in this chapter. The example of the add operation between floating numbers and integers is an example of overload. Coercion polymorphism is converting the type of a variable to meet the requirement of the function or operation. When adding a floating number and an integer, first we should do type coercion to convert an integer to a floating number. This is an example of coercion polymorphism.

Inclusion polymorphism is investigating polymorphic behaviors of the member functions with the same name in different classes within a class family. It is implemented by using virtual functions. Argument polymorphism and class templates (to be introduced in Chapter 9) are related to each other. This polymorphism must be assigned an actual type before realization. So, all classes instantiated from the class template have the same operations, but the types of operands are different.

This chapter mainly introduces overloading and inclusion polymorphisms. Function overloading has been elaborated in Chapters 3 and 4. Here we mainly introduce operator overload. Virtual function is the main issue of polymorphism.

8.1.2Implementation of Polymorphism

Polymorphism can be divided into two groups in terms of implementation: compiling polymorphism and executing polymorphism. For the former group, the operands in the same operation are determined during compiling while for the latter group, they are determined dynamically during execution. The process of determining the operands is binding. Binding is a procedure in which computer programs relate to each other by themselves. It also means combining the identifier and memory address. In object-oriented programming, binding is the process of connecting a message with a function of an object. According to the different stages of binding, there are two ways of binding: static binding and dynamic binding, corresponding to two methods of polymorphism implementation.

A static binding is a binding during the compiling and linking processes. Because binding is done before the execution, it is also called early binding. During compiling and linking, the system can determine the relation between the operation and the code for the operation according to the features such as type matching. In other words, it determines the proper code for a certain identifier. The operands with the same operation name can be decided during compiling and linking for some polymorphism types. It is done by static binding, such as overloading, coercion, and argument polymorphism.

If binding is performed during the execution, such a binding is called dynamic binding. In some cases, binding cannot be performed during compiling or linking and instead, it is performed during the execution. It is through dynamic binding that objects are determined if they have polymorphism operations.

8.2Operator Overload

Operands of the predefined operators in C++ are only basic data types. In fact, for many user-defined types (such as classes), similar operations are also needed. For example, the following program defines a class of a complex number.

We can then define an object of the complex number class with the following statement:

The question is how to perform the add operation between a and b. Naturally, we would like to use the operator “+” and to write the expression “a+b”, but such expression will render an error during compiling because the compiler does not know how to perform the add operation between two complex numbers. In this case, we must write a program to clearly specify the operation ‘+’ when it operates on complex number objects. This is called operator overload. Operator overload makes existing operators more versatile and, with overload, an operator can act on different types of data and have different behaviors.

Operator overload is essentially function overload. We should first convert the expression to the call of an operator function. The operands are converted to actual arguments of the operator function. And then the function to be called is determined according to the type of actual arguments, which is completed in the compilation process.

8.2.1Rules of Operator Overload

The rules of operator overload are as follows:

  1. All operators in C++ can be overloaded except a few exceptions and only those existing operators in C++ can be overloaded.
  2. After overloading, the priority and associability are still the same.
  3. Operator overload meets the actual needs of new types of data and makes appropriate modifications to the original operator. Generally speaking, an overloaded function should be similar with the original function. The number of operands cannot be changed and at least one operand should be a user-defined type.

Only five operators cannot be overloaded. They are the genetic relation operator “.”, member pointer operator “.*”, scope resolution operator “::”, sizeof operator, and ternary operator “?:”. The first two operators ensure the meaning of accessing members in C++ will not be changed. The operands of the scope resolution operator and sizeof operator are types, not regular expressions, so they do not have the overloading feature.

There are two types of operator overload: overloading as member functions and overloading as friend functions. The syntax of overloading as member functions is:

The prototype of a friend function should be declared in the class if the operator is overloaded as a friend function:

The function type is the type of return value of the overloaded operator, i.e., the type of the result of the operation. The term operator is the key word in defining operator overload. operatorname is the name of the operator to be overloaded. It must refer to an operator that can be overloaded, such as “+”, the add sign. The argument list lists the arguments and their types needed for the operator. If the operator is to be overloaded as a friend function, then the key word friend should be declared in the function in the class. The function is implemented outside the class.

If the operator is overloaded as a member function, the number of arguments is one less than the number of operands (not applicable to “++” or “−−”). If the operator is overloaded as a friend function, then the two numbers are the same. The reason is that if the operator overloaded as a member function and an object uses the overloaded member function, then the data of the object can be accessed directly, so there is no need to pass it by the argument list. The missing operand is the object itself. But when the operator is overloaded as a friend function, and the friend function operates on the data of an object, it must work through the object name. So the number of operands does not change.

The main advantage of operator overloading is that we can change the operations of existing operators so they can operate on a user-defined class type.

8.2.2Operator Overloaded as Member Function

Operator overload is essentially function overload. When an operator is overloaded as a member function, it can freely access data members in the class. We always use an object of the class to access overloaded operators. If it is a binary operator, an operand is the object’s data, pointed by the pointer this, and the other operand is passed by an argument list of the overloaded function. If the operator is unary, the operand is pointed by pointer this of the object, so it does not need any argument. These two cases are illustrated below.

For binary operator B, if it is overloaded as a member function in order to indicate the expression oprd1 B oprd2, where oprd1 is an object of class A, then B should be overloaded as a member function of A and the function has one formal argument, whose type is the type of oprd2. After overloading, expression oprd1 B oprd2 is equivalent to calling function oprd1.operator B (oprd2).

For a prefix operator U, such as “−” (minus sign), if it is to be overloaded as a member function in order to indicate expression U oprd, where oprd is an object of class A, then operator U should be overloaded as a member of class A, which has no argument list. After overloading, the expression U oprd is equivalent to calling function oprd. operator U().

For postfix operators “++” and “−−”, if they are to be overloaded as member functions in order to indicate expression oprd++ or oprd−−, where oprd is an object of class A, then the operator should be overloaded as a member function of class A and the function has an integer (int) as formal argument. After overloading, the expressions oprd++ and oprd−− is equivalent to calling function oprd.operator++ (0) and oprd.oprator−−(0) respectively. Here, the integer argument does not play any useful roles. It is used to differentiate prefix ++, −− and postfix ++, −−.

In UML language, the representations of overloaded operators are similar to that of other member functions, which is “operator operatorname (argument list): function type”.

Example 8.1: Operator (add and minus) overload for complex number class as member functions.

This is an example of binary operators overloaded as member functions. The rule of addition and subtraction is to separately add and subtract the real part and imaginary part respectively. The two operands must be objects of a complex number class. So operators “+” and “−” can be overloaded as member functions. The overloaded function has only one formal argument, an object of a complex number class. In the example, the UML representation of the complex number class with operators overloading is shown in Figure 8.1.

Fig. 8.1: The UML representation of a complex number class with overloaded operators “+” and “−”.

In the example, the addition and subtraction of complex numbers are overloaded as member functions of the complex class. We can see that operator overload member functions are almost the same as the ordinary member functions except the key word operator is used in the declaration and implementation. We can call the function by using the operator and operands. The original functions of operator “+” and “−” are still the same, which means for data of basic types such as floating numbers and integers, the operators still obey the predefined rules in C++, but they have new functions for operating on complex numbers. Operator “+”, when acting on different objects, will have different behaviors, and hence it has polymorphic features.

In this example, temporary objects are created for returning values in the overload functions of “+” and “−”:

This statement appears to be “calling the constructor function”. In reality, this is only a temporary object syntax, which means to “create a temporary object and return it”. We can also use the following statements to return the function value:

The efficiencies of these two methods are different. The latter execution includes the following steps. First a local object c (the constructor is called here) is created, then the copy-constructor is called when executing return statement, copy the value of c to a temporary object in main(). When the function operator “+” ends, the destructor is called to destroy object c. The former method is much more efficient, which creates a temporary object in main().

The result of the program is:

Example 8.2: Overload unary operator ++ as member function.

This example is to overload unary operator ++ as a member function. Here we use the example of class Clock. The operands of unary operator prefix ++ and postfix ++ are both objects of class Clock. We can overload the operator as a member function of class Clock. For the prefix unary operator, there is no formal argument in the overload function but for the postfix unary operator, there is an integer in the formal argument list.

In the example, we overload the prefix ++ and postfix ++ for time increments as member functions of class Clock. The most important difference between the overloads of the prefix operator and the postfix operator is the formal arguments of the overload functions. According to the syntax, the member function of a prefix unary operator has no formal argument, while for a postfix unary operator, an integer formal argument is required. The integer argument is not used in the function body. It is only for differentiating the prefix and postfix. Therefore the argument list can contain only the type name without the argument name. The execution result of the program is as follows.

8.2.3Operator Overloaded as Friend Function

The operator can also be overloaded as a friend function of the class. Thus, it can freely access any data member of the class. Here, all the operands need to be passed by a formal argument list. The argument order from left to right in the argument list is the order of operands.

For binary operator B, if it has an operand that is an object of class A, then B can be overloaded as a friend function of class A. The function has two formal arguments and the type of one argument is class A. After overloading, expression oprd1 B oprd2 is equivalent to calling function operator B(oprd1, oprd2).

For prefix unary operator U, e.g., “−”, to realize operation Uoprd, where oprd is an object of class A,U can be overloaded as a friend function of class A. The formal argument is the object oprd of class A. After overloading, the expression U oprd is equivalent to calling function operator U(oprd).

For postfix operators ++ and −−, to realize expression oprd++ or oprd−−, where oprd is an object of class A, the operators can be overloaded as friend functions of class A and there are two formal arguments: one is object oprd and the other is an integer. The second argument is used to distinguish itself from prefix operators. After overloading, the expression operd++ and oprd−− is equivalent to calling function operator++ (oprd, 0) and operator−−(oprd,0).

Example 8.3: Overload complex operators + and − as friend functions.

In the example, operators + and − are overloaded as friend functions of class complex for implementing complex addition and subtraction. The problem is the same as the one in Example 8.1. Since both of the operands are complex objects, the overload function has two complex numbers as formal arguments. In the example, the UML representation of overloading operator + and − as friend functions of the class complex is shown in Figure 8.2.

Fig. 8.2: The UML representation of overloading operator + and − as friend functions of the class complex.

If the operator is overloaded as a friend function, then all operands must be passed to operator overload function by formal arguments. Compared to Example 8.1, no change is made in main(). The change is mainly in the members of the class complex. The execution results are the same.

Here, we only introduce several simple operator overload. Overloading of certain operators, such as [], =, and casting is to some degree different. They are omitted in this chapter but will be introduced in Chapter 9 where the example “safe array class template” is used to demonstrate the concept.

8.3Virtual Function

In Section 7.7, we introduced a program for a staff information management system. A problem remains unsolved from that example, i.e., how to utilize iteration to deal with different objects of the same class. To do that, we now change the main() in Example 7.10 into the following form and observe the outcome of the program.

The execution result is:

From the execution result, function pay() in the derived classes is not executed. Readers can execute the program step-by-step and it will be clear that every time the base-class object pointer calls the derived-class member function, it actually calls the base-class member function instead of the new function in the derived class. Function pay() in the base class does not work. To solve this problem, we can use a virtual function to implement polymorphism.

Virtual functions are the basis of dynamic binding. A virtual function must be a non-static member function. After being overloaded, the virtual function can implement the execution polymorphism in a class family.

According to the type compatibility rule, we can use derived-class objects to replace base-class objects. If the base-class pointer points to a derived-class object, then we can access the object by the pointer. The problem is that what we access is the member inherited from the base class. One way to solve the problem is that if a base-class pointer points to a derived-class object, and we want to access a derived-class member whose name is the same as that of a base-class member, then the function in the base class should be declared a virtual function. By doing so, different objects of different derived classes will have different behaviors and the execution polymorphism can be realized.

8.3.1Ordinary Virtual Function Member

The syntax for declaring a virtual member function is as follows:

The keyword virtual is to restrict the member function in the class definition. The declaration of a virtual function can only appear in the function prototype declaration in the class definition, but not in the implementation of the member function.

Polymorphism must satisfy three conditions during execution. The first condition is having a compatible assignment rule among classes. The second one is the declaration of the virtual function, and the third is that the virtual function is called by a member function, pointer, or reference. If the virtual function is accessed by an object name, then binding can be completed during compiling (static compiling), and there is no need to do it during execution.

In UML representation, an ordinary virtual function is represented by adding <<virtual>> in front of the member function.

Example 8.4: Virtual member function.

This program is a modified version of Example 7.4’s “compatible type rule” in Chapter 7. The member function display() is declared virtual in base class B0. There is no modification to other parts. The difference between this program and the one in Example 7.4 is that the virtual member function of the derived-class object pointed by the base-class pointer can be accessed by the base-class pointer. The derivation relation is shown in Figure 8.3 in a UML representation where class B0 has a virtual function display().

Fig. 8.3: UML of derivation relation of class B0 with a virtual function.

Class B0, B1, and D1 belong to the same class family. They are derived in a public way so they satisfy the compatible type rule. At the same time, member function display() in base class B0 is declared as virtual. The member function is accessed by an object pointer in the program, so the binding is completed in execution, thus implementing execution polymorphism. We can access the members of the pointed objects using a base-class pointer, which allows objects of the same class family to be processed uniformly. The program is more succinct and efficient. The execution result of the program is as follows:

In the program, the derived class does not declare the virtual function explicitly. The system will follow the following rules to judge whether a derived-class member function is virtual.

If the function has the same name as some base-class virtual function

If the function has the same number of arguments and a corresponding argument type with the base-class virtual function

If the function has the same return value or the same return value of the pointer and reference as the base-class virtual function, satisfying the compatible assignment rule

After examining the name, arguments, and return value, the derived class is recognized as a virtual function if it satisfies all the above conditions. Here, the virtual function of the derived class overtakes the virtual function of base class. And the virtual function of the derived class also hides all other forms of overloading by the function with the same name.

When the base-class constructor calls a virtual function, the derived-class virtual function will not be called. Suppose there is a base class Base and a derived class Derived. They both have the virtual function virt(). If Base::Base() calls virtual function virt(), then it is Base::virt() that is called, not Derived::virt(). This is because when the base class is constructed, the object is not a derived-class object yet.

Similarly, when the base class is destructed, the object is no longer a derived-class object. So if Base::~Base() calls virt(), then what is actually called is Base::virt(), not Derived::virt().

Only virtual functions are bound dynamically. If the derived class needs to modify base-class behaviors (i.e., override the function with the same name), then the function should be declared virtual in the base class. Those functions that are not virtual functions cannot be modified by the derived class. They cannot become polymorphic. So, one usually should not override the nonvirtual functions inherited from the base class although the syntax does not prevent the programmer from doing so.

When the virtual function inherited from the base class is overloaded, and the function has a default formal argument value, one should never redefine different values. The reason is that the default formal argument value is bound statically, although the virtual function is bound dynamically. That is to say, the virtual function in a derived class can be accessed by a base-class pointer pointing to a derived-class object. The default formal argument value, however, can only be defined in the definition of the base class.

8.3.2Virtual Destructor

In C++, one is not allowed to declare a virtual constructor, but it is legal to declare a virtual destructor. Destructors do not have types or arguments. Compared to ordinary member functions, destructors are simpler.

The syntax of a virtual destructor’s declaration is as follows.

If the destructor of a class is a virtual one, then all child classes’ destructors are all virtual destructors. After setting a destructor to be a virtual one, the pointer and reference are bound dynamically to implement execution polymorphism. And the base classes’ pointer can be used to call an appropriate destructor to clean up according to different objects.

To sum up, if it is possible to call an object’s destructor by a base-class pointer (by delete), and an object to be destructed is an object of the class that has an important overloaded destructor, then the base-class destructor should be a virtual function.

Example 8.5: Example of virtual destructor.

Note that the following program does not have a virtual destructor.

The output message during execution is:

This illustrates that it is the base-class destructor, rather than the derived-class destructor, that is called when deleting a derived-class object by a base-class pointer. So the memory dynamically allocated to derived-class objects is not freed and this causes memory leak. That is to say that the memory that i_pointer points to is not used or freed after the disappearance of the object. For a program that needs a large memory and long running time, it is dangerous to repeat the error. It may eventually cause insufficient memory error and the program will be terminated.

An effective way to avoid this error is to declare the destructor to be a virtual function:

The output message during the execution is:

In this way, the derived-class destructor is called, the dynamically allocated memory is freed properly, and the virtual function implements polymorphism.

8.4Abstract Classes

An abstract class is a special class, which provides uniform interface to a class family. An abstract class is designed to abstract and design. The member functions can be called in a polymorphic way by abstract classes. An abstract class lies in an upper layer of classes. An abstract class cannot be instantiated, which means we cannot directly define an abstract-class object. An abstract class has to be inherited and derived from a nonabstract derived class before it can be instantiated.

An abstract class is a class with pure virtual function. To understand abstract classes, we must first learn pure virtual functions.

8.4.1Pure Virtual Functions

In Section 7.7, a problem remains unsolved in the staff management program. The problem is that the body of base-class member function pay() is empty but it is necessary, though cumbersome, to include it. The question for such functions is whether we can just declare the interface in the base class for the whole class family and implement it in the derived classes. This in fact can be accomplished by using pure virtual functions in C++.

A pure virtual function is a virtual function declared in the base class. It does not declare specific operations in the base class. All the derived classes will have their own version of definition according to their needs. The syntax of a pure virtual function declaration is:

In fact, it only differs from the ordinary virtual function by a “=0” in the syntax. After declaring a pure virtual function, the function should not be implemented in the base class but rather in the derived class.

In UML, a pure virtual function is also called an abstract function, which is represented by adding <<abstract>> before the italicized function name.

Please note the difference between an empty virtual function and pure virtual function. A pure virtual function does not have a body, but the empty virtual function has an empty function body. The former is in an abstract class that cannot be initialized directly, while the latter can. The common features they share are that they both can derive new classes, they are implemented in the new class, and the implementation can be polymorphic.

8.4.2Abstract Classes

A class with a pure virtual function is an abstract class. The primary role of an abstract class is to establish a common interface for a class family and to enhance its polymorphic characteristics. An abstract class declares a common interface for a derived class family. The implementation of the interface, i.e., the body of the pure virtual function, is to be defined by the derived class.

After the abstract class derives a new class, and the derived class has implemented all pure virtual functions, then the derived class can define its own objects and the derived class is no longer an abstract class. Also, if the derived class does not implement all pure virtual functions, then it is still an abstract class.

An abstract class cannot be instantiated. An object of an abstract class cannot be defined. We can, however, declare an abstract-class pointer and reference, by which derived-class objects can be accessed. This kind of access is polymorphic.

In UML language, the name of an abstract class is written in italics.

Example 8.6: Example of an abstract class.

This program is a modified version of the program in Example 8.4. Member function display() is declared to be a pure virtual function in base class B0, so B0 is an abstract class and we cannot declare any objects of B0.We can declare pointers and references of B0. Class B1 is derived publicly from B0, and B1 derives class D1 as a new base class. When a pointer of the abstract class B0 is pointed to a derived-class object, we can access the virtual function of the object using the pointer. The UML representation of the abstract class and virtual function in this example is shown in Figure 8.4.

Source code:

Fig. 8.4: The UML representation of the abstract class and virtual function.

In this program, classes B0, B1, and D1 belong to the same class family. Abstract class B0 provides a general external interface for the whole class family via a pure virtual function. The child class derived by public derivation implements the pure virtual function, so it is not an abstract class and we can define derived-class objects. According to the compatible assignment rule, the abstract-class pointer of B0 can also point to any derived classes’ object. We can access the objects of derived-classes B1 and D1 through the pointer of base class B0. This is an implementation of polymorphism of uniformly processing objects in a class family. The execution result of the program is:

Note that the key word virtual is not indicated explicitly in the virtual function of the derived class because it has the same name, argument, and return value as the pure virtual function of the base class. The system automatically recognizes it as a virtual function.

8.5Program Instance: Variable Stepwise Trapezoid Method to Calculate Functional Definite Integral

In many applications, definite integrals are evaluated by using numerical approximation methods. The reason is that many functions only have values at discrete points or the integrand cannot be represented by fundamental functions. In this example, we will introduce a basic variable step size trapezoid method to calculate a functional definite integral.

8.5.1Basic Principle

We only consider the simplest situation. Suppose the integrand is a function of one variable. And the expression of definite integral is:

I=abf(x)dx(8.1)

The meaning of the integral is the area under the graph of the function f(x) over the interval between a and b as shown in Figure 8.5.

If the integral is integrable for any integer n, let h = (ba)/n and

Xk=a+k×h

Ik=xk1xkf(x)dx

Fig. 8.5: Principle of trapezoid integral.

then we have I=k=1nIk

In the small interval [Xk−1, Xk], we take an approximate value of Ik and calculate the approximate value of I.

In the interval [Xk−1, Xk], let Ik = (f(Xk−1)+ f(Xk))×h/2 for the trapezoid integral. An easy way to understand the trapezoid integral is to see that the original interval is divided into a series of small intervals and in each small interval, we approximate the integral of the original function by the area of a trapezoid. If the interval is small enough, a close approximation of the value of the original integral can be achieved. Here, the integral can be approximated by:

Tn=k=0n1h2[f(xk)+f(xk+1)](8.2)

Before applying the integral formula, an appropriate step length must be given. If the step is too big, then the precision cannot be guaranteed. Therefore, we often use a variable step size method. We apply the integral formula repeatedly and in each integration, the step size is halved. This procedure is applied until the integral result meets the precision requirement.

Dividing the integral interval [a, b] into n equal intervals, we get n + 1 points of division. According to formula (8.2), we ought to calculate the function value n + 1 times. If the integral interval is halved, then the number of points of division reaches 2n + 1. We analyze the recursive relation between these two integrals as follows:

After halving, each subinterval [xk, xk+1] has one more point of division xk+1/2 = (xk + xk+1)/2. So the integral value after halving is:

h4[f(xk)+2f(xk+12)+f(xk+1)](8.3)

Note that here h = (ba)/n is still the step before halving. Adding each interval’s integral together, we get the integral result:

T2n=h4k=0n1[f(xk)+f(xk+1)]+h2k=0n1f(xk+12)(8.4)

We can get the recursive formula by exploiting the integral result before halving (8.2):

T2n=12Tn+h2k=0n1f(xk+12)(8.5)

For practical problems, we often take the following steps:

First, let n = 1, apply formula (8.2) to calculate the integral value.

Second, halve and apply recursive formula (8.5) to calculate the new integral value.

Third, judge if the difference between the two integral values is within the given error tolerance, and if so, then the integral value after halving is the expected result and we can stop the integration. Otherwise, go back to the second step and continue execution.

Note that this integral method has certain restrictions on the integrand. If a function happens to be 0 on the boundaries and center, but not 0 between these points, then the results of the first and the second step are both 0. In this case, the procedure is concluded with 0 as the final result, which is obviously wrong.

8.5.2Analysis of Program Design

From the above analysis, it is obvious that the two problems we face are: the calculation of the integrand values where we need to evaluate the function value over a very small interval at every step, and the implementation of the variable step size of a trapezoid integral.

Two abstract classes are defined: function class F and integral class Integ. They have virtual functions:

They are both overload operators (). The former calculates the function value at point x while the latter calculates the integral value with an error less than eps in interval [a,b]. The specific implementations are given by their derived classes. The advantage of doing so is that when the derived class offers different implementations, it can calculate different integrals using different methods (here we only use the variable step size trapezoid method, but we can also use new methods by deriving new classes). Figure 8.6 illustrates the classes we designed and their relations.

When calculating the integral, the member function in class Trapz needs to access the member function in class Fun. Data member f (reference of class F) is added to class Trapz. According to the features of virtual functions and the rule of compatible assignments, we can access the member function in the object of the derived class of class F using the pointer.

Fig. 8.6: The classes designed for the variable step size trapezoid method and their relations.

8.5.3Source Code and Explanation

Example 8.7: Variable step size trapezoid method to calculate functional definite integral.

The program has three independent files. File Trapzint.h has the class definitions. File Trapzint.cpp includes implementations of member functions. File intmain.cpp includes the main(). The main() defines objects of class Fun and Trapz. We calculate the integral of a test function in a given interval using these objects, and the error eps is 10−7.

I=12log(1+x)1+x2dx(8.6)

After public derivation, class Fun has all members of class F except the constructor and destructor. Because the base class has a pure virtual function, it is an abstract class. So the member function in the derived-class is a specific implementation of the pure virtual function in the base class. The integrand is shown in expression 8.6.

f(x)=log(1+x)1+x2(8.7)

Because the member function has the same name, argument, and return value as that of the base-class function, the system recognizes it as a virtual function. So there is no need to declare it explicitly. If we calculate the integral of another function, we only need to derive a new class from base class F, e.g., Fun1, and implement the function as a pure virtual function of class Fun1.

After public inheritance, class Trapz inherits members of class Integ. The base class is a virtual class. The pure virtual functions are implemented in member functions in the derived class. The function in file Trapzint.cpp is an implementation of the variable step size trapezoid integral algorithm.

The above function calculates the integral result of integrand f in interval [a, b] using the overloading operator (). The integral error is controlled by eps. Function f is a reference of abstract class F, whose implementation is given by derived class Fun and whose return value is the integral result.

In main(), an object f of class Fun and an object trapz1 of class Trapz are defined and trapz1 is initialized by the object of class Fun. A data member of class Trapz, the reference of abstract class F becomes an alias of object f. A reference operation on base class F in object trapz1 was performed on object f of class Fun. Through trapz1’s calling the overloaded operator (), calculation was implemented in interval [0,2] with an error of less than 1e–7.

8.5.4Execution Result and Analysis

The execution result is:

To calculate another function’s integral, we can modify the program as follows. The new integrand should be given in class Fun and then point out the interval a and b and error control especially when object trapz1 calls the overloaded operator (). If there are other integral algorithms, such as the variable step size Simpson algorithm, then we should derive a new class, e.g., a class named Simpson, and change the overload function of operator () to a Simpson algorithm. In main function, we also need to define a Simpson object.

This is a complicated example where we solve a practical problem involving almost all important aspects of object-oriented programming, such as inheritance, operator overload, abstract classes, and so on. It implements polymorphism using abstract classes. It is worthwhile for readers to carefully analyze this program and to test and run this program.

8.6Program Instance: Improvement on Staff Information Management System for a Small Corporation

In Chapter 7 ,we used the staff information management system as an example to illustrate derivation, virtual functions, and virtual base classes. There are two drawbacks to the program of Example 7.10:

First, the base-class member function pay() is empty. It is necessary but cumbersome to implement its body.

Second, similar operations are performed on the four different objects in main() by repeating similar statements four times. This is not succinct.

In this section, we take advantage of virtual functions and abstract classes to improve the program.

The class design is the same as that in Example 7.10 of Chapter 7. The only difference is that the member function pay() is designed to be a pure virtual function in base class employee. Then, we can use a base-class pointer array to process different derived-class objects according to the compatible assignment rule in main(). This is because the system will execute the pointed object’s member function when calling the virtual function using a base-class pointer.

Because different actual arguments are needed to pass the function promote() when called by objects of different classes, it is hard to process different objects uniformly in the iteration. So function promote() is declared to be a virtual function in base class employee. The derived classes then declare functions with the same name. The member function promote() in the base class is called with different formal arguments in derived classes. The derivation relation is shown in Figure 8.7.

Fig. 8.7: The UML representation of the derivation relation in Example 8.8.

Example 8.8: Staff information management.

As in Example 7.10, the program has three files: employee.h is the header file of class definitions, employee.cpp is the file of class implementation, and 8_8.cpp is the file of the main functions. File 8_8.cpp and employee.cpp are linked together after compiling. These two files should be in one project if developed in VC++.

The execution result is:

We can see that the execution result is the same as in Example 7.10. But because base class employee has virtual functions, it is the derived-class member functions that are accessed when using the base-class pointer array to process objects of different classes. So we can use an iterative structure in main() and, in the iterations, functions of different classes are called uniformly to fulfill different actions.

8.7Summary

We introduced an important feature of class: polymorphism. Polymorphism means that different objects behave differently when they are given the same message. It is another abstraction of member functions. The call by member functions is the ‘message’ and different behaviors are due to different implementations, i.e., calling different functions.

The polymorphism in C++ can be divided into four groups: overload polymorphism, coercion polymorphism, inclusion polymorphism, and argument polymorphism. The former two fall into the category of special polymorphism while the latter two are general polymorphisms. Overloads of ordinary functions and member functions belong to overload polymorphism. Overload is when a function or procedure can act on objects of different types. Coercion polymorphism converts the type of a variable by semantic operation to meet the requirement of a function or an operation. Inclusion polymorphism investigates polymorphic behaviors of member functions with the same name defined in different classes of the same class family. It is implemented by virtual functions. Argument polymorphism is related to class attributes, which are class templates that can be parameterized. The types related to the included operations must be instantiated by type arguments. Different classes instantiated by class attributes have the same operation but the types of the objects are different. This chapter mainly introduced overload and inclusion polymorphisms. Operator overload polymorphism and virtual function are key learning objectives of this chapter.

Polymorphism can be divided into two groups in terms of implementation: compiling polymorphism and executing polymorphism. For the former group, the specific operation objects are determined during compiling while for the latter group they are decided dynamically during execution. The process of determining operation objects is called ‘binding.’

Operator overload makes existing operators more versatile and allows them to operate on user-defined types (such as class). Operator overload is essentially function overload. We should first convert the expression to calling an operator function. The operands are converted to arguments of the operator function. And then the function to be called is determined according to the type of the arguments, which is completed in the compilation process.

Virtual functions are nonstatic member functions declared by the keyword virtual. According to the compatible assignment rule, we can use base-class pointers to point to derived-class objects. If the member function of the object is an ordinary member function, then using base-class pointers, we can only access base-class members. If the function with the same name in the base class is a virtual one, then we can access derived-class functions by base-class pointers. Objects of different classes have different behaviors if they are called by base-class pointers, thus executing polymorphism.

The variable step size trapezoid method is a common method for calculating functional definite integrals. In many practical applications, the function only has values at a discrete point or the integral of the function cannot be represented by elementary functions. In these cases, definite integrals are evaluated by numerical approximation methods. In the example of the variable step size trapezoid method, the concepts of inheritance, operator overload, and derived class are applied.

An improved version of the staff information management system is included at the end of the chapter to better illustrate the effect of virtual functions.

Exercises

8.1 What is polymorphism? How is it implemented in C++?

8.2 What is an abstract class? What is its function? Does the class derived from an abstract class have to implement the pure virtual function?

8.3 Declare a virtual function with no return value, whose argument is an integer and name is fn1.

8.4 Can a virtual constructor be declared in C++? Why? What about a virtual destructor? What is its application?

8.5 Write four overload functions Double(x). The return value is twice the input argument. The types of arguments are int, long, float, and double respectively. The type of return value is the same as that of the argument.

8.6 Write a class Rectangle, with data members itsLength (length), itsWidth(width) and so on. It has overloaded constructors Rectangle() and Rectangle(int width, int length).

8.7 Write a counter class Counter and overload the operator +.

8.8 Write a mammal class Mammal, then derive the dog class Dog from Mammal. Both declare the member function Speak(), which is declared as a virtual function in the base class. Define an object of class Dog. Call function speak through the object, and observe the execution result.

8.9 Write an abstract class Shape and derive classes Rectangle and Circle from it. Both Rectangle and Circle have function GetArea() to calculate the area of the object and function GetPerim() to calculate the perimeter.

8.10 Overload operators ++ (self increment) and – (self decrement) for class Point.

8.11 Define a base class BaseClass and derive class DerivedClass from it. Class Base-Class has member functions fn1() and fn2(), and fn1() is a virtual function. Class DerivedClass also has member functions fn1() and fn2(). Define an object of DerivedClass. Let two pointers, a BaseClass pointer and a DerivedClass pointer, point to the object. Call fn1() and fn2() through the pointers and observer the result.

8.12 Define a base class BaseClass and derive class DerivedClass from it. Declare a virtual destructor in BaseClass. In main(), assign a BaseClass pointer with the address of a DerivedClass object. Then free the memory through the pointer and observe the result.

8.13 Define class Point with data members X and Y. Overload operator + as its friend function.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset