Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]-1

.pdf
Скачиваний:
108
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

Class Inheritance and Virtual Functions

Direct base of CCan

class CContainer

Direct base of CBox

 

 

Indirect base of CGlassBox

More General

Direct base of CGlassBox

class CCan

class CBox

class CGlassBox

More Specialized

Figure 9-6

The class CGlassBox is derived from the CBox class exactly as before, but we omit the derived class version of ShowVolume() to show that the base class version still propagates through the derived classes. With the class hierarchy shown above, the class CContainer is an indirect base of the class CGlassBox, and a direct base of the classes CBox and CCan.

The GlassBox.h header file for the example contains:

// GlassBox.h for Ex9_11

 

#pragma once

 

#include “Box.h”

// For CBox

class CGlassBox: public CBox

// Derived class

{

 

public:

 

//Function to calculate volume of a CGlassBox

//allowing 15% for packing

virtual double Volume() const

{ return 0.85*m_Length*m_Width*m_Height; }

// Constructor

CGlassBox(double lv, double wv, double hv): CBox(lv, wv, hv){}

};

509

Chapter 9

The Container.h, Can.h, and Box.h header files contains the same code as those in the previous example, Ex9_10.

The source file for new example, with an updated function main() to use the additional class in the hierarchy, is as follows:

//Ex9_11.cpp

//Using an abstract class with multiple levels of inheritance

#include “Box.h”

// For CBox and CContainer

#include “Can.h”

// For CCan (and CContainer)

#include “GlassBox.h”

// For CGlassBox (and CBox and CContainer)

#include <iostream>

// For stream I/O

using std::cout; using std::endl;

const double PI = 3.14159265; // Global definition for PI

int main()

{

// Pointer to abstract base class initialized with CBox object address CContainer* pC1 = new CBox(2.0, 3.0, 4.0);

CCan myCan(6.5, 3.0);

//

Define

CCan object

CGlassBox myGlassBox(2.0, 3.0, 4.0); //

Define

CGlassBox object

pC1->ShowVolume();

//

Output the volume of CBox

delete pC1;

// Now clean up the free store

// initialized with address of CCan object

 

pC1 = &myCan;

// Put myCan address in pointer

pC1->ShowVolume();

// Output the volume of CCan

pC1 = &myGlassBox;

// Put myGlassBox address in pointer

pC1->ShowVolume();

// Output the volume of CGlassBox

cout << endl; return 0;

}

How It Works

You have the three-level class hierarchy shown in Figure 9-6 with CContainer as an abstract base class because it contains the pure virtual function, Volume(). The main()function now calls the ShowVolume()function three times using the same pointer to the base class, but with the pointer containing the address of an object of a different class each time. Because ShowVolume() is not defined in any of the derived classes you have here, the base class version is called in each instance. A separate branch from the base CContainer defines the derived class CCan.

The example produces this output:

CBox usable volume is 24

Volume is 45.9458

CBox usable volume is 20.4

510

Class Inheritance and Virtual Functions

The output shows that one of the three different versions of the function Volume() is selected for execution according to the type of object involved.

Note that you must delete the CBox object from the free store before you assign another address value to the pointer. If you don’t do this, you won’t be able to clean up the free store, because you would have no record of the address of the original object. This is an easy mistake to make when reassigning pointers and using the free store.

Virtual Destructors

One problem that arises when dealing with objects of derived classes using a pointer to the base class is that the correct destructor may not be called. You can see this effect by modifying the last example.

Try It Out

Calling the Wrong Destructor

You just need to add a destructor to each of the classes in the example that outputs a message so that you can track which destructor is called when the objects are destroyed. The Container.h file for this example is:

// Container.h for Ex9_12 #pragma once

#include <iostream> using std::cout; using std::endl;

class CContainer

// Generic base class for specific containers

{

 

public:

 

//Destructor ~CContainer()

{ cout << “CContainer destructor called” << endl; }

//Function for calculating a volume - no content

//This is defined as a ‘pure’ virtual function, signified by ‘= 0’ virtual double Volume() const = 0;

//Function to display a volume

virtual void ShowVolume() const

{

cout << endl

<< “Volume is “ << Volume();

}

};

The contents of Can.h in the example is:

// Can.h for Ex9_12 #pragma once

#include “Container.h” // For CContainer definition extern const double PI;

