Objectives
In this chapter, you’ll:
■ Define a custom class and use it to create objects.
■ Implement a class’s behaviors as member functions and attributes as data members.
■ Access and manipulate private
data members through public
get and set functions to enforce data encapsulation.
■ Use a constructor to initialize an object’s data.
■ Separate a class’s interface from its implementation for reuse.
■ Access class members via the dot (.
) and arrow (->
) operators.
■ Use destructors to perform “termination housekeeping.”
■ Learn why returning a reference or a pointer to private
data is dangerous.
■ Assign the data members of one object to those of another.
■ Create objects composed of other objects.
■ Use friend
functions and declare friend
classes.
■ Access non-static
class members via the this
pointer.
■ Use static
data members and member functions.
■ Use struct
s to create aggregate types, and use C++20 designated initializers to initialize aggregate members.
■ Do an objects-natural case study that serializes objects using JavaScript Object Notation (JSON) and the cereal
library.
Outline
9.2 Test-Driving an Account
Object
9.3 Account
Class with a Data Member and Set and Get Member Functions
9.3.2 Access Specifiers private
and public
9.4 Account
Class: Custom Constructors
9.5 Software Engineering with Set and Get Member Functions
9.6 Account
Class with a Balance
9.7 Time
Class Case Study: Separating Interface from Implementation
9.7.2 Separating the Interface from the Implementation
9.7.5 Including the Class Header in the Source-Code File
9.7.6 Scope Resolution Operator (::
)
9.7.7 Member Function setTime
and Throwing Exceptions
9.7.8 Member Function to24HourString
9.7.9 Member Function to12HourString
9.7.10 Implicitly Inlining Member Functions
9.7.11 Member Functions vs. Global Functions
9.8 Compilation and Linking Process
9.9 Class Scope and Accessing Class Members
9.10 Access Functions and Utility Functions
9.11 Time
Class Case Study: Constructors with Default Arguments
9.11.2 Overloaded Constructors and C++11 Delegating Constructors
9.13 When Constructors and Destructors Are Called
9.15 Default Assignment Operator
9.16 const
Objects and const
Member Functions
9.17 Composition: Objects as Members of Classes
9.18 friend
Functions and friend
Classes
9.19.1 Implicitly and Explicitly Using the this
Pointer to Access an Object’s Data Members
9.19.2 Using the this
Pointer to Enable Cascaded Function Calls
9.20 static
Class Members—Classwide Data and Member Functions
9.21.1 Initializing an Aggregate
9.21.2 C++20: Designated Initializers
9.22 Objects Natural Case Study: Serialization with JSON
9.22.1 Serializing a vector
of Objects containing public
Data
9.22.2 Serializing a vector
of Objects containing private
Data
Section 1.16 presented a friendly introduction to object orientation, discussing classes, objects, data members (attributes) and member functions (behaviors). In our objects-natural case studies, you’ve created objects of existing classes and called their member functions to make the objects perform powerful tasks without having to know how these classes worked internally.
1. This chapter depends on the terminology and concepts introduced in Section 1.16, Introduction to Object Orientation and “Objects Natural.”
This chapter begins our deeper treatment of object-oriented programming as we craft valuable custom classes. C++ is an extensible programming language—each class you create becomes a new type you can use to create objects. Some development teams in industry work on applications that contain hundreds, or even thousands, of custom classes.
Account
ObjectWe begin our introduction to custom classes with three examples that create an Account
class representing a simple bank account. First, let’s look at the main
program and output, so you can see an object of our initial Account
class in action. To help you prepare for the larger programs you’ll encounter later in this book and in industry, we define the Account
class and main
in separate files—main
in AccountTest.cpp
(Fig. 9.1) and class Account
in Account.h
, which we’ll show in Fig. 9.2.
1// Fig. 9.1: AccountTest.cpp
2// Creating and manipulating an Account object.
3#include <iostream>
4#include <string>
5#include <fmt/format.h> // In C++20, this will be #include <format>
6#include "Account.h"
7 8using namespace std;
9 10int main() {
11Account myAccount{}; // create Account object myAccount
12 13// show that the initial value of myAccount's name is the empty string
14cout << fmt::format("Initial account name: {} ", myAccount.getName());
15 16// prompt for and read the name
17cout << "Enter the account name: ";
18string name{};
19getline(cin, name); // read a line of text
20myAccount.setName(name); // put name in the myAccount object
21 22// display the name stored in object myAccount
23cout << fmt::format("Updated account name: {} ", myAccount.getName());
24}
Initial account name:
Enter the account name:
Jane GreenUpdated account name: Jane Green
1// Fig. 9.2: Account.h
2// Account class with a data member and
3// member functions to set and get its value.
4#include <string>
5#include <string_view>
6 7class Account {
8public:
9// member function that sets m_name in the object
10void setName(std::string_view name) {
11m_name = name; // replace m_name's value with name
12}
13 14// member function that retrieves the account name from the object
15const std::string& getName() const {
16return m_name; // return m_name's value to this function's caller
17}
18private:
19std::string m_name; // data member containing account holder's name
20}; // end class Account
Typically, you cannot call a class’s member functions until you create an object of that class.2 Line 11
Account myAccount{}; // create Account object myAccount
2. In Section 9.20, you’ll see that static
member functions are an exception.
creates an object called myAccount
. The variable’s type is Account
—the class we’ll define in Fig. 9.2.
When we declare int
variables, the compiler knows what int
is—it’s a fundamental type that’s built into C++. In line 11, however, the compiler does not know in advance what type Account
is—it’s a user-defined type.
When packaged properly, new classes can be reused by other programmers. It’s customary to place a reusable class definition in a file known as a header with a .h
filename extension.3 You include that header wherever you need to use the class, as you’ve been doing throughout this book with C++ Standard Library and third-party library classes.
3. C++ Standard Library headers, like <iostream>
, do not use the .h
filename extension and some C++ programmers prefer the .hpp
extension.
We tell the compiler what an Account
is by including its header, as in line 6:
#include "Account.h"
If we omit this, the compiler issues error messages wherever we use class Account
and any of its capabilities. A header that you define in your program is placed in double quotes (""
), rather than the angle brackets (<>
). The double quotes tell the compiler to check the folder containing AccountTest.cpp
(Fig. 9.1) before the compiler’s header search path.
Account
’s getName
Member FunctionClass Account
’s getName
member function returns the name stored in a particular Account
object. Line 14 calls myAccount.getName()
to get the myAccount
object’s initial name, which is the empty string
. We’ll say more about this shortly.
Account
’s setName
Member FunctionThe setName
member function stores a name in a particular Account
object. Line 20 calls
myAccount
’s setName
member function to store name
’s value in the object myAccount
.
To confirm that myAccount
now contains the name you entered, line 23 calls member function getName
again and displays its result.
Account
Class with a Data Member and Set and Get Member FunctionsNow that we’ve seen class Account
in action (Fig. 9.1), we present its internal details.
Class Account
(Fig. 9.2) contains an m_name
data member (line 19) that stores the account holder’s name. Each object of the class has its own copy of the class’s data members.4 Later, we’ll add a balance
data member to keep track of the money in each Account
. Class Account
also contains:
• member function setName
(lines 10–12) that stores a name in an Account
, and
• member function getName
(lines 15–17) that retrieves a name from an Account
.
4. In Section 9.20, you’ll see that static
data members are an exception.
class
and the Class BodyThe class definition begins with the keyword class
(line 7) followed immediately by the class’s name—in this case, Account
. By convention:
• each word in a class name starts with a capital first letter, and
• data-member and member-function names begin with a lowercase first letter.
Every class’s body is enclosed in braces {}
(lines 7 and 20). The class definition terminates with a required semicolon (line 20).
m_name
of Type std::string
Recall from Section 1.16 that an object has attributes, implemented as data members. Each object maintains its own copy of these throughout its lifetime. Usually, a class also contains member functions that manipulate the data members of particular objects of the class. The data members exist:
• before a program calls member functions on a specific object,
• while the member functions are executing and
• after the member functions finish executing.
Data members are declared inside a class definition but outside the class’s member functions. Line 19
std::string m_name; // data member containing account holder's name
declares a string
data member called m_name
. The "m_"
prefix is a common naming convention to indicate that a variable represents a data member. If there are many Account
objects, each has its own m_name
. Because m_name
is a data member, it can be manipulated by the class’s member functions. Recall that the default string
value is the empty string
(""
), which is why line 14 in main
(Fig. 9.1) did not display a name.
By convention, C++ programmers typically place a class’s data members last in the class’s body. You can declare the data members anywhere in the class outside its member-function definitions, but scattering the data members can lead to hard-to-read code.
std::
with Standard Library Components in HeadersThroughout the Account.h
header (Fig. 9.2), we use std::
when referring to string
(lines 10, 15 and 19). For subtle reasons that we explain in Section 19.4, headers should not contain using
directives or using
declarations.
setName
Member FunctionMember function setName
(lines 10–12):
void setName(std::string_view name) {
m_name = name; // replace m_name's value with name }
SE receives a string_view
representing the Account
’s name and assigns the name
argument to data member m_name
. Recall that a string_view
is a read-only view into a character sequence, such as a std::string
or a C-string. This copies name
’s characters into m_name
. The "m_"
in m_name
makes it easy to distinguish the parameter name
from the data member m_name
.
getName
Member FunctionMember function getName
(lines 15–17):
const std::string& getName() const {
return m_name; // return m_name's value to this function's caller
}
has no parameters and returns a particular Account
object’s m_name
to the caller as a const std::string&
. Declaring the returned reference const
ensures that the caller cannot modify the object’s data via that reference.
const
Member FunctionsNote the const
to the right of the parameter list in getName
’s header (line 15). When returning m_name
, member function getName
does not, and should not, modify the Account
object on which it’s called. Declaring a member function const
tells the compiler, “this function should not modify the object on which it’s called—if it does, please issue a compilation error.” This can help you locate errors if you accidentally insert code that would modify the object. It also tells the compiler that getName
may be called on a const Account
object, or called via a reference or pointer to a const Account
.
private
and public
CG The keyword private
(line 18) is an access specifier. Each access specifier is followed by a required colon (:
). Data member m_name
’s declaration (line 19) appears after private:
to indicate that m_name
is accessible only to class Account
’s member functions.5 This is known as information hiding (or, more generally, as hiding implementation details) and is a recommended practice of the C++ Core Guidelines6 and object-oriented programming in general. The data member m_name
is encapsulated (hidden) and can be used only in class Account
’s setName
and getName
member functions. Most data-member declarations appear after the private:
access specifier. We generally omit the colon when we refer to the private
and public
access specifiers in the text, as we did in this sentence.
5. Or to “friends” of the class, as you’ll see in Section 9.18.
6. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-private
.
This class also contains the public
access specifier (line 8):
public:
SE Data members or member functions listed after access specifier public
—and before the next access specifier if there is one—are “available to the public.” They can be used anywhere an object of the class is in scope. Making a class’s data members private
facilitates debugging because problems with data manipulations are localized to the class’s member functions. In Chapter 10, we’ll introduce the protected
access specifier.
By default, everything in a class is private
, unless you specify otherwise. Once you list an access specifier, everything has that level of access until you list another access specifier. The access specifiers public
and private
may be repeated, but this is unnecessary and can be confusing. We prefer to list public
only once, grouping everything that’s public
. We prefer to list private
only once, grouping everything that’s private
.
Account
Class: Custom ConstructorsAs you saw in the preceding example, when an Account
object is created, its string
data member m_name
is initialized to the empty string
by default. But what if you want to provide a name when you first create an Account
object? Each class can define constructors that specify custom initialization for objects of that class. A constructor is a special member function that must have the same name as the class. Constructors cannot return values, so they do not specify a return type (not even void
). C++ guarantees that a constructor is called when each object is created, so this is the ideal point to initialize an object’s data members. Every time you created an object so far in this book, the corresponding class’s constructor was called to initialize the object. As you’ll soon see, classes may have overloaded constructors.
Like member functions, constructors can have parameters—the corresponding argument values help initialize the object’s data members. For example, you can specify an Account
object’s name when the object is created, as you’ll do in line 12 of Fig. 9.4:
Account account1{"Jane Green"};
In the preceding statement, the string "Jane Green"
is passed to the Account
class’s constructor and used to initialize the account1
object’s data. The preceding statement assumes that class Account
has a constructor that can receive a string argument.
Account
Class DefinitionFigure 9.3 shows class Account
with a constructor that receives an accountName
parameter and uses it to initialize the data member m_name
when an Account
object is created.
1// Fig. 9.3: Account.h
2// Account class with a constructor that initializes the account name.
3#include <string>
4#include <string_view>
5 6class Account {
7public:
8// constructor initializes data member m_name with the parameter name
9explicit Account(std::string_view name)
10: m_name{name} { // member initializer
11// empty body
12}
13 14// function to set the account name
15void setName(std::string_view name) {
16m_name = name; // replace m_name's value with name
17}
18 19// function to retrieve the account name
20const std::string& getName() const {
21return m_name;
22}
23private:
24std::string m_name; // account name data member
25}; // end class Account
Account
Class’s Custom Constructor DefinitionLines 9–12 of Fig. 9.3 define Account
’s constructor:
explicit Account(std::string_view name)
: m_name{name} { // member initializer
// empty body }
Usually, constructors are public
, so any code with access to the class definition can create and initialize objects of that class.7 Line 9 indicates that the constructor has a name
parameter of type string_view
. When you create a new Account
object, you must pass a person’s name to the constructor, which then initializes the data member m_name
with the contents of the string_view
parameter.
7. Section 11.10.2 discusses why you might use a private
constructor.
The constructor’s member-initializer list (line 10):
: m_name{name}
initializes the m_name
data member. Member initializers appear between a constructor’s parameter list and the left brace that begins the constructor’s body. You separate the member initializer list from the parameter list with a colon (:
).
SE Each member initializer consists of a data member’s variable name followed by braces containing its initial value.8 This member initializer calls the std::string
class’s constructor that receives a string_view
. If a class has more than one data member, each member initializer is separated from the next by a comma. Member initializers execute in the order you declare the data members in the class so, for clarity, list the member initializers in the same order. The member initializer list executes before the constructor’s body executes.
8. Occasionally, parentheses rather than braces may be required, such as when initializing a vector
of a specified size, as we did in Fig. 6.14.
CG PERF Though you can perform initialization with assignment statements in the constructor’s body, the C++ Core Guidelines recommend using member initializers.9 You’ll see later that member initializers can be more efficient. Also, you’ll see that certain data members must be initialized using the member-initializer syntax, because you cannot assign to them in the constructor’s body.
9. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-initialize
.
explicit
Keyword CG A constructor with only one parameter should be declared explicit
to prevent the compiler from using the constructor to perform implicit type conversions.10 The keyword explicit
means that Account
’s constructor must be called explicitly, as in:
Account account1{"Jane Green"};
10. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-explicit
.
ERR For now, simply declare all single-parameter constructors explicit
. In Section 11.13, you’ll see that single-parameter constructors without explicit
can be called implicitly to perform type conversions. Such implicit constructor calls can lead to subtle errors and are generally discouraged. Line 9 of Fig. 9.3 does not specify a return type (not even void
) because, again, constructors cannot return values. Also, constructors cannot be declared const
(simply because initializing an object modifies it).
Account
Objects When They’re CreatedThe AccountTest
program (Fig. 9.4) initializes two Account
objects using the constructor. Line 12 creates the Account
object account1
:
Account account1{"Jane Green"};
1// Fig. 9.4: AccountTest.cpp
2// Using the Account constructor to initialize the m_name data
3// member at the time each Account object is created.
4#include <iostream>
5#include <fmt/format.h> // In C++20, this will be #include <format>
6#include "Account.h"
7 8using namespace std;
9 10int main() {
11// create two Account objects
12Account account1{"Jane Green"};
13Account account2{"John Blue"};
14 15// display initial each Account's corresponding name
16cout << fmt::format("account1 name is: {} account2 name is: {} ",
17account1.getName(), account2.getName());
18}
account1 name is: Jane Green
account2 name is: John Blue
When you create an object, C++ calls the class’s constructor to initialize that object. In line 12, the argument "Jane Green"
is used by the constructor to initialize the new object’s m_name
data member, as specified in lines 9–12 of Fig. 9.3. Line 13 of Fig. 9.4 repeats this process, passing the argument "John Blue"
to initialize m_name
for account2
:
Account account2{"John Blue"};
To confirm the objects were initialized properly, lines 16–17 call the getName
member function to get each object’s name. The output shows different names, confirming that each Account
maintains its own copy of the class’s data member m_name
.
Recall that line 11 of Fig. 9.1 created an Account
object with empty braces to the right of the object’s variable name:
Account myAccount{};
which, for an object of a class, is equivalent to:
Account myAccount;
In the preceding statements, C++ implicitly calls Account
’s default constructor. In any class that does not define a constructor, the compiler generates a default constructor with no parameters. The default constructor does not initialize the class’s fundamental-type data members but does call the default constructor for each data member that’s an object of another class. For example, though you do not see this in the first Account
class’s code (Fig. 9.2), Account
’s default constructor calls class std::string
’s default constructor to initialize the data member m_name
to the empty string (""
). An uninitialized fundamental-type variable contains an undefined (“garbage”) value.
SE If you define a custom constructor for a class, the compiler will not create a default constructor for that class. In that case, you will not be able to create an Account
object by calling the constructor with no arguments unless the custom constructor you define has an empty parameter list or has default arguments for all its parameters. We’ll show later that you can force the compiler to create the default constructor even if you’ve defined non-default constructors. Unless default initialization of your class’s data members is acceptable, provide a custom constructor that initializes each new object’s data members with meaningful values.
In addition to the default constructor, the compiler can generate default versions of five other special member functions—a copy constructor, a move constructor, a copy assignment operator, a move assignment operator and a destructor. We’ll briefly introduce copy construction, copy assignment and destructors in this chapter. In Chapter 11, we’ll discuss the details of all six special member functions, including:
• when they’re auto-generated,
• when you might need to define custom versions of each, and
• the various C++ Core Guidelines for these special member functions.
You’ll see that you should try to construct your custom classes such that the compiler can auto-generate these special member functions for you—this is called the “Rule of Zero.”
As you’ll see in the next section, set and get member functions can validate attempts to modify private
data and control how that data is presented to the caller, respectively. These are compelling software engineering benefits.
A client of the class is any other code that calls the class’s member functions. If a data member were public
, any client could see the data and do whatever it wanted with it, including setting it to an invalid value.
You might think that even though a client cannot directly access a private
data member, the client can nevertheless do whatever it wants with the variable through public
set and get functions. You’d think that you could peek at the private
data (and see exactly how it’s stored in the object) anytime with the public
get function and that you could modify the private
data at will through the public
set function.
SE Actually, set functions can be written to validate their arguments and reject any attempts to set the data to incorrect values, such as
• a negative body temperature
• a day in March outside the range 1 through 31, or
• a product code not in the company’s product catalog.
SE SE A get function can present the data in a different form, keeping the object’s actual data representation hidden from the user. For example, a Grade
class might store a numeric grade as an int
between 0 and 100, but a getGrade
member function might return a letter grade as a string
, such as "A"
for grades between 90 and 100, "B"
for grades between 80 and 89, etc. Tightly controlling the access to and presentation of private
data can reduce errors while increasing your programs’ robustness, security and usability.
Account
Object with Encapsulated DataYou can think of an Account
object, as shown in the following diagram. The private
data member m_name
, represented by the inner circle, is hidden inside the object and accessible only via an outer layer of public
member functions, represented by the outer ring containing getName
and setName
. Any client that needs to interact with the Account
object can do so only by calling the public
member functions of the outer layer.
SE Generally, data members are private
, and the member functions that a class’s clients need to use are public
. Later, we’ll discuss why you might use a public
data member or a private
member function. Using public
set and get functions to control access to private
data makes programs clearer and easier to maintain. Change is the rule rather than the exception. You should anticipate that your code will be modified, and possibly often.
Account
Class with a Balance CG Figure 9.5 defines an Account
class that maintains two related pieces of data—a bank account’s balance and the account holder’s name. The C++ Core Guidelines recommend defining related data items in a class (or, as you’ll see in Section 9.21, a struct
).11
1// Fig. 9.5: Account.h
2// Account class with m_name and m_balance data members, and a
3// constructor and deposit function that each perform validation.
4#include <string>
5#include <string_view>
6 7class Account {
8public:
9// Account constructor with two parameters
10Account(std::string_view name, double balance)
11: m_name{name} { // member initializer for m_name
12 13// validate that balance is greater than 0.0; if not,
14// data member m_balance keeps its default initial value of 0.0
15if (balance > 0.0) { // if the balance is valid
16m_balance = balance; // assign it to data member m_balance
17}
18}
19 20// function that deposits (adds) only a valid amount to the balance
21void deposit(double amount) {
22if (amount > 0.0) { // if the amount is valid
23m_balance += amount; // add it to m_balance
24}
25}
26 27// function returns the account balance
28double getBalance() const {
29return m_balance;
30}
31 32// function that sets the account name
33void setName(std::string_view name) {
34m_name = name; // replace m_name's value with name
35}
36 37// function that returns the account name
38const std::string& getName() const {
39return m_name;
40}
41private:
42std::string m_name; // account name data member
43double m_balance{0.0}; // data member with default initial value
44}; // end class Account
11. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-org
.
balance
A typical bank services many accounts, each with its own balance. Every object of this updated Account
class contains its own copies of data members m_name
and m_balance
. Line 43 declares a double
data member m_balance
and initializes its value to 0.0
:
double m_balance{0.0}; // data member with default initial value
CG CG PERF This is an in-class initializer. The C++ Core Guidelines recommend using in-class initializers when a data member should be initialized with a constant.12 The Core Guidelines also recommend when possible that you initialize all your data members with in-class initializers and let the compiler generate a default constructor for your class. A compiler-generated default constructor can be more efficient than one you define.13 Like m_name
, we can use m_balance
in the class’s member functions (lines 16, 23 and 29) because it’s a data member of the class.
12. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-in-class-initializer
.
13. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-default
.
The class has a constructor and four member functions. It’s common for someone opening an account to deposit money immediately, so the constructor (lines 10–18) receives a second parameter balance
of type double
that represents the starting balance. We did not declare this constructor explicit
, because it cannot be called with only one parameter.
Lines 15–17 ensure that data member m_balance
is assigned the balance
parameter’s value only if that value is greater than 0.0:
if (balance > 0.0) { // if the balance is valid
m_balance = balance; // assign it to data member m_balance
}
Otherwise, m_balance
will still have its default initial value 0.0
that was set at line 43 in class Account
’s definition.
deposit
Member FunctionMember function deposit
(lines 21–25) receives a double
parameter amount
and does not return a value. Lines 22–24 ensure that parameter amount
’s value is added to m_balance
only if amount
is greater than zero (that is, it’s a valid deposit amount).
getBalance
Member FunctionMember function getBalance
(lines 28–30) allows the class’s clients to obtain a particular Account
object’s m_balance
value. The member function specifies return type double
and an empty parameter list. Like member function getName
, getBalance
is declared const
because, in the process of returning m_balance
, the function does not, and should not, modify the Account
object on which it’s called.
Account
Objects with BalancesThe main
function in Fig. 9.6 creates two Account
objects (lines 10–11) and attempts to initialize them with a valid balance of 50.00
and an invalid balance of -7.00
, respectively. For our examples, we assume that balances must be greater than or equal to zero. Lines 14–17 output both Account
s’ names and balances by calling each objects’ getName
and getBalance
member functions. The account2
object’s balance is initially 0.0
because the constructor rejected the attempt to start account2
with a negative balance, so account2
’s m_balance
data member retains its default initial value.
1// Fig. 9.6: AccountTest.cpp
2// Displaying and updating Account balances.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Account.h"
6 7using namespace std;
8 9int main() {
10Account account1{"Jane Green", 50.00};
11Account account2{"John Blue", -7.00};
12 13// display initial balance of each object
14cout << fmt::format("account1: {} balance is ${:.2f} ",
15account1.getName(), account1.getBalance());
16cout << fmt::format("account2: {} balance is ${:.2f} ",
17account2.getName(), account2.getBalance());
18
account1: Jane Green balance is $50.00
account2: John Blue balance is $0.00
Lines 19–22 prompt for, input and display the account1
deposit amount. Line 23 calls object account1
’s deposit
member function with variable amount
as the argument to add that value to account1
’s balance. Lines 26–29 (Fig. 9.6) output both Account
s’ names and balances again to show that only account1
’s balance has changed.
19cout << "Enter deposit amount for account1: "; // prompt
20double amount;
21cin >> amount; // obtain user input
22cout << fmt::format("adding ${:.2f} to account1 balance ", amount);
23account1.deposit(amount); // add to account1's balance
24 25// display balances
26cout << fmt::format("account1: {} balance is ${:.2f} ",
27account1.getName(), account1.getBalance());
28cout << fmt::format("account2: {} balance is ${:.2f} ",
29account2.getName(), account2.getBalance());
30
Enter deposit amount for account1: 25.37
adding $25.37 to account1 balance
account1: Jane Green balance is $75.37
account2: John Blue balance is $0.00
Lines 31–33 prompt for, input and display account2
’s deposit amount. Line 34 calls object account2
’s deposit
member function with variable amount
as the argument to add that value to account2
’s balance. Finally, lines 37–40 output both Account
s’ names and balances again to show that only account2
’s balance has changed.
31cout << "Enter deposit amount for account2: "; // prompt
32cin >> amount; // obtain user input
33cout << fmt::format("adding ${:.2f} to account2 balance ", amount);
34account2.deposit(amount); // add to account2 balance
35 36// display balances
37cout << fmt::format("account1: {} balance is ${:.2f} ",
38account1.getName(), account1.getBalance());
39cout << fmt::format("account2: {} balance is ${:.2f} ",
40account2.getName(), account2.getBalance());
41}
Enter deposit amount for account2: 123.45
adding $123.45 to account2 balance
account1: Jane Green balance is $75.37
account2: John Blue balance is $123.45
Time
Class Case Study: Separating Interface from ImplementationEach of our prior custom class definitions placed a class in a header for reuse, then included the header into a source-code file containing main
, so we could create and manipulate objects of the class. Unfortunately, placing a complete class definition in a header reveals the class’s entire implementation to its clients. A header is simply a text file that anyone can open and read.
SE Conventional software-engineering wisdom says that to use an object of a class, the client code (e.g., main
) needs to know only
• what member functions to call,
• what arguments to provide to each member function and
• what return type to expect from each member function.
The client code does not need to know how those functions are implemented. This is another example of the principle of least privilege.
If the client-code programmer knows how a class is implemented, the programmer might write client code based on the class’s implementation details. Ideally, if that implementation changes, the class’s clients should not have to change. Hiding the class’s implementation details makes it easier to change the implementation while minimizing, and hopefully eliminating, changes to client code.
Our next example creates and manipulates an object of class Time
.14 We demonstrate two important C++ software engineering concepts:
• Separating interface from implementation.
• Using the preprocessor directive “#pragma once
” in a header to prevent the header code from being included into the same source code file more than once. Since a class can be defined only once, using such a preprocessing directive prevents multiple-definition errors.
14. In professional C++ development, rather than building your own classes to represent times and dates, you’ll typically use the header <chrono>
(https://en.cppreference.com/w/cpp/chrono
) from the C++ Standard Library.
MOD As you’ll see in Chapter 15, C++20 modules15 eliminate the need for preprocessor #pragma once
. You’ll also see that modules enable you to separate interface from implementation in a single source-code file or by using multiple source-code files.
15. At the time of this writing, the C++20 modules features were not fully implemented by the three compilers we use, so we cover them in a separate chapter.
Interfaces define and standardize how things such as people and systems interact with one another. For example, a radio’s controls serve as an interface between the radio’s users and its internal components. The controls allow users to perform a limited set of operations (such as changing the station, adjusting the volume, and choosing between AM and FM stations). Various radios may implement these operations differently—some provide push buttons, some provide dials and some support voice commands. The interface specifies what operations a radio permits users to perform but does not specify how the operations are implemented inside the radio.
Similarly, the interface of a class describes what services a class’s clients can use and how to request those services, but not how the class implements them. A class’s public
interface consists of the class’s public
member functions (also known as the class’s public
services). As you’ll soon see, you can specify a class’s interface by writing a class definition that lists only the class’s member-function prototypes in the class’s public
section.
To separate the class’s interface from its implementation, we break up class Time
into two files—Time.h
(Fig. 9.7) in which class Time
is defined and Time.cpp
(Fig. 9.8) in which Time
’s member functions are defined. This split:
1. helps make the class reusable,
2. ensures that the clients of the class know what member functions the class provides, how to call them and what return types to expect, and
3. enables the clients to ignore how the class’s member functions are implemented.
1// Fig. 9.7: Time.h
2// Time class definition.
3// Member functions are defined in Time.cpp
4#pragma once // prevent multiple inclusions of header
5#include <string>
6 7// Time class definition
8class Time {
9public:
10void setTime(int hour, int minute, int second);
11std::string to24HourString() const; // 24-hour string format
12std::string to12HourString() const; // 12-hour string format
13private:
14int m_hour{0}; // 0 - 23 (24-hour clock format)
15int m_minute{0}; // 0 - 59
16int m_second{0}; // 0 - 59
17};
1// Fig. 9.8: Time.cpp
2// Time class member-function definitions.
3#include <stdexcept> // for invalid_argument exception class
4#include <string>
5#include <fmt/format.h> // In C++20, this will be #include <format>
6#include "Time.h" // include definition of class Time from Time.h
7 8using namespace std;
9 10// set new Time value using 24-hour time
11void Time::setTime(int hour, int minute, int second) {
12// validate hour, minute and second
13if ((hour < 0 || hour >= 24) || (minute < 0 || minute >= 60) ||
14(second < 0 || second >= 60)) {
15throw invalid_argument{"hour, minute or second was out of range"};
16}
17 18m_hour = hour;
19m_minute = minute;
20m_second = second;
21}
22 23// return Time as a string in 24-hour format (HH:MM:SS)
24string Time::to24HourString() const {
25return fmt::format("{:02d}:{:02d}:{:02d}", m_hour, m_minute, m_second);
26}
27 28// return Time as string in 12-hour format (HH:MM:SS AM or PM)
29string Time::to12HourString() const {
30return fmt::format("{}:{:02d}:{:02d} {}",
31((
m_hour % 12 == 0) ? 12 : m_hour % 12), m_minute, m_second,
32(m_hour < 12 ? "AM" : "PM"));
33}
PERF In addition, this split can reduce compilation time because the implementation file can be compiled, then does not need to be recompiled unless the implementation changes.
By convention, member-function definitions are placed in a .cpp
file with the same base name (e.g., Time
) as the class’s header. Some compilers support other filename extensions as well. Figure 9.9 defines function main
, which is the client of our Time
class.
1// fig09_09.cpp
2// Program to test class Time.
3// NOTE: This file must be compiled with Time.cpp.
4#include <iostream>
5#include <stdexcept> // invalid_argument exception class
6#include <string_view>
7#include <fmt/format.h> // In C++20, this will be #include <format>
8#include "Time.h" // definition of class Time from Time.h
9using namespace std;
10 11// displays a Time in 24-hour and 12-hour formats
12void displayTime(string_view message, const Time& time) {
13cout << fmt::format("{} 24-hour time: {} 12-hour time: {} ",
14message, time.to24HourString(), time.to12HourString());
15}
16 17int main() {
18Time t{}; // instantiate object t of class Time
19 20displayTime("Initial time:", t); // display t's initial value
21t.setTime(13, 27, 6); // change time
22displayTime("After setTime:", t); // display t's new value
23 24// attempt to set the time with invalid values
25try {
26t.setTime(99, 99, 99); // all values out of range
27}
28catch (const invalid_argument& e) {
29cout << fmt::format("Exception: {} ", e.what());
30}
31 32// display t's value after attempting to set an invalid time
33displayTime("After attempting to set an invalid time:", t);
34}
Initial time:
24-hour time: 00:00:00
12-hour time: 12:00:00 AM
After setTime:
24-hour time: 13:27:06
12-hour time: 1:27:06 PM
Exception: hour, minute and/or second was out of range
After attempting to set an invalid time:
24-hour time: 13:27:06
12-hour time: 1:27:06 PM
The header Time.h
(Fig. 9.7) contains Time
’s class definition (lines 8–17). Rather than function definitions, the class contains function prototypes (lines 10–12) that describe the class’s public
interface without revealing the member-function implementations. The function prototype in line 10 indicates that setTime
requires three int
parameters and returns void
. The prototypes for to24HourString
and to12HourString
(lines 11–12) specify that they take no arguments and return a string
. Classes with one or more constructors would also declare them in the header, as we’ll do in subsequent examples.
11 The header still specifies the class’s private
data members (lines 14–16). Each uses a C++11 in-class initializer to set the data member to 0
. The compiler must know the class’s data members to determine how much memory to reserve for each object of the class. Including the header Time.h
in the client code provides the compiler with the information it needs to ensure that the client code calls class Time
’s member functions correctly.
#pragma once
ERR MOD In larger programs, headers also will contain other definitions and declarations. Attempts to include a header multiple times (inadvertently) often occur in programs with many headers that may themselves include other headers. This could lead to compilation errors if the same definition appears more than once in a preprocessed file. The #pragma once
directive (line 4) prevents time.h
’s contents from being included into the same source-code file more than once. In Chapter 15, we’ll discuss how C++20 modules help prevent such problems.
Time.cpp
(Fig. 9.8) defines class Time
’s member functions, which were declared in lines 10–12 of Fig. 9.7. For member functions to24HourString
and to12HourString
, the const
keyword must appear in both the function prototypes (Fig. 9.7, lines 11–12) and the function definitions (Fig. 9.8, lines 24 and 29).
To indicate that the member functions in Time.cpp
are part of class Time
, we must first include the Time.h
header (Fig. 9.8, line 6). This allows us to use the class name Time
in the Time.cpp
file (lines 11, 24 and 29). When compiling Time.cpp
, the compiler uses the information in Time.h
to ensure that
• the first line of each member function matches its prototype in Time.h
, and
• each member function knows about the class’s data members and other member functions.
::
)Each member function’s name (lines 11, 24 and 29) is preceded by the class name and the scope resolution operator (::
). This “ties” them to the (now separate) Time
class definition (Fig. 9.7), which declares the class’s members. The Time::
tells the compiler that each member function is in that class’s scope, and its name is known to other class members.
Without “Time::
” preceding each function name, the compiler would treat these as global functions with no relationship to class time. Such functions, also called “free” functions, cannot access Time
’s private
data and cannot call the class’s member functions without specifying an object. So, the compiler would not be able to compile these functions because it would not know that class Time
declares variables m_hour
, m_minute
and m_second
. In Fig. 9.8, lines 18–20, 25 and 31–32 would cause compilation errors because m_hour
, m_minute
and m_second
are not declared as local variables in each function nor are they declared as global variables.
setTime
and Throwing ExceptionsFunction setTime
(lines 11–21) is a public
function that declares three int
parameters and uses them to set the time. Lines 13–14 test each argument to determine whether the value is in range. If so, lines 18–20 assign the values to the m_hour
, m_minute
and m_second
data members, respectively. The hour
argument must be greater than or equal to 0
and less than 24
because the 24-hour time format represents hours as integers from 0 to 23. Similarly, the minute
and second
arguments must be greater than or equal to 0
and less than 60
.
If any of the values is outside its range, setTime
throws an exception (lines 15) of type invalid_argument
(header <stdexcept>
), which notifies the client code that an invalid argument was received. As you saw in Section 6.15, you can use try
…catch
to catch exceptions and attempt to recover from them, which we’ll do in Fig. 9.9. The throw
statement creates a new invalid_argument
object. That object’s constructor receives a custom error-message string. After the exception object is created, the throw
statement terminates function setTime
. Then, the exception is returned to the code that called setTime
.
Invalid values cannot be stored in a Time
object, because:
• when a Time
object is created, its default constructor is called and each data member is initialized to 0, as specified in lines 14–16 of Fig. 9.7—setting m_hour
, m_minute
and m_second
to 0 is the equivalent of 12 AM (midnight)—and
• all subsequent attempts by a client to modify the data members are scrutinized by function setTime
.
to24HourString
Member function to24HourString
(lines 24–26 of Fig. 9.8) takes no arguments and returns a formatted 24-hour string
with three colon-separated digit pairs. So, if the time is 1:30:07 PM, the function returns "13:30:07"
. Each {:02d}
placeholder in line 25 formats an integer (d
) in a field width of two. The 0
before the field width indicates that values with fewer than two digits should be formatted with leading zeros.
to12HourString
Function to12HourString
(lines 29–33) takes no arguments and returns a formatted 12-hour time string
containing the m_hour
, m_minute
and m_second
values separated by colons and followed by an AM or PM indicator (e.g., 10:54:27 AM
and 1:27:06 PM
). The function uses the placeholder {:02d}
to format m_minute
and m_second
as two-digit values with leading zeros, if necessary. Line 31 uses the conditional operator (?:
) to determine how m_hour
should be formatted. If m_hour
is 0
or 12
(AM or PM, respectively), it appears as 12; otherwise, we use the remainder operator (%
) to get a value from 1 to 11. The conditional operator in line 32 determines whether to include AM or PM.
PERF If a member function is fully defined in a class’s body (as we did in our Account
class examples), the member function is implicitly declared inline
(Section 5.12). This can improve performance. Remember that the compiler reserves the right not to inline any function. Similarly, optimizing compilers also reserve the right to inline functions even if they are not declared with the inline
keyword.
Only the simplest, most stable member functions (i.e., whose implementations are unlikely to change) should be defined in the class header. Every change to the header requires you to recompile every source-code file dependent on that header—a time-consuming task in large systems.
SE Member functions to24HourString
and to12HourString
take no arguments. They implicitly know about and are able to access the data members for the Time
object on which they’re invoked. This is a nice benefit of object-oriented programming. In general, member-function calls receive either no arguments or fewer arguments than function calls in non-object-oriented programs. This reduces the likelihood of passing wrong arguments, the wrong number of arguments or arguments in the wrong order.
Time
Once class Time
is defined, it can be used as a type in declarations, such as:
Time sunset{}; // object of type Time
array<Time, 5> arrayOfTimes{}; // std::array of 5 Time objects
Time& dinnerTimeRef{sunset}; // reference to a Time object
Time* timePtr{&sunset}; // pointer to a Time object
Figure 9.9 creates and manipulates a Time
object. Separating Time
’s interface from the implementation of its member functions does not affect the way that this client code uses the class. Line 8 includes Time.h
so the compiler knows how much space to reserve for the Time
object t
(line 18) and can ensure that Time
objects are created and manipulated correctly in the client code.
Throughout the program, we display string
representations of the Time
object using function displayTime
(lines 12–15), which calls Time
member functions to24HourString
and to12HourString
. Line 18 creates the Time
object t
. Recall that class Time
does not define a constructor, so this statement calls the compiler-generated default constructor. Thus, t
’s m_hour
, m_minute
and m_second
are set to 0
via their initializers in class Time
’s definition. Then, line 20 displays the time in 24-hour and 12-hour formats, respectively, to confirm that the members were correctly initialized. Line 21 sets a new valid time by calling member function setTime
, and line 22 again shows the time in both formats.
setTime
with Invalid ValuesTo show that setTime
validates its arguments, line 26 calls setTime
with invalid arguments of 99
for the hour
, minute
and second
parameters. We placed this statement in a try
block (lines 25–27) in case setTime
throws an invalid_argument
exception, which it will do in this example. When the exception occurs, it’s caught at lines 28–30, and line 29 displays the exception’s error message by calling its what
member function. Line 33 shows the time to confirm that setTime
did not change the time when invalid arguments were supplied.
People new to object-oriented programming often suppose that objects must be quite large because they contain data members and member functions. Logically, this is true. You may think of objects as containing data and functions physically (and our discussion has certainly encouraged this view). However, this is not the case.
PERF Objects contain only data, so they’re much smaller than if they also contained member functions. The member functions’ code is maintained separately from all objects of the class. Each object needs its own data because the data usually varies among the objects. The function code is the same for all objects of the class and can be shared among them.
Often a class’s interface and implementation will be created by one programmer and used by a separate programmer who implements the client code. A class-implementation programmer responsible for creating a reusable Time
class creates the header Time.h
and the source-code file Time.cpp
that #include
s the header, then provides these files to the client-code programmer. A reusable class’s source code often is available to client-code programmers as a library they can download from a website, like GitHub.com
.
The client-code programmer needs to know only Time
’s interface to use the class and must be able to compile Time.cpp
and link its object code. Since the class’s interface is part of the class definition in the Time.h
header, the client-code programmer must #include
this file in the client’s source-code file. The compiler uses the class definition in Time.h
to ensure that the client code creates and manipulates Time
objects correctly.
To create the executable Time
application, the last step is to link
1. the object code for the main
function (that is, the client code),
2. the object code for class Time
’s member-function implementations, and
3. the C++ Standard Library object code for the C++ classes (such as std::string
) used by the class-implementation programmer and the client-code programmer.
SE The linker’s output for the program of Section 9.7 is the executable application that users can run to create and manipulate a Time
object. Compilers and IDEs typically invoke the linker for you after compiling your code.
In Section 1.9, we showed how to compile and run C++ applications that contained one source-code (.cpp
) file. To compile and link multiple source-code files:16
• In Microsoft Visual Studio, add to your project (as shown in Section 1.9.1) all the custom headers and source-code files that make up the program, then build and run the project. You can place the headers in the project’s Header Files folder and the source-code files in the project’s Source Files folder, but these are mainly for organizing files in large projects. The programs will compile if you place all the files in the Source Files folder.
• For GNU C++, open a shell and change to the directory containing all the files for a given program. Then in your compilation command, either list each .cpp
file by name or use *.cpp
to compile all the .cpp
files in the current folder. The preprocessor automatically locates the program-specific headers in that folder.
• For Apple Xcode, add to your project (as shown in Section 1.9.2) all the headers and source-code files that make up a program, then build and run the project.
MOD 16. This process changes with C++20 modules, as we’ll discuss in Chapter 15.
A class’s data members and member functions belong to that class’s scope. Non-member functions are defined at global namespace scope, by default. (We discuss namespaces in more detail in Section 19.4.) Within a class’s scope, class members are immediately accessible by all of that class’s member functions and can be referenced by name. Outside a class’s scope, public
class members are referenced through
• an object name,
• a reference to an object, or
• a pointer to an object.
We refer to these as handles on an object. The handle’s type helps the compiler determine the interface (that is, the member functions) accessible to the client via that handle. We’ll see in Section 9.19 that an implicit handle (called the this
pointer) is inserted by the compiler each time you refer to a data member or member function from within an object.
.
) and Arrow (->
) Member-Selection OperatorsAs you know, you can use an object’s name or a reference to an object followed by the dot member-selection operator (.
) to access the object’s members. To reference an object’s members via a pointer to an object, follow the pointer name by the arrow member-selection operator (->
) and the member name, as in pointerName->
memberName.
public
Class Members Through Objects, References and PointersConsider an Account
class that has a public deposit
member function. Given the following declarations:
Account account{}; // an Account object
Account& ref{account}; // ref refers to an Account object
Account* ptr{&account}; // ptr points to an Account object
You can invoke member function deposit
using the dot (.
) and arrow (->
) member selection operators as follows:
account.deposit(123.45); // call deposit via account object
ref.deposit(123.45); // call deposit via reference to account
ptr->deposit(123.45); // call deposit via pointer to account
Again, you should use references over pointers whenever possible. We’ll continue to show pointers when they are required and to prepare you to work with them in the legacy code you’ll encounter in industry.
Access functions can read or display data, not modify it. Another use of access functions is to test whether a condition is true or false. Such functions are often called predicate functions. An example would be a std::array
’s or a std::vector
’s empty
function. A program might test empty
before attempting to read an item from the container object.17
17. Many programmers prefer to begin the names of predicate functions with the word “is
.” For example, useful predicate functions for our Time
class might be isAM
and isPM
.
SE A utility function (also called a helper function) is a private
member function that supports the operation of a class’s other member functions. Utility functions are declared private
because they’re not intended for use by the class’s clients. Typically, a utility function contains code that would otherwise be duplicated in several other member functions.
Time
Class Case Study: Constructors with Default ArgumentsThe program of Figs. 9.10–9.12 enhances class Time
to demonstrate a constructor with default arguments.
1// Fig. 9.10: Time.h
2// Time class containing a constructor with default arguments.
3// Member functions defined in Time.cpp.
4#pragma once // prevent multiple inclusions of header
5#include <string>
6 7// Time class definition
8class Time {
9public:
10// default constructor because it can be called with no arguments
11explicit Time(int hour = 0, int minute = 0, int second = 0);
12 13// set functions
14void setTime(int hour, int minute, int second);
15void setHour(int hour); // set hour (after validation)
16void setMinute(int minute); // set minute (after validation)
17void setSecond(int second); // set second (after validation)
18 19// get functions
20int getHour() const; // return hour
21int getMinute() const; // return minute
22int getSecond() const; // return second
23 24std::string to24HourString() const; // 24-hour time format string
25std::string to12HourString() const; // 12-hour time format string
26private:
27int m_hour{0}; // 0 - 23 (24-hour clock format)
28int m_minute{0}; // 0 - 59
29int m_second{0}; // 0 - 59
30};
1// Fig. 9.11: Time.cpp
2// Member-function definitions for class Time.
3#include <stdexcept>
4#include <string>
5#include <fmt/format.h> // In C++20, this will be #include <format>
6#include "Time.h" // include definition of class Time from Time.h
7using namespace std;
8 9// Time constructor initializes each data member
10Time::Time(int hour, int minute, int second) {
11setHour(hour); // validate and set private field m_hour
12setMinute(minute); // validate and set private field m_minute
13setSecond(second); // validate and set private field m_second
14}
15 16// set new Time value using 24-hour time
17void Time::setTime(int hour, int minute, int second) {
18// validate hour, minute and second
19if ((hour < 0 || hour >= 24) || (minute < 0 || minute >= 60) ||
20(second < 0 || second >= 60)) {
21throw invalid_argument{"hour, minute or second was out of range"};
22}
23 24m_hour = hour;
25m_minute = minute;
26m_second = second;
27}
28 29// set hour value
30void Time::setHour(int hour) {
31if (hour < 0 || hour >= 24) {
32throw invalid_argument{"hour must be 0-23"};
33}
34 35m_hour = hour;
36}
37 38// set minute value
39void Time::setMinute(int minute) {
40if (minute < 0 || minute >= 60) {
41throw invalid_argument{"minute must be 0-59"};
42}
43 44m_minute = minute;
45}
46 47// set second value
48void Time::setSecond(int second) {
49if (second < 0 && second >= 60) {
50throw invalid_argument{"second must be 0-59"};
51}
52 53m_second = second;
54}
55 56// return hour value
57int Time::getHour() const {return m_hour;}
58 59// return minute value
60int Time::getMinute() const {return m_minute;}
61 62// return second value
63int Time::getSecond() const {return m_second;}
64 65// return Time as a string in 24-hour format (HH:MM:SS)
66string Time::to24HourString() const {
67return fmt::format("{:02d}:{:02d}:{:02d}",
68getHour(), getMinute(), getSecond());
69}
70 71// return Time as string in 12-hour format (HH:MM:SS AM or PM)
72string Time::to12HourString() const {
73return fmt::format("{}:{:02d}:{:02d} {}",
74((getHour() % 12 == 0) ? 12 : getHour() % 12),
75getMinute(), getSecond(), (getHour() < 12 ? "AM" : "PM"));
76}
1// fig09_12.cpp
2// Constructor with default arguments.
3#include <iostream>
4#include <stdexcept>
5#include <string>
6#include <fmt/format.h> // In C++20, this will be #include <format>
7#include "Time.h" // include definition of class Time from Time.h
8using namespace std;
9 10// displays a Time in 24-hour and 12-hour formats
11void displayTime(string_view message, const Time& time) {
12cout << fmt::format("{} 24-hour time: {} 12-hour time: {} ",
13message, time.to24HourString(), time.to12HourString());
14}
15 16int main() {
17const Time t1{}; // all arguments defaulted
18const Time t2{2}; // hour specified; minute and second defaulted
19const Time t3{21, 34}; // hour and minute specified; second defaulted
20const Time t4{12, 25, 42}; // hour, minute and second specified
21 22cout << "Constructed with: ";
23displayTime("t1: all arguments defaulted", t1);
24displayTime("t2: hour specified; minute and second defaulted", t2);
25displayTime("t3: hour and minute specified; second defaulted", t3);
26displayTime("t4: hour, minute and second specified", t4);
27 28// attempt to initialize t5 with invalid values
29try {
30const Time t5{27, 74, 99}; // all bad values specified
31}
32catch (const invalid_argument& e) {
33cerr << fmt::format("t5 not created: {} ", e.what());
34}
35}
Constructed with:
t1: all arguments defaulted
24-hour time: 00:00:00
12-hour time: 12:00:00 AM
t2: hour specified; minute and second defaulted
24-hour time: 02:00:00
12-hour time: 2:00:00 AM
t3: hour and minute specified; second defaulted
24-hour time: 21:34:00
12-hour time: 9:34:00 PM
t4: hour, minute and second specified
24-hour time: 12:25:42
12-hour time: 12:25:42 PM
t5 not created: hour must be 0-23
Time
SE Like other functions, constructors can specify default arguments. Line 11 of Fig. 9.10 declares a Time
constructor with default arguments, specifying the default value 0
for each parameter. A constructor with default arguments for all its parameters is also a default constructor—that is, a constructor that can be invoked with no arguments. There can be at most one default constructor per class. Any change to a function’s default argument values requires the client code to be recompiled (to ensure that the program still functions correctly). Class Time
’s constructor is declared explicit
because it can be called with one argument. We discuss explicit
constructors in detail in Section 11.13. This Time
class also provides set and get functions for each data member (lines 15–17 and 20–22).
Time
’s ConstructorIn Fig. 9.11, lines 10–14 define the Time
constructor. The Time
constructor calls the setHour
, setMinute
and setSecond
functions to validate and assign values to the data members.
ERR Lines 11–13 of the constructor call setHour
to ensure that hour
is in the range 0–23, then calls setMinute
and setSecond
to ensure that minute
and second
are each in the range 0–59. Functions setHour
(lines 30–36), setMinute
(lines 39–45) and setSecond
(lines 48–54) each throw an exception if an out-of-range argument is received. If setHour
, setMinute
or setSecond
throws an exception during construction, the Time
object will not complete construction and will not exist for use in the program.
CG ERR The C++ Core Guidelines provide many constructor recommendations. We’ll see more in the next two chapters. If a class requires data members to have specific values (as in class Time
), the class should define a constructor that validates the data and, if invalid, throws an exception.18,19,20
18. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-ctor
.
19. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-complete
.
20. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-throw
.
Function main
in Fig. 9.12 initializes five Time
objects:
• one with all three arguments defaulted in the implicit constructor call (line 17),
• one with one argument specified (line 18),
• one with two arguments specified (line 19),
• one with three arguments specified (line 20) and
• one with three invalid arguments specified (line 30).
ERR The program displays each object in 24-hour and 12-hour time formats. For Time
object t5
(line 30), the program displays an error message because the constructor arguments are out of range. The variable t5
never represents a fully constructed object in this program, because the exception is thrown during construction.
Time
’s set and get Functions and ConstructorTime
’s set and get functions are called throughout the class’s body. In particular, the constructor (Fig. 9.11, lines 10–14) calls functions setHour
, setMinute
and setSecond
, and functions to24HourString
and to12HourString
call functions getHour
, getMinute
and getSecond
in lines 68 and lines 74–75. In each case, we could have accessed the class’s private
data directly.
SE Our current internal time representation uses three int
s, which require 12 bytes of memory on systems with four-byte int
s. Consider changing this to a single int
representing the total number of seconds since midnight, which requires only four bytes of memory. If we made this change, only the bodies of functions that directly access the private
data would need to change. In this class, we’d modify setTime
and the set and get function’s bodies for m_hour
, m_minute
and m_second
. There would be no need to modify the constructor or functions to24HourString
or to12HourString
because they do not access the data directly.
ERR Duplicating statements in multiple functions or constructors makes changing the class’s internal data representation more difficult. Implementing the Time
constructor and functions to24HourString
and to12HourString
as shown in this example reduces the likelihood of errors when altering the class’s implementation.
SE As a general rule: Avoid repeating code. This principle is generally referred to as DRY—“don’t repeat yourself.”21 Rather than duplicating code, place it in a member function that can be called by the class’s constructor or other member functions. This simplifies code maintenance and reduces the likelihood of an error if the code implementation is modified.
21. “Don't Repeat Yourself,” Wikipedia (Wikimedia Foundation, Accessed June 20, 2020), https://en.wikipedia.org/wiki/Don't_repeat_yourself
.
SE ERR A constructor can call the class’s other member functions. You must be careful when doing this. The constructor initializes the object, so data members used in the called function may not yet be initialized. Logic errors may occur if you use data members before they have been properly initialized.
SE Making data members private
and controlling access (especially write access) to those data members through public
member functions helps ensure data integrity. The benefits of data integrity are not automatic simply because data members are private
. You must provide appropriate validity checking.
Section 5.16 showed how to overload functions. A class’s constructors and member functions also can be overloaded. Overloaded constructors allow objects to be initialized with different types and/or numbers of arguments. To overload a constructor, provide a prototype and definition for each overloaded version. This also applies to overloaded member functions.
In Figs. 9.10–9.12, class Time
’s constructor had a default argument for each parameter. We could have defined that constructor instead as four overloaded constructors with the following prototypes:
Time(); // default m_hour, m_minute and m_second to 0
explicit Time(int hour); // default m_minute & m_second to 0
Time(int hour, int minute); // default m_second to 0
Time(int hour, int minute, int second); // no default values
CG Just as a constructor can call a class’s other member functions to perform tasks, constructors can call other constructors in the same class. The calling constructor is known as a delegating constructor—it delegates its work to another constructor. The C++ Core Guidelines recommend defining common code for overloaded constructors in one constructor, then using delegating constructors to call it.22 Before C++11, this would have been accomplished via a private
utility function called by all the constructors.
22. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-delegating
.
The first three of the four Time
constructors declared above can delegate work to one with three int
arguments, passing 0
as the default value for the extra parameters. To do so, you use a member initializer with the name of the class as follows:
Time::Time() : Time{0, 0, 0} {}
Time::Time(int hour) : Time{hour, 0, 0} {}
Time::Time(int hour, int minute) : Time{hour, minute, 0} {}
A destructor is a special member function that may not specify parameters or a return type. A class’s destructor name is the tilde character (~
) followed by the class name, such as ~Time
. This naming convention has intuitive appeal because, as we’ll see in a later chapter, the tilde is the bitwise complement operator. In a sense, the destructor is the complement of the constructor.
A class’s destructor is called implicitly when an object is destroyed, typically when program execution leaves the scope in which that object was created. The destructor itself does not actually remove the object from memory. It performs termination housekeeping (such as closing a file) before the object’s memory is reclaimed for later use.
Even though destructors have not been defined for the classes presented so far, every class has exactly one destructor. If you do not explicitly define a destructor, the compiler defines a default destructor that invokes any class-type data members’ destructors.23 In Chapter 12, we’ll explain why exceptions should not be thrown from destructors.
23. We’ll see that such a default destructor also destroys class objects that are created through inheritance (Chapter 10).
SE Constructors and destructors are called implicitly when objects are created and when they’re about to go out of scope, respectively. The order in which these are called depends on the objects’ scopes. Generally, destructor calls are made in the reverse order of the corresponding constructor calls, but as we’ll see in Figs. 9.13–9.15, global and static
objects can alter the order in which destructors are called.
1// Fig. 9.13: CreateAndDestroy.h
2// CreateAndDestroy class definition.
3// Member functions defined in CreateAndDestroy.cpp.
4#pragma once // prevent multiple inclusions of header
5#include <string>
6#include <string_view>
7 8class CreateAndDestroy {
9public:
10CreateAndDestroy(int ID, std::string_view message); // constructor
11~CreateAndDestroy(); // destructor
12private:
13int m_ID; // ID number for object
14std::string m_message; // message describing object
15};
1// Fig. 9.14: CreateAndDestroy.cpp
2// CreateAndDestroy class member-function definitions.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "CreateAndDestroy.h"// include CreateAndDestroy class definition
6using namespace std;
7 8// constructor sets object's ID number and descriptive message
9CreateAndDestroy::CreateAndDestroy(int ID, string_view message)
10:
m_ID{ID}, m_message{message} {
11cout << fmt::format("Object {} constructor runs {} ",
12m_ID, m_message);
13}
14 15// destructor
16CreateAndDestroy::~CreateAndDestroy() {
17// output newline for certain objects; helps readability
18cout << fmt::format("{}Object {} destructor runs {} ",
19(m_ID == 1 || m_ID == 6 ? " " : ""), m_ID, m_message);
20}
1// fig09_15.cpp
2// Order in which constructors and
3// destructors are called.
4#include <iostream>
5#include "CreateAndDestroy.h" // include CreateAndDestroy class definition
6using namespace std;
7 8void create(); // prototype
9 10const CreateAndDestroy first{1, "(global before main)"}; // global object
11 12int main() {
13cout << " MAIN FUNCTION: EXECUTION BEGINS ";
14const CreateAndDestroy second{2, "(local in main)"};
15static const CreateAndDestroy third{3, "(local static in main)"};
16 17create(); // call function to create objects
18 19cout << " MAIN FUNCTION: EXECUTION RESUMES ";
20const CreateAndDestroy fourth{4, "(local in main)"};
21cout << " MAIN FUNCTION: EXECUTION ENDS ";
22}
23 24// function to create objects
25void create() {
26cout << " CREATE FUNCTION: EXECUTION BEGINS ";
27const CreateAndDestroy fifth{5, "(local in create)"};
28static const CreateAndDestroy sixth{6, "(local static in create)"};
29const CreateAndDestroy seventh{7, "(local in create)"};
30cout << " CREATE FUNCTION: EXECUTION ENDS ";
31}
Object 1 constructor runs (global before main)
MAIN FUNCTION: EXECUTION BEGINS
Object 2 constructor runs (local in main)
Object 3 constructor runs (local static in main)
CREATE FUNCTION: EXECUTION BEGINS
Object 5 constructor runs (local in create)
Object 6 constructor runs (local static in create)
Object 7 constructor runs (local in create)
CREATE FUNCTION: EXECUTION ENDS
Object 7 destructor runs (local in create)
Object 5 destructor runs (local in create)
MAIN FUNCTION: EXECUTION RESUMES
Object 4 constructor runs (local in main)
MAIN FUNCTION: EXECUTION ENDS
Object 4 destructor runs (local in main)
Object 2 destructor runs (local in main)
Object 6 destructor runs (local static in create)
Object 3 destructor runs (local static in main)
Object 1 destructor runs (global before main)
SE ERR ERR Constructors are called for objects defined in global scope (also called global namespace scope) before any other function (including main
) in that file begins execution. The execution order of global object constructors among multiple files is not guaranteed. When main
terminates, the corresponding destructors are called in the reverse order of their construction. The exit
function often is used to terminate a program when a fatal unrecoverable error occurs. Function exit
forces a program to terminate immediately and does not execute the destructors of local objects. Function abort
performs similarly to function exit
but forces the program to terminate immediately, without allowing programmer-defined cleanup code of any kind to be called. Function abort
is usually used to indicate abnormal termination of the program. (See Appendix G for more information on functions exit
and abort
.)
static
Local ObjectsA non-static
local object’s constructor is called when execution reaches the object’s definition. Its destructor is called when execution leaves the object’s scope—that is, when the block in which that object is defined finishes executing normally or due to an exception. Destructors are not called for local objects if the program terminates with a call to function exit
or function abort
.
static
Local ObjectsThe constructor for a static
local object is called only once when execution first reaches the point where the object is defined. The corresponding destructor is called when main
terminates or the program calls function exit
. Global and static
objects are destroyed in the reverse order of their creation. Destructors are not called for static
objects if the program terminates with a call to function abort
.
The program of Figs. 9.13–9.15 demonstrates the order in which constructors and destructors are called for global, local and local static
objects of class CreateAndDestroy
(Fig. 9.13 and Fig. 9.14). This mechanical example is purely for pedagogic purposes.
Figure 9.13 declares class CreateAndDestroy
. Lines 13–14 declare the class’s data members—an integer (m_ID
) and a string
(m_message
) that identify each object in the program’s output.
The constructor and destructor implementations (Fig. 9.14) both display lines of output to indicate when they’re called. In the destructor, the conditional expression (line 19) determines whether the object being destroyed has the m_ID
value 1
or 6
and, if so, outputs a newline character to make the program’s output easier to follow.
Figure 9.15 defines object first
(line 10) in global scope. Its constructor is called before any statements in main
execute and its destructor is called at program termination after the destructors for all objects with automatic storage duration have run.
Function main
(lines 12–22) defines three objects. Objects second
(line 14) and fourth
(line 20) are local objects, and object third
(line 15) is a static
local object. The constructor for each of these objects is called when execution reaches the point where that object is defined. When execution reaches the end of main
, the destructors for objects fourth
then second
are called in the reverse of their constructors’ order. Object third
is static
, so it exists until program termination. The destructor for object third
is called before the destructor for global object first
, but after non-static
local objects are destroyed.
Function create
(lines 25–31) defines three objects—fifth
(line 27) and seventh
(line 29) are local automatic objects, and sixth
(line 28) is a static
local object. When create
terminates, the destructors for objects seventh
then fifth
are called in the reverse of their constructors’ order. Because sixth
is static
, it exists until program termination. The destructor for sixth
is called before the destructors for third
and first
, but after all other objects are destroyed.
Time
Class Case Study: A Subtle Trap — Returning a Reference or a Pointer to a private
Data MemberA reference to an object is an alias for the object’s name, so it may be used on the left side of an assignment statement. In this context, the reference makes a perfectly acceptable lvalue to which you can assign a value.
A member function can return a reference to a private
data member of that class. If the reference return type is declared const
, the reference is a nonmodifiable lvalue and cannot be used to modify the data. However, if the reference return type is not declared const
, subtle errors can occur.
The program of Figs. 9.16–9.18 uses a simplified Time
class (Fig. 9.16 and Fig. 9.17) to demonstrate returning a reference to a private
data member with member function badSetHour
(declared in Fig. 9.16 in line 12 and defined in Fig. 9.17 in lines 24–31). Such a reference return makes the result of a call to member function badSetHour
an alias for private
data member hour
! The function call can be used in any way that the private
data member can be used, including as an lvalue in an assignment statement, thus enabling clients of the class to overwrite the class’s private
data at will! A similar problem would occur if the function returned a pointer to the private
data.
1// Fig. 9.16: Time.h
2// Time class definition.
3// Member functions defined in Time.cpp
4 5// prevent multiple inclusions of header
6#pragma once
7 8class Time {
9public:
10void setTime(int hour, int minute, int second);
11int getHour() const;
12int& badSetHour(int h); // dangerous reference return
13private:
14int m_hour{0};
15int m_minute{0};
16int m_second{0};
17};
1// Fig. 9.17: Time.cpp
2// Time class member-function definitions.
3#include <stdexcept>
4#include "Time.h" // include definition of class Time
5using namespace std;
6 7// set values of hour, minute and second
8void Time::setTime(int hour, int minute, int second) {
9// validate hour, minute and second
10if ((hour < 0 || hour >= 24) || (minute < 0 || minute >= 60) ||
11(second < 0 || second >= 60)) {
12throw invalid_argument{"hour, minute or second was out of range"};
13}
14 15m_hour = hour;
16m_minute = minute;
17m_second = second;
18}
19 20// return hour value
21int Time::getHour() const {return m_hour;}
22 23// poor practice: returning a reference to a private data member.
24int& Time::badSetHour(int hour) {
25if (hour < 0 || hour >= 24) {
26throw invalid_argument{"hour must be 0-23"};
27}
28 29m_hour = hour;
30return m_hour; // dangerous reference return
31}
1// fig09_18.cpp
2// public member function that
3// returns a reference to private data.
4#include <iostream>
5#include <fmt/format.h>
6#include "Time.h" // include definition of class Time
7using namespace std;
8 9int main() {
10Time t{}; // create Time object
11 12// initialize hourRef with the reference returned by badSetHour
13int& hourRef{t.badSetHour(20)}; // 20 is a valid hour
14 15cout << fmt::format("Valid hour before modification: {} ", hourRef);
16hourRef = 30; // use hourRef to set invalid value in Time object t
17cout << fmt::format("Invalid hour after modification: {} ",
18t.getHour());
19 20// Dangerous: Function call that returns a reference can be
21// used as an lvalue! POOR PROGRAMMING PRACTICE!!!!!!!!
22t.badSetHour(12) = 74; // assign another invalid value to hour
23 24cout << "After using t.badSetHour(12) as an lvalue, "
25<< fmt::format("hour is: {} ", t.getHour());
26}
Valid hour before modification: 20
Invalid hour after modification: 30
After using t.badSetHour(12) as an lvalue, hour is: 74
ERR Figure 9.18 declares Time
object t
(line 10) and reference hourRef
(line 13), which we initialize with the reference returned by t.badSetHour(20)
. Line 15 displays hourRef
’s value to show how hourRef
breaks the class’s encapsulation. Statements in main
should not have access to the private
data in a Time
object. Next, line 16 uses the hourRef
to set hour
’s value to 30 (an invalid value). Lines 17–18 call getHour
to show that assigning to hourRef
modifies t
’s private
data. Line 22 uses the badSetHour
function call as an lvalue and assigns 74 (another invalid value) to the reference the function returns. Line 25 calls getHour
again to show that line 22 modifies the private
data in the Time
object t
.
SE Returning a reference or a pointer to a private
data member breaks the class’s encapsulation, making the client code dependent on the class’s data representation. There are cases where doing this is appropriate. We’ll show an example of this when we build our custom Array
class in Section 11.10.
The assignment operator (=
) can assign an object to another object of the same type. The default assignment operator generated by the compiler copies each data member of the right operand into the same data member in the left operand. Figures 9.19–9.20 define a Date
class. Line 15 of Fig. 9.21 uses the default assignment operator to assign Date
object date1
to Date
object date2
. In this case, date1
’s m_year
, m_month
and m_day
members are assigned to date2
’s m_year
, m_month
and m_day
members, respectively.
1// Fig. 9.19: Date.h
2// Date class declaration. Member functions are defined in Date.cpp.
3#pragma once // prevent multiple inclusions of header
4#include <string>
5 6// class Date definition
7class Date {
8public:
9explicit Date(int year, int month, int day);
10std::string toString() const;
11private:
12int m_year;
13int m_month;
14int m_day;
15};
1// Fig. 9.20: Date.cpp
2// Date class member-function definitions.
3#include <string>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Date.h" // include definition of class Date from Date.h
6using namespace std;
7 8// Date constructor (should do range checking)
9Date::Date(int year, int month, int day)
10: m_year{year}, m_month{month}, m_day{day} {}
11 12// return string representation of a Date in the format yyyy-mm-dd
13string Date::toString() const {
14return fmt::format("{}-{:02d}-{:02d}", m_year, m_month, m_day);
15}
1// fig09_21.cpp
2// Demonstrating that class objects can be assigned
3// to each other using the default assignment operator.
4#include <iostream>
5#include <fmt/format.h> // In C++20, this will be #include <format>
6#include "Date.h" // include definition of class Date from Date.h
7using namespace std;
8 9int main() {
10const Date date1{2004, 7, 4};
11Date date2{2020, 1, 1};
12 13cout << fmt::format("date1: {} date2: {} ",
14date1.toString(), date2.toString());
15date2 = date1; // uses the default assignment operator
16cout << fmt::format("After assignment, date2: {} ", date2.toString());
17}
date1: 2004-07-04
date2: 2020-01-01
After assignment, date2: 2004-07-04
Objects may be passed as function arguments and may be returned from functions. Such passing and returning are performed using pass-by-value by default—a copy of the object is passed or returned. In such cases, C++ creates a new object and uses a copy constructor to copy the original object’s data into the new object. For each class we’ve shown so far, the compiler provides a default copy constructor that copies each member of the original object into the corresponding member of the new object.24
24. In Chapter 11, we’ll discuss cases in which the compiler uses move constructors, rather than copy constructors.
const
Objects and const
Member FunctionsLet’s see how the principle of least privilege applies to objects. Some objects do not need to be modifiable, in which case you should declare them const
. Any attempt to modify a const
object results in a compilation error. The statement
const Time noon{12, 0, 0};
declares a const Time
object noon
and initializes it to 12 noon (12 PM). It’s possible to instantiate const
and non-const
objects of the same class.
PERF Declaring variables and objects const
can improve performance. Compilers can perform optimizations on constants that cannot be performed on non-const
variables.
SE C++ disallows calling a member function on a const
object unless that member functions is declared const
. So you should declared as const
any member function that does not modify the object on which it’s called.
ERR A constructor must be allowed to modify an object to initialize it. A destructor must be allowed to perform its termination housekeeping before an object’s memory is reclaimed by the system. So, attempting to declare a constructor or destructor const
is a compilation error. The “const
ness” of a const
object is enforced throughout the object’s lifetime, from when the constructor finishes initializing the object until that object’s destructor is called.
const
and Non-const
Member FunctionsThe program of Fig. 9.22 uses a copy of class Time
from Figs. 9.10–9.11, but removes const
from function to12HourString
’s prototype and definition to force a compilation error. We create two Time
objects—non-const
object wakeUp
(line 6) and const
object noon
(line 7). The program attempts to invoke non-const
member functions setHour
(line 11) and to12HourString
(line 15) on the const
object noon
. In each case, the compiler generates an error message. The program also illustrates the three other member-function-call combinations on objects:
• a non-const
member function on a non-const
object (line 10),
• a const
member function on a non-const
object (line 12) and
• a const
member function on a const
object (lines 13–14).
1// fig09_22.cpp
2// const objects and const member functions.
3#include "Time.h" // include Time class definition
4 5int main() {
6Time wakeUp{6, 45, 0}; // non-constant object
7const Time noon{12, 0, 0}; // constant object
8 9// OBJECT MEMBER FUNCTION
10wakeUp.setHour(18); // non-const non-const
11noon.setHour(12); // const non-const
12wakeUp.getHour(); // non-const const
13noon.getMinute(); // const const
14noon.to24HourString(); // const const
15noon.to12HourString(); // const non-const
16}
Microsoft Visual C++ compiler error messages:
C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(11,19): error C2662: 'void Time::setHour(int)': cannot convert 'this' pointer from 'const Time' to 'Time &'
C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(11,4): message : Conversion loses qualifiers
C:UsersPaulDeitelDocumentsexamplesch09fig09_22Time.h(15,9): message : see declaration of 'Time::setHour'
C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(15,26): error C2662: 'std::string Time::to12HourString(void)': cannot convert 'this' pointer from 'const Time' to 'Time &'
C:UsersPaulDeitelDocumentsexamplesch09fig09_22fig09_22.cpp(15,4): message : Conversion loses qualifiers
C:UsersPaulDeitelDocumentsexamplesch09fig09_22Time.h(25,16): message : see declaration of 'Time::to12HourString'
The error messages generated for non-const
member functions called on a const
object are shown in the output window. We added blank lines for readability.
SE A constructor must be a non-const
member function, but it can still be used to initialize a const
object (Fig. 9.22, line 7). Recall from Fig. 9.11 that the Time
constructor’s definition calls another non-const
member function—setTime
—to perform the initialization of a Time
object. Invoking a non-const
member function from the constructor call as part of the initialization of a const
object is allowed. The object is not const
until the constructor finishes initializing the object.
Line 15 in Fig. 9.22 generates a compilation error even though Time
’s member function to12HourString
does not modify the object on which it’s called. The fact that a member function does not modify an object is not sufficient. The function must explicitly be declared const
for this call to be allowed by the compiler.
SE An AlarmClock
object needs to know when it’s supposed to sound its alarm, so why not include a Time
object as a member of the AlarmClock
class? Such a software-reuse capability is called composition (or aggregation) and is sometimes referred to as a has-a relation-ship—a class can have objects of other classes as members.25 You’ve already used composition in this chapter’s Account
class examples. Class Account
contained a string
object as a data member.
25. As you’ll see in Chapter 10, classes also may be derived from other classes that provide attributes and behaviors the new classes can use—this is called inheritance.
You’ve seen how to pass arguments to the constructor of an object you created. Now we show how a class’s constructor can pass arguments to member-object constructors via member initializers.
The next program uses classes Date
(Figs. 9.23–9.24) and Employee
(Figs. 9.25–9.26) to demonstrate composition. Class Employee
’s definition (Fig. 9.25) has private
data members m_firstName
, m_lastName
, m_birthDate
and m_hireDate
. Members m_birth-Date
and m_hireDate
are const
objects of class Date
, which has private
data members m_year
, m_month
and m_day
. The Employee
constructor’s prototype (Fig. 9.25, lines 11–12) specifies that the constructor has four parameters (firstName
, lastName
, birthDate
and hireDate
). The first two parameters are passed via member initializers to the string
constructor for data members firstName
and lastName
. The last two are passed via member initializers to class Date
’s constructor for data members birthDate
and hireDate
.
1// Fig. 9.23: Date.h
2// Date class definition; Member functions defined in Date.cpp
3#pragma once // prevent multiple inclusions of header
4#include <string>
5 6class Date {
7public:
8static const int monthsPerYear{12}; // months in a year
9explicit Date(int year, int month, int day);
10std::string toString() const; // date string in yyyy-mm-dd format
11~Date(); // provided to show when destruction occurs
12private:
13int m_year; // any year
14int m_month; // 1-12 (January-December)
15int m_day; // 1-31 based on month
16 17// utility function to check if day is proper for month and year
18bool checkDay(int day) const;
19};
1// Fig. 9.24: Date.cpp
2// Date class member-function definitions.
3#include <array>
4#include <iostream>
5#include <stdexcept>
6#include <fmt/format.h> // In C++20, this will be #include <format>
7#include "Date.h" // include Date class definition
8using namespace std;
9 10// constructor confirms proper value for month; calls
11// utility function checkDay to confirm proper value for day
12Date::Date(int year, int month, int day)
13: m_year{year}, m_month{month}, m_day{day} {
14if (m_month < 1 || m_month > monthsPerYear) { // validate the month
15throw invalid_argument{"month must be 1-12"};
16}
17 18if (!checkDay(day)) { // validate the day
19throw invalid_argument{"Invalid day for current month and year"};
20}
21 22// output Date object to show when its constructor is called
23cout << fmt::format("Date object constructor: {} ", toString());
24}
25 26// gets string representation of a Date in the form yyyy-mm-dd
27string Date::toString() const {
28return fmt::format("{}-{:02d}-{:02d}", m_year, m_month, m_day);
29}
30 31// output Date object to show when its destructor is called
32Date::~Date() {
33cout << fmt::format("Date object destructor: {} ", toString());
34}
35 36// utility function to confirm proper day value based on
37// month and year; handles leap years, too
38bool Date::checkDay(int day) const {
39static const array daysPerMonth{
400, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
41 42// determine whether testDay is valid for specified month
43if (1 <= day && day <= daysPerMonth.at(m_month)) {
44return true;
45}
46 47// February 29 check for leap year
48if (m_month == 2 && day == 29 && (m_year % 400 == 0 ||
49(m_year % 4 == 0 && m_year % 100 != 0))) {
50return true;
51}
52 53return false; // invalid day, based on current m_month and m_year
54}
1// Fig. 9.25: Employee.h
2// Employee class definition showing composition.
3// Member functions defined in Employee.cpp.
4#pragma once // prevent multiple inclusions of header
5#include <string>
6#include <string_view>
7#include "Date.h" // include Date class definition
8 9class Employee {
10public:
11Employee(std::string_view firstName, std::string_view lastName,
12const Date& birthDate, const Date& hireDate);
13std::string toString() const;
14~Employee(); // provided to confirm destruction order
15private:
16std::string m_firstName; // composition: member object
17std::string m_lastName; // composition: member object
18Date m_birthDate; // composition: member object
19Date m_hireDate; // composition: member object
20};
1// Fig. 9.26: Employee.cpp
2// Employee class member-function definitions.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Employee.h" // Employee class definition
6#include "Date.h" // Date class definition
7using namespace std;
8 9// constructor uses member initializer list to pass initializer
10// values to constructors of member objects
11Employee::Employee(string_view firstName, string_view lastName,
12const Date &birthDate, const Date &hireDate)
13: m_firstName{firstName}, m_lastName{lastName},
14m_birthDate{birthDate}, m_hireDate{hireDate} {
15// output Employee object to show when constructor is called
16cout << fmt::format("Employee object constructor: {} {} ",
17m_firstName, m_lastName);
18}
19 20// gets string representation of an Employee object
21string Employee::toString() const {
22return fmt::format("{}, {} Hired: {} Birthday: {}", m_lastName,
23m_firstName, m_hireDate.toString(), m_birthDate.toString());
24}
25 26// output Employee object to show when its destructor is called
27Employee::~Employee() {
28cout << fmt::format("Employee object destructor: {}, {} ",
29m_lastName, m_firstName);
30}
Employee
Constructor’s Member-Initializer List SE CG The colon (:
) following the Employee
constructor’s header (Fig. 9.26, line 13) begins the member-initializer list. The member initializers pass the constructor’s parameters first-Name
, lastName
, birthDate
and hireDate
to the constructors of the composed string
and Date
data members m_firstName
, m_lastName
, m_birthDate
and m_hireDate
, respectively. The order of the member initializers does not matter. Data members are constructed in the order that they’re declared in class Employee
, not in the order they appear in the member-initializer list. For clarity, the C++ Core Guidelines recommend listing the member initializers in the order they’re declared in the class.26
26. C++ Core Guidelines. Accessed July 12, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-order
.
Date
Class’s Default Copy ConstructorAs you study class Date
(Fig. 9.23), notice it does not provide a constructor with a Date
parameter. So, why can the Employee
constructor’s member-initializer list initialize the m_birthDate
and m_hireDate
objects by passing Date
objects to their constructors? As we mentioned in Section 9.15, the compiler provides each class with a default copy constructor that copies each data member of the constructor’s argument object into the corresponding member of the object being initialized. Chapter 11 discusses how to define customized copy constructors.
Date
and Employee
Figure 9.27 creates two Date
objects (lines 10–11) and passes them as arguments to the constructor of the Employee
object created in line 12. There are fivetotal constructor calls when an Employee
is constructed:
• two calls to the string
class’s constructor (line 13 of Fig. 9.26),
• two calls to the Date
class’s default copy constructor (line 14 of Fig. 9.26),
• and the call to the Employee
class’s constructor, which calls the other four.
Line 14 of Fig. 9.27 outputs the Employee
object’s data.
1// fig09_27.cpp
2// Demonstrating composition--an object with member objects.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Date.h" // Date class definition
6#include "Employee.h" // Employee class definition
7using namespace std;
8 9int main() {
10const Date birth{1987 ,7, 24};
11const Date hire{2018, 3, 12};
12const Employee manager{"Sue", "Green", birth, hire};
13 14cout << fmt::format(" {} ", manager.toString());
15}
Date object constructor: 1987-07-24
Date object constructor: 2018-03-12
Employee object constructor: Sue Green
Green, Sue Hired: 2018-03-12 Birthday: 1987-07-24
Employee object destructor: Green, Sue
Date object destructor: 2018-03-12
Date object destructor: 1987-07-24
Date object destructor: 2018-03-12
Date object destructor: 1987-07-24
When each Date
object is created in lines 10–11, the Date
constructor (lines 12–24 of Fig. 9.24) displays a line of output to show that the constructor was called (see the first two lines of the sample output). However, line 12 of Fig. 9.27 causes two Date
copy-constructor calls (line 14 of Fig. 9.26) that do not appear in this program’s output. Since the compiler defines our Date
class’s copy constructor, it does not contain any output statements to demonstrate when it’s called.
Class Date
and class Employee
each include a destructor (lines 32–34 of Fig. 9.24 and lines 27–30 of Fig. 9.26, respectively) that prints a message when an object of its class is destructed. The destructors help us show that, though objects are constructed from the inside out, they’re destructed from the outside in. That is, the Date
member objects are destructed after the enclosing Employee
object.
Notice the last four lines in the output of Fig. 9.27. The last two lines are the outputs of the Date
destructor running on Date
objects hire
(Fig. 9.27, line 11) and birth
(line 10), respectively. The outputs confirm that the three objects created in main
are destructed in the reverse of the order from which they were constructed. The Employee
destructor output is five lines from the bottom. The fourth and third lines from the bottom of the output show the destructors running for the Employee
’s member objects m_hireDate
(Fig. 9.25, line 19) and m_birthDate
(line 18).
These outputs confirm that the Employee
object is destructed from the outside in. The Employee
destructor runs first (see the output five lines from the bottom). Then the member objects are destructed in the reverse order from which they were constructed. Class string
’s destructor does not contain output statements, so we do not see the first-Name
and lastName
objects being destructed.
ERR If you do not initialize a member object explicitly, the member object’s default constructor will be called implicitly to initialize the member object. If there is no default constructor, a compilation error occurs. Values set by the default constructor can be changed later by set functions. However, for complex initialization, this approach may require significant additional work and time.
PERF Initializing member objects explicitly through member initializers eliminates the overhead of “doubly initializing” member objects—once when the member object’s default constructor is called and again when set functions are called in the constructor body (or later) to change values in the member object.
friend
Functions and friend
ClassesA friend
function is a function with access to a class’s public
and non-public
class members. A class may have as friend
s:
• standalone functions,
• entire classes (and thus all their functions) or
• specific member functions of other classes.
This section presents a mechanical example of how a friend
function works. In Chapter 11, Operator Overloading, we’ll show friend
functions that overload operators for use with objects of custom classes. You’ll see that sometimes a member function cannot be used to define certain overloaded operators.
friend
To declare a non-member function as a friend
of a class, place the function prototype in the class definition and precede it with the keyword friend
. To declare all member functions of class ClassTwo
as friends of class ClassOne
, place in ClassOne
’s definition a declaration of the form:
friend class ClassTwo;
SE These are the basic friendship rules:
• Friendship is granted, not taken—For class B to be a friend
of class A, class A must declare that class B is its friend
.
• Friendship is not symmetric—If class A is a friend
of class B, you cannot infer that class B is a friend
of class A.
• Friendship is not transitive—If class A is a friend
of class B and class B is a friend
of class C, you cannot infer that class A is a friend
of class C.
friend
s Are Not Subject to Access ModifiersMember access notions of public
, protected
(Chapter 10) and private
do not apply to friend
declarations, so friend
declarations can be placed anywhere in a class definition. We prefer to place all friendship declarations first inside the class definition’s body and not precede them with any access specifier.
private
Data with a friend
FunctionFigure 9.28 defines friend
function setX
to set class Count
’s private
data member m_x
. Though a friend
declaration can appear anywhere in the class, by convention, place the friend
declaration (line 9) first in the class definition, .
1// fig09_28.cpp
2// Friends can access private members of a class.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5using namespace std;
6 7// Count class definition
8class Count {
9friend void setX(Count& c, int value); // friend declaration
10public:
11int getX() const {return m_x;}
12private:
13int m_x{0};
14};
15 16// function setX can modify private data of Count
17// because setX is declared as a friend of Count (line 8)
18void setX(Count& c, int value) {
19c.m_x = value; // allowed because setX is a friend of Count
20}
21 22int main() {
23Count counter{}; // create Count object
24 25cout << fmt::format("Initial counter.x value: {} ", counter.getX());
26setX(counter, 8); // set x using a friend function
27cout << fmt::format("counter.x after setX call: {} ", counter.getX());
28}
counter.x after instantiation: 0
counter.x after call to setX friend function: 8
Function setX
(lines 18–20) is a standalone (free) function, not a Count
member function. So, when we call setX
to modify counter
(line 26), we must pass counter
as an argument to setX
. Function setX
is allowed to access class Count
’s private
data member m_x
(line 19) only because the function was declared as a friend
of class Count
(line 9). If you remove the friend
declaration, you’ll receive error messages indicating that function setX
cannot modify class Count
’s private
data member m_x
.
friend
FunctionsIt’s possible to specify overloaded functions as friend
s of a class. Each overload intended to be a friend
must be explicitly declared in the class definition as a friend
, as you’ll see in Chapter 11.
this
PointerThere’s only one copy of each class’s functionality, but there can be many objects of a class, so how do member functions know which object’s data members to manipulate? Every object’s member functions access the object through a pointer called this
(a C++ keyword), which is an implicit argument to each of the object’s non-static
27 member functions.
27. Section 9.20 introduces static
class members and explains why the this
pointer is not implicitly passed to static
member functions.
this
Pointer to Avoid Naming Collisions SE Member functions use the this
pointer implicitly (as we’ve done so far) or explicitly to reference an object’s data members and other member functions. One explicit use of the this
pointer is to avoid naming conflicts between a class’s data members and constructor or member-function parameters. If a member function uses a local variable and data member with the same name, the local variable is said to hide or shadow the data member. Using just the variable name in the member function’s body refers to the local variable rather than the data member.
You can access the data member explicitly by qualifying its name with this->
. For instance, we could implement class Time
’s setHour
function as follows:
// set hour value
void Time::setHour(int hour) {
if (hour < 0 || hour >= 24) {
throw invalid_argument{"hour must be 0-23"};
}
this->hour = hour; // use this-> to access data member
}
where this->hour
represents a data member called hour
. You can avoid such naming collisions by naming your data members with the "m_"
prefix, as shown in our classes so far.
this
PointerThe this
pointer’s type depends on the object’s type and whether the member function in which this
is used is declared const
:
• In a non-const
member function of class Time
, the this
pointer is a Time*
—a pointer to a Time
object.
• In a const
member function, this
is a const Time*
—a pointer to a Time
constant.
this
Pointer to Access an Object’s Data MembersFigure 9.29 is a mechanical example that demonstrates implicit and explicit use of the this
pointer in a member function to display the private
data m_x
of a Test
object. In Section 9.19.2 and in Chapter 11, we show some substantial and subtle examples of using this
.
1// fig09_29.cpp
2// Using the this pointer to refer to object members.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5using namespace std;
6 7class Test {
8public:
9explicit Test(int value);
10void print() const;
11private:
12int m_x{0};
13};
14 15// constructor
16Test::Test(int value) : m_x{value} {} // initialize x to value
17 18// print x using implicit then explicit this pointers;
19// the parentheses around *this are required due to precedence
20void Test::print() const {
21// implicitly use the this pointer to access the member x
22cout << fmt::format(" x = {} ", m_x);
23 24// explicitly use the this pointer and the arrow operator
25// to access the member x
26cout << fmt::format(" this->x = {} ", this->m_x);
27 28// explicitly use the dereferenced this pointer and
29// the dot operator to access the member x
30cout << fmt::format("(*this).x = {} ", (*this).m_x);
31}
32 33int main() {
34const Test testObject{12}; // instantiate and initialize testObject
35testObject.print();
36}
x = 12 this->x = 12 (*this).x = 12
For illustration purposes, member function print
(lines 20–31) first displays x
using the this
pointer implicitly (line 22)—only the data member’s name is specified. Then print
uses two different notations to access x
through the this
pointer:
• this->m_x
(line 26) and
• (*this).m_x
(line 30).
The parentheses around *this
(line 30) are required because the dot operator (.
) has higher precedence than the *
pointer-dereferencing operator. Without the parentheses, the expression *this.x
would be evaluated as if it were parenthesized as *(this.x)
. The dot operator cannot be used with a pointer, so this would be a compilation error.
this
Pointer to Enable Cascaded Function Calls SE Another use of the this
pointer is to enable cascaded member-function calls—that is, invoking multiple functions sequentially in the same statement, as you’ll see in line 11 of Fig. 9.32. The program of Figs. 9.30–9.32 modifies class Time
’s setTime
, setHour
, set-Minute
and setSecond
functions such that each returns a reference to the Time
object on which it’s called. This reference enables cascaded member-function calls. In Fig. 9.31, the last statement in each of these member functions’ bodies returns a reference to *this
(lines 19, 29, 39 and 49).
1// Fig. 9.30: Time.h
2// Time class modified to enable cascaded member-function calls.
3#pragma once // prevent multiple inclusions of header
4#include <string>
5 6class Time {
7public:
8// default constructor because it can be called with no arguments
9explicit Time(int hour = 0, int minute = 0, int second = 0);
10 11// set functions
12Time& setTime(int hour, int minute, int second);
13Time& setHour(int hour); // set hour (after validation)
14Time& setMinute(int minute); // set minute (after validation)
15Time& setSecond(int second); // set second (after validation)
16 17int getHour() const; // return hour
18int getMinute() const; // return minute
19int getSecond() const; // return second
20std::string to24HourString() const; // 24-hour time format string
21std::string to12HourString() const; // 12-hour time format string
22private:
23int m_hour{0}; // 0 - 23 (24-hour clock format)
24int m_minute{0}; // 0 - 59
25int m_second{0}; // 0 - 59
26};
1// Fig. 9.31: Time.cpp
2// Time class member-function definitions.
3#include <stdexcept>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Time.h" // Time class definition
6using namespace std;
7 8// Time constructor initializes each data member
9Time::Time(int hour, int minute, int second) {
10setHour(hour); // validate and set private field m_hour
11setMinute(minute); // validate and set private field m_minute
12setSecond(second); // validate and set private field m_second
13}
14 15// set new Time value using 24-hour time
16Time& Time::setTime(int hour, int minute, int second) {
17Time time{hour, minute, second}; // create a temporary Time object
18*this = time; // if time is valid, assign its members to current object
19return *this; // enables cascading
20}
21 22// set hour value
23Time& Time::setHour(int hour) { // note Time& return
24if (hour < 0 || hour >= 24) {
25throw invalid_argument{"hour must be 0-23"};
26}
27 28m_hour = hour;
29return *this; // enables cascading
30}
31 32// set minute value
33Time& Time::setMinute(int m) { // note Time& return
34if (m < 0 || m >= 60) {
35throw invalid_argument{"minute must be 0-59"};
36}
37 38m_minute = m;
39return *this; // enables cascading
40}
41 42// set second value
43Time& Time::setSecond(int s) { // note Time& return
44if (s < 0 || s >= 60) {
45throw invalid_argument{"second must be 0-59"};
46}
47 48m_second = s;
49return *this; // enables cascading
50}
51 52// get hour value
53int Time::getHour() const {return m_hour;}
54 55// get minute value
56int Time::getMinute() const {return m_minute;}
57 58// get second value
59int Time::getSecond() const {return m_second;}
60 61// return Time as a string in 24-hour format (HH:MM:SS)
62string Time::to24HourString() const {
63return fmt::format("{:02d}:{:02d}:{:02d}",
64getHour(), getMinute(), getSecond());
65}
66 67// return Time as string in 12-hour format (HH:MM:SS AM or PM)
68string Time::to12HourString() const {
69return fmt::format("{}:{:02d}:{:02d} {}",
70((getHour() % 12 == 0) ? 12 : getHour() % 12),
71getMinute(), getSecond(), (getHour() < 12 ? "AM" : "PM"));
72}
1// fig09_32.cpp
2// Cascading member-function calls with the this pointer.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Time.h" // Time class definition
6using namespace std;
7 8int main() {
9Time t{}; // create Time object
10 11t.setHour(18).setMinute(30).setSecond(22); // cascaded function calls
12 13// output time in 24-hour and 12-hour formats
14cout << fmt::format("24-hour time: {} 12-hour time: {} ",
15t.to24HourString(), t.to12HourString());
16 17// cascaded function calls
18cout << fmt::format("New 12-hour time: {} ",
19t.setTime(20, 20, 20).to12HourString());
20}
24-hour time: 18:30:22
12-hour time: 6:30:22 PM
New 12-hour time: 8:20:20 PM
Notice the elegant new implementation of member function setTime
. Line 17 in Fig. 9.31, creates a local Time
object called time
using setTime
’s arguments. While initializing this object, the constructor call will fail and throw an exception if any argument is out of range. Otherwise, setTime
’s arguments are all valid, so line 18 assigns the time
object’s members to *this
—the Time
object on which setTime
was called.
In the next program (Fig. 9.32), we create Time
object t
(line 9), then use it in cascaded member-function calls (lines 11 and 19).
Why does the technique of returning *this
as a reference work? The dot operator (.
) groups left-to-right, so line 11
t.setHour(18).setMinute(30).setSecond(22);
first evaluates t.setHour(18)
, which returns a reference to (the updated) object t
as the value of this function call. The remaining expression is then interpreted as
t.setMinute(30).setSecond(22);
The t.setMinute(30)
call executes and returns a reference to the (further updated) object t
. The remaining expression is interpreted as
t.setSecond(22);
Line 19 (Fig. 9.32) also uses cascading. Note that we cannot chain another Time
member-function call after to12HourString
, because it does not return a reference to a Time
object. However, we could chain a call to a string
member function, because to12Hour-String
returns a string
. Chapter 11 presents several practical examples of using cascaded function calls.
static
Class Members—Classwide Data and Member FunctionsThere is an important exception to the rule that each object of a class has its own copy of all the class’s data members. In certain cases, all objects of a class should share only one copy of a variable. A static
data member is used for these and other reasons. Such a variable represents “classwide” information—that is, data shared by all objects of the class. You can use static
data members to save storage when a single copy of the data for all objects of a class will suffice, such as a constant that can be shared by all objects of the class.
Let’s further motivate the need for static
classwide data with an example. Suppose that we have a video game with Martian
s and other space creatures. Each Martian
tends to be brave and willing to attack other space creatures when the Martian
is aware that at least five Martian
s are present. If fewer than five are present, each Martian
becomes cowardly. So each Martian
needs to know the martianCount
. We could endow each object of class Martian
with martianCount
as a data member. If we do, every Martian
will have its own copy of the data member. Every time we create a new Martian
, we’d have to update the data member martianCount
in all Martian
objects. Doing this would require every Martian
object to know about all other Martian
objects in memory. This wastes space with redundant martianCount
copies and wastes time in updating the separate copies. Instead, we declare martianCount
to be static
to make it classwide data. Every Martian
can access martianCount
as if it were a data member of the Martian
, but only one copy of the static
variable martianCount
is maintained in the program. This saves space. We havve the Martian
constructor increment static
variable martianCount
and the Martian
destructor decrement martianCount
. Because there’s only one copy, we do not have to increment or decrement separate copies of martianCount
for every Martian
object.
static
Data Members17 A class’s static
data members have class scope. A static
data member must be initialized exactly once. Fundamental-type static
data members are initialized by default to 0
. A static const
data member can have an in-class initializer. As of C++17, you also may use in-class initializers for a non-const static
data member by preceding its declaration with the inline
keyword (as you’ll see momentarily in Fig. 9.33). If a static
data member is an object of a class that provides a default constructor, the static
data member need not be explicitly initialized because its default constructor will be called.
1// Fig. 9.33: Employee.h
2// Employee class definition with a static data member to
3// track the number of Employee objects in memory
4#pragma once
5#include <string>
6#include <string_view>
7 8class Employee {
9public:
10Employee(std::string_view firstName, std::string_view lastName);
11~Employee(); // destructor
12const std::string& getFirstName() const; // return first name
13const std::string& getLastName() const; // return last name
14 15// static member function
16static int getCount(); // return # of objects instantiated
17private:
18std::string m_firstName;
19std::string m_lastName;
20 21// static data
22inline static int m_count{0}; // number of objects instantiated
23};
static
Data MembersA class’s static
members exist even when no objects of that class exist. To access a public static
class data member or member function, simply prefix the class name and the scope resolution operator (::
) to the member name. For example, if our martianCount
variable is public
, it can be accessed with Martian::martianCount
, even when there are no Martian
objects. (Of course, using public
data is discouraged.)
SE A class’s private
(and protected
; Chapter 10) static
members are normally accessed through the class’s public
member functions or friend
s. To access a private static
or protected static
data member when no objects of the class exist, provide a public static
member function and call the function by prefixing its name with the class name and scope resolution operator. A static
member function is a service of the class as a whole, not of a specific object of the class.
static
Data MembersThis example demonstrates a private inline static
data member called m_count
(Fig. 9.33, line 22), which is initialized to 0
, and a public static
member function called getCount
(Fig. 9.33, line 16). A static
data member also can be initialized at file scope in the class’s implementation file. For instance, we could have placed the following statement in Employee.cpp
(Fig. 9.34) after the Employee.h
header is included:
int Employee::count{0};
1// Fig. 9.34: Employee.cpp
2// Employee class member-function definitions.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Employee.h" // Employee class definition
6using namespace std;
7 8// define static member function that returns number of
9// Employee objects instantiated (declared static in Employee.h)
10int Employee::getCount() {return m_count;}
11 12// constructor initializes non-static data members and
13// increments static data member count
14Employee::Employee(string_view firstName, string_view lastName)
15: m_firstName(firstName), m_lastName(lastName) {
16++m_count; // increment static count of employees
17cout << fmt::format("Employee constructor called for {} {} ",
18m_firstName, m_lastName);
19}
20 21// destructor decrements the count
22Employee::~Employee() {
23cout << fmt::format("~Employee() called for {} {} ",
24m_firstName, m_lastName);
25--m_count; // decrement static count of employees
26}
27 28// return first name of employee
29const string& Employee::getFirstName() const {return m_firstName;}
30 31// return last name of employee
32const string& Employee::getLastName() const {return m_lastName;}
In Fig. 9.34, line 10 defines static
member function getCount
. Note that line 10 does not include the static
keyword, which cannot be applied to a member definition that appears outside the class definition. In this program, data member m_count
maintains a count of the number of Employee
objects in memory at a given time. When Employee
objects exist, member m_count
can be referenced through any member function of an Employee
object. In Fig. 9.34, m_count
is referenced by both line 16 in the constructor and line 25 in the destructor.
Figure 9.35 uses static
member function getCount
to determine the number of Employee
objects in memory at various points in the program. The program calls Employee::getCount()
:
• before any Employee
objects have been created (line 12),
• after two Employee
objects have been created (line 23) and
• after those Employee
objects have been destroyed (line 33).
1// fig09_35.cpp
2// static data member tracking the number of objects of a class.
3#include <iostream>
4#include <fmt/format.h> // In C++20, this will be #include <format>
5#include "Employee.h" // Employee class definition
6using namespace std;
7 8int main() {
9// no objects exist; use class name and scope resolution
10// operator to access static member function getCount
11cout << fmt::format("Initial employee count: {} ",
12Employee::getCount()); // use class name
13 14// the following scope creates and destroys
15// Employee objects before main terminates
16{
17const Employee e1{"Susan", "Baker"};
18const Employee e2{"Robert", "Jones"};
19 20// two objects exist; call static member function getCount again
21// using the class name and the scope resolution operator
22cout << fmt::format("Employee count after creating objects: {} ",
23Employee::getCount());
24 25cout << fmt::format("Employee 1: {} {} Employee 2: {} {} ",
26e1.getFirstName(), e1.getLastName(),
27e2.getFirstName(), e2.getLastName());
28}
29 30// no objects exist, so call static member function getCount again
31// using the class name and the scope resolution operator
32cout << fmt::format("Employee count after objects are deleted: {} ",
33Employee::getCount());
34}
Initial employee count: 0
Employee constructor called for Susan Baker
Employee constructor called for Robert Jones
Employee count after creating objects: 2
Employee 1: Susan Baker
Employee 2: Robert Jones
~Employee() called for Robert Jones
~Employee() called for Susan Baker
Employee count after objects are deleted: 0
Lines 16–28 in main
define a nested scope. Recall that local variables exist until the scope in which they’re defined terminates. In this example, we create two Employee
objects in the nested scope (lines 17–18). As each constructor executes, it increments class Employee
’s static
data member count
. These Employee
objects are destroyed when the program reaches line 28. At that point, each object’s destructor executes and decrements class Employee
’s static
data member count
.
static
Member Function Notes SE A member function should be declared static
if it does not access the class’s non-static
data members or non-static
member functions. A static
member function does not have a this
pointer, because static
data members and static
member functions exist independently of any objects of a class. The this
pointer must refer to a specific object, but a static
member function can be called when there are no objects of its class in memory. So, using the this
pointer in a static
member function is a compilation error.
ERR ERR A static
member function may not be declared const
. The const
qualifier indicates that a function cannot modify the contents of the object on which it operates, but static
member functions exist and operate independently of any objects of the class. So, declaring a static
member function const
is a compilation error.
According to Section 9.4.1 of the C++ standard document
http://wg21.link/n4861
an aggregate type is a built-in array, an array
object or an object of a class that:
• does not have user-declared constructors,
• does not have private
or protected
(Chapter 10) non-static
data members,
• does not have virtual functions (Chapter 10) and
• does not have virtual
(Chapter 19), private
(Chapter 10) or protected
(Chapter 10) base classes.
20 The requirement for no user-declared constructors was a C++20 change to the definition of aggregates. It prevents a case in which initializing an aggregate object could circumvent calling a user-declared constructor.28
28. “Prohibit aggregates with user-declared constructors.” Accessed July 12, 2020. http://wg21.link/p1008r1
.
You can define an aggregate using a class in which all the data is declared public
. However, a struct
is a class that contains only public
members by default. The following struct
defines an aggregate type named Record
containing four public
data members:
struct Record {
int account;
string first;
string last;
double balance;
};
CG The C++ Core Guidelines recommend using class
rather than struct
if any data member or member function needs to be non-public
.29
29. C++ Core Guidelines. Accessed July 12. 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-class
.
You can initialize an object of aggregate type Record
as follows:
Record record{100, "Brian", "Blue", 123.45};
11 In C++11, you could not use a list initializer for an aggregate-type object if any of the type’s non-static
data-member declarations contained in-class initializers. For example, the initialization above would have generated a compilation error if the aggregate type Record
were defined with a default value for balance
, as in:
struct Record {
int account;
string first;
string last;
double balance{0.0};
};
14 C++14 removed this restriction. Also, if you initialize an aggregate-type object with fewer initializers than there are data members in the object, as in
Record record{0, "Brian", "Blue"};
the remaining data members are initialized as follows:
• Data members with in-class initializers use those values—in the preceding case, record
’s balance
is set to 0.0
.
• Data members without in-class initializers are initialized with empty braces ({}
). Empty-brace initialization sets fundamental-type variables to 0
, sets bool
s to false
and value initializes objects—that is, they’re zero initialized, then the default constructor is called for each object.
As of C++20, aggregates now support designated initializers in which you can specify which data members to initialize by name. Using the preceding struct Record
definition, we could initialize a Record
object as follows:
Record record{.first{"Sue"}, .last{"Green"}};
explicitly initializing only a subset of the data members. Each explicitly named data member is preceded by a dot (.
), and the identifiers that you specify must be listed in the same order as they’re declared in the aggregate type. The preceding statement initializes the data members first
and last
to "Sue"
and "Green"
, respectively. The remaining data members get their default initializer values:
• account
is set to 0
and
• balance
is set to its default value in the type definition—in this case, 0.0
.
SE Adding new data members to an aggregate type will not break existing statements that use designated initializers. Any new data members that are not explicitly initialized simply receive their default initialization. Designated initializers also improve compatibility with the C programming language, which has had this feature since C99.
30. “Designated Initialization.” Accessed July 12, 2020. http://wg21.link/p0329r0
.
More and more computing today is done “in the cloud”—that is, distributed across the Internet. Many applications you use daily communicate over the Internet with cloud-based services that use massive clusters of computing resources (computers, processors, memory, disk drives, databases, etc.).
A service that provides access to itself over the Internet is known as a web service. Applications typically communicate with web services by sending and receiving JSON objects. JSON (JavaScript Object Notation) is a text-based, human-and-computer-readable, data-interchange format that represents objects as collections of name–value pairs. JSON has become the preferred data format for transmitting objects across platforms.
Each JSON object contains a comma-separated list of property names and values in curly braces. For example, the following name–value pairs might represent a client record:
{"account": 100, "name": "Jones", "balance": 24.98}
JSON also supports arrays as comma-separated values in square brackets. For example, the following represents a JSON array of numbers:
[100, 200, 300]
Values in JSON objects and arrays can be:
• strings in double-quotes (like "Jones"
),
• numbers (like 100
or 24.98
),
• JSON Boolean values (represented as true
or false
),
• null
(to represent no value),
• arrays of any valid JSON value, and
• other JSON objects.
JSON arrays may contain elements of the same or different types.
Converting an object into another format for storage or transmission over the Internet is known as serialization. Similarly, reconstructing an object from serialized data is known as deserialization. JSON is just one of several serialization formats. Other common formats include binary data and XML (eXtensible Markup Language).
Some programming languages have their own serialization mechanisms that use a language-native format. Deserializing objects using these native serialization formats is a source of various security issues. According to the Open Web Application Security Project (OWASP), these native mechanisms “can be repurposed for malicious effect when operating on untrusted data. Attacks against deserializers have been found to allow denial-of-service, access control, and remote code execution (RCE) attacks.”31 OWASP also indicates that you can significantly reduce attack risk by avoiding language-native serialization formats in favor of “pure data” formats like JSON or XML.
31. “Deserialization Cheat Sheet.” OWASP Cheat Sheet Series. Accessed July 18, 2020. https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html
.
cereal
Header-Only Serialization LibraryThe cereal
header-only library32 serializes objects to and deserializes objects from JSON (which we’ll demonstrate), XML or binary formats. The library supports fundamental types and can handle most standard library types if you include each type’s appropriate cereal
header. As you’ll see in the next section, cereal
also supports custom types. The cereal
documentation is available at:
https://uscilab.github.io/cereal/index.html
32. Copyright (c) 2017, Grant, W. Shane and Voorhies, Randolph. cereal
—A C++11 library for serialization. URL: http://uscilab.github.io/cereal/
. All rights reserved.
We’ve included the cereal library for your convenience in the libraries
folder with the book’s examples. You must point your IDE or compiler at the library’s include
folder, as you’ve done in several earlier Objects Natural case studies.
vector
of Objects containing public
DataLet’s begin by serializing objects containing only public
data. In Fig. 9.36, we:
• create a vector
of Record
objects and display its contents,
• use cereal
to serialize it to a text file, then
• deserialize the file’s contents into a vector
of Record
objects and display the deserialized Record
s.
1// fig09_36.cpp
2// Serializing and deserializing objects with the cereal library.
3#include <iostream>
4#include <fstream>
5#include <vector>
6#include <fmt/format.h> // In C++20, this will be #include <format>
7#include <cereal/archives/json.hpp>
8#include <cereal/types/vector.hpp>
9 10using namespace std;
To perform JSON serialization, include the cereal
header json.hpp
(line 7). To serialize a std::vector
, include the cereal
header vector.hpp
(line 8).
Record
Record
is an aggregate type defined as a struct
. Recall that an aggregate’s data must be public
, which is the default for a struct
definition:
11 12struct Record {
13int account{};
14string first{};
15string last{};
16double balance{};
17};
18
serialize
for Record
ObjectsThe cereal
library allows you to designate how to perform serialization several ways. If the types you wish to serialize have all public
data, you can simply define a function template serialize
(lines 21–27) that receives an Archive
as its first parameter and an object of your type as the second.33 This function is called both for serializing and deserializing the Record
objects. Using a function template enables you to choose among serialization and deserialization using JSON, XML or binary formats by passing an object of the appropriate cereal
archive type. The library provides archive implementations for each case.
33. Function serialize
also may be defined as a member function template with only an Archive
parameter. For details, see https://uscilab.github.io/cereal/serialization_functions.html
.
19// function template serialize is responsible for serializing and
20// deserializing Record objects to/from the specified Archive
21template <typename Archive>
22void serialize(Archive& archive, Record& record) {
23archive(cereal::make_nvp("account", record.account),
24cereal::make_nvp("first", record.first),
25cereal::make_nvp("last", record.last),
26cereal::make_nvp("balance", record.balance));
27}
28
Each cereal
archive type has an overloaded parentheses operator that enables you to use parameter archive objects as function names, as shown with parameter archive
in lines 23–26. Depending on whether you’re serializing or deserializing a Record
, this function will either:
• output the contents of the Record
to a specified stream or
• input previously serialized data from a specified stream and create a Record
object.
Each call to cereal::make_nvp
(that is, “make name–value pair”), like line 23
cereal::make_nvp("account", record.account)
is primarily for the serialization step. It makes a name–value pair with the name in the first argument (in this case, "account"
) and the value in the second argument (in this case, the int
value record.account
). Naming the values is not required but makes the JSON output more readable as you’ll soon see. Otherwise, cereal uses names like value0
, value1
, etc.
displayRecords
We provide function displayRecords
to show you the contents of our Record
objects before serialization and after deserialization. The function simply displays the contents of each Record
in the vector
it receives as an argument:
29// display record at command line
30void displayRecords(const vector<Record>& records) {
31for (const auto& r : records) {
32cout << fmt::format("{} {} {} {:.2f} ",
33r.account, r.first, r.last, r.balance);
34}
35}
36
Record
Objects to SerializeLines 38–41 in main
create a vector
and initialize it with two Record
s (lines 39 and 40). The compiler determines the vector
’s Record
element type from the initializers. Line 44 outputs the vector
’s contents to confirm that the two Record
s were initialized properly:
37int main() {
38vector records{
39Record{100, "Brian", "Blue", 123.45},
40Record{200, "Sue", "Green", 987.65}
41};
42 43cout << "Records to serialize: ";
44displayRecords(records);
45
Records to serialize:
100 Brian Blue 123.45
200 Sue Green 987.65
Record
Objects with cereal::JSONOutputArchive
A cereal::JSONOutputArchive
serializes data in JSON format to a specified stream, such as the standard output stream or a stream representing a file. Line 47 attempts to open the file records.json
for writing. If successful, line 48 creates a cereal::JSONOutputArchive
object named archive
and initializes it with the output ofstream
object so archive
can write the JSON data into a file. Line 49 uses the archive
object to output a name–value pair with the name "records"
and the vector
of Record
s as its value. Part of serializing the vector
is serializing each of its elements. So line 49 also results in one call to serialize
(lines 21–27) for each Record
object in the vector
.
46// serialize vector of Records to JSON and store in text file
47if (ofstream output{"records.json"}) {
48cereal::JSONOutputArchive archive{output};
49archive(cereal::make_nvp("records", records)); // serialize records
50}
51
records.json
After line 49 executes, the file records.json
contains the following JSON data:
The outer braces represent the entire JSON document. The darker box highlights the document’s one name–value pair named "records"
, which has as its value a JSON array containing the two JSON objects in the lighter boxes. The JSON array represents the vector records
that we serialized in line 49. Each of the JSON objects in the array contains one Record
’s four name–value pairs that were serialized by the serialize
function.
Record
Objects with cereal::JSONInputArchive
Next, let’s deserialize the data and use it to fill a separate vector
of Record
objects. For cereal
to recreate objects in memory, it must have access to each type’s default constructor. It will use that to create an object, then directly access that object’s data members to place the data into the object. Like classes, the compiler provides a public
default constructor for struct
s if you do not define a custom constructor.
52// deserialize JSON from text file into vector of Records
53if (ifstream input{"records.json"}) {
54cereal::JSONInputArchive archive{input};
55vector<Record> deserializedRecords{};
56archive(deserializedRecords); // deserialize records
57cout << " Deserialized records: ";
58displayRecords(deserializedRecords);
59}
60}
Deserialized records:
100 Brian Blue 123.45
200 Sue Green 987.65
A cereal::JSONInputArchive
deserializes data in JSON format from a specified stream, such as the standard input stream or a stream representing a file. Line 53 attempts to open the file records.json
for reading. If successful, line 54 creates the object archive
of type cereal::JSONInputArchive
and initializes it with the input ifstream
object so archive
can read JSON data from the records.json
file. Line 55 creates an empty vector
of Record
s into which we’ll read the JSON data. Line 56 uses the archive
object to deserialize the file’s data into the deserializedRecords
object. Part of deserializing the vector
is deserializing its elements. This again results in calls to serialize
(lines 21–27), but because archive
is a cereal::JSONInputArchive
, each call to serialize
reads one Record
’s JSON data, creates a Record
object then inserts the data into it.
vector
of Objects containing private
DataIt is also possible to serialize objects containing private
data. To do so, you must declare the serialize
function as a friend
of the class, so it can access the class’s private
data. To demonstrate serializing private
data, we created a copy of Fig. 9.36 and replaced the aggregate Record
definition with the class Record
definition (lines 14–37) in Fig. 9.38.
1// fig09_37.cpp
2// Serializing and deserializing objects containing private data.
3#include <iostream>
4#include <fstream>
5#include <string>
6#include <string_view>
7#include <vector>
8#include <fmt/format.h> // In C++20, this will be #include <format>
9#include <cereal/archives/json.hpp>
10#include <cereal/types/vector.hpp>
11 12using namespace std;
13 14class Record {
15// declare serialize as a friend for direct access to private data
16template<typename Archive>
17friend void serialize(Archive& archive, Record& record);
18 19public:
20// constructor
21explicit Record(int account = 0, string_view first = "",
22string_view last = "", double balance = 0.0)
23: m_account{account}, m_first{first},
24m_last{last}, m_balance{balance} {}
25 26// get member functions
27int getAccount() const {return m_account;}
28const string& getFirst() const {return m_first;}
29const string& getLast() const {return m_last;}
30double getBalance() const {return m_balance;}
31 32private:
33int m_account{};
34string m_first{};
35string m_last{};
36double m_balance{};
37};
38 39// function template serialize is responsible for serializing and
40// deserializing Record objects to/from the specified Archive
41template <typename Archive>
42void serialize(Archive& archive, Record& record) {
43archive(cereal::make_nvp("account", record.m_account),
44cereal::make_nvp("first", record.m_first),
45cereal::make_nvp("last", record.m_last),
46cereal::make_nvp("balance", record.m_balance));
47}
48 49// display record at command line
50void displayRecords(const vector<Record>& records) {
51for (auto& r : records) {
52cout << fmt::format("{} {} {} {:.2f} ", r.getAccount(),
53r.getFirst(), r.getLast(), r.getBalance());
54}
55}
56 57int main() {
58vector records{
59Record{100, "Brian", "Blue", 123.45},
60Record{200, "Sue", "Green", 987.65}
61};
62 63cout << "Records to serialize: ";
64displayRecords(records);
65 66// serialize vector of Records to JSON and store in text file
67if (ofstream output{"records2.json"}) {
68cereal::JSONOutputArchive archive{output};
69archive(cereal::make_nvp("records", records)); // serialize records
70}
71 72// deserialize JSON from text file into vector of Records
73if (ifstream input{"records2.json"}) {
74cereal::JSONInputArchive archive{input};
75vector<Record> deserializedRecords{};
76archive(deserializedRecords); // deserialize records
77cout << " Deserialized records: ";
78displayRecords(deserializedRecords);
79}
80}
This Record
class provides a constructor (21–24), get member functions (lines 27–30) and private
data (lines 33–36). There are two key items to note about this class:
• Lines 16–17 declare the function template serialize
as a friend
of this class. This enables serialize
to access directly the data members account
, first
, last
and balance
.
• The constructor’s parameters all have default arguments, which allows cereal
to use this as the default constructor when deserializing Record
objects.
The serialize
function (lines 41–46) now accesses class Record
’s private
data members, and the displayRecords
function (lines 50–55) now uses each Record
’s get functions to access the data to display. The main
function is identical to section Section 9.22.1 and produces the same results, so we do not show the output here.
In this chapter, you created your own classes, created objects of those classes and called member functions of those objects to perform useful actions. You declared data members of a class to maintain data for each object of the class, and you defined member functions to operate on that data. You also learned how to use a class’s constructor to specify the initial values for an object’s data members.
We used a Time
class case study to introduce various additional features. We showed how to engineer a class to separate its interface from its implementation. You used the arrow operator to access an object’s members via a pointer to an object. You saw that member functions have class scope—the member function’s name is known only to the class’s other member functions unless referred to by a client of the class via an object name, a reference to an object of the class, a pointer to an object of the class or the scope resolution operator. We also discussed access functions (commonly used to retrieve the values of data members or to test whether a condition is true or false), and utility functions (private
member functions that support the operation of the class’s public
member functions).
You saw that a constructor can specify default arguments that enable it to be called multiple ways. You also saw that any constructor that can be called with no arguments is a default constructor and that there can be at most one default constructor per class. We demonstrated how to share code among constructors with delegating constructors. We discussed destructors for performing termination housekeeping on an object before that object is destroyed, and demonstrated the order in which an object’s constructors and destructors are called.
We showed the problems that can occur when a member function returns a reference or a pointer to a private
data member, which breaks the class’s encapsulation. We also showed that objects of the same type can be assigned to one another using the default assignment operator.
You learned how to specify const
objects and const
member functions to prevent modifications to objects, thus enforcing the principle of least privilege. You also learned that, through composition, a class can have objects of other classes as members. We demonstrated how to declare and use friend
functions.
You saw that the this
pointer is passed as an implicit argument to each of a class’s non-static
member functions, allowing them to access the correct object’s data members and other non-static
member functions. We used the this
pointer explicitly to access the class’s members and to enable cascaded member-function calls. We motivated the notion of static
data members and member functions and demonstrated how to declare and use them.
We introduced aggregate types and C++20’s designated initializers for aggregates. Finally, we presented our next “objects natural” case study on serializing objects with JSON (JavaScript Object Notation) using the cereal
library.
In the next chapter, we continue our discussion of classes by introducing inheritance. We’ll see classes that share common attributes and behavior can inherit them from a common “base” class. Then, we build on our discussion of inheritance by introducing polymorphism. This object-oriented concept enables us to write programs that handle, in a more general manner, objects of classes related by inheritance.