Defining and Invoking Functions

The logic of a C program is typically broken down into several functions. A function encapsulates a specific procedure or functionality, such that the procedure can be “called” whenever and wherever needed. A function can also call other functions, and can even call itself recursively, directly or indirectly. The program starts by calling the main function. We now review functions as a language construct, as well as the mechanism and semantics of function call. We also detail the main function.

Function Definition

At a very basic level, a function defines some code and gives it a name, so that the code can be referenced and ultimately executed from in various parts of the program. Functions are the most basic mechanism for modularization in C. A function most often encapsulate a general computation that can then be applied to specific data. Thus the function definition can also define a set of parameters that the code uses as variables and whose initial values are set for each invocation by the corresponding function-call expression, as we’ll see in detail later.

As an example, consider a game program that needs to print the game score in many phases of the game and therefore in many parts of the program. The code that prints the score could be encapsulated in a function called print_score such as the one displayed below. The function is defined with a parameter points that represents the score to be printed.

void print_score (int points) {
    if (points < 0) {
        printf("Something is wrong: your score is negative!\n");
    } else {
        printf("You have %d point", points);
        if (points != 1)
            printf("s");
        printf("\n");
    }   
}

Function are also often intended to compute something for the caller. So a function can also return a value. The type of the return value is also specified in the function declaration or definition. The print_score function is declared as having a void return type, but that isn’t a real object type, and instead means that the function does not return a value at all. Any other return type specification indicates that the function returns some value to the caller. Here’s an example:

int is_prime (int n) {
    for (int d = 2; d*d <= n; ++d)
        if (n % d == 0)
            return 0;
    return 1;
}

This should be pretty clear. So, let’s move on to the semantics of function calls.

Function Calls

This is an example of a program that calls the is_prime function defined above.

int main () {
    for (int i = 1; i < 100; ++i)
        printf ("%d -> %d\n", i, is_prime(i));
}

Did you see the function invocation in there? (Don’t just skip or skim the code!) Here the value returned by is_prime(i) is used directly as an argument to another function invocation (printf). In fact, a function invocation is an expression that can therefore be used as a sub-expression. Alright, alright. I know what you’re thinking. This is pretty simple: you first define a function f that you then call in other expressions—big deal, let’s move on. True, this should be pretty straightforward. Things can get a bit more complicated in C/C++, since in addition to referring to a function by name, you can in fact do that with an arbitrarily complex expression. Alas, that’s a feature we’ll see some other time.

What we should discuss instead is the semantics of function calls, and in particular the way that the invocation determines the values of the parameters seen by the code of the function. This, too, should be already pretty clear, but a quick refresher is definitely worth our while. The rule can be stated very simply with a bit of technical lingo: in C, function calls are always call-by-value. Now, I assume you understand the term always. It isn’t a technical term: it means ALWAYS. But what do we mean by call-by-value? This one is also simple, but let’s review. We first of all need to clearly distinguish the parameters, which are listed in the function definition, from the arguments, which are the expressions used in a function call. Here comes the example:

int gcd (int a, int b) {        /* parameters: int a, int b */
    while (a != b) {
        if (a > b) {
            a -= b;
        } else {
            b -= a;
        }
    }
    return a;
}

int main () {
    int n,m;
    printf ("input  two non-negative integers: "); 
    scanf ("%d%d", &n, &m); 
    printf ("the least common multiple between %d and %d is", n, m);
    printf ("%d\n", n / gcd (n,m) * m); /* arguments: a <- n; b <- m; */
}

We define a gcd function with two parameters: int a and int b, and we then call that function with arguments n and m. The parameters are variable declarations. The arguments don’t have to be variable names and instead can be arbitrary expressions. For example, we might as well have called gcd (n+7, m*2 + 1). So, what happens when the program executes the function call? The call executes the code of the function with the parameters defined as additional local variables and initialized each with the corresponding argument. This is what we mean by call-by-value. The following code illustrates what happens in the execution of the main function of the example above.

int main () {
    int n,m;
    printf ("input  two non-negative integers: "); 
    scanf ("%d%d", &n, &m); 
    printf ("the least common multiple between %d and %d is", n, m);
    int temp;                   /* code equivalent to: gcd (n,m) */
    {
        int a = n;              /* parameter passed by value */
        int b = m;              /* parameter passed by value */
        while (a != b) {
            if (a > b) {
                a -= b;
            } else {
                b -= a;
            }
        }
        temp = a; 
    }
    printf ("%d\n", n / temp * m);
}

Notice that with call-by-value semantics, the code of the function operates on copies of the arguments that therefore will never be modified by the function (not directly, that is).

The main Function

The execution of a program starts with the main function. As an entry point into the program, the main function serves as an interface to the invocation of the program. The specific mechanisms by which programs are invoked are platform specific and are outside the scope of C/C++. What C and C++ provide is an interface whereby, through the main function, a program can access an array of strings representing the arguments with which the program is invoked. The following is a program that prints all its invocation arguments:

#include <stdio.h>
int main (int argc, char * argv []) {
    for (int i = 0; i < argc; ++i) {
        printf ("arg[%d] = \"%s\"\n", i, argv[i]);
    }
}

Thus conceptually the main function takes a vector of strings represented with two parameters: an integer typically called argc that gives you the number of arguments, and an array of strings typically called argv that contains the argc arguments.

We typically invoke programs using a command shell. I mean, we hackers typically use a command shell. And as hackers, we like to understand exactly what happens between the shell and the C program we invoke through the shell. Here is a common example that uses the ls program.

$ ls -l *.c

The shell breaks down this command “line” into a list of components, interpreting spaces as separators, and also expanding some special characters (or other commands). In this case, the shell expands *.c into the list of file names in the current directory that end with .c (suffix). So, suppose there are two such files in the current directory called hello.c and printargs.c. So, the shell breaks down the command line into the sequence ls -l hello.c printargs.c. Then the shell looks for an executable program called ls, and if it finds one, say /bin/ls, it invokes that program with the argument vector ls -l hello.c printargs.c, such that the main function in /bin/ls is called with argc = 4 and argv = {"ls", "-l", "hello.c", "printargs.c"}. To see this clearly, compile the above program into an executable called printargs in the current directly, and run it with the the same arguments in the ls command. This is what you should get:

$ ./printargs -l *.c
arg[0] = "./printargs"
arg[1] = "-l"
arg[2] = "hello.c"
arg[3] = "printargs.c"

The main function is a bit special. It cannot be used anywhere in the program, so it cannot be called (recursively). It is declared as having an int return value, but it doesn’t have to contain a return statement. If it terminates without executing a return statement, it still effectively returns 0. The return value represents the exit status of the program. The specific interpretation of the exit status is system dependent. However, the stdlib.h header defines two constants, EXIT_SUCCESS and EXIT_FAILURE, that represent a “success” or “failure” status, respectively. For example:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char * argv []) {
    if (argc == 2) {
        printf ("Ciao %s!\n", argv[1]);
        return EXIT_SUCCESS;
    } else {
        printf ("usage: %s <your-name>\n", argv[0]);
        return EXIT_FAILURE;
    }
}