Chapter 9
Class Inheritance and Virtual Functions

  • How inheritance fits into object-oriented programming
  • How you define a new class in terms of an existing class
  • How you use the protected keyword
  • How a class can be a friend to another class
  • How to use virtual functions
  • What pure virtual functions are
  • What an abstract class is
  • When you should use a virtual destructor
  • How to define a conversion operator in a class
  • What a nested class is

You can find the wrox.com code downloads for this chapter on the Download Code tab at www.wrox.com/go/beginningvisualc. The code is in the Chapter 9 download and individually named according to the names throughout the chapter.

OBJECT-ORIENTED PROGRAMMING BASICS

As you have seen, a class is a data type that you define to suit your own application requirements. Classes define the objects to which your program relates. You program the solution to a problem in terms of the objects that are specific to the problem, using operations that work directly with those objects. You can define a class to represent something abstract, such as a complex number, which is a mathematical concept, or a truck, which is decidedly physical (especially if you run into one on the highway). So, as well as being a data type, a class can also define a set of real-world objects of a particular kind, at least to the degree necessary to solve a given problem.

You can think of a class as defining the characteristics of a particular group of things that are identified by a common set of parameters or properties and share operations that may be performed on or between them. The operations for objects of a given class type are defined by the class interface, which corresponds to the public function members of the class. The CBox class in the previous chapter is a good example — it defined a box in terms of its dimensions plus a set of public functions that you could apply to CBox objects to solve a problem.

Of course, there are many different kinds of boxes in the real world: there are cartons, coffins, candy boxes, and cereal boxes, to name but a few, and you will certainly be able to come up with many others. You can differentiate boxes by the kinds of things they hold, the materials from which they are made, and in a multitude of other ways; but even though there are many different kinds of boxes, they share some common characteristics — the essence of boxiness, perhaps. Therefore, you can visualize all kinds of boxes as being related to one another because, even though they have many differentiating features, they share some fundamental characteristics. You could define a general kind of box as having the generic characteristics of all boxes — perhaps just a length, a width, and a height. You could then add additional characteristics to the basic box type to differentiate a particular kind of box from the rest. You may also find that there are things you can do with one specific type of box that you can’t do with others.

It’s also possible that some objects may be the result of combining a particular kind of box with some other type of object: a box of candy or a crate of beer, for example. To accommodate this, you could define one kind of box as a generic box with basic “boxiness” characteristics and then specify another sort of box as a further specialization of that. Figure 9-1 illustrates an example of the kinds of relationships you might define between different sorts of boxes.

image

FIGURE 9-1

The boxes become more specialized as you move down the diagram, and the arrows run from a given box type to the one on which it is based. Figure 9-1 defines three kinds of boxes based on the generic type, CBox. It also defines beer crates as a refinement of crates designed to hold bottles.

Thus, a good way to approximate the real world relatively well is to define classes that are interrelated. A candy box can be considered to be a box with all the characteristics of a basic box, plus a few characteristics of its own. This precisely illustrates the relationship between classes when one class is defined based on another. A more specialized class has all the characteristics of the class on which it is based, plus a few characteristics of its own that identify what makes it special. Let’s look at how this works in practice.

INHERITANCE IN CLASSES

When you define one class based on an existing class, the new class is called a derived class. A derived class automatically contains all the data members of the class that you used to define it and, with some restrictions, the function members too. The class inherits the members of the class on which it is based.

The only members of a base class that are not inherited in a derived class are the destructor, the constructors, and any member functions overloading the assignment operator. All other members are inherited by a derived class. Of course, the reason for certain base members not being inherited is that a derived class always has its own constructors and destructor. If the base class has an assignment operator, the derived class provides its own version. When I say these functions are not inherited, I mean that they don’t exist as members of a derived class object. However, they still exist for the base class part of an object, as you will see.

What Is a Base Class?

