6.12.1. The Time Structure Example (2)

struct, Time structure (2), function testing, testing, overloaded functions, function overloading, functions, pass by pointer, pass by reference, multi-file program
Time: 00:02:21 | Download: Large, Large (CC), Small | Streaming Streaming (CC) | Slides: PDF, PPTX

The second version of the Time example begins with the Chapter 5 Time example, extending it by adding pass-by-reference and pass-by-pointer. Specifically, it makes the following changes:

  1. Modify the "print" function so that it performs a pass-by-reference
  2. Add a read function that:
    1. Has a return type of void
    2. Has one Time argument implemented as pass-by-pointer
    3. Prompts for and reads (three separate prompts and three separate reads) in data to fill the three Time fields in this order: hours, minutes, and seconds

The modified program's organization still has one header and two source code files. Modifying the print function (requirement 1) and adding the read function (requirement 2) involves editing both "Time" files (Time.h and Time.cpp). Testing the changes requires editing the driver. The three-file organization and function patterns make it easy to convert the example from a structure to a class with member functions in a subsequent chapter.

Overloaded functions must have unique argument lists. While a program can overload functions that are distinguished by pass-by-reference and pass-by-pointer, there is no practical value in including both in a "real" program, since both mechanisms implement INOUT passing. The example presents both to illustrate their different syntaxes. Conversely, programs can't overload functions solely on pass-by-value versus pass-by-reference because the function calls for both are identical.
struct, specification, prototype, Time structure (2)

Header files typically contain declarations, implying a program may include them many times without causing conflicts or programming errors. A structure (or class) specification qualifies as a declaration because it doesn't create objects or allocate memory. Similarly, function prototypes qualify as declarations because they don't have bodies for the compiler to translate to machine code and they don't use memory.

struct Time
{
	int	hours;
	int	minutes;
	int	seconds;
};

Time make_time(int h, int m, int s);
Time make_time(int s);
Time add(Time t1, Time t2);
//void print(Time t);			// (a) pass-by-value (or pass-by-copy)
void print(Time& t);			// (b) pass-by-reference
void read(Time* t);			// (c) pass-by-pointer

// alternate versions
void print(Time* t);			// (d) pass-by-pointer
void read(Time& t);			// (e) pass-by-reference
Updated version of the Time structure. The updated header removes the strikethrough and adds the underlined code.
  1. The pass-by-value version must be removed because it conflicts with the pass-by-reference version.
  2. The example modifies the print function to use pass-by-reference, which is (slightly) more efficient than pass-by-value.
  3. The read function must use an INOUT passing mechanism, either pass-by-pointer or pass-by-reference, because it returns data through its argument. This version demonstrates pass-by-pointer.
  4. An alternate implementation of print based on pass-by-pointer.
  5. An alternate implementation of read based on pass-by-reference.

make_time: Time Construction Functions

Time structure (2), make_time
#include <iostream>
#include <iomanip>
#include "Time.h"
using namespace std;

Time make_time(int h, int m, int s)				// (a)
{
	Time	temp;

	temp.hours = h;
	temp.minutes = m;
	temp.seconds = s;

	return temp;
}


Time make_time(int s)						// (b)
{
	Time	temp;

	temp.hours = s / 3600;
	s %= 3600;		// shortcut for s = s % 3600;
	temp.minutes = s / 60;
	temp.seconds = s % 60;

	return temp;
}
Building Time objects with the "make" functions. Two overloaded make_time functions build and return Time objects. The first function builds the objects from three discrete time elements, while the second must "tease apart" a number of seconds to obtain the same three elements. Object-oriented programs (Chapter 9) build objects with class member functions called constructors, and this example deliberately patterns the "make" functions after them to facilitate converting the structure to a class.
  1. The first overloaded function creates an instance of the Time structure and initializes its three fields with the past-in hours, minutes, and seconds.
  2. The second function also creates an instance of the Time structure but must programmatically decompose the input seconds to extract the hours, minutes, and seconds. The "60" appearing in the body of the function is from the smallest unit of conversion: 60 seconds/minute; the "3600" is the number of seconds in an hour (60 seconds/minutes * 60 minutes/hour).

