Tutorial : C - Functions

Functions

Functions break large computing tasks into smaller ones, and enable people to build on what others have done instead of starting over from scratch. Appropriate functions hide details of operation from parts of the program that don’t need to know them, thus clarifying the whole, and easing the pain of making changes.

Functions are fundamental to writing modular code. They provide the basic mechanism for enclosing low-level source code, hiding algorithmic details and presenting instead an interface that describes more intuitively what the code actually does. Functions present a higher level of abstraction and facilitate a divide-and-conquer strategy for program decomposition. When combined with file-modular design, functions make it possible to build and maintain large-scale software systems without being overwhelmed by complexity.


Function Prototypes

A function must be declared before it is used. This means that either the function declaration or definition must exist in the source file above the place where it is first called by some other function. A function declaration, or prototype, is an interface specification. It states the function name, the number and type of input arguments, and the return value type. It enables the compiler to perform type-checking—to ensure the argument types being passed to the function match the interface definition—which catches many coding errors at compile time. Some example prototypes are as follows.

void some_procedure(void);
int string_length(char *str);
double point_distance(double, double, double, double);

Notice that the variable names are optional in the declaration, only the types matter. However, variable names can help clarify how a function should be used.

 

Function Definition

A function definition contains the actual workings of the function—the declarations and statements of the function algorithm. The function is passed a number of input parameters (or arguments) and may return a value, as specified by its interface definition.

Function arguments are passed by a transaction termed “pass-by-value”. This means that the function receives a local copy of each input variable, not the variable itself. Thus, any changes made to the local variable will not affect the value of the variable in the calling function. For example,

int myfunc(int x, int y)
/* This function takes two int arguments, and returns an int. */
{
	x *= 3;
	++y;
	return x + y;
}
void caller_func(void)
/* A function that calls myfunc() */
{
	int a=1, b=2, c, d;
	c = myfunc(a,b); /* c = 6 */
	d = a + b; /* d = 3 */
}

In this case, the values passed to myfunc() are x=1 and y=2, respectively, and these are changed to x=3 and y=3 in the subsequent statements. However, the values of a and b are unaffected and d = 1+2 = 3.

To obtain a value from a function, it may specify a return value. The calling function is free to ignore the return value,1 but it is good practice to make this explicit by putting a (void) cast in front of the call. For example,

int an_algorithm(int, int); /* Prototype: two int arguments, and returns an int. */
void caller_func(void)
{
	int a=1, b=2, c;
	c = an_algorithm(a,b); /* use return value */
	an_algorithm(a,b); /* ignore return value (implicitly) */
	(void)an_algorithm(a,b); /* ignore return value (explicitly) */
}
int an_algorithm(int x, int y)
{
	return x*2 + x/y;
}

The return value can be of any type, but there is a limitation that any function may only have at most one return value. To return multiple values it is necessary to either (i) return a compound type in the form of struct, or (ii) directly manipulate the values of the input variables using an approach termed “pass-by-reference”. These methods are discussed in Sections 11.2 and 7.3, respectively.

While a function can only have one return value, it may possess several return statements. These define multiple exit points from the function, from which program-control returns to the next statement of the calling function. If a function is to return a value of a certain type, all return statements must return a value of that type. But, if a function does not return a value, then an empty return; suffices, and this may be omitted altogether for a no-value return occurring at the end of the function block.

1 int isleapyear (int year)
2 /* Return true if year is a leap-year */
3 {
4 	if ( year %4) return 0; /* not divisible by 4 */
5 	if ( year % 100 ) return 1; /* divisible by 4, but not 100 */
6 	if ( year % 400 ) return 0; /* divisible by 100, but not 400 */
7 	return 1; /* divisible by 400 */
8 }

Functions in C are recursive, which means that they can call themselves. Recursion tends to be less efficient than iterative code (i.e., code that uses loop-constructs), but in some cases may facilitate more elegant, easier to read code. The following code examples show two simple functions with both iterative and recursive implementations. The first calculates the greatest common divisor of two positive integers m and n, and the second computes the factorial of a non-negative integer n.

1 /* Iterative GCD: Returns the greatest common divisor of m and n. */
2 int gcd (int m, int n)
3 {
4 	while (n) {
5 		int tmp= n;
6 		n= m%n;
7 		m= tmp;
8 	}
9 	return m;
10 }
11
12 /* Recursive GCD */
13 int gcdr (int m, int n)
14 {
15 	if (n==0) return m;
16 	return gcdr(n, m%n);
17 }

 

Notice that the factorial functions below incorporate argument checking via the standard library macro assert, which causes the program to terminate with an error message if the conditional expression is FALSE.

1 /* Iterative factorial */
2 int factorial (int n)
3 {
4 		int result= 1;
5 		assert(n>=0);
6
7 		while (n)
8 		result *= n−−;
9 		return result;
10 }
11
12 /* Recursive factorial */
13 int factorial r (int n)
14 {
15 		assert(n>=0);
16 		if (n==0) return 1;
17 		return n * factorial r(n−1);
18 }

 

