Time
Class Case Study: Separating Interface from ImplementationEach of our prior class-definition examples 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 entire implementation of the class to the class’s clients—a header is simply a text file that anyone can open and read.
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.
If client code does know 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 class’s implementation while minimizing, and hopefully eliminating, changes to client code.
Our first example in this chapter creates and manipulates an object of class Time
. We demonstrate two important C++ software engineering concepts:
Separating interface from implementation.
Using an include guard 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 preprocessing directives prevents multiple-definition errors.
Interfaces define and standardize the ways in which 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 carries out the services. 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 and the class’s data members.
To separate the class’s interface from its implementation, we break up class Time
into two files—the header Time.h
(Fig. 9.1) in which class Time
is defined, and the source-code file Time.cpp
(Fig. 9.2) in which Time
’s member functions are defined—so that
the class is reusable,
the clients of the class know what member functions the class provides, how to call them and what return types to expect, and
the clients do not know how the class’s member functions are implemented.
By convention, member-function definitions are placed in a source-code file of the same base name (e.g., Time
) as the class’s header but with a .cpp
filename extension (some compilers support other filename extensions as well). The source-code file in Fig. 9.3 defines function main
(the client code). Section 9.3 shows a diagram and explains how this three-file program is compiled from the perspectives of the Time
class programmer and the client-code programmer—and what the Time
application user sees.
Time
Class DefinitionThe header Time.h
(Fig. 9.1) contains Time
’s class definition (lines 11–20). Instead of function definitions, the class contains function prototypes (lines 13–15) that describe the class’s public
interface without revealing the member-function implementations. The function prototype in line 13 indicates that setTime
requires three int
parameters and returns void
. The prototypes for member functions toUniversalString
and toStandardString
(lines 14–15) each specify that the function takes no arguments and returns a string
. This particular class does not define a constructor, but classes with constructors would also declare those constructors in the header (as we will do in subsequent examples).
The header still specifies the class’s private
data members (lines 17–19) as well—in this case, each uses a C++11 in-class list initializer to set the data member to 0
. Again, the compiler must know the data members of the class to determine how much memory to reserve for each object of the class. Including the header Time.h
in the client code (line 6 of Fig. 9.3) provides the compiler with the information it needs to ensure that the client code calls the member functions of class Time
correctly.
In Fig. 9.1, the class definition is enclosed in the following include guard (lines 7, 8 and 22):
#ifndef TIME_H
#define TIME_H
...
#endif
When we build larger programs, other definitions and declarations will also be placed in headers. The preceding include guard prevents the code between #ifndef
(which means “if not defined”) and #endif
from being #include
d if the name TIME_H
has been defined. When Time.h
is #include
d the first time, the identifier TIME_H
is not yet defined. In this case, the #define
directive defines TIME_H
and the preprocessor includes the Time.h
header’s contents in the .cpp
file. If the header is #include
d again, TIME_H
is defined already and the code between #ifndef
and #endif
is ignored by the preprocessor. Attempts to include a header multiple times (inadvertently) typically occur in large programs with many headers that may themselves include other headers.
Use #ifndef
, #define
and #endif
preprocessing directives to form an include guard that prevents headers from being included more than once in a source-code file.
By convention, use the name of the header in uppercase with the period replaced by an underscore in the #ifndef
and #define
preprocessing directives of a header.
Time
Class Member FunctionsThe source-code file Time.cpp
(Fig. 9.2) defines class Time
’s member functions, which were declared in lines 13–15 of Fig. 9.1. Note that for the const
member functions toUniversalString
and toStandardString
, the const
keyword must appear in both the function prototypes (Fig. 9.1, lines 14–15) and the function definitions (Fig. 9.2, lines 26 and 34).
::
)Each member function’s name (lines 12, 26 and 34) is preceded by the class name and the scope resolution operator (::
). This “ties” them to the (now separate) Time
class definition (Fig. 9.1), which declares the class’s members. The Time::
tells the compiler that each member function is within that class’s scope and its name is known to other class members.
Without “Time::
” preceding each function name, these functions would not be recognized by the compiler as Time
member functions. Instead, the compiler would consider them “free” or “loose” functions, like main
—these are also called global functions. Such functions cannot access Time
’s private
data or call the class’s member functions, without specifying an object. So, the compiler would not be able to compile these functions. For example, lines 28–29 and 36–38 in Fig. 9.2 that access data members hour
, minute
and second
would cause compilation errors because these variables are not declared as local variables in each function—the compiler would not know that hour
, minute
and second
are already declared in class Time
.
When defining a class’s member functions outside that class, omitting the class name and scope resolution operator (::
) that should precede the function names causes compilation errors.
To indicate that the member functions in Time.cpp
are part of class Time
, we must first include the Time.h
header (Fig. 9.2, line 7). This allows us to use the class name Time
in the Time.cpp
file (lines 12, 26 and 34). When compiling Time.cpp
, the compiler uses the information in Time.h
to ensure that
the first line of each member function (lines 12, 26 and 34) matches its prototype in the Time.h
file—for example, the compiler ensures that setTime
accepts three int
parameters and returns nothing, and
each member function knows about the class’s data members and other member functions—e.g., lines 28–29 and 36–38 can access data members hour
, minute
and second
because they’re declared in Time.h
as data members of class Time
.
Time
Class Member Function setTime
and Throwing ExceptionsFunction setTime
(lines 12–23) is a public
function that declares three int
parameters and uses them to set the time. Line 14 tests each argument to determine whether the value is in range, and, if so, lines 15–17 assign the values to the hour
, minute
and second
data members, respectively. The hour
value must be greater than or equal to 0
and less than 24
, because universal-time format represents hours as integers from 0 to 23 (e.g., 11 AM is hour 11, 1 PM is hour 13 and 11 PM is hour 23; midnight is hour 0 and noon is hour 12). Similarly, both minute
and second
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 20–21) of type invalid_argument
(from header <stdexcept>
), which notifies the client code that an invalid argument was received. As you saw in Section 7.10, you can use try
…catch
to catch exceptions and attempt to recover from them, which we’ll do in Fig. 9.3. The throw
statement creates a new object of type invalid_argument
. The parentheses following the class name indicate a call to the invalid_argument
constructor that allows us to specify a custom error-message string. After the exception object is created, the throw
statement immediately terminates function setTime
and the exception is returned to the code that attempted to set the time.
Invalid values cannot be stored in the data members of 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 17–19 of Fig. 9.1—setting hour
, minute
and second
to 0 is the universal-time equivalent of 12 AM (midnight)—and
all subsequent attempts by a client to modify the data members are scrutinized by function setTime
.
Time
Class Member Function toUniversalString
and String Stream ProcessingMember function toUniversalString
(lines 26–31 of Fig. 9.2) takes no arguments and returns a string
containing the time formatted as universal time with three colon-separated pairs of digits. For example, if the time were 1:30:07 PM, toUniversalString
would return 13:30:07
.
As you know, cout
is the standard output stream. Objects of class ostringstream
(from the header <sstream>
) provide the same functionality, but write their output to string
objects in memory. You use class ostringstream
’s str
member function to get the formatted string
.
Member function toUniversalString
creates an ostringstream
object named output (line 27), then uses it like cout
in lines 28–29 to create the formatted string. Line 28 uses parameterized stream manipulator setfill
to specify the fill character that’s displayed when an integer is output in a field wider than the number of digits in the value. The fill characters appear to the left of the digits in the number, because the number is right aligned by default—for left-aligned values (specified with the left
stream manipulator) the fill characters would appear to the right. In this example, if the minute
value is 2, it will be displayed as 02, because the fill character is set to zero ('0'
). If the number being output fills the specified field, the fill character will not be displayed. Once the fill character is specified with setfill
, it applies for all subsequent values that are displayed in fields wider than the value being displayed—setfill
is a sticky setting. This is in contrast to setw
, which applies only to the next value displayed—setw
is a nonsticky setting. Line 30 calls ostringstream
’s str
member function to get the formatted string
, which is returned to the client.
Each sticky setting (such as a fill character or precision) should be restored to its previous setting when it’s no longer needed. Failure to do so may result in incorrectly formatted output later in a program. Section 13.7.8 discusses how to reset the formatting for an output stream.
Time
Class Member Function toStandardString
Function toStandardString
(lines 34–40) takes no arguments and returns a string containing the time formatted as standard time with the hour
, minute
and second
values separated by colons and followed by an AM or PM indicator (e.g., 10:54:27
AM
and 1:27:06 PM
). Like function toUniversalString
, function toStandardString
uses setfill('0')
to format the minute
and second
as two-digit values with leading zeros if necessary. Line 36 uses the conditional operator (?:
) to determine the value of hour
to be displayed—if the hour
is 0
or 12
(AM or PM, respectively), it appears as 12; otherwise, we use the remainder operator (%
) to have the hour
appear as a value from 1 to 11. The conditional operator in line 38 determines whether AM or PM will be displayed. Line 39 calls ostringstream
’s str
member function to return the formatted string
.
If a member function is defined in a class’s body, the member function is implicitly declared inline. Remember that the compiler reserves the right not to inline any function.
Defining a member function inside the class definition inlines the member function (if the compiler chooses to do so). This can improve performance.
Only the simplest and most stable member functions (i.e., whose implementations are unlikely to change) should be defined in the class header, because every change to the header requires you to recompile every source-code file that’s dependent on that header (a time-consuming task in large systems).
The toUniversalString
and toStandardString
member functions take no arguments, because these member functions implicitly know that they’re to create string
representations of the data for the particular Time
object on which they’re invoked. This can make member-function calls more concise than conventional function calls in procedural programming.
Using an object-oriented programming approach often requires fewer arguments when calling functions. This benefit derives from the fact that encapsulating data members and member functions within a class gives the member functions the right to access the data members.
Member functions are usually shorter than functions in non-object-oriented programs, because the data stored in data members have ideally been validated by a constructor or by member functions that store new data. Because the data is already in the object, the member-function calls often have no arguments or fewer arguments than function calls in non-object-oriented languages. Thus, the calls, the function definitions and the function prototypes are shorter. This improves many aspects of program development.
The fact that member-function calls generally take either no arguments or fewer arguments than conventional function calls in non-object-oriented languages reduces the likelihood of passing the wrong arguments, the wrong types of arguments or the wrong number of arguments.
Time
As you know, once a class like Time
is defined, it can be used as a type in declarations, such as:
Time sunset; // object of type Time
array<Time, 5> arrayOfTimes; // array of 5 Time objects
Time& dinnerTimeRef{sunset}; // reference to a Time object
Time* timePtr{&sunset}; // pointer to a Time object
Figure 9.3 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. It affects only how the program is compiled and linked, which we discuss in Section 9.3. Line 6 of Fig. 9.3 includes the Time.h
header so the compiler knows how much space to reserve for the Time
object t
(line 16) and can ensure that Time
objects are created and manipulated correctly in the client code.
Throughout the program, we display 24-hour and 12-hour string
representations of a Time
object using function displayTime
(lines 10–13), which calls Time
member functions toUniversalString
and toStandardString
. Line 16 creates the Time
object t
. Recall that class Time
does not define a constructor, so line 16 invokes the compiler-generated default constructor, and t
’s hour
, minute
and second
are set to 0
via their initializers in class Time
’s definition. Then, line 18 displays the time in universal and standard formats, respectively, to confirm that the members were initialized properly. Line 19 sets a new valid time by calling member function setTime
, and line 20 again displays the time in both formats.
setTime
with Invalid ValuesTo illustrate that the setTime
member function validates its arguments, line 24 calls setTime
with invalid arguments of 99
for the hour
, minute
and second
. This statement is placed in a try
block (lines 23–25) in case setTime
throws an invalid_argument
exception, which it will do. When this occurs, the exception is caught at lines 26–28, and line 27 displays the exception’s error message by calling its what
member function. Line 31 displays the time again 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 (and our discussion has certainly encouraged this view); physically, however, this is not the case.
Objects contain only data, so objects are much smaller than if they also contained member functions. The compiler creates one copy (only) of the member functions separate from all objects of the class. All objects of the class share this one copy. Each object, of course, needs its own copy of the class’s data, because the data can vary among the objects. The function code is the same for all objects of the class and, hence, can be shared among them.