class CCan: public CContainer

511

Chapter 9

{

public:

//Destructor ~CCan()

{ cout << “CCan destructor called” << endl; }

//Function to calculate the volume of a can virtual double Volume() const

{ return 0.25*PI*m_Diameter*m_Diameter*m_Height; }

//Constructor

CCan(double hv = 4.0, double dv = 2.0): m_Height(hv), m_Diameter(dv){}

protected:

double m_Height; double m_Diameter;

};

The contents of Box.h should be:

// Box.h for Ex9_12

 

#pragma once

 

#include “Container.h”

// For CContainer definition

#include <iostream>

 

using std::cout;

 

using std::endl;

 

class CBox: public CContainer

// Derived class

{

 

public:

 

//Destructor ~CBox()

{ cout << “CBox destructor called” << endl; }

//Function to show the volume of an object virtual void ShowVolume() const

{

cout << endl

<<“CBox usable volume is “ << Volume();

}

//Function to calculate the volume of a CBox object virtual double Volume() const

{ return m_Length*m_Width*m_Height; }

//Constructor

CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0) :m_Length(lv), m_Width(wv), m_Height(hv){}

protected:

double m_Length; double m_Width; double m_Height;

};

512

Class Inheritance and Virtual Functions

The GlassBox.h header file should contain:

// GlassBox.h for Ex9_12

 

#pragma once

 

#include “Box.h”

// For CBox

class CGlassBox: public CBox

// Derived class

{

 

public:

 

//Destructor ~CGlassBox()

{ cout << “CGlassBox destructor called” << endl; }

//Function to calculate volume of a CGlassBox

//allowing 15% for packing

virtual double Volume() const

{ return 0.85*m_Length*m_Width*m_Height; }

// Constructor

CGlassBox(double lv, double wv, double hv): CBox(lv, wv, hv){}

};

Finally, the source file Ex9_12.cpp for the program should be as follows:

//Ex9_12.cpp

//Destructor calls with derived classes

//using objects via a base class pointer

#include “Box.h”

// For CBox and CContainer

#include “Can.h”

// For CCan (and CContainer)

#include “GlassBox.h”

// For CGlassBox (and CBox and CContainer)

#include <iostream>

// For stream I/O

using std::cout;

 

using std::endl;

 

const double PI = 3.14159265;

// Global definition for PI

int main()

{

// Pointer to abstract base class initialized with CBox object address CContainer* pC1 = new CBox(2.0, 3.0, 4.0);

CCan myCan(6.5, 3.0);

// Define CCan object

CGlassBox myGlassBox(2.0, 3.0, 4.0);

// Define CGlassBox object

pC1->ShowVolume();

// Output the volume of CBox

 

cout << endl << “Delete CBox” << endl;

delete pC1;

// Now clean up the free store

 

 

pC1 = new CGlassBox(4.0, 5.0, 6.0);

// Create CGlassBox dynamically

pC1->ShowVolume();

// ...output its volume...

cout << endl << “Delete CGlassBox” << endl;

delete pC1;

// ...and delete it

pC1 = &myCan;

// Get myCan address in pointer

513

Chapter 9

pC1->ShowVolume();

// Output the volume of CCan

pC1 = &myGlassBox;

// Get myGlassBox address in pointer

pC1->ShowVolume();

// Output the volume of CGlassBox

cout << endl;

 

return 0;

 

}

 

How It Works

Apart from adding a destructor to each class that outputs a message to the effect that it was called, the only other change is a couple of additions to the function main(). There are additional statements to create a CGlassBox object dynamically, output its volume and then delete it. There is also a message displayed to indicate when the dynamically created CBox object is deleted. The output generated by this example is shown as follows:

CBox usable volume is 24

Delete CBox

CContainer destructor called

CBox usable volume is 102

Delete CGlassBox

CContainer destructor called

Volume is 45.9458

CBox usable volume is 20.4

CGlassBox destructor called

CBox destructor called

CContainer destructor called

CCan destructor called

CContainer destructor called

You can see from this that when you delete the CBox object pointed to by pC1, the destructor for the base class CContainer is called but there is no call of the CBox destructor recorded. Similarly, when the CGlassBox object that you added is deleted, again the destructor for the base class CContainer is called but not the CGlassBox or CBox destructors. For the other objects, the correct destructor calls occur with the derived class constructor being called first, followed by the base class constructor. For the first CGlassBox object created in a declaration, three destructors are called: first, the destructor for the derived class, followed by the direct base destructor and, finally, the indirect base destructor.

