9

Scope, Lifetime, and
More on Functions

KNOWLEDGE GOALS

 

SKILL GOALS

images

As programs get larger and more complicated, the number of identifiers in a program increases. We invent function names, variable names, constant identifiers, and so on. Some of these identifiers we declare inside blocks. Other identifiers—function names, for example—we declare outside of any block. This chapter examines the C++ rules by which a function may access identifiers that are declared outside its own block. Using these rules, we return to the discussion of interface design that we began in Chapter 8.

Finally, we look at the second kind of subprogram provided by C++: the value-returning function. Unlike void functions, which return results (if any) through the parameter list, a value-returning function returns a single result—the function value—to the expression from which it was called. In this chapter, you learn how to write user-defined value-returning functions.

9.1 Scope of Identifiers

As we saw in Chapter 8, local variables are those declared inside a block, such as the body of a function. Recall that local variables cannot be accessed outside the block that contains them. The same access rule applies to declarations of named constants: Local constants may be accessed only in the block in which they are declared.

Any block—not just a function body—can contain variable and constant declarations. For example, this If statement contains a block that declares a local variable n:

images

Like any other local variable, n cannot be accessed by any statement outside the block containing its declaration. It is defined for all statements that follow it within its block, and it is undefined outside of the block.

If we listed all the places from which an identifier could be accessed legally, we would describe that identifier's scope of visibility or scope of access, often just called its scope.

C++ defines several categories of scope for any identifier. We begin by describing three of these categories.

1. Class scope. This term refers to the data type called a class, which we mentioned briefly in Chapter 4. We will postpone our detailed discussion of class scope until Chapter 12.

2. Local scope. The scope of an identifier declared inside a block extends from the point of declaration to the end of that block. Also, the scope of a function parameter extends from where it is declared in the heading to the end of the function body. You can think of a function parameter's scope as being the same as if it was a local variable declared before anything else in the function body.

3. Global scope. The scope of an identifier declared outside all functions and classes extends from the point of declaration to the end of the entire file containing the program code.

C++ function names have global scope. (We discuss an exception to this rule in Chapter 12.) Once a function name has been declared, it can be invoked by any other function in the rest of the program. In C++, there is no such thing as a local function—that is, you cannot nest a function definition inside another function definition.

Global variables and constants are those declared outside all functions. In the following code fragment, gamma is a global variable and can be accessed directly by statements in main and SomeFunc:

images

When a function declares a local identifier with the same name as a global identifier, the local identifier takes precedence within the function. This principle is called name precedence or name hiding.

Here's an example that uses both local and global declarations:

images

In this example, function SomeFunc accesses global constant A but declares its own local variable b and parameter c. Thus the output is

A = 17 b = 2.3 c = 42.8

Local variable b takes precedence over global variable b, effectively hiding global b from the statements in function SomeFunc. Parameter c also blocks access to global variable c from within the function. Function parameters act just like local variables in this respect; that is, parameters have local scope.

Scope Rules

When you write C++ programs, you rarely declare global variables. There are negative aspects to using global variables, which we discuss later. But when a situation crops up in which you have a compelling need for global variables, it pays to know how C++ handles these declarations. The rules for accessing identifiers that aren't declared locally are called scope rules.

In addition to local and global access, the C++ scope rules define what happens when blocks are nested within other blocks. Anything declared in a block that contains a nested block is nonlocal to the inner block. (Global identifiers are nonlocal with respect to all blocks in the program.) If a block accesses any identifier declared outside its own block, it is termed a nonlocal access.

Here are the detailed scope rules, excluding class scope and certain language features we have not yet discussed:

1. A function name has global scope. Function definitions cannot be nested within function definitions.

2. The scope of a function parameter is identical to the scope of a local variable declared in the outermost block of the function body.

3. The scope of a global variable or constant extends from its declaration to the end of the file, except as noted in Rule 5.

4. The scope of a local variable or constant extends from its declaration to the end of the block in which it is declared. This scope includes any nested blocks, except as noted in Rule 5.

5. The scope of an identifier does not include any nested block that contains a locally declared identifier with the same name (local identifiers have name precedence).

Here is a sample program that demonstrates C++ scope rules. To simplify the example, only the declarations and headings are spelled out. Note how the While loop body labeled Block3, located within function Block2, contains its own local variable declarations.

images

Let's look at the ScopeRules program in terms of the blocks it defines and see just what these rules mean. FIGURE 9.1 shows the headings and declarations in the ScopeRules program, with the scopes of visibility being indicated by boxes.

images

FIGURE 9.1 Scope Diagram for Scope Rules Program

Anything inside a box can refer to anything in a larger surrounding box, but outside-in references aren't allowed. Thus a statement in Block3 could access any identifier declared in Block2 or any global variable. A statement in Block3 could not access identifiers declared in Block1 because it would have to enter the Block1 box from outside.

Notice that the parameters for a function are inside the function's box, but the function name itself is outside the box. If the name of the function were inside the box, no function could call another function. This point demonstrates merely that function names are globally accessible.

Imagine the boxes in FIGURE 9.1 as rooms with walls made of two-way mirrors, with the reflective side facing out and the see-through side facing in. If you stood in the room for Block3, you would be able to see out through all the surrounding rooms to the declarations of the global variables (and anything between). You would not be able to see into any other rooms (such as Block1), however, because their mirrored outer surfaces would block your view. Because of this analogy, the term visible is often used in describing a scope of access. For example, variable a2 is visible throughout the program, meaning that it can be accessed from anywhere in the program.

