Lab: Multifile Projects

Today’s lab will help you practice working in a C project with more than one source file. Unlike some of our recent labs, this one will not include a Makefile. We’ll look at make again soon, but for today you will practice compiling code in the terminal again.

To get started, download multifile.tar.gz and extract it with the following terminal commands:

$ cd csc161/labs
$ tar xvzf ~/Downloads/multifile.tar.gz
$ cd multifile
$ code .

A. Calling Functions in Other Files

The starter code included two source files, farm.c and chicken.c. Complete the following exercises using these two source files.

Exercises

  1. Review the code in farm.c and chicken.c. Make a guess about what will happen when you compile the code with this terminal command:
    $ clang -o farm farm.c chicken.c
    

    Write down your guess, then check it by running the command.

  2. Write down a quick explanation of what happened and why. If you guessed incorrectly you may want to refer back to today’s assigned reading to see why we get this result.

  3. Edit farm.c to correct the provided code. Confirm your fix worked by compiling and running the program.

B. Header Files

As our reading discussed, it can be a nuisance to declare functions in every source file that will use them. In C, we use header files to write down declarations for functions that will be used across files. The following exercises walk you through creating a header file for this project.

Exercises

  1. Create a file named chicken.h. Take the chicken_sound declaration you wrote in farm.c in the previous part of this lab and move it to chicken.h.

  2. Now add this line to both farm.c and chicken.c:
    #include "chicken.h"
    

    The chicken.c file includes its own header file because this allows the compiler to check to make sure the declaration and definition match.

  3. Compile the program with the same clang command as before. Make sure everything still works.

  4. You may have noticed that we don’t pass chicken.h to the clang command when compiling this project. In fact, we will never pass a .h file to a compiler. Why do you think that is? Discuss with your partner and write down your guess.

C. Working with Multiple Header Files

You may have noticed two commented-out lines in farm.c that refer to other animal types. We’re going to add these in a moment, but first we need to think about how we can avoid duplicating code. Specifically, look at the make_sound function in chicken.c. Each one of the farm animals our farm simulator supports will use this function, so we should put it in a place where it can be shared easily.

Exercises

  1. Create a new header file util.h. Now copy the implementation of make_sound into util.h. You’ll also need to add this line to the top of util.h:
    #include <stdio.h>
    

    You might have noticed that #include can use either "" or <> around the file name, but these have slightly different meanings. A #include with <> looks for a standard include file on the system, while an #include with "" will look in the current directory.

  2. Now delete make_sound from chicken.c. We’re going to use the version in util.h instead. We can access this by including util.h in chicken.c. Add this line to the top of chicken.c:
    #include "util.h"
    

    You should now have a project with this arrangement:

    farm.c
    contains the definition of main and includes both stdio.h and chicken.h.
    chicken.c
    contains the definition of chicken_sound and includes both chicken.h and util.h.
    chicken.h
    contains the declaration of chicken_sound and includes stdio.h.
    util.h
    contains the definition of make_sound.
  3. Compile the project and make sure it works just as it did at the end of part B.

D. Adding More Animals

Now that you’ve moved make_sound to a header file, we’re going to create source files for two more animals in the farm simulator in the following exercises.

Exercises

  1. We’ll use chicken.c and chicken.h as a starting point for the next animal (a cow). Run these terminal commands to copy the files:
    $ cp chicken.c cow.c
    $ cp chicken.h cow.h
    
  2. Edit cow.c and cow.h so they define and declare a cow_sound function, and update the sound so it makes sense.

  3. Add the line #include "cow.h" to farm.c, and uncomment the call to cow_sound inside of main.

  4. Now compile the whole project with the following command. Note: This will not work, and we’ll discuss why in the next part of the lab.
    $ clang -o farm farm.c chicken.c cow.c
    

E. Fixing Linker Errors

You should see an error about a duplicated make_sound symbol when you compiled this project. This error looks a bit different from the errors we usually get from clang because it is a linker error. This error tells us that there are two definitions of the make_sound function: one in chicken.c and one in cow.c. The following exercises will help you understand and resolve this error.

Exercises

  1. Why do you think the linker says chicken.c and cow.c contain a definition of make_sound when it is actually written in util.h?

  2. A standard practice in header files is to add include guards around any definitions in the header. Typically they look like this:
    #ifndef SOMEFILE_H
    #define SOMEFILE_H
    // Declarations go in here
    #endif
    

    These include guards ensure that the header file is included only one time. You have to be careful to make sure you choose a different constant (SOMEFILE_H in the example above) for each header file. A more modern approach is to use this line at the top of a header file:

    #pragma once
    

    Add include guards or #pragma once to all your .h files and then move on to the next step.

  3. Do you think this will fix the error? Make a guess, then try compiling again with this command:
    $ clang -o farm farm.c chicken.c cow.c
    
  4. Unfortunately, include guards do not fix the error (although they are a very good idea to use). That’s because of how C compilation works. When we compile a multi-file project in C the compiler actually compiles the files separately and combines them later with the linker. The problem is that util.h defines the function make_sound, and both chicken.c and cow.c pull that definition in when they include util.h.

    The real fix for our linker issue is to add one more file, util.c. We will declare make_sound in util.h, and then define it in util.c. Add this file along with appropriate includes and then compile the project with this new command:

    $ clang -o farm farm.c chicken.c cow.c util.c
    
  5. Once you have the previous step working, repeat the process to add sheep to the farm simulator.

At the end of all these exercises your project should have the following organization:

farm.c
contains the definition of main and includes stdio.h, chicken.h, cow.h, and sheep.h.
chicken.c
contains the definition of chicken_sound and includes both chicken.h and util.h.
cow.c
contains the definition of cow_sound and includes both cow.h and util.h.
sheep.c
contains the definition of sheep_sound and includes both sheep.h and util.h.
util.c
contains the definition of make_sound and includes stdio.h.
chicken.h
is wrapped in include guards (or uses #pragma once) and contains the declaration of chicken_sound.
cow.h
is wrapped in include guards (or uses #pragma once) and contains the declaration of cow_sound.
sheep.h
is wrapped in include guards (or uses #pragma once) and contains the declaration of sheep_sound.
util.h
is wrapped in include guards (or uses #pragma once) and contains the declaration of make_sound.

This is obviously a ridiculous number of source files for such a simple project, but this is a common arrangement for multifile C projects. Most C developers will declare functions and #define constants in header files, then put their implementations in .c files. There are other ways to share data and functions across files in C, which we’ll discuss soon in our day focused on program design.