Function Prototypes and
an Overview of Functions with Address Parameters, Arrays, and
Testing
Introduction
As we have explored problem solving using C, main topics have included variables for several types of data, control structures, such as conditional statements (if, switch), loops (for, while), program organization including simple functions, and assertions.
To move our problem solving to a deeper and more sophisticated level, the next step is to explore two issues:
-
Minor issue: The declaration of functions must provide adequate information for the compiler to perform its work.
-
Major issue: Additional capabilities with functions and the capability to work with collections of data both require an understanding of the allocation of space for data within the computer's main memory.
This reading addresses the minor issue, by introducing the concept of function prototypes. The rest of this course segment discusses the storage of data within a computer's main memory, focusing on three foundational topics:
-
The run-time stack: The allocation and deallocation of main memory, when functions are called and when they return,
-
Parameter passage of values and addresses: Storage and access to parameters and data in the context of functions, and
-
Arrays: Storage of several data values within the context of a single variable name.
C Provides Low-level Machine Access
The C programming language provides considerable access to data in main memory with few checks or constraints. In general, an underlying principle is that a programmer knows what work needs to be done, and the language provides mechanisms to perform that work directly.
With this capacity to access and manipulate data directly, processing with C can be particularly efficient. Further, C often is the language of choice for low-level problem solving, such as developing operating systems or handling network communications. Of course, unencumbered access to data also implies the capability to generate errors with little checking or second guessing.
Up until now in the course, we have been able to focus on relatively high-level problem solving (e.g., variables, conditional statements, loops, and simple functions). At this point in the course, however, we need to explore low-level details within C more closely. As we shall discover, much processing within C depends upon the layout within main memory of variables, parameters, and other elements.
Observation/encouragement to readers: Much of this material will be new to many students, so readers may find that it takes some time to become comfortable with various concepts. Some ideas may seem reasonable, but it takes a moderate amount of experience to incorporate these concepts into one's problem solving and to become proficient and consistent in writing programs using these ideas.
If elements of this course segment seem awkward at first, be patient! The experience of many students indicates that these ideas will come together dependably with practice.
Function Prototypes
Consider the work of a compiler. A compiler reads a C program line-by-line and translates each statement to machine language, so the resulting code can be run. Although the art of constructing an efficient and effective compiler requires considerable skill, some elements are relatively straight forward:
-
The declaration of a variable allows the compiler to allocate the appropriate amount of space for the relevant type of data.
-
Rules dictate what machine operations should be used for various arithmetic operations. Since int, float, double data are stored in different ways (more about the representation of data in a later course segment), a compiler can examine the data types of each value and/or variable to utilize the appropriate machine instructions for the prescribed work.
-
Rules also dictate when conversions of data are to take place. For example, when an int is added to a double, C specifies that the int should be converted automatically to a double before the addition is performed.
Challenges arise, however, when functions are used. As a simple example, consider the following function that adds 2.0 to its parameter and returns the result.
double add2 (double value) { return 2.0 + value; }
Now suppose the main program contains the following lines:
int x = 5; double y = add2(x);
If the compiler knows about this function before it encounters the expression add2(x), then the compiler will recognize that the int 5 must be converted to the double 5.0 before the function is applied. However, if the compiler has not encountered the definition of add2 previously, the compiler could not know whether the parameter for add2 should be an int or double.
Given this possible confusion, C allows three approaches.
-
If the function is defined before it is used, the compiler uses the function header to determine parameter types and the return type. (Likely, this is what a programmer would expect.)
-
If the function is not defined before it is used, the compiler examines the actual parameters from the function call (e.g., the variable x in the example).
- If the actual parameter supplied is some type of integer (e.g., short, int, unsigned short, or unsigned int), C assumes the parameter is supposed to be an int.
- If the actual parameter supplied is a type of real number (e.g., float or double), C assumes the parameter is supposed to be a double.
-
If a function has not been formally defined, C allows a programmer to provide a function prototype — the function header without the braces or the function body. For example, a file might contain the prototype near the start of the program:
double add2 (double value);
In this case, the compiler will know that the parameter will be a double, and thus the compiler will not have to guess what to do with the call add2(x) — the compiler will know that the integer x must be converted to a double before the function add2 is called.
Altogether, a function prototype provides information about the parameters and return type of a function at the start of a program. In summary, a function prototype allows a compiler to generate appropriate code, even if the full details of a function have not yet been given within a program. Of course, if a function prototype is used, the full definition of the function (duplicated header together with the function body) must be defined before the program can be run.
Using Function Prototypes and Supplying Function Details
Program add2.c illustrates a complete program using the add2 function prototype early in the code and giving the details of the function near the end.
/* Program illustrating a function prototype near the start of the code and the full function definition near the end. */ #include <stdio.h>
/* function to add 2 to a value and return the result */ double add2 (double value); // function prototype, with semi-colon at end
A function prototype indicates the number and type of parameters and the return type of the function.
-
A function prototype provides full information to a compiler about what to expect when this function is called.
-
The semi-colon at the end of the function header identifies this as a function prototype. No braces are given following the header, and there is no function body for a function prototype.
int main () { printf ("program illustrating a function prototype\n"); int x = 5; double y = add2(x); // function used printf ("x = %d, y = %lf\n", x, y); return 0; }
-
Although the compiler has not yet seen the function body for add2, the function prototype above provides adequate information for the compiler to know that the integer x must be converted to a double before the function add2 is called.
/* function to add 2 to a value and return the result */ double add2 (double value) // function details given here { return 2.0 + value; }
This program supplies the details for add2 near the end of the program.
-
The function header must agree exactly with the number and type of parameters and with the return type of the function.
-
The body of the function is placed in braces { }.
Why Use Function Prototypes?
Although the program add2.c illustrates how function prototypes can be defined and used, a student still may wonder why? Why not just remove the function prototype in a program and place the function header, with its function body, at the start of the code?
Frankly, for program add2, moving the function header and body to the top of the code, replacing the function prototype, would work fine! However, for more complex problems and environments, there can be at least three motivations for the use of function prototypes.
-
Construction and use of libraries: Development of software often involves the use of various C libraries. For example, work with Scribbler 2 robots takes advantage of the MyroC infrastructure package. In using this material, a separate header file MyroC.h. A careful examination of this file shows that this header file is a listing of function prototypes — the file identifies the various MyroC functions, including their parameters and return types. Similarly, the header files for stdio.h, stdlib, time.h, assert.h, etc. all specify function prototypes.
When using these libraries, the #include statement effectively copies the function prototypes into the program, so the compiler will know what parameter names, parameter types, and return types to expect.
When libraries are used, the function bodies are compiled separately, so you do not have to wait for that material to be compiled when you are compiling your own MyroC programs. Then, when your program is compiled, as your program starts to run, the library functions (the details from the relevant function bodies) are linked into your program to yield a complete program.
Overall, function prototypes associated with C libraries allow compilers to incorporate C functions into programs, by specifying what parameters and return types can be expected for each function.
-
Program organization: When a program includes many functions, the list of functions, with their function bodies, can be quite long — extending to many pages. Unfortunately, in practice, searching through the many pages to find function headers is time consuming. Instead, collecting function prototypes together at the start of a program can serve as a convenient index or directory of what functions are defined and what parameters are specified in which order.
As a simple example, the rBeep command telling a robot to beep requires both a frequency and a duration. In practice, a programmer may remember that two parameters are needed, but it is easy to forget whether duration or frequency comes first. Consulting the rBeep function prototype indicates duration is the first parameter, and finding this in the header file is relatively quick. In contrast, a full listing of the function bodies for MyroC involves about 24,000 lines — not something to search quickly! Similar comments apply to programs containing numerous functions that you write yourself
-
Functions that call each other: In some contexts, problem solving yields multiple functions that call each other. For example, function A might call function B, but function B also might call function A. Function prototypes are essential in this context.
Functions that call each other are said to be recursive. The subject of recursive functions and recursive algorithms is deferred to a later time.
For example, if function A were defined first, then there would be a reference to function B, but the compiler would not know the details of B yet. Similarly, if function B were defined first, the compiler would not know about A. With function prototypes, the compiler will know about the parameters and return type of both functions at the start. Then, when the compiler encounters the calls in the function bodies, the compiler already knows how to interpret each context.
created 6 August 2016 by Henry M. Walker revised 10 August 2016 by Henry M. Walker |
![]() ![]() |
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu. |