A base class is any class that you use as a basis for defining another class. For example, if you define a class, B, directly in terms of a class, A, A is said to be a direct base class of B. In Figure 9-1, the CCrate class is a direct base class of CBeerCrate. When a class such as CBeerCrate is defined in terms of another class, CCrate, CBeerCrate is said to be derived from CCrate. Because CCrate is itself defined in terms of the class CBox, CBox is said to be an indirect base class of CBeerCrate. You’ll see how this is expressed in the class definition in a moment. Figure 9-2 illustrates the way in which base class members are inherited in a derived class.

image

FIGURE 9-2

Just because member functions are inherited doesn’t mean that you won’t want to replace them by new versions in the derived class, and, of course, you can do that when necessary.

Deriving Classes from a Base Class

We can define a simple CBox class with public data members:

// Header file Box.h in project Ex9_01
#pragma once
        
class CBox
{
public:
  double m_Length;
  double m_Width;
  double m_Height;
      
  explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0):
                         m_Length {lv}, m_Width {wv}, m_Height {hv} {}
};

Create an empty Win32 console project with the name Ex9_01 and save this code in a new header file in the project with the name Box.h. The #pragma once directive ensures the definition of CBox appears only once in a build. There’s a constructor in the class so that you can initialize objects when you create them. Suppose you need another class, CCandyBox, that defines objects that have the same characteristics as CBox objects but also have another data member — a pointer to a string that identifies the contents of the box. I’ll use a pointer here to demonstrate aspects of constructors and destructors in derived classes. In real-world code you should use std::string to store strings.

You can define CCandyBox as a derived class with CBox as the base class:

// Header file CandyBox.h in project Ex9_01
#pragma once
#include <cstring>                     // For strlen() and strcpy_s()
#include "Box.h"
 
class CCandyBox : CBox
{
public:
  char* m_Contents;
        
  explicit CCandyBox(const char* str = "Candy")               // Constructor
  {
    size_t length {strlen(str) + 1};
    m_Contents = new char[length];
    strcpy_s(m_Contents, length, str);
  }
 
  CCandyBox(const CCandyBox& box) = delete;
  CCandyBox& operator=(const CCandyBox& box) = delete;
        
  ~CCandyBox()                                                // Destructor
  { delete[] m_Contents; } 
};

Add this header file to the project Ex9_01. You need the #include directive for Box.h because you refer to the CBox class in the code. If you were to leave this directive out, CBox would be unknown to the compiler, so the code would not compile. The base class name, CBox, appears after the name of the derived class, CCandyBox, and is separated from it by a colon. In all other respects, it looks like a normal class definition. The new member, m_Contents is a pointer to a string so you need a constructor to initialize it and a destructor to release the memory for it. You also need an assignment operator to prevent shallow assignments, and a copy constructor; or if you don’t want them, define them as =delete. There’s a default value for the string describing the contents of a CCandyBox object in the constructor. Objects of type CCandyBox contain all the members of the base class, CBox, plus the additional data member, m_Contents.

Note the use of the strcpy_s() function that you first saw in Chapter 6. Here, there are three arguments — the destination for the copy operation, the length of the destination buffer, and the source. If both arrays were static — that is, not allocated on the heap — you could omit the second argument and just supply the destination and source pointers. This is possible because the strcpy_s() function is also available as a template function that can infer the length of the destination string buffer automatically. You can therefore call the function with just the destination and source strings as arguments when you are working with static destination string buffers.

ACCESS CONTROL UNDER INHERITANCE

The access to inherited members in a derived class needs to be looked at more closely. Let’s consider the status of the private members of a base class in a derived class.

There was a good reason to choose the version of the class CBox with public data members in the previous example, rather than the more secure version with private data members. Although private data members of a base class are also members of a derived class, they remain private to the base class in the derived class, so function members defined in the derived class cannot access them. They are only accessible in the derived class through function members of the base class that are not private. You can demonstrate this very easily by changing all the CBox class data members to private and putting a volume() function in the derived class, CCandyBox:

// Version of the classes that will not compile
#include <cstring>                     // For strlen() and strcpy_s()
 
class CBox
{
public:
  explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0):
                          m_Length {lv}, m_Width {wv}, m_Height {hv} {}
     
private:
  double m_Length;
  double m_Width;
  double m_Height;
};
        
class CCandyBox : public CBox
{
public:
  char* m_Contents;
      
