Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ For Dummies (2004) [eng].pdf
Скачиваний:
84
Добавлен:
16.08.2013
Размер:
8.09 Mб
Скачать

272 Part IV: Inheritance

You might want x.calcTuition() to call Student::calcTuition() when x is a Student but to call GraduateStudent::calcTuition() when x is a

GraduateStudent. It would be really cool if C++ were that smart.

Normally, the compiler decides which function a call refers to at compile time. When you click the button to tell the C++ compiler to rebuild your exe­ cutable program, the compiler snoops around in your program to decide which function you mean with every call based on the arguments used.

In the case described here, the declared type of the argument to fn() is not completely descriptive. Although the argument is declared Student, it may actually be a GraduateStudent. A decision can’t be made until you’re actu­ ally executing the program (this is known as runtime). Only when the function is actually called can C++ look at the type of the argument and decide whether it’s a plain old student or a graduate student.

The type that you’ve been accustomed to until now is called the declared or compile-time type. The declared type of x is Student in both cases because that’s what the declaration in fn() says. The other kind is the runtime type. In the case of the example function fn(), the runtime type of x is Student when fn() is called with s and GraduateStudent when fn() is called with gs. Aren’t we having fun?

The capability of deciding at runtime which of several overloaded member functions to call based on the runtime type is called polymorphism, or late binding. Deciding which overloaded to call at compile time is called early binding because that sounds like the opposite of late binding.

Overloading a base class function polymorphically is called overriding the base class function. This new name is used in order to differentiate this more complicated case from the normal overload case.

Why You Need Polymorphism

Polymorphism is key to the power of object-oriented programming. It’s so important that languages that don’t support polymorphism can’t advertise themselves as OO languages. (I think it’s an FDA regulation — you can’t label a language that doesn’t support OO unless you add a disclaimer from the Surgeon General, or something like that.)

Languages that support classes but not polymorphism are called object-based languages.

Without polymorphism, inheritance has little meaning. Let me spring yet another example on you to show why. Suppose that I had written a really fab­ ulous program that used some class called, just to pick a name out of the air,

Chapter 21: Examining Virtual Member Functions: Are They for Real? 273

Student. After months of design, coding, and testing, I release this application to rave reviews from colleagues and critics alike. (There’s even talk of starting a new Nobel Prize category for software, but I modestly brush such talk aside.)

Time passes and my boss asks me to add to this program the capability of handling graduate students who are similar but not identical to normal students. (The graduate students probably claim that they’re not similar at all.) Now, my boss doesn’t know or care that deep within the program,

someFunction() calls the member function calcTuition(). (There’s a lot that he doesn’t know or care about, by the way, and that’s a good thing if you ask me.)

void someFunction(Student& s)

{

//...whatever it might do...

s.calcTuition();

//...continues on...

}

If C++ didn’t support late binding, I would need to edit someFunction() to something like the following to add class GraduateStudent:

#define STUDENT 1 #define GRADUATESTUDENT 2

void someFunction(Student& s)

{

//...whatever it might do...

//add some member type that indicates

//the actual type of the object switch (s.type)

{

case STUDENT: s.Student::calcTuition(); break;

case GRADUATESTUDENT: s.GraduateStudent::calcTuition(); break;

}

// ...continues on...

}

I would have to add the variable type to the class. I would then add the assignment type = STUDENT to the constructor for Student and type =

GRADUATESTUDENT to the constructor for GraduateStudent. The value of type would then indicate the runtime type of s. I would then add the test shown in the preceding code snippet to every place where an overridden member function is called.

That doesn’t seem so bad, except for three things. First, this is only one func­ tion. Suppose that calcTuition() is called from a lot of places and suppose

274 Part IV: Inheritance

that calcTuition() is not the only difference between the two classes. The chances are not good that I will find all the places that need to be changed.

Second, I must edit (read “break”) code that was debugged and working, introducing opportunities for screwing up. Edits can be time-consuming and boring, which usually makes my attention drift. Any one of my edits may be wrong or may not fit in with the existing code. Who knows?

Finally, after I’ve finished editing, redebugging, and retesting everything, I now have two versions to keep track of (unless I can drop support for the original version). This means two sources to edit when bugs are found (perish the thought) and some type of accounting system to keep them straight.

Then what happens when my boss wants yet another class added? (My boss is like that.) Not only do I get to repeat the process, but I’ll have three copies to keep track of.

With polymorphism, there’s a good chance that all I need to do is add the new subclass and recompile. I may need to modify the base class itself, but at least it’s all in one place. Modifications to the application code are minimized.

At some philosophical level, there’s an even more important reason for poly­ morphism. Remember how I made nachos in the oven? In this sense, I was acting as the late binder. The recipe read: Heat the nachos in the oven. It didn’t read: If the type of oven is microwave, do this; if the type of oven is conventional, do that; if the type of oven is convection, do this other thing. The recipe (the code) relied on me (the late binder) to decide what the action (member function) heat means when applied to the oven (the particular instance of class Oven) or any of its variations (subclasses), such as a microwave oven (Microwave). This is the way people think, and designing a language along the lines of the way people think allows the programming model to more accurately describe the real world.

How Polymorphism Works

Any given language could support early or late binding upon its whim. Older languages like C tend to support early binding alone. Recent languages like Java only support late binding. As a fence straddler between the two, C++ supports both early and late binding.

You may be surprised that the default for C++ is early binding. The reason is simple, if a little dated. First, C++ has to act as much like C as possible by default to retain upward compatibility with its predecessor. Second, polymor­ phism adds a small amount of overhead to each and every function call both

Chapter 21: Examining Virtual Member Functions: Are They for Real? 275

in terms of data storage and code needed to perform the call. The founders of C++ were concerned that any additional overhead would be used as a reason not to adopt C++ as the system’s language of choice, so they made the more efficient early binding the default.

One final reason is that it can be useful as a programmer of a given class to decide whether you want a given member function to be overridden at some time in the future. This argument is strong enough that Microsoft’s new C# language also allows the programmer to flag a function as not overridable (however, the default is overridable).

To make a member function polymorphic, the programmer must flag the function with the C++ keyword virtual, as shown in the following modifica­ tion to the declaration in the OverloadOveride program:

class Student

{

public:

virtual float calcTuition()

{

cout << “We’re in Student::calcTuition” << endl; return 0;

}

};

The keyword virtual that tells C++ that calcTuition() is a polymorphic member function. That is to say, declaring calcTuition() virtual means that calls to it will be bound late if there is any doubt as to the runtime type of the object with which calcTuition() is called.

In the example OverloadOverride program at the beginning of this chapter, fn() is called through the intermediate function test(). When test() is passed a Base class object, b.fn() calls Base::fn(). But when test() is passed a SubClass object, the same call invokes SubClass::fn().

Executing the OverloadOveride program with calcTuition() declared vir­ tual generates the following output:

We’re in Student::calcTuition

We’re in GraduateStudent::calcTuition

Press any key to continue . . .

If you’re comfortable with the debugger that comes with your C++ environ­ ment, you really should single-step through this example.

You only need to declare the function virtual in the base class. The “virtual­ ness” is carried down to the subclass automatically. In this book, however, I follow the coding standard of declaring the function virtual everywhere (virtually).