Functions Top ConditionalsMore about Functions Contents

More about Functions

We have already seen some examples of functions, some user-defined and some built-in. For example, we have used the built-in functions, such as printf and defined our own functions, such as square. C has many built-in, or predefined, functions. No one, however, can anticipate all possible tasks that someone might want to perform, so most programming languages allow the user to define new functions. C is no exception and provides for the creation of new and novel functions. Of course, to be useful, these functions should be able to call built-in functions as well as other programmer created functions.

For example, a function that determines whether a given number is odd or even is not built into C but can be quite useful in certain situations. Here is a definition of a function named isEven which returns true (integer 1) if the given number is even, false (integer 0) otherwise:

    int
    isEven(int n)
        {
        return n % 2 == 0;
        }

Even though this function definition is very short, there is a lot going on. If you ever take a course on compilers, you will learn how this code is turned into language your computer understands. We, however, will stay on a higher plane. First, we will talk about the purpose of a function definition. Later, we'll talk about the syntax of a function definition. Finally, will talk about the mechanics of a function definition and a function call.

The purpose of functions

C programming is all about functions. We define a function to do some task. This function calls other functions, some that are built-in and some that are not. For those that are not, we define those as well. These newly defined, lower-level functions, in turn, call more functions, some that are built-in and some that are not. And so it goes, until our lowest-level functions call only built-in functions or functions we have previously defined.

Even in higher-level languages, such as Java, the same process holds as well. So learning to be comfortable with defining and calling functions is paramount to being a good programmer and to being a good Computer Scientist. Consider functions the language of Computer Scientists.

Function syntax

Recall that the words of a programming language include its primitives, keywords and variables. A function definition corresponds to a sentence in the language in that it is built up from the words of the language. And like human languages, the sentences must follow a certain form. This specification of the form of a sentence is known as its syntax. Computer Scientists often use a special way of describing syntax of a programming language called the Backus-Naur form (or Bnf). Here is a simplfied description of the syntax of a C function definition using Bnf:

    functionDefinition : signature block
    
    signature : type VARIABLE OPEN_PARENTHESIS parameterList CLOSE_PARENTHESIS

    parameterList : VOID
                  | type variable [COMMA type variable]*

    block : OPEN_BRACE statement+ CLOSE_BRACE

In Bnf, the items in all capital letters represent the words and punctuation in the program. A plus sign means "one or more" while an asterisk means "zero or more". A question mark, not shown in this example, means "zero or one". The first Bnf rule says that a function definition is composed of two pieces, a signature followed by a block, the more formal name for a function body. The signature, discussed previously, starts with a type of some kind, followed by a variable, followed by an open parenthesis, followed by a parameter list, and finally followed by a close parenthesis. The parameterList rule tells us that the formal parameters in the function signature consist of either the keyword void (which signifies no formal parameters) or a type followed by a variable. If there is more than one formal paramter, they are separated by a comma. A block is composed of an open brace, followed by one or more statements, and ending with a close brace.

As we can see from the Bnf rules, formal parameters are variables that will be bound to the values supplied in the function call. In the particular case of isEven, from the previous section, the variable x will be bound to the number whose evenness is to be determined. As noted earlier, it is customary to call x a formal parameter of the function isEven.

The syntax of function calls

Once a function is defined, it is used by calling the function with arguments. A function is called by supplying the name of the function followed by a parenthesized, comma separated, list of expressions. Those expressions may be literals, variables, array accesses, function calls, combinations thereof, and more.

In Bnf, a description of a function call would look like:

    functionCall : ID OPAREN argumentList? CPAREN

    argumentList : expression [COMMA expression]*

Here are some calls to the isEven function defined above:

    x = isEven(y);
    process(isEven(q+r),s);
    a = isEven(isEven(b));

Note that the formal paramaters include a type, but the arguments do not. In Computer Science speak, we say that the values of the arguments are bound to the formal parameters during the processing of a function call.

In general, if there are n formal parameters, there should be n arguments.23 Furthermore, the value of the first argument is bound to the first formal parameter, the second argument is bound to the second formal parameter, and so on. Moreover, all the arguments are evaluated before being bound to any of the parameters. If an argument is a literal, the corresponding formal parameter is bound to that literal value. If an argument is more complicated, C determines the value of that more complicated expression before the formal parameter receives it.