  // Function to calculate the volume of a CCandyBox object
  double volume() const              // Error - members not accessible
  { return m_Length*m_Width*m_Height; }
      
  // Rest of the code as before...
};

A program using these classes does not compile. The volume()function in CCandyBox attempts to access the private members of the base class, which is not legal, so the compiler will flag each instance with error C2248.

Constructor Operation in a Derived Class

Although I said that base class constructors are not inherited in a derived class, they still exist in the base class and are used to create the base part of a derived class object. This is because creating the base class part of a derived class object is really the business of a base class constructor, not the derived class constructor. After all, you have seen that private members of a base class are inaccessible in a derived class object, even though they are inherited, so responsibility for these has to lie with the base class constructors.

The default base class constructor was called by default in the last example to create the base part of a derived class object, but this doesn’t have to be the case. You can call a particular base class constructor from a derived class constructor. This enables you to initialize the base class data members with a constructor other than the default, or, indeed, to choose to call a particular base class constructor, depending on the data supplied to the derived class constructor.

Declaring Protected Class Members

In addition to the public and private access specifiers for members of a class, you can also declare members as protected. The protected keyword has the same effect as the private keyword within a class: members that are protected can only be accessed by member functions of the class, and by friend functions of the class (also by member functions of a friend class — you will learn about friend classes later in this chapter). Base class members that are protected can be accessed from any derived class function. Using the protected keyword, you could redefine CBox as follows:

// Box.h in Ex9_04
#pragma once
#include <iostream>
        
class CBox
{
public:
  // Base class constructor
  explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0):
                       m_Length {lv}, m_Width {wv}, m_Height {hv}
  {  std::cout << "CBox constructor called" << std::endl;  }
      
  // CBox destructor - just to track calls
  ~CBox()
  { std::cout << "CBox destructor called" << std::endl; }

protected:
  double m_Length;
  double m_Width;
  double m_Height;
};

The data members are still effectively private, in that they can’t be accessed by ordinary global functions, but they can still be accessed by member functions of a derived class.

The Access Level of Inherited Class Members

If you have no access specifier for the base class in the definition of a derived class, the default specification is private. This has the effect of causing the inherited public and protected members of the base class to be private in the derived class. The private members of the base class remain private to the base and, therefore, inaccessible in the derived class. In fact they remain private to the base class regardless of how the base class is specified in the derived class definition.

Specifying a base class as public gives base class members the same access level in the derived class as they had in the base, so public members remain public, and protected members remain protected.

The last possibility is that you declare a base class as protected. This makes the inherited public members of the base protected in the derived class. The protected and private base members retain their original access level in the derived class. This is summarized in Figure 9-3, which shows classes CABox, CBBox, and CCBox derived from CBox.

image

FIGURE 9-3

This may look a little complicated, but you can reduce it to the following three rules for inherited members of a derived class:

  • private members of a base class are never accessible in a derived class.
  • Defining a base class as public doesn’t change the access level of its members in a derived class.
  • Defining a base class as protected changes its public members to protected in a derived class.

Being able to change the access level of inherited members in a derived class gives you a degree of flexibility, but don’t forget that you cannot relax the level specified in the base class; you can only make the access level more stringent. This suggests that base classes need to have public members if you want to be able to vary the access level in derived classes. This may seem contrary to the idea of encapsulating data in a class in order to protect it from unauthorized access, but, as you’ll see, it is often the case that you define base classes that only act as a base for other classes and aren’t intended to be used for instantiating objects in their own right.

THE COPY CONSTRUCTOR IN A DERIVED CLASS

Remember that the copy constructor is called automatically when you define an object that is initialized with an object of the same class. Look at these statements:

CBox myBox {2.0, 3.0, 4.0};            // Calls constructor
CBox copyBox {myBox};                  // Calls copy constructor

The first statement calls the constructor that accepts three arguments of type double, and the second calls the copy constructor. If you don’t define a copy constructor, the compiler supplies one that copies the initializing object member by member to the corresponding members of the new object. So that you can see what is going on during execution, you can add your own version of a copy constructor to the CBox class. You can then use this class as a base for defining the CCandyBox class:

// Box.h in Ex9_05
#pragma once
#include <iostream>
        
class CBox                   // Base class definition
{
public:
  // Base class constructor
  explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0):
                       m_Length {lv}, m_Width {wv}, m_Height {hv}
  {  std::cout << "CBox constructor called" << std::endl;  }
      
  // Copy constructor
  CBox(const CBox& initB)
  {
    std::cout << "CBox copy constructor called" << std::endl;
    m_Length = initB.m_Length;
    m_Width = initB.m_Width;
    m_Height = initB.m_Height;
  }
 
  // CBox destructor - just to track calls
  ~CBox()
  { std::cout << "CBox destructor called" << std::endl; }
 
protected:
  double m_Length;
  double m_Width;
  double m_Height;
};

Don’t forget that a copy constructor must have its parameter specified as a reference to avoid the infinite number of calls to itself that would otherwise result from copying an argument by value. When the copy constructor in our example is called, it outputs a message, so you can see from the output when this is happening. You need to add a similar copy constructor to the CCandyBox class.

PREVENTING CLASS DERIVATION

Circumstances can arise where you want to be sure that your class cannot be used as a base class. You can do this by specifying your class as final. Here’s how you could prevent derivation from the CBox class:

class CBox final
{
  // Class details as before...
};

The final modifier following the class name tells the compiler that derivation from the CBox class is not to be allowed. If you modify the CBox class in Ex9_05 in this way, the code will not compile.

Note that final is not a keyword; it just has a special meaning in context. You are not allowed to use a keyword as a name, whereas you could use final as the name for a variable, for example.

CLASS MEMBERS AS FRIENDS

You saw in Chapter 7 how a function can be declared as a friend of a class. This gives the friend function the privilege of free access to any of the class members. Of course, there is no reason why a friend function cannot be a member of another class.

Suppose you define a CBottle class to represent a bottle:

// Bottle.h
#pragma once
        
class CBottle
{
public:
  CBottle(double height, double diameter) :
   m_Height {height}, m_Diameter {diameter} {}
      
private:
  double m_Height;                        // Bottle height
  double m_Diameter;                      // Bottle diameter
};

You now need a class to represent the packaging for a dozen bottles that automatically has custom dimensions to accommodate a particular kind of bottle. You might define this as the following — although this won’t compile as it is:

// Carton.h
#pragma once
class CBottle;                            // Forward declaration
        
class CCarton
{
public:
  CCarton(const CBottle& aBottle)
  {
    m_Height = aBottle.m_Height;          // Bottle height
    m_Length = 4.0*aBottle.m_Diameter;    // Four rows of ...
    m_Width = 3.0*aBottle.m_Diameter;     // ...three bottles
  }
      
private:
  double m_Length;                        // Carton length
  double m_Width;                         // Carton width
  double m_Height;                        // Carton height
};

We now have two class definitions that each reference the other class type. The forward declaration for the CBottle class in Carton.h is essential; without it the compiler won’t know what CBottle refers to. Forward declarations are always needed to resolve cyclic references between two or more classes. The CCarton constructor sets the height to be the same as that of the bottle it is to accommodate, and the length and width are set based on the diameter of the bottle so that 12 fit in the box. As you know by now, this won’t work. The data members of the CBottle class are private, so the CCarton constructor can’t access them. As you also know, a friend declaration in the CBottle class fixes it:

// Bottle.h
#pragma once;
class CCarton;                            // Forward declaration
        
class CBottle
{
public:
  CBottle(double height, double diameter) :
   m_Height {height}, m_Diameter {diameter} {}
      
private:
  double m_Height;                        // Bottle height
  double m_Diameter;                      // Bottle diameter
      
// Let the carton constructor in
friend CCarton::CCarton(const CBottle& aBottle);
};

The only difference between the friend declaration here and what you saw in Chapter 7 is that you must put the class name and the scope resolution operator with the friend function name to identify it. You must have a forward declaration for the CCarton class because the friend function refers to it.

