Getting data <i>into</i> a function




Document: Software Engineering 1: Course Notes

next Getting data back out of a function
up Moving Data Around
previous A (Short) Digression

Getting data into a function

All right: let's consider moving data from a function to a sub-function - from a calling site to a called function. We have already seen examples of this in using the standard library functions: in the function call we simply list the data to be passed in, separated by commas if necessary, and within brackets, thus:

    printf("Hi there...");

Here the function printf() is being called, and the string "Hi there..." is being passed in. Similarly, with:

    x = sin(y);

In this case the function sin() is being called, and the value of the variable y is being passed in to it. Again, with:

    printf("The answer is %i", result);

The function printf() is being called, but now two separate data items are being passed in - the string "The answer is %i", and the value of the variable result. These two are separated by a comma.

These values passed in to a function are called arguments to the function. They are, if you like, the values which the function is supposed to operate on or process in some way. And, as already noted, a function may not need any arguments - if, for that particular function, what it is supposed to do does not need or rely on any data values to be processed.

In general, function arguments are actually expressions. That is, any argument can involve a more or less complicated expression. In that case, the expression is evaluated, to yield a resulting value, and that value is what is actually passed into the function as the argument value. Here is an example:

    printf("%f", ((x * x) + (y * y))/(z + 3.141));

In this case the second argument is actually the expression ((x * x) + (y * y))/(z + 3.141). When execution reaches this invocation of printf(), the expression is evaluated first, and the resulting value is what actually gets passed in to printf(). Note that printf() itself neither knows nor cares how this value was generated - all it sees is the resulting value. This is what is described as "passing by value"; it is discussed by Alcock on page 21, and is specifically described by him as "fundamental to the C language". Be clear about this particular point: since C only allows values to be passed as arguments, nothing that a called function does with an argument given to it can affect or change the original source of the argument. Consider an example. Suppose there is a function called foobar() which expects to be given an argument of type int. Then a fragment of code where foobar() is called might look like this:

    int main(void)
    {
      int i, j, k;
      .
      .
      .
      foobar((k + i) * 15);
      foobar(j);
      .
      .
      .
    }

In both calls to foobar() the argument is technically an expression and is evaluated before foobar() is actually started up. This is clear enough with the argument (k + i) * 15, but it is true also even in the trivial case of the argument being simply j. In the latter case what happens is not that the variable j gets passed to foobar(), but only that the current value of j is passed in. The significance is that foobar() absolutely cannot alter or modify the variable j; in fact, foobar() doesn't even know of the existence of such a variable! All foobar() knows is the value it is given - it does not know whether that value resulted simply from taking the value of a single variable, or from evaluating some arbitrarily more complicated expression.

Some languages do support a mechanism whereby whole variables can, in effect, be passed as arguments. In that case, the called function can be given access to a variable actually belonging to the calling site (not just a copy of its current value); and the called function can, if it wishes, change the value of that variable, and that change will be visible back at the calling site when control returns there. Such a facility is a called "passing by reference" - because, in effect, what is passed is a reference or handle whereby the called function can get at the original variable, rather than just a copy of its value. BUT: the C language does not support passing by reference - it only supports passing by value.gif Fortunately, it turns out that this is not a significant limitation because the effect of passing by reference can be "simulated" using passing by value, in combination with certain other features of the C language. However (as should be clear) this issue only becomes important in the context of trying to get data back from a called function to the calling site - so it will be considered again below.

As noted with printf() above, a function can accept a number of arguments. In general, functions are designed so that they expect some definite, unique, number of arguments, be it 0, 1, 10, or whatever. In that case the function must be called with just that number of arguments - no more, no less.gif

Function calls are actually even more restrictive than this in general. Not only must the call provide precisely the correct number of arguments, but they must be of the correct data types. Remember: the data type defines the "kind" of information represented by a data value or a variable - such as int, double etc. So if a function has been designed to accept two arguments, the first of type int, the second of type float, then every call or invocation of that function must include precisely two arguments - no more, no less - and they must be of precisely those types.gif

The compiler will usually try to enforce these rules - i.e. it will try to check every invocation of a function to see that the arguments match the design or specification of the function in both number and type(s). But the compiler can obviously only do this correctly if it knows how many arguments, of what types, are expected. In the case of user-defined functions it will know these things if it has processed the function definition before it sees any invocation of it. This condition will be satisfied if the program file is laid out as suggested earlier - in "reverse" order - which, of course, was precisely the reason for suggesting that ordering.

The situation with library functions is a little more complicated. The whole idea of library functions is that they have been pre-written and compiled ahead of time, so, by definition, their definitions will not be available to the compiler. But although the full definitions of the library functions are not available, short "interface specifications" are provided. These interface specifications tell the compiler, for each function, exactly what arguments are expected, and of what types. Such a specification, for one function, is called a function prototype. Function prototypes for the standard library functions are provided in the standard header files - files like stdio.h and so on. So: if you want to use or invoke any standard library function in your program, you must ensure that an appropriate header file is first scanned by the compiler, so that the compiler will then be able to check that the invocation is legal. This is done with the #include directive. If you are ever in doubt as to which header file to include to provide the prototype for a particular library function, look up the function in the Turbo-C++ on-line help system - the header file will always be listed there.gif

Having said all that, be warned: the ability of the compiler to detect mismatches between the arguments expected by a function and the arguments actually provided, is still less than perfect. You should still take some care yourself in coding any function call, and not rely completely on the compiler to check these things for you!

So the format of a function call or function invocation is always function_name followed by a left bracket - ( - followed by zero, one, or more argument expressions, separated by commas, followed by a right bracket - ). The arguments must match what is expected by the function in both number and types. The compiler will try to check for this if - and only if - it has earlier processed either a full definition of the function, or at least a prototype, typically found in a #include'd header file.

So much for the invocation of functions with arguments - but how does this get represented in the function definition?

In all the function definitions given so far (including definitions of main()), the functions did not accept any arguments. This was denoted by the keyword void, placed between the brackets after the function name:

  void foo(void)
       /*  ^^^^ This denotes that foo()
                will not accept *any*
                arguments! */
  {
    /* Statements making up foo() go here... */
    .
    .
    .
  }

Note carefully that, in this example, the keyword void appears before the function name also - but in that position it has quite a different significance, nothing to do with the arguments which foo() accepts. We'll consider this again later.

OK: that was the special case of a function which does not accept any arguments. Where a function does accept arguments, this is denoted or coded by replacing the keyword void with a list of names and types for the arguments: these are technically called the parameters rather than the arguments. The difference is this: the arguments are the values which get generated at the calling site; the parameters are effectively a special kind of variable, belonging to the called function, into which the argument values are copied when the function is invoked. If you like, all the calling site can see are the arguments (or argument expressions) - it cannot see the parameters as such; and all the called function can see are the parameters (and their values) - it cannot see the original expressions, or arguments, from which they were derived. In this sense, arguments and parameters are like two views of the same thing: but the calling site only has the argument view, and the called function only has the parameter view.

The format for a parameter list, in a function definition is something like this:

    void foobar(int max, int min, double ecstasy)

This heading part of the function definition says that foobar() will accept exactly three arguments. foobar() will refer to these as max, min and ecstasy. The first two must be of type int, the third of type double. Whenever foobar() is invoked, the arguments at the calling site will be evaluated; parameters called max, min and ecstasy will be created; and the argument values will be copied into them; then execution will proceed with the statements in the body of foobar. The parameters are then effectively a special kind of local variable belonging to foobar. Within foobar() they can be used just like any normal variable. In particular, their values can be referenced, as one would normally reference the value of a variable - by using its name in an expression. Thus, a fragment of foobar() might be the following:

    void foobar(int max, int min, double ecstasy)
    {
      .
      .
      .
      if (ecstasy < 0.0) printf("\nOh dear...");
      else
        if (ecstasy > 100.0) printf("\nI don't believe it!");
      .
      .
      .
  }

Since parameters are a form of local variable you can, of course, change their values: but by doing so you overwrite the original value - the argument - passed in from the calling site. So this should only be done with care! Note also that, when the function terminates, the parameters, just like all other local variables, are disposed of, and their values lost. Changes to a parameter within a called function, just like changes to a local variable, have absolutely no effect on anything at the calling site.




Document: Software Engineering 1: Course Notes

next Getting data back out of a function
up Moving Data Around
previous A (Short) Digression



McMullin@ugmail.eeng.dcu.ie
Wed Apr 12 19:40:14 BST 1995