Programming in C

Working with Multiple Files

Gerald Senarclens de Grancy

Programming Projects

  • Growing projects raise the need to use multiple source files
  • Functionality belonging together is kept in the same file (aka. module)
  • Rule of thumb: different concerns go into different files; eg.
    • Code for user interaction
    • Retrieving data from and storing data in a database
    • Business logic and algorithms

Working with Multiple Files

Which problems do multiple files solve?

  • Sharing code for re-use between programs becomes possible
  • Functionality can be logically grouped together into modules
  • Multiple programmers can work on a project without frequent conflicts
  • Compile time can be drastically reduced (only re-compile affected code)
  • Multiple processors / cores can be used for compilation
  • Interface code can be clearly separated out into header files

Which New Problems Do They Cause?

  • Larger projects increase complexity
  • Splitting functionality requires experience (software architecture)
  • Compilation becomes complicated => we need build automation tools
  • Build tools need to know about dependencies
  • Intermediate files are created and might need to be cleaned up

Compiling and Executing a C Program

For creating executables, a compiler toolchain is needed.

  • GCC - GNU Compiler Collection
  • Clang - C language family frontend for LLVM
  • MSVC - Microsoft Visual C and C++ compiler

Creating a C executable is a four step process.

  1. Preprocessor
  2. Compiler
  3. Assembler
  4. Linker

Example

Download hello.c

By default, all three stages (preprocessor, compiler and linker) are done

clang hello.c
./a.out

Provide a name for the created executable (instead of the default a.out

clang hello.c -o hello
./hello

1. Preprocessor

  • Essentially a pure text processor
  • Removes comments, performs includes etc.
  • Usually never done separately

Stop after preprocessor and send output to stdout

clang -E hello.c

2. Compiler

  • Translates high level code to a lower level (assembly language)
  • Parses the source code
  • Performs type checking
  • Usually never done separately
  • Must recognize all identifiers (function declarations etc.)

Creates assembler files (usually *.s)

clang -S hello.c

3. Assembler

  • Translates assembly language to object code (machine language)
  • Creates *.o files
  • Usually done separately when working with multiple input files
    • Impossible to create executables without main(.)
    • Impossible to create executables with multiple main(.)
clang -c hello.c

4. Linker

  • Combines object files into a unified executable program, resolving all symbols
  • Has to be run separately when working with multiple input files
clang -o hello hello.o

Header Files

Why do we Need Header Files?

Imagine a project with three files: main.c and modules array.c and ui.c

[csource icon]
main.c
[csource icon]
array.c
[csource icon]
ui.c
=>
[csource icon]
executable

What do we have to do to satisfy the compiler when using functionality of array.c in all other files?

What if we have thousands of files and change array.c?

Solution

Write the declarations exported by a source file into a separate header file.

Include the header file where the declarations are used.

Header files do not need to be compiled separately since their content is copied into the source files by the preprocessor.

Example

  1. main.c uses functions defined in array.c and ui.c
  2. ui.c uses functions defined in array.c
project overview

extern and static Functions in C

Every identifier that is marked as extern is part of a file's public interface. These identifiers can be used in other files by including the header file.

  • Functions are automatically extern
  • Use the extern keyword to share variables (if really, really needed)
  • In C, the static keyword marks functions that should not be exported
  • static C functions cannot be used in other files of the same project

#include Guards

A construct used to avoid the problem of double inclusion when dealing with the include directive.

#ifndef FILENAME_H
#define FILENAME_H

// ... your header file's code

#endif // FILENAME_H

#include guards can also be referred to as macro guards, header guards or file guards

Compiling Multiple Files

Compiling all source files produces a single executable.
Remember: *.h files are already included in the *.c files.

clang *.c

It would be much faster to re-compile only the files that need to be re-compiled and then link the object files.
How do we know what needs re-compilation?

Everything affected by a change must be re-compiled. If, for example, a header file is changed, we need to re-compile all *.c files that #include that header file.

Example 1/2

Download main.c, ui.c, ui.h, array.c and array.h (or data/c/multiple_files).
Compile all files at once:

clang *.c -o array_tool

Since we did not create object files, any change would require complete re-compilation. Instead, we should create object files and link them together.

clang -c *.c
clang *.o -o array_tool

Example 2/2

Now, make a change to main.c. Check the return value of get_dimension(.) and issue a warning if the value is 0.

Once done, we can just re-compile main.c and link to the existing object files.

clang -c main.c
clang *.o -o array_tool

Exercise

Extend the program to contain another function
int amax(int *array, size_t dimension)
that returns the maximum of the given array. Add the function declaration to array.h and the definition to array.c. Call the function in main.c and print the result to stdout.

Which files need to re-compiled to object files before a new executable can be linked? Once you know the answer, compile them and create a new executable.

Build Automation Tools

Build tools allow you to re-compile exactly the files that need to be re-compiled with a single command or the click of a button.

There are many different build tools for each programming language.
For C and C++, popular examples are

  • Visual Studio (also takes care of this)
  • GNU make
  • CMake
  • ...

GNU Make

  • Controls the generation of executables and other non-source files
  • + Independent of IDE
  • + Open source
  • + Allows compiling files in parallel
  • - Platform dependent
  • - Requires a complicated Makefile for the project

GNU make quick reference

GNU Make Example 1/2

all: array_tool  # default target

array_tool: main.o ui.o array.o
gcc -o array_tool main.o ui.o array.o

array.o: array.c array.h
gcc -std=c11 -Wall -c array.c

ui.o: ui.c ui.h array.h
gcc -std=c11 -Wall -c ui.c

main.o: main.c ui.h array.h
gcc -std=c11 -Wall -c main.c

.PHONY: clean
clean:
find . -name '*~' -o -name '*.o' -o -name 'array_tool' | xargs rm

Download Makefile

Makefiles quickly get complicated: example Makefile using more features

GNU Make Example 2/2

To make a build, run make -f YourMakeFile

If no -f option is present, make will look for the makefiles GNUmakefile, makefile, and Makefile, in that order.

make  # run the default (first) target
make array_tool # run the target explicitly (usually it is aliased to `all`)
make clean # run the target clean
make array.o # run the target to build array.o if that is needed
make -j8 # run 8 jobs in parallel (use up to 8 CPU cores)

CMake

  • + Powerful, cross-platform build environment
  • + Independent of IDE
  • + Open source
  • + Harnesses the power of GNU Make by generating the Makefile
  • + Builds "out of source" in a separate directory by default

CMake Documentation

CMake Example 1/2

cmake_minimum_required(VERSION 3.18)
project(array_tool VERSION 1.0)
set(C_SRCS # all c source files
array.c
main.c
ui.c
)
set(CMAKE_C_STANDARD 11)
add_executable(${PROJECT_NAME} ${C_SRCS})

Download CMakeLists.txt

CMake isn't trivial either: example CMakeLists.txt using more features

CMake Example 2/2

To make a build, run the following commands

mkdir build  # create directory for all files created during build
cd build
cmake .. # create the Makefile for your setup
make -j8 # start actual build on 8 cores; alternatively `cmake --build . -j8`

Questions
and feedback...