WHAT YOU WILL LEARN IN THIS CHAPTER:
Defining a new class in terms of an existing one
How to use the protected
keyword to define a new access specification for class members
How a class can be a friend to another class
How to use virtual functions
Pure virtual functions
Abstract classes
When to use virtual destructors
In this chapter, you're going to look into a topic that lies at the heart of object-oriented programming (OOP): class inheritance. Simply put, inheritance is the means by which you can define a new class in terms of one you already have. This is fundamental to programming in C++, so it's important that you understand how inheritance works.
As you have seen, a class is a data type that you define to suit your own application requirements. Classes in OOP also 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 be a definition of 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 specified by a common set of parameters and share a common set of operations that may be performed on them. The operations that you can apply to objects of a given class type are defined by the class interface, which corresponds to the functions contained in the public
section of the class definition. The CBox
class that you used 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 still visualize all kinds of boxes as actually being related to one another, even though they have many differentiating features. You could define a particular kind of box as having the generic characteristics of all boxes — perhaps just a length, a width, and a height. You could then add some additional characteristics to the basic box type to differentiate a particular kind of box from the rest. You may also find that there are new things you can do with your specific kind of box that you can't do with other boxes.
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 different kinds of boxes based on the generic type, CBox
. It also defines beer crates as a further refinement of crates designed to hold bottles.
Thus, a good way to approximate the real world relatively well using classes in C++ is through the ability 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 in C++ 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 referred to as 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 as well. The class is said to inherit the data members and function members of the class on which it is based.
The only members of a base class that are not inherited by a derived class are the destructor, the constructors, and any member functions overloading the assignment operator. All other function members, together with all the data members of a base class, 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 in the derived class with new versions, and, of course, you can do that when necessary.
Let's go back to the original CBox
class with public
data members that you saw at the beginning of the previous chapter:
// Header file Box.h in project Ex9_01 #pragma once class CBox { public: double m_Length; double m_Width; double m_Height; CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Length(lv), m_Width(wv), m_Height(hv){} };
Create a new 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 declare them. Suppose you now need another class of objects, CCandyBox
, that are the same as CBox
objects but also have another data member — a pointer to a text string — that identifies the contents of the box.
You can define CCandyBox
as a derived class with the CBox
class as the base class, as follows:
// Header file CandyBox.h in project Ex9_01 #pragma once #include "Box.h" class CCandyBox: CBox { public: char* m_Contents; CCandyBox(char* str = "Candy") // Constructor { m_Contents = new char[ strlen(str) + 1 ]; strcpy_s(m_Contents, strlen(str) + 1, str); } ~CCandyBox() // Destructor { delete[] m_Contents; }; };
Add this header file to the project Ex9_01
. You need the #include
directive for the Box.h
header file 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. You have added the new member, m_Contents
, and, because it is a pointer to a string, you need a constructor to initialize it and a destructor to release the memory for the string. You have also put a default value for the string describing the contents of a CCandyBox
object in the constructor. Objects of the CCandyBox
class type 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 automatically. You can therefore call the function just with the destination and source strings as arguments when you are working with static strings.
The access to inherited members in a derived class needs to be looked at more closely. 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 later, more secure version with private
data members. The reason was that 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 member functions added to the derived class cannot access them. They are only accessible in the derived class through function members of the base class that are not in the private
section of the base class. 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
, so that the class definition is as follows:
// Version of the classes that will not compile class CBox { public: 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; } CCandyBox(char* str = "Candy") // Constructor { m_Contents = new char[ strlen(str) + 1 ]; strcpy_s(m_Contents, strlen(str) + 1, str); } ~CCandyBox() // Destructor { delete[] m_Contents; } };
A program using these classes does not compile. The function Volume()
in the class CCandyBox
attempts to access the private
members of the base class, which is not legal, so the compiler will flag each instance with error number C2248.
The Ex9_02.cpp
file in the project contains:
// Ex9_02.cpp
// Using a function inherited from a base class
#include <iostream> // For stream I/O #include <cstring> // For strlen() and strcpy() #include "CandyBox.h" // For CBox and CCandyBox using std::cout; using std::endl; int main() { CBox myBox(4.0,3.0,2.0); // Create CBox object CCandyBox myCandyBox; CCandyBox myMintBox("Wafer Thin Mints"); // Create CCandyBox object cout << endl << "myBox occupies " << sizeof myBox // Show how much memory << " bytes" << endl // the objects require << "myCandyBox occupies " << sizeof myCandyBox << " bytes" << endl << "myMintBox occupies " << sizeof myMintBox << " bytes";cout << endl
<< "myMintBox volume is " << myMintBox.Volume(); // Get volume of a
// CCandyBox object
cout << endl; return 0; }
This example produces the following output:
myBox occupies 24 bytes myCandyBox occupies 32 bytes
myMintBox occupies 32 bytes myMintBox volume is 1
Although I said the base class constructors are not inherited in a derived class, they still exist in the base class and are used for creating 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 automatically in the last example to create the base part of the derived class object, but this doesn't have to be the case. You can arrange to call a particular base class constructor from the 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 class constructor, depending on the data supplied to the derived class constructor.
The CandyBox.h
header file should contain:
// CandyBox.h in Ex9_03
#pragma once#include <iostream>
#include "Box.h"using std::cout;
using std::endl;
class CCandyBox: public CBox { public: char* m_Contents;// Constructor to set dimensions and contents
// with explicit call of CBox constructor
CCandyBox(double lv, double wv, double hv, char* str = "Candy")
:CBox(lv, wv, hv)
{
cout << endl <<"CCandyBox constructor2 called";
m_Contents = new char[ strlen(str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
}
// Constructor to set contents
// calls default CBox constructor automatically
CCandyBox(char* str = "Candy")
{
cout << endl << "CCandyBox constructor1 called";
m_Contents = new char[ strlen(str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
}
~CCandyBox() // Destructor { delete[] m_Contents; } };
The #include
directive for the <iostream>
header and the two using declarations are not strictly necessary here because Box.h
contains the same code, but it does no harm to put them in. On the contrary, putting these statements in here also means that if you were to remove this code from Box.h
because it was no longer required there, CandyBox.h
would still compile.
The contents of Ex9_03.cpp
are:
// Ex9_03.cpp
// Calling a base constructor from a derived class constructor
#include <iostream> // For stream I/O #include <cstring> // For strlen() and strcpy() #include "CandyBox.h" // For CBox and CCandyBox using std::cout; using std::endl; int main() { CBox myBox(4.0, 3.0, 2.0); CCandyBox myCandyBox;CCandyBox myMintBox(1.0, 2.0, 3.0, "Wafer Thin Mints");
cout << endl << "myBox occupies " << sizeof myBox // Show how much memory << " bytes" << endl // the objects require << "myCandyBox occupies " << sizeof myCandyBox << " bytes" << endl << "myMintBox occupies " << sizeof myMintBox << " bytes"; cout << endl << "myMintBox volume is " // Get volume of a << myMintBox.Volume(); // CCandyBox object cout << endl; return 0; }
In addition to the public
and private
access specifiers for members of a class, you can also declare members of a class as protected
. Within the class, the protected
keyword has the same effect as the private
keyword: members of a class 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 class that is declared as a friend
of the class — you will learn about friend
classes later in this chapter). Using the protected
keyword, you could redefine the CBox
class as follows:
// Box.h in Ex9_04
#pragma once #include <iostream> using std::cout; using std::endl; class CBox { public: // Base class constructor CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Length(lv), m_Width(wv), m_Height(hv) { cout << endl << "CBox constructor called"; }// CBox destructor - just to track calls
∼CBox()
{ cout << "CBox destructor called" << endl; }
protected:
double m_Length; double m_Width; double m_Height; };
Now, the data members are still effectively private
, in that they can't be accessed by ordinary global functions, but they'll still be accessible to member functions of a derived class.
You know that 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 become private
in the derived class. The private
members of the base class remain private
to the base and, therefore, inaccessible to member functions of 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.
You have also used public
as the specifier for a base class. This leaves the members of the base class with 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 has the effect of making the inherited public
members of the base protected
in the derived class. The protected
(and private
) inherited members retain their original access level in the derived class. This is summarized in Figure 9-3.
This may look a little complicated, but you can reduce it to the following three points about the inherited members of a derived class:
Members of a base class that are declared as private
are never accessible in a derived class.
Defining a base class as public
doesn't change the access level of its members in the derived class.
Defining a base class as protected
changes its public
members to protected
in the 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 your base classes need to have public
members if you want to be able to vary the access level in derived classes. This may seem to run 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 in such a manner that their only purpose is to act as a base for other classes, and they aren't intended to be used for instantiating objects in their own right.
Remember that the copy constructor is called automatically when you declare 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 supply your own 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 class CBox
. You can then use this class as a base for defining the CCandyBox
class:
// Box.h in Ex9_05
#pragma once #include <iostream> using std::cout; using std::endl; class CBox // Base class definition { public: // Base class constructor CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Length(lv), m_Width(wv), m_Height(hv) { cout << endl << "CBox constructor called"; }// Copy constructor
CBox(const CBox& initB)
{
cout << endl << "CBox copy constructor called";
m_Length = initB.m_Length;
m_Width = initB.m_Width;
m_Height = initB.m_Height;
}
// CBox destructor - just to track calls ~CBox() { cout << "CBox destructor called" << endl; } protected: double m_Length; double m_Width; double m_Height; };
Also recall that the copy constructor must have its parameter specified as a reference to avoid an infinite number of calls to itself, which would otherwise result from the need to copy an argument that is transferred by value. When the copy constructor in our example is invoked, it outputs a message to the screen, so you'll be able to see from the output when this is happening. All you need now is to add a copy constructor to the CCandyBox
class.
You can now run this new version (Ex9_05
) of the last example with the following function main()
to see how the new copy constructor works:
int main() { CCandyBox chocBox(2.0, 3.0, 4.0, "Chockies"); // Declare and initialize CCandyBox chocolateBox(chocBox); // Use copy constructor cout << endl << "Volume of chocBox is " << chocBox.Volume() << endl << "Volume of chocolateBox is " << chocolateBox.Volume()
<< endl; return 0; }
How It Works
When you run this example, it produces the following output:
CBox constructor called CCandyBox constructor2 called CBox constructor called CCandyBox copy constructor called Volume of chocBox is 24 Volume of chocolateBox is 1 CCandyBox destructor called CBox destructor called CCandyBox destructor called CBox destructor called
Although, at first sight, this looks okay, there is something wrong. The third line of output shows that the default constructor for the CBox
part of the object chocolateBox
is called, rather than the copy constructor. As a consequence, the object has the default dimensions rather than the dimensions of the initializing object, so the volume is incorrect. The reason for this is that when you write a constructor for an object of a derived class, you are responsible for ensuring that the members of the derived class object are properly initialized. This includes the inherited members.
The fix for this is to call the copy constructor for the base part of the class in the initialization list for the copy constructor for the CCandyBox
class. The copy constructor then becomes:
// Derived class copy constructor
CCandyBox(const CCandyBox& initCB): CBox(initCB)
{
cout << endl << "CCandyBox copy constructor called";
// Get new memory
m_Contents = new char[ strlen(initCB.m_Contents) + 1 ];
// Copy string
strcpy_s(m_Contents, strlen(initCB.m_Contents) + 1, initCB.m_Contents);
}
Now, the CBox
class copy constructor is called with the initCB
object. Only the base part of the object is passed to it, so everything works out. If you modify the last example by adding the base copy constructor call, the output is as follows:
CBox constructor called CCandyBox constructor2 called CBox copy constructor called CCandyBox copy constructor called Volume of chocBox is 24 Volume of chocolateBox is 24
CCandyBox destructor called CBox destructor called CCandyBox destructor called CBox destructor called
The output shows that all the constructors and destructors are called in the correct sequence, and the copy constructor for the CBox
part of chocolateBox
is called before the CCandyBox
copy constructor. The volume of the object chocolateBox
of the derived class is now the same as that of its initializing object, which is as it should be.
You have, therefore, another golden rule to remember:
If you write any kind of constructor for a derived class, you are responsible for the initialization of all members of the derived class object, including all its inherited members.
Of course, as you saw in the previous chapter, if you want to make a class that allocates memory on the heap as efficient as possible, you should overload the copy constructor with a version that uses an rvalue reference parameter. You could add the following to the CCandyBox
class to take care of this:
// Move constructor CCandyBox(CCandyBox&& initCB): CBox(initCB) { cout << endl << "CCandyBox move constructor called"; m_Contents = initCB.m_Contents; initCB.m_Contents = 0; }
You still have to call the base class copy constructor to get the base members initialized.
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:
#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 could define this as:
#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 };
The constructor here 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. The forward declaration for the CBottle
class is necessary because the constructor refers to CBottle
. As you know by now, this won't work. The data members of the CBottle
class are private
, so the CCarton
constructor cannot access them. As you also know, a friend
declaration in the CBottle
class fixes it:
#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 have a forward declaration for the CCarton
class because the fried function refers to it.
You might think that this will compile correctly, but there is a problem. The CCarton
class definition refers to the CBottle
class, and the CBottle
class with the friend function added refers to the CCarton
class, so we have a cyclic dependency here. You can put a forward declaration of the CCarton
class in the CBottle
class, and vice versa, but this still won't allow the classes to compile. The problem is with the CCarton
class 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. The header file holding the CCarton
class definition will be:
#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:
#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 is able to compile both class definitions and the carton.cpp
file.
You can also allow all the function members 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 now 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> using std::cout; using std::endl; class CBox // Base class { public: // Function to show the volume of an object void ShowVolume() const { cout << endl << "CBox usable volume is " << Volume(); } // Function to calculate the volume of a CBox object double Volume() const { return m_Length*m_Width*m_Height; } // Constructor CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0) :m_Length(lv), m_Width(wv), m_Height(hv) {} protected: double m_Length; double m_Width; double m_Height; };
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 the body of the function. The data members are as before and are specified as 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 conceivably be other additional 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 objects just calls the base class constructor in its initialization list to set the data member values. No statements are necessary in its body. You have included 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 an object of the class CGlassBox
.
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 there is another definition of the function in a derived class, it signals to the compiler that you don't want static linkage for this function. What you do want is the selection of the function to be called at any given point in the program to be based on the kind of object for which it is called.
Using pointers with objects of a base class and of a derived class 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. You can thus use a pointer of the type "pointer to base" to obtain different behavior with virtual functions, depending on what kind 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 reference to a base class as a parameter, you can pass an object of a derived class to it as an argument. When your function executes, the appropriate virtual function for the object passed is selected automatically. We could see this happening by modifying the function main()
in the last example to call a function that has a reference as a parameter.
At the beginning of the previous example, you have the prototype declaration for the Output()
function. To process this declaration, the compiler needs to have access to the definition of the CBox
class because the parameter is of type CBox&
. In this case, the definition of the CBox
class is available at this point because you have a #include
directive for GlassBox.h
that has its own #include
directive for Box.h
.
However, there may be situations where you have such a declaration and the class definition cannot be included in this way, in which case, you would need some other way to at least identify that the name CBox
refers to a class type. In this situation, you could provide an incomplete definition of the class CBox
preceding the prototype of the output function. The statement that provides an incomplete definition of the CBox
class is simply:
class CBox;
The statement just identifies that the name CBox
refers to a class that is not defined at this point, but this is sufficient for the compiler to know that CBox
is the name of a class, and this allows it to process the prototype of the function Output()
. Without some indication that CBox
is a class, the prototype causes an error message to be generated.
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 to suit the objects of that class, but that there is no meaningful definition you could give for the function in the base class.
For example, you could conceivably have a class CContainer
, which could be used as 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 a virtual member function Volume()
to allow it to be called polymorphically for any derived classes. Because the CContainer
class has no data members and, therefore, no dimensions, there is no sensible definition that you can write for the Volume()
function. You can still define the class, however, including the member function Volume()
, as follows:
// Container.h for Ex9_10 #pragma once #include <iostream> using std::cout; using std::endl; 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 { cout << endl << "Volume is " << Volume(); } };
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. Any class derived from this class must either define the Volume()
function or redefine it as a pure virtual function. Because you have declared Volume()
as const
, its implementation in any derived class must also be const
. Remember that const
and non-const
varieties of a function with the same name and parameter list are different functions. In other words, a const
version of a function is an overload of a non-const
version.
The class also contains the function ShowVolume()
, which displays the volume of objects of derived classes. Because this is declared as virtual
, it can be replaced in a derived class, but if it isn't, the base class version that you see here is called.
A class containing a pure virtual function is called an abstract class. It's called abstract because you can't define objects of a class containing a pure virtual function. It exists only for the purpose of defining classes that are derived from it. If a class derived from an abstract class still defines a pure virtual function of the base as pure, it, too, is an abstract class.
You should not conclude, from the previous example of the CContainer
class, that an abstract class can't have data members. An abstract class can have both data members and function members. The presence of a pure virtual function is the only condition that determines that a given class is abstract. In the same vein, an abstract class can have more than one pure virtual function. In this case, a derived class must have definitions for every pure virtual function in 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()
member function that is const
, as well as the non-const Volume()
function that you have defined.
At the beginning of this chapter, I said that a base class for a subclass could, in turn, be derived from another, "more" base class. A small extension of the last example provides you with an illustration of this, as well as demonstrating the use of a virtual function across a second level of inheritance.
One problem that arises when dealing with objects of derived classes using a pointer to the base class is that the correct destructor may not be called. You can see this effect by modifying the last example.
It's a good idea always to declare your base class destructor as virtual as a matter of course when using inheritance. There is a small overhead in the execution of the class destructors, but you won't notice it in the majority of circumstances. Using virtual destructors ensures that your objects will be properly destroyed and avoids potential program crashes that might otherwise occur.
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
up the class hierarchy to type CBox*
. The third statement casts the address in pContainer
to its actual type, CGlassBox*
.
You can apply the dynamic_cast
operator to references as well as pointers. The difference between dynamic_cast
and static_cast
is that the dynamic_cast
operator checks the validity of a cast at runtime, whereas the static_cast
operator does not. If a dynamic_cast
operation is not valid, the result is null. The compiler relies on the programmer for the validity of a static_cast
operation, so you should always use dynamic_cast
for casting up and down a class hierarchy and check for a null result if you want to avoid abrupt termination of your program as a result of using a null pointer.
You can put the definition of one class inside the definition of another, in which case, you have defined a nested class. A nested class has the appearance of being a static member of the class that encloses it and is subject to the member access specifiers, just like any other member of the class. If you place the definition of a nested class in the private section of the class, the class can only be referenced from within the scope of the enclosing class. If you specify a nested class as public
, the class is accessible from outside the enclosing class, but the nested class name must be qualified by the outer class name in such circumstances.
A nested class has free access to all the static members of the enclosing class. All the instance members can be accessed through an object of the enclosing class type, or a pointer or reference to an object. The enclosing class can only access the public members of the nested class, but in a nested class that is private in the enclosing class, the members are frequently declared as public
to provide free access to the entire nested class from functions in the enclosing class.
A nested class is particularly useful when you want to define a type that is only to be used within another type, whereupon the nested class can be declared as private
. Here's an example of that:
// A push-down stack to store Box objects class CStack { private: // Defines items to store in the stack struct CItem { CBox* pBox; // Pointer to the object in this node CItem* pNext; // Pointer to next item in the stack or null // Constructor CItem(CBox* pB, CItem* pN): pBox(pB), pNext(pN){} }; CItem* pTop; // Pointer to item that is at the top public: // Constructor CStack():pTop(nullptr){} // 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 ~CStack() { CItem* pTemp(nullptr); 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 making use of the Stack
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 the CStack
class. The stack is implemented as a set of CItem
objects, where each CItem
object stores a pointer to a CBox
object plus the address of the next CItem
object down in the stack. The Push()
function in the CStack
class pushes a CBox
object onto the top of the stack, and the Pop()
function pops an object off the top of the stack.
Pushing an object onto the stack involves creating a new CItem
object that stores the address of the object to be stored plus the address of the previous item that was on the top of the stack — this is null the first time you push an object onto the stack. Popping an object off the stack returns the address of the object in the item, pTop
. The top item is deleted and the next item becomes the item at the top of the stack.
Because a CStack
object creates CItem
objects on the heap, we need a destructor to make sure any remaining CItem
objects are deleted when a CStack
object is destroyed. The process is to work 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.
All C++/CLI classes, including classes that you define, are derived classes by default. This is because both value classes and reference classes have a standard class, System::Object
, as a base class. This means that both value classes and reference classes inherit from the System::Object
class and, therefore, have the capabilities of the System::Object
class in common. Because the ToString()
function is defined as a virtual function in System::Object
, you can override it in your own classes and have the function called polymorphically when required. This is what you have been doing in previous chapters when you defined the ToString()
function in a class.
Because System::Object
is a base class for all C++/CLI classes, the handle type System::Object^
fulfils a similar role to the void*
type in native C++, in that it can be used to reference any type of object.
The System::Object
base class for all value class types is also responsible for enabling the boxing and unboxing of values of the fundamental types. Boxing a value type instance converts it to an object on the garbage-collected heap, so it will carry full type information along with the basic value. Unboxing is the reverse of boxing. The boxing/unboxing capability means that values of the fundamental types can behave as objects, but can participate in numerical operations without carrying the overhead of being objects. Values of the fundamental types are stored on the stack just as values for the purposes of normal operations and are only converted to an object on the heap that is referenced by a handle of type System::Object^
when they need to behave as objects. For example, if you pass an unboxed value to a function with a parameter that is an appropriate value class type, the compiler will arrange for the value to be converted to an object on the heap; this is achieved by creating a new object on the heap containing the value. Thus, you get implicit boxing and the argument value will be boxed automatically.
Of course, explicit boxing is also possible. You can force a value to be boxed by assigning it to a variable of type Object^
. For example:
double value = 3.14159265; Object^ boxedValue = value;
The second statement forces the boxing of value
, and the boxed representation is referenced by the handle boxedValue
.
You can also force boxing of a value using gcnew
to create a boxed value on the garbage-collected heap, for example:
long^ number = gcnew long(999999L);
This statement implicitly boxes the value 999999L
and stores it on the heap in a location referenced by the handle number
.
You can unbox a value type using the dereference operator, for example:
Console::WriteLine(*number);
The value pointed to by the handle number
is unboxed and then passed as a value to the WriteLine()
function.
Finally, you can unbox a boxed value using safe_cast
:
long n = safe_cast<long>(number);
This statement unboxes number
and stores the value in n
. Note that without the safe_cast
, this statement will not compile because there is no implicit conversion in this situation.
Although value classes always have the System::Object
class as a base, you cannot derive a value class from an existing class. To put it another way, when you define a value class, you are not allowed to specify a base class. This implies that polymorphism in value classes is limited to the functions that are defined as virtual in the System::Object
class. These are the virtual functions that all value classes inherit from System::Object
:
DESCRIPTION | |
---|---|
| Returns a |
| Compares the current object to |
| Returns an integer that is a hash code for the current object. Hash codes are used as keys to store objects in a collection that stores |
Of course, because System::Object
is also a base class for reference classes, you may want to override these functions in reference classes, too. Note that if you override the Equals()
function in a class, you must also implement the operator==()
function in the class so that it returns the same result as the Equals()
function.
You can derive a reference class from an existing reference class in the same way as you define a derived class in native C++. Let's re-implement Ex9_12
as a C++/CLI program as this also demonstrates nested classes in a CLR program. We can start by defining the Container
class:
// Container.h for Ex9_14 #pragma once using namespace System; // Abstract base class for specific containers ref class Container abstract { public: // Function for calculating a volume - no content // This is defined as an "abstract" virtual function, // indicated by the "abstract" keyword virtual double Volume() abstract; // Function to display a volume virtual void ShowVolume() { Console::WriteLine(L"Volume is {0}", Volume()); } };
The first thing to note is the abstract
keyword following the class name. If a C++/CLI class contains the equivalent of a native C++ pure virtual function, you must specify the class as abstract. You can, however, also specify a class as abstract that does not contain any abstract functions, which prevents you from creating objects of that class type. The abstract
keyword also appears at the end of the Volume()
function member declaration to indicate that it is not defined for this class. You could also add the " = 0"
to the end of the member declaration for Volume()
as you would for a native C++ member, but it is not required.
Both the Volume()
and ShowVolume()
functions are virtual here, so they can be called polymorphically for objects of class types that are derived from Container
.
You can define the Box
class like this:
// Box.h for Ex9_14 #pragma once #include "Container.h" // For Container definition ref class Box : Container // Derived class { public: // Function to show the volume of an object virtual void ShowVolume() override { Console::WriteLine(L"Box usable volume is {0}", Volume()); } // Function to calculate the volume of a Box object virtual double Volume() override { return m_Length*m_Width*m_Height; } // Constructor Box() : m_Length(1.0), m_Width(1.0), m_Height(1.0){} // Constructor Box(double lv, double wv, double hv) : m_Length(lv), m_Width(wv), m_Height(hv){} protected: double m_Length; double m_Width; double m_Height; };
A base class for a ref class is always public and the public
keyword is assumed by default. You can specify the base class explicitly as public
, but it is not necessary to do so. A base class to a ref class cannot be specified as anything other than public
. Because you cannot supply default values for parameters as in the native C++ version of the class, you define the no-arg constructor so that it initializes all three fields to 1.0. The Box
class defines the Volume()
function as an override to the inherited base class version. You must always specify the override
keyword when you want to override a function in the base class. If the Box
class did not implement the Volume()
function, it would be abstract, and you would need to specify it as such to compile the class successfully.
Here's how the GlassBox
class definition looks:
// GlassBox.h for Ex9_14 #pragma once #include "Box.h" // For Box ref class GlassBox : Box // Derived class { public: // Function to calculate volume of a GlassBox // allowing 15% for packing virtual double Volume() override { return 0.85*m_Length*m_Width*m_Height; } // Constructor GlassBox(double lv, double wv, double hv): Box(lv, wv, hv){} };
The base class is Box
, which is public
by default. The rest of the class is essentially the same as the original.
Here's the Stack
class definition:
// Stack.h for Ex9_14 // A push-down stack to store objects of any ref class type #pragma once ref class Stack { private: // Defines items to store in the stack ref struct Item { Object^ Obj; // Handle for the object in this item Item^ Next; // Handle for next item in the stack or nullptr // Constructor Item(Object^ obj, Item^ next): Obj(obj), Next(next){} }; Item^ Top; // Handle for item that is at the top public: // Push an object onto the stack void Push(Object^ obj) { Top = gcnew Item(obj, Top); // Create new item and make it the top } // Pop an object off the stack Object^ Pop() {
if(Top == nullptr) // If the stack is empty return nullptr; // return nullptr Object^ obj = Top->Obj; // Get object from item Top = Top->Next; // Make next item the top return obj; } };
The first difference to notice is that the function parameters and fields are now handles, because you are dealing with ref class objects. The inner struct
, Item
, now stores a handle of type Object^
, which allows objects of any CLR class type to be stored in the stack; this means either value class or ref class objects can be accommodated, which is a significant improvement over the native C++ CStack
class. You don't need to worry about deleting Item
objects when the Pop()
function is called or when a Stack
object is destroyed, because the garbage collector takes care of that.
Here's a summary of the differences from native C++ that these classes have demonstrated:
A base class for a derived ref class is always public.
A function that has no definition for a ref class is an abstract function and must be declared using the abstract
keyword.
A class that contains one or more abstract functions must be explicitly specified as abstract by placing the abstract
keyword following the class name.
A class that does not contain abstract functions can be specified as abstract
, in which case, instances of the class cannot be defined.
You must explicitly use the override
keyword when specifying a function that overrides a function inherited from the base class.
All you need to try out these classes is a CLR console project with a definition of main()
, so let's do it.
The definition of an interface class looks quite similar to the definition of a ref class, but it is quite a different concept. An interface is a class that specifies a set of functions that are to be implemented by other classes to provide a standardized way of providing some specific functionality. Both value classes and ref classes can implement interfaces. An interface does not define any of its function members — these are defined by each class that implements the interface.
You have already met the System::IComparable
interface in the context of generic functions where you specified the IComparable
interface as a constraint. The IComparable
interface specifies the CompareTo()
function for comparing objects, so all classes that implement this interface have the same mechanism for comparing objects. You specify an interface that a class implements in the same way as a base class. For example, here's how you could make the Box
class from the previous example implement the System::IComparable
interface:
ref class Box : Container, IComparable // Derived class
{ public:// The function specified by IComparable interface
virtual int CompareTo(Object^ obj)
{
if(Volume() < safe_cast<Box^>(obj)->Volume())
return −1;
else if(Volume() > safe_cast<Box^>(obj)->Volume())
return 1;
else
return 0;
}
// Rest of the class as before...
};
The name of the interface follows the name of the base class, Container
. If there were no base class, the interface name alone would appear here. A ref class can only have one base class, but it can implement as many interfaces as you want. The class must define every function specified by each of the interfaces that it claims to implement. The IComparable
interface only specifies one function, but there can be as many functions in an interface as you want. The Box
class now defines the CompareTo()
function with the same signature as the IComparable
interface specifies for the function. Because the parameter to the CompareTo()
function is of type Object^
, you have to cast it to type Box^
before you can access members of the Box
object it references.
You define an interface class using either of the keywords interface class
or interface struct
. Regardless of whether you use the interface class
or the interface struct
keyword to define an interface, all the members of an interface are always public
by default, and you cannot specify them to be otherwise. The members of an interface can be functions, including operator functions, properties, static fields, and events, all of which you'll learn about later in this chapter. An interface can also specify a static constructor and can contain a nested class definition of any kind. In spite of all that potential diversity of members, most interfaces are relatively simple. Note that you can derive one interface from another in basically the same way as you use to derive one ref class from another. For example:
interface class IController : ITelevison, IRecorder { // Members of IController... };
The IController
interface contains its own members, and it also inherits the members of the ITelevision
and IRecorder
interfaces. A class that implements the IController
interface has to define the member functions from IController
, ITelevision
, and IRecorder
.
You could use an interface instead of the Container
base class in Ex9_14
. Here's how the definition of this interface would look:
// IContainer.h for Ex9_15 #pragma once interface class IContainer { double Volume(); // Function for calculating a volume void ShowVolume(); // Function to display a volume };
By convention, the names of interfaces start with I in C++/CLI, so the interface name is IContainer
. It has two members: the Volume()
function and the ShowVolume()
function, which are public because members of an interface are always public. Both functions are effectively abstract because an interface never includes function definitions — indeed, you could add the abstract
keyword to both here, but it is not required. Instance functions in an interface definition can be specified as virtual
and abstract
, but it is not necessary to do so as they are anyway.
Any class that implements the IContainer
interface must implement both functions if the class is not to be abstract. Let's see how the Box
class looks:
// Box.h for Ex9_15
#pragma once#include "IContainer.h" // For interface definition
using namespace System;ref class Box : IContainer
{ public: // Function to show the volume of an objectvirtual void ShowVolume()
{
Console::WriteLine(L"Box usable volume is {0}", Volume());
}
// Function to calculate the volume of a Box object
virtual double Volume()
{ return m_Length*m_Width*m_Height; }
// Constructor
Box() : m_Length(1.0), m_Width(1.0), m_Height(1.0){}
// Constructor
Box(double lv, double wv, double hv)
: m_Length(lv), m_Width(wv), m_Height(hv){}
protected:
double m_Length;
double m_Width;
double m_Height;
};
The name of the interface goes after the colon in the first line of the class definition, just as if it were a base class. Of course, there could also be a base class, in which case, the interface name would follow the base class name, separated from it by a comma. A class can implement multiple interfaces, in which case, the names of the interfaces are separated by commas.
The Box
class must implement both function members of the IContainer
interface class; otherwise, it would be an abstract class and would need to be declared as such. The definitions for these functions in the Box
class do not have the override
keyword appended because you are not overriding existing function definitions here; you are implementing them for the first time.
The GlassBox
class is derived from the Box
class and, therefore, inherits the implementation of IContainer
. The GlassBox
class definition needs no changes at all to accommodate the introduction of the IContainer
interface class.
The IContainer
interface class has the same role as a base class in polymorphism. You can use a handle of type IContainer
to store the address of an object of any class type that implements the interface. Thus, a handle of type IContainer
can be used to reference objects of type Box
or type GlassBox
and obtain polymorphic behavior when calling the functions that are members of the interface class. Let's try it.
A C++/CLI application always resides in one or more assemblies, so C++/CLI classes always reside in an assembly. The classes we have defined for each example up to now have all been contained in a single simple assembly that is the executable, but you can create assemblies that contain your own library classes. C++/CLI adds visibility specifiers for classes that determine whether a given class is accessible from outside the assembly in which it resides, which is referred to as its parent assembly
. In addition to the public
, private
, and protected
member access specifiers that you have in native C++, C++/CLI has additional access specifiers for class members that determine from where they may be accessed in different assemblies.
You can specify the visibility of a non-nested class, interface, or enum as private
or public
. A public class is visible and accessible outside the assembly in which it resides, whereas a private class is only accessible within its parent assembly. Classes, interfaces, and enum classes are private by default and, therefore, only visible within their parent assembly. To specify a class as public, you just use the public
keyword, like this:
public interface class IContainer { // Details of the interface... };
The IContainer
interface here is visible in an external assembly because you have defined it as public. If you omit the public
keyword, the interface would be private by default and only usable within its parent assembly. You can specify a class, enum
, or interface explicitly as private
if you want, but it is not necessary.
C++/CLI adds three more access specifiers for class members: internal
, public protected
, and private protected
. The effects of these are described in the comments in the class definition:
public ref class MyClass // Class visible outside assembly { public: // Members accessible from classes inside and outside the parent assembly internal: // Members accessible from classes inside the parent assembly public protected: // Members accessible in types derived from MyClass outside the parent assembly // and in any classes inside the parent assembly private protected: // Members accessible in types derived from MyClass inside the parent assembly };
Obviously, the class must be public for the member access specifiers to allow access from outside the parent assembly. Where the access specifier involves two keywords, such as private protected
, the less restrictive keyword applies inside the assembly and the more restrictive keyword applies outside the assembly. You can reverse the sequence of the keyword pairs, so protected private
has the same meaning as private protected
.
To use some of these, you need to create an application that consists of more than one assembly, so let's recreate Ex9_15
as a class library assembly plus an application assembly that uses the class library.
The IContainer
interface class, the Box
class, and the Stack
class are now in this library. The changes to the original definitions for these classes are shaded. Each class is now public, which makes them accessible from an external assembly. The fields in the Box
class are public protected
, which means that they are inherited in a derived class as protected
fields but are public
so far as classes within the parent assembly are concerned. You don't actually refer to these fields from other classes within the parent assembly, so you could have left the fields in the Box
class as protected
in this case.
When you have built this project successfully, the assembly containing the class library is in a file Ex9_16lib.dll
that is in the debug
subdirectory to the project directory if you built a debug version of the project, or in a release
subdirectory if you built the release version. The .dll
extension means that this is a dynamic link library, or DLL. You now need another project that uses your class library.
You have seen how you use the override
keyword to override a function in a base class. You can also specify a function in a derived class as new
, in which case, it hides the function in the base class that has the same signature, and the new function does not participate in polymorphic behavior. To define the Volume()
function as new in a class NewBox
that is derived from Box
, you code it like this:
ref class NewBox : Box // Derived class { public: // New function to calculate the volume of a NewBox object virtual double Volume() new { return 0.5*m_Length*m_Width*m_Height; } // Constructor NewBox(double lv, double wv, double hv): Box(lv, wv, hv){} };
This version of the function hides the version of the Volume()
function that is defined in Box
, so if you call the Volume()
function using a handle of type NewBox^
, the new version is called. For example:
NewBox^ newBox = gcnew NewBox(2.0, 3.0, 4.0); Console::WriteLine(newBox->Volume()); // Output is 12
The result is 12 because the new Volume()
function hides the polymorphic version that the NextBox
class inherits from Box
.
The new Volume()
function is not a polymorphic function, so for polymorphic calls using a handle to a base class type, the new version is not called. For example:
Box^ newBox = gcnew NewBox(2.0, 3.0,4.0); Console::WriteLine(newBox->Volume()); // Output is 24
The only polymorphic Volume()
function in the NewBox
class is the one that is inherited from the Box
class, so that is the function that is called in this case.
An event is a member of a class that enables an object to signal when a particular event has occurred, and the signaling process for an event involves a delegate that provides the mechanism for responding to the event in some way. A mouse click is a typical example of an event, and the object that originated the mouse-click event would signal that the event has occurred by calling one or more functions that are responsible for dealing with the event; a delegate would provide the means to access the function that is to respond to the event. Let's look at delegates first and return to events a little later in this chapter.
The idea of a delegate is very simple — it's an object that can encapsulate one or more pointers to functions that have a given parameter list and return type. A function that a delegate points to will deal with a particular kind of event. Thus, a delegate provides a similar facility in C++/CLI to a function pointer in native C++. Although the idea of a delegate is simple, however, the details of creating and using delegates can get a little confusing, so it's time to concentrate.
The declaration for a delegate looks like a function prototype preceded by the delegate
keyword, but, in reality, it defines two things: the reference type name for a delegate object, and the parameter list and return type of the functions that can be associated with the delegate. A delegate reference type has the System::Delegate
class as a base class, so a delegate type always inherits the members of this class. Here's an example of a declaration for a delegate:
public delegate void Handler(int value); // Delegate declaration
This defines a delegate reference type Handler
, where the Handler
type is derived from System::Delegate
. An object of type Handler
can contain pointers to one or more functions that have a single parameter of type int
and a return type that is void
. The functions pointed to by a delegate can be instance functions or static functions.
Having defined a delegate type, you can now create delegate objects of this type. You have a choice of two constructors for a delegate: one that accepts a single argument, and another that accepts two arguments.
The argument to the delegate constructor that accepts one argument must be a static function member of a class or a global function. The function you specify as the argument must have the same return type and parameter list as you specified in the delegate declaration. Suppose you define a class with the name HandlerClass
like this:
public ref class HandlerClass { public: static void Fun1(int m) { Console::WriteLine(L"Function1 called with value {0}", m); } static void Fun2(int m) { Console::WriteLine(L"Function2 called with value {0}", m); } void Fun3(int m) { Console::WriteLine(L"Function3 called with value {0}", m+value); } void Fun4(int m) { Console::WriteLine(L"Function4 called with value {0}", m+value); } HandlerClass():value(1){} HandlerClass(int m):value(m){} protected: int value; };
The class has four functions, with a parameter of type int
and a return type of void
. Two of these are static functions, and two are instance functions. It also has two constructors, including a no-arg constructor. This class doesn't do much except produce output where you'll be able to determine which function was called and, for instance functions, what the object was.
You could create a delegate of the type Handler
that we defined earlier like this:
Handler^ handler = gcnew Handler(HandlerClass::Fun1); // Delegate object
The handler
object contains the address of the static function, Fun1
, in the HandlerClass
class. If you call the delegate, the HandlerClass::Fun1()
function is called, with the argument the same as you pass in the delegate call. You can write the delegate call like this:
handler->Invoke(90);
This calls all the functions in the invocation list for the handler delegate. In this case, there is just one function in the invocation list, HandlerClass::Fun1()
, so the output is:
Function1 called with value 90
You could also call the delegate with the following statement:
handler(90);
This is shorthand for the previous statement that explicitly called the Invoke()
function, and this is the form of delegate call you see generally.
The +
operator is overloaded for delegate types to combine the invocation lists for two delegates into a new delegate object. For example, you could apparently modify the invocation list for the handler
delegate with this statement:
handler += gcnew Handler(HandlerClass::Fun2);
The handler variable now references a delegate object with an invocation list containing two functions: Fun1
and Fun2
. However, this statement creates a new delegate object. The invocation list for a delegate cannot be changed, so the +
operator works in a similar way to the way it works with String
objects — you always get a new object created. You could invoke the delegate again with this statement:
handler(80);
Now, you get the output:
Function1 called with value 80 Function2 called with value 80
Both functions in the invocation list are called, and they are called in the sequence in which they were added to the delegate object.
You can effectively remove an entry from the invocation list for a delegate by using the subtraction operator:
handler -= gcnew Handler(HandlerClass::Fun1);
This creates a new delegate object that contains just HandlerClass::Fun2()
in its invocation list. The effect of using the -= operator is to remove the functions that are in the invocation list on the right side (HandlerClass::Fun1
) from the list for the handler, and create a new object pointing to the functions that remain.
The invocation list for a delegate must contain at least one function pointer. If you remove all the function pointers using the subtraction operator, then the result will be nullptr
.
When you use the delegate constructor that has two parameters, the first argument is a reference to an object on the CLR heap, and the second object is the address of an instance function for that object's type. Thus, this constructor creates a delegate that contains a pointer to the instance function specified by the second argument for the object specified by the first argument. Here's how you can create such a delegate:
HandlerClass^ obj = gcnew HandlerClass; Handler^ handler2 = gcnew Handler (obj, &HandlerClass::Fun3);
The first statement creates an object of type HandlerClass
, and the second statement creates a delegate of type Handler
pointing to the Fun3()
function for the HandlerClass
object obj
. The delegate expects an argument of type int
, so you can invoke it with the statement:
handler2(70);
This results in Fun3()
for obj
being called with an argument value of 70, so the output is:
Function3 called with value 71
The value stored in the value field for obj
is 1 because you create the object using the default constructor. The statement in the body of Fun3()
adds the value field to the function argument — hence the 71 in the output.
Because they are both of the same type, you could combine the invocation list for handler
with the list for the handler2
delegate:
Handler^ handler = gcnew Handler(HandlerClass::Fun1); // Delegate object handler += gcnew Handler(HandlerClass::Fun2); HandlerClass^ obj = gcnew HandlerClass; Handler^ handler2 = gcnew Handler (obj, &HandlerClass::Fun3); handler += handler2;
Here, you recreate handler
to reference a delegate that contains pointers to the static Fun1()
and Fun2()
functions. You then create a new delegate referenced by handler
that contains the static functions plus the Fun3()
instance function for obj
. You can now invoke the delegate with the statement:
handler(50);
This results in the following output:
Function1 called with value 50 Function2 called with value 50 Function3 called with value 51
As you see, invoking the delegate calls the two static functions plus the Fun3()
member of obj
, so you can combine static and non-static functions with a single invocation list for a delegate.
Let's put some of the fragments together in an example to make sure it really does work.
This example produces the following output:
Delegate with one pointer to a static function: Function1 called with value 90 Delegate with two pointers to static functions: Function1 called with value 80 Function2 called with value 80 Delegate with three pointers to functions: Function1 called with value 70 Function2 called with value 70 Function3 called with value 71 Shortening the invocation list... Delegate with pointers to one static and one instance function: Function2 called with value 60 Function3 called with value 61
The delegates you have seen up to now have been examples of bound delegates. They are called bound delegates because they each have a fixed set of functions in their invocation list. You can also create unbound delegates; an unbound delegate points to an instance function with a given parameter list and return type for a given type of object. Thus, the same delegate can invoke the instance function for any object of the specified type. Here's an example of declaring an unbound delegate:
public delegate void UBHandler(ThisClass^, int value);
The first argument specifies the type of the this
pointer for which a delegate of type UBHandler
can call an instance function; the function must have a single parameter of type int
and a return type of void
. Thus, a delegate of type UBHandler
can only call a function for an object of type ThisClass
, but for any object of that type. This may sound a bit restrictive but turns out to be quite useful; you could use the delegate to call a function for each element of type ThisClass^
in an array, for example.
You can create an unbound delegate of type UBHandler
like this:
UBHandler^ ubh = gcnew UBHandler(&ThisClass::Sum);
The argument to the constructor is the address of a function in the ThisClass
class that has the required parameter list and return type.
Here's a definition for ThisClass
:
public ref class ThisClass { public: void Sum(int n) { Console::WriteLine(L"Sum result = {0}", value + n); } void Product(int n) { Console::WriteLine(L"Product result = {0}", value*n); } ThisClass(double v) : value(v){} private: double value; };
The Sum()
function is a public instance member of the ThisClass
class, so invoking the ubh
delegate can call the Sum()
function for any given object of this class type.
When you call an unbound delegate, the first argument is the object for which the functions in the invocation list are to be called, and the subsequent arguments are the arguments to those functions. Here's how you might call the ubh
delegate:
ThisClass^ obj = gcnew ThisClass(99.0); ubh(obj, 5);
The first argument is a handle to a ThisClass
object that you created on the CLR heap by passing the value 99.0 to the class constructor. The second argument to the ubh
call is 5, so it results in the Sum()
function being called with an argument of 5 for the object referenced by obj
.
You can combine unbound delegates using the +
operator to create a delegate that calls multiple functions. Of course, all the functions must be compatible with the delegate, so for ubh
, they must be instance functions in the ThisClass
class that have one parameter of type int
and a void
return type. Here's an example:
ubh += gcnew UBHandler(&ThisClass::Product);
Invoking the new delegate referenced by ubh
calls both the Sum()
and Product()
functions for an object of type ThisClass
. Let's see it in action.
This example produces the following output:
Sum result = 8 Sum result = 13 Sum result = 18 Sum result = 23 Sum result = 28 Sum result = 7 product result = 10 Sum result = 12 product result = 20 Sum result = 17 product result = 30 Sum result = 22 product result = 40 Sum result = 27 product result = 50
As I said earlier, the signaling of an event involves a delegate, and the delegate contains pointers to the functions that are to be called when the event occurs. Most of the events you work with in your programs are events associated with controls such as buttons or menu items, and these events arise from user interactions with your program, but you can also define and trigger events in your own program code.
An event is a member of a reference class that you define using the event
keyword and a delegate class name:
public delegate void DoorHandler(String^ str); // Class with an event member public ref class Door { public: // An event that will call functions associated // with an DoorHandler delegate object event DoorHandler^ Knock; // Function to trigger events void TriggerEvents() { Knock("Fred"); Knock("Jane"); } };
The Door
class has an event member with the name Knock
that corresponds to a delegate of type DoorHandler
. Knock
is an instance member of the class here, but you can specify an event as a static class member using the static
keyword. You can also declare an event to be virtual
. When a Knock
event is triggered, it can call functions with the parameter list and return type that are specified by the DoorHandler
delegate.
The Door
class also has a public function, TriggerEvents()
, that triggers two Knock
events, each with different arguments. The arguments are passed to the functions that have been registered to receive notification of the Knock
event. As you see, triggering an event is essentially the same as calling a delegate.
You could define a class that might handle Knock
events like this:
public ref class AnswerDoor { public: void ImIn(String^ name) { Console::WriteLine(L"Come in {0}, it's open.",name); } void ImOut(String^ name) { Console::WriteLine(L"Go away {0}, I'm out.",name); } };
The AnswerDoor
class has two public function members that potentially could handle a Knock
event, because they both have the parameter list and return type identified in the declaration of the DoorHandler
delegate.
Before you can register functions that are to receive notifications of Knock
events, you need to create a Door
object. You can create a Door
object like this:
Door^ door = gcnew Door;
Now, you can register a function to receive notification of the Knock
event in the door
object like this:
AnswerDoor^ answer = gcnew AnswerDoor; door->Knock += gcnew DoorHandler(answer, &AnswerDoor::ImIn);
The first statement creates an object of type AnswerDoor
— you need this because the ImIn()
and ImOut()
functions are not static class members. You then add an instance of the DoorHandler
delegate type to the Knock
member of class Door
. This exactly parallels the process of adding function pointers to a delegate, and you could add further handler functions to be called when a Knock
event is triggered in the same way. We can see it operating in an example.
Executing this example results in the following output:
Come in Fred, it's open. Come in Jane, it's open. Go away Fred, I'm out. Go away Jane, I'm out.
You can define a destructor for a reference class in the same way as you define a destructor for a native C++ class. The destructor for a reference class is called when the handle goes out of scope or the object is part of another object that is being destroyed. You can also apply the delete
operator to a handle for a reference class object, and that results in the destructor being called. The primary reason for implementing a destructor for a native C++ class is to deal with data members allocated on the heap, but obviously, that doesn't apply to reference classes, so there is less need to define a destructor in a ref class. You might do this when objects of the class are using other resources that are not managed by the garbage collector, such as files that need to be closed in an orderly fashion when an object is destroyed. You can also clean up such resources in another kind of class member called a finalizer.
A finalizer is a special kind of function member of a reference class that is called automatically by the garbage collector when destroying an object. The purpose of a finalizer is to deal with unmanaged resources used by a reference class object that will not be dealt with by the garbage collector. Note that the finalizer is not called for a class object if the destructor was called explicitly, or was called as a result of applying the delete
operator to the object. In a derived class, finalizers are called in the same sequence as destructor calls would be, so the finalizer for the most derived class is called first, followed by the finalizers for successive parent classes in the hierarchy, with the finalizer for the most base class being called last.
You define a finalizer in a class like this:
public ref class MyClass { // Finalizer definition !MyClass() { // Code to clean-up when an object is destroyed... } // Rest of the class definition... };
You define a finalizer function in a class in a similar way to a destructor, but with !
instead of the ~
that you use preceding the class name for a destructor. Similar to a destructor, you must not supply a return type for a finalizer, and the access specifier for a finalizer will be ignored. You can see how destructors and finalizers operate with a little example.
The output from this example is:
MyClass object(1) destructor called. MyClass object(2) destructor called. End Program MyClass object(3) finalizer called.
C++/CLI provides you with the capability for defining generic classes where a specific class is instantiated from the generic class type at runtime. You can define generic value classes, generic reference classes, generic interface classes, and generic delegates. You define a generic class using one or more type parameters in a similar way to generic functions that you saw in Chapter 6.
For example, here's how you could define a generic version of the Stack
class you saw in Ex9_14
:
// Stack.h for Ex9_21 // A generic pushdown stack generic<typename T> ref class Stack { private: // Defines items to store in the stack ref struct Item { T Obj; // Handle for the object in this item Item^ Next; // Handle for next item in the stack or nullptr // Constructor Item(T obj, Item^ next): Obj(obj), Next(next){} }; Item^ Top; // Handle for item that is at the top public: // Push an object onto the stack void Push(T obj) { Top = gcnew Item(obj, Top); // Create new item and make it the top } // Pop an object off the stack T Pop() { if(!Top) // If the stack is empty return T(); // return null equivalent T obj = Top->Obj; // Get object from item Top = Top->Next; // Make next item the top return obj; } };
The generic version of the class now has a type parameter, T
. Note that you could use the class
keyword instead of the typename
keyword when specifying the parameter — there is no difference between them in this context. A type argument replaces T
when the generic class type is used; T
is replaced by the type argument through the definition of the class, so a major advantage over the original version is that the generic class type is much safer without losing any of its flexibility. The Push()
member of the original class accepts any handle, so you could happily push a mix of objects of type MyClass^
, String^
, or, indeed, any handle type onto the same stack, whereas an instance of the generic type accepts only objects of the type specified as the type argument or objects of a type that have the type argument as a base.
Look at the implementation of the Pop()
function. The original version returned nullptr
if the top item in the stack was null, but you can't return nullptr
for a type parameter because the type argument could be a value type. The solution is to return T()
, which is a no-arg constructor call for type T
. This results in the equivalent of 0 for a value type and nullptr
for a handle.
You can specify constraints on a generic class type parameter using the where
keyword in the same way as you did for generic functions in Chapter 6.
You could create a stack from the Stack<>
generic type that stores handles to Box
objects like this:
Stack<Box^>^ stack = gcnew Stack<Box^>;
The type argument Box^
goes between the angled brackets and the statement creates a Stack<Box^>
object on the CLR heap. This object allows handles of type Box^
to be pushed onto the stack as well as handles of any type that have Box
as a direct or indirect base class. You can try this out with a revised version of Ex9_14
.
This example produces the following output:
The array of 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 Now pushing integers onto the stack: 2 4 6 8 10 12 Popping integers off the stack produces: 12 10 8 6 4 2
You can define generic interfaces in the same way as you define generic reference classes, and a generic reference class can be defined in terms of a generic interface. To show how this works, you can define a generic interface that can be implemented by the generic class, Stack<>
. Here's a definition for a generic interface:
// Interface for stack operations generic<typename T> public interface class IStack { void Push(T obj); // Push an item onto the stack T Pop(); };
This interface has two functions identifying the push and pop operations for a stack.
The definition of the generic Stack<>
class that implements the IStack<>
generic interface is:
generic<typename T> ref class Stack : IStack<T>
{ private: // Defines items to store in the stack ref struct Item { T Obj; // Handle for the object in this item Item^ Next; // Handle for next item in the stack or nullptr // Constructor Item(T obj, Item^ next): Obj(obj), Next(next){} }; Item^ Top; // Handle for item that is at the top public: // Push an object onto the stackvirtual void Push(T obj)
{ Top = gcnew Item(obj, Top); // Create new item and make it the top } // Pop an object off the stackvirtual T Pop()
{ if(!Top) // If the stack is empty return T(); // return null equivalent T obj = Top->Obj; // Get object from item Top = Top->Next; // Make next item the top return obj; } };
The changes from the previous generic Stack<>
class definition are shaded. In the first line of the generic class definition, the type parameter, T
, is used as the type argument to the interface IStack
, so the type argument used for the Stack<>
class instance also applies to the interface. The Push()
and Pop()
functions in the class now have to be specified as virtual
because the functions are virtual in the interface. You could add a header file containing the IStack
interface to the previous example, and amend the generic Stack<>
class definition to the example, and recompile the program to see it operating with a generic interface.
A collection class is a class that organizes and stores objects in a particular way; a linked list and a stack are typical examples of collection classes. The System::Collections::Generic
namespace contains a wide range of generic collection classes that implement strongly typed collections. The generic collection classes available include the following:
TYPE | DESCRIPTION |
---|---|
| Stores items of type T in a simple list that can grow in size automatically when necessary |
| Stores items of type T in a doubly linked list |
| Stores item of type T in a stack, which is a first-in last-out storage mechanism |
| Stores items of type T in a queue, which is a first-in first-out storage mechanism |
| Stores key/value pairs where the keys are of type K and the values are of type V |
I won't go into details of all these, but I'll mention briefly just three that you are most likely to want to use in your programs. I'll use examples that store value types for simplicity, but of course, the collection classes work just as well with reference types. The code fragments that follow assume a using directive for the System::Collections::Generic
namespace is in effect.
List<T>
defines a generic list that automatically increases in size when necessary. You can add items to a list using the Add()
function and you can access items stored in a List<T>
using an index, just like an array. Here's how you define a list to store values of type int
:
List<int>^ numbers = gcnew List<int>;
This has a default capacity, but you could specify the capacity you require. Here's a definition of a list with a capacity of 500:
List<int>^ numbers = gcnew List<int>(500);
You can add objects to the list using the Add()
functions:
for(int i = 0 ; i<1000 ; i++) numbers->Add( 2*i+1);
This adds 1000 integers to the numbers list. The list grows automatically if its capacity is less than 1000. When you want to insert an item in an existing list, you can use the Insert()
function to insert the item specified by the second argument at the index position specified by the first argument. Items in a list are indexed from zero, like an array.
You could sum the contents of the list like this:
int sum = 0; for(int i = 0 ; i<numbers->Count ; i++) sum += numbers[i];
Count
is a property that returns the current number of items in the list. The items in the list may be accessed through the default indexed property, and you can get and set values in this way. Note that you cannot increase the capacity of a list using the default indexed property. If you use an index outside the current range of items in the list, an exception is thrown.
You could also sum the items in the list like this:
for each(int n in numbers) sum +=n;
You have a wide range of other functions you can apply to a list, including functions for removing elements, and sorting and searching the contents of the list.
LinkedList<T>
defines a linked list with forward and backward pointers so you can iterate through the list in either direction. You could define a linked list that stores floating-point values like this:
LinkedList<double>^ values = gcnew LinkedList<double>;
You could add values to the list like this:
for(int i = 0 ; i<1000 ; i++) values->AddLast(2.5*i);
The AddLast()
function adds an item to the end of the list. You can add items to the beginning of the list by using the AddFirst()
function. The Find()
function returns a handle of type LinkedListNode<T>^
to a node in the list containing the value you pass as the argument to Find()
. You could use this handle to insert a new value before or after the node that you found. For example:
LinkedListNode<double>^ node = values->Find(20.0); // Find node containing 20.0 if(node) values->AddBefore(node, 19.9); // Insert 19.9 before node
The first statement finds the node containing the value 20.0. If it does not exist, the Find()
function returns nullptr
. The last statement that is executed if node
is not nullptr
adds a new value of 19.9 before node
. You could use the AddAfter()
function to add a new value after a given node. Searching a linked list is relatively slow because it is necessary to iterate through the elements sequentially.
You could sum the items in the list like this:
double sumd = 0; for each(double v in values) sumd += v;
The for each
loop iterates through all the items in the list and accumulates the total in sum
.
The Count
property returns the number of items in the linked list, and the First
and Last
properties return the values of the first and last items.
The generic Dictionary<>
collection class requires two type arguments; the first is the type for the key, and the second is the type for the value associated with the key. A dictionary is especially useful when you have pairs of objects that you want to store, where one object is a key to accessing the other object. A name and a phone number are an example of a key value pair that you might want to store in a dictionary, because you would typically want to retrieve a phone number using a name as the key. Suppose you have defined Name
and PhoneNumber
classes to encapsulate names and phone numbers, respectively. You can define a dictionary to store name/number pairs like this:
Dictionary<Name^, PhoneNumber^>^ phonebook = gcnew Dictionary<Name^, PhoneNumber^>;
The two type arguments are Name^
and PhoneNumber^
, so the key is a handle for a name and the value is a handle for a phone number.
You can add an entry in the phonebook
dictionary like this:
Name^ name = gcnew Name("Jim", "Jones"); PhoneNumber^ number = gcnew PhoneNumber(914, 316, 2233); phonebook->Add(name, number); // Add name/number pair to dictionary
To retrieve an entry in a dictionary, you can use the default indexed property — for example:
try { PhoneNumber^ theNumber = phonebook[name]; } catch(KeyNotFoundException^ knfe) { Console::WriteLine(knfe); }
You supply the key as the index value for the default indexed property, which, in this case, is a handle to a Name
object. The value is returned if the key is present, or an exception of type KeyNotFoundException
is thrown if the key is not found in the collection; therefore, whenever you are accessing a value for a key that may not be present, the code should be in a try
block.
A Dictionary<>
object has a Keys
property that returns a collection containing the keys in the dictionary, as well as a Values
property that returns a collection containing the values. The Count
property returns the number of key/value pairs in the dictionary.
Let's try some of these in a working example.
The output from this example should be as follows:
Creating a List<T> of integers: Total = 1000000 Creating a LinkedList<T> of double values: Total = 1248750 Total after adding values = 1248800 Creating a Dictionary<K,V> of name/number pairs: List all the numbers: 914 316 2233 123 234 3456 515 224 6864 Access the keys to list all name/number pairs: Jim Jones : 914 316 2233 Fred Fong : 123 234 3456 Janet Smith : 515 224 6864
This chapter covered the principal ideas involved in using inheritance for native C++ classes and C++/CLI classes.
You have now gone through all of the important language features of ISO/ANSI C++ and C++/CLI. It's important that you feel comfortable with the mechanisms for defining and deriving classes and the process of inheritance in both language versions. Windows programming with Visual C++ 2010 involves extensive use of all these concepts.
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 |
Access specifiers for a base class | A base class can be specified for a derived class with the keyword |
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 destructors | You should declare the destructor in a native C++ base class that contains a virtual function as |
| A native C++ class may be designated as a |
Pure virtual functions | A virtual function in a native C++ base class can be specified as pure by placing |
Derived classes in C++/CLI | A C++/CLI reference class can be derived from another reference class. Value classes cannot be derived classes. |
Interface classes | An interface class declares a set of public functions that represent a specific capability that can be implemented by a reference class or value class. An interface class can contain public functions, events, and properties. An interface can also define static data members, functions, events, and properties, and these are inherited in a class that implements the interface. |
Derived interface classes | An interface class can be derived from another interface class, and the derived interface contains the members of both interfaces. |
Delegates | A delegate is an object that encapsulates one or more pointers to functions that have the same return type and parameter list. Invoking a delegate calls all the functions pointed to by the delegate. |
Event members of a class | An event member of a class can signal when the event occurs by calling one or more handler functions that have been registered with the event. |
Generic C++/CLI classes | A generic class is a parameterized type that is instantiated at runtime. The arguments you supply for type parameters when you instantiate a generic type can be value class types or reference class types. |
Collection classes | The |
C++/CLI class libraries | You can create a C++/CLI class library in a separate assembly, and the class library resides in a |
18.226.52.124