Strings Top More about FunctionsFunctions Contents

Functions

Recall from the chapter on assignment the series of expressions we evaluated to find the y-value of a point on the line:

    y = 5x - 3

First, we assigned values to the slope, the x-value, and the y-intercept:

    int m = 5;
    int x = 9;
    int b = -3;

Once those variables have been assigned, we can compute the value of y:

    int y = m * x + b;

Now, suppose we wished to find the y-value corresponding to a different x-value or, worse yet, for a different x-value on a different line. All the work we did would have to be repeated. A function is a way to encapsulate all these operations so we can repeat them with a minimum of effort.

Encapsulating a series of operations

First, we will define a not-too-useful function that calculates y, given a slope of 5, a y-intercept of -3, and an x-value of 9 (exactly as above). We do this by wrapping a function around the sequence of operations above. The return value of this function is the computed y value:

    int
    getY(void)
        {
        int m = 5;
        int x = 9;
        int b = -3;
        return m * x + b;
        }

There are a few things to note. A function definition begins with a type such as int, double, or char *. This type is known as the return type. In our example, the return type is int, meaning the function we are defining will return an integer. Following the return type is the name of the function; the name of this particular function is getY. The names of the things being sent to the function are given between the parentheses. If the function does not need any thing sent to it, we use the keyword void to signify this fact. Together, the return type, the name of the function, and the information about what needs to be sent to the function is known as the function signature.

The stuff indented from the function signature is called the function body and is the code that will be evaluated (or executed) when the function is used. You must remember this: the function body is not evaluated until the function is actually used.

While in many languages the name of a function is a variable, in C the name of a function is a constant. It is, in fact, a pseudopointer, much like the name of a statically allocated array. Thus we cannot reassign getY, but we can create a pointer that points to what getY points to. This thing that getY points to is known as a function object or a closure. A true pointer that points to a function object is known as a function pointer. We will discuss function pointers in a later chapter.

Here is a program that uses our function:

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include <string.h>

    int getY(void);                     //function declarations go here

    int
    main(int argc,char **argv)
        {
        int result;

        result = getY();                //function call

        printf("getY returned %d\n",result);

        return 0;
        }

    int                                 //function definitions go here
    getY(void)
        {
        int m = 5;
        int x = 9;
        int b = -3;
        return m * x + b;
        }

There are a number of points to go over with respect to this program.

  1. The first is we place all the signatures of the functions we write near the top of the file. The signatures must precede any calls (or uses) of the function. A function signature by itself is known as a declaration, as it tells the compiler that the function exists and is defined elsewhere. Another name for a function declaration is prototype.
  2. Second, to call a function, we give the name of the function followed by a parenthesized list of the things we want to send to it. Since the function getY needs nothing, we send nothing, signified by the empty parentheses.
  3. Third, we place our function definitions after the definition of main.

This placement of function definitions is not a hard and fast rule. We could have written our program like this:

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include <string.h>

    int                                 //function definitions go here
    getY(void)
        {
        int m = 5;
        int x = 9;
        int b = -3;
        return m * x + b;
        }

    int
    main(int argc,char **argv)
        {
        int result;

        result = getY();                //function call

        printf("getY returned %d\n",result);

        return 0;
        }

In this version, there is no need for separate function declarations since definitions include function signatures. Which style you use is a matter of choice.

One final comment on the function declaration lines. The use of these declarations is strongly encouraged. Among other things, you will eventually encounter situations where they are mandatory. For example, consider a main routine that calls functions foo and bar. During execution, foo will occasionally make calls to bar for computations, and bar might also have to make a call to foo. It is impossible to identify an order to define main and foo and bar that the C compiler will accept without the use of function declaration lines.

Passing arguments

The getY function, as written above, is not too useful in that we cannot use it to compute similar things, such as the y-value for a different value of x. This is because we "hard-wired" the values of b, x, and m in the definition of the function.

A hallmark of a good function is that it lets you compute more than one thing. We can modify our getY function to take in the value of x in which we are interested. In this way, we can compute more than one value of y. We do this by passing in some information. This information that is passed to a function in a function call is known as an argument21, in this case, the value of x:

    int
    getY(int x)
        {
        int slope = 5;
        int intercept = -3;
        return slope * x + intercept;
        }

