Defining Functions in C

In this lesson, we will take a look at the syntax for defining functions in C language. We will also examine how to invoke functions and obtain results from them. Finally, we will see some of the most common errors that developers should avoid.

In detail, we will see:

  1. How to declare a function in C
  2. How to write the body of a function in C
  3. How to invoke a function in C
  4. How to return a value from a function in C

Definition of a Custom Function

The first step in creating a custom function in C language consists of its definition.

The definition of a function in C is composed of two parts:

  • The function header: defines the name of the function, its parameters, and the type of value that the function returns.
  • The function body contains the instructions that must be executed when a function is invoked.

Furthermore, the function body must immediately follow its header.

Let's try to clarify the concepts better with an example. We want to create a function that prints to screen a sequence of 10 asterisks. We will call this function print_asterisks. Let's observe the following program:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

void print_asterisks(void)
{
    for (int i = 0; i < 10; ++i) {
        printf("*");
    }
    printf("\n");
}

int main() {
    print_asterisks();
    printf("Hello, how is it going?\n");
    print_asterisks();
    return 0;
}

Compiling and executing the above program, we will obtain the following result:

**********
Hello, how is it going?
**********

Now let's examine the code. First, we notice that our function print_asterisks is invoked twice within the main, at lines 12 and 14. If we had not defined the function, we would have had to replicate the function's code itself twice. Therefore, the function mechanism allows us to avoid replicating code with all the resulting benefits:

  1. The code is more modular
  2. The amount of code to write is reduced
  3. Programs are simpler to test.

The definition of the function print_asterisks is divided into two parts:

  • Line 3: is the function header. It defines the name, parameters, and return type of the function.
  • Lines 4-9: define the function body between curly braces.

In general, to define a function you must specify the return type before its name and the parameters between round parentheses separated by commas in this way:

Return_Type Function_Name(Type_A Parameter_Name_A, Type_B Parameter_Name_B)

However, the function print_asterisks defined above does not return any value, so its name is preceded by void. Furthermore, it does not need parameters, so between round parentheses we have inserted void.

As for the body of the function, it contains all the instructions that the function executes between curly braces. In our case, the body of print_asterisks is defined at lines 4-9.

Definition

Definition of a Function in C

The syntax for defining a function in C is the following:

Return_Type Function_Name(Type_A Parameter_Name_A, Type_B Parameter_Name_B)
{
    /* Function body */
}

The syntax can be broken down into two parts:

  • The function header:

    c Return_Type Function_Name(Type_A Parameter_Name_A, Type_B Parameter_Name_B)

  • The function body:

    must immediately follow the header and is enclosed between two curly braces {}.

The header of a function, in turn, is composed of:

  • Return_Type: is the return type of the function. If the function does not return any value, void is used.
  • Function_Name: is the name of the function.
  • Type_A Parameter_Name_A, Type_B Parameter_Name_B: the list of input parameters of the function.

An important point to emphasize is that C compilers are Single Pass Compilers. What does this mean? It means that the compiler, when analyzing the source file, starts from the first line and, in sequence, analyzes the subsequent lines until the end but never goes back. The consequence is that any identifier that the compiler finds must have been necessarily defined before.

For functions in C, this translates into the fact that, when the C compiler finds the invocations to the function print_asterisks at lines 12 and then 14, it expects that the function has already been defined. If we had inverted the main function and the print_asterisks function, we would have obtained a compilation warning or a compilation error. Let's try to examine the following listing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* WARNING: This code uses a function before defining it */
#include <stdio.h>

int main() {
    /*
     * The C compiler has not yet found
     * the definition of print_asterisks
     */
    print_asterisks();
    printf("Hello, how is it going?\n");
    print_asterisks();
    return 0;
}

void print_asterisks(void)
{
    for (int i = 0; i < 10; ++i) {
        printf("*");
    }
    printf("\n");
}

In this example, the invocations of print_asterisks at lines 9 and 11 could produce a compilation error or a warning. For example, the gcc compiler produces the following output:

test.c: In function 'main':
test.c:9:5: warning: implicit declaration of function 'print_asterisks' [-Wimplicit-function-declaration]
    9 |     print_asterisks();
      |     ^~~~~~~~~~~~~~~

In practice, gcc is warning us that the function print_asterisks whose invocation was found at line 9 has been implicitly declared. The compiler, in this case, does not provide an error but a warning. Other compilers might, instead, terminate compilation with an error. For this reason, you must always define a function before its use.

To be able to define a function after the point where it is used in C, it is possible to use function prototypes which we will see later.

In general:

Definition

A C Compiler is a Single Pass Compiler

Every C compiler is a Single Pass compiler. This means that during compilation the source files are analyzed only once from the first line to the last.

