Getting data back <i>out</i> of a function




Document: Software Engineering 1: Course Notes

next Preferences?
up Moving Data Around
previous Getting data into a function

Getting data back out of a function

All right: that's passing data into a function, both from the point of view of the calling site (argument expressions) and from the point of view of defining the called function (parameter variables). Now we turn to the complementary question of getting information back from the called function to the calling site.

The simplest, and arguably most convenient, mechanism provided for this in C is the use of so-called "return" values. Essentially, a function can terminate using a form of the return statement which specifies a value (it actually has a complete expression in general: but this will be evaluated to yield some particular, single, value, of some particular type). If a function terminates in this way, the specified value is retained and made available back at the calling site. In effect, wherever the function name appeared at the calling site, its name gets replaced by the returned value, and execution then continues at the calling site.

Consider this simple example, using the library function sin():

    x = sin(0.5);

In this case the function sin() is invoked (incidentally being passed one argument, the double value 0.5). sin() is executed, and when it terminates, it does so using a return statement. That return statement specifies a return value. Back at the calling site, the name of the function is effectively replaced by this value, and execution continues, so that this value is then assigned to the variable called x.gif

That was a simple example. Here is a somewhat more complicated one:

    printf("Answer: %f", sqrt(sin(x) + cos(y)) * 2);

When this is executed, what happens? Well, printf() cannot be started before its arguments have been evaluated. To evaluate the second argument, sqrt() (a function for extracting square roots) must be invoked - but again, that cannot happen until its argument has been evaluated. So the calls to the sin() and cos() functions have to happen first. It does not matter which is done first - lets say it is sin(). Its argument is evaluated - simple, it is just the value of the variable x. This is passed in and sin() gets executed. In due course it terminates with a return statement, specifying a return value. This is retained in temporary storage at the calling site. The argument for cos() is then evaluated - again simple, just the value of y. When cos() terminates, again with a return statement, its return value comes back to the calling site and gets added to the value returned earlier from sin(). This is now the argument value for sqrt() so sqrt() is started up, with the value passed in to it. Again, in due course, sqrt() terminates with a return statement, and specifies a return value. This value is multiplied by 2, and the resulting value is the second argument for printf(), so printf() can now finally be invoked. It will do its thing (printing out the text Answer: followed by the value of its second argument, regarded - correctly - as a double), and will then terminate. printf() actually also ends with a return statement which specifies a return value, and this will indeed be passed back to the calling site (this return value signals whether printf() executed without detecting any difficulties). In this particular case, the return value is not used at the calling site - it is not, for example, stored in a variable, or used in some further expression evaluation. So it is simply discarded, and execution proceeds to the next statement.

Phew!

That covers how return values are used at the calling site, and we have also given the gist of how the return value is implemented in the function definition - namely by using a return statement with an appropriate operand expression. But let's just detail this with another example:

    double silly_sum(double apples, double oranges)
    {
      return (apples + oranges);
    }

Note that the void that we were previously placing in front of the function name, in the heading, has now been replaced by double. This is a general rule. The word that comes before the function name in the heading specifies the type of return value (if any) which the function will yield. If the function does not yield any value, then the return type must be specified here as void, as indeed, we have generally been doing up to now. But if the function does return a value, then the type of that value must be advertised or notified ahead of time, by specifying it here in the heading. So, in our case, just from the heading, we know that the function silly_sum() must terminate using a return statement, with an operand which is of type double. If there is a subsequent inconsistency in the definition of silly_sum() - if it does not contain any return statement, or it has a return statement with no operand, or it has a return statement with an operand which is not of type double - then the compiler will complain bitterly.

As an aside, note that we have consistently been defining the function main() as if it returns a value of type int, and then actually ending it with a return statement specifying a return value of 0:

    int main(void)
    {
      .
      .
      .
      return 0;
    }

This is, to say the least, a trifle odd. main() is not being called by any other function - it is automatically started as the very start of program execution. We already mentioned that, for that very reason, it would not make sense for the parameter list of main() to be anything other than void. Yet, notwithstanding that, we are defining main() as if it makes sense for it to be passing a return value back to some calling site. Why is this?

Well, this is yet another of those funny historical oddities of C. The essential idea is that the return value from main() is passed back to the enclosing "environment" - wherever the program was started up from. In our case, this is the Turbo-C++ IDE. The general idea is that this return value could be used to signal whether program execution was "successful" or not - with 0 signalling success, and anything else indicating some kind of failure. However, this idea of signalling success or failure to the enclosing environment doesn't actually serve any useful purpose in the case of running programs under the Turbo-C++ IDE. Using it under other environments (where it might be useful) is beyond the scope of this course. In fact, for our purposes, it would be legal, and preferable, to define main() as not returning any value at all, and omit any return statement from it:

    void main(void)
    {
      .
      .
      .
    }

Unfortunately Alcock has insisted in showing main() as yielding a return value of type int, so, in the interests of consistency with his presentation, I have stuck with that...

OK: time for another substantial example. Let's revisit the triangle area calculating program, but now avoiding the use of global variables, and moving data around with arguments and return statements instead. Have a look at TRI-LCL.C. Compare it with TRI-GLBL.C. Again experiment with single stepping through it. Try to predict where execution is going to go next at each stage, and then check whether you are right. Use the Watch window to monitor the variables (including the various function parameters). Note that because the variables are now all local, they only exist at all when the relevant function is active. Furthermore, it is a characteristic of the Watch window that only variables of the function which currently contains the execution highlight bar are visible.gif Again, try to predict when changes are going to happen, and see if you get it right. Notice how the parameters and return values are allowing information to be moved around between the different functions. Again, try varying the program somewhat: e.g. to calculate the area of a rectangle, or square etc.

Now notice one particular difference between TRI-GLBL.C and TRI-LCL.C. In TRI-GLBL.C I used a single function (read_dimensions()) to prompt for and read in both the dimensions of the triangle; whereas, in TRI-LCL.C I have broken this down into two separate functions, read_base() and read_height(). Stop for a moment, and see if you can figure out why I made this change...

Well, if I had retained the original structure of TRI-GLBL.C, but without using global variables, then the function read_dimensions() would have to somehow return two distinct pieces of information. But that cannot be done with the return mechanism in C.gif The return statement can only have one operand, and thus only a single value can be passed back to the calling site. Indeed, if you think about the way the returned value is handled at the calling site (being inserted wherever the function name appeared) it should be clear why this must be so: if more than one value was returned then there would be no unambiguous way of deciding what to do at the calling site.

So: I elected to re-organise the program to remove the need for a function which would return more than one value - by replacing it with a pair of functions, each of which individually only return one value.

This works fine. But it also gives me an excuse for mentioning an alternative way for getting information back from a called function to the calling site - a way which does not suffer the limitation of the return mechanism, which can only handle a single value.

First recall the earlier discussion of "passing by value" and "passing by reference". Clearly, if we had a mechanism for passing by reference we could use this to get as many distinct items of data as we like back from a function: we just pass ("by reference") that number of variables into the function; the function then stashes the relevant results in these variables; and when the function terminates, and control returns to the calling site, those results can be accessed - as many of them as you like.

But, of course, C does not support passing by reference. So what can we do?

The answer is that we can achieve the effect of passing by reference, in a somewhat roundabout way: we can pass, as values, and using the normal C passing by value mechanism, the addresses of some variables. Using these addresses, the function can then, indirectly, get at the variables, and deposit results in them - exactly as if the variables had been passed by reference.

In fact, we have already been implicitly using this mechanism, though without drawing too much attention to it. It is the way we have been getting information back from the standard library function scanf():

  scanf("%d %f %f", &i, &x, &y);

In this case the additional arguments to scanf() are addresses or pointers to variables. We generate the address of a variable by preceding its name with the "address-of" operator, denoted with the ampersand character, &.

Of course, this mechanism is used by scanf() precisely because scanf() is supposed to be able to get more than one item of information back to the calling site. In fact, a single call to scanf() can be used to pass an indefinitely large number of separate items back, just according to how the format specification string is laid out.

Can you use this mechanism in your own user-defined functions?

Yes - but it is quite messy. To define a function of your own which uses this mechanism you need to know two new things: how to specify that the type of a parameter is an address or pointer; and given an address or pointer, how to "de-reference" it - access the thing pointed at. The program TRI-LCL1.C gives an example, if you want to have a look - but we will not consider this in detail here.




Document: Software Engineering 1: Course Notes

next Preferences?
up Moving Data Around
previous Getting data into a function



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