Once the evaluated arguments are bound to the parameters, then the body of the function is evaluated. Most times, the expressions in the body of the function will reference various variables, some of which may be the formal parameters, while some may not. For those that are not formal parameters, how does C find the values of those variables? That question is answered in the chapter on scope.

Returning from functions

Usually, the evaluation of a function body proceeds, statement by statement, until there are no more statements or when a return statement is encountered. If the return statement has an expression, the value of that expression is determined and becomes the return value of the function. The return value should match the return type of the function.

A function body may have more than one return statement; the first one reached wins. Look at this example:

    int
    safeDivide(int x,int y)
        {
        if (y == 0)             //conditional
            return 0;
        else
            return x / y;
        }

We haven't covered conditional statements yet, but the code is easy enough to read. When this function is called, if the value of the second argument, which will be bound to y, is equal to zero, then the function returns zero. If not, then the function returns the value of the first argument, bound to x, divided by the value of the second, bound do y.

When a return statement is processed, execution of the function body terminates. Thus, it is possible that some statements in the function can never be processed. For example, consider this version of safeDivide:

    int
    safeDivide(int x,int y)
        {
        if (y == 0)             //conditional
            return 0;
        else
            return x / y;

        return 1;               //not reached
        }

Since either y is zero or it is not, one of the two returns in the if statement must be reached. Once either is reached, the processing of the function body terminates. Therefore, the return statement at the end of the function can never be reached.

Changing arguments using the procedure pattern

Consider writing a function to swap two values:

    void
    swap(int x,int y)
        {
        int temp = x;   //line 1
        x = y;          //line 2
        y = temp;       //line 3
        }

The function, as written, correctly swaps the values of the formal parameters x and y. But can we use the function to swap the values of variables passed to the function? For example, what does the following code fragment produce?

    //test (with swap defined)
    int a = 4;
    int b = 23;
    printf("a was %d, b was %d\n",a,b);
    swap(a,b);
    printf("a now is %d, b now is %d\n",a,b);

Surprisingly, we get the following output:

    a was 4, b was 23
    a now is 4, b now is 23

Even though swap correctly swaps the values of the formal parameters, the arguments to swap remained unchanged. If we look at memory, we can see why. Just before swap is called, both a and b refer to memory initialized to 4 and 23, respectively:

When swap is called, the space for the formal parameters is allocated and the formal parameters are given their initial values:

As the body of swap runs, the variable temp is created (line 1) and initialized to the value of x:

After x gets the value of y (line 2), we have:

After y gets the value of temp (line 3), we have:

We see that swap has indeed swapped the values of the formal parameters. At this point, swap returns and the space allocated for x, y, and temp is reclaimed by the system. Note that through this whole process, the variables a and b remain unchanged.

So how do we get a function to swap the values of a and b? We do so by passing the addresses of a and b to the swap function. Here is the new version of swap:

    void
    swap2(int *x,int *y)
        {
        int temp = *x;
        *x = *y;
        *y = temp;
        }

Taking the address of a variable creates a pointer. Hence, the formal parameters need to be declared int * (pointer to int). Note the formals x and y look the same as if we passed in an array of integers to each. In fact, C does not know the difference, which is why you must pass in the size of an array to a function when you pass an array to that function. If you wanted to, you could consider x and y to be arrays of a single element each, so an alternative, but equivalent, version of swap would be:

    void
    swap3(int *x,int *y)
        {
        int temp = x[0];
        x[0] = y[0];
        y[0] = temp;
        }

We will stick with the swap2 version, since that is how 99.999% of the world would write swap.

To call swap, we send the addresses of the variables whose values are to be swapped:

    //test (with swap2 defined)
    int a = 4;
    int b = 23;
    printf("a was %d, b was %d\n",a,b);
    swap(&a,&b);
    printf("a now is %d, b now is %d\n",a,b);

Now, we get the results we desire:

    a was 4, b was 23
    a now is 23, b now is 4

Let's look again at memory while our code is running. This time, since we are manipulating addresses, we will pay attention to the addresses of a and b. For illustration, we will assume the value of a is stored at memory location 1002, while b's value is stored at location 1010. Here is the state of memory just before swap2 is called:

When swap2 is called, formal parameters x and y are initialized to the address of a and the address of b, respectively:

Next, the body of swap2 is executed, causing the variable temp to be created. The line:

    int temp = *x;

says, "Don't initialize temp with the value of x. Instead, intialize temp to the value at the address found in x." This leaves memory looking like:

Now, the line:

    *x = *y;

