Arthur Held is vice-president of Glacier Software, publishers of Pic Trak, a photo management and labeling system for businesses, and amateur and semi-professional photographers. He also teaches C Language courses for VoTech and Adult Continuing Education programs. Art can be reached at Glacier Software, PO Box 3358, Missoula MT 59806. (406) 251-5870.
Fully understanding what you can (and can't) do with function return values will help you understand others' code and the operation of the standard library functions. Mastering the C return mechanism can also help you write better code and more useful, flexible functions. This tutorial will survey the various return mechanisms available in C, and show you how and when to use each.
The Basics
A C function may or may not return a value. You can determine whether a library function returns a value by checking the manual. For other functions, the same information is available in either the function prototype (often found in an include file) or the function header. The prototype
int my_func(int x);tells us that my_func() will return an integer. Alternatively,
void my_func_2(int x);tells us my_func_2() returns void, i.e., it doesn't return anything.Generally, when a function does not return a value, two specific conditions should be met: first, the function should have no useful information to pass back, and second, nothing can go wrong during its execution, at least nothing that the function can detect.
For example, the standard library srand() function reseeds the random number generator. srand() has no information to send back, (one uses rand() to actually get a random number.) There is also nothing to go wrong, so the function is written to return nothing. Thus the prototype in the include file math.h gives the return type as void:
void srand(unsigned seed);Conversely, there are two cases when you should be getting and using a returned value:
Functions are seldom as simple as srand(); functions with both result and error return values are more prevelant than those without.
- when the function called has information to pass back and
- when you need to know if something has gone wrong.
In addition to deciding whether to return a value, you must also decide how to use the returned value. There are six common cases:
- discard it
- assign it to a variable,
- use it in a formula without assignment,
- assign and use it within a conditional test,
- use the result in a conditional test without assigning it, and
- pass it as an argument to another function.
Cases 1 & 2
The first two cases are straightforward:
char ch; /* idle until user presses a key */ printf("Press any key to continue...."); getch(); /* or wait for a response, then save it */ printf ("Please enter your menu selection: "); ch = getch();However, many functions return a special value (or flag) to indicate that something has gone wrong. For example, fopen () opens a file, returning a pointer to a structure typedefed as FILE:
FILE *fp; fp = fopen("MYFILE.DAT", "r");After a successful fopen () call, fp will have been assigned a pointer.But, if the file is not found fopen() will not create it (since we have told fopen() to open the file in the "r" (read) mode. When fopen() is unable to open the file, it returns a flag telling you it has failed. This flag is NULL (or 0). Since the C standard guarantees that no pointer will be zero, when you get a NULL back from fopen(), you know something is wrong.
To test for this result, use:
if (fp == NULL) printf("file not found\n"); else printf("file found\n");Case 3
Assignment is not required before using a returned value. The following example determines the hypotenuse of a right triangle. Given a function called square() that returns the square of a double as a double, you can state:
double square(), sqrt(); double side_a, side_b, temp, hyp; temp = square(side_a) + square (side_b); hyp = sqrt(temp);In the third line, the squares are determined and returned by the function square, but the returns are never explicitly assigned. Rather, these results are added together, and the sum is placed in the variable temp.
Case 4
Because C assignment "statements" are actually expressions and have a value, you can merge the function call into a conditional test. Using this technique the test for fopen()'s success becomes:
FILE *fp; if ((fp = fopen("MYFILE.DAT", "r")) == NULL) printf("file not found\n"); else printf ("file exists\n");At first glance
if (fp = fopen("MYFILE.DAT", "r") == NULL)appears to be an equivalent test. It is not! The order of precedence puts the comparison operator == above the assignment operator =. Thus, leaving out the parentheses results in a statement that evaluates as:
if (fp = (fopen ("MYFILE. DAT", "r") == NULL))Without the parentheses, the test compares the value returned by fopen() with NULL, and then assigns the result of the comparison to the variable fp.If fopen() failed, returning a NULL, then the evaluated condition is NULL == NULL, which evaluates to TRUE. On failure, TRUE (non-zero and probably 1) will be assigned to fp. Since 1 is non-zero, the overall if conditional also evaluates TRUE, and the program appears to work correctly, reporting a failure.
If the file is present and fopen() can successfully open it, fopen() returns a non-zero value (the pointer to the structure FILE). This non-zero is compared with NULL. The condition:
<some non-zero pointer value> == NULLis FALSE, so fp is assigned a 0. The else branch is taken, and again, the program appears to work correctly, reporting that file present. Unfortunately, when you attempt to use the pointer fp (which now contains 0, rather than a valid pointer to the opened file,) to access a file, you get an error.
Case 5
Often you need to check and act upon the result returned by a function only once. Example 1, for instance, assigns the result of fgets() to more_data, but never uses more_data again.The function fgets() returns the address of buff when it successfully reads data. When it reaches end-of-file, or if there is an error reading the file, it returns NULL. You can simplify the fgets() statement in Example 1 by leaving off the assignment, yielding:
while(fgets(buff, 180, fp) != FALSE)Since in a conditional test, any non-zero value is considered TRUE, you don't really need to compare the return value with FALSE. Instead you can write
while(fgets(buff, 180, fp))As long as fgets() doesn't return NULL, the loop continues; on end-of-file, (or a file handling error), the loop terminates.
Case 6
One function's return value can be used immediately as the argument to a second function. This coding style can shorten and clarify a program. (But be careful, the implementation of some simple functions as macros in some standard libraries may produce confusing results!)Perhaps the most common use of function return values as function arguments is in conjunction with printf(). For example:
double i = 5; printf("the square root of %3.1f is %3.1f\n", i, sqrt(i));This evaluation requires two function calls. While collecting the arguments for the call to printf(), sqrt() is encountered and evaluated (called). The sqrt() return value becomes the third argument. Only then is printf() called.Many libraries implement certain functions as macros. These code substitutions simplify and clarify code and also speed execution by eliminating a function call.
For example, to convert a character to upper case you need only make a range comparison or do a table lookup to determine whether the character is lower case. Lower case letters can be converted to the corresponding upper case code by decrementing the ASCII value as necessary. For example:
char ch; (if ch >= 'a' && ch <= 'z' ? ch - ('a' - 'A') : ch);(Note the use of colon operator.) Since the translation to upper case is often needed, compiler authors often put it into a handy macro disguised as a function call:
#define toupper(x) \ (if x>='a' && x <= 'z' ? ch - ('a' - 'A') : ch);(This define is generally in the include header file ctype.h, provided with the compiler.)With this macro definition present,
ch = toupper(ch);will translate the character, with code that looks like a function call, but that doesn't invoke the overhead of a function call.Your compiler's macro is probably more complex (and more efficient), but still hides the same pitfall for the uninitiated: the macro argument is substituted (and evaluated) in more than one place.
Consider the statement:
ch = toupper(getch());When toupper() is implemented as a real function call, it works fine; getch() is called, and the returned value is passed to the function toupper(). But if toupper() is really implemented with the macro given earlier, when expanded it becomes
(if getch()>='a' && getch() <= 'z' ? ch - ('a' - 'A') : ch);In this expansion, getch () will be called twice before the evaluations are complete! Clearly, the best way to write this code is the old, longer way:
ch = getch(); ch = toupper(ch);Using function calls as arguments is often the briefest, cleanest coding style. However, if you are to avoid this multiple evaluation trap, you must know which "functions" are really implemented as macros. These macros will all reside in one of the standard header files. The most commonly used macros are probably those in ctype.h and math. h.You can gain additional economies of expression by combining directly returned function values in equations used as arguments to other functions.
For example, these two lines from the earlier hypotenuse example:
temp = square(side_a) + square(side_b); hyp = sqrt(temp);could be condensed to:
hyp = sqrt(square(side_a) + square(side_b));eliminating the variable temp entirely, and representing the entire operation in a single statement.
Deciding What To Return
It is usually most convenient to report errors via a function's return value. A NULL error value has the advantage of allowing the short, easy syntax illustrated by the earlier fgets() controlled loop.However, don't blindly assume that all functions will or should return NULL upon failure. In some cases a NULL can't be used as the error flag. For example, sscanf() returns the number of fields assigned. Zero is clearly a valid response, distinctly different from an attempt to read beyond the end of the input string. When zero is a valid return value, consider using EOF (typically a -1) to indicate an error. To test for EOF and save the return value, one could write:
if ((counter = sscanf(fp, format_in_buff, &my_var)) !=EOF)...If the returned count won't be needed later, the assignment can be omitted:
if (sscanf(fp, format_in_buff, &my_var) != EOF)...However,
if (sscanf(fp, format_in_buff, &my_var))...does not have the same meaning or result!When multiple errors are possible and more information about the error is desired, a single value flag is usually still returned to indicate the failure. However, before returning, the function sets a global variable, such as errno, to a value specifying in detail the cause of the problem. The calling function still uses the returned value to test for an error. But it may refer to the global for more details, if you, as the programmer, so choose.
Conclusion
Function return values can play a constructive roll in writing clear, clean code. A good understanding of how that return value can be best tested and applied will help you develop functions that are both efficient and reusable.