As we have previously
stated functions are essentially just groups of statements that are to be
executed as a unit in a given order and that can be referenced by a unique
name. The only way to execute these statements is by invoking them or calling
them using the function’s name.
Traditional program
design methodology typically involves a top-down or structured approach to
developing software solutions. The main task is first divided into a number
simpler sub-tasks. If these sub-tasks are still too complex they are subdivided
further into simpler sub-tasks, and so on until the sub-tasks become simple
enough to be programmed easily.
Functions are the
highest level of the building blocks given to us in C and correspond to the
sub-tasks or logical units referred to above. The identification of functions
in program design is an important step and will in general be a continuous
process subject to modification as more becomes known about the programming
problem in progress.
We have already seen
many C functions such as main( ) , printf(), etc. the common trait they share
being the braces that indicate they are C functions.
Syntax : return_type
function_name ( parameter_list )
{
body of function ;
}
The above is termed
the function definition in C
parlance.
Many functions will
produce a result or return some information to the point at which it is called.
These functions specify the type of this quantity via the return_type section of
the function definition. If the return type is void it indicates the function returns nothing.
The function_name
may be any valid C identifier and must be unique in a particular program.
If a function
requires information from the point in the program from which it is called this
may be passed to it by means of the parameter_list. The parameter list
must identify the names and types of all of the parameters to the function
individually. If the function takes no parameters the braces can be left empty
or use the keyword void to indicate that situation more clearly.
Function Prototype ( declaration)
When writing programs
in C it is normal practice to write the main() function first and to position
all user functions after it or indeed in another file. Thus if a user function
is called directly in main() the compiler will not know anything about it at
this point i.e. if it takes parameters etc. This means we need to give the
compiler this information by providing a function prototype or declaration
before the function is called.
Syntax : type_spec
function_name( type_par1, type_par2, etc. );
This declaration
simply informs the compiler what type the function returns and what type and
how many parameters it takes. Names may or may not be given to the parameters
at this time.
For Example :- A more
complicated “Hello World” program.
#include <stdio.h> /*
standard I/O function prototypes */
void hello( void ) ; /*
prototype */
void main( void )
{
hello () ; // function call
}
void hello ( ) //
function definition
{
printf ( "Hello
World \n" ) ;
}
Function Definition & Local Variables
A function definition
actually defines what the function does and is essentially a discrete block of
code which cannot be accessed by any statement in any other function except by
formally calling the function. Thus any
variables declared and used in a function are private or local to that function
and cannot be accessed by any other function.
For Example :-
#include
<stdio.h>
void hello( void
) ;
void main( )
{
hello () ;
}
void hello ( )
{
int i ; /*
local or automatic variable */
for ( i=0;
i<10; i++ )
printf(
"Hello World \n" );
}
The variable i in the
hello() function is private to the hello function i.e. it can only be accessed
by code in the hello() function.
Local variables are
classed as automatic variables because each time a function is called the
variable is automatically created and is destroyed when the function returns
control to the calling function. By created we mean that memory is set aside to
store the variable’s value and by destroyed we mean that the memory required is
released. Thus a local variable cannot hold a value between consecutive calls
to the function.
Static Local Variables
The keyword static
can be used to force a local variable to retain its value between function
calls.
For Example :-
#include
<stdio.h>
void hello( void
) ;
void main ()
{
int i ;
for ( i = 0; i
< 10; i++ )
hello ( ) ;
}
void hello( )
{
static int i = 1
;
printf(
"Hello World call number %d\n", i ++ );
}
The static int i is
created and initialised to 1 when the function is first called and only then.
The variable retains its last value during subsequent calls to the function and
is only destroyed when the program terminates.
NB :
The variables i in main() and i in hello() are completely different
variables even though they have the same name because they are private to the
function in which they are declared. The compiler distinguishes between them by
giving them their own unique internal names.
Scope Rules
The scope of an
identifier is the area of the program in which the identifier can be accessed.
Identifiers declared
inside a code block are said to have block scope. The block scope ends at the
terminating } of the code block. Local variables for example are visible, i.e.
can be accessed, from within the function, i.e. code block, in which they are
declared. Any block can contain variable declarations be it the body of a loop
statement, if statement, etc. or simply a block of code marked off by a curly
brace pair. When these blocks are nested and an outer and inner block contain
variables with the same name then the variable in the outer block is
inaccessible until the inner block terminates.
Global variables are
variables which are declared outside all functions and which are visible to all
functions from that point on. These are said to have file scope.
All functions are at
the same level in C i.e. cannot define a function within a function in C. Thus
within the same source file all functions have file scope i.e. all functions
are visible or can be called by each other ( assuming they have been prototyped
properly before they are called ).
Returning a Value
The return statement is used to return a
value to the calling function if necessary.
Syntax : return
expression ;
If a function has a
return type of type void the expression section can be omitted completely or
indeed the whole return statement can be omitted and the closing curly brace of
the function will cause execution to return appropriately to the calling
function.
For Example :-
#include
<stdio.h>
int hello( void
) ;
int main( )
{
int count, ch =
‘\0’;
while ( ch !=
'q' )
{
count =
hello( ) ;
ch =
getchar() ;
_flushall()
;
}
printf(
"hello was called %d times\n", i ) ;
return 0 ;
}
int hello( )
{
static int i = 1
;
printf(
"Hello World \n" ) ;
// hello() keeps
track of how many times it was called
return ( i++ ) ; // and passes that information back to its caller
}
NB : The return value of the function
need not always be used when calling it. In the above example if we are not
interested in know how often hello() has been called we simply ignore that
information and invoke the function with
hello() ;
NB : When the main() function returns a
value, it returns it to the operating system. Zero is commonly returned to
indicate successful normal termination of a program to the operating system and
other values could be used to indicate abnormal termination of the program.
This value may be used in batch processing or in debugging the program.
Function Arguments
The types of all
function arguments should be declared in the function prototype as well as in
the function definition.
NB : In C arguments are passed to
functions using the call-by-value scheme.
This means that the compiler copies the value of the argument passed by the
calling function into the formal parameter list of the called function. Thus if
we change the values of the formal parameters within the called function we
will have no effect on the calling arguments. The formal parameters of a
function are thus local variables of the function and are created upon entry and
destroyed on exit.
For Example :- Program to add two numbers.
#include <stdio.h>
int add( int, int ) ; /* prototype -- need to indicate types
only */
void main ( )
{
int x, y ;
puts ( "Enter two
integers ") ;
scanf( "%d
%d", &x, &y) ;
printf( "%d + %d
= %d\n" , x, y, add(x,y) ) ;
}
int add ( int a, int b
)
{
int result ;
result = a + b ;
return result ; // parentheses used for clarity here
}
NB : In the formal parameter list of a
function the parameters must be individually typed.
The add() function
here has three local variables, the two formal parameters and the variable
result. There is no connection between the calling arguments, x and y, and the
formal parameters, a and b, other than that the formal parameters are initialised
with the values in the calling arguments when the function is invoked. The
situation is depicted below to emphasise the independence of the various
variables.
NB : The information flow is in one
direction only.
For Example :- Program that attempts to swap the
values of two numbers.
#include
<stdio.h>
void swap( int, int )
;
void main( )
{
int a, b ;
printf( "Enter
two numbers" ) ;
scanf( " %d %d
", &a, &b ) ;
printf( "a = %d
; b = %d \n", a, b ) ;
swap( a, b ) ;
printf( "a = %d
; b = %d \n", a, b ) ;
}
void swap( int , int
)//This is original form of declarator
int num1, num2 ; // which you may see in older texts and
code
{
int temp ;
temp = num2 ;
num2 = num1 ;
num1 = temp ;
}
Since C uses call by
value to pass parameters what we have actually done in this program is to swap
the values of the formal parameters but we have not changed the values in
main(). Also since we can only return one value via the return statement we
must find some other means to alter the values in the calling function.
The solution is to
use call by reference where the addresses of the calling arguments are passed
to the function parameter list and the
parameters are pointers which we will encounter later on. For example when we
use the scanf() standard library function to read values from the keyboard we
use the & operator to give the address of the variables into which we want
the values placed.
Recursion
A recursive function
is a function that calls itself either directly or indirectly through another
function.
Recursive
function calling is often the simplest method to encode specific types of
situations where the operation to be encoded can be eventually simplified into
a series of more basic operations of the same type as the original complex
operation.
This is especially
true of certain types of mathematical functions. For example to evaluate the
factorial of a number, n
n! = n * n-1
* n-2 * ... * 3 * 2 * 1.
We can simplify this
operation into
n! = n * (n-1)!
where the original
problem has been reduced in complexity slightly. We continue this process until
we get the problem down to a task that may be solved directly, in this case as
far as evaluating the factorial of 1 which is simply 1.
So a recursive
function to evaluate the factorial of a number will simply keep calling itself
until the argument is 1. All of the previous (n-1) recursive calls will still
be active waiting until the simplest problem is solved before the more complex
intermediate steps can be built back up giving the final solution.
For Example : Program
to evaluate the factorial of a number using recursion.
#include <stdio.h>
short factorial( short ) ;
void main()
{
short i ;
printf(“Enter an integer and i will try to calculate its
factorial : “ ) ;
scanf( “%d”, &i ) ;
printf( “\n\nThe factorial of %d, %d! is %d\n”, i, i, factorial(
i ) ) ;
}
short factorial( short num )
{
if ( num <= 1 )
return 1 ;
else
return ( num *
factorial( num - 1 ) ) ;
}
This program will not
work very well as is because the values of factorials grow very large very
quickly. For example the value of 8! is 40320 which is too large to be held in
a short so integer overflow will occur when the value entered is greater than
7. Can you offer a solution to this ?
While admittedly
simple to encode in certain situations programs with recursive functions can be
slower than those without because there is a time delay in actually calling the
function and passing parameters to it.
There
is also a memory penalty involved. If a large number of recursive calls are
needed this means that there are that many functions active at that time which
may exhaust the machine’s memory resources. Each one of these function calls
has to maintain its own set of parameters on the program stack.
#define directive
This is a
preprocessor command which is used to replace any text within a C program with
a more informative pseudonym.
For Example :- #define
PI 3.14159
When the preprocessor
is called it replaces each instance of the phrase PI with the correct
replacement string which is then compiled. The advantage of using this is that
if we wish to change the value of PI at any stage we need only change it in
this one place rather than at each point the value is used.
Macros
Macros make use of
the #define directive to replace a chunk of C code to perform the same task as
a function but will execute much faster since the overhead of a function call
will not be involved. However the actual code involved is replaced at each call
to the macro so the program will be larger than with a function.
For Example :- Macro to print an error message.
#define ERROR printf(
"\n Error \n" )
void main( )
{
...
if ( i > 1000 )
ERROR ; /*
note must add ; in this case to make correct …C statement */
}
Macros can also be
defined so that they may take arguments.
For Example :-
#define PR( fl )
printf( "%8.2f ", fl )
void main()
{
float num =
10.234 ;
PR( num ) ;
}
What the compiler
actually sees is : printf( "%8.2f ", num ) ;
While admittedly
advantageous and neat in certain situations care must be taken when coding
macros as they are notorious for introducing unwanted side effects into
programs.
For Example :-
#define MAX(A,
B) ( ( A ) > ( B ) ? ( A ) : ( B ) )
void main( )
{
int i = 20, j =
40 , k ;
k = MAX( i++,
j++ ) ;
printf( " i
= %d, j = %d\n", i, j );
...
}
The above program
might be expected to output the following
i = 21, j = 41
whereas in fact it
produces the following
i = 21, j = 42
where the larger
value is incremented twice. This is because the macro MAX is actually
translated into the following by the compiler
( ( i++ ) > ( j++ ) ? ( i++ ) : ( j++ ) )
so that the larger
parameter is incremented twice in error since parameter passing to macros is
essentially text replacement.
Efficiency Considerations
As we have mentioned
previously the use of functions involves an overhead in passing parameters to
them and obtaining a return value from them. For this reason they can slow down
your program if used excessively.
The alternative to
this is to use macros in place of functions. This eliminates the penalty
inherent in the function call but does make the program larger. Therefore in
general macros should only be used in place of small ‘would be’ functions.
The penalty involved
in the function call itself is also the reason why iterative methods are
preferred over recursive methods in numerical programming.
Exercises
1. Write a program that can convert temperatures
from the Fahrenheit scale to Celsius and back. The relationship is °C = (5/9)(°F - 32). Your program should read a temperature and which scale is
used and convert it to the other, printing out the results. Write one or more
functions to carry out the actual conversion.
2. Write a program that reads in the
radius of a circle and prints the circle’s diameter, circumference and area.
Write functions for appropriate logical tasks in your program. You should
#define all appropriate constants in your program.
3.
Write and test a function to convert an unsigned integer to binary
notation. Use the sizeof operator to make the program machine independent i.e.
portable.
4.
Write and test two functions, one that packs two character variables
into one integer variable and another which unpacks them again to check the
result.
5. Write and test a circular shift left
function for one byte unsigned variables
i.e. unsigned characters.
e.g. 10011100
circular shift left by 2
yields 01110010.
6.
An integer is said to be prime
if it is divisible only by one and itself. Write a function which determines
whether an integer is prime or not.
To test your function write a program that prints out all prime numbers between
1 and 10,000.
7.
Write and test a function to read in a signed integer from the standard
input device. Your function should not be allowed to use the scanf function.
The function should have the prototype
int getint( void ) ;
and should be able to
accommodate the presence of a minus sign, a plus sign or no sign ( i.e.
positive by default ). Note that the ASCII values of the digits 0 - 9 are
consecutive, 48 - 57 and that if we wish to convert the digit ‘3’ for example
to an integer we simply subtract the ASCII value of digit ‘0’ from its ASCII
value i.e. ‘3’ - ‘0’ = 51 - 48 = 3.
8.
Write and test a function to get a floating point number from the
standard input device. The function should have the prototype
float getfloat( void ) ;
and should be able to
accommodate normal floating point notation and exponential notation. You should
make use of the getint() function in exercise 5. to read in the various components of the
floating point number.
9.
Write and test a function
double
power ( double x, int n );
to calculate the
value of x raised to the power of n.
(a) Use
your own calculations.
(b) Use
the standard library function pow() ( for more information use help system ) paying particular
attention to type compatibilities.
10. Write a recursive function to
calculate and print out all the Fibonacci values up to and including the nth
. Recall that the Fibonacci series is 1, 1, 2, 3, 5, 8, 13, ... .To test your
program allow the user to enter the value of n and then print out the series.
The program should run continuously until it is explicitly terminated by the
user.
11. Programming Assignment :
Non-linear Equations.
Your basic task in
this assignment is to write a C program which will allow the user to solve the
following non-linear equation
to an accuracy of 10-5 using both the Bisection method and Newton's method.
In order to be able
to solve equations using these methods you will need to provide your program
with suitable initial estimates to the actual root, an interval over which the
sign of f(x) changes in the case of the Bisection method and an initial
estimate x0 where f'( x0 ) is not very small in the case of
Newton's method.
To help the user to
provide such estimates your program should first tabulate the function f(x),
repeatedly until the user is satisfied, over a user specified interval [ x1, x2] and using a
user specified tabulation step.
This should allow the
user to begin by tabulating the function over a large interval with a coarse
tabulation step and to fine down the interval and step until he / she can
determine the behaviour of f(x) in or about the actual root.
Once the user is
satisfied with the tabulation your program should read in the appropriate initial
estimates from the user and test their validity and then solve the equation
using both methods. Finally your program should compare the number of
iterations required to compute the root using both methods and print out the
value of the root.
You should break your
program up into appropriate logical functions
and try to intercept all erroneous situations as soon as possible and take
appropriate action.
Note : The exact solution to the above
equation is 1.595869359.