You might think that this will compile correctly, but there’s a problem. You have put a forward declaration of the CCarton class in the CBottle class and vice versa to resolve the cyclic dependency, but this still won’t allow the classes to compile. The problem is with the CCarton constructor. This appears within the CCarton class definition and the compiler cannot compile this function without having first compiled the CBottle class. On the other hand, it can’t compile the CBottle class without having compiled the CCarton class. The only way to resolve this is to put the CCarton constructor definition in a .cpp file, thus removing the need to compile it when the CCarton class is compiled. The header file holding the CCarton class definition will be:

// Carton.h
#pragma once
class CBottle;                            // Forward declaration
        
class CCarton
{
public:
  CCarton(const CBottle& aBottle);
      
private:
  double m_Length;                        // Carton length
  double m_Width;                           // Carton width
  double m_Height;                          // Carton height
};

The contents of the Carton.cpp file will be:

// Carton.cpp
#include "Carton.h"
#include "Bottle.h"
        
CCarton::CCarton(const CBottle& aBottle)
{
  m_Height = aBottle.m_Height;              // Bottle height
  m_Length = 4.0*aBottle.m_Diameter;        // Four rows of ...
  m_Width = 3.0*aBottle.m_Diameter;         // ...three bottles
}

Now, the compiler can compile both class definitions and the carton.cpp file.

Friend Classes

You can allow all the member functions of one class to have access to all the data members of another by declaring it as a friend class. You could define the CCarton class as a friend of the CBottle class by adding a friend declaration within the CBottle class definition:

friend CCarton;

With this declaration in the CBottle class, all function members of the CCarton class have free access to all the data members of the CBottle class.

Limitations on Class Friendship

Class friendship is not reciprocated. Making the CCarton class a friend of the CBottle class does not mean that the CBottle class is a friend of the CCarton class. If you want this to be so, you must add a friend declaration for the CBottle class to the CCarton class.

Class friendship is also not inherited. If you define another class with CBottle as a base, members of the CCarton class will not have access to its data members, not even those inherited from CBottle.

VIRTUAL FUNCTIONS

Let’s look more closely at the behavior of inherited member functions and their relationship with derived class member functions. You could add a function to the CBox class to output the volume of a CBox object. The simplified class then becomes:

// Box.h in Ex9_06
#pragma once
#include <iostream>
        
class CBox                             // Base class
{
public:
  // Function to show the volume of an object
  void showVolume() const
  { std::cout << "CBox usable volume is " << volume() << std::endl; }
      
  // Function to calculate the volume of a CBox object
  double volume() const
  { return m_Length*m_Width*m_Height; }
      
  // Constructor
  explicit 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;
};

Now, you can output the usable volume of a CBox object just by calling the showVolume() function for any object for which you require it. The constructor sets the data member values in the initialization list, so no statements are necessary in its body. The data members are protected so they are accessible to the member functions of any derived class.

Suppose you want to derive a class for a different kind of box called CGlassBox, to hold glassware. The contents are fragile, and because packing material is added to protect them, the capacity of the box is less than the capacity of a basic CBox object. You therefore need a different volume() function to account for this, so you add it to the derived class:

// GlassBox.h in Ex9_06
#pragma once
#include "Box.h"
      
class CGlassBox : public CBox           // Derived class
{
public:
  // Function to calculate volume of a CGlassBox
  // allowing 15% for packing
  double volume() const
  { return 0.85*m_Length*m_Width*m_Height; }
      
  // Constructor
  CGlassBox(double lv, double wv, double hv): CBox {lv, wv, hv} {}
};

There could be other members of the derived class, but we’ll keep it simple and concentrate on how the inherited functions work for the moment. The constructor for the derived class calls the base constructor in its initialization list to set the data member values. No statements are necessary in its body. You have a new version of the volume() function to replace the version from the base class, the idea being that you can get the inherited function showVolume() to call the derived class version of the member function volume() when you call it for a CGlassBox object.

What Is a Virtual Function?

A virtual function is a function in a base class that is declared using the keyword virtual. If you specify a function in a base class as virtual and the function is redefined in a derived class, it signals to the compiler that you don’t want early binding for it. What you do want is the function to be called at any given point in the program to be chosen based on the kind of object for which it is called.

