More on                                                                    
Parameter Passing
 

 

We have encountered various types of parameters, which are summarized below:

Kind of Parameter

Usage

Actual Parameter Appears in a procedure call. The corresponding formal parameter may be either a reference or a value parameter.
Formal value parameter Appears in a procedure heading. Receives a copy of the value stored in the corresponding actual parameter.
Formal reference parameter Appears in a procedure heading. Receives the address of the corresponding actual parameter.

Recall that with a value parameter, which is declared in the formal parameter list by using the keyword ByVal, the procedure receives a copy of the actual parameter's value. With a reference parameter, which is declared in the formal parameter list by using the keyword ByRef, the procedure receives the location (memory address) of the actual parameter.

There should be the same number of actual parameters in a procedure call as there are formal parameters in the procedure heading. (VB actually allows you to violate this--more later.)  Also, each actual parameter should have the same data type as the formal parameter in the same position. 


Value Parameters

Because value parameters are passed copies of their actual parameters, anything that has a value may be passed to a value parameter. This includes constants, variables, and even expressions. Expressions are evaluated and a copy of the result is sent to the corresponding value parameter.

Because a value parameter does not receive the location of the actual parameter, the actual parameter cannot be directly accessed or changed. When a procedure returns, the contents of any value parameters are destroyed, along with the contents of the local variables. The difference between value parameters and local variables is that the values of local variables are undefined when a procedure starts to execute, whereas value parameters are automatically initialized to the values of the corresponding actual parameters.

Because the contents of value parameters are destroyed when the procedure returns, they cannot be used to return information to the calling code.


Reference Parameters

When reference parameters are used, the called procedure refers to the corresponding actual parameter directly. Specifically, the procedure is allowed to inspect and modify the caller's actual parameter.

When a procedure is invoked using a reference parameter, it is the location (memory address) of the actual parameter, not its value, that is passed to the procedure. There is only one copy of the information, and it is used by both the caller and the called procedure. When a procedure is called, the actual parameter and formal parameter become synonyms for the same location in memory. Whatever value is left by the called procedure in this location is the value that the caller will find there.  Therefore, you must be careful using a formal reference parameter because any change made to it affects the actual parameter in the calling code.

Only a variable can be passed as an actual parameter to a reference parameter, because a procedure can assign a new value to the actual parameter. (In contrast, remember that an arbitrarily complicated expression can be passed to a value parameter.)


Designing procedures

The body of a procedure is like any other segment of code, except that it is contained in a separate block within the program. Isolating a segment of code in a separate block means that its implementation details can be "hidden" from view. As long as you know how to call a procedure and what its purpose is, you can use it without knowing how it actually works. For example, you don't know how the code for a library function like IsNumeric is written (its implementation is hidden from view), yet you still can use it effectively.

The specification of what a procedure does and how it is invoked defines its interface. By hiding a procedure's implementation, or encapsulating the procedure, we can make changes to it without changing the calling routine, as long as the interface remains the same. For example, you might rewrite the body of a procedure using a more efficient algorithm.

The figure below indicates that the function heading, included in the unshaded segment, is known to other procedures within the program.  The actual implementation of the function, that is, the statements that make up the body of the function, are in the shaded area, indicating that they are hidden from view.

Heading: Private Function IsLeapYear (ByVal Year As Integer) As Boolean

Implementation

Encapsulation is what we do in the functional decomposition process when we postpone the solution of a difficult subproblem. We write down its purpose and what information it takes and returns, and then we write the rest of our design as if the subproblem already had been solved. We could hand this interface specification to someone else, and that person could develop a procedure for us that solves the subproblem. We needn't be concerned about how it works, as long as it conforms to the interface specification. Interfaces and encapsulation are the basis for team programming, in which a group of programmers work together to solve a large problem.

Thus, designing a procedure can (and should) be divided into two tasks: designing the interface and designing the implementation. We already know how to design an implementation--it is a segment of code that corresponds to an algorithm. To design the interface, we focus on the what, not the how. We must define the behavior of the procedure (what it does) and the mechanism for communicating with it. To define the mechanism for communicating with the procedure, make a list of the following items:

  1. Incoming values that the procedure receives from the caller.
  2. Outgoing values that the procedure produces and returns to the caller.
  3. Incoming/outgoing values--values that the procedure changes (receives and returns)

Decide which identifiers inside the module match the values in this list. These identifiers become the variables in the formal parameter list for the procedure. Then the formal parameters are declared in the procedure heading. All other variables that the procedure needs are local and must be declared within the body of the procedure. This process may be repeated for all the modules at each level.


Documenting the Direction of Data Flow

Another helpful piece of documentation in a procedure interface is the direction of data flow for each parameter in the parameter list. Data flow is the flow of information between the procedure and its caller. Each parameter can be classified as an incoming parameter, an outgoing parameter, or an incoming/outgoing parameter. (Some people refer to these as input parameters, output parameters, and input/output parameters.)

