Friday, January 17, 2003

Functions (K&R § 1.7, Chapter 4)

So far, all the programs that we have written so far have been written as one function, namely main(). In practice, software becomes more manageable when we break it down in into smaller units called functions. We have already used several functions already including, for example, printf(), puts() and a collection of string functions (e.g. strlen(), strcpy etc.). Now we are going to define our own functions.

Consider the following program which defines two functions (in addition to main()).


#include	<stdio.h>

/* Function declarations */
void mod_array(int array[], int len);
void show_array(int array[], int len);

#define	CUBE(x)	(x) * (x) * (x)

int
main()
{
	int	array[] = { 2, 3, 5, 7, 11 };
	int	len = sizeof(array)/sizeof(array[0]);

	printf("sizeof(array) in main() = %d bytes\n", sizeof(array));

	printf("'len' in main() is %d\n", len); 
	mod_array(array, len);
	printf("'len' in main() is still %d\n", len); 
	show_array(array, len);
	return 0;
}

void
mod_array(int array[], int len)
{
	printf("sizeof(array) in mod_array() = %d bytes\n", sizeof(array));
	printf("'len' in mod_array() is initially %d\n", len); 
	while (--len >= 0)
		array[len] = CUBE(array[len] + 1);
	printf("'len' in mod_array() is now %d\n", len); 
}

void
show_array(int array[], int len)
{
	int	i;
	printf("array is { ");
	for (i = 0; i < len; i++)
		printf("%d%s", array[i], (i == len - 1) ? " " : ", ");
	puts("}");
}


Function prototypes (K&R § 1.7)

At the top of program, we declare the functions mod_array() and show_array() by providing function prototypes. Each prototype gives the function's return type, its name and its respective arguments. The compiler will use this prototype to ensure that all subsequent function invocations are correct (e.g. that the function arguments present during invocation correspond to both the number and type of arguments provided in the function prototype.) When processing the function prototypes, the compiler ignores the names of the parameters. So, for example, the above prototypes could have been equivalently written as:

void mod_array(int [], int);
void show_array(int [], int);

Without function declarations, a compiler would produce warnings if it sees a function invocation without function prototype. The compiler may then make assumptions about the function (for example, that the function will return an int) that may not match the actual function definition. By providing the function prototypes, we are telling the compiler the return type and number and types of all of its arguments so that the compiler can make sure that the function is being used correctly.

The #include file stdio.h contains function prototypes (or declarations) for printf(), puts() and many other functions. When we #include this header file, the compiler will process the prototype and when we use one of these functions, the compiler can make sure that we are using it correctly, for example, by making sure that we are using the return type (if any) correctly and that we are specifying the right number and types of arguments.

Incidentally, if we put the main() program at the bottom, then the compiler can deduce the return type and the numbers/types of function arguments directly from the definition. So if place all the function definitions in the file before their actual use, there is no need to provide function prototypes for them.

void (K&R § 4.2, 1.9)

None of the functions that we define return a value, therefore in the prototype and in the definition, we specify the return type as void. Calling a function with a void return type on the right hand side of an assignment would produce a compiler error. For example:

int i = mod_array(array, len); /* Error! */

The void keyword is also used in a function prototype to tell the compiler that a function has no arguments at all. For example, the declaration:

void some_function(void);

indicates the some_function takes no parameters and does not return any value. The function definition should also use void in its argument list. To call the function, simply write some_function();

Preincrement/Predecrement vs. Postincrement/Postdecrement (K&R § 2.8)

Note that in the mod_array() function we use the predecrement operator (--len) to decrease the value of len This implies that we update the values in the array backwards (which is perfectly okay).

The predecrement operator (e.g. --len) decrements the value of its operand and returns that value as the result of the operation. The postdecrement (e.g. len--) would decrease the value of len but return the previous value of len.

