Chapter 9. Variable Scope and Functions

But in the gross and scope of my opinion, This bodes some strange eruption to our state.

Shakespeare, Hamlet, Act I, Scene I

So far you have been using only global variables. These are variables that can be set or used almost anywhere in the program. In this chapter you learn about other kinds of variables and how to use them. This chapter also tells you how to divide your code into functions. Many aspects of functions are detailed, including function overloading, using functions to build structured programs, and the use of recursive function calls.

Scope and Storage Class

All variables have two attributes, scope and storage class. The scope of a variable is the area of the program where the variable is valid. A global variable is valid from the point it is declared to the end of the program. A local variable’s scope is limited to the block where it is declared and cannot be accessed (set or read) outside that block. A block is a section of code enclosed in curly braces ({ }). Figure 9-1 illustrates the difference between local and global variables.

Local and global variables
Figure 9-1. Local and global variables

It is possible to declare a local variable with the same name as a global variable. Normally, the scope of the variable count (first declaration in Figure 9-2) would be the whole program. The declaration of a second, local count takes precedence over the global declaration inside the small block where the local count is declared. In this block, the global count is said to be hidden. You can also nest local declarations and hide local variables. These “very local” variables have an even smaller and more local scope than the “normal local” variables. (The clarity of the previous sentence gives you some idea why using nesting to hide local variables does not make your program easy to understand.) Figure 9-2 illustrates a hidden variable.

Hidden variables
Figure 9-2. Hidden variables

The variable count is declared both as a local variable and as a global variable. Normally the scope of count (global) would be the entire program, but when a variable is declared inside a block, that instance of the variable becomes the active one for the length of the block. The global count has been hidden by the local count for the scope of this block. The shaded area in the figure shows where the scope of count (global) is hidden.

It is not good programming practice to hide variables. The problem is that when you have the statement:

count = 1;

it is difficult to tell which count you are referring to. Is it the global count—the one declared at the top of main—or the one in the middle of the while loop? It is better to give these variables different names, such as total_count, current_count, and item_count.

The storage class of a variable may be either permanent or temporary. Global variables are always permanent. They are created and initialized before the program starts and remain until it terminates. Temporary variables are allocated from a section of memory called the stack at the beginning of the block. If you try to allocate too many temporary variables, you will get a stack overflow error. The space used by the temporary variables is returned to the stack at the end of the block. Each time the block is entered, the temporary variables are initialized.

The size of the stack depends on the system and compiler you are using. On many Unix systems, the program is automatically allocated the largest possible stack. On other systems, a default stack size is allocated that can be changed by a compiler switch.

Local variables are temporary unless they are declared static.

Tip

static has an entirely different meaning when used with global variables. (It indicates that a variable is local to the current file.) See Chapter 23. For a complete discussion of the many meanings of the word static, see Table 14-1.

Example 9-1 illustrates the difference between permanent and temporary variables. We have chosen obvious variable names; temporary is a temporary variable and permanent is permanent. C++ initializes temporary each time it is created (at the beginning of the for statement block); permanent gets initialized only once, at program start-up time.

In the loop both variables are incremented. However, at the top of the loop -- temporary is initialized to 1.

Example 9-1. perm/perm.cpp
#include <iostream>

int main(  ) {
    int counter;    // loop counter 

    for (counter = 0; counter < 3; ++counter) {
        int temporary = 1;
        static int permanent = 1;

        std::cout << "Temporary " << temporary << 
                " Permanent " << permanent << '
';
        ++temporary;
        ++permanent;
    }
    return (0);
}

The output of this program looks like:

Temporary 1 Permanent 1
Temporary 1 Permanent 2
Temporary 1 Permanent 3

Tip

Temporary variables are sometimes referred to as automatic variables because the space for them is allocated automatically. The qualifier auto can be used to denote a temporary variable; however, in practice auto is almost never used.

Table 9-1 describes the different ways a variable can be declared.

Table 9-1. Declaration modifiers

Declared

Scope

Storage class

Initialized

Outside all blocks

Global

Permanent

Once

static outside all blocks

File

Permanent

Once

Inside a block

Local

Temporary

Each time block is entered

static inside a block

Local

Permanent

Once

Tip

The keyword static is the most overloaded C++ keyword. It means a lot of different things depending on how it is used. For a complete list see Table 14-1.

The for Scope

The for statement is similar to a set of curly braces in that you can declare variables inside the statement whose scope goes from the start of the for to the end of the statement. (This includes the statement or block controlled by the for.) In the following statement:

