0% found this document useful (0 votes)
2 views18 pages

Chapter 9 Class Notes

Chapter 9 covers the scope and lifetime of variables in C++, including the rules for identifier access, the concept of global and local scope, and the importance of avoiding side effects in function design. It explains how local variables are only accessible within their block, while global variables can be accessed throughout the program, and emphasizes the significance of using value-returning functions correctly. Additionally, the chapter discusses namespaces, variable lifetime, and the design of interfaces to prevent unintended side effects in programming.

Uploaded by

turfs247
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views18 pages

Chapter 9 Class Notes

Chapter 9 covers the scope and lifetime of variables in C++, including the rules for identifier access, the concept of global and local scope, and the importance of avoiding side effects in function design. It explains how local variables are only accessible within their block, while global variables can be accessed throughout the program, and emphasizes the significance of using value-returning functions correctly. Additionally, the chapter discusses namespaces, variable lifetime, and the design of interfaces to prevent unintended side effects in programming.

Uploaded by

turfs247
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Chapter 9 - Scope, Lifetime, and More on Functions - Class Notes

This chapter, we have the following learning objections.

• Examine the C++ rules by which a function may access identifiers that are declared outside its own block.
• Know what a global reference is.
• Learn more about how a value-returning function returns a single result – the function value – to the
expression form which it was called.
• To understand and be able to avoid unwanted side effects.
• To know when to use a value-returning functions.
You should be able to:

• Determine which variables in a program are local.


• Determine which variables are accessible in a given block.
• Determine the lifetime of each variable in a program.
• Design and code a value-returning functions for a specific task (learn how to write user-defined value-
returning functions.)
• Invoke a value-returning function properly.

9.1 Scope of Identifiers


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:
if (alpha > 3)
{
int n; // n is defined here
cin >> n;
beta = beta + n;
} // n becomes undefined here

Like any other local variable, n cannot be accessed by any statement outside the block containing its declaration.

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.

Categories of Scope for any Identifier:


1. Class scope. To be discussed in Chapter 12.
2. Local scope. The scope of an indentifier declared inside a block extends from the point of declaration to
the end of that block. Same as function parameters.
3. Global scope. The scope of an indentifier declared outside all functions and classes extends from the
point of declaration to the end of the file containing the program code.

C++ function names have global scope. Once a function name has been declared, it can be invoked by another
function in the rest of the program. In C++, there is no such thing as a local function. You cannot nest a function
definition inside another function definition.

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

int main ()
{
gamma = 3;
……
}

void SomeFunc ()
{
gamma = 5;
……
}

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

Name precedence. The precedence that a local identifier in a function has over a global identifier with the same
name in any references that the function makes to that identifier; also called name hiding.

>>>> Let’s type and run a program using both local and global declarations and analyze it. p. 403-404.

In the example below, function SomeFunc access global constant A but declares its own local variable b and
parameter c. So, 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. The rules for accessing identifiers that
aren’t declared locally are called scope rules.
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:
1. A function name has global scope. Function definitions cannot be nested within function definitions.
2. The scope of a function parameter is identified 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 those 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 over global identifiers).
Let’s review the below sample program that demonstrates the C++ scope rules. Note how the While loop body
labeled Block3, located within function Block2, contains its own local variable declarations.
// This program shell demonstrates scope.

#include <iostream>
using namespace std;

void Block1(int, char&);


void Block2 ();

int a1; // One global variable


char a2; // Another global variable

int main ()
{
…..
}

//***************************************************
void Block1 (int a1, // Prevents access to global a1
char& b2) // Has same scope as c1 and d2

{
int c1; // A variable local to Block1
int d2; // Another variable local to Block1
……
}

//****************************************************
void Block2 ()
{
int a1; // Local to Block2; prevents access to global a1
int b2; // Local to Block2; no conflict with b2 in Block1

while (……)
{ // Block3
int c1; // Local to Block3; no conflict with c1 in Block1
int b2; // Local to Block 3; Prevents nonlocal access to b2 in
// Block 2; no conflicts with b2 in Block1
……
}
}

Anything inside a box can refer to anything in a larger surrounding box. Thus, Block 3 could access any
identifier declared in Block 2 or any global variable. A statement in Block 3 could not access identifiers declared
in Block 1 because it would have to enter Block 1 from outside.

Function names are global since they are declared outside of the function blocks.

Visible is often used in describing a scope of access.

Remember, local identifiers have name precedence which was rule 5. See variable a1 is declared in (3) various
locations. Because of the name precedence rule, Block 2 and Block 3 access the a1 declared in Block 2 rather
than the global a1. Similarly, the scope of variable b2 is declared in Block 2 does not include the declaration of
variable b2 in Block 3.