In the mod_array() function, we use the predecrement operator and check if the (current) value of len is greater than or equal to zero. Doing this ensures we don't access one element beyond the length of the array, and that we update all elements of the array. If, instead, we had written

while (len-- >= 0)

then when len got down to zero, the while loop test would be executed again. len-- would decrement len (making it -1), but zero would be returned as the result of the postdecrement operation. Because 0 >= 0, the loop body would execute again and we would end up writing to array[-1] which is outside the array. Bad things ensue. (Try it and see.)

We could correct this by writing:

while (len-- > 0)

instead.

Note that if we had used:

while (--len > 0)

then the first element of the array would not be updated.

The moral of the story: when you use post/predecrement operators, make sure you know what you are doing.

Note that as a stand-alone statement, --len; and len--; are essentially the same.

The same ideas above to the pre/postincrement operators.

Parameter Passing (pass by value) (K&R § 1.8)

Nearly all parameters are passed by value. What this means is that the function is given a copy (i.e. the value) of the parameter rather than a reference to the parameter itself. Therefore any changes to the function's formal parameter will not be reflected in the actual parameter used in the function call. For example, in the above program, the value of len is passed to the mod_array() function. Despite the fact that mod_array() modifies this parameter, the original value of len in main() is preserved because mod_array() was modifying a copy if this variable.

Passing Arrays (K&R § 1.8)

One notable exception to this rule occurs when we pass an array. When we pass an array, the entire array is not copied to the function. Instead a pointer to the first element of the array is passed instead (we'll be discussing pointers in more detail in the next class). As a result any changes to the the array elements will also be made to the array that was passed into the function.

One consequence of passing a pointer to the first element of the array is that in the context of the function that accepts the array parameter, we have no idea how many elements the array has. When we ask for the sizeof the array, we will get the size of a pointer to the first element of the array (which, on a machine with 32-bit addresses , would be 4) -- not the size of the array itself. Therefore, in mod_array() and show_array(), we cannot use the

sizeof(array)/sizeof(array[0])

idiom to determine how many elements are in the array. Instead, we determine the number of elements in the array in the main() function and pass this value to the other two functions.

We could also use a #define macro to denote the number of elements in the array and then use the macro in the definition of the array in main() and as an upper bound for loops in the functions that iterate over the array.

Incidentally, note that when we pass an array, we simply specify the array name as the actual parameter -- we do not adorn the array name with []. In the function's formal parameter list, however, we use [] but there is no need to specify a number inside the brackets.

When passing arrays with more than one dimension, we again only specify the array name as the actual parameter during the function call. In the formal parameter list of the function definition, the number of elements in each dimension beyond the first must be specified. You can see an example of this in the function definition of the border() function of Assignment #1:

int border(char ar[][MAX_COL], int nrow, int ncol)

Macros with Arguments (K&R § 4.11.2)

The macro:

#define CUBE(x) (x) * (x) * (x)

macro definition specifies a macro with an argument. Everytime the macro is used in the program:

array[len] = CUBE(array[len] + 1);

the macro and its arguments will be expanded out by the preprocessor.

array[len] = (array[len] + 1) * (array[len] + 1) * (array[len] + 1)

Note that in the macro definition, each time the argument is used, parenthesis are placed around it. If the macro were defined as:

#define CUBE(x) x * x * x

then its use in the above program would translate to:

array[len] = array[len] + 1 * array[len] + 1 * array[len] + 1

Because multiplication is of higher precedence than addition, the above expression would be evaluated as:

array[len] = array[len] + (1*array[len]) + (1*array[len]) + 1

which is clearly not the same thing.

You must be very careful when using macros with arguments because the arguments are textually expanded on each use. For example, using the CUBE macro with the argument ++array[len] may produce unexpected results.

Important: Note that there is no space between the name of the macro and the left parenthesis that starts the argument list. Therefore, the follow macro definition is not the same as above:

#define CUBE (x) (x) * (x) * (x)

Last modified: Mon Jan 20 02:02:17 2003