Chapter 3
IN THIS CHAPTER
Recognizing objects so that you can create classes
Encapsulating classes into self-contained capsules
Building hierarchies of classes through inheritance
Step outside for a moment and look down. What is the thing you are standing on? (Hint: It’s giant, it’s made of rock and sand and stone and molten lava, and it’s covered with oceans and land.) The answer? A thing! (Even a planet is a thing.) Now go back inside. What’s the thing that you opened — the thing with a doorknob? It’s a thing, too! It’s a slightly different kind of thing, but a thing nevertheless. What are you standing in inside? Okay, you get the idea. Everything you can imagine is a thing — or, to use another term, an object.
Over the years, researchers in the world of computer programming have figured out that one of the better ways to program computers is to divide whatever it is you’re trying to model into a bunch of objects. These objects have methods (capabilities) and properties (characteristics). (Eventually they have relationships, but that comes later.)
In this chapter, you see how to make use of objects to create a software application. In the process, you get to twist some of the nuts and bolts of C++ that relate to objects and get tips on how to get the most out of them.
Think of an object as anything that a computer can describe. Just as physical things have characteristics, such as size, weight, and color, objects in an application can have properties — say, a particular number of accounts, an engine, or even other objects that it contains., A car, for example, contains engines, doors, and other objects.
Further, just as you can use real-world objects in certain ways because they have particular capabilities, an object in an application can use methods. For example, it might be able to withdraw money or send a message or connect to the Internet.
Here’s an example of modeling an object by thinking about how it’s put together and how you use it. Outside, in front of your house, you might see a mailbox. That mailbox is an object. A mailbox is a useful device. You can receive mail, and depending on the style (kind) of mail, you can send mail. (The style of mail is important — you can send a letter because you know how much postage to attach, but you can’t send a package because the amount of postage is unknown.) Those are the mailbox’s methods. And what about its characteristics? Different mailboxes come in different shapes, sizes, colors, and styles. So those are four properties. Now, some mailboxes, such as the kind often found at apartment buildings, are great big metal boxes with several little boxes inside, one for each apartment. The front has doors for each individual box, and the back has a large door for the mail carrier to fill the boxes with all those wonderful ads addressed to your alternative name: Resident.
In this case, you could think of the apartment mailbox as one big mailbox with lots of little boxes, or you could think of it as a big container for smaller mailboxes. In a sense, each of the little boxes has a front door that a resident uses, and the back of each one has an entry that the mail carrier uses. The back opens when the big container door opens.
So think about this: The mail carrier interacts with the container, which holds mailboxes. The container has a big door, and when that door opens, it exposes the insides of the small mailboxes inside, which open, too. Meanwhile, when a resident interacts with the system, he or she interacts with only his or her own particular box.
Take a look at Figures 3-1 and 3-2. Figure 3-1 shows the general look of the back of the mailbox container, where the mail carrier can open the container and put mail in all the different boxes. Figure 3-2 shows the front of the container, with the boxes open so that residents can take out the mail.
FIGURE 3-1: The outer object in this picture is a mailbox container.
FIGURE 3-2: The smaller inner objects in this picture are the mailboxes.
So far, there are two kinds of objects here: the container box and the mailboxes. But wait! There are multiple mailboxes. So, really, you have one container box and multiple mailboxes. But each mailbox is pretty much the same, except for a different lock and a different apartment number, right? In Figure 3-2, each box that’s open is an example of a single mailbox. The others are also examples of the type of object called mailbox. In Figure 3-2, you can see 16 examples of the objects classified as mailbox. In other words, Figure 3-2 shows 16 instances of the class called Mailbox. All those mailboxes are inside an instance of the class that you would probably call Mailboxes.
What can you say about the Mailboxes
collection object?
Mailboxes
collection contains 16 mailbox instances.Mailboxes
collection object is 24 inches by 24 inches in front and back, and it is 18 inches deep.By using this list, you can discover some of the properties and methods of the Mailboxes
collection. The following list shows its properties:
Mailbox
objects insideAnd here’s a list of some of the Mailboxes
collection methods:
Think about the process of the carrier opening or closing the door. Here we seem to have a bizarre thing: The mail carrier asks the Mailboxes
collection to close its door, and the door closes. That’s the way you need to look at modeling objects: Nobody does anything to an object. Rather, someone asks the object to do something, and the object does it itself.
For example, when you reach up to shove a slice of pizza into your mouth, your brain sends signals to the muscles in your arm. Your brain sends out the signals, and your arms move up, and so does the pizza. The point is that you make the command; then the arms carry it out, even though you feel like you’re causing your arms to do it.
Objects are the same way: They have their methods, and you tell them to do their job. You don’t do it for them. At least, that’s the way computer scientists view it. The more you think in this manner, the better you understand object-oriented programming.
Consider the characteristics and capabilities of the Mailbox
class. Each Mailbox
has these properties:
And each Mailbox
has these methods:
Notice that the methods are from the perspective of the Mailbox
, not the person opening the Mailbox
.
Now think about the question regarding the address printed on the Mailbox
. There are 16 different Mailbox
objects, and each one gets a different number. So it’s possible to say this: The Mailbox
class includes an address, which is an integer. Each instance of the Mailbox
class gets its own number. The first may get 1
, the second may get 2
, and so on. So you have two concepts here for representing the mailboxes in code:
Mailbox
instance belongs to the class Mailbox
. There can be any number of instances of the Mailbox
class.Think of the Mailbox
class as a cookie cutter — or, in C++ terminology, the type. The Mailbox
instance is an actual example of the class. In C++, you can create a variable of class Mailbox
and set its Address
integer to 1
. Then you can create another variable of class Mailbox
and set its Address
integer to 2
. Thus, you’ve created two distinct Mailbox
objects, each of class Mailbox
.
But all these Mailbox
instances have a width of 6, a height of 6, and a depth of 18 inches. These properties are the same throughout the Mailboxes
collection. Thus, you would probably not set those manually; instead, you would probably set them in the constructor for the class Mailbox
. Nevertheless, the values of width, height, and depth go with each instance, not with the class; and the instances could, conceivably, each have their own width, height, and depth. However, when you design the class, you would put a stipulation in the class that these properties can’t be changed.
If you are dealing with a Mailboxes
instance and an instance of Mailbox
, you can probably come up with some other classes. When you start considering the parts involved, you can think of the following objects:
Lock
: Each Mailbox
instance would have a Lock
, and so would the Mailboxes
instance.Key
: Each Lock
instance would require one or more Key
instances.Mail
: Each Mailbox
instance can hold several Mail
instances. The carrier puts these in the Mailbox
instances, and the residents take them out.LetterOpener
: Some residents would use these to open the Mail
.So you now have four more types of objects (Lock
, Key
, Mail
, and LetterOpener
). But are these classes necessary? Their need depends on the application you’re building. In this case, you’re modeling the mailbox system simply as an exercise. Therefore, it’s possible to choose the desired classes. But if this were an actual application for a post office, for example, you would have to determine whether the classes are necessary for the people using the software. If the application is a training exercise for people learning to be mail carriers, the application may need more detail, such as the Key
objects. If the application were a video game, it may need all the classes mentioned and even more.
People have come up with various definitions for what exactly object-oriented means. The phrase various definitions in the preceding sentence means that there aren’t simple discussions around a table at a coffeehouse about what the term means. Rather, there are outright arguments! One of the central points of contention is whether C++ is object-oriented. In such discussions, one of the words that usually pops up is encapsulation, which hides data values within the class and prevents unauthorized access to them. People who defend C++ as being object-oriented point out that it supports encapsulation.
Encapsulation is an important concept because it helps you create easier-to-use, safer, and more reliable applications. In the world of computer programming, encapsulation refers to the process of creating a stand-alone object that can take care of itself and do what it must do while holding on to information. For example, to model a cash register, an application would encapsulate the cash register by putting everything about the register (its methods and properties) into a single class.
In Chapter 1 of this minibook, you see how to build classes and instantiate objects from them. The examples in that chapter are straightforward, and all you really deal with are properties and methods. A property in Chapter 1 is essentially a variable, such as Color InkColor;
. However, real-world classes work a little differently. You create a class member, which is actually the property from Chapter 1, and access it through methods that consist of the following:
When viewed in this way, a property can consist of a setter (write-only), getter (read-only), or both (read/write). The property is never actually touched as part of the API. Here are the reasons you want to use this approach:
Methods also access properties, but in a different manner than properties do. When designing a cash register class, you’d probably have a property representing the total dollar amount that the register contains—the methods that use the class wouldn’t directly modify that value. Instead, they’d call various methods to perform transactions. One transaction might be Sale()
. Another transaction might be Refund()
; another might be Void()
. These would be the capabilities of the register in the form of public methods, and they would modify the cash value inside the register, making sure that it balances with the sales and returns. If a method could just modify the cash value directly, the balance would get out of whack. Encapsulation, then, is this:
The cash amount would be a private or protected property. It would be hidden from the caller. As for which it would be, private or protected, that depends on whether you expect to derive new classes from the cash register class and whether you want these new classes to have access to the members.
In the situation of a cash register, you probably wouldn’t want other parts of the application to access the cash register total if you’re worried about security, so you might choose private. On the other hand, if you think that you’ll create derived classes that have added features involving the cash (such as automatically sending the money to a bank via an electronic transaction), you’d want the members to be protected. In general, developers often choose protected, rather than private, because they’ve been bitten too many times by using classes that have too many private members. In those cases, you’re unable to derive useful classes because everything is private!
The encapsulation process matters more than simply enclosing code in an easily accessed form. When you design objects and classes, you encapsulate your information into individual objects. If you keep the process in mind, you’ll be better off. Here are the things you need to do every time you design a class:
Sale()
, add the new behavior to a new method, Sale2()
.A common saying in object-oriented programming is that you should never make your properties public. The idea is that if users of the object can easily make changes to the object’s properties, a big mess could result. Previous sections mention properties and then talk about special methods as well. There are two methods of accessing property values in C++, and most developers today implement both when possible:
setValue()
and getValue()
. This is the approach that you can easily use with all versions of C++ and is the only officially supported technique.Property approach: Developers who have a background in other languages, such as C#, prefer the property approach, in which you have the object name, a dot, and then the property you want to change, such as MyObject.Value
. The selection of getter or setter is automatic, based on context. To implement this approach, you must either use the correct C++ language product, such as Microsoft C++, or create a specialized class.
The article at
https://www.codeproject.com/Articles/118921/C-Properties
tells you about the Microsoft approach to creating properties, which involves creating a standard getter, setter, or both and then relying on __declspec()
to define the property. The discussion at https://stackoverflow.com/questions/8368512/does-c11-have-c-style-properties
describes a number of methods you can use with standards-based C++, including the creation of a template. The point is that you can use the property approach with C++, but it requires some additional work.
The ImplementProperties
example, shown in Listing 3-1, demonstrates the process for working with read-only, read/write, and write-only properties for a class.
LISTING 3-1: Working with Properties
#include <iostream>
using namespace std;
class MyDog {
protected:
string _Name;
int _Weight = 300;
bool _IsHealthy = false;
public:
// Properties
string getName() {
return _Name;
}
int getWeight() {
return _Weight;
}
void setWeight(int Weight) {
if (Weight > 0)
_Weight = Weight;
}
void setIsHealthy(bool IsHealthy) {
if (_Weight > 200)
_IsHealthy = false;
else
_IsHealthy = IsHealthy;
}
// Methods
MyDog(string Name);
void DoDogRun();
};
MyDog::MyDog(string Name) {
if (Name.length() == 0)
throw "Error: Couldn't create MyDog!";
MyDog::_Name = Name;
}
void MyDog::DoDogRun() {
if (MyDog::_IsHealthy)
cout << MyDog::_Name << " is running!" << endl;
else if (MyDog::_Weight > 200)
cout << MyDog::_Name << " is too fat to run!" << endl;
else
cout << MyDog::_Name
<< " is unhealthy; see vet first!" << endl;
}
int main() {
MyDog *ThisDog;
try {
// Uncomment to generate an error.
//ThisDog = new MyDog("");
ThisDog = new MyDog("Fred");
} catch (const char *msg) {
cerr << msg << endl;
return -1;
}
cout << ThisDog->getName() << " needs exercise."
<< endl;
ThisDog->DoDogRun();
ThisDog->setWeight(100);
ThisDog->DoDogRun();
ThisDog->setIsHealthy(true);
ThisDog->DoDogRun();
delete ThisDog;
ThisDog = 0;
return 0;
}
The code begins by creating protected properties. The default dog doesn’t have a name, but it does weigh 300 pounds and is definitely unhealthy. The properties provide setter and getter code as needed. For example, you don’t want to change the dog’s name after you create it, but you do want to change its weight as needed. Keeping the dog’s health state a secret provides personal protections for the dog, so you can set it, but you can’t get it. Notice how you use the getters and setters to interact with the data. For example, you can’t set the dog’s weight to a negative amount.
Error: Couldn't create MyDog!
At this point, you can interact with ThisDog
in the same way that you interact with any other object. The example discovers that the poor dog needs exercise, but you can’t exercise the dog at first because he’s too fat. Even after losing weight, Fred needs to become healthy before going out for a good run. However, look at the setIsHealthy()
code. If Fred weighs more than 200 pounds, the code ignores that the input value indicates that Fred still isn’t healthy. Here is the output from this example:
Fred needs exercise.
Fred is too fat to run!
Fred is unhealthy; see vet first!
Fred is running!
One of the great powers in C++ is the capability to take a class and build new classes from it. When you use any of the available C++ libraries, such as the Standard C++ Library, you will probably encounter many classes — sometimes dozens of classes — that are all related to each other. Some classes are derived from other classes, although some classes are stand-alone. This gives programmers great flexibility. It’s good for a class library to be flexible because when you’re using a flexible library, you have many choices in the different classes you want to use.
When you design a class, you have the option of deriving the class you’re creating from an original base class — creating a child/parent relationship. The new class inherits the capabilities and characteristics of the base class. Normally, the members that are public in the base class will remain public in the derived class. The members that are protected in the base class will remain protected in the derived class; thus, if you derive even further, those final classes will also inherit the protected members. Private members, however, live only in the base class.
Suppose you have a base class called FrozenFood
, and from there you derive a class called FrozenPizza
. From FrozenPizza
, you then derive a class called DeepDishPizza
. FrozenFood
is at the top of the hierarchy. It includes various members common to all classes. Now suppose that the FrozenFood
class has the following properties:
int Price
(private): This is a private variable that represents the price of the product.int Weight
(protected): This is a protected variable that represents the weight of the product.The FrozenFood
class also has these methods:
Price
and Weight
properties, respectively.GetPrice()
: This is a public access method that returns the value in the private Price
property.GetWeight()
: This is a public access method that returns the value in the protected Weight
property.To make this concept clearer, it helps to list these items in a box, putting the name of the class (FrozenFood
) at the top of the box. Then the box has a horizontal line through it, and under that you list the properties. Under the properties, you have another line, and then a list of methods, as shown in Figure 3-3.
FIGURE 3-3: You can draw a class by using a box divided into three horizontal sections.
Note that in this figure, you can describe the visibility of each property and method:
+
: Public#
: ProtectedIn C++, you have options for how you derive a class. To understand this, remember that when you derive a class, the derived class inherits the members from the base class. With the different ways to derive a class, you can specify whether those inherited members will be public, protected, or private in the derived class. Here are the options:
Think of it as an order of diminishing accessibility: The highest access is public. When a member is public, users can access the member. The middle access is protected. Users cannot access protected members, but derived classes will have access to the protected members. The lowest access is private. Users cannot access private members, and derived classes can’t, either.
When you derive a class as public, the base class portion of the derived class remains unchanged: Those items that were private remain in the base class portion; therefore, the derived class does not have access to them. Those that were protected are still protected, and those that were public are still public.
But when you derive a class as protected, the base class portion is different from the original base class: Its public members are now protected members of this derived class. The actual base class itself did not change; only the base class portion of the derived class becomes protected. Thus, the members that were public in the base class but are now protected in the derived class are not accessible to other methods and classes.
And finally, if you derive a class as private, the base class portion is again different from the original base class: All its members are now private. Because its members are private, any classes you derive from this newly derived class can’t access these members: They’re private. However, as before, the original base class itself didn’t change.
In C++, you specify the type of inheritance you want in the header line for the derived class. Look at the InheritedMembers
example, shown in Listing 3-2. Notice the three classes at the top of the listing: FrozenFood
, FrozenPizza
, and DeepDishPizza
. FrozenFood
is the base class of FrozenPizza
, and FrozenPizza
is the base class of DeepDishPizza
. Figure 3-4 shows this relationship using arrows to point toward the base class.
FIGURE 3-4: The arrows in this UML diagram point toward the base class.
LISTING 3-2: Specifying the Access Levels of the Inherited Members
#include <iostream>
using namespace std;
class FrozenFood {
private:
int Price;
protected:
int Weight;
public:
FrozenFood(int APrice, int AWeight);
int GetPrice();
int GetWeight();
};
class FrozenPizza : public FrozenFood {
protected:
int Diameter;
public:
FrozenPizza(int APrice, int AWeight, int ADiameter);
void DumpInfo();
};
class DeepDishPizza : public FrozenPizza {
private:
int Height;
public:
DeepDishPizza(int APrice, int AWeight, int ADiameter,
int AHeight);
void DumpDensity();
};
FrozenFood::FrozenFood(int APrice, int AWeight) {
Price = APrice;
Weight = AWeight;
}
int FrozenFood::GetPrice() {
return Price;
}
int FrozenFood::GetWeight() {
return Weight;
}
FrozenPizza::FrozenPizza(int APrice, int AWeight,
int ADiameter) :
FrozenFood(APrice, AWeight) {
Diameter = ADiameter;
}
void FrozenPizza::DumpInfo() {
cout << "\tFrozen pizza info:" << endl;
cout << "\t\tWeight: " << Weight << " ounces" << endl;
cout << "\t\tDiameter: " << Diameter << " inches"
<< endl;
}
DeepDishPizza::DeepDishPizza(int APrice, int AWeight,
int ADiameter, int AHeight) :
FrozenPizza(APrice, AWeight,
ADiameter) {
Height = AHeight;
}
void DeepDishPizza::DumpDensity() {
// Calculate pounds per cubic foot of deep-dish pizza
cout << "\tDensity: ";
cout << Weight * 12 * 12 * 12 * 14 /
(Height * Diameter * 22 * 16);
cout << " pounds per cubic foot" << endl;
}
int main() {
cout << "Thin crust pepperoni" << endl;
FrozenPizza pepperoni(450, 12, 14);
pepperoni.DumpInfo();
cout << "\tPrice: " << pepperoni.GetPrice()
<< " cents" << endl;
cout << "Deep dish extra-cheese" << endl;
DeepDishPizza extracheese(650, 21592, 14, 3);
extracheese.DumpInfo();
extracheese.DumpDensity();
cout << "\tPrice: " << extracheese.GetPrice()
<< " cents" << endl;
return 0;
}
When you run Listing 3-2, you see the following output:
Thin crust pepperoni
Frozen pizza info:
Weight: 12 ounces
Diameter: 14 inches
Price: 450 cents
Deep dish extra-cheese
Frozen pizza info:
Weight: 21592 ounces
Diameter: 14 inches
Density: 35332 pounds per cubic foot
Price: 650 cents
The first five lines show information about the object of class FrozenPizza
. The remaining lines show information about the object of class DeepDishPizza
, including the fact that it weighs 21,592 ounces (which happens to be 1349.5 pounds), It has a density of 35,332 pounds per cubic foot.
The derivations are all public. Thus, the items that were public in FrozenFood
are still public in FrozenPizza
and DeepDishPizza
. Note where the different information in the output comes from. The line Frozen pizza info:
and the two lines that follow (Weight:
and Diameter:
) come from the public method DumpInfo()
, which is a member of FrozenPizza
. DumpInfo()
is public in the FrozenPizza
class. Since DeepDishPizza
derives from FrozenPizza
as public, DumpInfo(
)
is also a public member of DeepDishPizza
.
Try changing the header for DeepDishPizza
from
class DeepDishPizza : public FrozenPizza
to
class DeepDishPizza : protected FrozenPizza
You’re changing the word public
to protected
. Make sure that you change the correct line. Compile and run the application. You see an error that looks similar to this one:
In function 'int main()':
error: 'void FrozenPizza::DumpInfo()' is inaccessible
error: within this context
error: 'FrozenPizza' is not an accessible base of 'DeepDishPizza'
error: 'int FrozenFood::GetPrice()' is inaccessible
error: within this context
error: 'FrozenFood' is not an accessible base of 'DeepDishPizza'
This message refers to the extracheese.DumpInfo();
line in main()
. DumpInfo()
is now a protected
member of DeepDishPizza
, thanks to the word protected
in the class header. By putting the word protected
in the class definition, you’re saying the inherited members that are currently public will instead be protected. Because the DumpInfo()
member is protected, you can’t call it from main()
. However, DumpInfo()
is still public in the FrozenPizza
class, so this call is fine:
pepperoni.DumpInfo();
Change the line back to a public inheritance, as it was in Listing 3-2: class DeepDishPizza : public FrozenPizza
.
And now change the header of FrozenPizza
so that it looks like this:
class FrozenPizza : private FrozenFood
Again, make sure to change the correct lines. Compile and run the application to see the following error:
In function 'int main()':|
error: 'void FrozenPizza::DumpInfo()' is inaccessible|
error: within this context|
error: 'FrozenPizza' is not an accessible base of 'DeepDishPizza'|
error: 'int FrozenFood::GetPrice()' is inaccessible|
error: within this context|
error: 'FrozenFood' is not an accessible base of 'DeepDishPizza'|
This error refers to the line inside DeepDishPizza::DumpDensity()
where the code is trying to access the Weight
member. The compiler doesn’t allow access now because the member, which was public in the original FrozenFood
class, became private when it became a part of FrozenPizza
. And because it’s private in FrozenPizza
, the derived class DeepDishPizza
can’t access it from within its own methods. Make sure to change back the header of FrozenPizza
so that it looks like this: class FrozenPizza : public FrozenFood
.
One of the cool things about classes is that you can declare a method in one class, and then when you derive a new class, you can give that class a different version of the same method. This is called overriding the method. For example, if you have a class FrozenFood
and a derived class FrozenPizza
, you may want to include a method in FrozenFood
called BakeChemistry()
, which modifies the food when it’s baked. Because all foods are different, the BakeChemistry()
method would be different for each class derived from FrozenFood
.
In C++, you can provide a different version of the method for the different derived classes by adding the word virtual
before the method name in the base class declaration, as in this line of code:
virtual void BakeChemistry();
This line is the prototype inside the class definition. Later, you would provide the code for this method. In the class for your derived class, you would then just put the method prototype, without the word virtual
:
void BakeChemistry();
And as before, you would include the code for the method later on. For example, you might have something like the following example. First, here are the classes:
class FrozenFood {
private:
int Price;
protected:
int Weight;
public:
FrozenFood(int APrice, int AWeight);
int GetPrice();
int GetWeight();
virtual void BakeChemistry();
};
class FrozenPizza : public FrozenFood {
protected:
int Diameter;
public:
FrozenPizza(int APrice, int AWeight, int ADiameter);
void DumpInfo();
void BakeChemistry();
};
You can see the word virtual
in the FrozenFood
class, and then you see the method declaration again in the FrozenPizza
class. Now, here are the BakeChemistry()
methods:
void FrozenFood::BakeChemistry() {
cout << "Baking, baking, baking!" << endl;
}
void FrozenPizza::BakeChemistry() {
cout << "I'm getting crispy!" << endl;
}
Note that the word virtual
doesn’t appear in front of the methods; it appears only in the class declaration. Now, whenever you make an instance of each class and call BakeChemistry()
for each instance, you call the one for the given class. Consider the following two lines of code:
FrozenPizza pepperoni(450, 12, 14);
pepperoni.BakeChemistry();
Because pepperoni
is an instance of FrozenPizza
, this code calls the BakeChemistry()
for the FrozenPizza
class, not for the FrozenFood
class. You may not want any code in your base class for the BakeChemistry()
method. If so, you can do this:
virtual void BakeChemistry() {}
class FrozenFood
{
private:
int Price;
protected:
int Weight;
public:
FrozenFood(int APrice, int AWeight);
int GetPrice();
int GetWeight();
virtual void BakeChemistry() {}
};
Suppose you have a method called Bake()
and you want it to take as a parameter a FrozenFood
instance. If you derive FrozenPizza
from FrozenFood
and then derive DeepDishPizza
from FrozenPizza
, by the “is a” rule, objects of the class FrozenPizza
and DeepDishPizza
are both examples of FrozenFood
objects. This is true in general: If you have a class called Base
and you derive from that a class called Derived
, instances of class Derived
are also instances of class Base
. Therefore, if you have a method called Bake()
and you declare it as follows, you are free to pass to this method a FrozenFood
instance or to pass an instance of any class derived from FrozenFood
, such as FrozenPizza
or DeepDishPizza
:
void Bake(FrozenFood *)
{
cout << "Baking" << endl;
}
Suppose that in this Bake()
method, you set the oven temperature to a fixed amount, turn on the oven, and then cook the food. Every food behaves differently in the oven. For example, a deep-dish frozen pizza might rise and become thicker, but a regular frozen pizza will become crispier but not get any thicker.
You don’t really want to put all the different food types inside the Bake()
method, with if
statements for each food type. Instead, you can put the actual baking chemistry in the class for the food itself. The FrozenPizza
would have its own BakeChemistry()
method, and the DeepDishPizza
would also have its own BakeChemistry()
method. Then the Bake()
method would call BakeChemistry()
for whatever object it receives as a parameter. C++ knows how to do this because of the virtual methods. The Bake()
method doesn’t even know or care what type of FrozenFood
it receives. It just calls BakeChemistry()
for whatever object it receives. And when you modify the application by writing a new class derived from FrozenFood
and give it its own BakeChemistry()
method, you can pass an instance of this class to Bake()
, without even having to modify Bake()
. This whole process is called polymorphism.
When you create a base class with a virtual method and then derive other classes, you may want to override the virtual method in all the derived classes. Furthermore, you may want to make sure that nobody ever creates an instance of the base class. You do this because the base class might contain basic things that are common to all the other classes, but the class itself doesn’t make much sense as an instance. For example, no one would want you to go to the store and pick up a frozen food without specifying the sort of frozen food to get. Consequently, it doesn’t make much sense to have an instance of a class called FrozenFood
.
In C++, you don’t actually specify that the class is abstract. The word abstract doesn’t appear in the language. To specify that the class is abstract, you add at least one virtual method that has no code. But instead of just putting an empty code block, as in {}
, you follow the method prototype in the class definition with = 0
(called the pure specifier, which makes the method a pure virtual method or an abstract virtual method, depending on which you prefer), as in
class FrozenFood
{
private:
int Price;
protected:
int Weight;
public:
FrozenFood(int APrice, int AWeight);
int GetPrice();
int GetWeight();
virtual void BakeChemistry() = 0;
};
In this class definition, the method BakeChemistry()
has = 0
after it (but before the semicolon). The = 0
transforms the virtual method into an abstract virtual method, which transforms the class into an abstract class.
After you create an abstract class, you must also modify the derived classes by overriding the abstract virtual method. Otherwise, the derived classes will also be abstract. When your class is abstract, you can’t create instances of it. To override the abstract virtual method, you override as you would with any virtual method. This class includes a method that overrides the BakeChemistry()
method:
class FrozenPizza : public FrozenFood
{
protected:
int Diameter;
public:
FrozenPizza(int APrice, int AWeight, int ADiameter);
void DumpInfo();
void BakeChemistry();
};
Then you provide the code for the BakeChemistry()
method, as in
void FrozenPizza::BakeChemistry()
{
cout << "I'm getting crispy under this heat!" << endl;
}
There’s nothing magical about defining the override method, but you are required to override it if you want to create an instance of this class.