4.2. The type of functions
All functions have a type: they return a value of that type whenever
they are used. The reason that C doesn't have ‘procedures’, which
in most other languages are simply functions without a value, is that in
C it is permissible (in fact well-nigh mandatory) to discard the eventual
value of most expressions. If that surprises you, think of an
assignment
a = 1;
That's a perfectly valid assignment, but don't forget that it has a
value too. The value is discarded. If you want a bigger surprise, try
this one:
1;
That is an expression followed by a semicolon. It is a well formed
statement according to the rules of the language; nothing wrong with it,
it is just useless. A function used as a procedure is used in the same
way—a value is always returned, but you don't use it:
f(argument);
is also an expression with a discarded value.
It's all very well saying that the value returned by a function can be
ignored, but the fact remains that if the function really does
return a value then it's probably a programming error not to do something
with it. Conversely, if no useful value is returned then it's a good idea
to be able to spot anywhere that it is used by mistake. For both of those
reasons, functions that don't return a useful value should be declared to
be void .
Functions can return any type supported by C (except for arrays and
functions), including the pointers, structures and unions which are
described in later chapters. For the types that can't be returned from
functions, the restrictions can often be sidestepped by using pointers
instead.
All functions can be called recursively.
4.2.1. Declaring functions
Unfortunately, we are going to have to use some jargon now. This is
one of the times that the use of an appropriate technical term really
does reduce the amount of repetitive descriptive text that would be
needed. With a bit of luck, the result is a shorter, more accurate and
less confusing explanation. Here are the terms.
- declaration
- The point at which a name has a type associated with it.
- definition
- Also a declaration, but at this point some storage is reserved for
the named object. The rules for what makes a declaration into a
definition can be complicated, but are easy for functions: You turn a
function declaration into a definition by providing a body for the
function in the form of a compound statement.
- formal parameters
- parameters
- These are the names used inside a function to refer to its
arguments.
- actual arguments
- arguments
- These are the values used as arguments when the function is actually
called. In other words, the values that the formal parameters
will have on entry to the function.
The terms ‘parameter’ and ‘argument’ do tend to get used
as if they were interchangeable, so don't read too much into it if you
see one or the other in the text below.
If you use a function before you declare it, it is implicitly declared
to be ‘function returning int ’. Although this will
work, and was widely used in Old C, in Standard C it is bad
practice—the use of undeclared functions leads to nasty problems to
do with the number and type of arguments that are expected for them. All
functions should be fully declared before they are used. For example,
you might be intending to use a function in a private library called,
say, aax1 . You know that it takes no arguments and returns
a double . Here is how it should be declared:
double aax1(void);
and here is how it might be used:
main(){
double return_v, aax1(void);
return_v = aax1();
exit(EXIT_SUCCESS);
} Example 4.1
The declaration was an interesting one. It defined
return_v , actually causing a variable to come into
existence. It also declared aax1 without defining it; as we
know, functions only become defined when a body is provided for
them. Without a declaration in force, the default rules mean that
aax1 would have been assumed to be int , even
though it really does return a double —which means that
your program will have undefined behaviour. Undefined behaviour is
disastrous!
The presence of void in the argument list in the
declaration shows that the function really takes no arguments. If it had
been missing, the declaration would have been taken to give no
information about the function's arguments. That way, compatibility with
Old C is maintained at the price of the ability of the compiler to
check.
To define a function you also have to provide a body for
it, in the form of a compound statement. Since no function can itself
contain the definition of a function, functions are all separate from
each other and are only found at the outermost level of the program's
structure. Here is a possible definition for the function
aax1 .
double
aax1(void) {
/* code for function body */
return (1.0);
}
It is unusual for a block-structured language to prohibit you from
defining functions inside other functions, but this is one of the
characteristics of C. Although it isn't obvious, this helps to improve
the run-time performance of C by reducing the housekeeping associated
with function calls.
4.2.2. The return statement
The return statement is very important. Every function
except those returning void should have at least one, each
return showing what value is supposed to be returned at
that point. Although it is possible to return from a function by falling
through the last } , unless the function returns
void an unknown value will be returned, resulting in
undefined behaviour.
Here is another example function. It uses getchar to read
characters from the program input and returns whatever it sees except
for space, tab or newline, which it throws away.
#include <stdio.h>
int
non_space(void){
int c;
while ( (c=getchar ())=='\t' || c== '\n' || c==' ')
; /* empty statement */
return (c);
}
Look at the way that all of the work is done by the test in the
while statement, whose body was an empty statement. It is
not an uncommon sight to see the semicolon of the empty statement
sitting there alone and forlorn, with only a piece of comment for
company and readability. Please, please, never write it like this:
while (something);
with the semicolon hidden away at the end like that. It's too easy to
miss it when you read the code, and to assume that the following
statement is under the control of the while .
The type of expression returned must match the type of the function,
or be capable of being converted to it as if an assignment statement
were in use. For example, a function declared to return
double could contain
return (1);
and the integral value will be converted to double . It is
also possible to have just return without any
expression—but this is probably a programming error unless the
function returns void . Following the return
with an expression is not permitted if the function returns
void .
4.2.3. Arguments to functions
Before the Standard, it was not possible to give any information about
a function's arguments except in the definition of the function itself.
The information was only used in the body of the function and was
forgotten at the end. In those bad old days, it was quite possible to
define a function that had three double arguments and only
to pass it one int, when it was called. The program would
compile normally, but simply not work properly. It was considered to be
the programmer's job to check that the number and the type of arguments
to a function matched correctly. As you would expect, this turned out to
be a first-rate source of bugs and portability problems. Here is an
example of the definition and use of a function with arguments, but
omitting for the moment to declare the function fully.
#include <stdio.h>
#include <stdlib.h>
main(){
void pmax(); /* declaration */
int i,j;
for(i = -10; i <= 10; i++){
for(j = -10; j <= 10; j++){
pmax(i,j);
}
}
exit(EXIT_SUCCESS);
}
/*
* Function pmax.
* Returns: void
* Prints larger of its two arguments.
*/
void
pmax(int a1, int a2){ /* definition */
int biggest;
if(a1 > a2){
biggest = a1;
}else{
biggest = a2;
}
printf("larger of %d and %d is %d\n",
a1, a2, biggest);
} Example 4.2
What can we learn from this? To start with, notice the careful
declaration that pmax returns void . In the
function definition, the matching void occurs on the line
before the function name. The reason for writing it like that is purely
one of style; it makes it easier to find function definitions if their
names are always at the beginning of a line.
The function declaration (in main ) gave no indication of
any arguments to the function, yet the use of the function a couple of
lines later involved two arguments. That is permitted by both the old
and Standard versions of C, but must nowadays be considered to be
bad practice. It is much better to include information about the
arguments in the declaration too, as we will see. The old style is now
an ‘obsolescent feature’ and may disappear in a later version of
the Standard.
Now on to the function definition, where the body is supplied. The
definition shows that the function takes two arguments, which will be
known as a1 and a2 throughout the body of the
function. The types of the arguments are specified too, as can be
seen.
In the function definition you don't have to specify the type
of each argument because they will default to int , but this
is bad style. If you adopt the practice of always declaring arguments,
even if they do happen to be int , it adds to a reader's
confidence. It indicates that you meant to use that type, instead of
getting it by accident: it wasn't simply forgotten. The definition of
pmax could have been this:
/* BAD STYLE OF FUNCTION DEFINITION */
void
pmax(a1, a2){
/* and so on */
The proper way to declare and define functions is through the use of
prototypes.
4.2.4. Function prototypes
The introduction of function prototypes is the biggest
change of all in the Standard.
A function prototype is a function declaration or definition which
includes information about the number and types of the arguments that
the function takes.
Although you are allowed not to specify any information about a
function's arguments in a declaration, it is purely because of backwards
compatibility with Old C and should be avoided.
A declaration without any information about the arguments is
not a prototype.
Here's the previous example ‘done right’.
#include <stdio.h>
#include <stdlib.h>
main(){
void pmax(int first, int second); /*declaration*/
int i,j;
for(i = -10; i <= 10; i++){
for(j = -10; j <= 10; j++){
pmax(i,j);
}
}
exit(EXIT_SUCCESS);
}
void
pmax(int a1, int a2){ /*definition*/
int biggest;
if(a1 > a2){
biggest = a1;
}
else{
biggest = a2;
}
printf("largest of %d and %d is %d\n",
a1, a2, biggest);
} Example 4.3
This time, the declaration provides information about the function
arguments, so it's a prototype. The names first and
second are not an essential part of the declaration, but
they are allowed to be there because it makes it easier to refer to
named arguments when you're documenting the use of the function. Using
them, we can describe the function simply by giving its declaration
void pmax (int xx, int yy );
and then say that pmax prints whichever of the arguments
xx or yy is the larger. Referring to arguments
by their position, which is the alternative (e.g. the fifth argument),
is tedious and prone to miscounting.
All the same, you can miss out the names if you want to. This
declaration is entirely equivalent to the one above.
void pmax (int,int);
All that is needed is the type names.
For a function that has no arguments the declaration is
void f_name (void);
and a function that has one int , one double
and an unspecified number of other arguments is declared this way:
void f_name (int,double,...);
The ellipsis (...) shows that other arguments follow. That's useful
because it allows functions like printf to be written. Its
declaration is this:
int printf (const char *format_string,...)
where the type of the first argument is ‘pointer to const
char ’; we'll discuss what that means later.
Once the compiler knows the types of a function's arguments, having
seen them in a prototype, it's able to check that the use of the
function conforms to the declaration.
If a function is called with arguments of the wrong type, the presence
of a prototype means that the actual argument is converted to the type
of the formal argument ‘as if by assignment’. Here's an example: a
function is used to evaluate a square root using Newton's method of
successive approximations.
#include <stdio.h>
#include <stdlib.h>
#define DELTA 0.0001
main(){
double sq_root(double); /* prototype */
int i;
for(i = 1; i < 100; i++){
printf("root of %d is %f\n", i, sq_root(i));
}
exit(EXIT_SUCCESS);
}
double
sq_root(double x){ /* definition */
double curr_appx, last_appx, diff;
last_appx = x;
diff = DELTA+1;
while(diff > DELTA){
curr_appx = 0.5*(last_appx
+ x/last_appx);
diff = curr_appx - last_appx;
if(diff < 0)
diff = -diff;
last_appx = curr_appx;
}
return(curr_appx);
} Example 4.4
The prototype tells everyone that sq_root takes a single
argument of type double . The argument actually passed in
the main function is an int , so it has to be converted to
double first. The critical point is that if no prototype
had been seen, C would assume that the programmer had meant to pass an
int and an int is what would be passed. The
Standard simply notes that this results in undefined behaviour, which is
as understated as saying that catching rabies is unfortunate. This is a
very serious error and has led to many, many problems in Old C
programs.
The conversion of int to double could be
done because the compiler had seen a protoytpe for the function and knew
what to do about it. As you would expect, there are various rules used
to decide which conversions are appropriate, so we need to look at them
next.
4.2.5. Argument Conversions
When a function is called, there are a number of possible conversions
that will be applied to the values supplied as arguments depending on
the presence or absence of a prototype. Let's get one thing clear:
although you can use these rules to work out what to do if you
haven't used prototypes, it is a recipe for pain and misery in the long
run. It's so easy to use prototypes that there really is no excuse for
not having them, so the only time you will need to use these rules is if
you are being adventurous and using functions with a variable number of
arguments, using the ellipsis notation in the prototype that is
explained in Chapter 9.
The rules mention the default argument promotions and compatible
type. Where they are used, the default argument promotions
are:
- Apply the integral promotions (see Chapter 2) to the
value of each argument
- If the type of the argument is
float it is converted to
double
The introduction of prototypes (amongst other things) has increased
the need for precision about ‘compatible types’, which was not
much of an issue in Old C. The full list of rules for type compatibility
is deferred until Chapter 8, because we suspect that most C
programmers will never need to learn them. For the moment, we will
simply work on the basis that if two types are the same, they are
indisputably compatible.
The conversions are applied according to these rules (which are
intended to be guidance on how to apply the Standard, not a direct
quote):
- At the point of calling a function, if no prototype is in scope,
the arguments all undergo the default argument promotions.
Furthermore:
- If the number of arguments does not agree with the number of
formal parameters to the function, the behaviour is undefined.
- If the function definition was not a definition containing a
prototype, then the type of the actual arguments after promotion must
be compatible with the types of the formal parameters in
the definition after they too have had the promotions applied.
Otherwise the behaviour is undefined.
- If the function definition was a definition containing a
prototype, and the types of the actual arguments after promotion are
not compatible with the formal parameters in the prototype, then the
behaviour is undefined. The behaviour is also undefined it the
prototype included ellipsis (
, ... ).
-
At the point of calling a function, if a prototype is in
scope, the arguments are converted, as if by assignment, to the types
specified in the prototype. Any arguments which fall under the
variable argument list category (specified by
the ... in the prototype) still undergo the default
argument conversions.
It is possible to write a program so badly that you have a
prototype in scope when you call the function, but for the function
definition itself not to have a prototype. Why anyone should do this
is a mystery, but in this case, the function that is called must have
a type that is compatible with the apparent type at the
point of the call.
The order of evaluation of the arguments in the function call is
explicitly not defined by the Standard.
4.2.6. Function definitions
Function prototypes allow the same text to be used for both the
declaration and definition of a function. To turn a declaration:
double
some_func(int a1, float a2, long double a3);
into a definition, we provide a body for the function:
double
some_func(int a1, float a2, long double a3){
/* body of function */
return(1.0);
}
by replacing the semicolon at the end of the declaration with a
compound statement.
In either a definition or a declaration of a function, it serves as a
prototype if the parameter types are specified; both of the examples
above are prototypes.
The Old C syntax for the declaration of a function's formal arguments
is still supported by the Standard, although it should not be used by
new programs. It looks like this, for the example above:
double
some_func(a1, a2, a3)
int a1;
float a2;
long double a3;
{
/* body of function */
return(1.0);
}
Because no type information is provided for the parameters at the
point where they are named, this form of definition does not
act as a prototype. It declares only the return type of the function;
nothing is remembered by the compiler about the types of the arguments
at the end of the definition.
The Standard warns that support for this syntax may disappear in a
later version. It will not be discussed further.
Summary
- Functions can be called recursively.
- Functions can return any type that you can declare, except for
arrays and functions (you can get around that restriction to some
extent by using pointers). Functions returning no value should return
void .
- Always use function prototypes.
- Undefined behaviour results if you call or define a function
anywhere in a program unless either
- a prototype is always in scope for every call
or definition, or
- you are very, very careful.
- Assuming that you are using prototypes, the values of the
arguments to a function call are converted to the types of the formal
parameters exactly as if they had been assigned using
the
= operator.
- Functions taking no arguments should have a prototype with
(
void ) as the argument specification.
-
Functions taking a variable number of arguments must take at least
one named argument; the variable arguments are indicated
by ... as shown:
int
vfunc(int x, float y, ...);
Chapter 9 describes how to write this sort of
function.
4.2.7. Compound statements and declarations
As we have seen, functions always have a compound statement as their
body. It is possible to declare new variables inside any compound
statement; if any variables of the same name already exist, then the old
ones are hidden by the new ones within the new compound statement. This
is the same as in every other block-structured language. C restricts the
declarations to the head of the compound statement (or ‘block’);
once any other kind of statement has been seen in the block,
declarations are no longer permitted within that block.
How can it be possible for names to be hidden? The following example
shows it happening:
int a; /* visible from here onwards */
void func(void){
float a; /* a different 'a' */
{
char a; /* yet another 'a' */
}
/* the float 'a' reappears */
}
/* the int 'a' reappears */ Example 4.5
A name declared inside a block hides any outer versions of the same
name until the end of the block where it is declared. Inner blocks can
also re-declare that name—you can do this for ever.
The scope of a name is the range in which it has meaning.
Scope starts from the point at which the name is mentioned and continues
from there onwards to the end of the block in which it is declared. If
it is external (outside of any function) then it continues to the end of
the file. If it is internal (inside a function), then it disappears at
the end of the block containing it. The scope of any name can be
suspended by redeclaring the name inside a block.
Using knowledge of the scope rules, you can play silly tricks like
this one:
main () {}
int i;
f () {}
f2 () {}
Now f and f2 can use i , but
main can't, because the declaration of the variable comes
later than that of main . This is not an aspect that is used
very much, but it is implicit in the way that C processes declarations.
It is a source of confusion for anyone reading the file (external
declarations are generally expected to precede any function definitions
in a file) and should be avoided.
The Standard has changed things slightly with respect to a function's
formal parameters. They are now considered to have been declared inside
the first compound statement, even though textually they aren't: this
goes for both the new and old ways of function definition. So, if a
function has a formal parameter with the same name as something declared
in the outermost compound statement, this causes an error which will be
detected by the compiler.
In Old C, accidental redefinition of a function's formal parameter was
a horrible and particularly difficult mistake to track down. Here is
what it would look like:
/* erroneous redeclaration of arguments */
func(a, b, c){
int a; /* AAAAgh! */
}
The pernicious bit is the new declaration of a in the body of the
function, which hides the parameter called a . Since the
problem has now been eliminated we won't investigate it any further.
Footnotes
|