Chapter 11

Functions, I Declare!

In This Chapter

arrow Breaking programs down into functions

arrow Writing and using functions

arrow Returning values from a function

arrow Passing values to a function

arrow Providing a function prototype declaration

The programs you see prior to this chapter are small enough and simple enough that you can write any of them in one sequence of instructions. Sure, there have been branches using if statements and looping with while and for loops, but the entire program was in one place for all to see.

Real-world programs aren’t usually that way. Programs that are big enough to deal with the complexities of the real world are generally too large to write in one single block of C++ instructions. Real-world programs are broken into modules called functions in C++. This chapter introduces you to the wonderful world of functions.

Breaking Your Problem Down into Functions

Even the Tire-Changing Program from Chapter 1 is too big to write in a single block. It only tackles the problem of removing the lug nuts. It doesn’t even touch the problem of jacking up the car, removing the wheel, getting the spare out, and so on.

In fact, suppose I were to put the lug-nut-removing code into a module that I call something fiendishly clever, like RemoveLugNuts(). (I add the parentheses to follow C++ grammar.) I could bundle up similar modules for the other functions.

The resulting top-level module for changing a tire might look like the following:

  1. Grab spare tire;
2. RaiseCar();
3. RemoveLugNuts();  // we know what this does
4. ReplaceWheel();
5. AttachLugNuts();  // inverse of RemoveLugNuts()
6. LowerCar();

Only the first statement is actually an instruction written in Tire-Changing Language. Each of the remaining statements is a reference to a module somewhere. These modules consist of sequences of statements written in Tire-Changing Language (including possible references to other, simpler modules).

Imagine how this program is executed: The tire-changing processor starts at statement 1. First it sees the simple instruction Grab spare tire, which it executes without complaint (it always does exactly what you tell it to do). It then continues on to Statement 2.

Statement 2, however, says, “Remember where you are at and go find the set of instructions called RaiseCar() and execute them. Once you’ve finished there, come back here for further instructions.” In similar fashion, Statements 3 through 6 also direct the friendly, mechanically inclined processor off to separate sets of instructions.

Understanding How Functions Are Useful

There are several reasons for breaking complex problems up into simpler functions. The original reason that a function mechanism was added to early programming languages was the Holy Grail of reuse. The idea was to create functions that could be reused in multiple programs. For example, factorial is a common mathematical procedure. If I rewrote the Factorial program as a function, I could invoke it from any program in the future that needs to calculate a factorial. This form of reuse allows code to be easily reused — from different programs as well as from different areas within the same program.

Once a function mechanism was introduced, however, people discovered that breaking up large problems into simpler, smaller problems brought with it further advantages. The biggest advantage has to do with the number of things that a person can think about at one time. This is often referred to as the “Seven Plus or Minus Two” Rule. That’s the number of things that a person can keep active in his mind at one time. Almost everyone can keep at least five objects in their active memory, but very few can keep more than nine objects active in their consciousness at one time.

You’ve noticed, no doubt, that there are a lot of details to worry about when writing C++ code. A C++ module quickly exceeds the nine-object upper limit as it increases in size. Such functions are hard to understand — and therefore hard to write and to get working properly.

It turns out to be much easier to think of the top-level program in terms of high-level functionality, much as I did in the tire changing example at the beginning of this chapter. This example divided the act of changing a tire into six steps, implemented in five functions.

Of course, I still have to implement each of these functions, but these are much smaller problems than the entire problem of changing a tire. For example, when implementing RaiseCar(), I don’t have to worry about tires or spares, and I certainly don’t have to deal with the intricacies of loosening and tightening lug nuts. All I have to think about in that function is how to get the car off the ground.

technicalstuff.eps In computer-nerd-speak, we say that these different functions are written at different levels of abstraction. The Tire-Changing program is written at a very high level of abstraction; the RemoveLugNuts() function in Chapter 1 is written at a low level of abstraction.

Writing and Using a Function

Like so many things, functions are best understood by example. The following code snippet shows the simplest possible example of creating and invoking a function:

  void someFunction()
{
    // do stuff
    return;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // do something
    
    // now invoke someFunction()
    someFunction();

    // keep going here once control returns
}

This example contains all the critical elements necessary to create and invoke a function:

  1. The declaration: The first thing is the declaration of the function: the name of the function with a type in front, followed by a set of open and closed parentheses. In this case, the name of the function is someFunction(), and its return type is void. (I’ll explain what that last part means in the “Returning things” section of this chapter.)
  2. The definition: The declaration of the function is followed by the definition of what it does, also called the body of the function. The body of a function always starts with an open brace and ends with a closed brace. The statements inside the body are just like those within a loop or an if statement.
  3. The return: The body of the function contains zero or more return statements. A return returns control to a point immediately after the point where the function was invoked. Control returns automatically if it ever reaches the final closed brace of the function body.
  4. The call: A function is called by invoking the name of the function followed by open and closed parentheses.