If in a line there is a reference to an identifier that has not been defined yet, because its definition is located on a subsequent line, you get a warning or a compilation error.

It follows that every function invocation must be subsequent to the definition of the function itself or its prototype.

Function Parameters

In the example program seen above, we defined a function, print_asterisks, that prints 10 asterisks to video. Let's try to modify the program so that we can print, depending on the case, a different number of asterisks.

A simple way to address this problem could be to define various functions. For example, we could define:

  • print_10_asterisks to print 10 asterisks to screen
  • print_20_asterisks to print 20 asterisks

and so on...

This solution, however, is not optimal at all. The number of functions to define could multiply enormously. It would follow that we would have to replicate the code of print_asterisks many times, changing only the number of times that the internal for loop should repeat.

How can we, therefore, indicate to a function a value that modifies its behavior depending on the case? Through the use of parameters.

Function parameters provide the means through which it is possible to pass information to the function itself. In general, a parameter is a placeholder for the arguments that the caller passes to the function itself:

Definition

Function Parameter

A function parameter is a placeholder variable that receives within it a value passed by the caller of the function. This variable can be used within the body of the function itself.

Definition

Function Argument

The function argument is the actual value that the caller of the function passes to the latter within one of its parameters.

To specify the parameters of a function, it is necessary to indicate the list of parameters between parentheses and separated by commas. In our case, a single parameter is needed that indicates how many times the asterisk character must be printed:

void print_n_asterisks(int n)

We have called the parameter n and with it the caller can indicate the number of asterisks to print. Below is the complete example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

void print_n_asterisks(int n)
{
    for (int i = 0; i < n; ++i) {
        printf("*");
    }
    printf("\n");
}

int main() {
    print_n_asterisks(10);
    printf("Hello, how is it going?\n");
    print_n_asterisks(20);
    return 0;
}

If we try to compile and execute this program, we get the following output:

**********
Hello, how is it going?
********************

In this program, the new function print_n_asterisks accepts a parameter n which is an int and which indicates the number of times that the function must print the asterisk character to screen. This parameter is local to the function, that is, the parameter n can be used exclusively within the function body.

The value of parameter n changes at each invocation depending on the argument with which the function is invoked. In fact, the function print_n_asterisks is invoked twice:

  • At line 12 with argument 10
  • At line 14 with argument 20

In the first case, n will be worth 10 within the function body while in the second case it will be worth 20.

We can further extend our example. Let's say that we want to modify the function so that we can indicate to it, in addition to the number of times, also the character to print.

We can define a new function: print_n_characters

void print_n_characters(char c, int n)

In this case, we have specified two parameters of the function separated by a comma:

  • char c indicates the character to print
  • int n indicates the number of times

Therefore, we can rewrite the example in this way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

void print_n_characters(char c, int n)
{
    for (int i = 0; i < n; ++i) {
        printf(c);
    }
    printf("\n");
}

int main() {
    print_n_characters('*', 10);
    printf("Hello, how is it going?\n");
    print_n_characters('-', 20);
    return 0;
}

If we try to compile and execute this program, we get the following output:

**********
Hello, how is it going?
--------------------

Functions that Return a Value

A function can also return a value, that is, provide as output a result of a processing.

To specify that a function returns a value, you must specify the type of that value in the function definition itself. Often in technical jargon, when a function returns a value of a certain type "A", it is said that the function is of type "A".

Let's try with an example: let's define a function cube that returns the cube of an integer number, that is, the value of the number raised to the cube. The function cube will be a function of type int:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int cube(int n) {
    return n * n * n;
}

int main() {
    int v = 0;
    printf("Enter an integer number: ");
    scanf("%d", &v);
    printf("The cube of %d is %d\n", v, cube(v));
    return 0;
}

The first thing to notice in this example is the way we defined the function, in particular its header:

int cube(int n)

In particular, we inserted the type int of the returned value before the name cube. We are telling the compiler that this function returns an integer. Any valid type of C can be used as a return type. In case, instead, the function does not return any value (it is therefore a procedure) we can use void as we did in previous examples.

When a function returns a value, it can be used within any expression that admits that type. For example, we could have used the function cube defined above within an expression of the type:

int x = 5 + cube(6);

During execution, the program will first give control to the function cube and, subsequently, take the result of cube(6) and then substitute it in the expression. In the end, the variable x will contain the value 221.

Keyword return

When a function returns a value, you must use a mechanism to indicate to the compiler what this value is. In C, the keyword return is used. Returning to the above example, let's observe line 4:

return n * n * n;

Here we used the C keyword return to indicate to the compiler what the value is that our function must return. In particular, return must be followed by a valid C expression. In our case, it is followed by the expression n * n * n which calculates the value of n cubed.

Definition

Keyword return

