Chapter 6: Modular Programming In Chapter 3, we learned the basics of writing functions for a program. We discussed how functions could be used to correspond to the individual steps in a problem solution. We have been using functions with no parameters and no return value. We have also written functions that were passed input parameters and returned a single output, the return value. In this chapter we will expand upon our knowledge and learn how to use functions to create a program system – an arrangement of separate modules that pass information from one to the other. 6.1 Value and Reference Parameters So far, we know how to write functions that return a single result or value. Now, we will learn how functions can be used to return multiple values. Call-by-Value and Call-by-Reference Parameters As we have seen, the method of calling a function and passing values to it is referred to as a function call by value. Calling a function by value is a distinct advantage of C++. It allows functions to be written as independent entities without concern that altering an argument or variable in one function may inadvertently alter the value of a variable in another function. At no time does the called function have direct access to any variable contained in the calling function. There are situations, however, when it is convenient to alter this approach and give a function direct access to variables of its 1 Chapter 6: Modular Programming calling function. To do this requires that both the sending and receiving arguments reference the same storage locations in memory. Once the called function can reference the storage locations of a passed argument it can directly access and change the value stored there. When a reference is passed to a function, it is referred to as a call by reference. Passing and Using References From the sending side calling a function and passing a reference is exactly the same as calling a function and passing a value. In this case, the function needs to be able to declare two arguments that can store references. For example, the function prototype: void newval (float &, float &); and the function definition header: void newval (float &num1, float &num2) and using these function prototype and definition header in a program: Example: Program 6-1 #include <iostream> using namespace std; 2 Chapter 6: Modular Programming int main( ) { float firstnum, secnum; // prototype - accept two references void newval(float &, float &); cout << "Enter two numbers: "; cin >> firstnum >> secnum; cout << "\nThe value in firstnum is: " << firstnum << '\n'; cout << "The value in secnum is: " << secnum << "\n\n"; newval(firstnum, secnum); // call the function cout << "The value in firstnum is now: " << firstnum << '\n'; cout << "The value in secnum is now: " << secnum << '\n'; return (0); } void newval(float & num1, float & num2) { cout << "The value in num1 is: " << num1<< '\n'; cout << "The value in num2 is: " << num2<< "\n\n"; num1= 89.5; num2= 99.5; return; } 3 Chapter 6: Modular Programming Note: The formal arguments num1 and num2 do not store copies of the values in firstnum and secnum, but directly access the locations in memory set aside for these two arguments. The equivalence between actual calling arguments and formal function arguments provides the basis for returning multiple values from within a function. Example: void calc(float num1, float num2, float num3, float &total, float &product) { total = num1 + num2 + num3; product = num1 * num2 * num3; } Program 6-2: #include <iostream> using namespace std; void main(void) { float firstnum, secnum, thirdnum, sum, product; void calc(float, float, float, float &, float &); // prototype cout << "Enter three numbers: "; cin >> firstnum >> secnum >> thirdnum; 4 Chapter 6: Modular Programming // function call calc(firstnum, secnum, thirdnum, sum, product); cout << "\nThe sum of the numbers is: " << sum; cout << "\nThe product of the numbers is: " << product; cout << endl; return; } void calc(float num1, float num2, float num3, float &total, float &product) { total = num1 + num2 + num3; product = num1 * num2 * num3; return; } // Input // Output In-Class Exercise: Write function that accepts a real number as input and returns it whole and fractional parts as output. For example, if the input is 5.32, the function outputs should be the integer 5 and the real value 0.32. Test your function in a program that takes its input from the user. 5 Chapter 6: Modular Programming void Functions Can Return Results Even though we declared the calc( ) as a void function, it can still return results to the calling functions by using output or reference parameters. By declaring this function as type void, we are only telling the compiler that we will not be using the return statement to return a value to the calling function. When to Use a Reference or a Value Parameter How do we decide when to use a reference parameter or a value parameter? 1. If the information passed into a function does not have to be returned or passed out of the function, then the formal parameter used for that information should be a value or an input parameter. 2. If the information needs to be returned to the calling function through a parameter (and not a return statement), then the formal parameter used for that information must be a reference or an output parameter. 3. If the information is to be passed into a function, possibly modified, and a new value returned, then the formal parameter used for that information must be a reference or an input/output parameter (or inout parameter). Note: C++ does not distinguish between output parameters and input/output parameters. Both must be defined as reference parameters in both the prototype and the function header by using the ampersand (&) symbol. 6 Chapter 6: Modular Programming This will cause the address of the corresponding actual argument to be stored in the data area of the called function. It is assumed that the argument used in the function call for an input/output parameter will contain some meaningful data before the function is called and executed. Argument/Parameter List Correspondence Revisited In Section 3.5, we discussed argument/parameter list correspondence. We used the acronym not to summarize the constraints on the number, order and type of input arguments and how they must agree. The rules are repeated below with one addition (as quoted from your book): 1. Number: The number of actual arguments used in a call to function must be the same as the number of formal parameters listed in the function prototype. 2. Order: The order of arguments in the lists determines the correspondence. The first must correspond to the first, the second to the second, and so on. 3. Type: Each actual argument must be of a data type that can be assigned to the corresponding formal parameter without loss of information. 4. For reference parameters, an actual argument must be a variable. For value parameters, an actual argument may be a variable, a constant or an expression. 7 Chapter 6: Modular Programming In-Class Exercise: Write a function that accepts an input argument consisting of two words with a space between them and returns each work through its two output parameters. All three parameters should be type string. Use the following main( ) to call test your function: #include <iostream> #include <string> using namespace std; int main() { char input[50]; // Input array - two words separated by space string inString; string firstWord; // Output string string secondWord; // Output string cout << "Enter two words separated by a space: "; cin.getline(input, sizeof(input), '\n'); inString = input; // Assign array value to string // Insert function call here. cout << "The first word is: " << firstWord << endl << "The second word is: " << secondWord << endl; return (0); 8 Chapter 6: Modular Programming } 9 Chapter 6: Modular Programming Inline Functions Calling a function places a certain amount of overhead on a computer. This consists of placing argument values in a reserved memory region that the function has access to (stack), passing control to the function providing a reserved memory location for any returned value, and finally returning to the proper point in the calling program. Paying this overhead is well justified when a function is called many times. For small functions that are not called many times, however, the overhead for passing and returning values may not be warranted. Telling the C++ compiler that a function is inline causes a copy of the function code to be placed in the program at the point where the function is called. To accomplish this simply requires placing the reserved word inline before the function name and defining the function before any calls are made to it. The advantage of using an inline function is an increase in execution speed. There is no execution time loss due to the call and return overhead required by a non-inline function. The disadvantage is the increase in program size when an inline function is called repeatedly. Each time an inline function is referenced the complete function code is reproduced and stored as an integral part of the program. A non-inline function, however, is stored in memory only once. 10 Chapter 6: Modular Programming EXAMPLE: Program 6-3 #include <iostream> using namespace std; inline double tempvert(double in_temp) { return( (5.0/9.0) * (in_temp - 32.0) ); } int main( ) { int count; double fahren; // start of declarations for (count = 1; count <= 4; count++) { cout << "\nEnter a Fahrenheit temperature: "; cin >> fahren; cout << "The Celsius equivalent is " << tempvert(fahren) << endl; } return (0); } 11 Chapter 6: Modular Programming 6.2 Functions with Output and Inout Parameters In the previous section, we reviewed examples of functions which accepted input parameters and returned the results through output parameters. In this section, we will review examples of functions that have only output or inout (input/output) parameters. The swap ( ) function In this example a function called swap( ) will be written for exchanging the value of two of the main function variables. One way of doing this is by using reference variables. This is an example of inout or input/output parameters. There are three steps involved in this function: 1. Store the first argument's value in a temporary location. 2. Store the second argument's value in the first variable. 3. Store the temporary value in the second argument. void swap (float &num1, float &num2) { float temp; temp = num1; //step 1 num1 = num2; //step 2 num2 = temp; // step 3 } 12 Chapter 6: Modular Programming The Complete Program: Program 6-4 #include <iostream> using namespace std; void main(void) { float firstnum = 20.5, secnum = 6.25; // function prototype - receives 2 references void swap(float &, float &); cout << "The value stored in firstnum is: " << firstnum << "\n"; cout << "The value stored in secnum is: " << secnum << "\n\n"; // call the function with references swap(firstnum, secnum); cout << "The value stored in firstnum is now: " << firstnum << "\n"; cout << "The value stored in secnum is now: " << secnum << "\n"; return; } 13 Chapter 6: Modular Programming void swap(float &num1, float &num2) { float temp; temp = num1; num1 = num2; num2 = temp; // save num1's value // store num2's value in num1 // change num2's value return; } Two cautions: 1. The reference arguments cannot be used to change constants. For example: swap( 20.5, 6.5) does not change the values of these constants. 2. The reference arguments should only be used in restricted situations that actually require multiple return values, such as the swap( ) function. The calc( ) function in Program 6-2, while useful for illustrative purposes, could also be written as two separate functions, each returning a single value. 14 Chapter 6: Modular Programming The getFrac( ) function In this example a function called getFrac( ) will be written which actually reads its data items from the keyboard. This is an example of a function using output parameters only. EXAMPLE: Program 6-4 #include <iostream> using namespace std; void getFrac(int&, int&); int main() { int num, denom; // Function prototype // input - fraction numerator // input - fraction denominator cout << "Enter a common fraction " << "as 2 integers separated by a slash: "; getFrac(num, denom); cout << "Fraction is " << num // There is an error << " / " << denom << endl; // in your book here return (0); } 15 Chapter 6: Modular Programming // Reads a fraction void getFrac(int& numerator, // OUTPUT int& denominator) // OUTPUT { char slash; // temporary storage for slash cin >> numerator >> slash >> denominator; return; } In-Class Exercise: Write a function called displayFrac( ) that displays a common fraction given its numerator and denominator as input arguments. Modify Program 6-4 to use your new function. 16 Chapter 6: Modular Programming The order( ) function In this next example, multiple calls will be made to a function called order( ) which uses inout parameters. The order( ) function stores the smaller of its two arguments in its first actual argument and the larger is stored in its second actual argument. The main( ) function will call the order( ) function three times to sort three data values in increasing order. EXAMPLE: Program 6-5 #include <iostream> using namespace std; void order(float&, float&); // INOUT - numbers to sort int main( ) { // user input - numbers to sort float num1, num2, num3; // Read 3 numbers cout << "Enter 3 numbers to sort: "; cin >> num1 >> num2 >> num3; // Sort them order(num1, num2); order(num1, num3); order(num2, num3); // order data in num1 & num2 // order data in num1 & num3 // order data in num2 & num3 17 Chapter 6: Modular Programming // Display results cout << "The three numbers in order are:" << endl; cout << num1 << " " << num2 << " " << num3 << endl; return (0); } void order(float& x, float& y) { float temp; // storage for number in x // Compare x and y, exchange values if not in order if (x > y) { // exchange values in x and y temp = x; // store old x in temp x = y; // store old y in x y = temp; // store old x in y } return; } 18 Chapter 6: Modular Programming 6.3 Stepwise Design with Functions Passing information into and from functions improves the ability of the programmer to solve problems. When designing a program, we separate the primary problem of the program into subproblems. This allows the programmer to modularize the program into individual subproblems or pieces. If any solution to a single subproblem cannot be written easily in a few C++ statements, it makes sense to code it as a function. CASE STUDY: General Sum and Average Problem Problem: We need create code that can be used to accumulate a sum and average a list of data values using functions. These tasks are required in many programs; therefore, we need to design a general set of functions that can be reused in other programs and, later on, when we design actual classes. Analysis: 1. This is something we have done several times already. Let’s analyze the requirements. What are the general steps involved to accomplish this task? 19 Chapter 6: Modular Programming 2. What are the data requirements? a. Problem Input: b. Problem Output: 3. What formula(s) are needed? Design: At this stage, we need to develop the algorithm necessary to accomplish our task and document the data flow between the main problem and each of its subproblems. Initial Algorithm: 1. Read the number of items. 2. Read the data items and compute the sum of the data. 3. Compute the average of the data. 4. Print the sum and the average. 20 Chapter 6: Modular Programming In-Class Exercise: Develop a structure chart that documents this initial algorithm and the data flow between the main( ) function and each of the subproblems or functions. Implementation: Using the structure chart we developed in the design phase, we can now write the main( ) function before we develop the algorithms involved for any of the steps or subproblems. 1. What data elements or variables are needed in main( )? 2. Move the initial algorithm into main( ) as comments. 3. Code each step in-line (as part of the code) or as a function call. 21 Chapter 6: Modular Programming In-Class Exercise: Implement the above steps in a source code file. At this point, we need only include the necessary prototypes for our functions. You should be able to compile this code. (Do not build or link it since we have not developed the functions yet.) Stepwise design of the functions At this stage, we need to repeat the above steps for each of the three functions as prototyped and called in the above implementation: 1. 2. 3. float computeSum(int); float computeAverage(int, float); void printSumAve(int, float, float); I. Analysis for computeSum( ) Function Interface --Input parameters: int numItems // number of items Output parameters: none Return value: the sum (float) of data items processed Local Data: float item float sum int count // each data item as read in // accumulates the sum of data read in // number of items processed 22 Chapter 6: Modular Programming Design for computeSum( ) Need a loop to control the reading of the data items. We already know the number of items so we can use a count-controlled loop. Initial Algorithm: 1. Initialize sum to 0. 2. For each value of count (count = 0 ; count < numItems) a. Read in the data item. b. Add the data item to sum. 3. Return sum to the calling function. II. Analysis for computeAverage( ) Function Interface --Input parameters: int numItems // number of items float sum // sum of all items Output parameters: none Return value: the average (float) of data items Design for computeAverage( ) Initial Algorithm: 1. If the number of items is less than1 a. Display “invalid number of items” message. b. Return value of 0. 2. Return value of the sum divided by numItems. 23 Chapter 6: Modular Programming III. Analysis for printSumAve( ) Function Interface --Input parameters: int numItems // number of items float sum // sum of all items float average // average of all items Output parameters: none Return value: none Design for printSumAve( ) Initial Algorithm: 1. If the number of items is positive ( > 0) a. Display the number of items, the sum, and the average of the data. Else a. Display “invalid number of items” message. In-Class Exercise: Implement the function definitions in your source code file. Test your program. Issues of Program Style Use of Functions for Relatively Simple Algorithm Steps Although some functions are quite short and would be easier to include as in-line code, it is better to use a separate function. The use of functions provide some of the following benefits: 24 Chapter 6: Modular Programming 1. 2. 3. Help keep details of code separate and hidden from those who do not need to see the details. Make your program easier to debug, test, and even modify. Allow possibility of the reuse of the function at some other time. From now on, our main( ) functions should primarily consist of a sequence of function calls. Cohesive Functions Our computeSum( ) function only performs one task. It computes the sum of the data items as they are read in. Functions that perform a single operation are called functionally cohesive. It is a good programming style to write such singlepurpose, highly cohesive functions. These functions are relatively compact, easy to read, write and debug. In-Class Exercise: Design and implement an algorithm for readNumItems( ) that uses a loop to ensure that the user enters a positive value for the numItems variable. The loop should continue reading numbers until the user enters a positive number. Modify the previous exercise to use this function instead of the code inside main( ). 25 Chapter 6: Modular Programming 6.4 Using Objects with Functions There are two ways that you can use functions to modify objects in C++: 1. You can use dot notation to apply a member function to an object. The member function may modify one or more data attributes of the object to which it is applied. 2. You can pass an object as an argument to a function. The following example uses both methods. moneyToNumberTest.cpp (from textbook) #include <string> #include <iostream> using namespace std; // Function prototype void moneyToNumberString(string&); int main() { string mString; // input - a "money" string cout << "Enter a dollar amount with $ and commas: "; cin >> mString; moneyToNumberString(mString); cout << "The dollar amount as a number is " << mString << endl; return 0; } 26 Chapter 6: Modular Programming // Removes the $ and commas from a money string. void moneyToNumberString (string& moneyString) // INOUT - string w/poss. $ and commas { // Local data . . . int posComma; // position of next comma // Remove $ from moneyString if (moneyString.at(0) == '$') moneyString.erase(0, 1); else if (moneyString.find("-$") == 0) moneyString.erase(1, 1); // Starts with $ ? // Remove $ // Starts with -$ ? // Remove $ // Remove all commas posComma = moneyString.find(","); while (posComma >= 0 && posComma < moneyString.length()) { moneyString.erase(posComma, 1); posComma = moneyString.find(","); } } // end moneyToNumberString // Find first , // posComma valid? // Remove , // Find next , In-Class Exercise: Write a function doRemove( ) that removes the first occurrence of a substring (an input argument) from a second argument string (input/output argument). Write a main( ) function to test your new function. 27 Chapter 6: Modular Programming 6.5 Debugging and Testing a Program System The possibility of errors increase as the size of your programs or program system grows. By keeping each function to a manageable size keeps this increase to a minimum. Testing and debugging each function is also much easier. By writing a large program as a set of independent functions, you can simplify the overall programming process. You can also simplify the testing and debugging process by testing in stages as your program evolves. There are two kinds of testing used: top-down testing and bottom-up testing. You should use a combination of these methods to test your program and its functions. Top-Down Testing and Stubs When you are part of a programming team (or even if working alone), you will find that not all functions will be ready at the same time. To simplify the debugging process, it is desirable, also, to test your program as it evolves rather than waiting for it to be completed. You can begin by testing the overall flow of control between the main program and its level-1 functions that are complete. Level-1 functions are those functions that are called directly from main( ). The process of testing the flow of control between a main( ) function and its subordinate functions is called top-down testing. 28 Chapter 6: Modular Programming Because main( ) calls all level-1 functions, we need to provide a substitute for all functions that are not yet coded. This substitute is called a stub – a function with a heading and a minimal body that is used to test the flow of control. The stub will test the basic functionality of the call to the function and the return statement. The body of the stub should include a simple cout that displays a message identifying the function being executed. It should also assign simple values to any outputs. Stub for function computeSum( ): // Computes sum of data – stub float computeSum(int numItems) // IN – number of data items { cout << “Function computeSum( ) entered” << endl; return 100.0; // arbitrary value returned } Bottom-Down Testing and Drivers When a function is completed, you can substitute it for its stub in your program. But, before making it part of the large program, you will normally want to test the new function itself. By testing the function independently, it will be easier to locate and correct any errors, since you will not be dealing with a complete program system. This independent test of an individual function is called a unit test. We can perform a unit test by writing a short driver function to call it. A driver function is a main( ) function that is used to test the functionality of a single function. It does not need to be complex; rather, it should contain only those declarations and executable statements that are necessary to test a single 29 Chapter 6: Modular Programming function. It should begin by reading or assigning values to all input arguments and to input/output arguments needed for the function call. The function should then be called. After calling the function, the driver should display the function results. Driver to test function computeSum( ): int main( ) { int n; // Keep calling computeSum( ) and displaying the result. do { cout << “Enter number of items or 0 to quit: ”; cin >> n; cout << “The sum is “ << computeSum(n) << endl; } while (n != 0); } return (0); // end driver When you are sure that the function works properly, you can substitute it for its stub in your program system. This process of separately testing individual functions before inserting them in a program system is called bottom-up testing. After replacing all of its stubs with functions that have been independently pretested, you should test the entire system. These tests are called system integration tests. 30 Chapter 6: Modular Programming Debugging Tips for Program Systems Here are some suggestions for debugging a program system. 1. Use comments to document each function parameter and local variable as you write your code. Also describe the purpose of the function. 2. Create a trace of execution be displaying the function name as you enter it. 3. Trace or display the values of all input and input/output parameters upon entry to a function. Check that these values make sense. 4. Trace or display the values of all function outputs after returning from a function. Verify that these values are correct by hand computation. Make sure that you declare all input/output and output parameters as reference parameters using the ‘&’ symbol. 5. Make sure that the function stub assigns a value to each output parameter when testing. In-Class Exercise: Write a driver function to test the computeAverage( ) function. 31 Chapter 6: Modular Programming 6.6 Recursive Functions C++ allows a function to call itself. A function that calls itself is a recursive function. A recursive function is often used in place of iteration or looping. A recursive function calls itself repeatedly, but with different argument value(s) for each call. The key to using recursion successfully is to include a conditional test to terminate the recursion. This condition is called a stopping case and is similar to the conditional expression used to exit from a loop. Without a stopping case, the function will call itself forever. You need to realize that each time the function is called the system creates a new set of automatic variables for that function on the stack. The size of the stack grows in relation to the number of times you call the function. If you call the function too many times (which will happen if fail to incorporate a stop), the stack could grow so large that you run out of stack space. This is called a stack overflow. Form or template for a recursive function: If the stopping case is reached Return a value for the stopping case Else Return a value computed by calling the function again with different arguments. 32 Chapter 6: Modular Programming Here’s a simple example: Program 6-8 # include <iostream> using namespace std; // Function prototype void upAndDown(int); int main() { upAndDown(1); return (0); } void upAndDown(int n) { cout << “Level “ << n << endl; if (n < 4) upAndDown(n+1); cout << “LEVEL “ << n << endl; } Program output: Level 1 Level 2 Level 3 Level 4 LEVEL 4 LEVEL 3 LEVEL 2 LEVEL 1 33 // print #1 // print #2 Chapter 6: Modular Programming Fundamentals Let’s look at some basic points of recursion. 1. Each level of recursion or function call has its own set of variables. Each has its own distinct value. 2. Each function call is balanced with a return. Once the program flow reaches the stopping case or return at the end of the last recursion level, control is passed back to the previous recursion level. Control moves back level by level until the first function or recursion level is reached. This process is called unwinding the recursion. 3. Statements in a recursive function that come before the recursive call are executed in the same order that the functions are called. They are executed as control is passed forward. 4. Statements in a recursive function that come after the recursive call are executed in the opposite order from which the functions are called. They are executed as control is being passed backward. 5. Although each level of recursion has its own set of variables, the code itself is not duplicated. The code is a sequence of instructions, and a function call is a command to go to the beginning of that set of instructions. 34 Chapter 6: Modular Programming Tail Recursion The simplest form of recursion is called tail recursion, or end recursion, because the recursive call comes at the end of the function before the return statement. This type of recursion acts like a loop. Recursive function for factorial: // Returns: The product of 1 * 2 * 3 * … * n for n > 1; // otherwise 1. int factorial(int n) { if (n <= 1) return 1; else return (n * factorial(n – 1)); // Recursive call } Iterative function for factorial: // Returns: The product of 1 * 2 * 3 * … * n for n > 1; // otherwise 1. int factorial(int n) { int productSoFar = 1 ; // output: accumulated product for (int i = n; i > 1; i--) productSoFar = productSoFar * i; return (productSoFar); } 35 Chapter 6: Modular Programming The main( ) function for either factorial: #include <iostream> using namespace std; int factorial(int); // function prototype int main() { int num; cout << "This program calculates the factorial " << "for any integers\n" << "entered by the user. Please enter only " << "positive integers\n" << "under 16." << endl << endl; do { cout << "\nEnter a positive integer (or 0 to quit): "; cin >> num; if (num < 0) cout << "No negative numbers, please." << endl; else if (num > 15) cout << "Keep input under 16." << endl; else cout << "The factorial for " << num << " is: " << factorial(num) << endl; } while (num != 0); return (0); } 36 Chapter 6: Modular Programming In-Class Exercise: 1. Write a recursive function that, given an input value of n, computes: n + (n – 1) + … + 2 + 1. 2. Given the following code, fill in the recursive function to print the users input in reverse: #include <iostream> #include <string> #include <conio.h> using namespace std; void readInput(string&); // prototype void printBackward(string, int); int main() { string inString; readInput(inString); // function call cout << "\nYou entered: " << inString << endl; return (0); } void readInput(string &input) { char ch; cout << "Enter a line of input: "; do { ch = getchar(); if (ch == '\n') input += '\0'; else input += ch; } while (ch != '\n'); return; } void printBackward(string input, int n) { // Enter recursive code here } 37