Chapter 1
IN THIS CHAPTER
Understanding objects and classes
Becoming familiar with methods and properties
Making parts of a class public, private, and protected
Using constructors and destructors
Building hierarchies of classes
Back in the early 1990s, the big buzzword in the computer world was object-oriented. For anything to sell, it had to be object-oriented. Programming languages were object-oriented. Software applications were object-oriented. Computers were object-oriented. Unfortunately, object-oriented was simply a cool catchphrase at the time that meant little in real terms. Often, ideas begin poorly formed and gain resolution as people work to implement the idea in the real world.
Now it’s possible to explore what object-oriented really means and how you can use it to organize your C++ applications. In this chapter, you discover object-oriented programming and see how you can do it in C++. Although people disagree on the strict definition of object-oriented, in this book it means programming with objects and classes.
Consider a pen, a regular, old pen. Here’s what you can say about it:
Now, look around for other things, such as a printer. Here’s a description of a printer:
These lists describe the objects you might see. They provide dimensions, color, model, brand, and other details. The lists also describe what the objects can do. The pen can break in half and run out of ink. The printer can take print jobs, print pages, and have its cartridges replaced.
When describing what objects can do, you carefully write it from the perspective of the object itself, not from the perspective of the person using the object. A good way to name the capability is to test it by preceding it with the words “I can” and see if it makes sense. Thus, because “I can write on paper” works from the perspective of a pen, the list contains write on paper for one of the pen’s capabilities. But is seeing all the objects in the universe possible, or are some objects hidden? Certainly, some objects are physical, like atoms or the dark side of the moon, and you can’t see them. But other objects are abstract. For example, you may have a credit card account. What is a credit card account, exactly? A credit card account is abstract because you can’t touch it — it has no physical presence. The following sections of the chapter examine various kinds of objects: those with physical representations and those that are abstract.
When you pick up a pen, you can ask somebody, “What type of object is this an instance of?” Most people would probably say, “a pen.” In computer programming, instead of using type of object, you say class. This thing in your hand belongs to the pen class. Now if you point to the object parked out in the driveway and ask, “What class does that belong to?” the answer is, “class Car.” Of course, you could be more specific. You may say that the object belongs to class 2020 Ford Taurus.
When you see a pen, you might ask what class this object belongs to. If you then pick up another pen, you see another example of the same class. One class; several examples. If you stand next to a busy street, you see many examples of the class called car. Or you may see many examples of the class Ford Explorer, a few instances of the class Toyota Corolla, and so on. It depends on how you classify those objects roaring down the road. Regardless, you likely see several examples of any given class.
So when you organize things, you specify a class, which is the type of object. And when you’re ready, you can start picking out examples (or instances) of the class. Each class may have several instances. Some classes have only one instance. That’s a singleton class. For example, at any given time, the class United States President would have one instance.
If you choose a class, you can describe its characteristics. However, because you’re describing only the class characteristics, you don’t actually specify them. You may say the pen has an ink color, but you don’t actually say what color. That’s because you don’t yet have an example of the class Pen
. You have only the class itself. When you finally find an example, it may be one color, or it may be another. So, if you’re describing a class called Pen
, you may list the characteristics presented in the introduction to this section.
You don’t specify ink color, shell color, length, or any of these properties (terms that describe the class) as actual values. You’re listing only general characteristics for all instances of the class Pen
. That is, every pen has these properties. But the actual values for these properties might vary from instance to instance. One pen may have a different ink color from another, but both might have the same brand. Nevertheless, they are both separate instances of the class Pen
.
After creating an instance of class Pen
, you can provide values for the properties. For example, Table 1-1 lists the property values of three actual pens.
TABLE 1-1 Specifying Property Values for Instances of Class Pen
Property Name |
First Pen |
Second Pen |
Third Pen |
---|---|---|---|
Ink Color |
Blue |
Red |
Black |
Shell Color |
Grey |
Red |
Grey |
Cap Color |
Blue |
Black |
Black |
Style |
Ballpoint |
Fountain |
Felt-tip |
Length |
5.5 inches |
5 inches |
6 inches |
Brand |
Office Depot |
Parker |
Paper Mate |
Ink Level |
30% |
60% |
90% |
In Table 1-1, the first column holds the property names. The second column holds property values for the first pen. The third column holds the property values for the second pen, and the final column holds the property values for the third pen. All the pens in the class share properties. But the values for these properties may differ from pen to pen. When you instantiate (build or create) a new Pen
, you follow the list of properties, giving the new pen instance its own values. You may make the shell purple with yellow speckles, or you may make it transparent. But you would give it a shell that has some color, even if that color is transparent.
In Table 1-1, you didn’t see a list of methods (ways of interacting with the Pen
class to exercise its capabilities). But all these pens have the same methods:
Unlike properties, methods don’t change from instance to instance. They are the same for each class.
To implement a class in C++, you use the keyword class
. And then you add the name of the class, such as Pen
. You then add an open brace, list your properties and methods, and end with a closing brace.
The PenClass
example, shown in Listing 1-1, contains a C++ class description that appears inside the Pen.h
header file. (See Book 1, Chapter 7, for information on how to put code in a header file.) Review the header file, and you see how it implements the different characteristics. The properties of a header file are just like variables: They have a type and a name. The methods are implemented using functions. All this code goes inside curly brackets and is preceded by a class header. The header gives the name of the class. And, oh yes, the word public is stuck in there, and it has a colon after it. The “Accessing members,” section later in this chapter explains the word public
. By itself, this code isn’t very useful, but you put it to use in Listing 1-2, an application that you can actually compile and run.
LISTING 1-1: Pen.h Contains the Class Description for Pen
#ifndef PEN_H_INCLUDED
#define PEN_H_INCLUDED
using namespace std;
enum Color {
blue,
red,
black,
clear,
grey
};
enum PenStyle {
ballpoint,
felt_tip,
fountain_pen
};
class Pen {
public:
Color InkColor;
Color ShellColor;
Color CapColor;
PenStyle Style;
float Length;
string Brand;
int InkLevelPercent;
void write_on_paper(string words) {
if (InkLevelPercent <= 0) {
cout << "Oops! Out of ink!" << endl;
}
else {
cout << words << endl;
InkLevelPercent = InkLevelPercent - words.length();
}
}
void break_in_half() {
InkLevelPercent = InkLevelPercent / 2;
Length = Length / 2.0;
}
void run_out_of_ink() {
InkLevelPercent = 0;
}
};
#endif // PEN_H_INCLUDED
Note in Listing 1-1, earlier in this chapter, that the methods access the properties. However, we said that these variables don’t have values yet, because this is just a class, not an instance of a class. How can that be? When you create an instance of this class, you can give values to these properties. Then you can call the methods. And here’s the really great part: You can make a second instance of this class and give it its own values for the properties. Yes, the two instances will each have their own sets of properties. And when you run the methods for the second instance, these functions operate on the properties for the second instance. Isn’t C++ smart? Now look at Listing 1-2. This is a source file that uses the header file in Listing 1-1. In this code, you see the Pen
class in action.
LISTING 1-2: main.cpp Contains Code That Uses the Class Pen
#include <iostream>
#include "Pen.h"
using namespace std;
int main() {
Pen FavoritePen;
FavoritePen.InkColor = blue;
FavoritePen.ShellColor = grey;
FavoritePen.CapColor = blue;
FavoritePen.Style = ballpoint;
FavoritePen.Length = 5.5;
FavoritePen.Brand = "Office Depot";
FavoritePen.InkLevelPercent = 30;
Pen WorstPen;
WorstPen.InkColor = red;
WorstPen.ShellColor = red;
WorstPen.CapColor = black;
WorstPen.Style = fountain_pen;
WorstPen.Length = 5.0;
WorstPen.Brand = "Parker";
WorstPen.InkLevelPercent = 60;
cout << "This is my favorite pen" << endl;
cout << "Color: " << FavoritePen.InkColor << endl;
cout << "Brand: " << FavoritePen.Brand << endl;
cout << "Ink Level: " << FavoritePen.InkLevelPercent
<< "%" << endl;
FavoritePen.write_on_paper("Hello I am a pen");
cout << "Ink Level: " << FavoritePen.InkLevelPercent
<< "%" << endl;
return 0;
}
There are two variables of class Pen
: FavoritePen
and WorstPen
. To access the properties of these objects, you type the name of the variable holding the object, a dot (or period), and then the property name. For example, to access the InkLevelPercent
member of WorstPen
, you type:
WorstPen.InkLevelPercent = 60;
Remember, WorstPen
is the variable name, and this variable is an object. It is an object or an instance of class Pen
. This object has various properties, including InkLevelPercent
.
You can also run some of the methods that are in these objects. This code calls:
FavoritePen.write_on_paper("Hello I am a pen");
This called the function write_on_paper()
for the object FavoritePen
. Look at the code for this function, which is in the header file, Listing 1-1:
void write_on_paper(string words) {
if (InkLevelPercent <= 0) {
cout << "Oops! Out of ink!" << endl;
}
else {
cout << words << endl;
InkLevelPercent = InkLevelPercent - words.length();
}
}
This function uses the variable called InkLevelPercent
. But InkLevelPercent
isn’t declared in this function. The reason is that InkLevelPercent
is part of the object and is declared in the class. Suppose you call this method for two different objects, as in the following:
FavoritePen.write_on_paper("Hello I am a pen");
WorstPen.write_on_paper("Hello I am another pen");
The first of these lines calls write_on_paper()
for the FavoritePen
object; thus, inside the code for write_on_paper()
, the InkLevelPercent
refers to InkLevelPercent
for the FavoritePen
object. It looks at and possibly decreases the variable for that object only. But WorstPen
has its own InkLevelPercent
property, separate from that of FavoritePen
. So in the second of these two lines, write_on_paper()
accesses and possibly decreases the InkLevelPercent
that lives inside WorstPen
. In other words, each object has its own InkLevelPercent
. When you call write_on_paper()
, the function modifies the property based on which object you are calling it with. The first line calls it with FavoritePen
. The second calls it with WorstPen
. When you run this application, you see the following output:
This is my favorite pen
Color: 0
Brand: Office Depot
Ink Level: 30%
Hello I am a pen
Ink Level: 14%
You should notice something about the color line. Here’s the line of code that writes it:
cout << "Color: " << FavoritePen.InkColor << endl;
This line outputs the InkColor
member for FavoritePen
. But what type is InkColor
? It’s the new Color
enumerated type. But something is wrong. It printed 0
despite being set as follows:
FavoritePen.InkColor = blue;
The code sets it to blue
, not 0
. Unfortunately, that’s the breaks with using enum
. You can use it in your code, but under the hood, it just stores numbers. When printed, you get a number. The compiler chooses the numbers for you, and it starts the first entry in the enum
list as 0
, the second as 1
, then 2
, then 3
, and so on. Thus, blue
is stored as 0
, red
as 1
, black
as 2
, clear
as 3
, and grey
as 4
. Fortunately, people have found a way to create a new class that handles the enum
for you (that is, it wraps around the enum
), and then you can print what you really want: blue
, red
, black
, clear
, and grey
. Book 2, Chapter 2 has tips on how to do this astounding feat.
When you work with functions, you can either make sure that the code to your function is positioned before any calls to the function, or you can use a forward reference, also called a function prototype. Book 1, Chapter 6 discusses this feature.
When you work with classes and methods, you have a similar option. Most C++ programmers prefer to keep the code for their methods outside the class definition. The reason for placing them outside is to make the code easier to read; you don’t end up with a single, huge block of code that is incredibly difficult to follow. In addition, someone using the class may not care about how the methods work, so keeping things simple is the best option. The class definition contains only method prototypes, or, at least, mostly method prototypes. If the method is one or two lines of code, people may leave it in the class definition.
When you use a method prototype in a class definition, you write the prototype by ending the method header with a semicolon where you would normally have the open brace and code. If your method looks like this:
void break_in_half() {
InkLevelPercent = InkLevelPercent / 2;
Length = Length / 2.0;
}
a method prototype would look like this:
void break_in_half();
After you write the method prototype in the class, you write the method code again outside the class definition. However, you need to doctor it up just a bit. In particular, you need to throw in the name of the class, so that the compiler knows which class this method goes with. The following is the same method described earlier, but with the class information included. You separate the class name and method name with a scope resolution operator (::
) that links the method to the class:
void Pen::break_in_half() {
InkLevelPercent = InkLevelPercent / 2;
Length = Length / 2.0;
}
You put the method after your class definition. And you would want to put the method code inside one of your source code files if your class definition is in a header file.
The PenClass2
example, shown in Listings 1-3 and 1-4, contains the modified version of the Pen
class that appeared earlier in this chapter in Listing 1-1. You can use these two files together with Listing 1-2, which hasn’t changed.
LISTING 1-3: Using Method Prototypes with the Modified Pen.h file
#ifndef PEN_H_INCLUDED
#define PEN_H_INCLUDED
using namespace std;
enum Color {
blue,
red,
black,
clear,
grey
};
enum PenStyle {
ballpoint,
felt_tip,
fountain_pen
};
class Pen {
public:
Color InkColor;
Color ShellColor;
Color CapColor;
PenStyle Style;
float Length;
string Brand;
int InkLevelPercent;
void write_on_paper(string words);
void break_in_half();
void run_out_of_ink();
};
#endif // PEN_H_INCLUDED
LISTING 1-4: Containing the Methods for Class Pen in the New Pen.cpp File
#include <iostream>
#include "Pen.h"
using namespace std;
void Pen::write_on_paper(string words) {
if (InkLevelPercent <= 0) {
cout << "Oops! Out of ink!" << endl;
}
else {
cout << words << endl;
InkLevelPercent = InkLevelPercent - words.length();
}
}
void Pen::break_in_half() {
InkLevelPercent = InkLevelPercent / 2;
Length = Length / 2.0;
}
void Pen::run_out_of_ink() {
InkLevelPercent = 0;
}
All the functions from the class are now in a separate source (.cpp
) file. The header file now just lists prototypes and is a little easier to read. The source file includes the header file at the top. That’s required; otherwise, the compiler won’t know that Pen
is a class name, and it will get confused (as it so easily can).
Here is a summary of the parts of a class and the different ways classes can work together:
When you divide the class, you put part in the header file and part in the source code file. The following list describes what goes where:
.h
or .hpp
extension. Thus, the class Pen
, for instance, might be in the file Pen.h
.::
). If you named the header file the same as the class, you probably want to name the source file the same as the class as well but with a .cpp
extension.Many handy tricks are available for working with classes. In this section, you explore several clever ways of working with classes, starting with the way you can hide certain parts of your class from other functions that are accessing them.
When you work with an object in real life, there are often parts of the object that you interact with and other parts that you don’t. For example, when you use the computer, you type on the keyboard but don’t open the box and poke around with a wire attached to a battery. For the most part, the stuff inside is off-limits except when you’re upgrading it.
In object terminology, the words public and private refer to properties and methods. When you design a class, you might want to make some properties and methods freely accessible by class users. You may want to keep other members tucked away. A class user is the part of an application that creates an instance of a class and calls one of its methods. In Listing 1-2, earlier in the chapter, main()
is a class user. If you have a function called FlippityFlop()
that creates an instance of your class and does a few things to the instance, such as change some its properties, FlippityFlop()
is a class user. In short, a user is any function that accesses your class.
When designing a class, you may want only specific users calling certain methods. You may want to keep other methods hidden away, to be called only by other methods within the class. Suppose you’re writing a class called Oven
. This class includes a method called Bake()
, which takes a number as a parameter representing the desired oven temperature. Now you may also have a method called TurnOnHeatingElement()
and one called TurnOffHeatingElement()
.
Here’s how it would work. The Bake()
method starts out calling TurnOnHeatingElement()
. Then it keeps track of the temperature, and when the temperature is correct, it calls TurnOffHeatingElement()
. You wouldn’t want somebody walking in the kitchen and calling the TurnOnHeatingElement()
method without touching any of the dials, only to leave the room as the oven gets hotter and hotter with nobody watching it. You allow the users of the class to call only Bake()
. The other two methods, TurnOnHeatingElement()
and TurnOffHeatingElement()
, are reserved for use only by the Bake()
function.
The OvenClass
example, shown in Listing 1-5, defines a sample Oven
class and a main()
that uses it. Look at the class definition. It has two sections: one private and the other public. The code for the functions appears after the class definition. The two private functions don’t do much other than print a message. (Although they’re also free to call other private functions in the class.) The public function, Bake()
, calls each of the private functions, because it’s allowed to.
LISTING 1-5: Using the Public and Private Words to Hide Parts of Your Class
#include <iostream>
using namespace std;
class Oven {
private:
void TurnOnHeatingElement();
void TurnOffHeatingElement();
public:
void Bake(int Temperature);
};
void Oven::TurnOnHeatingElement() {
cout << "Heating element is now ON! Be careful!" << endl;
}
void Oven::TurnOffHeatingElement() {
cout << "Heating element is now off. Relax!" << endl;
}
void Oven::Bake(int Temperature) {
TurnOnHeatingElement();
cout << "Baking!" << endl;
TurnOffHeatingElement();
}
int main() {
Oven fred;
fred.Bake(875);
return 0;
}
When you run this application, you see some messages:
Heating element is now ON! Be careful!
Baking!
Heating element is now off. Relax!
Nothing too fancy here. Now if you tried to include a line in your main()
such as the one in the following code, where you call a private function
fred.TurnOnHeatingElement();
you see an error message telling you that you can’t do it because the function is private. In Code::Blocks, you see this message:
error: 'void Oven::TurnOnHeatingElement()' is private
class Oven {
public:
void Bake(int Temperature);
private:
void TurnOnHeatingElement();
void TurnOffHeatingElement();
public:
void Broil();
};
This and other sections of the chapter discuss the use of raw pointers with objects. In the “Understanding the Changes in Pointers for C++ 20” section of Book 1, Chapter 8, you discover that there are other pointer types, including smart and optional pointers. Because most code still relies on raw pointers to work with objects, the majority of this chapter focuses on their use.
As with any variable, you can have a pointer variable that points to an object. As usual, the pointer variable’s type must match the type of the class. This creates a pointer variable that points to a Pen
instance:
Pen *MyPen;
The variable MyPen
is a pointer, and it can point to an object of type Pen
. The variable’s own type is pointer to Pen
, or in C++ notation, Pen *
. Because you’re always working with pointers when interacting with objects, you leave ptr off the variable name to save typing time and focus attention on the variable’s purpose, which is to serve as your personal pen.
After you create the variable MyPen
, you can create an instance of class Pen
and point MyPen
to it using the new
keyword, like so:
MyPen = new Pen;
Or you can combine both Pen *MyPen;
and the preceding line:
Pen *MyPen = new Pen;
Now you have two variables: You have the actual object, which is unnamed and sitting on the heap. (See the “Heaping and Stacking the Variables” section of Book 1, Chapter 8, for more information on pointers and heaps.) You also have the pointer variable, which points to the object: two variables working together. Because the object is out on the heap, the only way to access it is through the pointer. To access the members through the pointer, you use a special notation — a minus sign followed by a greater-than sign. It bears a passing resemblance to an arrow (and is therefore called the arrow operator), as the following line makes clear:
MyPen->InkColor = red;
This goes through the MyPen
pointer to set the InkColor
property of the object to red
.
As with other variables you created with new
, after you are finished using an object, you should call delete
to free the memory used by the object pointed to by MyPen
. To do so, start with the word delete
and then the name of the object pointer, MyPen
, as in the following:
delete MyPen;
The PenClass3
example, shown in Listing 1-6, demonstrates the process of declaring a pointer, creating an object and pointing to it, accessing the object’s members through the pointer, deleting the object, and clearing the pointer back to 0
.
LISTING 1-6: Managing an Object’s Life
#include <iostream>
#include "../PenClass2/Pen.h"
using namespace std;
int main() {
Pen *MyPen;
MyPen = new Pen;
MyPen->InkColor = red;
cout << MyPen->InkColor << endl;
delete MyPen;
MyPen = 0;
return 0;
}
TABLE 1-2 Steps to Using Objects
Step |
Sample Code |
Action |
---|---|---|
1 |
|
Declares the pointer |
2 |
|
Calls |
3 |
|
Accesses the members of the object through the pointer |
4 |
|
Deletes the object |
5 |
|
Clears the pointer |
Now that you have an overview of the process through Listing 1-6 and understand the basics through Table 1-2, you can see how to formalize the procedure. The following steps describe precisely how to work with raw pointers and objects:
Declare the pointer.
The pointer must match the type of object you intend to work with, except that the pointer’s type name in C++ is followed by an asterisk, *
.
Call new
, passing the class name, and store the results of new
in the pointer.
You can combine Steps 1 and 2 into a single step.
Access the object’s members through the pointer with the arrow operator, ->
.
You could dereference the pointer and put parentheses around it, but everyone uses the shorthand notation.
When you are finished with the pointer, call delete
.
This step frees the object from the heap. Remember that this does not delete the pointer itself, but frees the object memory.
Clear the pointer by setting it to 0
.
If your
delete
statement is at the end of the application, you don’t need to clear the pointer to 0
because the pointer is going out of scope. The pointer won’t exist any longer, so setting it to 0
isn’t essential, but it’s good practice because you get into the habit of doing it in places where clearing the pointer to 0
would be important.
If you’re working with C++ 17 or above, you probably want to use smart pointers with your objects, rather than the labor-intensive and error-prone raw pointers. The SmartPtr
example, shown in Listing 1-7, shows the same process as Listing 1-6 but uses smart pointers instead. You still need to add Pen.cpp
and Pen.h
from PenClass2.
LISTING 1-7: Managing an Object’s Life Using Smart Pointers
#include <iostream>
#include <memory>
#include "../PenClass2/Pen.h"
using namespace std;
int main() {
unique_ptr<Pen> MyPen;
MyPen.reset(new Pen());
MyPen->InkColor = red;
cout << MyPen->InkColor << endl;
MyPen.reset();
return 0;
}
Notice that you still use the arrow operator to assign the color red
to MyPen->InkColor
and to retrieve the value later. This part of the code appears the same as when using a raw pointer. The final step is to free the object memory using reset()
. The pointer will automatically delete itself, saving you a line of code in this example.
When you write a function, normally you base your decision about using pointers on whether or not you want to change the original variables passed into the function. Suppose you have a function called AddOne()
, and it takes an integer as a parameter. If you want to modify the original variable, you can use a pointer (or you can use a reference). If you don’t want to modify the variable, just pass the variable by value.
The following prototype represents a function that can modify the variable passed into it:
void AddOne(int *number);
And this prototype represents a function that cannot modify the variable passed into it:
void AddOne(int number);
With objects, you can do something similar. For example, this function takes a pointer to an object and can, therefore, modify the object:
void FixFlatTire(Car *mycar);
This version doesn’t allow modification of the original object:
void FixFlatTire(Car mycar);
However, unlike a primitive type, the function gets its own instance. In other words, every time you call this function, it creates an entirely new instance of class Car
. This instance would be a duplicate copy of the myCar
object that is an instance of class Car
— it wouldn’t be the same instance.
When you work with objects, a complete copy is not always a sure thing. The original object may have properties that are pointers to other objects, but the object copy may not get copies of those pointers. The properties that contain pointers may end up blank (due to a lack of proper copying technique), point to the same values as the original (a shallow copy), or point to new variables (a deep copy). The difference is the kind of copy that the object provides:
Because your function receives its objects as pointers, you continue accessing them by using the arrow operator. For example, the function FixFlatTire()
may do this:
void FixFlatTire(Car *mycar) {
mycar->RemoveTire();
mycar->AddNewTire();
}
Or, if you prefer references, you would do this:
void FixFlatTire2(Car &mycar) {
mycar.RemoveTire();
mycar.AddNewTire();
}
Remember that pointers contain the address of an object, while a reference is simply another name (alias) for an object. Even though the reference is still an address, it’s the actual address of the object, rather than a pointer to the object. (Book 1, Chapter 8 discusses pointers in more detail.) In this code, because you’re dealing with a reference, you access the object’s members using the dot operator (.
) rather than the arrow operator (->
).
A constant is a variable or object that another function can’t change even when you pass a reference to it to another function. To define a variable or an object as constant, unchangeable, you use the const
keyword. For example, to define a variable as constant, you use:
const int MyInt = 3;
If someone were to come along and try to use this code:
MyInt = 4;
The compiler would display an error message saying, error: assignment of read-only variable ’MyInt’
. The same holds true for a function using a const
primitive like this one:
void DisplayInt(const int Value) {
cout << Value << endl;
}
It’s possible to display Value
or interact with it in other ways, but trying to change Value
will raise an error. This version will raise an error because Value
is being changed:
void DisplayInt(const int Value) {
Value += 1;
cout << Value << endl;
}
The const
keyword is useful when working with objects because you generally don’t want to pass an object directly. That involves copying the object, which is messy. Instead, you normally pass by using a pointer or reference, which would allow you to change the object. If you put the word const
before the parameter, the compiler won’t allow you to change the parameter. The PenClass4
example that appears in Listing 1-8 has const
inserted before the parameter. The function can look at the object but can’t change it.
LISTING 1-8: The Inspect Function Is Not Allowed to Modify Its Parameter
#include <iostream>
#include "../PenClass2/Pen.h"
using namespace std;
void Inspect(const Pen *Checkitout) {
cout << Checkitout->Brand << endl;
}
int main() {
Pen *MyPen = new Pen();
MyPen->Brand = "Spy Plus Camera";
Inspect(MyPen);
return 0;
}
Now suppose that you tried to change the object in the Inspect
function. You may have put a line in that function like this:
Checkitout->Length = 10.0;
If you try this, the compiler issues an error. In Code::Blocks, you get: error: assignment of member ’Pen::Length’ in read-only object
.
void Inspect(const Pen *Checkitout, Spy *one,
const Spy *two);
Consider a function called OneMoreCheeseGone()
. It’s not a method, but it takes an object of instance Cheese
as a parameter. Its prototype looks like this:
void OneMoreCheeseGone(Cheese *Block);
This is just a simple function with no return type. It takes an object pointer as a parameter. For example, after you eat a block of cheese, you can call:
OneMoreCheeseGone(MyBlock);
Now consider this: If you have an object on the heap, it has no name. You access it through a pointer variable that points to it. But what if the code is currently executing inside a method of an object? How do you refer to the object itself?
C++ has a secret variable that exists inside every method: this
. It’s a pointer variable. The this
variable always points to the current object. So if code execution is occurring inside a method and you want to call OneMoreCheeseGone()
, passing in the current object (or block of cheese), you would pass this
.
The following sections discuss what you might call the standard use of this
, the version of this
that exists in most code now. Once you understand the standard use of this
, you move on to modifications to this
that occur in C++ 20. Like most pointer usage in C++ 20, this
has undergone changes to make it safer, smarter, and easier.
This section tells you how this
is used for application development in most applications today. The CheeseClass
example, shown in Listing 1-9, demonstrates this
.
LISTING 1-9: Passing an Object from Inside Its Methods by Using the this Variable
#include <iostream>
using namespace std;
class Cheese {
public:
string status;
void eat();
void rot();
};
int CheeseCount;
void OneMoreCheeseGone(Cheese *Block) {
CheeseCount--;
Block->status = "Gone";
};
void Cheese::eat() {
cout << "Eaten up! Yummy" << endl;
OneMoreCheeseGone(this);
}
void Cheese::rot() {
cout << "Rotted away! Yuck" << endl;
OneMoreCheeseGone(this);
}
int main() {
Cheese *asiago = new Cheese();
Cheese *limburger = new Cheese();
CheeseCount = 2;
asiago->eat();
limburger->rot();
cout << endl;
cout << "Cheese count: " << CheeseCount << endl;
cout << "asiago: " << asiago->status << endl;
cout << "limburger: " << limburger->status << endl;
return 0;
}
The this
listing has four main parts. First is the definition for the class called Cheese
. The class contains a couple of methods.
Next is the function OneMoreCheeseGone()
along with a global variable that it modifies. This function subtracts one from the global variable and stores a string in a property, status
, of the object passed to it.
Next come the actual methods for class Cheese
. (You must put these functions after OneMoreCheeseGone()
because they call it. If you use a function prototype as a forward reference for OneMoreCheeseGone()
, the order doesn’t matter.)
Finally, main()
creates two new instances of Cheese
. Then it sets the global variable to 2
, which keeps track of the number of blocks left. Next, it calls the eat
() function for the asiago
cheese and rot()
for the limburger
cheese. And then it prints the results of everything that happened: It displays the Cheese
count, and it displays the status
of each object.
When you run the application in Listing 1-9, you see this output:
Eaten up! Yummy
Rotted away! Yuck
Cheese count: 0
asiago: Gone
limburger: Gone
The first line is the result of calling asiago->eat()
, which prints one message. The second line is the result of calling limburger->rot()
, which prints another message. The third line is simply the value in the variable CheeseCount
. This variable was decremented once each time the computer called OneMoreCheeseGone()
. Because the function was called twice, CheeseCount
went from 2
to 1
to 0
. The final two lines show the contents of the status
variable in the two objects. (OneMoreCheeseGone()
stores "Gone"
in these variables.)
Take a careful look at the OneMoreCheeseGone()
function. It operates on the current object provided as a parameter by setting its status variable to the string Gone
. The eat()
method calls it, passing the current object using this
. The rot()
method also calls it, again passing the current object via this
.
Unless you’re actually working with C++ 20 at a somewhat detailed level, you can probably skip this section and not really lose much. Of course, you may just be curious and learning something new is always a good thing.
C++ 20 brings a few changes to the this
pointer with it. Even though you don’t see anything about functional programming until Book 3, it’s important to know that like the examples in this chapter, you can use the this
pointer in a lambda expression. A lambda expression is a mathematically based approach to dealing with certain kinds of programming problems that is concise and easier to understand than some standard C++ approaches. You can also pass a lambda expression, essentially a kind of function, to other functions as you would any other argument. The change of the use of the this
pointer for lambda expressions is simply a clarification — you must now actually declare use of the this
pointer before you’re allowed to use it. You can get an overview of lambda expressions in the “Using Lambda Expressions for Implementation” section of Book 3 Chapter 1 and read about using lambda expressions in your code in Book 3 Chapter 2. The discussion at http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0806r2.html
will fill in some very technical details if you’re interested.
It’s important to note that the C++ definition of an object as described in this chapter differs from the definition used by some other languages. There is lengthy and involved discussion of the topic at https://blog.panicsoftware.com/objects-their-lifetimes-and-pointers/
, but the point is that if you understand objects as described in this chapter, then you know how C++ developers view them. You may have noticed that there is a great deal of emphasis in this chapter on destroying objects by releasing their storage. The “Starting and Ending with Constructors and Destructors” section of this chapter discusses another technique, which is to call a destructor. However, until C++ 20, standard objects, such as string
, don’t have a destructor as such, the calling of it is a no-op (a no operation, nothing happens). Because the manner in which objects are destroyed is changing, so is the use of the this
pointer, which relies on the existence of an object to work.
You may want a method in a class to handle different types of parameters. For example, you might have a class called Door
and a method called GoThrough()
. You might want the GoThrough()
method to take as parameters objects of class Dog
,
class Human
,
or class Cat
. Depending on which class is entering, you might want to change the GoThrough()
function’s behavior.
A way to handle this is by overloading the GoThrough()
function. C++ lets you design a class that has multiple methods that are all named the same. However, the parameters must differ between these methods. With the GoThrough()
method, one version will take a Human
, another a Dog
, and another a Cat
.
View the code for the DoorClass
example in Listing 1-10 and notice the GoThrough()
methods. There are three of them. To use these methods, main()
creates four different objects — a cat, a dog, a human, and a door. It then sends each creature through the door.
LISTING 1-10: Overloading Functions in a Class
#include <iostream>
using namespace std;
class Cat {
public:
string name;
};
class Dog {
public:
string name;
};
class Human {
public:
string name;
};
class Door {
private:
int HowManyInside;
public:
void Start();
void GoThrough(Cat *acat);
void GoThrough(Dog *adog);
void GoThrough(Human *ahuman);
};
void Door::Start() {
HowManyInside = 0;
}
void Door::GoThrough(Cat *somebody) {
cout << "Welcome, " << somebody->name << endl;
cout << "A cat just entered!" << endl;
HowManyInside++;
}
void Door::GoThrough(Dog *somebody) {
cout << "Welcome, " << somebody->name << endl;
cout << "A dog just entered!" << endl;
HowManyInside++;
}
void Door::GoThrough(Human *somebody) {
cout << "Welcome, " << somebody->name << endl;
cout << "A human just entered!" << endl;
HowManyInside++;
}
int main() {
Door entrance;
entrance.Start();
Cat *SneekyGirl = new Cat;
SneekyGirl->name = "Sneeky Girl";
Dog *LittleGeorge = new Dog;
LittleGeorge->name = "Little George";
Human *me = new Human;
me->name = "John";
entrance.GoThrough(SneekyGirl);
entrance.GoThrough(LittleGeorge);
entrance.GoThrough(me);
delete SneekyGirl;
delete LittleGeorge;
delete me;
return 0;
}
The application allows dogs and cats to enter like humans. The beginning of this application declares three classes, Cat
, Dog
, and Human
, each with a name
member. Next is the Door
class. A private member, HowManyInside
, tracks how many beings have entered. The Start()
function activates the door. Finally, the class contains the overloaded functions. They all have the same name and the same return type. You can have different return types, but for the compiler to recognize the functions as unique, they must differ by parameters. These do; one takes a Cat
pointer; one takes a Dog
pointer; and one takes a Human
pointer.
Next is the code for the methods. The first function, Start()
sets HowManyInside
to 0
. The next three functions are overloaded. They do similar things, but they write slightly different messages. Each takes a different type.
The first step in main()
is to create a Door
instance. The code doesn’t use a pointer to show that you can mix pointers with stack variables in an application. After creating the Door
instance, the code calls Start()
. Next, the code creates three creature instances: Cat
, Dog
, and Human
, and sets the name
property for each.
The calls to the entrance.GoThrough()
method passes a Cat
, a Dog
, and a Human
(all in order). Because you can see the Door
class, you know the code calls three different methods that are all named the same. But when using the class, you consider them one method that accepts a Cat
, a Dog
, or a Human
. That’s the goal of overloading: to create what feels like versions of the one function. Here’s what you see when you run this application:
Welcome, Sneeky Girl
A cat just entered!
Welcome, Little George
A dog just entered!
Welcome, John
A human just entered!
You can add two special methods to your class that let you provide special startup and shutdown functionality: a constructor and a destructor. The following sections provide details about these methods.
When you create a new instance of a class, you may want to do some basic object setup. Suppose you have a class called Apartment
, with a private property called NumberOfOccupants
and a method called ComeOnIn()
. The code for ComeOnIn()
adds 1
to NumberOfOccupants
.
When you create a new instance of Apartment
, you probably want to start NumberOfOccupants
at 0
. The best way to do this is by adding a special method, a constructor, to your class. This method has a line of code such as
NumberOfOccupants = 0;
Whenever you create a new instance of the class Apartment
, the computer first calls this constructor for your new object, thereby setting NumberOfOccupants
to 0
. Think of the constructor as an initialization function: The computer calls it when you create a new object.
To write a constructor, you add it as another method to your class, and make it public. You name the constructor the same as your class. For the class Apartment
, you name the constructor Apartment()
. The constructor has no return type, not even void
. You can have parameters in a constructor; see “Adding parameters to constructors,” later in this chapter. Listing 1-11, later in this section, shows a sample constructor along with a destructor, which is covered in the next section.
When you delete an instance of a class, you might want some cleanup code to straighten things out before the object memory is released. For example, your object may have properties that are pointers to other objects. It’s essential to delete those other objects. You put cleanup code in a special function called a destructor. A destructor is a finalization function that the computer calls before it deletes your object.
The destructor function gets the same name as the class, except it has a tilde, ~
, at the beginning of it. (The tilde is usually in the upper-left corner of the keyboard.) For a class called Squirrel
, the destructor would be ~Squirrel()
. The destructor doesn’t have a return type, not even void
, because you can’t return anything from a destructor (the object is gone, after all). You just start with the function name and no parameters. The next section, “Sampling constructors and destructors,” shows an example that uses both constructors and destructors.
The WalnutClass
example, shown in Listing 1-11, uses a constructor and destructor. This application involves two classes, the main one called Squirrel
that demonstrates the constructor and destructor, and one called Walnut
, which is used by the Squirrel
class.
LISTING 1-11: Initializing and Finalizing with Constructors and Destructors
#include <iostream>
using namespace std;
class Walnut {
public:
int Size;
};
class Squirrel {
private:
Walnut *MyDinner;
public:
Squirrel();
~Squirrel();
};
Squirrel::Squirrel() {
cout << "Starting!" << endl;
MyDinner = new Walnut;
MyDinner->Size = 30;
}
Squirrel::~Squirrel() {
cout << "Cleaning up my mess!" << endl;
delete MyDinner;
}
int main() {
Squirrel *Sam = new Squirrel;
Squirrel *Sally = new Squirrel;
delete Sam;
delete Sally;
return 0;
}
The Squirrel
class has a property called MyDinner
that is a pointer to a Walnut
instance. The Squirrel
constructor creates an instance of Walnut
and stores it in MyDinner
. The destructor deletes the instance of Walnut
. In main()
, the code creates two instances of Squirrel
. Each instance gets its own Walnut
to eat. Each Squirrel
creates its Walnut
when it starts and deletes the Walnut
when the Squirrel
is deleted.
Notice in this code that the constructor has the same name as the class, Squirrel()
. The destructor also has the same name, but with a tilde, ~
, tacked on to the beginning of it. Thus, the constructor is Squirrel()
and the destructor is ~Squirrel()
. Destructors never take parameters and you can’t call them directly, but the runtime calls them automatically when it’s time to destroy an object.
When you run this application, you can see the following lines, which were spit up by the Squirrel
in its constructor and destructor. (You see two lines of each because main()
creates two squirrels.)
Starting!
Starting!
Cleaning up my mess!
Cleaning up my mess!
If the Walnut
class also had a constructor and destructor, and you made the MyDinner
property a variable in the Squirrel
class, rather than a pointer, the computer would create the Walnut
instance after it creates the Squirrel
instance, but before it calls the Squirrel()
constructor. It then deletes the Walnut
instance when it deletes the Squirrel
instance, after calling the ~Squirrel()
destructor. The code performs these steps for each instance of Squirrel
.
Like other methods, constructors allow you to include parameters. When you do, you can use these parameters in the initialization process. To use them, you list the arguments inside parentheses when you create the object. Because constructors have parameters, you can create multiple overloaded constructors for a class by varying the number and type of parameters.
Suppose that you want the Squirrel
class to have a name
property. Although you could create an instance of Squirrel
and then set its name
property, you can specify the name
directly by using a constructor. The constructor’s prototype looks like this:
Squirrel(string StartName);
Then, you create a new instance like so:
Squirrel *Sam = new Squirrel("Sam");
The constructor is expecting a string
, so you pass a string
when you create the object.
The SquirrelClass
example, shown in Listing 1-12, presents an application that includes all the basic elements of a class with a constructor that accepts parameters.
LISTING 1-12: Placing Parameters in Constructors
#include <iostream>
using namespace std;
class Squirrel {
private:
string Name;
public:
Squirrel(string StartName);
void WhatIsMyName();
};
Squirrel::Squirrel(string StartName) {
cout << "Starting!" << endl;
Name = StartName;
}
void Squirrel::WhatIsMyName() {
cout << "My name is " << Name << endl;
}
int main()
{
Squirrel *Sam = new Squirrel("Sam");
Squirrel *Sally = new Squirrel("Sally");
Sam->WhatIsMyName();
Sally->WhatIsMyName();
delete Sam;
delete Sally;
return 0;
}
In main()
, you pass a string into the constructors. The constructor code takes the StartName
parameter and copies it to the Name
property. The WhatIsMyName()
method writes Name
to the console.
When you start going crazy describing classes, you usually discover hierarchies of classes. For example, you have a class Vehicle
that you want to divide into classes: Car
, PickupTruck
, TractorTrailer
, and SUV
. The Car
class is further divided into the StationWagon
, FourDoorSedan
, and TwoDoorHatchback
classes.
Or you could divide Vehicle
into car brands, such as Ford
, Honda
, and Toyota
. Then you could divide the class Toyota
into models, such as Prius
, Avalon
, Camry
, and Corolla
. You can create similar groupings of objects for the other class hierarchies; your decision depends on how you categorize things and how the hierarchy is used. In the hierarchy, class Vehicle
is at the top. This class has properties you find in every brand or model of vehicle. For example, all vehicles have wheels. How many they have varies, but it doesn’t matter at this point, because classes don’t have specific values for the properties.
Each brand has certain characteristics that might be unique to it, but each has all the characteristics of class Vehicle
. That’s called inheritance. The class Toyota
, for example, has all the properties found in Vehicle
. And the class Prius
has all the properties found in Toyota
, which includes those inherited from Vehicle
.
In C++, you can create a hierarchy of classes. When you take one class and create a new one under it, such as creating Toyota
from Vehicle
, you are deriving a new class, which means Toyota
is a child of Vehicle
in the hierarchy.
To derive a class from an existing class, you write the new class as you would any other class, but you extend the header after the class name with a colon, :
, the word public
, and then the class you’re deriving from, as in the following class header line:
class Toyota : public Vehicle {
When you do so, the class you create (Toyota
) inherits the properties and methods from the parent class (Vehicle
). For example, if Vehicle
has a public property called NumberOfWheels
and a public method called Drive()
, the class Toyota
has these members, although you didn’t write the members in Toyota
.
The VehicleClass
example, shown in Listing 1-13, demonstrates class inheritance. It starts with a class called Vehicle
, and a derived class called Toyota
. You create an instance of Toyota
in main()
and call two methods for the instance, MeAndMyToyota()
and Drive()
. The definition of the Toyota
class doesn’t show a Drive()
function. The Drive()
function is inherited from the Vehicle
class. You can call this function like a member of the Toyota
class because in many ways it is.
LISTING 1-13: Deriving One Class from Another
#include <iostream>
using namespace std;
class Vehicle {
public:
int NumberOfWheels;
void Drive() {
cout << "Driving, driving, driving…" << endl;
}
};
class Toyota : public Vehicle {
public:
void MeAndMyToyota() {
cout << "Just me and my Toyota!" << endl;
}
};
int main() {
Toyota MyCar;
MyCar.MeAndMyToyota();
MyCar.Drive();
return 0;
}
When you run this application, you see the output from two functions:
Just me and my Toyota!
Driving, driving, driving…
When you create a class, its methods can access both public and private properties and methods. Users of the class can access only the public properties and methods. When you derive a new class, it cannot access the private members in the parent class. Private members are reserved for a class itself and not for any derived class. When members need to be accessible by derived classes, there’s a specification you can use beyond public and private: protected.
An alias is another name for something. If your name is Robert, someone could use an alias of Bob when calling your name. Both Robert and Bob point to the same person — you. However, the names are actually different. One is your real name, Robert, and the other is your alias, Bob. In real life, using aliases can make things easier: saying Bob is definitely easier than saying Robert (although not by much). Using aliases in C++ applications can make things easier, too.
However, sending a pointer to someone gives the recipient access to the original data. The recipient could modify the data in ways that you don’t want. So, you could create an alias of the original object that is a constant. No one can modify a constant. The ObjectAlias
example, shown in Listing 1-14, demonstrates how to create a constant alias of a string object. The same technique works with any other sort of object you might want to work with.
LISTING 1-14: Creating an Object Alias
#include <iostream>
using namespace std;
int main() {
string OriginalString = "Hello";
const string &StringCopy(OriginalString);
OriginalString = "Goodbye";
cout << OriginalString << endl;
cout << StringCopy << endl;
return 0;
}
The code begins by creating a string
named OriginalString
that contains a value of Hello
. It then creates a const string
alias of OriginalString
named StringCopy
. When the code changes the value of OriginalString
, the value of StringCopy
is also changed because StringCopy
points to the same location in memory. So when you run this example, you see output of
Goodbye
Goodbye
It may not seem like you’ve accomplished anything, but if you try to modify the value of StringCopy
, Code::Blocks outputs an error message like this:
error: passing 'const string {aka const
std::basic_string<char>}' as 'this' argument of
'std::basic_string<_CharT, _Traits, _Alloc>&
std::basic_string<_CharT, _Traits,
_Alloc>::operator=(const _CharT*) [with _CharT = char;
_Traits = std::char_traits<char>; _Alloc =
std::allocator<char>; std::basic_string<_CharT, _Traits,
_Alloc> = std::basic_string<char>]' discards qualifiers
[-fpermissive]|
The point is that you can’t modify the value of StringCopy
, but you can modify the value of OriginalString
. Sending StringCopy
to someone who needs access to the value is safe. Just to ensure that you understand what is happening, try making StringCopy
a standard string
rather than a const string
. You’ll be able to modify the value, and the modification will now affect OriginalString
as well. StringCopy
truly is an alias of OriginalString
, but as a const string
, it’s an alias that prevents modification of the underlying string
value.