Benefits of Functions

Novice programmers tend to pack all their code into main(), which soon becomes unmanageable.
Scalable software design involves breaking a problem into sub-problems, which can each be tackled
separately. Functions are the key to enabling such a division and separation of concerns.

Writing programs as a collection of functions has manifold benefits, including the following.
• Functions allow a program to be split into a set of subproblems which, in turn, may be further split into smaller subproblems. This divide-and-conquer approach means that small parts of the program can be written, tested, and debugged in isolation without interfering with other parts of the program.
• Functions can wrap-up difficult algorithms in a simple and intuitive interface, hiding the implementation details, and enabling a higher-level view of the algorithm’s purpose and use.
• Functions avoid code duplication. If a particular segment of code is required in several places, a function provides a tidy means for writing the code only once. This is of considerable benefit if the code segment is later altered.2

Consider the following examples. The function names and interfaces give a much higher-level
idea of the code’s purpose than does the code itself, and the code is readily reusable.

1 int toupper (int c)
2 /* Convert lowercase letters to uppercase, leaving all other characters unchanged. Works correctly
3 * only for character sets with consecutive letters, such as ASCII. */
4 {
5 		if (c >= ’a’ && c <= ’z’)
6 			c += ’A’−’a’;
7 		return c;
8 }
9
10 int isdigit(int c)
11 /* Return 1 if c represents an integer character (’0’ to ’9’). This function only works if the character
12 * codes for 0 to 9 are consecutive, which is the case for ASCII and EBCDIC character sets. */
13 {
14 		return c >= ’0’ && c <= ’9’;
15 }
16
17 void strcpy (char *s, char *t)
18 /* Copy character-by-character the string t to the character array s. Copying ceases once the terminating
19 * ’\0’ has been copied. */
20 {
21 		int i=0;
22 		while ((s[i] = t[i]) != ’\0’)
23 		++i;
24 }
25
26 double asinh(double x)
27 /* Compute the inverse hyperbolic sine of an angle x, where x is in radians and -PI <= x <= PI. */
28 {
29 		return log(x + sqrt(x * x + 1.0));
30 }

 

As a more complex example, consider the function getline() below.3 This function reads a line of characters from standard-input (usually the keyboard) and stores it in a character buffer. Notice that this function, in turn, calls the standard library function getchar(), which gets a single character from standard input. The relative simplicity of the function interface of getline() compared to its definition is immediately apparent.

1 /* Get a line of data from stdin and store in a character array, s, of size, len. Return the length of the line.
2 * Algorithm from K&R page 69. */
3 int getline(char s[ ], int len)
4 {
5 		int i=0, c;
6
7 		/* Loop until: (i) buffer full, (ii) no more input available, or (iii) the end-of-line is reached (marked
8 		* by newline character). */
9 		while (−−len > 0 && (c=getchar()) != EOF && c != ’\n’)
10 			s[i++] = c;
11 		if (c == ’\n’) /* loop terminated by end-of-line, want to keep newline character */
12 			s[i++] = c;
13 			s[i] = ’\0’; /* mark end-of-string */
14 			return i;
15 }

 

Designing For Errors

When writing programs, and especially when designing functions, it is important to perform appropriate error-checking. This section discusses two possible actions for terminal errors, assert() and exit(), and also the use of function return values as a mechanism for reporting non-terminal errors to calling functions.

The standard library macro assert() is used to catch logical errors (i.e., coding bugs, errors that cannot happen in a bug-free program). Situations that “can’t happen” regularly do happen, and assert() is an excellent means for weeding out these often subtle bugs. The form of assert() is as follows,

assert(expression);

 

where the expression is a conditional test with non-zero being TRUE and zero being FALSE. If the expression is FALSE, then an error has occurred and assert() prints an error message and terminates the program. For example, the expression

assert(idx>=0 && idx<size);

 

will terminate the program if idx is outside the specified bounds. A common use of assert() is within function definitions to ensure that the calling program uses it correctly. For example

1 int isprime(int val)
2 /* Brute-force algorithm to check for primeness */
3 {
4 	int i;
5 	assert(val >= 2);
6
7 	for (i = 2; i < val; ++i)
8 		if (val % i == 0)
9 			return 0;
10 	return 1;
11 }

 

Another common practice is to place an assert() in the final else of an if-else chain or the default case of a switch when the default condition is not supposed to ever occur.

switch (expression)
{
	case label1: statements; break;
	case label2: statements; break;
	default: assert(0); /* can’t happen */
}

 

Being a macro, assert() is processed by the C preprocessor, which performs text-replacement on the source code before it is parsed by the compiler.5 If the build is in debug-mode, the preprocessor transforms the assert() into a conditional that, if FALSE, prints a message of the form

