9.2 Time Class Case Study: Separating Interface from Implementation

Each 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.

9.2.1 Interface of a Class

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.

9.2.2 Separating the Interface from the Implementation

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

  1. the class is reusable,

  2. the clients of the class know what member functions the class provides, how to call them and what return types to expect, and

  3. 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.

9.2.3 Time Class Definition

The 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).

Fig. 9.1 Time class definition.

Alternate View

 1   // Fig. 9.1: Time.h
 2   // Time class definition.                  
 3   // Member functions are defined in Time.cpp
 4   #include <string>
 5
 6   // prevent multiple inclusions of header
 7   #ifndef TIME_H
 8   #define TIME_H
 9
10    // Time class definition
11    class Time {
12    public:
13       void setTime(int, int, int); // set hour, minute and second
14       std::string toUniversalString() const; // 24-hour time format string
15       std::string toStandardString() const; // 12-hour time format string
16    private:
17       unsigned int hour{0}; // 0 - 23 (24-hour clock format)
18       unsigned int minute{0}; // 0 - 59
19       unsigned int second{0}; // 0 - 59
20    };
21
22    #endif

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.

Include Guard1

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 #included if the name TIME_H has been defined. When Time.h is #included 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 #included 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.

Error-Prevention Tip 9.1

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.

 

Good Programming Practice 9.1

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.

9.2.4 Time Class Member Functions

The 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).

Fig. 9.2 Time class member-function definitions.

Alternate View

 1   // Fig. 9.2: Time.cpp
 2   // Time class member-function definitions.
 3   #include <iomanip> // for setw and setfill stream manipulators
 4   #include <stdexcept> // for invalid_argument exception class
 5   #include <sstream> // for ostringstream class
 6   #include <string>
 7   #include "Time.h" // include definition of class Time from Time.h
 8
 9   using namespace std;
10
11   // set new Time value using universal time
12   void Time::setTime(int h, int m, int s) {
13      // validate hour, minute and second
14      if ((h >= 0 && h < 24) && (m >= 0 && m < 60) && (s >= 0 && s < 60)) {
15         hour = h;
16         minute = m;
17         second = s;
18      }
19      else {
20         throw invalid_argument(                            
21            "hour, minute and/or second was out of range"); 
22      }
23    }
24
25    // return Time as a string in universal-time format (HH:MM:SS)
26    string Time::toUniversalString() const {
27       ostringstream output;
28    output << setfill('0') << setw(2) << hour << ":"
29       << setw(2) << minute << ":" << setw(2) << second;
30    return output.str(); // returns the formatted string
31    }
32
33    // return Time as string in standard-time format (HH:MM:SS AM or PM)
34    string Time::toStandardString() const {
35       ostringstream output;
36       output << ((hour == 0 || hour == 12) ? 12 : hour % 12) << ":"
37          << setfill('0') << setw(2) << minute << ":" << setw(2)
38          << second << (hour < 12 ? " AM" : " PM");
39       return output.str(); // returns the formatted string
40    }

9.2.5 Scope Resolution Operator (::)

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.

Common Programming Error 9.1

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.

9.2.6 Including the Class Header in the Source-Code File

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.

9.2.7 Time Class Member Function setTime and Throwing Exceptions

Function 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 trycatch 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.

9.2.8 Time Class Member Function toUniversalString and String Stream Processing

Member 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.

Error-Prevention Tip 9.2

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.

9.2.9 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.

9.2.10 Implicitly Inlining Member Functions

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.

Performance Tip 9.1

Defining a member function inside the class definition inlines the member function (if the compiler chooses to do so). This can improve performance.

 

Software Engineering Observation 9.1

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).

9.2.11 Member Functions vs. Global Functions

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.

Software Engineering Observation 9.2

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.

 

Software Engineering Observation 9.3

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.

 

Error-Prevention Tip 9.3

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.

9.2.12 Using Class 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.

Fig. 9.3 Program to test class Time.

Alternate View

 1   // Fig. 9.3: fig09_03.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 "Time.h" // definition of class Time from Time.h
 7   using namespace std;
 8
 9   // displays a Time in 24-hour and 12-hour formats
10   void displayTime(const string& message, const Time& time) {
11      cout << message << "
Universal time: " << time.toUniversalString()
12         << "
Standard time: " << time.toStandardString() << "

";
13   }
14
15   int main() {
16      Time t; // instantiate object t of class Time
17
18      displayTime("Initial time:", t); // display t's initial value
19      t.setTime(13, 27, 6); // change time
20      displayTime("After setTime:", t); // display t's new value
21
22      // attempt to set the time with invalid values
23      try {
24         t.setTime(99, 99, 99); // all values out of range
25      }
26      catch (invalid_argument& e) {
27         cout << "Exception: " << e.what() << "

";
28      }
29
30      // display t's value after attempting to set an invalid time
31      displayTime("After attempting to set an invalid time:", t);
32    }

Initial time:
Universal time: 00:00:00
Standard time: 12:00:00 AM

After setTime:
Universal time: 13:27:06
Standard time: 1:27:06 PM

Exception: hour, minute and/or second was out of range

After attempting to set an invalid time:
Universal time: 13:27:06
Standard time: 1:27:06 PM

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.

Calling setTime with Invalid Values

To 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.

9.2.13 Object Size

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.

Performance Tip 9.2

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset