Main

January 16, 2004 (Friday)

Functions (K&R § 1.7, Chapter 4)

So far, all the programs that we have written 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("}");
}
function1.c

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 erroneous 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, in advance, 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 I/O functions. When we #include this header file, the compiler will see the function prototypes 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 function definition. So if you 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 formal 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--;

have essentially the same effect, namely, the value of len is decreased by one. Because we are not directly using the result of the operator, the return value is irrelevant.

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 of 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 subsequent classes). As a result any changes to 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 (len) 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, but if we were to add more elements to the array during its initialization in main(), we would have to update the #define appropriately.

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. This is demonstrated by the following example:

#include	<stdio.h>

#define MAX_COL 3

/* No need for function prototypes because all the functions
 * are defined before they are called.
 */

/* Add 1 to all elements in the 2-D array */
void addone(int ar[][MAX_COL], int nrows, int ncols)
{
	int	r, c;

	for (r = 0; r < nrows; r++)
		for (c = 0; c < ncols; c++)
			++ ar[r][c];
}

void display(int ar[][MAX_COL], int nrows, int ncols)
{
	int	r, c;

	for (r = 0; r < nrows; r++) {
		for (c = 0; c < ncols; c++)
			printf("%3d", ar[r][c]);
		putchar('\n');
	}

}

int
main()
{
	int twodim[][MAX_COL] = { { 1, 2, 3 },
			          { 4, 5, 6 } };

	addone(twodim, sizeof(twodim)/sizeof(twodim[0]),
			 sizeof(twodim[0])/sizeof(twodim[0][0]));
	display(twodim, sizeof(twodim)/sizeof(twodim[0]),
			 sizeof(twodim[0])/sizeof(twodim[0][0]));

	return 0;
}
twodim.c

Note that we can calculate the number of rows in the two dimensional array with the expression:

sizeof(twodim)/sizeof(twodim[0])

and we can calculate the number of columns in each of the rows of the two dimensional array using the expression:

sizeof(twodim[0])/sizeof(twodim[0][0]))

As in the previous example, we can use this sizeof idiom only in the function where the array is defined (i.e. main()). The value of sizeof(ar) in addone() and display() is 4 (and not 24) because these functions receive a pointer (or more precisely, a pointer to an array of integers) and the sizeof a pointer (on a 32-bit machine) is 4.

As an aside, instead of calculating and passing in the number of columns in each row, we could have simply just used MAX_COL in each of the two functions instead of ncols.

Macros with Arguments (K&R § 4.11.2)

The definition:

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

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.

Subtle bugs with macros

Despite our efforts to carefully parenthesize each use of the argument in the macro, there is still a subtle bug, in the CUBE macro. Consider the following (somewhat artificial) statement:

	int b = !CUBE(0);

This initialization is using the logical not operator (!) on the CUBE macro which has 0 as its argument. C does not have a boolean type. Instead integer (or pointer) values are commonly used to represent boolean values. A value that is non-zero is treated as true whereas a value that is zero is treated as false when used in the context of a conditional. When we use the ! operator on a non-zero value, zero is the result; when used on a zero value, the ! operator returns one.

Intuitively, it may appear that the above initialization will compute the cube of 0, which is 0. We then apply the ! operator which returns 1 and assigns it to b. Unfortunately, this is not what happens. Instead, the preprocessor expands the above statement to:

	int b = !(0)*(0)*(0)

Because the ! operator has higher precedence than * (see K&R p.53), the logical not will be performed first on the first parenthesized (0), giving 1. Then 1 is multiplied twice by 0 giving 0 as the final result which is then assigned to b, not 1. To get the result we intended, we should surround the entire expression by parenthesis when we #define the CUBE macro:

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

Now the macro behaves as originally intended and 1 will be assigned to b in the initialization.

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

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

<ctype.h> Macros (K&R § 2.7)

The standard C library defines a collection of macros which can be used to test the attributes of a character. This macros are all defined in the ctype.h header file. Some of the macros defined in this header file are:

isalpha() check if the character is alphabetic
isdigit() check if the character is a digit
isalnum() check if the character is alphabetic or a digit
isspace() check if the character is a whitespace (e.g. tab, space, newline)
ispunct() check if the character is a punctuation symbol.
islower() check if the character is a lowercase letter.
isupper() check if the character is an uppercase letter.
isprint() check if the character is printable

For example

	char	ch1 = '!', ch2 = '\t', ch3 = 'A';

	ispunct(ch1);		/* true */
	isprint(ch2);		/* false */
	isalnum(ch3);		/* true */
	isupper(ch3);		/* true */
	isdigit(ch3);		/* false */

These functions return true (i.e. non-zero) if the given character satisfies the macro and false (i.e. 0), otherwise.

ARRAY_SIZE macro

We can use #define to create a macro that will calculate how many elements are in an array:

#define ARRAY_SIZE(a)	(sizeof(a) / sizeof((a)[0]))

We can then use this macro in the main() function of our two-dimensional array example above to calculate the total number of rows (i.e. the number of one dimensional arrays) in the 2D array as well as the total number of columns (i.e. the total number of elements in each row) as follows:

	...
	addone(twodim, ARRAY_SIZE(twodim), ARRAY_SIZE(twodim[0]));
	display(twodim, ARRAY_SIZE(twodim), ARRAY_SIZE(twodim[0]));
	...

Notice that these macros make the code look somewhat cleaner.


Last modified: January 31, 2004 09:56:21 NST (Saturday)