Assertion failed: <expression>, file <file name>, line <line number>

 

and terminates the program. But, if the build is in release-mode (i.e., the non-debug version of the program), then the preprocessor transforms the assert() into nothing—the assertion is ignored. Thus, assertion statements have no effect on the efficiency on release code.

Note. Assertions greatly assist the code debugging process and incur no runtime penalty on releaseversion code. Use them liberally.
The standard library function exit() is used to terminate a program either as a normal completion (e.g., in response to a user typing “quit”),

if (user_input == ’q’)
   exit(0);

 

or upon encountering a non-recoverable error (e.g., insufficient memory to complete a dynamic memory request).

int* mem = (int*) malloc(50 * sizeof(int));
if (mem == NULL)
   exit(1);

 

The form of exit() is

void exit(int status);

 

where status is the exit-status of the program, which is returned to the calling environment. The value 0 indicates a successful termination and a non-zero value indicates an abnormal termination.
(Also, the standard defines two symbolic constants EXIT_SUCCESS and EXIT_FAILURE for this purpose.)
The need to terminate a program in response to a non-recoverable error is not a bug; it can occur in a bug-free program. For example, requesting dynamic memory or opening a file,

FILE* pfile = fopen("myfile.txt", "r");
if (pfile == NULL)
   exit(1);

 

is dependent on the availability of resources outside of the program control. As such, exit() statements will remain in release-version code. Use exit() sparingly—only when the error is terminal, and never inside a function that is designed to be reusable (i.e., functions not tailored to a specific program). Functions designed for reuse should return an error flag to allow the calling function to determine an appropriate action.

Recognising the difference between situations that require assert() (logical errors caused by coding bugs), and those that require exit() (runtime errors outside the control of the program), is primarily a matter of programming experience.

 

Note. The function exit() performs various cleanup operations before killing the program (e.g., flushing output streams and calling functions registered with atexit()). A stronger form of termination function is abort(), which kills the program without any cleanup; abort() should be avoided in general.

Function return values are often used to report errors to the calling function. The return value might either be used exclusively as a status value,

int function_returns_status (arguments)
{
	statements
	if (success) return 0;
	return 1;
}

 

or might return a certain range of values in normal circumstances, and a special value in the case of an error. For example,

int function_returns_value (arguments)
{
	int val;
	statements
	if (error) return -1;
	return val; /* normal values are non-negative */
}

 

The idea of a return value is to inform the calling function that an error has occurred, and the calling function is responsible for deciding what action is appropriate. For example, an appropriate action might be to ignore bad input, or to print a message and continue, or, in the worst case, to terminate the program.

In particular, many of the standard library functions return error values. It is common practice in toy programs to ignore function return values, but production code should always check and respond suitably. In addition, the standard library defines a global error variable errno, which is used by some standard functions to specify what kind of error has occurred. Standard functions that use errno will typically return a value indicating an error has occurred, and the calling function should check errno to determine the type of error.

 

Interface Design

Good design of function interfaces is a somewhat nebulous topic, but there are some fundamental principles that are generally applicable.

• Functions should be self-contained and accessible only via well-defined interfaces. It is usually bad practice to expose function internals. That is, an interface should expose an algorithm’s purpose, not an algorithm’s implementation. Functions are an abstraction mechanism that
allow code to be understood at a higher level.

• Function dependences should be avoided or minimised. That is, it is desirable to minimise the effect that changing one function will have upon another. Ideally, a function can be altered, enhanced, debugged, etc, independently, with no effect on the operation of other functions.

• A function should perform a single specific task. Avoid writing functions that perform several tasks; it is better to split such a function into several functions, and later combine them in a “wrapper” function, if required. Wrapper functions are useful for ensuring that a set of related functions are called in a specific sequence.

• Function interfaces should be minimal. It should have only the arguments necessary for its specific task, and should avoid extraneous “bells and whistles” features.

• A good interface should be intuitive to use.

 

The Standard Library

The standard library has a large number of functions (about 145) which provide many commonlyused routines and operations. These functions exist on all standard-conforming systems; they are portable and correct, so use them before writing implementations of your own.7 Also, the standard library functions are a good example of quality interface design. Note the use of short, descriptive function names, and intuitive, minimal interfaces.

It pays to become familiar with the standard library. Learn what functions are available and their various purposes. The following is a selection of particularly useful functions listed by category.

• Mathematical functions. sqrt, pow, sin, cos, tan.
• Manipulating characters. isdigit, isalpha, isspace, toupper, tolower.
• Manipulating strings. strlen, strcpy, strcmp, strcat, strstr, strtok.
• Formatted input and output. printf, scanf, sprintf, sscanf.
• File input and output. fopen, fclose, fgets, getchar, fseek.
• Error handling. assert, exit.
• Time and date functions. clock, time, difftime.
• Sort and search. qsort, bsearch.
• Low-level memory operations. memcpy, memset.