FIGURE 9.1 does not tell the whole story; however, it represents only scope Rules 1 through 4, but not Rule 5. Variable a1 is declared in three different places. Because of the name precedence rules, Block2 and Block3 access the a1 declared in Block2 rather than the global a1. Similarly, the scope of the variable b2 declared in Block2 does not include the “hole” created by the declaration of variable b2 in Block3.

Name precedence rules are implemented by the compiler as follows. When an expression refers to an identifier, the compiler first checks the local declarations. If the identifier isn't local, the compiler works its way outward through each level of nesting until it finds an identifier with the same name. There it stops. If there is an identifier with the same name declared at a level even farther out, it is never reached.

If the compiler reaches the global declarations (including identifiers inserted by #include directives) and still can't find the identifier, an error message such as UNDECLARED IDENTIFIER is issued. Such a message most likely indicates a misspelling or an incorrect capitalization, or it could mean that the identifier was not declared before the reference to it or was not declared at all. It may also indicate that the blocks are nested so that the identifier's scope doesn't include the reference1.

Variable Declarations and Definitions

In Chapter 8, you learned that C++ distinguishes between function declarations and definitions. Definitions cause memory space to be reserved, whereas declarations do not.

C++ applies the same terminology to variable declarations. A variable declaration becomes a variable definition if it also reserves memory for the variable. All of the variable declarations we have seen so far have been variable definitions. What would a variable declaration look like if it was not also a definition?

In Chapter 8, we talked about multifile programs. C++ has a reserved word extern that lets you reference a global variable located in another file. A definition such as

int someInt;

causes the compiler to reserve a memory location for someInt. By contrast, the statement

extern int someInt;

is known as an external declaration. It states that someInt is a global variable located in another file and that no storage should be reserved for it here. In C++ terminology, the preceding statement is a declaration but not a definition of someInt. It associates a variable name with a data type so that the compiler can perform type checking. In C++, you can declare a variable or a function many times, but there can be only one definition.

Except where it's important to distinguish between declarations and definitions of variables, we'll continue to use the more general phrase variable declaration instead of the more specific variable definition.

Namespaces

For some time, we have been including the following using directive in our programs:

using namespace std;

In Chapter 2, we noted that without this directive, we would have to refer to cout using the qualified name

std::cout

Now that we have explored the basic concept of scope, we are ready to answer the question: What exactly is a namespace? It is a mechanism by which the programmer can create a named scope2. For example, the standard header file cstdlib contains function prototypes for several library functions, one of which is the absolute value function, abs. The declarations are contained within a namespace definition as follows:

images

A namespace definition consists of the word namespace, then an identifier of the programmer's choice, and then the namespace body between braces. Identifiers declared within the namespace body are said to have namespace scope. Such identifiers cannot be accessed outside the body except by using one of three methods.

The first method is to use a qualified name: the name of the namespace, followed by the scope resolution operator (::), followed by the desired identifier. Here is an example:

images

The general idea is to inform the compiler that we are referring to the abs declared in the std namespace, and not to some other abs (such as a global function named abs that we might have written ourselves).

The second method is to use a statement called a using declaration as follows:

images

images

These using declarations allow the identifiers abs and cout to be used throughout the body of main as synonyms for the longer std::abs and std::cout, respectively.

The third method—one with which we are familiar—is to use a using directive (not to be confused with a using declaration):

images

With a using directive, all identifiers from the specified namespace are accessible, but only in the scope in which the using directive appears. In the preceding fragment, the using directive is in local scope (it's within a block), so identifiers from the std namespace are accessible only within main. Conversely, suppose we put the using directive outside all functions (as we have been doing), like this:

images

Then the using directive is in global scope; consequently, identifiers from the std namespace are accessible globally.

Placing a using directive in global scope is a convenience. For example, all of the functions we write can refer to identifiers such as abs, cin, and cout without our having to insert a using directive locally in each function.

Creating global using directives is considered a bad idea when we are creating large, multifile programs, where programmers often use multiple libraries. Two or more libraries may, just by coincidence, use the same identifier for different purposes. Global using directives then lead to name clashes (multiple definitions of the same identifier), because all the library identifiers are in the same global scope. (C++ programmers refer to this problem as “polluting the global namespace.”)

We continue to use global using directives for the std namespace because our programs are relatively small and, therefore, name clashes aren't likely. Also, the identifiers in the std namespace are so commonly used that most C++ programmers avoid duplicating them.

Given the concept of namespace scope, we refine our description of C++ scope categories as follows:

1. Class scope. This term refers to the class data type, which we discuss in Chapter 12.

2. Local scope. The scope of an identifier declared inside a block extends from the point of declaration to the end of that block. Also, the scope of a function parameter (formal parameter) extends from the point of declaration to the end of the block that is the body of the function.

3. Namespace scope. The scope of an identifier declared in a namespace definition extends from the point of declaration to the end of the namespace body, and its scope includes the scope of any using directive specifying that namespace.

4. Global (or global namespace) scope. The scope of an identifier declared outside all namespaces, functions, and classes extends from the point of declaration to the end of the entire file containing the program code.

Note that these are general descriptions of scope categories and not scope rules. The descriptions do not account for name hiding.

9.2 Lifetime of a Variable

A concept related to, but separate from, the scope of a variable is its lifetime—the period of time during program execution when an identifier actually has memory allocated to it. We have said that storage for local variables is created (allocated) at the moment control enters a function. The variables remain “alive” while the function is executing, and the storage is destroyed (deallocated) when the function exits. In contrast, the lifetime of a global variable is the same as the lifetime of the entire program. Memory is allocated only once, when the program begins executing, and is deallocated only when the entire program terminates. Observe that scope is a compile-time issue, whereas lifetime is a run-time issue.

In C++, an automatic variable is one whose storage is allocated at block entry and deallocated at block exit. A static variable is one whose storage remains allocated for the duration of the entire program. All global variables are static variables. By default, variables declared within a block are automatic variables. However, you can use the reserved word static when you declare a local variable. If you do so, the variable is a static variable and its lifetime persists from function call to function call.

The next program contains a function that keeps track of the number of times it is called. The output is shown following the program.

images

Here is the program's output:

images

It is usually a better idea to declare a local variable as static than to use a global variable. As for a global variable, the memory for a static variable remains allocated throughout the lifetime of the entire program. But unlike for a global variable, its local scope prevents other functions in the program from tinkering with it.

Initializations in Declarations

One of the most common things we do in programs is first declare a variable and then, in a separate statement, assign an initial value to the variable. Here's a typical example:

int sum;
sum = 0;

As we said in Chapter 8, C++ allows you to combine these two statements. The result is known as an initialization in a declaration. Here we initialize sum as part of its declaration:

int sum = 0;

In a declaration, the expression that specifies the initial value is called an initializer. In the preceding statement, the initializer is the constant 0. Implicit type coercion takes place if the data type of the initializer differs from the data type of the variable.

An automatic variable is initialized to the specified value each time control enters the block:

images

In contrast, initialization of a static variable (either a global variable or a local variable explicitly declared static) occurs once only, the first time control reaches its declaration. Here's an example in which two local static variables are initialized only once (the first time the function is called):

images

Although an initialization gives a variable an initial value, it is perfectly acceptable to reassign it another value during program execution.

9.3 Interface Design

We return now to the issue of interface design, which we first discussed in Chapter 8. Recall that the data flow through a module interface can take three forms: incoming only (In), outgoing only (Out), and incoming/outgoing (In/out). Any item that can be classified as purely incoming should be coded as a value parameter. Items in the remaining two categories (outgoing and incoming/outgoing) must be reference parameters; the only way the function can deposit results into the caller's arguments is to use the addresses of those arguments. Remember that stream objects passed as parameters must be reference types.

Sometimes it is tempting to skip the interface design step when writing a module, letting it communicate with other modules by referencing global variables. Don't! Without the interface design step, you would actually be creating a poorly structured and undocumented interface. Except in well-justified circumstances, the use of global variables is a poor programming practice that can lead to program errors. These errors are extremely hard to locate and usually take the form of unwanted side effects.

Side Effects

Suppose you made a call to the sqrt library function in your program:

y = sqrt(x);

You expect the call to sqrt to do one thing only: compute the square root of the variable x. You would be surprised if sqrt also changed the value of your variable x because sqrt, by definition, does not make such changes. This outcome would be an example of an unexpected and unwanted side effect.

Side effects are sometimes caused by a combination of reference parameters and careless coding in a function. Perhaps an assignment statement in the function stores a temporary result into one of the reference parameters, accidentally changing the value of an argument back in the calling code. As we mentioned earlier, use of value parameters avoids this type of side effect by preventing the change from reaching the argument. We saw these errors demonstrated in the last Software Maintenance Case Study.

Side effects also can occur when a function accesses a global variable. For example, forgetting to declare a local variable that has name precedence over a global variable with the same name is another source of side effect errors. While you may think the function is accessing a local variable, it is actually changing a global variable. The error doesn't appear until after the function returns and main or some other function makes use of the value left in the global variable. Because of how nonlocal scope works in C++, every function interface has the potential to produce this kind of unintended side effect.

The symptoms of side-effect errors are especially misleading about their source because the trouble shows up in one part of the program when it really is caused by something in another part that may be completely unrelated. Every programmer has had the experience of spending several days attempting to isolate a bug that makes no sense at all, and even seems to violate the rules of the programming language. When you begin to think that an error must be the compiler's fault, it's a good bet that you're really looking at a side-effect bug!

Given their challenging nature, avoiding such errors is extremely important. The only external effect that a module should have is to transfer information through the well-structured interface of the parameter list (see FIGURE 9.2). To be more explicit: The only variables used in a module should be either parameters or local. In addition, incoming-only parameters should be value parameters. If these steps are taken, then each module is essentially isolated from other parts of the program and side effects cannot occur. When a module is free of side effects, we can treat it as an independent module and reuse it in other programs. It is hazardous or impossible to reuse modules with side effects.

images

FIGURE 9.2 Side Effects

The following program, called Trouble, runs but produces incorrect results because of global variables and side effects:

images

Program Trouble is supposed to count and print the number of integers on each line of input. As a final output, it should print the number of lines. But each time the program is run, it reports that the number of lines of input is the same as the number of integers in the last line. Your first thought would be to look closely at main for a logic error in keeping track of the number of lines. But the algorithm in main is fine. Where could the bug be? The problem is that the CountInts function uses the global variable count to store the number of integers on each input line. The programmer probably intended to declare a local variable called count and forgot.

Furthermore, there is no reason for count to be a global variable. If a local variable count had been declared in main, then the compiler would have reported that CountInts was using an undeclared identifier. With local variables called count declared in both main and CountInts, the program works correctly. There is no conflict between the two variables, because each is visible only inside its own block.

This side-effect error was easy to locate because we had just one other function to examine. Of course, in a program with hundreds of functions, it would be much more challenging to determine which one was causing main to behave strangely.

The Trouble program also demonstrates one common exception to the rule of not accessing global variables. Technically, cin and cout are global objects declared in the header file iostream. The CountInts function reads and writes directly to these streams. To be absolutely correct, cin and cout should be passed as arguments to the function. However, cin and cout are fundamental I/O facilities supplied by the standard library, and by convention C++ functions access them directly.

Global Constants

Contrary to what you might think, it is acceptable to reference named constants globally. Because the values of global constants cannot be changed while the program is running, no side effects can occur.

There are two advantages to referencing constants globally: ease of change and consistency. If you need to change the value of a constant, it's easier to change only one global declaration than to change a local declaration in every function. By declaring a constant in only one place, we also ensure that all parts of the program use exactly the same value.

This is not to say that you should declare all constants globally. If a constant is needed in only one function, then it makes sense to declare it locally within that function.

At this point, you may want to turn to the Problem-Solving Case Study at the end of this chapter, which further illustrates interface design and the use of value and reference parameters.

9.4 Value-Returning Functions

In Chapter 8 and the first part of this chapter, we have been writing our own void functions. We now look at the second kind of subprogram in C++, the value-returning function. You are already familiar with several value-returning functions supplied by the C++ standard library: sqrt, abs, fabs, and others. From the caller's perspective, the main difference between void functions and value-returning functions is the way in which they are called. A call to a void function is a complete statement; a call to a value-returning function is part of an expression.

From a design perspective, value-returning functions are used when a function will return only one result and that result is to be used directly in an expression. For example, the C++ standard library provides a power function, pow, that raises a floating-point number to a floating-point power. This library does not supply a power function for int values, however, so let's build one of our own. The function receives two integers, number and n (where n is greater than or equal to 0), and computes numbern. We use a simple approach to this calculation, multiplying repeatedly by number. Because the number of iterations is known in advance, a count-controlled loop is appropriate for implementing our new function. The loop counts down to 0 from the initial value of n. For each iteration of the loop, number is multiplied by the previous product.

images

The first thing to note is that the function definition looks like a void function, except for the fact that the heading begins with the data type int instead of the word void. The second thing to observe is the Return statement at the end, which includes an integer expression between the word return and the semicolon.

A value-returning function returns one value—not through a parameter, but rather by means of a Return statement. The data type at the beginning of the heading declares the type of value that the function returns. This data type is called the function type, although a more precise term is function value type (or function return type or function result type).

The program shown in FIGURE 9.3 invokes function Power. The last statement in the Power function returns the result as the function value.

images

FIGURE 9.3 Returning a Function Value to the Expression That Called the Function

You now have seen two forms of the Return statement. The form

return;

is valid only in void functions. It causes control to exit the function immediately and return to the caller. In contrast, the form

return Expression;

is valid only in value-returning functions. It returns control to the caller, sending back the value of Expression as the function value. (If the data type of Expression is different from the declared function type, its value is coerced to the correct type.)

In Chapter 8, we presented a syntax template for the function definition of a void function. We now update the syntax template to cover both void functions and value-returning functions:

images

If DataType is the word void, the function is a void function; otherwise, it is a value-returning function. Notice from the shading in the syntax template that DataType is optional. If you omit the data type of a function, a data type of int is assumed. We mention this point only because you sometimes encounter programs where DataType is missing from the function heading. Many programmers consider this practice to be poor programming style.

The parameter list for a value-returning function has exactly the same form as for a void function: a list of parameter declarations, separated by commas. Also, a function prototype for a value-returning function looks just like the prototype for a void function except that it begins with a data type instead of void.

Remember your friend's program that was so buggy? The function LineCount had a reference parameter that was used to return the number of lines in the file. This function should be written as a value-returning function, where the return value is the number of lines. The change requires the following steps:

1. Replace the word void with the word int in the prototype for function LineCount.

2. Remove count from the parameter list.

3. Make the same changes in the function definition.

4. Declare a local variable to keep a count of the number of lines.

5. Return the value of the line count variable.

6. Remove the declaration of count from the main program.

7. Insert the call to LineCount into the output statement.

8. Correct the documentation.

  Here is this program with LineCount with these steps taken. The changed lines are shaded.

images

Complete Example

Let's look at another problem. Suppose we are writing a program that calculates a prorated refund of tuition for students who withdraw in the middle of a semester. The amount to be refunded is the total tuition times the remaining fraction of the semester (the number of days remaining divided by the total number of days in the semester). The people who use the program want to be able to enter the dates on which the semester begins and ends and the date of withdrawal, and they want the program to calculate the fraction of the semester that remains.

Because each semester at this particular school begins and ends within one calendar year, we can calculate the number of days in a period by determining the day number of each date and subtracting the starting day number from the ending day number. The day number is the number associated with each day of the year if you count sequentially from January 1. December 31 has the day number 365, except in leap years, when it is 366. For example, if a semester begins on 1/3/09 and ends on 5/17/09, the calculation is as follows:

The day number of 1/3/09 is 3
The day number of 5/17/09 is 137
The length of the semester is 137 2 3 + 1 = 135

We add 1 to the difference of the days because we count the first day as part of the period.

The algorithm for calculating the day number for a date is complicated by leap years and by months of different lengths. We could code this algorithm as a void function named ComputeDay. The refund could then be computed by the following code segment:

images

The first three arguments to ComputeDay are received by the function, and the last one is returned to the caller. Because ComputeDay returns only one value, we can write it as a value-returning function instead of a void function.

Let's look at how the calling code would be written if we had a value-returning function named Day that returned the day number of a date in a given year:

images

This second version of the code segment is much more intuitive. Because Day is a value-returning function, you know immediately that all its parameters receive values and that it returns just one value (the day number for a date).

Let's look at the function definition for Day. Don't worry about how Day works; for now, you should concentrate on its syntax and structure.

images

See how useful it is to have a collection of functions that we can use later? We created the IsLeapYear function in Chapter 1. We can now use it here without bothering to see how the function works. We can embed function Day in a driver (test) program that prompts for and reads three dates and prints the tuition refund. Here we just set the tuition as a constant, although in the real program it would be input.

images

Here is the output:

images

Boolean Functions

Value-returning functions are not restricted to returning numerical results, as the previous example shows. Boolean functions can be useful when a branch or loop depends on some complex condition. Rather than code the condition directly into the If or While statement, we can call a Boolean function to form the controlling expression.

Suppose we are writing a program that works with triangles. The program reads three angles as floating-point numbers. Before performing any calculations on those angles, however, we want to check that they really form a triangle by adding the angles to confirm that their sum equals 180 degrees. We can write a value-returning function that takes the three angles as parameters and returns a Boolean result. Such a function would look like this (recall from Chapter 5 that you should test floating-point numbers only for near-equality):

images

The following program shows how the IsTriangle function is called. (The function definition is shown without its documentation to save space.)

images

Here is the output:

images

In the main function of the Triangle program, the If statement is much easier to understand with the function call than it would be if the entire condition were coded directly. When a conditional test is at all complicated, a Boolean function is in order.

Interface Design and Side Effects

The interface to a value-returning module is designed in much the same way as the interface to a void module. We simply write down a list of what the module needs and what it must return. Because value-returning modules return only one value, there is only one item labeled “Out” in the list: the module return value. Everything else in the list is labeled “In,” and there aren't any items labeled “In/out.”

Returning more than one value from a value-returning module (by modifying the caller's arguments) is a side effect and, as such, should be avoided. If your interface design calls for multiple values to be returned, then you should use a void function instead of a value-returning function.

A rule of thumb is to avoid reference parameters in the parameter list of a value-returning module, and to use value parameters exclusively. Let's look at a function that demonstrates the importance of this rule. Suppose we define the following function:

images

This function returns the square of its incoming value, but it also increments the caller's argument before returning. Now suppose we call this function with the following statement:

y = x + SideEffect(x);

If x is originally 2, what value is stored into y? The answer depends on the order in which your compiler generates code to evaluate the expression. If the compiled code calls the function first, then the answer is 7. If it accesses x first in preparation for adding it to the function result, then the answer is 6. This uncertainty is precisely why you should not use reference parameters with value-returning functions. A function that causes an unpredictable result has no place in a well-written program.

An exception to this rule is when a module is being designed for C++ implementation using an I/O stream object as a parameter. Recall that C++ requires stream objects to be passed as reference parameters. Keep in mind that reading from or writing to a file within a function is really a side effect. If you choose to go this route, be sure that your decision is clearly documented in the postcondition.

There is another advantage to using only value parameters in a value-returning function definition: You can use constants and expressions as arguments. For example, we can call the IsTriangle function using literals and other expressions:

images

When to Use Value-Returning Functions

There aren't any formal rules that specify when to use a void function and when to use a value-returning function, but here are some guidelines:

1. If the module must return more than one value or modify any of the caller's arguments, do not use a value-returning function.

2. Avoid using value-returning functions to perform I/O. If you must, clearly document the side effect in the postcondition.

3. If only one value is returned from the module and it is a Boolean value, a value-returning function is appropriate.

4. If only one value is returned and it is to be used immediately in an expression, a value-returning function is appropriate.

5. When in doubt, use a void function. You can recode any value-returning function as a void function by adding an extra outgoing parameter to carry back the computed result.

6. If both a void function and a value-returning function are acceptable, use the form you feel more comfortable implementing.

 Value-returning functions were included in C++ to provide a way of simulating the mathematical concept of a function. The C++ standard library supplies a set of commonly used mathematical functions through the header file cmath. A list of these appears in Appendix C.

9.5 Type Coercion in Assignments, Argument Passing, and Return of a Function Value

In general, promotion of a value from one type to another does not cause loss of information. Think of promotion as the process of moving your baseball cards from a small shoe box to a larger shoe box. All of the cards still fit into the new box and there is room to spare. By comparison, demotion (or narrowing) of data values can potentially cause loss of information. Demotion is like moving a shoe box full of baseball cards into a smaller box—something has to be thrown out.

Consider the assignment operation

v = e

where v is a variable and e is an expression. Regarding the data types of v and e, there are three possibilities:

1. If the types of v and e are the same, no type coercion is necessary.

2. If the type of v is “higher” than that of e, then the value of e is promoted to v's type before being stored into v.

3. If the type of v is “lower” than that of e, the value of e is demoted to v's type before being stored into v.

 Demotion, which you can think of as shrinking a value, may cause loss of information:

images Demotion from a longer integral type to a shorter integral type (such as from long to int) results in discarding the leftmost (most significant) bits in the binary number representation. The result may be a drastically different number.

images Demotion from a floating-point type to an integral type causes truncation of the fractional part (and an undefined result if the whole-number part will not fit into the destination variable). The result of truncating a negative number varies from one machine to another.

images Demotion from a longer floating-point type to a shorter floating-point type (such as from double to float) may result in a loss of digits of precision.

 Our description of type coercion in an assignment operation also holds for argument passing (the mapping of arguments onto parameters) and for returning a function value with a Return statement. For example, assume that INT_MAX on your machine is 32767 and that you have the following function:

images

If the function is called with the statement

DoSomething(50000);

then the value 50000 (which is implicitly of type long because it is larger than INT_MAX) is demoted to a completely different, smaller value that fits into an int location. In a similar fashion, execution of the function

images

causes demotion of the value 70000 to a smaller int value because int is the declared type of the function return value.

One interesting consequence of implicit type coercion is the futility of declaring a variable to be unsigned, in hopes that the compiler will prevent you from making a mistake like this:

unsignedVar = -5;

The compiler does not complain at all. It generates code to coerce the int value to an unsigned int value. But if you now print out the value of unsignedVar, you'll see a strange-looking positive integer. As we have pointed out before, unsigned types are most appropriate for advanced techniques that manipulate individual bits within memory cells. It's best to avoid using unsigned for ordinary numeric computations.

To be safe, avoid implicit coercion whenever you can!

Testing and Debugging

One of the advantages of a modular design is that you can test the program 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, it must be called by something (unless it is main). 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 functions called stubs to satisfy those calls.

A stub usually consists of an output statement that prints a message such as “Function such-and-such just got called.” Even though the stub is a dummy, it allows us to determine whether the function is called at the right time by main or another function.

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

Here is a stub that simulates the Name function in the Profile program by returning an arbitrarily chosen string:

images

This stub is simpler than the function it simulates, which is typical because the object of using a stub is to provide a simple, predictable environment for testing a module.

In addition to supplying a stub for each call within the module, you must 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. You write a test plan for a module, and then a driver implements that test plan.

Here is a test plan and a driver for the EvaluateCholesterol function in the Profile program. This test plan is based on the interpretation guidelines used to write the function. Each category is taken once. Case Study Follow-Up Exercise 1 asks you to determine if this test plan is sufficient.

images

images

images

images

The driver calls the function eight times, which allows the points in the If statements to be executed. Here are the input and the output for this code:

Input

images

Output

images

images

Stubs and drivers are important tools in a team programming environment. With this approach, the programmers develop the overall design and the interfaces between the modules. Each programmer then designs and codes one or more of the modules and uses drivers and stubs to test the code. When all of the modules have been coded and tested, they are assembled into what should be a working program.

For team programming to succeed, it is essential that all of the module interfaces be defined explicitly and that the coded modules adhere strictly to the specifications for those interfaces. Obviously, global variable references must be carefully avoided in a team-programming situation because it is impossible for each person to know how the rest of the team is using every variable.

Testing and Debugging Hints

1. Make sure that variables used as arguments to a function are declared in the block where the function call is made.

2. Carefully define the precondition, postcondition, and parameter list to eliminate side effects. Variables used only in a function should be declared as local variables. Do not use global variables in your programs. (Exception: It is acceptable to reference cin and cout globally.)

3. If the compiler displays a message such as UNDECLARED IDENTIFIER, check that the identifier isn't misspelled (and that it is, in fact, declared), that the identifier is declared before it is referenced, and that the scope of the identifier includes the reference to it.

4. If you intend to use a local name that is the same as a nonlocal name, be aware that a misspelling in the local declaration will wreak havoc. The C++ compiler won't complain, but will cause every reference to the local name to go to the nonlocal name instead.

5. Remember that the same identifier cannot be used in both the parameter list and the outermost local declarations of a function.

6. With a value-returning function, be sure the function heading and prototype begin with the correct data type for the function return value.

7. With a value-returning function, don't forget to use a statement

images

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 value-returning function is part of an expression, whereas a call to a void function is a separate statement. (C++ softens this distinction, however, by letting you call a value-returning function as if it were a void function, ignoring the return value. Be careful here.)

9. In general, don't use reference parameters in the parameter list of a value-returning function. A reference parameter must be used, however, when an I/O stream object is passed as a parameter.

10. If necessary, use your system's debugger (or use debug output statements) to determine when a function is called and if it is executing correctly. The values of the arguments can be displayed immediately before the call to the function (to show the incoming values) and immediately after the call completes (to show the outgoing values). You also may want to display the values of local variables in the function itself to indicate what happens each time it is called.

images  Summary

The scope of an identifier refers to the parts of the program in which it is visible. C++ function names have global scope, as do the names of variables and constants that are declared outside all functions and namespaces. Variables and constants declared within a block have local scope; they are not visible outside the block. The parameters of a function have the same scope as local variables declared in the outermost block of the function.

   With rare exceptions, it is not considered good practice to declare global variables and reference them directly from within a function. All communication between the modules of a program should be through the argument and parameter lists (and via the function value sent back by a value-returning function). The use of global constants, by contrast, is considered to be an acceptable programming practice because it adds consistency and makes a program easier to change while avoiding the pitfalls of side effects. Well-designed and well-documented functions that are free of side effects can often be reused in other programs. Many programmers, in fact, keep a library of functions that they use repeatedly.

   The lifetime of a variable is the period of time during program execution when memory is allocated to it. Global variables have a static lifetime: Memory remains allocated for the duration of the program's execution. By default, local variables have an automatic lifetime: Memory is allocated and deallocated at block entry and block exit. A local variable may be given static lifetime by using the word static in its declaration. This variable has the lifetime of a global variable but the scope of a local variable.

   C++ allows a variable to be initialized in its declaration. For a static variable, the initialization occurs once only—when control first reaches its declaration. An automatic variable is initialized each time control reaches the declaration.

   C++ provides two kinds of subprograms, void functions and value-returning functions. A value-returning function is called from within an expression and returns a single result that is used in the evaluation of the expression. For the function value to be returned, the last statement executed by the function must be a Return statement containing an expression of the appropriate data type.

   All the scope rules, as well as the rules about reference and value parameters, apply to both void functions and value-returning functions. It is considered poor programming practice, however, to use reference parameters in a value-returning function definition. Doing so increases the potential for unintended side effects. (An exception is when I/O stream objects are passed as parameters. Other exceptions are noted in later chapters.)

   We can use stubs and drivers to test functions in isolation from the rest of a program. They are particularly useful in the context of team-programming projects.

images  Quick Check

1. If a function references a variable that is not declared in its block or its parameter list, is the reference global or local? (pp. 394–395)

2. How do references to global variables contribute to the potential for unwanted side effects? (pp. 408–411)

3. If a module has three In parameters and one Out parameter, should it be implemented as a void function or a value-returning function? (p. 423)

4. A program has two functions, Quick1 and Quick2. The program itself declares variables check and qc. Function Quick1 declares variables called quest and qc. Function Quick2 declares one variable called quest and a static variable called forever. Which of these variables are local? (pp. 394–402)

5. In Question 4, which variables are accessible within function Quick1? (pp. 394–402)

6. In Question 4, what is the lifetime of each of the six variables? (pp. 402–404)

7. What distinguishes a value-returning function from a void function? (pp. 413–415)

8. Given the following function heading, how would you write a call to it that will pass it the value 98.6 and assign the result to the variable fever? (pp. 413–415)

images

images  Answers

1. Global. 2. They enable a function to affect the state of the program through a means other than the well-defined interface of the parameter list. 3. A value-returning function. 4. Variables declared within the functions are local. In Quick1, quest and qc. In Quick2, quest and forever. 5. check, quest, and the locally declared qc. 6. check and qc in the program, together with forever, are static. The variables in Quick1 and the variable quest in Quick2 are automatic. 7. Using a type name in place of void, and using a Return statement to pass a value back to the caller. 8. fever = TempCheck(98.6);

images  Exam Preparation Exercises

1. A function parameter is local to the entire block that is the body of the function. True or false?

2. A reference parameter has the same scope as a global variable. True or false?

3. A global variable can be referenced anywhere within the program. True or false?

4. Function names have global scope. True or false?

5. Match the following terms with the definitions given below.

a. Scope

b. Name precedence

c. Scope rules

d. Nonlocal identifier

e. Lifetime

f. Automatic variable

g. Static variable

h. Side effect

i. The semantics that specify where we can reference nonlocal identifiers.

ii. A variable for which memory is allocated for the duration of the program.

iii. When one function affects another function in a manner that isn't defined by their interface.

iv. The precedence that a local identifier has over a global identifier with the same name.

v. The region of program code where it is legal to reference an identifier.

vi. A variable that has memory allocated at block entry and deallocated at block exit.

vii. An identifier declared outside of the current block.

viii. The period in which an identifier has memory allocated to it.

6. Identify the side effect in the following function.

images

7. Identify the side effect in the following program (which uses poor style for naming variables).

images

images

8. What is the scope of a namespace that is specified in a using directive outside of all functions?

9. What is the scope of the std namespace in the following code?

images

10. What is the lifetime of each of the following?

  a. A global variable.

  b. A local variable in a function.

  c. A local, static variable in a function.

11. Rewrite the following declaration and initialization as a single statement.

images

12. If a local, static variable is initialized in its declaration within a function, when does the variable get initialized and how often?

13. If a local, nonstatic variable is initialized in its declaration within a function, when does the variable get initialized and how often?

14. A value-returning function can have just one Return statement. True or false?

15. What's wrong with the following function?

images

16. What's wrong with the following function?

images

17. What's wrong with the following function?

images

18. Using a reference parameter in a value-returning function makes which kind of programming error more likely?

images  Programming Warm-Up Exercises

1. The following program is written in a very poor style that uses global variables instead of parameters and arguments, resulting in unwanted side effects. Rewrite it using good style.

images

2. Write the heading for a bool function Equals that has two value float parameters, x and y.

3. Write the function prototype for the function in Exercise 2.

4. Write a body for the function in Exercise 2 that compares x and y, returning true if their difference is less than 0.00000001, and false otherwise.

5. Write the heading and function prototype for a float function called ConeVolume that takes two value float parameters, radius and height.

6. Write the body for the function heading in Exercise 5. The body computes the volume of a cone using the following formula:

images

7. Rewrite the void function described in Programming Warm-Up Exercises 4 and 6 in Chapter 8 as a value-returning function. The function, which is called GetLeast, takes an ifstream parameter called infile as an input parameter that is changed. It returns an int value that is the lowest value read from the file.

8. Rewrite the void function called Reverse that is described in Programming Warm-Up Exercises 8 and 10 in Chapter 8 as a value-returning function. It should take a string parameter as input. The function returns a string that is the character-by-character reverse of the string in the parameter. The parameter is called original.

9. Rewrite the void function called LowerCount that is described in Programming Warm-Up Exercise 12 of Chapter 8 as a value-returning function. The function reads a line from cin, and returns an int containing the number of lowercase letters in the line. In Appendix C, you will find the description of function islower, which returns true if its char parameter is a lowercase character.

10. Write a value-returning float function called SquareKm that takes two float parameters, length and width, and outputs a return value that is in square kilometers. The parameters are the length and width of an area in miles. The conversion factor for kilometers from miles is 1.6.

11. Write a value-returning bool function called Exhausted that takes an int parameter called filmRolls. The function keeps track of how many prints have been processed by the chemistry in a photo-processing machine. When the total number of prints exceeds 1000, it returns true. Each time the function is called, the value in the parameter is added to the total. When the total exceeds 1000, the variable containing the total is reset to zero before the function returns, under the assumption that the exhausted chemistry will be replaced before more prints are processed.

12. Write a value-returning string function called MonthAbbrev that takes an int value as a parameter. The parameter, month, represents the number of the month. The function returns a string containing the three-letter abbreviation for the corresponding month number. Assume that the month number is in the range of 1 to 12.

13. Modify the function in Exercise 12 to handle month numbers that are not in the valid range by returning the string “Inv”.

14. Write a value-returning float function called RunningAvg that takes a float variable, value, as its input and returns the running average of all the values that have been passed to the function since the program first called it.

images   Programming Problems

1. You are working on a project that requires climate data for a location. The maximum daily change in barometric pressure is one aspect of climate that your company needs. You have a file (barometric.dat) containing hourly barometer readings taken over the course of a year. Each line of the file contains the readings for a single day, separated by blanks. Each reading is expressed in inches of mercury, so it is a decimal number ranging from approximately 28.00 to 32.00. For each line of data, you need to determine the maximum and minimum readings, and output the difference between those readings to file differences.dat. Each output value should appear on a separate line of the file. Once the file has been read, the program should output the greatest and least differences for the year on cout. Develop the program using functional decomposition, and use proper style and documentation in your code. Your program should make appropriate use of value-returning functions in solving this problem.

2. Extend the program in Problem 1 so that it also outputs the maximum and minimum barometric readings for each day on file differences.dat, and then outputs the maximum and minimum readings for the year on cout at the end of the run.

3. You're working for a lumber company, and your employer would like a program that calculates the cost of lumber for an order. The company sells pine, fir, cedar, maple, and oak lumber. Lumber is priced by board feet. One board foot equals one square foot, one inch thick. The price per board foot is given in the following table:

Pine 0.89
Fir 1.09
Cedar 2.26
Maple 4.50
Oak 3.10

The lumber is sold in different dimensions (specified in inches of width and height, and feet of length) that need to be converted to board feet. For example, a 2 3 4 3 8 piece is 2 inches wide, 4 inches high, and 8 feet long, and is equivalent to 5.333 board feet. An entry from the user will be in the form of a letter and four integer numbers. The integers are the number of pieces, width, height, and length. The letter will be one of P, F, C, M, O (corresponding to the five kinds of wood) or T, meaning total. When the letter is T, there are no integers following it on the line. The program should print out the price for each entry, and print the total after T is entered. Here is an example run:

images

Develop the program using functional decomposition, and use proper style and documentation in your code. Your program should make appropriate use of value-returning functions in solving this problem. Be sure that the user prompts are clear, and that the output is labeled appropriately.

4. Write a program that determines the day of the week for a given date. You can invent your own complex algorithm that takes into account the special leap year rules, and changes in calendars, but this is a case where it makes sense to look for things that are familiar. Who else might need to compute values from dates over a wide span of time? Historians work with dates, but generally don't compute from them. Astronomers, however, need to know the difference in time between orbital events in the solar system that span hundreds of years. Consulting an astronomy text, you will find that there is a standard way of representing a date, called the Julian Day Number (JDN). This value is the number of days that have elapsed since January 1, 4713 b.c. Given the JDN for a date, there is a simple formula that tells the day of the week:

images

The result is in the range of 0 to 6, with 0 representing Sunday.

   The only remaining problem is how to compute the JDN, which is not so simple. The algorithm computes several intermediate results that are added together to give the JDN. We look at the computation of each of these three intermediate values in turn.

   If the date comes from the Gregorian calendar (later than October 15, 1582), then compute intRes1 with the following formula; otherwise, let intRes1 be zero.

 intRes1 = 2 - year / 100 + year / 400 (integer division)

The second intermediate result is computed as follows:

        intRes2 = int(365.25 * Year)

We compute the third intermediate value with this formula:

       intRes3 = int(30.6001 * (month + 1))

Finally, the JDN is computed this way:

       JDN = intRes1 + intRes2 + intRes3 + day + 1720994.5

Your program should make appropriate use of value-returning functions in solving this problem. These formulas require nine significant digits; you may have to use the integer type long and the floating-point type double. Your program should prompt the user appropriately for input of the date; it should also properly label the output. Use proper coding style with comments to document the algorithm as needed.

5. Reusing functions from Problem 4 as appropriate, write a C++ program that computes the number of days between two dates. If your design for Problem 4 used good functional decomposition, this should be a trivial program to write. You merely need to input two dates, convert them to their JDNs, and take the difference of the JDNs.

    Your program should make appropriate use of value-returning functions in solving this problem. These formulas require nine significant digits; you may have to use the integer type long and the floating-point type double. Your program should prompt the user appropriately for input of the date; it should also properly label the output. Use proper coding style with comments to document the algorithm as needed.

images   Case Study Follow-Up

1. Was the test plan outlined for function EvaluateCholesterol complete? If not, why not?

2. The BMI measure is calculated as an integer. Is this sufficient? Would it be a better idea to make this a real value? What would have to be changed to do so?

3. Write the algorithm for the Evaluate Blood Pressure module.

4. Write a test plan and a driver to test function EvaluateBloodPressure.

5. Add a Boolean function to the three Evaluate modules, which is true if the data are okay and false otherwise. Test the input within each module for negative or zero input values, which should be considered errors. If an error occurs, state in which module it occurs and set the error flag.

6. Rewrite the main program to test the flag set in Exercise 5 after each call and exit the program if an error occurs.


1. C++ provides syntax to work around name precedence and access a global identifier even when an identifier with the same name is declared locally. We can use the :: operator without specifying a namespace. For example, if we have declared a local constant called A1, we can write ::A1 to refer to a global constant called A1.

2. As a general concept, namespace is another word for scope. Here we consider it as a specific C++ language feature.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.225.113