
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
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