6.12.1. The Time Structure Example (2)

Time: 00:06:20 | Download: Large Small | Streaming
Review

Multi-file programs consist of a combination of header (.h) and source code (.cpp) files. Header files are always #included in source code files, but #including one source code file into another is very rare. The linker (or loader) assembles the final program or executable from the machine code generated from the individual source code files. Please review:

This version of the Time example begins with and extends the Time example started in the previous chapter. It builds on passing arguments to functions. Specifically, the updated example illustrates pass-by-reference and pass-by-pointer by making the following changes to Time:

  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. Adding a read function (requirement 2) and modifying the print function (requirement 1) involves editing the two "Time" files. Testing the changes requires editing the driver. The three-file organization and function patterns make it easy to convert the example to a class with member functions in a subsequent chapter.

Recall that overload functions must each have a unique argument list. While a program can have functions overloaded as pass-by-reference and pass-by-pointer, there is no practical value to including both in a "real" program because both implement INOUT passing. The example has both to illustrate their different syntaxes. Conversely, programs can't define functions overloaded as pass-by-value and pass-by-reference because the function calls for both are identical.

Header files typically contain declarations, implying they can be included in a program many times without causing conflicts or programming errors. A structure 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 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 Time.h. The updated header removes the strikeout code and adds the underlined statements.
  1. Pass-by-value must be removed because it conflicts with the pass-by-reference
  2. print modified to use pass-by-reference, which is more efficient than pass-by-value
  3. The program must implement the read function with an INOUT parameter passing mechanism: either pass-by-pointer, used here, or pass-by-reference
  4. An alternate implementation of print based on pass-by-pointer
  5. An alternate implementation of read based on pass-by-reference

make_time: The Construction Functions

#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. Object-oriented programs (Chapter 9) build objects with class member functions called constructors. This example patterns the structure "make" functions after constructors to facilitate converting the structure to a class.
  1. An overloaded function that creates an instance of the Time structure and initializes the three fields with the parameter values
  2. Another overloaded function creates an instance of the Time structure. It initializes the three structure fields with values derived from the parameter. Note that 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: Process Function

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 from structures to classes. The function adds two instances of the Time structure. Both instances are converted to an equivalent amount of time in seconds, facilitating the addition operation. "3600" is again the number of seconds in an hour, and "60" is the number of seconds in a minute. The add functions end by calling the second make_time function to convert the sum of the seconds into a new Time structure.

read and print: Time I/O Functions

//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 version of Time.cpp. The updated functions remove the strikeout code and add the underlined statements.
  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
  3. The example implements the read function as pass-by-pointer, an INOUT mechanism. The arrow operator is used to select the fields because the variable "t" is a pointer
  4. An alternate pointer version of print
  5. An alternate reference version of read

Testing Time: driver.cpp

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 restricted to a small code region.
  2. One function may depend on another. Correcting the independent function before implementing a dependent function reduces the need to correct or update dependent functions.

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. The purpose of a driver is simply testing the functions, so there are no specific requirements for how it names variables or calls functions - as long as all variables are defined and initialized before being displayed or used in more complex operations. The previous Time example tested the overloaded "make" functions with the illustrated "hard coded" data, so this example skips them. This driver tests the read, print, and add functions.

Downloadable Time Code