Name precedence rules are implemented by the compiler. It first searches local declarations then goes out until it
locates the variable. It stops once that variable is found so it does not continue to the outer areas. If it does not
find that variable then it generates an error message UNDECLARED IDENTIFIER.
Variable Declarations and Definitions
C++ has a reserved word extern that lets you reference a global variable located in another file. A definition like..
int someInt;
causes the compiler to reserve 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.

Namespaces
What exactly is namespace? It is a mechanism by which the programmer can create a named scope. For
example, the standard header file cstdlib contains function prototypes for several library functions, one of which
is the absolute value function, abs. The declaration are contained within a namespace definition as follows:
// In header file cstdlib:
namespace std
{
……
int abs(int);
…..
}

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 be namespace
scope. Such as identifiers cannot be accessed outside the body expect by using one of the below (3) 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:
#include <cstdlib>
int main (

{
int alpha;
int beta;
…..
alpha = std :: abs(beta); // A qualified name referring to abs
std :: cout << alpha; // A qualified name referring to cout
…..
}

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:

#include <cstdlib>
int main ()
{
int alpha;
int beta;
using std :: abs; // A using declaration for abs
using std :: cout; // A using declaration for cout
…..
alpha = abs(beta);
cout << alpha;
……
}

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):

#include <cstdlib>
int main ()
{
int alpha;
int beta;
using namespace std; // A using directive
……
alpha = abs(beta);
cout << alpha;
…..
}

With a using directive, all identifiers from the specified namespace are accessible, but only in the scope which the
using directive appears. In the previous fragment, the using directive is in local scope, so identifiers from the std
namespace are accessible only within main.

When we put the using directive outside all functions like we normally do then the using directive is in global
scope; thus, identifiers from std namespace are accessible globally. This is the most convenient method.

#include <cstdlib>
using namespace std;
int main ()
{
…..
}

So, why are we covering this information?


Why don’t we just always implement the using directive in a global scope in this manner?

Creating global using directives is considered a bad idea when we are creating large, multifile programs, where
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 will still use global namespace since our programs are
small.
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.

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: These are general descriptions and not rules. They don’t account for name hiding either (like Rule 5 –
were local identifiers have name precedence.)

9.2 Lifetime of a Variable


Lifetime – the period of time during program execution when an identifier actually has memory allocated to it.

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.

Lifetime is a run-time issue instead of a compile time concern (like the scope variable). Different than scope
which focused on ….? compile-time concerns.

• An automatic variable is one whose storage is allocated at block entry and deallocated at block exit.
• Variable declared within a block are automatic variables.

• A static variable is one whose storage remains allocated for the duration of the entire program.
• All global variables are static variables.
• You can use the reserved word static when you declare a local variable.

>>>>>>>>>>>>>> Let’s write a program which has a function that keeps track of the number of times it is
called. p. 410 – 411. It will demonstrate the use of a static variable.
Usually, it is better to declare a local variable as static than to use a global variable.
The memory for a static variable remains allocated throughout the lifetime of the entire program.
Its local scope prevents other functions in the program for change it.
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. Below is an example of two local static variables that are
initialized only once (the first time the function is called):
void AnotherFunc (int param)
{
static char ch = ‘A’; // Initialized once only
static int m = param +1; // Initialized once only
……
}
Although an initialization gives the variable an initial value, it is acceptable to reassign it another value during the
program execution.

9.3 Interface Design


Remember from Chapter 8, the data flow through a module interface can take three forms:
1. incoming only (In)
2. outgoing only (Out)
3. incoming/outgoing (In/out)
Any item that can be classified as incoming should be coded as a value parameter.
Items in the other two categories (outgoing and incoming/outgoing) must be reference parameters; the only way
the function can deposits results into the caller’s arguments is to use the addresses of those argument.
Remember that stream objects passed as parameters must be reference types.
It can be tempting to skip the interface design step when writing a module, letting it communicate with other
modules by referencing global variables. Don’t! A poorly structured and undocumented interface can easily
result in errors which are extremely difficult to locate and usually have unwanted side effects.

