Our first example (Figs. 9.19.3) creates class Time and a driver program that tests the class. You have already created several classes in this book. In this section, we review many of the concepts covered in Chapter 3 and demonstrate an important C++ software engineering conceptusing a "preprocessor wrapper" in header files to prevent the code in the header from being included into the same source code file more than once. Since a class can be defined only once, using such preprocessor directives prevents multiple-definition errors.
Figure 9.1. Time class definition.
1 // Fig. 9.1: Time.h 2 // Declaration of class Time. 3 // Member functions are defined in Time.cpp 4 5 // prevent multiple inclusions of header file 6 #ifndef TIME_H 7 #define TIME_H 8 9 // Time class definition 10 class Time 11 { 12 public: 13 Time(); // constructor 14 void setTime( int, int, int ); // set hour, minute and second 15 void printUniversal(); // print time in universal-time format 16 void printStandard(); // print time in standard-time format 17 private: 18 int hour; // 0 - 23 (24-hour clock format) 19 int minute; // 0 - 59 20 int second; // 0 - 59 21 }; // end class Time 22 23 #endif |
Time Class Definition
The class definition (Fig. 9.1) contains prototypes (lines 1316) for member functions Time, setTime, printUniversal and printStandard. The class includes private integer members hour, minute and second (lines 1820). Class Time's private data members can be accessed only by its four member functions. Chapter 12 introduces a third access specifier, protected, as we study inheritance and the part it plays in object-oriented programming.
Good Programming Practice 9.1
For clarity and readability, use each access specifier only once in a class definition. Place public members first, where they are easy to locate. |
Software Engineering Observation 9.1
Each element of a class should have private visibility unless it can be proven that the element needs public visibility. This is another example of the principle of least privilege. |
In Fig. 9.1, note that the class definition is enclosed in the following preprocessor wrapper (lines 57 and 23):
// prevent multiple inclusions of header file #ifndef TIME_H #define TIME_H ... #endif
When we build larger programs, other definitions and declarations will also be placed in header files. The preceding preprocessor wrapper prevents the code between #ifndef (which means "if not defined") and #endif from being included if the name TIME_H has been defined. If the header has not been included previously in a file, the name TIME_H is defined by the #define directive and the header file statements are included. If the header has been included previously, TIME_H is defined already and the header file is not included again. Attempts to include a header file multiple times (inadvertently) typically occur in large programs with many header files that may themselves include other header files. [Note: The commonly used convention for the symbolic constant name in the preprocessor directives is simply the header file name in upper case with the underscore character replacing the period.]
Error-Prevention Tip 9.1
Use #ifndef, #define and #endif preprocessor directives to form a preprocessor wrapper that prevents header files from being included more than once in a program. |
Good Programming Practice 9.2
Use the name of the header file in upper case with the period replaced by an underscore in the #ifndef and #define preprocessor directives of a header file. |
Time Class Member Functions
In Fig. 9.2, the Time constructor (lines 1417) initializes the data members to 0 (i.e., the universal-time equivalent of 12 AM). This ensures that the object begins in a consistent state. Invalid values cannot be stored in the data members of a Time object, because the constructor is called when the Time object is created, and all subsequent attempts by a client to modify the data members are scrutinized by function setTime (discussed shortly). Finally, it is important to note that the programmer can define several overloaded constructors for a class.
Figure 9.2. Time class member-function definitions.
1 // Fig. 9.2: Time.cpp 2 // Member-function definitions for class Time. 3 #include 4 using std::cout; 5 6 #include 7 using std::setfill; 8 using std::setw; 9 10 #include "Time.h" // include definition of class Time from Time.h 11 12 // Time constructor initializes each data member to zero. 13 // Ensures all Time objects start in a consistent state. 14 Time::Time() 15 { 16 hour = minute = second = 0; 17 } // end Time constructor 18 19 // set new Time value using universal time; ensure that 20 // the data remains consistent by setting invalid values to zero 21 void Time::setTime( int h, int m, int s ) 22 { 23 hour = ( h >= 0 && h < 24 ) ? h : 0; // validate hour 24 minute = ( m >= 0 && m < 60 ) ? m : 0; // validate minute 25 second = ( s >= 0 && s < 60 ) ? s : 0; // validate second 26 } // end function setTime 27 28 // print Time in universal-time format (HH:MM:SS) 29 void Time::printUniversal() 30 { 31 cout << setfill( '0' ) << setw( 2 ) << hour << ":" 32 << setw( 2 ) << minute << ":" << setw( 2 ) << second; 33 } // end function printUniversal 34 35 // print Time in standard-time format (HH:MM:SS AM or PM) 36 void Time::printStandard() 37 { 38 cout << ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 ) << ":" 39 << setfill( '0' ) << setw( 2 ) << minute << ":" << setw( 2 ) 40 << second << ( hour < 12 ? " AM" : " PM" ); 41 } // end function printStandard |
The data members of a class cannot be initialized where they are declared in the class body. It is strongly recommended that these data members be initialized by the class's constructor (as there is no default initialization for fundamental-type data members). Data members can also be assigned values by Time's set functions. [Note: Chapter 10 demonstrates that only a class's static const data members of integral or enum types can be initialized in the class's body.]
Common Programming Error 9.1
Attempting to initialize a non-static data member of a class explicitly in the class definition is a syntax error. |
Function setTime (lines 2126) is a public function that declares three int parameters and uses them to set the time. A conditional expression tests each argument to determine whether the value is in a specified range. For example, the hour value (line 23) must be greater than or equal to 0 and less than 24, because the universal-time format represents hours as integers from 0 to 23 (e.g., 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 values (lines 24 and 25) must be greater than or equal to 0 and less than 60. Any values outside these ranges are set to zero to ensure that a Time object always contains consistent datathat is, the object's data values are always kept in range, even if the values provided as arguments to function setTime were incorrect. In this example, zero is a consistent value for hour, minute and second.
A value passed to setTime is a correct value if it is in the allowed range for the member it is initializing. So, any number in the range 023 would be a correct value for the hour. A correct value is always a consistent value. However, a consistent value is not necessarily a correct value. If setTime sets hour to 0 because the argument received was out of range, then hour is correct only if the current time is coincidentally midnight.
Function printUniversal (lines 2933 of Fig. 9.2) takes no arguments and outputs the date in universal-time format, consisting of three colon-separated pairs of digitsfor the hour, minute and second, respectively. For example, if the time were 1:30:07 PM, function printUniversal would return 13:30:07. Note that line 31 uses parameterized stream manipulator setfill to specify the fill character that is displayed when an integer is output in a field wider than the number of digits in the value. By default, the fill characters appear to the left of the digits in the number. 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. Note that, 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 (i.e., setfill is a "sticky" setting). This is in contrast to setw, which applies only to the next value displayed (setw is a "nonsticky" setting).
Error-Prevention Tip 9.2
Each sticky setting (such as a fill character or floating-point precision) should be restored to its previous setting when it is no longer needed. Failure to do so may result in incorrectly formatted output later in a program. Chapter 15, Stream Input/Output, discusses how to reset the fill character and precision. |
Function printStandard (lines 3641) takes no arguments and outputs the date in standard-time format, consisting of the hour, minute and second values separated by colons and followed by an AM or PM indicator (e.g., 1:27:06 PM). Like function printUniversal, function printStandard uses setfill( '0' ) to format the minute and second as two digit values with leading zeros if necessary. Line 38 uses a conditional operator (?:) to determine the value of hour to be displayedif the hour is 0 or 12 (AM or PM), it appears as 12; otherwise, the hour appears as a value from 1 to 11. The conditional operator in line 40 determines whether AM or PM will be displayed.
Defining Member Functions Outside the Class Definition; Class Scope
Even though a member function declared in a class definition may be defined outside that class definition (and "tied" to the class via the binary scope resolution operator), that member function is still within that class's scope; i.e., its name is known only to other members of the class unless referred to via an object of the class, a reference to an object of the class, a pointer to an object of the class or the binary scope resolution operator. We will say more about class scope shortly.
If a member function is defined in the body of a class definition, the C++ compiler attempts to inline calls to the member function. Member functions defined outside a class definition can be inlined by explicitly using keyword 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.2
Defining a small member function inside the class definition does not promote the best software engineering, because clients of the class will be able to see the implementation of the function, and the client code must be recompiled if the function definition changes. |
Software Engineering Observation 9.3
Only the simplest and most stable member functions (i.e., whose implementations are unlikely to change) should be defined in the class header. |
Member Functions vs. Global Functions
It is interesting that the printUniversal and printStandard member functions take no arguments. This is because these member functions implicitly know that they are to print the data members of the particular Time object for which they are invoked. This can make member function calls more concise than conventional function calls in procedural programming.
Software Engineering Observation 9.4
Using an object-oriented programming approach can often simplify function calls by reducing the number of parameters to be passed. This benefit of object-oriented programming derives from the fact that encapsulating data members and member functions within an object gives the member functions the right to access the data members. |
Software Engineering Observation 9.5
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 at least have fewer arguments than typical function calls in non-object-oriented languages. Thus, the calls are shorter, the function definitions are shorter and the function prototypes are shorter. This facilitates many aspects of program development. |
Error-Prevention Tip 9.3
The fact that member function calls generally take either no arguments or substantially 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. |
Using Class Time
Once class Time has been defined, it can be used as a type in object, array, pointer and reference declarations as follows:
Time sunset; // object of type Time Time arrayOfTimes[ 5 ], // array of 5 Time objects Time &dinnerTime = sunset; // reference to a Time object Time *timePtr = &dinnerTime, // pointer to a Time object
Figure 9.3 uses class Time. Line 12 instantiates a single object of class Time called t. When the object is instantiated, the Time constructor is called to initialize each private data member to 0. Then, lines 16 and 18 print the time in universal and standard formats to confirm that the members were initialized properly. Line 20 sets a new time by calling member function setTime, and lines 24 and 26 print the time again in both formats. Line 28 attempts to use setTime to set the data members to invalid valuesfunction setTime recognizes this and sets the invalid values to 0 to maintain the object in a consistent state. Finally, lines 33 and 35 print the time again in both formats.
Figure 9.3. Program to test class Time.
(This item is displayed on page 488 in the print version)
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 5 using std::cout; 6 using std::endl; 7 8 #include "Time.h" // include definition of class Time from Time.h 9 10 int main() 11 { 12 Time t; // instantiate object t of class Time 13 14 // output Time object t's initial values 15 cout << "The initial universal time is "; 16 t.printUniversal(); // 00:00:00 17 cout << " The initial standard time is "; 18 t.printStandard(); // 12:00:00 AM 19 20 t.setTime( 13, 27, 6 ); // change time 21 22 // output Time object t's new values 23 cout << " Universal time after setTime is "; 24 t.printUniversal(); // 13:27:06 25 cout << " Standard time after setTime is "; 26 t.printStandard(); // 1:27:06 PM 27 28 t.setTime( 99, 99, 99 ); // attempt invalid settings 29 30 // output t's values after specifying invalid values 31 cout << " After attempting invalid settings:" 32 << " Universal time: "; 33 t.printUniversal(); // 00:00:00 34 cout << " Standard time: "; 35 t.printStandard(); // 12:00:00 AM 36 cout << endl; 37 return 0; 38 } // end main
|
Looking Ahead to Composition and Inheritance
Often, classes do not have to be created "from scratch." Rather, they can include objects of other classes as members or they may be derived from other classes that provide attributes and behaviors the new classes can use. Such software reuse can greatly enhance programmer productivity and simplify code maintenance. Including class objects as members of other classes is called composition (or aggregation) and is discussed in Chapter 10. Deriving new classes from existing classes is called inheritance and is discussed in Chapter 12.
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 truethe programmer may think of objects as containing data and functions (and our discussion has certainly encouraged this view); physically, however, this is not true.
Performance Tip 9.2
Objects contain only data, so objects are much smaller than if they also contained member functions. Applying operator sizeof to a class name or to an object of that class will report only the size of the class's data members. 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 nonmodifiable (also called reentrant code or pure procedure) and, hence, can be shared among all objects of one class. |
Introduction to Computers, the Internet and World Wide Web
Introduction to C++ Programming
Introduction to Classes and Objects
Control Statements: Part 1
Control Statements: Part 2
Functions and an Introduction to Recursion
Arrays and Vectors
Pointers and Pointer-Based Strings
Classes: A Deeper Look, Part 1
Classes: A Deeper Look, Part 2
Operator Overloading; String and Array Objects
Object-Oriented Programming: Inheritance
Object-Oriented Programming: Polymorphism
Templates
Stream Input/Output
Exception Handling
File Processing
Class string and String Stream Processing
Web Programming
Searching and Sorting
Data Structures
Bits, Characters, C-Strings and structs
Standard Template Library (STL)
Other Topics
Appendix A. Operator Precedence and Associativity Chart
Appendix B. ASCII Character Set
Appendix C. Fundamental Types
Appendix D. Number Systems
Appendix E. C Legacy Code Topics
Appendix F. Preprocessor
Appendix G. ATM Case Study Code
Appendix H. UML 2: Additional Diagram Types
Appendix I. C++ Internet and Web Resources
Appendix J. Introduction to XHTML
Appendix K. XHTML Special Characters
Appendix L. Using the Visual Studio .NET Debugger
Appendix M. Using the GNU C++ Debugger
Bibliography