Functions

Aims

To enable students to:

  1. handle function prototypes

  2. understand reference parameters

  3. default parameter values

  4. use function and operator overloading

  5. inline functions

Handle function prototypes

Whenever writing source code the compiler must have seen a function before it is used. If the compiler has not previously encountered a function it will raise an error on its use - as far as the compiler is concerned the invocation of the function is just a series of unrecognisable characters.

In OOP programming each class should reside in a separate source file. In this situation how can the source in one file make use of function declarations in another? The answer is to use function prototypes.

A function prototype tells the compiler what a function looks like, but does not actually define it. For example, a function may exist entitled max that takes in three numbers, x, y and z, whose purpose is to return the largest of the three numbers. The declaration of the prototype appears as

float max (float x, float y, float z);

Note the ; at the end of the line. This indicates to the compiler that a prototype has bee provided. As no function declaration is provided at this point the variables x, y and z are not needed, the prototype can be reduced to:

float max (float, float, float);

Both forms of prototype are equally valid.

The float preceding the function name max indicates that it returns a float value, in this case the largest of the three values provided.

Understand reference parameters

In order for a function to perform any useful task it must be able to take in and pass back values. Frequently functions are written to take in values through a parameter list (the variable names included within the brackets) and pass back a value to the caller through the return type (the data type preceding the name of the function).

The values being passed in are said to be passed by value. The compiler provides an exact copy of the variable being passed by the caller for the function to work on. The compiler can manipulate this data in any way it likes, but none of the changes will be propagated back to the caller. The only value that the function can pass back is through the return type. Consequently the function must be used in the form of an expression, as in:

float max (float, float);

...

void main ()

{

float fValue1, fValue2 = 2f, fValue3 = 4f, fValue4 = 0f;

fValue1 = max (fValue2, fValue3, fValue4);

}

After execution fValue1 holds the value of 4.

Obviously this can be limiting, so C++ provides the ability to pass parameters by reference. In this case any changes made by the function to a variable passed in by reference will be propagated back to the caller. To indicate to the compiler that a parameter is to be passed by reference the & character is placed after the parameter type.

If the max function introduced previously was intended to change the first parameter passed in to 0 after execution the prototype becomes:

float max (float &, float, float);

while the invocation stays the same:

void main ()
{
    float fValue1, fValue2 = 2l, fValue3 = 4l, fValue4 = 0l;
    fValue1 = max (fValue2, fValue3, fValue4);  
}

After execution fValue1 holds 4 and fValue2 contains 0.

It is possible to pass in values by reference to a function and ensure it can not change the value of the variables by preceding the parameter definition with the const keyword, as in

float max (float &, const float &, const float &);

This version of the max function takes three reference parameters, ensuring that neither the second or third values passed in are changed during its execution.

Why use the const keyword and not just restrict the function to using call by value? There are two reasons, the first is that the function can not make any changes to the variable within itself - any attempt to generates a compilation error, but the most important is efficiency.

As discussed previously, passing parameters by value involves the compiler making an exact copy of the data involved and storing it on the function stack. If a large amount of data is being passed between two functions this can involve a considerable amount of work for the machine copying data around between different parts of the program. Reference parameters can be used to efficiently pass structures between functions. A structure in C++ is a collection of data items grouped together, for example the following defines a structure called person and declares a variable of that type:

struct person // Declare person struct type
{
    int iAge; // Declare member types
    float fWeight;
    char cName[25];
};

person me;

When passing a variable by reference only a pointer to it is passed between the functions, in the case of the variable me this will be a pointer to the first element of the structure (in this case iAge). If the structure was passed by value the system would have to make an exact copy of the variable me - a time consuming task. Passing structures by reference permits large amounts of data to be passed between functions with minimal overhead, by preceding the parameter with the const keyword the data passed is protected from modification.

Default parameter values

When writing programs it pays to make them as flexible as possible. This has a knock on effect on individual functions - they must be able to handle many diverse situations efficiently. To describe a given situation to a function may require many parameters, in most situations many of them may have the same value - some may only take different values in very rare situations. To minimise the number of parameters that must be passed to each invocation of a function it is possible to make use of what are termed default parameters.

When a function is prototyped or defined the developer can specify a default value for some (or all) of the parameters. Callers of the function do not need to provide values for such parameters, the function will use the default values provided by the original author of the code, for example the following code defines a function taking four parameters, the last two having default values:

float func1 (int a, int b, int c=10, int d=5);

The function func1 can be called as follows:

void main ()  
{  
    func1 (); // Illegal - must supply t least two parameters
    func1 (27, 1); // The function will work on the values 27, 1, 10, 5
    func1 (27, 1, 5, 77); // The function will work on 27, 1, 5, 77  
}

The following points must be observed when using default parameters:

They must be the last argument(s) in a function call, the following prototype is illegal because the default parameters a and b precede the standard arguments c and d:

float func1 (int a = 10, int b = 5, int c, int d);

A default parameter can not be redefined even if the redefinition is identical. The assignment of 5 to d in the function declaration is not possible as this has already taken place in the prototype:

// Prototype for func1 function.   
float func1 (int a, int b, int c=10, int d=5);

...

// Declaration for func1 function.  
float func1 (int a, int b, int c, int d=5)  
{  
...

}

Use function and operator overloading

Function overloading is the practice of supplying more than one definition for a given function name in the same scope (source file, class, etc.) - i.e. the same function name can manipulate different types of data. The compiler is left to pick the appropriate version of the function based on the arguments with which it is called - no notice is taken of the return type. The following declarations define two versions of the function max, each taking different arguments. The function max is considered an overloaded function.

float max( float f1, float f2 )  
{  
    if (f1 > f2)
    {
        return f1;
    }
    else
    {
        return f2;
    }
}

int max( int i1, int i2 )
{
    if (d1 > d2)
    {
        return d1;
    }
    else
    {
        return d2;
    }
}

The function max can be used as follows:

main()
{
    int i = max( 12, 8 );
    float f = max( 32.9f, 17.4f );
    return i + (int)f;
}

Why use overloaded functions? Overloaded functions enable programmers to specify different behaviours for a function, depending on the types or number of arguments. For example, a print function that takes a string (or char *) argument performs very different tasks than one that takes an argument of type float. Overloading permits uniform naming - developers do not have to invent names such as sz_print or f_print.

Just as functions can be overloaded, so can operators (+, -, /, etc.). It is possible for a programmer to redefine the behaviour of an operator for a particular class. The name of an overloaded operator is operatorx, where x is the operator. For example, to overload the addition operator, you define a function called operator+. Similarly, to overload the addition/assignment operator, +=, define a function called operator+=. Operator overloading is frequently used to make programmers intentions clearer, for example a class, CMyStyring, is defined to manipulate strings and it would be desirable to be able to compare the data contained within two instances using the following code:

if (object1 == object2)

Unfortunately the standard definition for the == operator will not compare the data contained within the class, but rather the location in memory of the instance. Even if the two objects contained an identical string, because they exist in different memory locations the comparison will fail. To make the above line of code work in the way expected it is necessary to overload the == operator, as in:

int CMyStrig::operator== (char *rhs)
{
    // Compare the right hand side of the == operator with the string value
    // stored within the class by member variable m_string.
    if (strcmp (rhs, m_string) == 0)
    {
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

Two forms of operator exist, unary and binary. To overload a unary operator for a class the following form of prototype for a member function (and associated definition) is used:

ret_type operator!();

To overload a binary operator for a class the following form of prototype for a member function (and associated definition) is used:

ret_type operator==(arg);

Inline functions

Normally when a section of code calls upon a function (either user written or provided in a library) the program has to store the current values of any variables local to the current function, the state of the registers in the processor, etc. before jumping to another section of the program where the function is defined. It is possible to eliminate this overhead through the use of inline functions.

An inline function is very similar to a constant defined by the #define statement, i.e. all references to the function are expanded in place within the source file. The program does not need to jump to another place in order to execute the function, instead its body is copied into the source file at the location it is called.

While considerably improving execution speed it makes the resultant program much larger - each reference to an inline function produces a copy of it. Consequently inline functions should only be used for small, frequently used pieces of code - large or infrequently used ones should be defined normally.

There are two ways to define an inline function. The first is to place the definition of the function within the body of a class definition, such as:

class CAccount  
{  
    public:  
        CAccount(double initial_balance) { balance = initial_balance; }
        double GetBalance();
        double Deposit( double Amount );
        double Withdraw( double Amount );

   private:  
        double balance;  
};

where the CAccount function is implicitly inline.

The second is to use the inline keyword as in

inline double Account::GetBalance()  
{
    return balance;
}  

Download