All the problems are with objects created in the free store. In both cases, the wrong destructor is called. The reason for this is that the linkage to the destructors is resolved statically, at compile time. For the automatic objects, there is no problem — the compiler knows what they are and arranges for the correct destructors to be called. With objects created dynamically and accessed through a pointer, things are different. The only information that the compiler has when the delete operation is executed is that the pointer type is a pointer to the base class. The type of object the pointer is actually pointing to is

unknown to the compiler because this is determined when the program executes. The compiler therefore simply ensures that the delete operation is set up to call the base class destructor. In a real application, this can cause a lot of problems, with bits of objects left strewn around the free store and possibly more serious problems, depending on the nature of the objects involved.

514

Class Inheritance and Virtual Functions

The solution is simple. You need the calls to be resolved dynamically — as the program is executed. You can organize this by using virtual destructors in your classes. As I said when I first discussed virtual functions, it’s sufficient to declare a base class function as virtual to ensure that all functions in any derived classes with the same name, parameter list and return type are virtual as well. This applies to destructors just as it does to ordinary member functions. You need to add the keyword virtual to the definition of the destructor in the class CContainer in Container.h so that the class definition is as follows:

class CContainer

// Generic base class for containers

{

 

public:

 

//Destructor virtual ~CContainer()

{ cout << “CContainer destructor called” << endl; }

//Rest of the class as before

};

Now the destructors in all the derived classes are automatically virtual, even though you don’t explicitly specify them as such. Of course, you’re free to specify them as virtual if you want the code to be absolutely clear.

If you rerun the example with this modification, it produces the following output:

CBox usable volume is 24

Delete CBox

CBox destructor called

CContainer destructor called

CBox usable volume is 102

Delete CGlassBox

CGlassBox destructor called

CBox destructor called

CContainer destructor called

Volume is 45.9458

CBox usable volume is 20.4

CGlassBox destructor called

CBox destructor called

CContainer destructor called

CCan destructor called

CContainer destructor called

As you can see, all the objects are now destroyed with a proper sequence of destructor calls. Destroying the dynamic objects produces the same sequence of destructor calls as the automatic objects of the same type in the program.

The question may arise in your mind at this point, can constructors be declared as virtual? The answer is no — only destructors and other member functions.

515

Chapter 9

It’s a good idea always to declare your base class destructor as virtual as a matter of course when using inheritance. There is a small overhead in the execution of the class destructors, but you won’t notice it in the majority of circumstances. Using virtual destructors ensures that your objects will be properly destroyed and avoids potential program crashes that might otherwise occur.

Casting Between Class Types

You have seen how you can store the address of a derived class object in a variable of a base class type so a variable of type CContainer* can store the address of a CBox object for example. So if you have an address stored in a pointer of type CContainer* can you cast it to type CBox*? Indeed you can and the dynamic_cast operator is specifically intended for this kind of operation. Here’s how it works:

CContainer* pContainer = new CGlassBox(2.0, 3.0, 4.0);

CBox* pBox = dynamic_cast<CBox*>( pContainer);

CGlassBox* pGlassBox = dynamic_cast<CGlassBox*>( pContainer);

The first statement stores the address of the CGlassBox object created on the heap in a base class pointer of type CContainer*. The second statement cast pContainer up the class hierarchy to type CBox*. The third statement casts the address in pContainer to its actual type, CGlassBox*.

You can apply the dynamic_cast operator to references as well as pointers. The difference between dynamic_cast and static_cast is that the dynamic_cast operator checks the validity of a cast at run-time whereas the static_cast operator does not. If a dynamic_cast operation is not valid, the result is null. The compiler relies on the programmer for the validity of a static_cast operation so you should always use dynamic_cast for casting up and down a class hierarchy and check for a null result if you want to avoid abrupt termination of your program as a result of using a null pointer.

Nested Classes

You can put the definition of one class inside the definition of another, in which case you have defined a nested class. A nested class has the appearance of being a static member of the class that encloses it and is subject to the member access specifiers, just like any other member of the class. If you place the definition of a nested class in the private section of the class, the class can only be referenced from within

