The basic property of functions is that they enable us to break a program down into a number of smaller units. There are several different reasons why programs should be broken down in this way. Different users attach different importance to these reasons; some of the considerations below may be considered irrelevant by some practitioners.
If a solution has been created for a particular sub-problem (an implementation of the solution involving means for storing the related data items, and the means of accessing them in an appropriate way) may be useful in more than one problem. An example is the sorting of a number of items into a particular order; sorting is a basic operation which is needed at many points in many computer problems.
Units which solve frequently occurring sub-problems can therefore be re-used in appropriate places in a variety of larger problems.
In this C++ course we teach just the programming techniques needed for producing useful re-usable code; we are not emphasising just WHY you may choose to break down your problem into these particular units. We need what are called "functions" and "structures" in C++; in the next course you will learn techniques for using functions and structures as the basis of an object-oriented approach to the design of programs; they can be used in other design methodologies too.
In this section of the course, we teach the techniques for implementing functions. For those who have used other programming languages, the concept of a "function" in C++ corresponds to a "procedure" or "subroutine" elsewhere.
// First the declaration and definition // of two functions void dothis () { cout << "Hello\n"; } // end of dothis void dothat () { cout << "Goodbye\n"; } // end of dothat // Now the program calls them main () { // These are the calls dothis (); ....; dothat (); } // end of main
The parentheses following the function identifier in the function calls in the main program indicate that "dothis" and "dothat" are identifiers representing functions to be executed. The functions may, of course, be called as many times as you wish within the program.
For each function the compiler needs two distinct items of information.
This should be compared with the declarations of variables, where again there are two distinct aspects to any declaration, one to inform the compiler about the type and identifier for compile-time, and one to arrange to claim space at run-time.
In the above simple example, declaration and definition are combined.
These two parts (declaration and definition) of a function can be separated in C++ as in the example
// declarations void dothis (); void dothat (); main () { // calls dothis (); ....; dothat (); } // end main // definitions void dothis () { cout << "Hello\n"; } // end of dothis void dothat () { cout << "Goodbye\n"; } // end of dothat
The first declaration "void dothis();" is to warn the compiler what to expect as calls of the function, for example by introducing the identifier to be used. The compiler will not now be surprised to see the calls of the function when it encounters them in the main program. Although the compiler knows that dothis is the identifier of a function, not of a variable, it still requires parentheses after the identifier in function calls in the program.
The later definition "void dothis() { ... }" with executable statements within curly braces will define between those curly braces the code to be executed whenever the function is called. It will involve executable C++ code.
The function declaration needs to inform the compiler of
A function to compute the square root of a given value.
// identifier "sqrt" // takes one double parameter // gives double result double sqrt ( double );
A function to compute the larger of two integer values.
// identifier "max" // takes two integer parameters, // gives integer result int max ( int, int );
A function to print an error message involving an integer value.
// identifier "error" // takes integer parameter // gives no returned result void error ( int );
The above functions would be called from elsewhere (perhaps from the main program, perhaps from another function) by statements such as the following.
Square root would take a "double" parameter and return a double result.
double y = sqrt ( 2.0 ); if ( sqrt ( x * 5 ) > 2 ) .... cout << "Sqrt " << sqrt( 2.0 ) << "\n";
We would normally expect to use the returned result as a value.
Maximum of two integers
int this, that; int biggest, mark; cin >> this >> that; biggest = max( this, that ); mark = max( this, 40 ); cout << "Larger value is " << max ( this, that ); non_negative = max( this, 0 );
Reporting an error
if ( n < 0 ) { error ( 5 ); // no result } if ( n > 100 ) { error ( 6 ); // no result }
A function declaration consists of
<returned type> <identifier> ( <param type>, <param type>, .... ); // Note the semicolon
The returned type may be any ordinary C++ type such as int , float and so on, or the type void if no value is being returned to the main program.
In some older programming languages, the word "function" was used only when a result was to be delivered; the word "procedure" or "subroutine" was used is there was no result to deliver. In C++ all such objects are called functions.
The function definition needs to inform the compiler of the actions to be taken when the function is called. In the declaration we described only the types of the parameters. The compiler needed this information to enable it to handle the parameters correctly wherever the function is called. In order to describe the actions to be taken when the function is called, we need to identify each of the parameters, so that we can refer to them and use them from within the code which forms the body of the function.
We want a function to deliver the larger of the values of the two values given as parameters.
// deliver the larger of the 2 params int max ( int i, int j ) { if ( i > j ) { return i; } return j; } // end max
The "return" keyword acts both to specify the particular value to be returned by the call of the function, and to cause dynamic exit from the function.
If the function returns type void we leave the function using the statement
return;
with no value specified. If the function returns a non-void type, the return must be followed by a value which the compiler can force into the required type.
Notice that we do not need an "else" statement in this example because of the dynamic exit caused by the "return i;" statement.
We want a function to print an error message and then abandon the program.
void error ( int errno ) { cout << "Error number " << errno << " has occurred\n"; cout << "Program abandoned\n"; exit ( 1 ); } // end error
In this example, the "exit" causes the whole program to terminate; if we merely wished to report the error and continue the program, we would use a
return;
statement with no value given.
void error ( int errno ) { cout << "Error number " << errno << " has occurred\n"; cout << "Program continues\n"; return; } // end error
If the function has its delivered type declared as void its code does not return values, and the function is left by using
return;
Convention with the exit function is that
exit( 0 ); // indicates normal exit exit( 1 ); // indicates error exit exit( 2 ); // indicates error exit
The UNIX shell can use the value returned by a terminating program to determined whether it terminated successfully or not.
The Ceilidh system will, in some exercises, require you to use the "exit" function to indicate errors in the data.
We need to calculate the square root of the given parameter value, and return it. Please forgive the crude method!
double sqrt( double x ) { // code not guaranteed, I'm in a hurry! const double accuracy = 0.00000001; double low = 1, high = x, mid = x / 2; if ( x < 0 ) { error( 5 ); // error if negative } if ( x < 1 ) { low = 0; high = 1; } while( high - low > accuracy ) { if ( mid * mid > x ) { high = mid; } else { low = mid; } // end if guess too large mid = ( low + high ) / 2; } // end while accuracy not reached return mid; } // end sqrt
A definition consists of
<returned type> <identifier> ( <param type> <param ident>, <param type> <param ident>, .... ) { // No semi-colon after the ")" ....; // body } // usually an end comment here
The compiler needs to know the type of the value to be returned by the function so that it can be treated correctly in the call of the function. If no value is to be returned (cf "error" or "dothis" above) then the return type is given as "void".
The particular value to be returned is specified by the line
return <value>;
With a void function write simply
return;
The compiler will do type conversions where necessary. If the function declaration says that it returns a float and the function includes "return 1;" then the compiler knows to sort this out.
The parameter type sequence in the declaration and the definition must agree. The compiler will make the necessary type conversions to the parameters and to the delivered result in a call of the function. Again, the compiler will perform type conversions if the function declaration specifies a float parameter, and if the call includes an actual integer parameter.
The parameters in the function definition are called the formal parameters. The parameter values supplied in calls of the function are called actual parameters.
A function defined with, say, 3 parameters can be called with less than three parameters if default values have been set. Default values are set using what looks like initialised declarations for the formal parameters.
A possible example might be
int max( int i = -1000000, int j = -1000000, int k = -1000000 ) { if ( i >= j ) { if ( i >= k ) { return i; } else { // i >= j && i < k return k; } } else { // i < j if ( j >= k ) { return j; else // i < j && j < k return k; } // end if else i >= j } // end max
This could now be called as
m = max ( p, q, r );
to find the maximum of the three values "p, q, r" or as
m = max( p, q );
in which case the last (third) parameter in the definition assumes its default value; we effectively get the maximum of the first two parameters.
A drawback of using this technique is that errors in giving the wrong number of parameters to a function may not be detected by the compiler.
Another example could be written as in
float volume ( float height = 1.0, float width = 1.0, float depth = 1.0 ) { return height * width * depth; } // end volume
The function can then be called as in
float x; x = volume ( 2.3, 4.5, 6.7 ); // returns 2.3 * 4.5 * 6.7 x = volume ( 2.3, 4.5 ); // returns 2.3 * 4.5 x = volume ( 2.3 ); // returns 2.3 x = volume (); // returns 1.0
You can use the same function identifier for more than one function, providing that the compiler can determine the particular one to be chosen by the types (and number) of its parameters.
int square ( int i ) { // int squared is an int return i * i; }
double square ( double x ) { // double squared is a double return x * x; }
double square ( double x, double height ) { // return the volume of a square prism return x * x * height; }
The calls might be
i = square( 3 ); double z; if ( square( z ) > 3.14159 ) { .... vol = square( z, 1.27 );
The compiler would then look at each call of the function, and determine from the types of the parameters which of the definitions was to be invoked.
The strict rules to define what parameter types can be distinguished are very complicated; the best advice is just to make the difference very obvious!
The main program and function definitions can appear in any order in your program. It is normally clearest to read and understand if the main program appears before the function definitions. Reading the main program gives an overview of the sequence of operations.
Function declarations MUST appear before the function is called. They are usually put together before the main program.
We can declare local variables at the start of our function code if we require additional variables for computations within the function. The declarations appear after the opening curly brace, just as they do in the main program.
If a local identifier clashes with a global constant, or with the name of another function, then that global constant or other function becomes inaccessible within this function. The local object will be referred to by the identifier.
const float pi = 3.1416; int fred( int ); main() { .... ... pi ... // global constant } // end main int fred( int i ) { int pi; // local variable .... pi = ...; // local variable } // end fred
When a function is called, the values of the (actual) parameters at the point where the function is called are calculated, and passed to the function definition for execution. Within the function, the parameters act like variables, initialised to the value of the corresponding actual parameter at the call, and their values can be changed by ordinary assignment.
Thus if a function definition is
int fred( int number ) { ....; number = number + p; ....; } // end function fred
the identifier "number" can be considered as a local variable to the function. Changing its value inside the function as shown in the above example has no effect on the outside world.
int count( int value ) { ....; for ( ; value > 0; value-- ) { .... } // end for loop executed "value" times ....; } // end function fred
If the call of the function is
int counter = ...; fred( counter );
the value of the variable counter in the calling program will not be changed. We could also call the function by
fred( 23 );
which would pass the value 23 to the formal parameter. This way of passing parameters is referred to as "passing by value".
There are some cases where we wish to change the value of a variable in the world from which the program is called; the use of a "swap" function to interchange two integer variable values is an example. We wish to pass to the function the names of two variables as parameters, and require the function to interchange the values in the two named variables. These are variables of the calling program, not of the function. We would want to call the function as in
int p1, p2; ...; swap( p1, p2 ) // swap the values
If we declared it as
void swap ( int p, int q ) { int temp = p; p = q; q = temp; } // end swap
then the above call would not have any effect on the world outside the function. It would interchange only the values of the internal variables "p" and "q", not the program variables "p1" and "p2".
To have the effect we require we must change the declaration to
void swap ( int &, int & );
and the first line of the definition to
void swap ( int & p, int & q ) {
If the function parameters are shown just as normal, without the "&" symbol, the only effect of the function is to interchange the two "local" variable values. To ensure that the actual variables whose identifiers are passed as parameters to the call of the function are swapped, the "&" sign shown in the example is necessary. The call will now interchange the values of the main program variables named as arguments. This is referred to as a parameters being "called by address" or "called by name" or "called by reference".
A mathematical example might be a function to solve quadratic equations (non-mathematicians don't panic ...). The input to the function is three float values ("a", "b" and "c"), and the output is two float values (the roots, "x" and "y"). We cannot deliver two values directly as the result of a function. Instead we use two address parameters, so that the function specification is
void quadratic( float, float, float, float &, float & );
and a call might be
float x1, x2; ...; quadratic( 1.2, 3.4, 5.6, x1, x2 );
The values of roots will appear in "x1" and "x2".
The definition might be
void quadratic ( float a, float b, float c, float & x, float & y ) { float d = sqrt( b * b - 4 * a * c ); x = ( -b + d ) / ( 4 * a * c ); y = ( -b - d ) / ( 4 * a * c ); } // end quadratic
A less mathematical example; we are given a number of bicycle wheel rims and spokes in stock, and as a global constant the number of spokes needed to make one wheel. How many wheels can we make, how many rims and spokes will be left over? There are three computed values to pass back to the calling program. We choose in the example below to return as the result of the function the number of wheels it is possible to make, and to store in two address parameters the numbers of remaining rims and spokes.
int wheels( int rims0, int spokes0, int & rims1, int & spokes1 ) { int made = rims0; if ( spokes0 < rims0 * spokes_per_rim ) { made = spokes0 / spokes_per_rim; } rims1 = rims0 - made; spokes1 = spokes0 - made * spokes_per_rim; return made; } // end wheels
A call might be
int rim, spoke; int r, s, number; // 10 rims and 150 spokes number = wheels( 10, 150, rim, spoke ); // left-overs in "rim", "spoke" // "r" rims and "s" spokes number = wheels( r, s, r, s ); // left-overs in "r" and "s"
We could instead have had a void function, and pass three values back through address parameters.
Do not use the "&" sign unless it is really needed. The fact that most parameters are "called by value" is a safety feature, to ensure that a function cannot accidentally change the values of program variables unintentionally.
If you have nominated a parameter to be called by address, it does then not make sense to substitute a specific value in a call, as in
swap( i, 3 );
or
swap( i + 1, .... );
since the object "3" or "i+1" is not something which can be swapped.
The parameters as specified at the start of the function definition are referred to as "formal parameters".
The parameters substituted in any particular call of the function are referred to as "actual parameters".
The complete program with function declaration, main program and function definition will look roughly as follows. We use an example with the Halberstam function.
// Program example // Halberstam function being declared // then used // then defined #include <iostream.h> // Declare the function int halberstam( int ); // The main program main() { int numb = 0, calcs = 0; // Loop reading integers until we meet a zero while ( cout << "Enter the number now please: ", cin >> numb, numb != 0 ) { // This is the call calcs = halberstam( numb ); cout << "numb " << numb << " result " << calcs << "\n"; } // end while read number > 0 } // end main program // Now for the definition int halberstam( int numb ) { int calcs = 0; // Error exit if ( numb <= 0 ) { return -1; } // Loop counting how many times while ( numb != 1 ) { if ( numb % 2 ) { // number is odd numb *= 3; numb++; } else { // number is even numb /= 2; } calcs++; } // End of while loop return calcs; } // end function halberstam
In general functions will perform operations on data, and not perform their own input output except
We show here two functions taking float parameters, and giving float results. The first takes a radius length, and delivers the area of a circle of that radius. The second takes an area, and delivers the radius of a circle with that area.
const float pi = 3.14159; float area ( float radius ) { return pi * radius * radius; } // end radius to area float radius ( float area ) { if ( area < 0 ) { cerr << "Sqrt invalid param " << area << "\n"; // You might choose to exit here with // exit( 1 ); return -1; } return sqrt ( area / pi ); } // end area to radius
Calls of these functions might be
float a1, r1, a2, r2; cin >> r1; a1 = area( r1 ); cin >> a2; r2 = radius( a2 ); if ( r2 < 0 ) { ...
This function sums the series
for a given value of "x". We keep adding terms until we reach a term whose value is less than 0.001.
float expl( float x ) { // sum the exponential series float total = 0; float term = 1; int count = 0; // now loop while ( term > 0.001 ) { total += term; count++; term *= x/count; } // return the total return total; } // end expl
Calls of this function might be
cout << "Value is " << expl( 1 );
We require a function to print out a given message as a question, and return TRUE if the user replies "y" and FALSE otherwise. The type of a message (a string of text contained within double quotes) is "char *" (the reasons for this will appear later), and the definition of the function might be:
int ok( char * message ) { char ch; cout << message << " [Type y or n]? "; cin >> ch; return ch == 'y'; }
In a real situation, the function body might repeat the question until the given answer is "y" or "n". In the above example, we respond with TRUE if the answer is "y" and FALSE otherwise.
Note that we do not need to say
if ( ch == 'y' ) return 1; return 0;
We can return the comparison result directly.
Calls of this function might be:
if ( ok( "Was that correct" ) ) { ....; } else { ....; } if ( ok ( "Overwrite the file" ) && ok ( "Are you sure" ) ) { ....; }
Observe the subtlety of the last example. The program first asks
and, because the "&&" operator is lazy, only if the reply to the first question is "y" asks for confirmation
If the reply to the first question is "n", the second question is not asked.
A number of standard mathematical functions are available. For details try
man exp man sqrt man trig
or
ls /usr/man/man3/*.3m
Your program layout should now be as typified by the above examples. The function declarations are near the top, all starting at the left hand edge.
The function definitions follow the program. The first line and the final "}" of each definition start in the first column. The internal statements are indented, the final "}"s are followed by a comment on the same line.
Copyright Eric Foxley 1996
Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!