Ensuring Correct Virtual Function Operation

As I said in the previous section, for a function to behave as virtual, it must have the same name, parameter list, and return type in any derived class as a function in the base class. It’s not difficult to make a mistake though. If you forget to specify volume()as const in CGlassBox in Ex9_07, the program will still compile — it just won’t work correctly. When the function in the derived class has a different signature from the function in the base class it is supposed to be overriding, you are not overriding the base function at all. You can tell the compiler that a virtual function in a derived class is overriding a virtual function in a base class by using the override modifier. You could do this for the volume() function in CGlassBox in Ex9_07 like this:

  class CGlassBox : public CBox           // Derived class
  {
  public:
    // Function to calculate volume of a CGlassBox allowing 15% for packing
    virtual double volume() const override
    { return 0.85*m_Length*m_Width*m_Height; }
        
    // Constructor
    CGlassBox(double lv, double wv, double hv): CBox {lv, wv, hv} {}
  };

Now the compiler will check that there is a base class volume() function with the same signature. If there isn’t, you will get an error message. You can demonstrate this by changing the definition of volume() in CGlassBox by adding the override modifier and omitting the const keyword.

If you always use the override modifier with virtual functions in derived classes, you are guaranteed that any mistakes in specifying the overrides will be reported by the compiler. Note that like the final modifier, override is not a keyword. It just has special meaning in context.

Preventing Function Overriding

You may want to prevent a member function being overridden. This could be because you want to preserve a particular aspect of behavior. In this case you can specify a member function as final. For example, you could specify that the volume() member CBox class in Ex9_07 is not to be overridden like this:

  class CBox                             // Base class
  {
  public:
    // Class definition as before....
        
    // Function to calculate the volume of a CBox object
    virtual double volume() const final
    { return m_Length*m_Width*m_Height; }
        
    // Rest of the class as before...
  };

The final modifier tells the compiler that the volume() function must not be overridden. With this amendment in Ex9_07 the compiler will flag the volume() function in the derived class as an error.

Using Pointers to Class Objects

Using pointers to base class and derived class objects is an important technique. You can use a pointer to a base class type to store the address of a derived class object, as well as that of a base class object. Thus you can use a pointer of type “pointer to base” to obtain different behavior with virtual functions, depending on what type of object the pointer is pointing to. You’ll see more clearly how this works by looking at an example.

Using References with Virtual Functions

If you define a function with a parameter that is a reference to a base class type, you can pass an object of a derived class type to it. When the function executes, the appropriate virtual function for the object passed as the reference argument is selected automatically. You can see this happening by modifying main() in the previous example to call a function that has a reference parameter.

Pure Virtual Functions

It’s possible that you’d want to include a virtual function in a base class so that it may be redefined in a derived class and thus get polymorphic behavior with derived class objects, but there is no meaningful definition for the function in the base class. For example, you might have a CContainer class, which could be a base for defining the CBox class, or a CBottle class, or even a CTeapot class. The CContainer class wouldn’t have data members, but you might want to provide volume()as a virtual member function to allow it to be called polymorphically for any derived class object. Because CContainer has no data members and therefore no container dimensions, there is no sensible definition for the volume() function. However, you can still define the class including volume() like this:

// Container.h for Ex9_10
#pragma once
#include <iostream>
        
class CContainer        // Generic base class for specific containers
{
public:
  // 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
  {  std::cout << "Volume is " << volume() << std::endl;  }
};

The statement for the virtual function volume() defines it as having no content by placing the equals sign and zero in the function header. This is called a pure virtual function. The class also contains the showVolume() function that displays the volume of derived class objects. Because this function is virtual, it can be replaced in a derived class but if it isn’t, this inherited base class version is called for derived class objects.

Abstract Classes

A class that contains a pure virtual function is called an abstract class. It’s called abstract because you can’t define objects of a class that contains a pure virtual function. However, you can define pointers and references of an abstract class type. An abstract class exists only for the purpose of deriving classes from it. If a class that is derived from an abstract class does not define a pure virtual function that is inherited from the base class, then it is also an abstract class.