the scope of the enclosing class. If you specify a nested class as public, the class is accessible from outside the enclosing class but the nested class name must be qualified by the outer class name in such circumstances.

A nested class has free access to all the static members of the enclosing class. All the instance members can be accessed through an object of the enclosing class type, or a pointer or reference to an object. The enclosing class can only access the public members of the nested class, but in a nested class that is private in the enclosing class the members are frequently declared as public to provide free access to the entire nested class from functions in the enclosing class.

A nested class is particularly useful when you want to define a type that is only to be used within another type, whereupon the nested class can be declared as private. Here’s an example of that:

516

Class Inheritance and Virtual Functions

// A push-down stack to store Box objects class CStack

{

private:

// Defines items to store in the stack struct CItem

{

 

CBox* pBox;

// Pointer to the object in this node

CItem* pNext;

// Pointer to next item in the stack or null

// Constructor

 

 

CItem(CBox* pB, CItem* pN): pBox(pB),

pNext(pN){}

};

 

 

CItem* pTop;

// Pointer to item that is at the top

public:

 

 

// Push a Box object on to the stack

 

 

void Push(CBox* pBox)

 

 

{

 

 

pTop = new CItem(pBox, pTop);

// Create new Item and make it the top

}

 

 

// Pop an object off the stack

 

 

CBox* Pop()

 

 

{

 

 

if(pTop == 0)

// If the stack is empty

return 0;

// return null

CBox* pBox = pTop->pBox;

// Get box from item

CItem* pTemp = pTop;

// Save address of the top Item

pTop = pTop->pNext;

// Make next item the top

delete pTemp;

// Delete old top Item from the heap

return pBox;

 

 

}

};

The CStack class defines a push-down stack for storing CBox objects. To be absolutely precise it stores pointers to CBox objects so the objects pointed to are still the responsibility of the code making use of the Stack class. The nested struct, CItem, defines the items that are held in the stack. I chose to define Item as a nested struct rather than a nested class because members of a struct are public by default. You could define CItem as a class and then specify the members as public so they can be accessed from the functions in the CStack class. The stack is implemented as a set of CItem objects where each CItem object stores a pointer to a CBox object plus the address of the next CItem object down in the stack. The Push() function in the CStack class pushes a CBox object on to the top of the stack and the Pop() function pops an object off the top of the stack.

Pushing an object on to the stack involves creating a new CItem object that stores the address of the object to be stored plus the address of the previous item that was on the top of the stack — this is null the first time you push an object on to the stack. Popping an object off the stack returns the address of the object in the item, pTop. The top item is deleted and the next item becomes the item at the top of the stack. Let’s see if it works.

517

Chapter 9

Try It Out

Using a Nested Class

This example uses CContainer, CBox, and CGlassBox classes from Ex9_12 so create an empty WIN32 console project, Ex9_13, and add the header files containing those class definitions to it. Then add Stack.h to the project containing the definition of the CStack class from the previous section, and add Ex9_13.cpp to the project with the following contents:

//Ex9_13.cpp

//Using a nested class to define a stack

#include “Box.h”

// For CBox and CContainer

#include “GlassBox.h”

// For CGlassBox (and CBox and CContainer)

#include “Stack.h”

// For the stack class with nested struct

Item

 

#include <iostream>

// For stream I/O

using std::cout;

 

using std::endl;

 

int main()

{

CBox* pBoxes[] = { new

CBox(2.0, 3.0,

4.0),

new

CGlassBox(2.0,

3.0, 4.0),

new

CBox(4.0, 5.0,

6.0),

new CGlassBox(4.0, 5.0, 6.0)

};

 

 

cout << “The array of boxes have the following volumes:”;

for (int i = 0 ; i<4 ; i++)

 

pBoxes[i]->ShowVolume();

// Output the volume of a box

cout << endl << endl

<<“Now pushing the boxes on the stack...”

<<endl;

CStack* pStack

= new CStack;

// Create the stack

for (int i = 0

; i<4 ; i++)

 

pStack->Push(pBoxes[i]);

 

cout << “Popping the boxes off the stack presents them in reverse order:”; for (int i = 0 ; i<4 ; i++)

pStack->Pop()->ShowVolume();

cout << endl; return 0;

}

The output from this example is:

The array of boxes have the following volumes:

CBox usable volume is 24

518