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("}"); }
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();
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.
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.
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)
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