Instructor’s Resource Manual for Savitch Absolute C++ 01/02/02 Page 14
Chapter 4 Parameters and Overloading
Chapter 4
Parameters and Overloading
0. Introduction
This chapter treats the call-by-value and call-by-reference parameter passing mechanisms in function calls, function name overloading, default arguments and basic testing techniques.
1. Outline of topics in the chapter
4.1 Parameters
Call-by-Value Parameters
A First Look at Call-by-Reference Parameters
Call-by-Reference Mechanism in Detail
Constant Reference Parameters
Mixed Parameter Lists
4.2 Overloading and Default Arguments
Introduction to Overloading
Rules for Resolving Overloading
Default Arguments
4.3 Testing and Debugging Functions
The assert Macro
Stubs and Drivers.
2. General remarks on the chapter
Call-by-Value Parameters
The default parameter passing mechanism in C++ is call-by-value. In Chapter 2 the text points out that a call-by-value parameter is just a local variable that has a special mechanism for initialization. The call-by-value parameter is defined in the outermost block of the function, and the value of the caller’s argument is copied to the parameter to initialize it. This defines the “plugging-in” mechanism the text uses to describe call-by-value. This also explains why defining a local variable with the same name as a parameter is an error. You have two local variables defined in the same scope.
Call-by-Reference Parameters
Recall that value parameters have the value of the argument 'plugged in' for the formal parameter. We point out the behavior is exactly that of a local variable that has its initial value provided by the value of the actual argument. Here we are interested in a different mechanism. With call-by-reference, the formal parameter itself automagically[1] becomes the actual parameter. The mechanism is that the types are checked. Next, the address of the actual parameter is copied into the places where the formal parameters (i.e. parameters) would need to be assigned or have their values fetched. When a value needs to be fetched, the value that is stored at the address of the caller's argument is fetched. When an assignment needs to be made, the value to be assigned is stored into the memory at the address of the caller's argument. (See the discussion in the text in the section “Call-by-Reference Mechanism in Detail” on page 139 and following.) I find that even with fairly weak students a discussion such as the summary presented here along with the text's discussion results in a good understanding of these concepts.
Notice that the syntactic distinction between call-by-reference and call-by-value is the ampersand sign, &, between the type and the identifier name in the argument list. This distinction allows us to mix call-by-value and call-by-reference mechanisms.
Note that there is no C++ reason for placing the ampersand adjacent to the type, but this is typically C++ programming style, found in most C++ texts and treatises. It is important to note that the ampersand for a reference parameter must be put both into the declaration (prototype) and into the definition of a function using pass-by-reference. One further warning: overloading based on a distinction between a reference parameter and a value parameter is not supported.
Using Procedural Abstraction
My students do not want to supply pre and post conditions for functions they write. I assert that without specifications, it is impossible to determine whether the code is correct in any sense other than that it compiles without error and runs without error for some data.
No notion of correctness of a solution to a problem is possible without knowing a function's specifications, which are expressed in the preconditions and postconditions. Without preconditions, the user cannot determine that the input data is correct. Without postconditions, the user cannot determine whether the output is correct for any data, regardless of whether the data is correct.
Correctness may be defined as follows.
A program is correct if it
1) executes with no abnormal termination of the program for data that meets the preconditions on the input, that is, the program should run to a normal successful completion for correct data, and
2) generates correct output given correct input. Input that meets the preconditions is correct. Output that meets the post conditions is correct.
3) behaves in a reasonable way in response to incorrect data. This may mean summary termination in the face of error, or detecting errors then giving the user another chance, or behaving in some other fail-safe way. (Consider an elevator control, perhaps?)
The pre- and post- conditions should be machine testable. Accomplishing this can be difficult in some instances. This is a goal to be sought in writing pre- and post- conditions. Failure to meet this goal means increased difficulty in demonstrating that the program is correct.
Overloading functions
After looking at the cmath header and the cstdlib header for C++, we find three different versions of the absolute value function:
double fabs(double);
int abs(int); and
long int labs(long int);
One could conclude that overloading function names was not important to the library authors, but this would be an error. When C++ was being developed, there was a significant body of code in the C libraries. These libraries could be taken over by C++ with little work. Reimplementing these libraries would be labor intensive. As a consequence of this trade-off, in the beginning C++ library writers did not take advantage of having the absolute value function abs that makes use of the type of the argument and performs accordingly. (Actually, unchanged C libraries and header files were extensively used by early C++ compilers. Most implementations have replaced these C libraries with equivalent C++ libraries. The C++ Standard requires name changes in these libraries that we mention elsewhere in this IRM.)
It is a good exercise to write overloaded abs functions for several types.
Finally, the several pizza programs are important for two more reasons. First, these problems put function name overloading into a context in which the utility of overloading is evident. Second, they introduce procedural abstraction, or viewing the notion of function as a device for information hiding and modularizing code.
Default Arguments
My approach to default arguments is slightly different from the author’s. A function with default arguments can be thought of as overloaded version of the function. For example, if I write the function
double volume(double len, double width, double height);
and need to call this mostly with height of 3.0, but sometimes with a width of 2.0 and a height of 3.0. I could write keep the original function and write two other overloaded versions of this function, setting the unneeded parameters inside the function.
double volume(double len, double width);
double volume(double len);
As an alternative, I could use default arguments to write one version. Consider this
double volume(double len, double width = 2.0,
double height = 3.0);
Then, when I call the function with one argument, the second and third parameters use the default arguments. If I call the function with two arguments, the arguments given are given to the first two parameters, and the third parameter gets the default argument. What you cannot do is to mix these. If you do, you get name conflicts.
Testing and Debugging Functions
Testing is essential to the process of writing correct code. There are two ways to test code. One follows the top-down design technique, where the first step is to divide the problem into subproblems. The condition on the subproblems is that if each is solved, the original problem is solved. Note that it is desirable to test before doing a lot of programming.
How?
The main program can be written, then tested independently of the subprograms by writing 'stub' subprograms. A stub is a small subprogram that does little more than return fictitious data that the calling function needs to be able to continue. It can be useful for a stub to emit a message that lets the tester know that the subprogram has been encountered. These stub subprograms should simple enough that correctness is not a problem.
This technique allows part of a program to be tested before the rest of it has been tested, or perhaps before the subprograms have been written.
The other technique is bottom up: test the functions independently of the calling program.
How?
Write a driver. A driver is a main program or calling program substitute that exercises the subprogram with data that a correct calling program might present to the subprogram during execution. In fact, incorrect data should also be used to test the robustness of the subprogram. Proper selection of data is essential to the success of this testing technique.
These testing techniques are useful to the extent that the client program author and the subprogram program author use procedural abstraction. This means that the specification of the subprogram is all that is known to the client, and, beyond the specifications, any use to which the subprogram in put is not known to the subprogram author.
Dividing a program into separate pieces, which are tested apart from each other, is a useful technique independently of pre and post- conditions. However, the testing is limited if exact conditions are not known. Clearly, a specification of input, output and action for all functions is essential to the ability to full use of this testing technique.
The text has a boxed section that says, “Every function should be tested in a program in which every other function has been fully tested and debugged.”
This can be implemented using carefully written drivers and stub functions.
3. Solutions to Selected Programming Projects
Detailed solutions to the first 6 projects, are presented here. The rest are essentially the same problems, except for what is being converted. Notes about the remaining problems are included.
One of the more important things in programming is planning, even for the simplest program. If the planning is thorough, the coding will be easy, at lest for these programs. The only errors likely to be encountered are syntax errors. These are usually caused by typing errors, boundary condition problems (frequently off by one errors), or (we hope not) lack of knowledge of the language details.
1. 24 hour time conversion
Task: Convert 24-hour time notation to 12 hour AM/PM notation.
General comments: The student should note that:
a) The convert function has boundary cases that require careful attention.
b) ALL commentary and planning should be done PRIOR to beginning to write the program. Once this is done, the program is almost written. The sooner coding begins, the longer the program will take to do correctly.
c) When testing for equality, as in
if(12 == hours)
put the constant first. The compiler will catch errors such as if (12= hours) which are hard to see otherwise. I made many errors of this type while coding this problem.
//Task: Convert 24-hour time notation to 12 hour AM/PM notation.
//Input: 24 hour time
//Output: corresponding 12-hour time, with AM/PM indication
//Required: 3 functions: input, conversion, and output.
// keep AM/PM information in a char variable
// allow repeat at user's option
//Notes: conversion function will have a char reference
// parameter to return whether the time is AM/PM. Other
// parameters are required.
#include <iostream>
using namespace std;
void input(int& hours24, int& minutes);
//Precondition: input(hours, minutes) is called with
//arguments capable of being assigned.
//Postcondition:
// User is prompted for time in 24-hour format:
// HH:MM, where 0 <= HH < 24, 0 <= MM < 60.
// hours is set to HH, minutes is set to MM.
//KNOWN BUG: NO CHECKING IS DONE ON INPUT FORMAT. Omitting
//the “:” (colon) from the input format “eats” one character
//from the minutes data, and silently gives erroneous
//results.
void convert(int& hours, char& AMPM);
//Precondition: 0 <= hours < 24,
//Postcondition:
// if hours > 12, // Note: definitely in the afternoon
// hours is replaced by hours - 12,
// AMPM is set to 'P'
// else if 12 == hours // boundary afternoon hour
// AMPM is set to 'P', // hours is not changed.
// else if 0 == hours // boundary morning hour
// hours = hours + 12;
// AMPM = 'A';
// else
// (hours < 12)
// AMPM is set to 'A';
// hours is unchanged
void output(int hours, int minutes, char AMPM);
//Precondition:
// 0 < hours <=12, 0 <= minutes < 60,
// AMPM == 'P' or AMPM == 'A'
//Postconditions:
// time is written in the format
// HH:MM AM or HH:MM PM
int main()
{
int hours, minutes;
char AMPM, ans;
do
{
input(hours, minutes);
convert(hours, AMPM);
output(hours, minutes, AMPM);
cout < "Enter Y or y to continue, anything else quits."
< endl;
cin > ans;
} while('Y'== ans || 'y' == ans);
return 0;
}
void input(int& hours24, int& minutes)
{
char colon;
cout < "Enter 24 hour time in the format HH:MM "
< endl;
cin > hours24 > colon > minutes;
}
//Precondition: 0 <= hours < 24,
//Postcondition:
// if hours >= 12,
// hours is replaced by hours - 12,
// AMPM is set to 'P'
// else // (hours < 12)
// hours is unchanged and AMPM is set to 'A'
void convert(int& hours, char& AMPM)
{
if(hours > 12) // definitely in the afternoon
{
hours = hours - 12;
AMPM = 'P';
}
else if (12 == hours) // boundary afternoon hour
AMPM = 'P'; // but hours is not changed.
else if (0 == hours) // boundary morning hour
{
hours = hours + 12;
AMPM = 'A';
}
else // (hours < 12) // definitely morning hour
AMPM = 'A'; // hours is unchanged
}
void output(int hours, int minutes, char AMPM)
{
cout < "Time in 12-hour format: " < endl
< hours < ":" < minutes < " "
< AMPM < 'M' < endl;
}
A typical run follows:
20:33:03:~/AW$ a.out
Enter 24-hour time in the format HH:MM
0:30
Time in 12-hour format:
12:30 AM
Enter Y or y to continue, anything else quits.
y
Enter 24-hour time in the format HH:MM
2:15
Time in 12-hour format:
2:15 AM
Enter Y or y to continue, anything else quits.