The flow of control is shown in Figure 11-1.

9781118823873-fg1101.tif

Figure 11-1: Invoking a function passes control to the module. Control returns to immediately after the call.

Returning things

Functions often return a value to the caller. Sometimes this is a calculated value — a function like factorial() might return the factorial of a number. Sometimes this value is an indication of how things went — this is usually known as an error return. So the function might return a zero if everything went OK, and a non-zero if something went wrong during the execution of the function.

To return a value from a function, you need to make two changes:

  1. Replace void with the type of value you intend to return.
  2. Place the value you want the function to return after the keyword return. C++ does not allow you to return from a function by running into the final closed brace if the return type is other than void.

technicalstuff.eps The keyword void is C++-ese for “nothing.” Thus a function declared with a return type of int returns an integer. A function declared with a return type of void returns nothing.

Reviewing an example

The following FunctionDemo program uses the function sumSequence() to sum a series of numbers entered by the user at the keyboard. This function is invoked repeatedly until the user enters a zero length sequence.

  //
//  FunctionDemo - demonstrate how to use a function
//                 to simplify the logic of the program.
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

//
// sumSequence() - return the sum of a series of numbers
//                 entered by the user. Exit the loop
//                 when the user enters a negative
//                 number.
int sumSequence()
{
    // create a variable into which we will add the
    // numbers entered by the user
    int nAccumulator = 0;

    for(;;)
    {
        // read another value from the user
        int nValue;
        cout << "Next: ";
        cin  >> nValue;

        // exit if nValue is negative
        if (nValue < 0)
        {
            break;
        }

        // add the value entered to the accumulated value
        nAccumulator += nValue;
    }

    // return the accumulated value to the caller
    return nAccumulator;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    cout << "This program sums sequences of numbers. "
         << "Enter a series of numbers. Entering a "
         << "negative number causes the program to "
         << "print the sum and start over with a new "
         << "sequence. "
         << "Enter two negatives in a row to end the "
         << "program." << endl;

    // stay in a loop getting input from the user
    // until he enters a negative number
    for(;;)
    {
        // accumulate a sequence
        int nSum = sumSequence();

        // if the sum is zero...
        if (nSum == 0)
        {
            // ...then exit the program
            break;
        }

        // display the result
        cout << "Sum = " << nSum << endl;
     }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

First, concentrate on the main() program. After outputting rather verbose instructions to the user, the program enters a for loop.

remember.eps A for loop whose conditional expression is empty (as in for(;;)) will loop forever unless something within the body of the loop causes control to exit the loop (or until Hell freezes over).

The first non-comment line within this loop is the following:

  int nSum = sumSequence();

This expression passes control to the sumSequence() function. Once control returns, the declaration uses the value returned by sumSequence() to initialize nSum.

The function sumSequence() first initializes nAccumulator to zero. It then prompts the user for value from the keyboard. If the number entered is not negative, it is added to the value in nAccumulator, and the user is prompted for another value in a loop. As soon as the user enters a negative number, the function breaks out of the loop and returns the value accumulated in nAccumulator to the caller.

The following is a sample run from the FunctionDemo program:

  This program sums sequences of numbers.
Enter a series of numbers. Entering a
negative number causes the program to
print the sum and start over with a new
sequence. Enter two negatives in a row to end the
program.
Next: 5
Next: 15
Next: 20
Next: -1
Sum = 40
Next: 1
Next: 2
Next: 3
Next: 4
Next: -1
Sum = 10
Next: -1
Press Enter to continue …

Passing Arguments to Functions

Functions that do nothing but return a value are of limited value because the communication is one-way — from the function to the caller. Two-way communication requires function arguments, which I discuss next.

Function with arguments

A function argument is a variable whose value is passed to the function during the call. The following FactorialFunction converts the previous factorial operation into a function:

  //
//  FactorialFunction - rewrite the factorial code as
//                a separate function.
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

//
// factorial - return the factorial of the argument
//             provided. Returns a 1 for invalid arguments
//             such as negative numbers.
int factorial(int nTarget)
{
    // start with an accumulator that's initialized to 1
    int nAccumulator = 1;
    for (int nValue = 1; nValue <= nTarget; nValue++)
    {
        nAccumulator *= nValue;
    }

    return nAccumulator;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    cout << "This program calculates factorials"
         << " of user input. "
         << "Enter a negative number to exit" << endl;

    // stay in a loop getting input from the user
    // until he enters a negative number
    for (;;)
    {
        // enter the number to calculate the factorial of
        int nValue;

        cout << "Enter number: ";
        cin  >> nValue;

        // exit if the number is negative
        if (nValue < 0)
        {
            break;
        }

        // display the result
        int nFactorial = factorial(nValue);
        cout << nValue << " factorial is "
             << nFactorial << endl;
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The declaration of factorial() includes an argument nTarget of int. Looking ahead, you can see that this is intended to be the value whose factorial the program calculates. The return value of the function is the calculated factorial.

In main(), the program prompts the user for a value, which it stores in nValue. If the value is negative, the program terminates. If not, it calls factorial() and passes the value of nValue. The program stores the returned value in nFactorial. It then outputs both values before returning to prompt the user for a new value.

Functions with multiple arguments

A function can have multiple arguments by separating them by commas. Thus the following function returns the product of two integer arguments:

  int product(int nValue1, int nValue2)
{
    return nValue1 * nValue2;
}

Exposing main()

Now the truth can be told: The “keyword” main() from our standard template is nothing more than a function — albeit a function with strange arguments, but a function nonetheless.

When a program is built, C++ adds some boilerplate code that executes before your program ever gains control. This code sets up the environment in which your program will operate. For example, this boilerplate code opens the default input and output channels and attaches them to cin and cout.

After the environment has been established, the C++ boilerplate code calls the function main(), thereby beginning execution of your code. When your program finishes, it returns from main(). This enables the C++ boilerplate to clean up a few things before terminating the program and handing control back to the operating system.

Defining Function Prototype Declarations

There’s a little more to the previous program examples than meets the eye. Consider the second program, FactorialFunction, for example. During the build process, the C++ compiler scanned through the file. As soon as it came upon the factorial() function, it made a note in an internal table somewhere of the function’s extended name and its return type. This is how the compiler was able to understand what I was talking about when I invoked the factorial() function later on in main() — it saw that I was trying to call a function, and it said, “Let me look in my table of defined functions for one called factorial(). Aha, here’s one!”

In this case, the function was defined and the types and number of arguments matched perfectly, but that isn’t always the case. What if I had invoked the function not with an integer but with something that could be converted into an integer? Suppose I had called the function as follows:

  factorial(1.1);

That’s not a perfect match, 1.1 is not an integer, but C++ knows how to convert 1.1 into an integer. So it could make the conversion and use factorial(int) to complete the call. The question is, does it?

The answer is “Yes.” C++ will generate a warning in some cases to let you know what it’s doing, but it will generally make the necessary type conversions to the arguments to use the functions that it knows about.

Note: I know that I haven’t discussed the different variable types yet (I do so in Chapter 14), but the argument I am making is fairly generic. You will also see in Chapter 14 how to avoid warnings caused by automatic type conversions.

What about a call like the following:

  factorial(1, 2);

There is no conversion that would allow C++ to lop off an argument and use the factorial(int) function to satisfy this call, so C++ generates an error in this case.

The only way C++ can sort out this type of thing is if it sees the function declaration before it sees the attempt to invoke the function. This means each function must be declared before it is used.

I know what you’re thinking (I think): C++ could be a little less lazy and look ahead for function declarations that occur later on before it gives up and starts generating errors, but the fact is that it doesn’t. It’s just one of those things, like my crummy car; you learn to live with it.

So does that mean you have to define all of your functions before you can use them? No. C++ allows you to declare a function without a body in what is known as a prototype declaration.

A prototype declaration creates an entry for the function in the table I was talking about. It fills in the extended name, including the number and type of the arguments, and the return type. C++ leaves the definition of the function, the function body, empty until later.

In practice, a prototype declaration appears as follows:

  // the prototype declaration
int factorial(int nTarget);

int main(int nNumberofArgs, char* pszArgs[])
{
    cout << "The factorial of 10 is "
         << factorial(10) << endl;

    return 0;
}

// the definition of the factorial(int) function;
// this satisfies our promise to provide a definition
// for the prototype function declaration above
int factorial(int nTarget)
{
    // start with an accumulator that's initialized to 1
    int nAccumulator = 1;
    for (int nValue = 1; nValue <= nTarget; nValue++)
    {
        nAccumulator *= nValue;
    }

    return nAccumulator;
}

The prototype declaration tells the world (or at least that part of the world after the declaration) that factorial() takes a single integer argument and returns an integer. That way, C++ can check the call in main() against the declaration to see whether any type conversions need to take place — or whether the call is even possible.

The prototype declaration also represents a promise to C++ to provide a complete definition of factorial(int) somewhere else in the program. In this case, the full definition of factorial(int) follows right after main().

tip.eps It’s common practice to provide prototype declarations for all functions defined within a module. That way, you don’t have to worry about the order in which they are defined. I have more to say about this topic in the next chapter.

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

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