Function Declarations in C
In the lesson on defining custom functions in C language, we saw that the C compiler is a single-pass compiler.
In other words, when the compiler encounters a function call, it must know the function definition before being able to invoke it.
In this lesson we will see how to address this problem through function declarations.
The Problem of Implicit Functions
In previous lessons, when we defined one or more functions, we always wrote the function code before using it.
For example, wanting to write a program that performs the sum of two numbers, we wrote the code of the sum function before invoking it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
In this example, the sum function is defined at lines 3-5 and invoked at line 10.
Any C compiler reads the source code from beginning to end without ever going back to previous lines. In technical jargon, we say that a C compiler is a single-pass compiler.
This means that, examining the example program, the compiler encounters first the definition of sum and, subsequently, its invocation. When it reaches its invocation, line 10, the compiler knows exactly what the function looks like in the sense that it knows what is the type of data it returns and which and how many parameters it needs.
However, defining a function before its invocation is not mandatory. According to the C language standard, it is not necessary to define a function before invoking it. We could have, in fact, written the code of the example in this way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
In this case the compiler, examining the code from the beginning, reaches line 6 and finds the call to the sum function. At this point the compiler knows nothing about the sum function. It does not know what type of result it returns, assuming it returns one, and does not know how many parameters and what types of parameters it accepts.
Modern compilers, that is those that adopt the standard from C99 onwards, such as the latest versions of gcc, clang and Microsoft Visual Studio, return an exception in this case. For example, gcc returns the following error:
error: implicit declaration of function 'sum' [-Wimplicit-function-declaration]
Older versions of compilers, adhering to the C89 standard, however, did not return exceptions. The reason is that, when they encountered a function call whose definition they did not know, the compilers made two assumptions:
- That the returned type is of type
int; - That the passed arguments are of the correct type.
We speak in this case of implicit functions.
Implicit Functions or Implicit Function Declaration
An Implicit Function Declaration represents, in C language, the invocation of a function whose list and type of accepted parameters and the type of returned value the compiler does not yet know.
In the presence of an implicit function declaration:
- Modern compilers return an exception;
- Older compilers assume that the function returns a value of type
intand accepts a number of parameters corresponding to those passed.
Starting from the C99 standard, implicit function declarations are considered a compilation error.
A first way to solve the problem of implicit functions is to adopt the style used so far: to define functions before the point where they are actually used.
This approach can be simple for programs of reduced size. Unfortunately, as complexity grows and, as we will see, when programs are written dividing them into more than one source file, this approach becomes problematic.
Therefore, the C language provides a mechanism to solve the problem: function declarations.
Function Declarations
In a C program, to help the compiler recognize functions that will be defined later, it is possible to declare functions before defining them.
In simple words, it is as if we gave the compiler a preview of the function that we will define later.
A function declaration resembles the first line of a function definition, but without the body of the function itself:
return_type function_name(parameter_type1, parameter_type2, ..., parameter_typeN);
It is obvious that, a function declaration must be consistent with the subsequent function definition. In particular, the return types and the parameter types must be the same.
Returning to the example above, we can define the sum function after the body of main using a function declaration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
In this way, the compiler first encounters at line 4 the declaration of sum. The declaration reveals to the compiler that there exists somewhere a sum function that returns a value of type int and accepts two parameters of type int.
Subsequently, at line 9, the compiler finds an invocation of the sum function. Knowing its declaration, the compiler can check that the passed arguments are actually of type int (as in the example) and that the return type is compatible with the type of variable to which we assign the result.
Finally, at line 17, the compiler finds the definition of sum. In this case, the compiler can check that the definition is consistent with the declaration.
The function declarations we have described also take the very commonly used name of function prototypes.
Since what matters to the compiler are the number and type of parameters, plus the return type, we can declare the prototype of a function without specifying the parameter names:
int sum(int, int);
Function Prototype
A Function Declaration or Function Prototype represents a preview of a function definition. It specifies the return type of the function and the type of parameters it accepts.
The syntax to use to declare a function is the following:
return_type function_name(type1 parameter1, ..., typeN parameterN);
The parameter names can be omitted, specifying only the parameter types:
return_type function_name(type1, ..., typeN);
Function declarations are particularly useful when writing larger programs, where functions are defined in separate source files as we will see in future lessons.
Always Include Parameter Names in Function Prototypes
Although not mandatory, the advice is to always specify the parameter names in function prototypes. This makes the code more readable and clear.
By also including the names, in fact, the purpose of each one is documented and developers using the function are reminded of the order in which arguments must be passed.
As we saw above, starting from the C99 standard, implicit function declarations are considered a compilation error. Therefore, the use of function prototypes is almost always necessary.
Function Prototypes without Parameters
Regarding function prototypes in C language, there is a very important detail to keep in mind.
Suppose we want to declare a function that does not accept parameters. For example, a function that prints a message to the screen.
We might be led to think of declaring the function in this way:
void print_message();
In reality, by doing so, according to the C language standard, we are declaring a function that accepts an undefined number of parameters. In other words, the compiler does not know how many parameters the function accepts.
In this case, the compiler cannot check in advance how many and of what type are the parameters passed to the function. Even though it is a perfectly valid declaration, the advice is to avoid this declaration style.
Function Prototypes without Parameters
If a function is declared without parameters, in C language, the compiler assumes that it accepts an undefined number of parameters:
void function_name();
This reasoning is also valid for function definitions. If a function is defined without parameters, the compiler assumes that it accepts an undefined number of parameters:
void function_name() {
// ...
}
Precisely because the compiler cannot check the number and type of parameters passed to the function, what happens is that we can write invalid programs. For example:
#include <stdio.h>
int sum();
int main() {
int a = 10;
int b = 20;
int result = sum(a, b); // Compilation error
printf("The sum of %d and %d is %d\n", a, b, result);
return 0;
}
int sum(int a, int b, int c) {
return a + b + c;
}
In this example, the sum function is declared without parameters but is defined with three parameters. The program will, surprisingly, be compiled without errors. However, when the program is executed, the fact that the parameter c is not passed to the sum function will lead to undefined behavior.
If we had declared the sum function in this way:
int sum(int a, int b, int c);
The compiler would have returned a compilation error:
error: too few arguments to function 'sum'
The question then arises spontaneously: how do you declare a function that does not accept parameters?
The answer is simple: use the void keyword in the parameter list:
void function_name(void);
In this way, the compiler knows that the function does not accept parameters and can check that no argument is passed to the function.
Function Declaration without Parameters
If a function does not accept parameters, in C language, the compiler must be explicitly informed. To do this, use the void keyword in the parameter list:
void function_name(void);
This strange behavior of the C language is one of the few but important differences compared to the C++ language. In C++, in fact, if a function does not accept parameters, it is sufficient to declare it without parameters:
void function_name();
In C++, the compiler assumes that the function does not accept parameters and does not worry about how many and which parameters to pass.
Empty Parameter List in Function Prototype
When creating a C program, it is important to keep in mind that an empty parameter list is not the same thing as a void parameter list.
In other words, if you declare a function in this way:
void function_name();
The compiler assumes that the function accepts an undefined number of parameters.
If instead you declare the function in this way:
void function_name(void);
The compiler assumes that the function does not accept parameters.
Therefore, it is recommended to always use the void keyword when declaring a function that does not accept parameters.
This behavior is different from that of the C++ language. In C++, an empty parameter list is equivalent to a void parameter list.
Function Signature
A very important concept, related to the prototype of a function, is that of function signature.
In essence, the signature of a function consists of the function name plus the list of parameter types.
Let's take, for example, the following function:
int sum(int x, int y);
The signature of this function is sum(int, int). Note that the signature does not include the return type.
The concept of signature is very important in C language, as there are two fundamental limitations:
-
It is not possible to define two functions with the same signature. In other words, it is not possible to define two functions with the same name and the same list of parameter types but that return different return types.
For example, it is an error to define two functions like this:
int sum(int x, int y); float sum(int x, int y);This is because both functions have the same signature
sum(int, int)and the compiler does not know which of the two to call. -
It is not possible to define two functions with the same name. Even if the two functions have different signatures, it is not possible to define two functions with the same name.
For example, it is an error to define two functions like this:
int sum(int x, int y); int sum(float x, float y);Also in this case, the compiler does not know which of the two to call.
The C++ language, which can be considered in all respects an extension of the C language, removes the second limitation as we will see in the lessons on C++. In C++, in fact, it is possible to define two functions with the same name but different signatures.
For the C language, however, the rule applies that it is not possible to define two functions with the same name.
Function Signature
The function signature, also called function signature, consists of the function name plus the list of parameter types.
The function signature does not include the return type.
Given a function:
return_type function_name(type1 parameter1, ..., typeN parameterN);
The signature of this function is function_name(type1, ..., typeN).
In C language, it is not possible to define two functions with the same signature.
Furthermore, it is not possible to define two functions with the same name.
In Summary
In this lesson we introduced a particular concept of the C language: function declarations or function prototypes.
Function declarations are useful for solving the problem of implicit functions. The latter represent calls to functions whose definition the compiler does not know.
The C compiler is a single-pass compiler. This means that, when it encounters a function call, it must know the function definition before being able to invoke it. In the C99 standard and subsequent ones, implicit function declarations are considered a compilation error.
Function declarations allow to declare a function before defining it. A function declaration consists of the return type of the function, the name of the function and the list of parameter types.
We also introduced the concept of function signature. The signature of a function consists of the function name plus the list of parameter types. The function signature does not include the return type.
In the next lesson we will deepen the concept of argument and parameter of a function.