is executed. This line says, "Don't update x with the value of y. Instead, update the address found in x with the value at the address found in y." Now, memory looks like:

Finally, the line:

    *y = temp;

is executed. This line says, "Don't update y with the value of temp. Instead, update the address found in y with the value of temp." After this update, memory looks like: Now, memory looks like:

At this point, swap2 returns and we see that the values of a and b have been exchanged, as desired.

Functions that "return" multiple items

The C language is limited somewhat because a function can return only one thing. However, sometimes you need more than one piece of information from a function. Consider a function which returns a dynamically allocated array of a random length:

    int *
    randomlySizedIntegerArray(void)
        {
        int size;
        char **array;

        size = random() % 100 + 1; //size will be between 1 and 100
        array = malloc(sizeof(int) * size); //check for malloc failure omitted

        return ???
        }

What should this function return? It can return the array, but without knowing the size, the caller of this function won't know how many slots are in the array it is receiving. The function can return the size, but then the caller will not have access to the array. One solution is to return the array, but require the caller to pass the address of a variable; that variable will be updated by the function, much like the swap2 function in the previous section updated variables supplied by the caller. Here is a possible implementation:

    int *
    randomlySizedIntegerArray(int *size)
        {
        int *array;

        *size = random() % 100 + 1; //size will be between 1 and 100
        array = malloc(sizeof(int) * *size); //check for malloc failure omitted

        return array;
        }

A caller of this function would pass in the address of the variable that is to hold the randomly generated size:

    int arraySize;
    int *array;

    array = randomlySizedIntegerArray(&arraySize); //pass in address of arraySize
    //arraySize now holds the size of the array

This is an important rule to remember: you cannot change the value of a variable by passing it to a function. If you wish to change its value, you must pass the address of the variable. An easy way to write such functions is to first assume a value is being sent, as in the original, non-working, swap function. Then, for every formal parameter that is to receive an address, place an asterisk before all occurences in the function signature and the function body. Then, place an ampersand before the variable in the function call. If you do this for swap, you end up with the correctly working swap2. By following this advice, you will ensure proper updates to any variable whose address is to be sent to the function.

Function pointers

In the same way we can have a variable point to an array, we can have a variable that points to a function. Suppose with have a function whose signature is:

    char *getString(int,double);

The function getString is a function that, when passed an integer and a double in that order, returns a string. To create a variable named g that can point to this function, we start with the function signature, wrapping the name of the function in the signature with parentheses:

    char *(getString)(int,double);

Then, we place an star in front of the function name (inside the parentheses we just added):

    char *(*getString)(int,double);

Finally, we substitute the variable name g for the function name getString:

    char *(*g)(int,double);

Given the original signature and this definition of g (plus a definition of getString somewhere), we can do this:

    char *getString(int,double);  //function prototype for getString
    char *(*g)(int,double);       //creating a function pointer named g
    g = getString;                //getString is NOT called here
    char *s = g(2,3.3);           //getString IS called here

The type of variable g is char *(*)(int,double), and we will refer to this as the long form of a function pointer. This long form is rather unwieldy to type over and over, so we can use the typedef mechanism to create a shorthand for this type. If we add the keyword typedef in front of the type and change the variable name g to a name that represents the new type:

    typedef char *(*FPtr)(int,double);

we can make a new type called FPtr; this new type can be used to define variables that point to functions like getString:

    typedef char *(*FPtr)(int,double);  //creating a type named FPtr
    char *getString(int,double);        //function prototype for getString
    FPtr g;                             //creating a function pointer named g
    g = getString;                      //getString is NOT called here
    char *s = g(2,3.3);                 //getString IS called here

We will refer to the typedef'ed version of the function pointer as the short form.

With function pointers, we can now pass functions as arguments and return functions as return values. When passing a function like getString as an argument, the accepting formal parameter can be declared with the long form or the short form:

    int f(char *(*u)(int,double));  //long form formal parameter
    int g(FPtr v);                  //short form formal parameter
    ...
    int x = f(getString);           //call to function f, passing getString
    int y = g(getString);           //call to function g, passing getString

Presumably, functions f and g call getString via the formal parameters u and v, respectively.

When defining a function h that returns a function like getString, we must use the short form to specify the return type:

    FPtr g(void) { return getString; }
    ...
    FPtr h = g();

We will make use of function pointers when we explore the map pattern in the Loops chapter.

lusth@cs.ua.edu


Functions Top ConditionalsMore about Functions Contents