For an incoming parameter, the direction of data flow is one-way -- into the procedure. The procedure inspects and uses the current value of the parameter but does not modify it. In a comment section preceding the procedure heading, we include a description of the procedure, as well as a list of the parameters annotated by the classification in, out, or in/out.

Pass-by-value is appropriate for each parameter that is incoming only. As you can see in the procedure body, displayAverage does not modify the values of the parameters sum and count. It merely uses their current values. The direction of data flow is one-way -- into the procedure.

The data flow for an outgoing parameter is one-way -- out of the procedure. The procedure produces a new value for the parameter without using the old value in any way. Pass-by-reference must be used for an outgoing parameter

Finally, the data flow for an incoming/outgoing parameter is two-way -- into and out of the procedure. The procedure uses the old value and also produces a new value for the parameter. Here is an example of a procedure that uses two parameters, one of them incoming only and the other one incoming/outgoing:

This procedure first inspects the incoming value of balance so that it can evaluate the expression to the right of the equal sign. Then it stores a new value in balance by using the assignment operation. The data flow for balance is therefore considered a two-way flow of information. Pass-by-value is appropriate for deposit (it's incoming only), but pass-by-reference is required for balance (it's an incoming/outgoing parameter).


Functions

Returning more than one value from a value-returning function (by modifying the actual parameters) is an unwanted side effect and should be avoided. If your interface design calls for multiple values to be returned or for the values of actual parameters to be changed, then you should use a sub.


Function or Sub?

There aren't any formal rules for determining when to use a sub and when to use a function, but here are some guidelines

  1. If the module must return more than one value or modify any actual parameters, do not use a function.
  2. If the module must perform I/O, do not use a function.
  3. If there is only one value returned from the module and it is a Boolean value, a function is appropriate.
  4. If there is only one value returned and that value is to be used immediately in an expression, a function is appropriate.
  5. When in doubt, use a sub. You can recode any function as a sub by adding an extra outgoing parameter to carry back the computed result.  (However, you are expected to be able to determine which one is more appropriate in this class!)
  6. If both a sub and a function are acceptable, use the one you feel more comfortable implementing

Functions provide a way of simulating the mathematical concept of a function.


Testing and Debugging

One of the advantages of a modular design is that you can test it long before the code has been written for all of the modules. If we test each module individually, then we can assemble the modules into a complete program with much greater confidence that the program is correct. In this section, we introduce a technique for testing a module separately.

Stubs and Drivers

Suppose you were given the code for a module and your job was to test it. How would you test a single module by itself? First of all, it must be called by something (unless it is the main procedure). Second, it may have calls to other modules that aren't available to you. To test the module, you must fill in these missing links.

When a module contains calls to other modules, we can write dummy procedure called stubs to satisfy those calls. A stub usually consists of an output statement that prints a message such as , "Function IsLeapYear just got called." Even though the stub is a dummy, it allows us to determine whether the procedure is called at the right time.

A stub can also be used to print the set of values that are passed to it; this tells us whether or not the module being tested is supplying the proper information. Sometimes the stub will assign new values to its reference parameters to simulate data being read or results being computed to give the module something to keep working on. Because we can choose the values that are returned by the stub, we have better control over the conditions of the test run.

In addition to supplying a stub for each call within the module, you may also provide a dummy program -- a driver -- to call the module itself. A driver program contains the bare minimum of code required to call the module being tested.

By surrounding a module with a driver and stubs, you gain complete control of the conditions under which it executes. This allows you to test different situations and combinations that may reveal errors

Testing and Debugging Hints

  1. Make sure that variables used as actual parameters to a procedure are declared in the code segment where the procedure call is made.
  2. Carefully define the parameter list to eliminate side effects. Variables used only in a procedure should be local. Do not use global variables in your programs.
  3. If the compiler displays a message such as "Name x is not declared," check that identifiers aren't misspelled, that they are declared, and that their declaration precedes any references.
  4. If you intend to use a local name that is the same as a nonlocal name, a misspelling in the local declaration will wreak havoc. The compiler won't complain, but will cause every reference to the local name to go to the nonlocal name instead.
  5. The same identifier cannot be used in both the formal parameter list and as a local variable in a procedure.  
  6. With a function, be sure the function heading includes the correct data type for the function return value.
  7. With a function, don't forget to use a statement to return the function value. Make sure the expression is of the correct type, or implicit type coercion will occur.
  8. Remember that a call to a function can be part of an expression, whereas a call to a sub is a separate statement.
  9. When calling a sub be sure to use the keyword Call.  If you omit it, you must also omit the parentheses around the argument list.
  10. In general, don't use reference parameters in the formal parameter list of a function.
  11. If necessary, use debug output statements to indicate when a procedure is called and if it is executing correctly. The values in the actual parameters can be printed immediately before (to show the incoming values) and immediately after (to show the outgoing values) the call to the procedure. You may want to use debug output statements in the procedure itself to indicate what happens each time it is called.