More on Classes
static Length^ operator++(Length^ len)
{
++len->inches;
len->feet += len->inches/len->inchesPerFoot;
len->inches %= len->inchesPerFoot; return len;
}
};
//Multiply operator implementation - right operand double Length^ Length::operator*(double x, Length^ len)
{
int ins = safe_cast<int>(x*len->inches +x*len->feet*inchesPerFoot); return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot);
}
//Multiply operator implementation - left operand double
Length^ Length::operator*(Length^ len, double x) { return operator*(x, len); }
int main(array<System::String ^> ^args)
{
Length^ len1 = gcnew Length(2,6); Length^ len2 = gcnew Length(3,5); Length^ len3 = gcnew Length(14,6);
//2 feet 6 inches
//3 feet 5 inches
//14 feet 6 inches
// Use +, * and / operators
Length^ total = 12*(len1+len2+len3) + (len3/gcnew Length(1,7))*len2; Console::WriteLine(total);
// Use remainder operator
Console::WriteLine(
L”{0} can be cut into {1} pieces {2} long with {3} left over.”,
|
len3, len3/len1, len1, len3%len1); |
Length^ len4 = gcnew Length(1, 11); |
// 1 foot 11 inches |
// Use preand postfix increment operator |
|
Console::WriteLine(len4++); |
// Use postfix increment operator |
Console::WriteLine(++len4); |
// Use postfix increment operator |
return 0; |
|
}
This example produces the following output:
275 feet 9 inches
14 feet 6 inches can be cut into 5 pieces 2 feet 6 inches long with 2 feet 0 inches left over.
2 feet 0 inches
2 feet 1 inches
Press any key to continue . . .
Chapter 8
How It Works
The main differences are in the parameter and return types for the overloaded operator functions, the use of the -> operator as a consequence of that, and objects of type Length are now created on the CLR heap using the gcnew keyword. Apart from these changes the code is basically the same and the operator functions work just as effectively as in the previous example.
Summar y
In this chapter, you learned the basics of how you can define classes and how you create and use class objects. You have also learned about how you can overload operators in a class to allow the operators to be applied to class objects.
The key points to keep in mind from this chapter are:
Objects are created by functions called constructors. The primary role of a constructor is to set values for the data members (fields) for a class object.
C++/CLI classes can also have a static contructor that initializes the static fields in a class.
Objects are destroyed using functions called destructors. It is essential to define a destructor in native C++ classes to destroy objects which contain members that are allocated on the heap because the default constructor will not do this.
The compiler will supply a default copy constructor for a native C++ class if you do not define one. The default copy constructor will not deal correctly with objects of classes that have data members allocated on the free store.
When you define your own copy constructor in a native C++ class, you must use a reference parameter.
You must not define a copy constructor in a value classes; copies of value class objects are always created by copying fields.
No default copy constructor is supplied for a reference class although you can define your own when this is necessary.
If you do not define an assignment operator for your native C++ class, the compiler will supply a default version. As with the copy constructor, the default assignment operator will not work correctly with classes that have data members allocated on the free store.
You must not define the assignment operator in a value class. Assignment of value class objects is always done by copying fields.
A default assignment operator is not provided is reference classes but you can define your own assignment operator function when necessary.
It is essential that you provide a destructor, a copy constructor and an assignment operator for native C++ classes that have members allocated by new.
A union is a mechanism that allows two or more variables to occupy the same location in memory.
More on Classes
C++/CLI classes can contain literal fields that define constants within a class. The can also contain initonly fields that are cannot be modified once they have been initialized.
Most basic operators can be overloaded to provide actions specific to objects of a class. You should only implement operator functions for your classes that are consistent with the normal interpretation of the basic operators.
A class template is a pattern that you can use to create classes with the same structure but support different data types.
You can define a class template that has multiple parameters, including parameters that can assume constant values rather than types.
You should put definitions for your program in .h files, and executable code — function definitions — in .cpp files. You can then incorporate .h files into your .cpp files by using #include directives.
Exercises
You can download the source code for the examples in the book and the solutions to the following exercises from http://www.wrox.com.
1.Define a native C++ class to represent an estimated integer, such as ‘about 40’. These are integers whose value may be regarded as exact or estimated, so the class needs to have as data members a value and an ‘estimation’ flag. The state of the estimation flag affects arithmetic operations, so that ‘2 * about 40’ is ‘about 80’. The state of variables should be switchable between ‘estimated’ and ‘exact’.
Provide one or more constructors for such a class. Overload the + operator so that these integers can be used in arithmetic expressions. Do you want the + operator to be a global or a member function? Do you need an assignment operator? Provide a Print() member function so that they can be printed out, using a leading ‘E’ to denote that the ‘estimation’ flag is set. Write a program to test the operation of your class, checking especially that the operation of the estimation flag is correct.
2.Implement a simple string class in native C++ that holds a char* and an integer length as private data members. Provide a constructor which takes an argument of type const char*, and implement the copy constructor, assignment operator and destructor functions. Verify that your class works. You will find it easiest to use the string functions from the <cstring> header file.
3.What other constructors might you want to supply for your string class? Make a list, and code them up.
4.(Advanced) Does your class correctly deal with cases such as this?
string s1;
...
s1 = s1;
If not, how should it be modified?
5.(Advanced) Overload the + and += operators of your class for concatenating strings.
6.Modify the stack example from Exercise 7 in the previous chapter so that the size of the stack is specified in the constructor and dynamically allocated. What else do you need to add? Test the operation of your new class.
7.Define a Box ref class with the same functionality as the CBox class in Ex8_08.cpp and reimplement the example as a program for the CLR.
9
Class Inheritance and
Vir tual Functions
In this chapter, you’re going to look into a topic that lies at the heart of object-oriented programming 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.
In this chapter, you will learn about:
How inheritance fits into the idea of object-oriented programming
Defining a new class in terms of an existing one
The use of the protected keyword to define a new access specification for class members
How a class can be a friend to another class
Virtual functions and how you can use them
Pure virtual functions
Abstract classes
Virtual destructors and when to use them
Basic Ideas of OOP
As you have seen, a class is a data type that you define to suit your own application requirements. Classes in object-oriented programming 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 with which they are made, and in a multitude of other ways, but even though there are many different kinds of box, 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.
More General
class CCarton
m_MaxWeight
More Specialized
Figure 9-1
class CBox
m_Length m_Width m_Height
class CCandyBox
m_Contents
Class Inheritance and Virtual Functions
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 box 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.
Inheritance in Classes
When you define one class based on another, the former 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.
What Is a Base Class?
A base class is any class that you use as a basis for defining another class. For example, if you define a class B directly in terms of a class A, A is said to be a direct base class of B. In Figure 9-1 the CCrate class is a direct base class of CBeerCrate. When a class such as CBeerCrate is defined in terms of another class CCrate, CBeerCrate is said to be derived from CCrate. Because CCrate is itself defined in
terms of the class CBox, CBox is said to be an indirect base class of CBeerCrate. You’ll see how this is expressed in the class definition in a moment. Figure 9-2 illustrates the way in which base class members are inherited in a derived class.
Chapter 9
|
Base Class |
Inherited |
Derived Class |
|
Members |
|
Data Members |
Data Members |
|
|
|
Function Members |
|
Function Members |
|
Constructors |
/ No |
|
|
Destructor |
/ No |
|
|
Overloaded = operator |
/ No |
|
|
Other Overloaded Operators |
|
Other Overloaded Operators |
|
|
|
Own Data Members |
|
|
|
Own Function Members |
|
|
|
Own Constructors |
|
|
|
Own Destructors |
|
Figure 9-2 |
|
|
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.
Deriving Classes from a Base Class
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 appear 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 Inheritance and Virtual Functions |
|
|
|
|
|
|
|
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.
Try It Out Using a Derived Class
Now you’ll see how the derived class works in an example. Add the following code to the Ex9_01 project as the source file Ex9_01.cpp:
// Ex9_01.cpp |
|
// Using a derived 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; |
|
|
|
Chapter 9
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
<< “myBox length is “ << myBox.m_Length;
myBox.m_Length = 10.0; |
|
// myCandyBox.m_Length = 10.0; |
// uncomment this for an error |
cout << endl; |
|
return 0; |
|
}
How It Works
You have an #include directive for the CandyBox.h header here, and because you know that that contains an #include directive for Box.h, you don’t need to add a directive to include Box.h. You could put an #include directive for Box.h in this file, in which case the #pragma once directive in Box.h would prevent its inclusion more than once. This is important because each class can only be defined once; two definitions for a class in the code would be an error.
After declaring a CBox object and two CCandyBox objects, you output the number of bytes that each object occupies. Let’s look at the output:
myBox occupies 24 bytes myCandyBox occupies 32 bytes myMintBox occupies 32 bytes myBox length is 4
The first line is what you would expect from the discussion in the previous chapter. A CBox object has three data members of type double, each of which is 8 bytes, making 24 bytes in all. Both the CCandyBox objects are the same size — 32 bytes. The length of the string doesn’t affect the size of an object, as the memory to hold the string is allocated in the free store. The 32 bytes are made up of 24 bytes for the three double members inherited from the base class CBox, plus 4 bytes for the pointer member m_Contents, which makes 28 bytes. So where did the other four bytes come from? This is due to the compiler aligning members at addresses that are multiples of eight bytes. You should be able to demonstrate this by adding an extra member of type int, say, to the class CCandyBox. You will find that the size of a class object is still 32 bytes.
You also output the value of the difficulty accessing this member function main(),
m_Length member of the CBox object myBox. Even though you have no of the CBox object, if you uncomment the following statement in the
// myCandyBox.m_Length = 10.0; |
// uncomment this for an error |