The keyword return is used within the body of a function to specify the return value of the function itself.

It must be followed by an expression that has the same type as that returned by the function:

return expression;

Exit Code

As we have had the opportunity to observe, in all the examples shown so far, the main function also has a return value.

In fact, most C programs have a main function made in this way:

int main() {
    /* Code */
    return 0;
}

In light of the new information seen in this lesson, we therefore know that main is a function that returns an integer, but in most of the examples seen so far this integer is always equal to zero: 0. What does this value represent?

The value returned by the main function is called Exit Status, in Italian: Exit Code.

This exit code is used in all operating systems, Linux, Windows, and MacOS X, to generically indicate whether the program executed correctly or had problems during its execution. In particular, an exit code equal to 0 indicates that the program closed correctly. Conversely, any value other than 0 indicates that an anomaly occurred during execution.

A code other than 0 represents an anomaly, but the meaning of each code varies from program to program and is up to the developer.

The C standard defines, in particular, two values in the header file stdlib.h:

  • EXIT_SUCCESS: which is typically 0 and indicates success.
  • EXIT_FAILURE: which is typically 1 and indicates a generic failure.

Therefore, we can rewrite our programs in this way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>

int main() {
    /* Code */

    if ( /* error */ ) {
        return EXIT_FAILURE;
    }
    else {
        return EXIT_SUCCESS;
    }
}
Definition

Exit Code

The Exit Code is a value returned by the main function that is used to indicate to the operating system whether the program execution was successful or an anomaly occurred.

An Exit Code equal to 0 indicates Success.

An Exit Code other than 0 indicates a Failure and its specific meaning is at the developer's discretion.

Absence of return

When we define a function that returns a value but there is no return expression, typically the compiler warns us with a warning. This is because the behavior is not defined.

Take, for example, the following program:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>

int square(int n) {
    /* WARNING: No return expression */
}

int main() {
    printf("The square of 6 is: %d\n", square(6));
    return 0;
}

In this program, we defined a function square that should return an int but that does not have any return expression in its body. When we subsequently call the function at line 8, we cannot know in advance what can happen. At that point, in fact, the program expects a return value that does not exist, so the behavior is unpredictable.

Trying to compile this program with GCC, we get no error, unless we use the compilation flag -Wall which tells the compiler to report any warning. In that case, in fact, we get an output similar to the following:

$ gcc -Wall test.c -o test
test.c: In function 'square':
test.c:5:1: warning: control reaches end of non-void function [-Wreturn-type]
    5 | }
      | ^

The GCC compiler, in fact, signals the warning: "control reaches end of non-void function".

We can also force the compiler to report an error in this case by using the compilation flag -Werror which transforms any warning into an error:

$ gcc -Wall -Werror test.c -o test
test.c: In function 'square':
test.c:5:1: error: control reaches end of non-void function [-Werror=return-type]
    5 | }
      | ^
cc1: all warnings being treated as errors

Since the behavior is not predictable in these situations, it is always good practice to ensure that non-void functions return a value with a return expression:

Definition

Every non-void function must always have a return expression

It is good programming practice to insert a return expression within non-void functions.

void Functions and return

A void function, that is, a function that does not return any value (and therefore a procedure), should not have a return expression inside it.

However, there may be some cases where it may be useful to exit early from the function itself. In that case, you can use an empty return expression, that is, not followed by any expression, in this way:

return;

Let's try to clarify with an example. Suppose we want to implement a function that takes an integer number as input. If this number is positive or equal to 0, the function prints to screen a line of 10 asterisks *. If the number is negative, in addition to the asterisks, the function must print a line of 10 at signs @.

A first implementation could be:

1
2
3
4
5
6
7
8
9
void my_function(int n) {
    if (n >= 0) {
        printf("**********\n");
    }
    else {
        printf("**********\n");
        printf("@@@@@@@@@@\n");
    }
}

We could rewrite the above function in this way:

1
2
3
4
5
6
7
void my_function(int n) {
    printf("**********\n");
    if (n >= 0) {
        return;
    }
    printf("@@@@@@@@@@\n");
}

Even if it is not properly elegant, in this example we used an empty return expression to exit early from our function in case of a positive number or equal to 0. In this way, the instruction at line 6 is executed only in case of a negative number.

Definition

Empty return Expressions

An empty return expression can be used exclusively in a void function to exit early from the function body:

return;

In Summary

This lesson represents an overview of how to define functions in C language. We have seen how to define a function, how to pass parameters, and how to return a value. We have also discussed how to use the keyword return and what the Exit Code of a function represents.

Furthermore, we introduced the main difference between parameter and argument of a function.

In the next lessons, we will examine the concepts seen in this lesson in detail, examining particular cases.

We will start right from the Invocation Mechanism of a function.