for (int count = 0; count < MAX; ++count)
    sum += count;
// count is out of scope from here on.

the variable count is declared inside the for. Its scope is to the end of the statement; the scope ends with the first semicolon after the for.

Namespaces

The size of programs has grown steadily, and as the number of lines of code in a program grows larger and larger, so does the number of global variables. As a result, the global namespace has become very crowded.

One solution is to use stylized variable names. For example, in one program, all variables in the data processing module would begin with dp_ and all variables in the storage module would begin with st_.

This works after a fashion, but things get a little hairy as more and more modules are added. When you’re dealing with the core software system, the user interface module, the game group, and the backgammon module, and you have to prefix all your variables with core_ui_games_back_, things have gotten out of hand.

The C++ solution to this problem is to divide the program into namespaces. You deal with namespaces every day in real life. For example, chances are that you refer to the members of your family by their first names, such as Steve, Bill, Sandra, and Fred. Someone outside the family would use more formal names, like Steve Smith, Bill Smith, and so on.

C++ lets you define something called a namespace. All the variables declared inside a namespace are considered to be members of the same family, or namespace. For example, the following code declares three integers that are members of the namespace display:

namespace display {
    int width;       // The width of the display
    int height;      // Height of the display in lines
    bool visible;    // Is the display visible?
};

A family member’s full name might be Stephen Douglas Smith. The C++ equivalent of a full name is something called a fully qualified name. In this case, the fully qualified name of the variable width is display::width. Functions that belong to the family (i.e., functions that are part of the namespace display) can use the less formal name width.

Namespace std

We started out using the object std::cout for output. What this actually means is that we are using the variable cout in the namespace std. This namespace is used by C++ to define its standard library objects and functions.

You may remember that we began most of our programs with the statement:

#include <iostream>

The first statement causes the compiler to read in a file called iostream, which contains the definitions of the C++ standard variables. For example, a simplified iostream might look like this:

namespace std {
    istream cin;   // Define the input stream cin
    ostream cout;  // Define the output stream cout
    ostream cerr;  // Define the standard error stream
   // Lots of other stuff
}

Once the compiler has seen these definitions, std::cin, std::cout, and std::cerr are available for our use.

Global Namespace

If you do not enclose your code or variables in any namespace, a blank namespace is assigned to them. For example, the expression:

::global = 45;

assigns 45 to the variable global, which was declared outside any namespace declaration.

File-Specific Namespace

Let’s suppose you want to define a module and you want most of the functions and variables in the file to exist in their own unique namespace.

You could put the following statement at the top of your file:

namespace my_file_vars {

But what happens if, by some strange quirk of fate, someone else defines a namespace with the same name? The result is a namespace collision.

To avoid this, C++ has invented the unnamed namespace. The declaration:

namespace {

with no name specified, puts all the enclosed declarations in a namespace unique to the file.

Nested Namespaces

Namespaces may be nested. For example, we could declare some variables as follows:

namespace core {
    namespace games {
       namespace dice {
          int roll;    // The value of the last roll

Nesting this deep is a little verbose (core::games::dice::roll), but if you have a lot of code to organize, nested namespaces may be useful.

The using Statement

Let’s assume we have a program with a command module (with the namespace command) and a command parsing module (with the namespace command_parser). These two modules are very closely related, and the command module makes frequent references to variables inside the parsing module. (We’ll also ignore the fact that tight coupling like this is a bad design.)

When writing the command module, if you want to refer to a variable in the parsing module, you have to prefix it with the namespace identifier:

if (command_parser::first_argument == "ShowAll")

Because these modules are tightly coupled, you have to write out command_parser::first_argument a lot of times. This can get tiring after a while.

But you can tell C++, “I know that first_argument is in the command_parser module, but pretend that it’s in mine too.” This is accomplished through the using statement:

using command_parser::first_argument;

C++ will now let you use the name first_argument instead of command_parser::first_argument.

using command_parser::first_argument;

if (first_argument == "ShowAll")

Tip

The scope of a using declaration is the same as any other variable declaration. It ends at the end of the block in which it is declared.

Now let’s suppose there are a lot of variables that we wish to import from the module command_parser. We could put a using statement in our code for each one, but this would require a lot of statements. Or we can do things wholesale and tell C++ that all the names in the namespace command_parser are to be imported into our module. This is done with the statement:

using namespace command_parser;

The problem with the using statement

The use of the using statement should be avoided in most cases. The example we presented here had many interconnects between the two namespaces which necessitated the use of the using statement. But it’s considered bad program design to have so many interconnects.

The using statement also causes namespace confusion. Normally if you see a variable without a scope declaration (e.g., signal_curve) you can assume it belongs to the current namespace. If there are using statements in the program, this assumption is no longer valid and your life just got more complex. Programs are complex enough already, and this complication is not welcome.

Functions

Functions allow you to group commonly used code into a compact unit that can be used repeatedly. You have already encountered one function, main. It is a special function called at the beginning of the program after all static and global variables have been initialized.

Suppose you want to write a program to compute the area of three triangles. You could write out the formula three times, or you could create a function to do the work and then use that function three times. Each function should begin with a comment block containing the following:

Name

Name of the function

Description

Description of what the function does

Parameters

Description of each parameter to the function

Returns

Description of the return value of the function

Additional sections may be added, such as file formats, references, or notes. Refer to Chapter 3 for other suggestions.

The function to compute the area of a triangle could begin with the following comment block:

/******************************************* 
 * Triangle -- compute area of a triangle  * 
 *                                         * 
 * Parameters                              * 
 *  width -- width of the triangle         * 
 *  height -- height of the triangle       * 
 *                                         * 
 * Returns                                 * 
 *  area of the triangle                   * 
 *******************************************/

The function proper begins with the line:

float triangle(float width, float height)

float is the function type. This defines the type of data returned by the function. width and height are the parameters to the function. Parameters are variables local to the function that are used to pass information into the function.

We first check the parameters from the caller. Everybody knows that a triangle can’t have a negative width or height. But programming is a world of its own, and you can trust nothing. So let’s verify the input with a couple of assert statements:

assert(width >= 0.0);
assert(height >= 0.0);

This sort of paranoia is extremely useful when debugging large programs. assert statements like these can be a tremendous help when tracking down bad code. They serve to stop the program at the earliest possible time, thus saving you lots of time tracing bad data back to its source. Remember: just because you’re paranoid, it doesn’t mean they aren’t out to get you.

The function computes the area with the statement:

    area = width * height / 2.0;

What’s left is to give the result to the caller. This is done with the return statement:

    return (area)

The full triangle function can be seen in Example 9-2.

Example 9-2. tri/tri-sub.cpp
/*******************************************
 * triangle -- compute area of a triangle  *
 *                                         *
 * Parameters                              *
 *  width -- width of the triangle         *
 *  height -- height of the triangle       *
 *                                         *
 * Returns                                 *
 *  area of the triangle                   *
 *******************************************/
#include <cassert>
float triangle(float width, float height)
{
    float area; // area of the triangle 

    assert(width >= 0.0);
    assert(height >= 0.0);
    area = width * height / 2.0;
    return (area);
}

The line:

size = triangle(1.3, 8.3);

is a call to the function triangle. When C++ sees this function call, it performs the following operations:

triangle’s variable width = 1.3
triangle’s variable height = 8.3
Begin execution of the first line of the function triangle.

The technical name for this type of parameter passing is “call by value.” The assignment occurs only when the function is called, so data flows through the parameters only one way: in.

The return statement is how you get data out of the function. In the triangle example, the function assigns the local variable area the value 5.4 and then executes the statement return (area), so the return value of this function is 5.4. This value is assigned to size.

image with no caption

Example 9-3 computes the area of three triangles.

Example 9-3. tri/tri.cpp
#include <iostream>
#include <cassert>

int main(  ) 
{
    // function to compute area of triangle 
    float triangle(float width, float height);   

    std::cout << "Triangle #1 " << triangle(1.3, 8.3) << '
';
    std::cout << "Triangle #2 " << triangle(4.8, 9.8) << '
';
    std::cout << "Triangle #3 " << triangle(1.2, 2.0) << '
';
    return (0);
}
/*******************************************
 * triangle -- compute area of a triangle  *
 *                                         *
 * Parameters                              *
 *  width -- width of the triangle         *
 *  height -- height of the triangle       *
 *                                         *
 * Returns                                 *
 *  area of the triangle                   *
 *******************************************/
float triangle(float width, float height)
{
    float area; // area of the triangle 

    assert(width >= 0.0);
    assert(height >= 0.0);
    area = width * height / 2.0;
    return (area);
}

Functions must be declared just like variables. The declaration tells the C++ compiler about the function’s return value and parameters. There are two ways of declaring a function. The first is to write the entire function before it’s used. The other is to define what’s called a function prototype, which gives the compiler just enough information to call the function. A function prototype looks like the first line of the function, but the prototype has no body. For example, the prototype for the triangle function is:

float triangle(float width, float height);

Note the semicolon at the end of the line. This is used to tell C++ that this is a prototype and not a real function.

C++ allows you to leave out the parameter names when declaring a prototype. This function prototype could just as easily have been written:

float triangle(float, float);

However, this technique is not commonly used, because including the parameter names gives the reader more information about what the function is doing and makes the program easier to understand. Also, it’s very easy to create a prototype by simply using the editor to copy the first line of a function and putting that line where you want the prototype. (Many times this will be in a header file, as described in Chapter 23.)

Functions that have no parameters are declared with a parameter list of ( ). For example:

int get_value(  );

You can also use the parameter list (void). This is a holdover from the old C days when an empty parameter list “( )” signaled an old K&R-style C function prototype. Actually, C++ will accept both an empty list and a void declaration.

Almost all C++ programmers prefer the empty list. The advantages of the (void) form are:

  • It provides an obvious indicator that there is no parameter list. (In other words, if the programmer puts in the void she tells the world, “This function really takes no arguments, and I didn’t forget the parameter list.”

  • It is compatible with the older C language.

The advantages of the empty list are:

  • The syntax ( ) is more sane and consistent with the way we declare parameters than that of (void).

  • The void list is a historical hack put into C to solve a syntax problem that existed because the empty list was used for something else. It was ported from C to C++ for compatibility.

  • We are programming C++, not C, so why should we use relics from the past in our code?

For these reasons most people use the empty list. This author is one exception. I prefer the (void) construct, but when three reviewers and an editor tell you you’re wrong, it’s time to rethink your choices. The empty list is used throughout this book.

Returning void

The keyword void is also used to indicate a function that does not return a value (similar to the FORTRAN SUBROUTINE or PASCAL Procedure). For example, this function just prints a result; it does not return a value:

void print_answer(int answer) 
{ 
    if (answer < 0) { 
        std::cout << "Answer corrupt
"; 
        return; 
    } 
    std::cout << "The answer is " << answer '
'; 
}

Namespaces and Functions

Namespaces affect not only variables but functions as well. A function belongs to the namespace in which it is declared. For example:

namespace math {

int square(const int i) {
    return (i * i);
}

} // End namespace

namespace body {

int print_value(  )
{
    std::cout << "5 squared is " << math::square(5) << '
';
}

}

All the functions in a namespace can access the variables in that namespace directly and don’t need a using clause or a namespace qualification. For example:

namespace math {

const double PI = 3.14159;

double area(const double radius)
{
    return (2.0 * PI * radius);
}

}

const Parameters and Return Values

A parameter declared const cannot be changed inside the function. Ordinary parameters can be changed inside functions, but the changes will not be passed back to the calling program.

For example, in the triangle function, we never change width or height. These could easily be declared const. Since the return value is also something that cannot be changed, it can be declared const as well. The const declarations serve to notify the programmer that the parameters do not change inside the function. If you do attempt to change a const parameter, the compiler generates an error. The improved triangle function with the const declarations can be seen in Example 9-4.

Example 9-4. tri/tri-sub2.cpp
const float triangle(const float width, const float height)
{
    float area; // area of the triangle 

    assert(width >= 0.0);
    assert(height >= 0.0);
    area = width * height / 2.0;
    return (area);
}

As it stands now, the const declaration for the return value is merely a decoration. In the next section you’ll see to how to return references and make the const return declaration useful.

Reference Parameters and Return Values

Remember that in Chapter 5 we discussed reference variables. A reference variable is a way of declaring an additional name for a variable. For global and local variables, reference variables are not very useful. However, they take on an entirely new meaning when used as parameters.

Suppose you want to write a subroutine to increment a counter. If you write it like Example 9-5, it won’t work.

Example 9-5. value/value.cpp
#include <iostream>

// This function won't work
void inc_counter(int counter)
{
    ++counter;
}

int main(  )
{
   int a_count = 0;     // Random counter

   inc_counter(a_count);
   std::cout << a_count << '
';
   return (0);
}

Why doesn’t it work? Because C++ defaults to call by value. This means that values go in, but they don’t come out.

What happens if you convert the parameter counter to a reference? References are just another way of giving the same variable two names. When inc_counter is called, counter becomes a reference to a_count. Thus, anything done to counter results in changes to a_count. Example 9-6, using a reference parameter, works properly.

Example 9-6. value/ref.cpp
#include <iostream>

// Works
void inc_counter(int& counter)
{
    ++counter;
}

int main(  )
{
   int a_count = 0;     // Random counter

   inc_counter(a_count);
   std::cout << a_count << '
';
   return (0);
}

Reference declarations can also be used for return values. Example 9-7 finds the biggest element in an array.

Example 9-7. value/big.cpp
int& biggest(int array[], int n_elements)
{
    int index;  // Current index
    int biggest; // Index of the biggest element

    // Assume the first is the biggest
    biggest = 0;
    for (index = 1; index < n_elements; ++index) {
        if (array[biggest] < array[index])
            biggest = index;
    }

    return (array[biggest]);
}

If you wanted to print the biggest element of an array, all you would have to do is this:

int item_array[5] = {1, 2, 5000, 3, 4}; // An array

std::cout << "The biggest element is " << 
         biggest(item_array, 5) << '
';

Let’s examine this in more detail. First of all, consider what happens when you create a reference variable:

int& big_reference = item_array[2]; // A reference to element #2

The reference variable big_reference is another name for item_array[2]. You can now use this reference to print a value:

std::cout << big_reference << '
';   // Print out element #2

But since this is a reference, you can use it on the left side of an assignment statement as well. (Expressions that can be used on the left side of the = in an assignment are called lvalues.)

big_reference = 0;      // Zero the largest value of the array

The function biggest returns a reference to item_array[2]. Remember that in the following code, biggest( ) is item_array[2]. The following three code sections all perform equivalent operations. The actual variable, item_array[2], is used in all three:

// Using the actual data
std::cout << item_array[2] << '
';
item_array[2] = 0;

// Using a simple reference
int big_reference = &item_array[2];
std::cout << big_reference << '
';
big_reference = 0;

// Using a function that returns a reference
std::cout << biggest(  ) << '
';  
biggest(  ) = 0;

Because biggest returns a reference, it can be used on the left side of an assignment operation (=). But suppose you don’t want that to happen. You can accomplish this by returning a const reference:

const int& biggest(int array[], int n_elements);

This tells C++ that even though you return a reference, the result cannot be changed. Thus, code like the following is illegal:

biggest(  ) = 0;              // Now it generates an error

Dangling References

Be careful when using return by reference, or you could wind up with a reference to a variable that no longer exists. Example 9-8 illustrates this problem.

Example 9-8. ref/ref.cpp
 1: const int& min(const int& i1, const int& i2)
 2: {
 3:     if (i1 < i2)
 4:         return (i1);
 5:     return (i2);
 6: }
 7: 
 8: int main(  )
 9: {
10:    const int& i = min(1+2, 3+4);
11: 
12:    return (0);
13: }

Line 1 starts the definition of the function min. It returns a reference to the smaller of two integers.

In line 10 we call this function. Before the function min is called, C++ creates a temporary integer variable to hold the value of the expression 1 + 2. A reference to this temporary variable is passed to the min function as the parameter i1. C++ creates another temporary variable for the i2 parameter.

The function min is then called and returns a reference to i1. But what does i1 refer to? It refers to a temporary variable that C++ created in main. At the end of the statement, C++ can destroy all the temporaries.

Let’s look at the call to min (line 10) in more detail. Here’s a pseudo-code version of line 10, including the details that C++ normally hides from the programmer:

create integer tmp1, assign it the value 1 + 2
create integer tmp2, assign it the value 3 + 4
bind parameter i1 so it refers to tmp1
bind parameter i2 so it refers to tmp2
call the function "min"
bind main's variable i so it refers to 
        the return value (i1-a reference to tmp1)
// At this point i is a reference to tmp1
destroy tmp1 
destroy tmp2

//    At this point i still refers to tmp1
//    It doesn't exist, but i refers to it

At the end of line 10 we have a bad situation: i refers to a temporary variable that has been destroyed. In other words, i points to something that does not exist. This is called a dangling reference and should be avoided.

Array Parameters

So far you’ve dealt only with simple parameters. C++ treats arrays a little differently. First of all, you don’t have to put a size in the prototype declaration. For example:

int sum(int array[]);

C++ uses a parameter-passing scheme called “call by address” to pass arrays. Another way of thinking of this is that C++ automatically turns all array parameters into reference parameters. This allows any size array to be passed. The function sum we just declared may accept integer arrays of length 3, 43, 5,000, or any length.

However, you can put in a size if you want to. C++ allows this, although it ignores whatever number you put there. But by putting in the size, you alert the people reading your program that this function takes only fixed-size arrays.

int sum(int array[3]);

For multidimensional arrays you are required to put in the size for each dimension except the last one. That’s because C++ uses these dimensions to compute the location of each element in the array.

int sum_matrix(int matrix1[10][10]);   // Legal
int sum_matrix(int matrix1[][10]);     // Legal
int sum_matrix(int matrix1[][]);       // Illegal

Question 9-1: The function in Example 9-9 should compute the length of a C-style string.[1] Instead it insists that all strings are of length zero. Why?

Example 9-9. length/length.cpp
/********************************************************
 * length -- compute the length of a string             *
 *                                                      *
 * Parameters                                           *
 *      string -- the string whose length we want       *
 *                                                      *
 * Returns                                              *
 *      the length of the string                        *
 ********************************************************/
int  length(char string[])
{
    int index;      // index into the string 

    /*
     * Loop until we reach the end of string character
     */
    for (index = 0; string[index] != ''; ++index)
        /* do nothing */
    return (index);
}

Function Overloading

Let’s define a simple function to return the square of an integer:

int square(int value) {
    return (value * value);
}

We also want to square floating-point numbers:

float square(float value) {
    return (value * value);
}

Now we have two functions with the same name. Isn’t that illegal? In older languages, such as C and PASCAL, it would be. In C++ it’s not. C++ allows function overloading, which means you can define multiple functions with the same names. Thus you can define a square function for all types of things: int, float, short int, double, and even char, if we could figure out what it means to square a character.

To keep your code consistent, all functions that use the same name should perform the same basic function. For example, you could define the following two square functions:

// Square an integer
int square(int value);

// Draw a square on the screen
void square(int top, int bottom, int left, int right);

This is perfectly legal C++ code, but it is confusing to anyone who has to read the code.

There is one limitation to function overloading: C++ must be able to tell the functions apart. For example, the following is illegal:

int get_number(  );
float get_number(  );  // Illegal

The problem is that C++ uses the parameter list to tell the functions apart. But the parameter list of the two get_number routines is the same: ( ). The result is that C++ can’t tell these two routines apart and flags the second declaration as an error.

Default Arguments

Suppose you want to define a function to draw a rectangle on the screen. This function also needs to be able to scale the rectangle as needed. The function definition is:

void draw(const int width, const int height, double scale)

After using this function for a while, you discover that 90% of the time you don’t use the draw’s scale ability. In other words, 90% of the time the scale factor is 1.0.

C++ allows you to specify a default value for scale. The statement:

void draw(const int width, const int height, double scale = 1.0)

tells C++, “If scale is not specified, make it 1.0.” Thus the following are equivalent:

draw(3, 5, 1.0);    	// Explicity specify scale 	
draw(3, 5);         		// Let it default to 1.0

There are some style problems with default arguments. Study the following code:

draw(3, 5);

Can you tell whether the programmer intended for the scale to be 1.0 or just forgot to put it in? Although sometimes useful, the default argument trick should be used sparingly.

Unused Parameters

If you define a parameter and fail to use it, most good compilers will generate a warning. For example, consider the following code:

void exit_button(Widget& button) {
    std::cout << "Shutting down
";
    exit (0);
}

This example generates the message:

Warning: line 1.  Unused parameter "button"

But what about the times you really don’t want to use a parameter? Is there a way to get C++ to shut up and not bother you? There is. The trick is to leave out the name of the parameter:

// No warning, but style needs work 
void exit_button(Widget&) {
    std::cout << "Shutting down
";
    exit (0); 	
}

This is nice for C++, but not so nice for the programmer who has to read your code. We can see that exit_button takes a Widget& parameter, but what is the name of the parameter? A solution to this problem is to reissue the parameter name as a comment:

// Better 	
void exit_button(Widget& /*button*/) { 
    std::cout << "Shutting down
"; 	    
    exit (0); 
}

Some people consider this style ugly and confusing. They’re right that it’s not that easy to read. There ought to be a better way; I just wish I could think of one.

One question you might be asking by now is, “Why would I ever write code like this? Why not just leave the parameter out?”

It turns out that many programming systems make use of callback functions . For example, you can tell the X Window System, “When the EXIT button is pushed, call the function exit_button.” Your callback function may handle many buttons, so it’s important to know which button is pushed. So X supplies button as an argument to the function.

What happens if you know that button can only cause X to call exit_button? Well, X is still going to give it to you, you’re just going to ignore it. That’s why some functions have unused arguments.

Inline Functions

Looking back at the square function for integers, we see that it is a very short function, just one line. Whenever C++ calls a function, there is some overhead generated. This includes putting the parameters on the stack, entering and leaving the function, and stack fix-up after the function returns.

For example, consider the following code:

int square(int value) {
    return (value * value);
}
int main(  ) {
    // .....
    x = square(x);

The code generates the following assembly code on a 68000 machine (paraphrased):

label "int square(int value)"
        link a6,#0              // Set up local variables

        // The next two lines do the work
        movel a6@(8),d1         // d1 = value
        mulsl a6@(8),d1         // d1 = value * d1

        movel d1,d0             // Put return value in d0
        unlk a6                 // Restore stack
        rts                     // Return(d0)

label "main"
//....
//      x = square(x)
//
        movel a6@(-4),sp@-      // Put the number x on the stack
        jbsr "void square(int value)" 
                                // Call the function

        addqw #4,sp             // Restore the stack
        movel d0,a6@(-4)        // Store return value in X
// ...

As you can see from this code, there are eight lines of overhead for two lines of work. C++ allows you to cut out that overhead through the use of an inline function. The inline keyword tells C++ that the function is very small. This means that it’s simpler and easier for the C++ compiler to put the entire body of the function in the code stream instead of generating a call to the function. For example:

inline int square(int value) {
    return (value * value);
}

Changing the square function to an inline function generates the following, much smaller, assembly code:

label "main"
// ...
//      x = square(x)
//
        movel d1,a6@(-4)        // d1 = x
        movel a6@(-4),d0        // d0 = x
        mulsl d0,d0             // d0 = (x * x)

        movel d0,a6@(-4)        // Store result

Expanding the function inline has eliminated the eight lines of overhead and results in much faster execution.

The inline specifier provides C++ a valuable hint it can use when generating code, telling the compiler that the code is extremely small and simple. Like register, the inline specifier is a hint. If the C++ compiler can’t generate a function inline, it will create it as an ordinary function.

Summary of Parameter Types

Table 9-2 lists the various parameter types.

Table 9-2. Parameter types

Type

Declaration

Call by value

function(int var)

Value is passed into the function and can be changed inside the function, but the changes are not passed to the caller.

Constant call by value

function(const int var)

Value is passed into the function and cannot be changed.

Reference

function(int& var)

Reference is passed to the function. Any changes made to the parameter are reflected in the caller.

Constant reference

function(const int& var)

Value cannot be changed in the function. This form of parameter is more efficient than “constant call by value” for complex data types. (See Chapter 12.)

Array

function(int array[])

Value is passed in and may be modified. C++ automatically turns arrays into reference parameters.

Call by address

function(int *var)

Passes a pointer to an item. Pointers are covered in Chapter 15.

Recursion

Recursion occurs when a function calls itself directly or indirectly. Some programming functions lend themselves naturally to recursive algorithms, such as the factorial.

A recursive function must follow two basic rules:

  • It must have an ending point.

  • It must reduce the amount of work to be done each time it’s called.

A definition of factorial is:

fact(0) 	= 1 
fact(n) 	= n * fact(n-1)

In C++, this definition translates to:

int fact(int number) 
{ 
    if (number == 0) 
        return (1); 
    /* else */ 
    return (number * fact(number-1)); 
}

This satisfies the two rules. First, it has a definite ending point (when number == 0). Second, it reduces the amount of work to be done because computing fact(number-1) is simpler than fact(number).

Factorial is legal only for number >= 0. But what happens if we try to compute fact( -- 3)? The program aborts with a stack overflow or similar message. fact( -- 3) calls fact( -- 4) calls fact( -- 5) and so on. There is no ending point. This is called an infinite recursion error . In this case it was caused by a bad parameter. We should check for that:

int fact(int number) 
{ 
    assert(number >= 0);
    if (number == 0) 
        return (1); 
    /* else */ 
    return (number * fact(number-1)); 
}

Many things we do iteratively can be done recursively, like summing the elements of an array. You can define a function to add elements m through n of an array as follows:

If you have only one element, the sum is simple.
Otherwise, it is the sum of the first element and the sum of the rest.

In C++ this is:

int sum(const int first, const int last, const int array[], 
        const int array_size) 
{ 
    assert((first > 0) && (first < array_size));
    assert((last > 0) && (last < array_size));

    if (first == last) 
        return (array[first]); 
    /* else */ 
        return (array[first] + sum(first + 1, last, array, array_size)); 
}

For example:

Sum(1 8 3 2) =
1 + Sum(8 3 2) =
8 + Sum(3 2) =
3 + Sum(2) =
2
3 + 2 = 5
8 + 5 = 13
1 + 13 = 14
Answer = 14

This is not to say that this is the clearest or fastest way to sum a loop. In this case, a loop would be much faster. But it does illustrate how recursion can be used to create a nontraditional solution to a problem.

Structured Programming Basics

Computer scientists spend a great deal of time and effort studying how to program. The result is that they come up with the absolutely, positively, best programming methodology—a new one each month. Some of these systems include flow charts, top-down programming, bottom-up programming, structured programming, and object-oriented programming.

Now that you have learned about functions, we can talk about using structured programming techniques to design programs. This is a way of dividing up or structuring a program into small, well-defined functions. It makes the program easy to write and easy to understand. I don’t claim that this system is the absolute best way to program, but it happens to be the system that works best for me. If another system works better for you, use it.

Structured programming focuses on a program’s code. Later you’ll see how to merge code and data to form classes and begin to perform object-oriented programming.

The first step in programming is to decide what you are going to do. This has already been described in Chapter 7. Next, decide how you are going to structure your data.

Finally, the coding phase begins. When writing a paper, you start with an outline, with each section in the paper described by a single sentence. The details are filled in later. Writing a program is similar. You start with an outline, but this outline is your main function. The details can be hidden within other functions. For example, the program in Example 9-10 solves all of the world’s problems.

Example 9-10. A global solution
int main(  ) 
{ 

    init(  ); 
    solve_problems(  ); 
    finish_up(  );
}

Of course, some of the details remain to be filled in.

Start by writing the main function. It should be less than two pages long. If it grows longer, consider splitting it up into two smaller, simpler functions. The size of the function should be limited to three pages, because that is about the maximum amount of information a human being can store in short-term memory at one time. After the main function is complete, you can start on the other functions. This type of structured programming is called top-down programming. You start at the top (main) and work your way down.

Another type of coding is called bottom-up programming. This involves writing the lowest-level function first, testing it, and then building on that working set. I tend to use some bottom-up techniques when I’m working with a new standard function that I haven’t used before. I write a small function to make sure I really know how the function works and continue from there. This is the approach used in Chapter 7 to construct the calculator program.

Later on, in Chapter 13, we’ll learn about object-oriented programming. That’s where you design your data and the things that can be done with it together in something called a class.

Real-World Programming

Over the years I’ve used a lot of different programming techniques. The one I use depends on the problem I’m trying to solve. I’ve discovered a few things about what it take to create a successful program.

The first step is to think about what you are doing before you do it. Resist the urge to start coding, and sit down and do some design. Make things as simple as possible. The simpler your code, the less that can go wrong with it.

Also try to make your design as flexible as possible. After all, you may know things tomorrow that you don’t know today.

Next, organize the information you need for your program in a way that makes it as clear as possible. Depending on what you are doing, this may involve documentation, charts, diagrams, or something else. It all depends on the problem you’re trying to solve and how you think. Do whatever works for you.

When you code, make sure that you are able to test your code at every step of the way. A bunch of small, correct steps will get you there much faster than one great leap in the wrong direction.

Finally, realize that there’s not one “right” coding technique. Different systems work for different problems. Use whatever works best for you.

Programming Exercises

Exercise 9-1: Write a procedure that counts the number of words in a string. (Your documentation should describe exactly how you define a word.) Write a program to test your new procedure.

Exercise 9-2: Write a function begins(string1, string2) that returns true if string1 begins string2. Write a program to test the function.

Exercise 9-3: Write a function count(number, array, length) that will count the number of times number appears in array. The array has length elements. The function should be recursive. Write a test program to go with the function.

Exercise 9-4: Write a function that will take a character string and return a primitive hash code by adding up the value of each character in the string.

Exercise 9-5: Write a function that returns the maximum value of an array of numbers.

Exercise 9-6: Write a function that scans a string for the character “-” and replaces it with “_”.

Answers to Chapter Questions

Answer 9-1:The programmer went to a lot of trouble to explain that the for loop did nothing (except increment the index). However, there is no semicolon at the end of the for. C++ keeps reading until it sees a statement (in this case return(index)) and puts it in the for loop. Example 9-11 contains a correctly written version of the program.

Example 9-11. length/rlen.cpp
int  length(char string[])
{
    int index;      // index into the string 

    /*
     * Loop until we reach the end of string character
     */
    for (index = 0; string[index] != ''; ++index)
        /* do nothing */ ;
    return (index);
}


[1] This function (when working properly) performs the same function as the library function strlen.

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

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