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

Chapter 10

Building Classes with Inheritance

In Chapter 3, you learned that an “is-a” relationship recognizes the pattern that real-world objects tend to exist in hierarchies. In programming, that pattern becomes relevant when you need to write a class that builds on, or slightly changes, another class. One way to accomplish this aim is to copy code from one class and paste it into the other. By changing the relevant parts or amending the code, you can achieve the goal of creating a new class that is slightly different from the original. This approach, however, leaves an OOP programmer feeling sullen and slightly annoyed for the following reasons:

A bug fix to the original class will not be reflected in the new class because the two classes contain completely separate code.

The compiler does not know about any relationship between the two classes, so they are not polymorphic — they are not just different variations on the same thing.

This approach does not build a true is-a relationship. The new class is very similar to the original because it shares code, not because it really is the same type of object.

The original code might not be obtainable. It may exist only in a precompiled binary format, so copying and pasting the code might be impossible.

Not surprisingly, C++ provides built-in support for defining a true is-a relationship. The characteristics of C++ is-a relationships are described in the following section.

Extending Classes

When you write a class definition in C++, you can tell the compiler that your class is inheriting from, or extending, an existing class. By doing so, your class will automatically contain the data members and methods of the original class, which is called the parent class or superclass. Extending an existing class gives your class (which is now called a derived class or a subclass) the ability to describe only the ways in which it is different from the parent class.

To extend a class in C++, you specify the class you are extending when you write the class definition. To show the syntax for inheritance, we use two classes called Super and Sub. Don’t worry — more interesting examples are coming later. To begin, consider the following definition for the Super class.

class Super

{

public:

Super();

void someMethod();

protected:

int mProtectedInt;

private:

int mPrivateInt;

};

If you wanted to build a new class, called Sub, which inherits from Super, you would tell the compiler that Sub derives from Super with the following syntax:

224

Discovering Inheritance Techniques

class Sub : public Super

{

public:

Sub();

void someOtherMethod();

};

Sub itself is a full-fledged class that just happens to share the characteristics of the Super class. Don’t worry about the word public for now — its meaning is explained later in this chapter. Figure10-1 shows the simple relationship between Sub and Super. You can declare objects of type Sub just like any other object. You could even define a third class that subclasses Sub, forming a chain of classes, as shown in Figure 10-2.

Super

Sub

Figure 10-1

Super

Sub

SubSub

Figure 10-2

Sub doesn’t have to be the only subclass of Super. Additional classes can also subclass Super, effectively becoming siblings to Sub, as shown in Figure 10-3.

Super

Sub Foo

Figure 10-3

Clients’ View of Inheritance

To a client, or another part of your code, an object of type Sub is also an object of type Super because Sub inherits from Super. This means that all the public methods and data members of Super and all the public methods and data members of Sub are available.

225

Chapter 10

Code that uses the subclass does not need to know which class in your inheritance chain has defined a method in order to call it. For example, the following code calls two methods of a Sub object even though one of the methods was defined by the Super class.

Sub mySub;

mySub.someMethod();

mySub.someOtherMethod();

It is important to understand that inheritance only works in one direction. The Sub class has a very clearly defined relationship to the Super class, but the Super class, as written, doesn’t know anything about the Sub class. That means that objects of type Super do not support public methods and data members of Sub because Super is not a Sub.

The following code will not compile because the Super class does not contain a public method called someOtherMethod().

Super mySuper;

mySuper.someOtherMethod(); // BUG! Super doesn’t have a someOtherMethod().

From the perspective of other code, an object belongs to its defined class as well as to any superclasses.

A pointer or reference to an object can refer to an object of the declared class or any of its subclasses. This tricky subject is explained in detail later in this chapter. The concept to understand at this point is that a pointer to a Super can actually be pointing a Sub object. The same is true for a reference. The client can still access only the methods and data members that exist in Super, but through this mechanism, any code that operates on a Super can also operate on a Sub.

For example, the following code compiles and works just fine even though it initially appears that there is a type mismatch:

Super* superPointer = new Sub(); // Create a sub, and store it in a super pointer.

Subclass’s View of Inheritance

To the subclass itself, nothing much has changed in terms of how it is written or how it behaves. You can still define methods and data members on a subclass just as you would on a regular class. The previous definition of Sub declares a method called someOtherMethod(). Thus, the Sub class augments the Super class by adding an additional method.

A subclass can access public and protected methods and data members declared in its superclass as though they were its own, because technically, they are. For example, the implementation of someOtherMethod() on Sub could make use of the data member mProtectedInt, which was declared as part of Super. The following code shows this implementation. Accessing a superclass data member or method is no different than if the data member of method were declared as part of the subclass.

void Sub::someOtherMethod()

{

226

Discovering Inheritance Techniques

cout << “I can access the superclass data member mProtectedInt.” << endl; cout << “Its value is “ << mProtectedInt << endl;

}