You should not conclude from the example of the CContainer class that an abstract class can’t have data members. An abstract class can have both data members and member functions. The presence of a pure virtual function is the only condition that makes a class abstract. An abstract class can have several pure virtual functions. In this case a derived class must define every pure virtual function inherited from its base, otherwise it too will be an abstract class. If you forget to make the derived class version of the volume() function const, the derived class will still be abstract because it contains the pure virtual volume() function that is const, as well as the non-const version. const and non-const functions are always differentiated.

Indirect Base Classes

At the beginning of this chapter, I said that a base class for a given class could, in turn, be derived from another, “more” base class. A small extension of the last example will illustrate this, as well as demonstrating the use of a virtual function across a second level of inheritance.

Virtual Destructors

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

CASTING BETWEEN CLASS TYPES

You have seen how you can store the address of a derived class object in a variable that is a pointer to 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 casts pContainer down 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 dynamic_cast 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 nullptr. 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 nullptr result if you want to avoid abrupt termination of your program.

Defining Conversion Operators

You can define operator functions in a class that convert an object to another type. The conversion can be to a fundamental type or a class type. For example, suppose you want to test whether a CBox object has dimensions other than the defaults of 1. You could provide for this by defining an operator function for CBox objects for conversion to type bool. For example, you could define the following member within the CBox class definition:

operator bool()
{  return m_Length == 1 && m_Width == 1 && m_Height == 1;  }

This defines the function operator bool(). The function returns true when all the dimensions of the CBox object are 1 and false otherwise. The name of an operator function for conversion is always the operator keyword followed by the destination type name. The destination type in the function name is the return type, so no return type needs to be specified in addition.

With the operator bool() function defined in the CBox class you could write this:

CBox box1;                            // Calls default constructor
if(box1)                              // Implicit conversion of box1 to bool
  std::cout << "box1 has default dimensions." << std::endl;

The if expression has to be type bool so the compiler will insert a call of the operator bool() function for box1 to make the if expression box1.operator bool().

You can also write the following:

CBox box2 {1, 2, 3};
bool isDefault {true};
isDefault = box2;                     // Implicit conversion to bool

Assigning the value of box2 to isDefault also requires an implicit conversion so the operator function call will be inserted. Of course, you can write explicit conversions, too:

isDefault = static_cast<bool>(box1);   // Explicit conversion

This statement also calls operator bool() so it is equivalent to:

isDefault = box1.operator bool();

Explicit Conversion Operators

It may be that you do not want to allow implicit conversions that use a conversion operator function. This is particularly the case for conversions between class types. You can prevent this by prefixing the conversion operator function with the explicit keyword. Now compilation of any statement requiring an implicit type conversion will fail with an error message.

Only explicit conversion will compile correctly.

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 a nested class definition in the private section of a 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 allow functions in the enclosing class free access to the entire nested class.

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

// A push-down stack to store CBox objects
#pragma once
class CBox;                           // Forward class declaration
 
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:
  CStack()=default;                   // Constructor
 
  // Inhibit copy construction and assignment
  CStack(const CStack& stack) = delete;
  CStack& operator=(const CStack& stack) = delete;
        
  // Push a Box object onto 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)                         // If the stack is empty
      return nullptr;                 // 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;
  }
        
  // Destructor
  virtual ~CStack()
  {
    CItem* pTemp {};
    while(pTop)                       // While pTop not null
    {
      pTemp = pTop;
      pTop = pTop->pNext;
      delete pTemp;
    }
  }
};

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 using the CStack class. The nested struct, CItem, defines the items that are held in the stack. I chose to define CItem 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 CStack. The stack is implemented as a linked list of CItem objects, where each object stores a pointer to a CBox object plus the address of the next CItem object down in the stack. The push() function in CStack pushes a CBox object onto the stack, and the pop() function pops an object off the stack.

Pushing an object onto the stack creates a new CItem object holding the address of the object to be stored plus the address of the previous top item. The top item is nullptr initially. Popping an object off the stack returns the address of the object in pTop. The top item is deleted and the next item becomes the top of the stack.