Side Effects
A side effect is any effect of one function or another function that is not a part of the explicitly defined interface
between them. ( Unexpected and unwanted program results)
Suppose you made a call to the sqrt library function:
y = sqrt (x);
The call to sqrt was expected to do one thing only: compute the square root of the variable x. You would be
surprised that is could change the value of your variable x because sqrt, by definition, does not make such
changes.
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 parameter,
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.
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 global variable with the same name. It can be changing the global
variable instead of your intentions for the local impact. The error doesn’t appear until after the function return
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.
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. When you begin to think that an error must be the compiler’s fault, it’s a good bet that you are 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. 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. Re-using a module with a side effect within
another part or multiple other locations within a program would be hazardous and destructive.
The below program, called Trouble, runs but produces incorrect results because of global variables and side
effects. ((page 418 – 419)
This program is suppose to do the following:
1. count the number of integers on each line
2. print the integers on each from each line
3. 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. Where is the problem? Main? Subprogram? Order of count? global vs local?
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 local variable called count and forgot. Also, 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 be reported that CountInts was
using an undeclared indentifier. 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 but in a program with hundreds of
functions; it would be much more challenging and time-consuming.
Global Constants
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 (2) advantages to referencing constants globally: 1. ease of change 2. consistency
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.

9.4 Value-Returning Functions


Recall, several value-returning functions supplied by the C++ standard library: sqrt, abs, fabs, etc. From the
caller’s perspective, the main difference between void 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.
Below is an example of an user-defined value-returning function written to function the same as the pow function.
#include <iostream>
using namespace std;

int Power (int, int);

int main ()
{
cout << “5 raised to the 4th power is” << Power (5, 4) << endl;
return 0;
}

int Power (int number, // Base number


int n // Power to raise base to

// This function calculates and returns number to the nth power.


// Pre: n must be greater than or equal to 0

{
int result = 1; // Holds intermediate powers of x
while (n > 0)
{
result = result * number;
n --;
}
return result;
}
Notice function type and the return statement. Unlike a void function…. heading begins with int instead of void.
The return statement at the end includes an integer expression between the word return and the semicolon.

Function value type – the data type of the result returned by a function.
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 the heading declares the type of the value that the function returns.
This data type is called the function type or function value type or function return type or function result type.
The above program invokes function Power. The last statement in the Power function returns the result as the
function value.
Notice the syntax for the return statement is return Expression;
is valid only in value-returning function. It returns control the caller, sending back the value of Expression as the
function value. (If the data type of Expression is different form the declared function type, its value is coerced to
the correct type.)
Syntax Template for the Function Definition of a Value-returning Function
DataType FunctionName (ParameterList)
{
Statement
…..
}
The blue shading in the syntax template above is shows optional parts. If you omit the data type then, it is
assumed to be of int type. Many programmers consider it to be poor programming style to omit the function type.
The parameter list for the value-returning function has exactly the same form a 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.
<<< Let’s write the program on page 424 and analyze it. >>>>
<<< Now, let’s work on a complete example and type in and the program >>>
In this example, we are writing a program that calculates a prorated refund of tuition for students who withdrew in
the middle of the semester. The amount to be refunded is the total tuition (times) the remaining fraction of the
semester days (the number of days remaining /divided by the total number of days in the semester.) Entering the
dates the semester begins and ends and the date of withdrawal, the user expected this program to calculate the
fraction of the semester that remains.
➢ Let’s assume that the semester for this particular school begins and ends within one calendar year. The
day number is the number associated with each day of the year if you count sequentially form January 1
to December 31. For example, if the semester begins on 1/3/09 and ends 5/17/09, the calculations:
o The day number of 1/3/09 is 3
o The day number of 5/17/09 is 137
o The length of the semester is 137 – 3 +1 = 135
o We add 1 to the difference of the days because we count the first day of school.
o Complications = Leap Year and months of different lengths. We could use the Leap Year
function from Chapter 1
o We will code the above algorithm as a void or value-returning function named ComputeDay.
<<< Let’s type, run, and analyze the program code on pages 427 – 428 >>>
Boolean Functions
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.
<<< Let’s learn more by typing, running, and analyzing the program code on pages 429 – 430 >>>

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 condition 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 is 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 returning value.
Everything else in the list is labeled “In” and there aren’t any items labeled “In/out”.

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.
int SideEffect (int& n)
{
int result = n * n;
n++; // Side Effect
return result ;
}

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 why you
should not use reference parameters with value-returning functions. A function that causes an unpredictable
result has no place with 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. Remember 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.

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 below 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 function 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 its 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 results.
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.

9.5 Type Coercion in Assignments, Argument Passing, and Return of a Function Value
General promotion of a value is moving your baseball cards form a small shoe box to a larger shoe box.
Demotion (narrowing) – The conversion of a value from an “higher” type to a “lower” type according to a
programming language’s precedence of data types. Demotion may cause loss of information. It is lick 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 (3) 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.
▪ 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 results may be a
drastically different number.
▪ 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.
▪ Demotion from a longer floating-point to a shorter floating-point type (such as a double to float) may
result in a loss of digits of precision.
It is best to avoid using unsigned for ordinary numeric computations.
To be safe, avoid implicit coercion whenever you can!
Congratulations!!! You have completed all of the course learning topics for CS I (Chapters 1 – 9) !!!!
Computer Science II will use this same textbook and cover Chapters 10 – 17.

You might also like