When we introduced access specifiers (public, private, and protected) in Chapter 8, the difference between private and protected may have been confusing. Now that you understand subclasses, the difference should be clearer. If a class declares methods or data members as protected, subclasses have access to them. If they are declared as private, subclasses do not have access.

The following implementation of someOtherMethod() will not compile because the subclass attempts to access a private data member from the superclass.

void Sub::someOtherMethod()

{

cout << “I can access the superclass data member mProtectedInt.” << endl; cout << “Its value is “ << mProtectedInt << endl;

cout << “The value of mPrivateInt is “ << mPrivateInt << endl; // BUG!

}

The private access specifier gives you control over how a potential subclass could interact with your class. In practice, most data members are declared as protected, and most methods are either public or protected. The reason is that most of the time, you or someone you work with will be extending the class so you don’t want to shut out any potential uses by making methods or members private. Occasionally, the private specifier is useful to block subclasses from accessing potentially dangerous methods. It is also useful when writing classes that external or unknown parties will extend because you can block access to prevent misuse.

From the perspective of a subclass, all public and protected data members and methods from the superclass are available for use.

Overriding Methods

As you read in Chapter 3, the main reasons to inherit from a class are to add or replace functionality. The definition of Sub adds functionality to its parent class by providing an additional method, someOtherMethod(). The other method, someMethod(), is inherited from Super and behaves in the subclass exactly as it does in the superclass. In many cases, you will want to modify the behavior of a class by replacing, or overriding, a method.

How I Learned to Stop Worrying and Make Everything virtual

There is one small twist to overriding methods in C++ and it has to do with the keyword virtual. Only methods that are declared as virtual in the superclass can be overridden properly by subclasses. The keyword goes at the beginning of a method declaration as shown in the modified version of Super that follows.

class Super

{

public:

Super();

227

Chapter 10

virtual void someMethod();

protected:

int mProtectedInt;

private:

int mPrivateInt;

};

The virtual keyword has a few subtleties and is often cited as a poorly designed part of the language. A good rule of thumb is to just make all of your methods virtual. That way, you won’t have to worry about whether or not overriding the method will work. The only drawback is a small performance hit. The subtleties of the virtual keyword are covered toward the end of this chapter, and performance is discussed further in Chapter 17.

Even though it is unlikely that the Sub class will be extended, it is a good idea to make its methods virtual as well, just in case.

class Sub : public Super

{

public:

Sub();

virtual void someOtherMethod();

};

As a rule of thumb, make all your methods virtual (including the destructor, but not constructors) to avoid problems associated with omission of the virtual keyword.

Syntax for Overriding a Method

To override a method, you simply redeclare it in the subclass class definition exactly as it was declared in the superclass. In the subclass’s implementation file, you provide the new definition.

For example, the Super class contains a method called someMethod(). The definition of someMethod() is provided in Super.cpp and shown here:

void Super::someMethod()

{

cout << “This is Super’s version of someMethod().” << endl;

}

Note that you do not repeat the virtual keyword in front of the method definition.

If you wish to provide a new definition for someMethod() in the Sub class, you must first add it to the class definition for Sub, as follows:

class Sub : public Super

{

public:

Sub();

228

Discovering Inheritance Techniques

virtual void someMethod(); // Overrides Super’s someMethod() virtual void someOtherMethod();

};

The new definition of someMethod() is specified along with the rest of Sub’s methods.

void Sub::someMethod()

{

cout << “This is Sub’s version of someMethod().” << endl;

}

Clients’ View of Overridden Methods

With the preceding changes, other code would still call someMethod() the same way it did before. Just as before, the method could be called on an object of class Super or an object of class Sub. Now, however, the behavior of someMethod() will vary based on the class of the object.

For example, the following code works just as it did before, calling Super’s version of someMethod():

Super mySuper;

mySuper.someMethod(); // Calls Super’s version of someMethod().

The output of this code is:

This is Super’s version of someMethod().

If the code declares an object of class Sub, the other version will automatically be called.

Sub mySub;

mySub.someMethod(); // Calls Sub’s version of someMethod()

The output this time is:

This is Sub’s version of someMethod().

Everything else about objects of class Sub remains the same. Other methods that might have been inherited from Super will still have the definition provided by Super unless they are explicitly overridden in Sub.

As you learned earlier, a pointer or reference can refer to an object of a class or any of its subclasses. The object itself “knows” the class of which it is actually a member, so the appropriate method is called as long as it was declared virtual. For example, if you have a Super reference that refers to an object that is really a Sub, calling someMethod() will actually call the subclass’s version, as shown next. This aspect of overriding will not work properly if you omit the virtual keyword in the superclass.

Sub mySub;

Super& ref = mySub;

ref.someMethod();

// Calls Sub’s version of someMethod()

229