Because a CStack object creates CItem objects on the heap, we need a destructor to delete any remaining CItem objects when a CStack object is destroyed. The process works down through the stack, deleting the top item after the address of the next item has been saved in pTop. Let’s see if it works.

SUMMARY

This chapter covered the principal ideas involved in using inheritance.

You have now gone through all of the important language features of C++. It’s important that you feel comfortable with the mechanisms for defining and deriving classes and the process of inheritance. Windows programming with Visual C++ involves extensive use of all these concepts.

EXERCISES

  1. What’s wrong with the following code?
    class CBadClass
    {
    private:
       int len;
       char* p;
    public:
       CBadClass(const char* str): p {str}, len {strlen(p)} {}
       CBadClass(){}
    };
  2. Suppose you have a CBird class that you want to use as a base class for deriving a hierarchy of bird classes:
    class CBird
    {
    protected:
       int wingSpan {};
       int eggSize {};
       int airSpeed {};
       int altitude {};
    public:
       virtual void fly() { altitude = 100; }
    };
    1. Is it reasonable to create a CHawk by deriving from CBird? How about a COstrich? Justify your answers. Derive an avian hierarchy that can cope with both of these birds.
  3. Given the following class,
    class CBase
    {
    protected:
       int m_anInt;
    public:
       CBase(int n): m_anInt {n} { std::cout << "Base constructor
    "; }
       virtual void print() const = 0;
    };
    1. what sort of class is CBase and why? Derive a class from CBase that sets the value of m_anInt when an object is created and prints it on request. Write a test program to verify that your class is correct.
  4. A binary tree is a structure made up of nodes, where each node contains a pointer to a “left” node and a pointer to a “right” node plus a data item, as shown in Figure 9-6.
    image

    FIGURE 9-6

    1. The tree starts with a root node, and this is the starting point for accessing the nodes in the tree. Either or both pointers in a node can be nullptr. Figure 9-6 shows an ordered binary tree, which is a tree organized so that the value of each node is always greater than or equal to the left node and less than or equal to the right node.
    2. Define a class that defines an ordered binary tree that stores integer values. You also need to define a Node class, but that can be an inner class to the BinaryTree class. Write a program to test the operation of your BinaryTree class by storing an arbitrary sequence of integers in it and retrieving and outputting them in ascending sequence.
    3. Hint: Don’t be afraid to use recursion.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC CONCEPT
Inherited members of a class A derived class inherits all the members of a base class except for constructors, the destructor, and the overloaded assignment operator.
Accessibility of inherited members of a class Members of a base class declared as private in the base class are not accessible in any derived class. To obtain the effect of the keyword private but allow access in a derived class, you should use the keyword protected in place of private.
Access specifiers for a base class A base class can be specified for a derived class with the keyword public, private, or protected. If none is specified, the default is private. Depending on the keyword specified for a base, the access level of the inherited members may be modified.
Constructors in derived classes If you write a derived class constructor, you must arrange for data members of the base class to be initialized properly, as well as those of the derived class.
Virtual functions A function in a base class may be declared as virtual. This allows other definitions of the function appearing in derived classes to be selected at execution time, depending on the type of object for which the function call is made.
Using override When you define a virtual function in a derived class with the override modifier specified, the compiler will verify that a direct or indirect base class contains a virtual function with the same signature, and will issue an error message if this is not the case.
Final function members of a class If a member function of a class is specified using the final modifier, a derived class cannot override the function. Any attempt to override the function will result in a compiler error message.
Final classes A class that is final cannot be used as a base for another class. Attempting to use a final class as a base will result in a compiler error message.
Virtual destructors You should declare class destructors as virtual in a class that can be a base for other classes. This ensures correct selection of a destructor for dynamically-created derived class objects.
friend classes A class may be designated as a friend of another class. In this case, all the member functions of the friend class may access all the members of the other class. If class A is a friend of B, class B is not a friend of A unless it has been declared as such.
Pure virtual functions A virtual function in a base class can be specified as pure by placing =0 at the end of the function declaration. The class then is an abstract class for which no objects can be created. In any derived class, all pure virtual functions inherited from the base class must be defined; if not, the derived class is abstract.
..................Content has been hidden....................

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