add: Time Process Function

Time structure (2), add
Time add(Time t1, Time t2)
{
	int	i1 = t1.hours * 3600 + t1.minutes * 60 + t1.seconds;
	int	i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds;

	return make_time(i1 + i2);
}
Adding two Time objects. The object-oriented paradigm calls functions operating on objects process functions. The example adopts the object-oriented terminology to assist in segueing later from structures to classes. The function adds two instances of the Time structure. Both instances are converted to an equivalent amount of time, expressed in seconds only, to facilitate addition. "3600" is again the number of seconds in an hour, and "60" is the number of seconds in a minute. The add function ends by calling the second make_time function to convert the sum of the times in seconds into a new Time structure.

read and print: Time I/O Functions

Time structure (2), read, print
//void print(Time t)						// (a)
void print(Time& t)						// (b)
{
	cout.fill('0');
	cout << t.hours << ":" << setw(2) << t.minutes << ":" <<
		setw(2) << t.seconds << endl;
	cout.fill(' ');
}


void read(Time* t)						// (c)
{
	cout << "Please enter the hours: ";
	cin >> t->hours;

	cout << "Please enter the minutes: ";
	cin >> t->minutes;

	cout << "Please enter the seconds: ";
	cin >> t->seconds;
}


// Alternate implementations

void print(Time* t)						// (d)
{
	cout.fill('0');
	cout << t->hours << ":" << setw(2) << t->minutes << ":" << setw(2) << t->seconds << endl;
	cout.fill(' ');
}

void read(Time& t)						// (e)
{
	cout << "Please enter the hours: ";
	cin >> t.hours;
	cout << "Please enter the minutes: ";
	cin >> t.minutes;
	cout << "Please enter the seconds: ";
	cin >> t.seconds;
}
Updated versions of the Time I/O functions. The updated functions remove the strikethrough and add the underlined code.
  1. Discard the pass-by-value version from the previous chapter.
  2. Replace pass-by-value with pass-by-reference. Together, fill and setw make the hours and minutes display as two digits; the leading digit is 0 if the values are less than 10. The second fill resets the fill or padding character a space.
  3. The example implements the read function as pass-by-pointer, an INOUT mechanism. The arrow operator selects the fields, as the variable "t" is a pointer.
  4. An alternate pointer version of print.
  5. An alternate reference version of read.

Testing Time: driver.cpp

Time structure (2), driver, function testing, testing

In a bottom-up implementation, programmers use "drivers" to test functions. They frequently "hard code" argument values in the calls because the drivers don't solve "real" or application problems - they are just a sequence of test calls. Programmers often follow a cyclic development process of implementing and testing a function or a small set of functions. Cyclic development has at least two advantages:

  1. It's easier to localize, identify, and correct errors when testing is restricted to a small amount of code.
  2. One function may depend on another. Validating and correcting the independent functions first increases the likelihood that subsequent errors are in the dependent function, cyclicly applying advantage 1.

Following a cyclic process has advantages, but a functional "critical mass" is necessary for any testing. Specifically, the driver must build, populate (i.e., fill with data), and display objects. When a driver can perform these basic tasks, it can begin testing the (typically) more complex process functions.

#include <iostream>
#include "Time.h"
using namespace std;

int main()
{
	Time t = make_time(3666);
	Time t;
	read(&t);
	print(t);

	Time s = make_time(1, 30, 4);
	Time s;
	read(&s);
	print(s);

	Time u = add(t, s);		// adds two Time structs, stores the sum in u
	print(u);			// prints the sum

	return 0;
}
Testing the Time structure and functions with a driver. Driver programs "solve" the artificial "problem" of testing functions, so there are no specific requirements for how they name variables or call functions. The previous Time example tested the overloaded "make" functions with the illustrated "hard-coded" data, so this example skips them and only tests the read, print, and add functions.

Downloadable Time Code

Time structure (2), multi-file program
ViewDownloadComments
Time.h Time.h The Time structure specification and function prototypes
Time.cpp Time.cpp The Time function definitions
driver.cpp driver.cpp A driver testing the Time functions