Introduction to C: Moving Code to Another File

Now that we've enhanced our Makefile to cope with separate .c files. we can now start organizing code according to its purpose. Let's move the code that reads a line of input into a separate .c file.

Creating a Header File: input.h

When you separate your code into multiple files, it's necessary to document which functions are intended to be visible from other files. (This is also known as an extern in C.)

To create a header file, you must declaring a list of the functions you want exported (including their return type, name, and parameter types) and placing them into a header file. Our header file, input.h, will be as follows:

#ifndef _INPUT_H
#define _INPUT_H

char* input_getline();

#endif // _INPUT_H

While not required, it's common to surround declarations within a header file with the #ifndef / #define / #endif pattern shown above. This is done in case the header file is included more than once (either by accident, or implicitly via another header file). Without the #ifndef guard, this could result in duplicate definitions for the function. For this purpose, you must choose a unique identifier that will not be used anywhere else in the program to use for this purpose. Typically, this will include the name of the header file.

Moving our getline() code to input.c

Our code in input.c will be as follows:

#define _POSIX_C_SOURCE 200809L

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

char* input_getline()
{
    char* line = NULL;
    size_t buffer_size = 0;
    ssize_t result;

    result = getline(&line, &buffer_size, stdin);
    if(result < 0)
    {
        perror(__func__);
        exit(1);
    } else if (result > 0) {
        // Remove trailing line break.
        line[result-1] = '\0';
    } 

    return line;
}

In this example, we've simplified the API for getline() so that it only includes what we'll need. Since we are relying on getline() to allocate a buffer for us, the function won't need to take in a char** as a parameter. Since we won't be re-using the buffer, that means we won't need to know the size of the buffer either! Finally, we will always be reading from stdin. So our function won't need any parameters.

Another common pattern in C is to prefix the function name with the name of the module it is contained in. Since we've created input.c, it seems to make sense that all the functions exported from it should be called input_<something>(). Some teams will use uppercase to indicate the module, to disambiguate the module name from the function name.

Defensive Programming

In this example, I've also added some defensive programming (in other words, robust error checking). This helps ensure that users can debug the problem if something goes wrong.

According to man getline, if an error occurs, -1 will be returned in place of a count of characters read. If an error occurs, the errno variable will be set to indicate what happened.

Standardized Error Codes

The possible values for errno can be found in the manual page, under man 3 errno. C provides functions to convert the error code into a human-readable message, such as perror() and strerror().

The if / else statement

To run a different section of code depending on the result of getline(), we'll use the if statement. If the condition inside the if(<condition>) portion of the statement evaluates to true (in C, that means anything non-zero) , the code after the if will be executed. If you do not include curly braces ({, }), only the next statement will be executed. It's common practice to include curly braces even when only executing a single line, so that the code is easier to read and maintain.

According to the manual, a return value of result < 0 will only occur if there is an error. So that's what I've used as the condition for the if(...) statement.

If the result was not negative, there is one more case we need to handle: removing the linefeed. To do this, we can use an else if(...) construct after the scope for the initial if(...). Just in case zero bytes were returned (although it's unclear if that can even happen), we'll check if at least one character was read before doing that.

Some new concepts: perror(), __func__, and exit()

Let's take a look at the error checking code we added in the first part of the if(...):

perror(__func__);
exit(1);

This code makes use of a couple of new concepts: The __func__ keyword. This keyword (only available in newer C standards) will be replaced with a string indicating which function it is contained in. (In this case, it will be "input_getline()". The perror() function. This function (defined via stdio.h) will print the human-readable version of the errno string, after printing the string passed into it. In our case, if an error occurs, the user will see a line of text such as input_getline: <error-string> printed to the stderr. * The exit() function. This function, available when stdlib.h is included, immediately exits the program with the specified error code. Note that it's not good practice to simply exit the program in this way. In this case, the conditions in which getline() could return an error are both rare and difficult to recover from, so calling exit() might be appropriate. But if you use exit() regularly, it risks becoming a difficult-to-track-down source of bugs. Since we've made sure to use perror() just before exiting, the reason for the exit() should be easy to diagnose. (If you use exit(), it's good practice to use a unique error code for each different call, so the reason for the exit() is easily identified.)

Using our function from another file

Back in main.c, we'll want to import our new header file (input.h) in order to get the definition for input_getline(). To do that, we'll write the following line:

#include "input.h"

We use single-quotes here to indicate that the compiler should prefer user-defined header files over header files provided by the system.

Now, we can greatly simplify the code in main.c:

int main(int argc, char* argv[])
{
    char* line;

    printf("What is your name?\n");
    line = input_getline();
    printf("Hello, %s!\n", line);
    free(line);

    return 0;
}

Running the Code

With main.c changed to use our new function, let's give it a try:

$ make
cc -Wall -Werror -std=c11 -g -c main.c -o main.o
cc -Wall -Werror -std=c11 -g -c input.c -o input.o
cc -o main *.o
$ ./main
What is your name?
Mike
Hello, Mike!

Conclusion

By now, you should know how to structure your code so that you can call it from another file. We've also touched on the if statement, and a few more library functions.

In the next lesson, we'll look at how to create a basic data structure (a struct in C), and functions to operate on it.