Note that we have moved the variable x from the body of the function to between the parentheses. We have also refrained from giving it a value since its value is to be sent to the function when the function is called. What we have done is to parameterize the function to make it more general and more useful. The variable x is now called a formal parameter since it sits between the parentheses in the first line of the function definition.

Now we can compute y for an infinite number of x's:

    int
    main(int argc,char **argv)
        {
        int result;

        result = getY(9);              
        printf("getY(9) returned %d\n",result);     //should be: 42

        result = getY(0);              
        printf("getY(0) returned %d\n",result);     //should be: -3

        result = getY(-2);              
        printf("getY(-2) returned %d\n",result);    //should be: -13

        return 0;
        }

What if we wish to compute a y-value for a given x for a different line? One approach would be to pass in the slope and intercept as well as x:

    int
    getY(int x,int m,int b)
        {
        int ans = m * x + b;
        return ans;
        }

If we wish to calculate using a different line, we just pass in the new slope and intercept along with our value of x. This certainly works as intended, but is not the best way. One problem is that we keep on having to type in the slope and intercept even if we are computing y-values on the same line. Anytime you find yourself doing the same tedious thing over and over, be assured that someone has thought of a way to avoid that particular tedium. If so, how do we customize our function so that we only have to enter the slope and intercept once per particular line? We'll have to postpone these thoughts until we learn about another data structure called objects.

You should note that, in the last version of our getY function, we introduced a local variable ans. We calculated an answer in ans and then that value was returned to the calling routine. The variable ans `disappears' when the function quits. That is, the space that was allocated for it is freed and made available for other uses.

In any case, the things you should take away, so far, about functions are:

This last point is very important. Whoever calls a function needs to handle the return value either by assigning it to a variable or by passing it immediately to another function (nested function calls). Here is an example of the former:

    y = square(x);
    z = square(y);

and here is an example of both the former and the latter:

    z = square(square(x));

Both approaches yield identical results.

The Function and Procedure Patterns

When a function calculates (or obtains) a value and returns it, we say that it implements the function pattern. If a function does not have a return value, we say it implements the procedure pattern.

The square function mentioned in the previous section is an example of the function pattern:

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

This function takes a value, stores it in x, computes the square of x and returns the result of the computation.

In contrast, here is an example of the procedure pattern:

    void
    displayLine(int m,int b)
        {
        printf("y = %dx + %d",m,b);
        return;
        }

We use the void return type to indicate procedures. Also, we give no value for the return.

Almost always, a function that implements the function pattern does not print anything, while a function that implements the procedure pattern often does22. A mistake often made by beginning programmers is to print a calculated value rather than to return it. So, when defining a function, you should ask yourself, should I implement the function pattern or the procedure pattern?

Most of the functions you will implement in this class follow the function pattern.

The dangers of functions and statically allocated arrays

A common mistake mistake made by beginners to have a function define a statically allocated array and then attempt to return the array:

    int *
    bundle3(int a,int b,int c)
       {
       int bundle[3];
       bundle[0] = a;
       bundle[1] = b;
       bundle[2] = c;
       return bundle;
       }

    ...
    int *p = bundle3(x,y,z); //the returned array's memory may be reused!

Modern C compilers will flag this attempt to return bundle. The reason this is a bad idea is, once the function returns, the memory reserved for the statically allocated array is reclaimed by the system. Remember that we had mentioned above that any statically declared variables are released/destroyed when a function completes.

While the "array" p, in the example above, may appear normal for a while, once that memory allocated to bundle is reused, the elements of p will be corrupted. Note that this memory reuse only affects arrays statically allocated within a function; arrays statically allocated outside of functions last the lifetime of the program.

The proper way to implement the bundle3 function is to dynamically allocate the array:

    int *
    bundle3(int a,int b,int c)
       {
       int *bundle = malloc(sizeof(int) * 3);
       //check for malloc failure omitted
       bundle[0] = a;
       bundle[1] = b;
       bundle[2] = c;
       return bundle;
       }

    ...
    int *p = bundle3(x,y,z); //the returned array's memory will NOT be reused!

Our function now returns a pointer to a location that contains three integers. Since that location was allocated dynamically, it remains available after the function has completed. The user can now reference the three values that were placed in bundle. However, the user should also remember to free that memory when it is no longer needed. Otherwise, repeated calls to this function will result in a memory leak that could cause issues over time.

lusth@cs.ua.edu


Strings Top More about FunctionsFunctions Contents