January 14 (Wednesday) January 19 (Monday)
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
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();
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 (++
).
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.
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
.
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.
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))
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)