protected
keywordYou 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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.public
doesn’t change the access level of its members in a derived class.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.
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.
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.
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.
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.
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
.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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();
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.
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.
This example uses the 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 CItem
#include <iostream> // For stream I/O
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}
};
std::cout << "The boxes have the following volumes:
";
for (const CBox* pBox : pBoxes)
pBox->showVolume(); // Output the volume of a box
std::cout << "
Now pushing the boxes on the stack...
";
CStack stack; // Create the stack
for (CBox* pBox : pBoxes) // Store box pointers in the stack
stack.push(pBox);
std::cout << "Popping the boxes off the stack presents them in reverse order:
";
CBox* pTemp {};
while(pTemp = stack.pop())
pTemp->showVolume();
for (CBox* pBox : pBoxes) // Delete the boxes
delete pBox;
return 0;
}
I removed the output statements from the CContainer, CBox
, and CGlassBox
class destructors. The output from this example is:
The boxes have the following volumes:
CBox usable volume is 24
CBox usable volume is 20.4
CBox usable volume is 120
CBox usable volume is 102
Now pushing the boxes on the stack...
Popping the boxes off the stack presents them in reverse order:
CBox usable volume is 102
CBox usable volume is 120
CBox usable volume is 20.4
CBox usable volume is 24
An array of pointers to CBox
objects can store addresses of CBox
objects or addresses of any type that is derived from CBox
. The pBoxes
array is initialized with the addresses of four objects created on the heap:
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}
};
There are two CBox
objects and two CGlassBox
objects with the same dimensions as the CBox
objects.
You list the volumes of the objects in the pBoxes
array in a range-based for
loop. You then create a CStack
object and push the pointers to the objects onto the stack in another range-based for
loop:
CStack stack; // Create the stack
for (CBox* pBox : pBoxes) // Store box pointers in the stack
stack.push(pBox);
Each element in the pBoxes
array is pushed onto the stack by passing the element to the push()
member for the stack
object. This results in the first element from the array being at the bottom of the stack, and the last element at the top.
You pop the objects off the stack in a while
loop:
CBox* pTemp {};
while(pTemp = stack.pop())
pTemp->showVolume();
The pop()
function returns the address of the element at the top of the stack, and you use this to call the showVolume()
function for the object. The loop ends when the pop()
function returns nullptr
. Because the last element was at the top of the stack, the loop lists the volumes of the objects in reverse order. From the output, you can see that the CStack
class does, indeed, implement a stack using a nested struct
to define the items to be stored in the stack.
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.
class CBadClass
{
private:
int len;
char* p;
public:
CBadClass(const char* str): p {str}, len {strlen(p)} {}
CBadClass(){}
};
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; }
};
CHawk
by deriving from CBird
? How about a COstrich
? Justify your answers. Derive an avian hierarchy that can cope with both of these birds.class CBase
{
protected:
int m_anInt;
public:
CBase(int n): m_anInt {n} { std::cout << "Base constructor
"; }
virtual void print() const = 0;
};
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.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.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.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. |
52.15.135.175