Chuck Allison is a software architect for the Family History Department of Jesus Christ of Latter Day Saints Church Headquarters in Salt Lake City. He has a B.S. and M.S. in mathematics, has been programming since 1975, and has been teaching and developing in C since 1984. His current interest object-oriented technology and education. He is a member of X3J16, the ANSI C++ Standards Committee. Chuck can be reached on the Internet at allison@decus.org, or at (801) 240-4510.
In last month's capsule I presented the beginnings of a simple date class. In addition to providing a member function to calculate the interval between two dates, this class illustrated the following features of C++:
inline functions
references
constructors
controlled access to private data members In this month's installment I will add relational operators, input/output operations, and the capability to get the current date, while demonstrating these features:
operator overloading
streams
friend functions
static members When using dates you often need to determine if one precedes another. I will add the member function
int compare(const Date& d2) const;to the date class (see Listing 1) .Date::compare behaves like strcmp it returns a negative integer if the current object (*this) precedes d2, 0 if both represent the same date, and a positive integer otherwise (see Listing 2 for the implementation and Listing 3 for a sample program). For those of you familiar with qsort from the Standard C library, you can use Date::compare to sort dates just like you use strcmp to sort strings. Here is a compare function to pass to qsort:
#include "date.h" int datecmp(const void *p1, const void *p2) { const Date *d1p = (const Date *) p1, *d2p = (const Date *) p2; return d1p->compare(*d2p); }(Next month's CODE CAPSULE will cover qsort).
Operator Overloading
Most of the time, it is more convenient to have relational operators, for example
if (d1 < d2) // do something appropriate..Adding a less-than operator is trivial using Date::compare just insert the following in-line member function into the class definition:
int operator<(const Date& d2) const {return compare(d2) < 0);The compiler translates each occurrence of an expression such as
d1 < d2into the function call
d1.operator<(d2)Listing 4 has the class definition with all six relational operators and the updated sample program is in Listing 5.Since the functionality of Date::interval is like a subtraction (it gives us the difference between two dates), it would seem natural to rename it as Date::operator-. Before doing this, take a closer look at the semantics of the statement
a = b - c;No matter the type of the variables, the following should hold:a is a distinct object created by the subtraction, and b - c == - (c - b)
I will use the convention that a "positive" Date object has all positive data members, and conversely for a "negative" date (mixed signs are not allowed). In Listing 7, I have replaced Date::interval with Date::operator- (const Date&), which affixes the proper signs to the data members and returns a newly-constructed Date object. The new class definition in Listing 6 also contains a unary minus operator, which also has the name Date::operator-, but takes no arguments. The compiler will transform the statements
d1 - d2; -d1;respectively into
d1.operator-(d2); // Calls Date::operator-(const Date&) d1.operator-(); // Calls Date::operator-()A sample program using these new member functions is in Listing 8.
Stream I/O
One thing remains before I can say that a Date object has the look and feel of a built-in type input/output support. C++ supplies stream objects that handle I/O for standard types. For example, the output from the program
#include <iostream.h> main() { int i; cout << "Enter an integer: "; cin >> i; cout << "You typed " << i << endl; return 0; }will be something like
Enter an integer: 5 You typed 5cout is an output stream (class ostreom) supplied by the C++ streams library and cin is an input stream (class istream), which are associated by default with standard output and standard input, respectively. When the compiler sees the expression
cout << "Enter an integer: "it replaces it with
cout.operator<<("Enter an integer: ")which calls the member function ostream::operator<<(const char *). Likewise, the expression
cout << iwhere i is an integer calls the function
ostream::-operator<<(int).endl is a special stream directive (called a manipulator) which outputs a newline and flushes the output buffer. Output items can be chained together:
cout << "You typed " << ibecause ostream::operator<< returns a reference to the stream itself. The above statement becomes
(cout.operator<<("You typed ")).operator<<(i)To accommodate output of Date objects, then, you will need a global function that sends the printed representation to a given output stream, and then returns a reference to that stream:
ostream& operator<<(ostream& os, const Date& d) { os << d.get_month() << '/' << d.get_day() << '/' << d.get_year(); return os; }This of course can't be a member function, since the stream (not the object being output) always appears to the left of the stream insertion operator.
Friends
For efficiency, it is customary to give operator<< access to the private data members of an object. (Most implementations of a class provide the associated I/O operators too, so it seems safe to break the encapsulation boundary in this case.) To bypass the restriction on access to private members, you need to declare operator<< to be a friend of the Date class by adding the following statement to the class definition:
friend ostream& operator<<(ostream&, const Date&);This declaration appears in the new class definition in Listing 9, along with the corresponding friend declaration for the input function, operator>>. The implementation for these functions is in Listing 10, and Listing 11 has a sample program.
Static Members
A class in C++ defines a scope. This is why the function Date::compare would not conflict with a global function named compare (even if the arguments and return value were of the same type). Now consider the array dtab[] in the implementation file. dtab's static storage class makes it private to the file, but it really belongs to the class, not to the file. If I chose to spread the Date member functions across multiple files, I would be forced to group those that needed access to dtab together in the same file.
A better solution is to make dtab a static member of the Date class. Static members belong to the whole class, not to a single object. This means that only one copy of dtab exists, and is shared by all objects of the class. Making the function isleap static allows you to call it without an associated object, i.e., you just need to say
isleap(y);instead of
d.isleap(y);To make isleap available to any caller, place it in the public section of the class definition, and call it like this:
Date::isleap(y);As a finishing touch, I will redefine the default constructor to initialize its object with the current date. The final class definition, implementation and sample program are in Listing 12 - Listing 14 respectively.
Summary
In these last two installments I've tried to illustrate how C++ supports data abstraction the creation of user-defined types. Constructors cause objects to be initialized automatically when you declare them. You can protect class members from inadvertent access by making them private. Overloading common operators makes your objects look and feel like built-in types a plus for readability and maintenance.
Listing 1 Introduces a function to compare dates
// date4.h class Date { int month; int day; int year; public: // Constructors Date() {month = day = year= 0;} Date(int m, int d, int y) {month = m; day = d; year = y;} // Accessor Functions int get_month() const {return month;} int get_day() const {return day;} int get_year() const {return year;} Date * interval(const Date&) const; int compare(const Date&) const; }; // End of File
Listing 2 Implements the interval and compare member functions
// date4.cpp #include "date4.h" inline int isleap(int y) {return y%4 == 0 && y%100 != 0 || y%400 == 0;} static int dtab[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }; Date * Date::interval(const Date& d2) const { static Date result; int months, days, years, prev_month; // Compute the interval - assume d1 precedes d2 years = d2.year - year; months = d2.month - month; days = d2.day - day; // Do obvious corrections (days before months!) // // This is a loop in case the previous month is // February, and days < -28. prev_month = d2.month - 1; while (days < 0) { // Borrow from the previous month if (prev_month == 0) prev_month = 12; -months; days += dtab[isleap(d2.year)][prev_month-]; } if (months < 0) { // Borrow from the previous year -years; months += 12; } // Prepare output result.month = months; result.day = days; result.year = years; return &result; } int Date::compare(const Date& d2) const { int months, days, years, order; years = year - d2.year; months = month - d2.month; days = day - d2.day; // return <0, 0, or >0, like strcmp() if (years == 0 && months == 0 && days == 0) return 0; else if (years == 0 && months == 0) return days; else if (years == 0) return months; else return years; } // End of File
Listing 3 Tests the compare member function
// tdate4.cpp #include <stdio.h> #include "date4.h" void compare_dates(const Date& d1, const Date& d2) { int compval = d1.compare(d2); char *compstr - (compval < 0) ? "precedes" : ((compval > 0) ? "follows" : "equals"}; printf("%d/%d/%d %s %d/%d/%d\n", d1.get_month(),d1.get_day(0),d1.get_year(), compstr, d2.get_month(),d2.get_day(),d2.get_year()); } main() { Date d1(1,1,1970); compare dates(d1,Date(10,1,1951)); compare_dates{d1,Date(1,1,1970)); compare_dates(d1,Date(12,31,1992)); return 0; } /* OUTPUT 1/1/1970 follows 10/1/1951 1/1/1970 equals 1/1/1970 1/1/1970 precedes 12/31/1992 */ // End of File
Listing 4 Defines relational operators for the Date class
// date5.h class Date { int month; int day; int year; public: // Constructors Date() {month = day = year = 0;} Date(int m, int d, int y) {month = m; day = d; year = y;} // Accessor Functions int get_month() const {return month;} int get_day() const {return day;} int get_year() const {return year;} Date * interval(const Date&) const; int compare(const Date&) const; // Relational operators int operator<(const Date& d2) const {return compare(d2) < 0;} int operator<=(const Date& d2) const {return compare(d2) <= 0;} int operator>(const Date& d2) const {return compare(d2) > 0;} int operator>=(const Date& d2) const {return compare(d2) >= 0;} int operator!=(const Date& d2) const {return compare(d2) != 0;} int operator!=(const Date& d2) const {return compare(d2) !=0;} }; // End of File
Listing 5 Uses the Date relational operators
// tdate5.cpp #include <stdio.h> #include <stdlib.h> #include "date5.h" void compare_dates(const Date& d1, const Date& d2) { char *compstr = (d1 < d2) ? "precedes" : ((d1 > d2) ? "follows" : "equals"); printf("%d/%d/%d %s %d/%d/%d\n", d1.get_month(),d1.get_day(),d1.get_year(), compstr, d2.get_month(),d2.get_day(),d2.get_year()); } main() { Date d1(1,1,1970); compare_dates(d1,Date(10,1,1951)); compare_dates(d1,Date(1,1,1970)); compare_dates(d1,Date(12,31,1992)); return 0; } /* OUTPUT 1/1/1970 follows 10/1/1951 1/1/1970 equals 1/1/1970 1/1/1970 precedes 12/31/1992 */ // End of File
Listing 6 Adds binary and unary minus to the Date class
// date6.h class Date { int month; int day; int year; public: // Constructors Date() {month = day = year = 0;} Date(int m, int d, int y) {month = m; day = d; year = y;} // Accessor Functions int get_month() const {return month;} int get_day() const {return day;} int get_year() const {return year;} Date operator-(const Date& d2) const; Date& operator-() {month = -month; day = -day; year = -year; return *this;} int compare(const Date&) const; // Relational operators int operator<(const Date& d2) const {return compare(d2) < 0;} int operator<=(const Date& d2) const {return compare(d2) <= 0;} int operator>(const Date& d2) const {return compare(d2) > 0;} int operator>=(const Date& d2) const {return compare(d2) >= 0;} int operator==(const Date& d2) const {return compare(d2) == 0;} int operator!=(const Date& d2) const {return compare(d2) != 0;} }; // End of File
Listing 7 Implements the binary minus operator
// date6.cpp #include <assert.h> #include "date6.h" inline int isleap(int y) {return y%4 == 0 && y%100 != 0 || y%400 == 0;} static int dtab[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }; Date Date::operator-(const Date& d2) const { int months, days, years, prev_month, order; const Date * first, * last; // Must know which date is first if (compare(d2) <= 0) { // this <= d2 order = -1; first = this; last = &d2; } else { order = 1; first = &d2; last = this; } // Compute the interval; first <= last years = last->year - first->year; months = last->month - first->month; days = last->day - first->day; assert(years >= 0 && months >= 0 && days >= 0); // Do obvious corrections (days before months!) // This is a loop in case the previous month is // February, and days < -28. prev_month = last->month - 1; while (days < 0) { // Borrow from the previous month if (prev_month == 0) prev_month = 12; --months; days += dtab[isleap(last->year)][prey_month--]; } if {months < 0) { // Borrow from the previous year --years; months += 12; } // Return a date object with the interval if (order == -1) return Date(-months,-days,-years); else return Date(months, days, years); } int Date::compare(const Date& d2) const { // same as in Listing 2 } // End of File
Listing 8 Subtracts two dates
// tdate6.cpp: #include <stdio.h> #include "date6.h" main() { Date d1(1,1,1970), d2(12,8,1992); Date result = d1 - d2; printf("years: %d, months: %d, days: %d\n", result.get_year(), result.get_month(), result.get_day()); result = d2 - d1; printf("years: %d, months: %d, days: %d\n", result.get_year(), result.get_month(), result.get_day()); int test = d1 - d2 == -(d2 - d1); printf("d1 - d2 == -(d2 - d1)? %s\n", test ? "yes" : "no"); return 0; } /* OUTPUT years: -22, months: -11, days: -7 years: 22, months: 11, days: 7 d1 - d2 == -(d2 - d1)? yes */ // End of File
Listing 9 Adds stream I/O to the Date class
// date7.h class ostream; class Date { int month; int day; int year; public: // Constructors Date() {month = day = year = 0;} Date(int m, int d, int y) {month = m; day = d; year = y;} // Accessor Functions int get_month() const {return month;} int get_day() const {return day;} int get_year() const {return year;} Date operator-(const Date& d2) const; Date& operator-() {month= -month; day = -day; year = -year; return *this;} int compare(const Date&) const; // Relational operators int operator<(const Date& d2) const {return compare(d2) < 0;} int operator<=(const Date& d2) const {return compare(d2) <= 0;) int operator>(const Date& d2) const {return compare(d2) > 0;} int operator>=(const Date& d2) const {return compare(d2) >= 0;} int operator==(const Date& d2) const {return compare(d2) == 0;} int operator!=(const Date& d2) const {return compare(d2) != 0) // I/O operators friend ostream& operator<<(ostream&, const Date&); friend istream& operator>>(istream&, Date&); }; // End of File
Listing 10 Implements Date stream I/O functions
#include <iostream.h> #include "date7.h" ostream& operator<<(ostream& os, const Date& d) { os << d.month << '/' << d.day << '/' << d.year; return os; } istream& operator>>(istream& is, Date& d) { char slash; is >> d.month >> slash >> d.day >> slash >> d.year; return is; } // End of File
Listing 11 Illustrates stream I/O of Date objects
// tdate7.cpp: #include <iostream.h> #include "date7.h" main() { Date d1, d2; cout << "Enter a date: "; cin >> d1; cout << "Enter another date: "; cin >> d2; cout << "d1 - d2 = "<< d1 - d2 << endl; cout << "d2 - d1 = "<< d2 - d1 << endl; return 0; } /* OUTPUT Enter a date: 10/1/1951 Enter another date: 5/1/1954 d1 - d2 = -7/0/-2 d2 - d1 = 7/0/2 */ // End of File
Listing 12 Defines static members
// date8.h // Forward declarations class istream; class ostream; class Date { int month; int day; int year; static int dtab[2][13]; public: // Constructors Date(); // Get today's date (see .cpp file) Date(int m, int d, int y) {month = m; day = d; year = y;} // Accessor Functions int get_month() const {return month;} int get_day() const {return day;} int get_year() const {return year;} Date operator-(const Date& d2) const; Date& operator-() {month = -month; day = -day; year = -year; return *this;} int compare(const Date&) const; // Relational operators int operator<(const Date& d2) const {return compare{d2) < 0;} int operator<=(const Date& d2) const {return compare(d2) <= 0;} int operator>(const Date& d2) const {return compare(d2) > 0;} int operator>=(const Date& d2) const {return compare(d2) >= 0;} int operator==(const Date& d2) const {return compare(d2) == 0;} int operator!=(const Date& d2) const {return compare(d2) != 0;} // Stream I/O operators friend ostream& operator<<(ostream&, const Date&); friend istream& operator>>(istream&, Date&); static int isleap(int y) {return y%4 == 0 && y%100 != 0 || y%400 == 0;} }; // End of File
Listing 13 Final implementation of the Date class
// date8.cpp #include <iostream.h> #include <time.h> #include <assert.h> #include "date8.h" // Must initialize statics outside the class definition int Date::dtab[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }; Date Date::operator-(const Date& d2) const { int months, days, years, prev_month, order; const Date * first, * last; // Must know which date is first if (compare(d2) <= 0) { // this <= d2 order= -1; first = this; last = &d2; } else { order = 1; first = &d2; last = this; } // Compute the interval; first <= last years = last->year - first->year; months = last->month - first->month; days = last->day - first->day; assert(years >= 0 && months >= 0 && days >= 0); // Do obvious corrections (days before months!) // // This is a loop in case the previous month is // February, and days < -28. prev_month = last->month - 1; while (days < 0) { // Borrow from the previous month if (prev_month == 0) prev_month = 12; -months; days += dtab[isleap(last->year)][prev_month-]; } if (months < 0) { // Borrow from the previous year -years; months += 12; } // Return a date object with the interval if (order == 1) return Date(-months,-days,-years); else return Date(months,days,years); } int Date::compare(const Date& d2) const { int months, days, years, order; years = year - d2.year; months = month - d2.month; days = day - d2.day; // return <0, 0, or >0, like strcmp() if (years == 0 && months == 0 && days == 0) return 0; else if (years == 0 && months == 0) return days; else if (years == 0) return months; else return years; } ostream& operator<<(ostream& os, const Date& d) { os << d.month << '/' << d.day << '/' << d.year; return os; } istream& operator>>(istream& is, Date& d) { char slash; is >> d.month >> slash >> d.day >> slash >> d.year; return is; } Date::Date() ( // Get today's date time_t tval = time(0); struct tm *tmp= localtime(&tval); month = tmp->tm_mon+1; day = tmp->tm_mday; year = tmp->tm_year + 1900; } // End of File
Listing 14 Gets today's date
// tdate8.cpp: #include <iostream.h> #include "date8.h" main() { Date today, d2; cout << "Today's date is "<< today << endl; cout << "Enter another date: "; cin >> d2; cout << "today - d2 = "<< today - d2 << endl; cout << "d2 - today = "<< d2 - today << endl; return 0; } /* OUTPUT Today's date is 12/12/1992 Enter another date: 1/1/1970 today - d2 = 11/11/22 d2 - today = -11/-11/-22 */ // End of File