5/9
4.1. Compilation
Chain of compilation principle
We call generally, compilation the process of converting the human readable source code into a binary executable. C++ compilation works in many steps.
A first set of steps are performed on each individual C++ source code. We can note
-
Preprocessor
-
Parse and expand macros statements starting by # (ex. #include, #define, etc)
-
Output of the preprocessor can be viewed using the compiler argument -E.
ex$ g++ -E main.cpp
-
-
Compiler
-
Convert C++ code into assembly code (instructions to processors).
-
This actual compilation step contains itself several substeps, including code optimization.
-
Output of the compilation can be viewed using the compiler argument -S.
ex.$ g++ -S main.cpp
-
-
Assembler
-
Convert human readable assembly code into binary machine code.
-
The resulting file is called object file.
-
Output of this step is obtained using the compiler argument -c.
ex.$ g++ -c main.cpp
-
At this step, object files are not yet executable. They contain machine code instruction and data from a given compiled C++ source code, but cannot access to functions needed from other external source code files or precompiled libraries.
A last step called is performed by the linker.
Link consists in grouping all object files needed for a program together, and resolve external function calls in them. This results in an executable file.
Compiler options
Common compilers such as GCC and Clang follows similar command line arguments.
Among the most common, you may remember
-
-Wall -Wextra: Activate most common warnings on your code. You should always activate at least these two during development that help detecting bugs. Take the habits to correct warnings in your code. Warnings, when not indicating bugs, indicates at least usually bad practices. -
-g: Insert debug information in the compiled code. Allows you to use debuggers and profilers on your program. You should always activate this option during your development. -
-O0,-O1,-O2,-O3: Level of code optimization. O0 doesn’t perform any optimization, while O3 perform all possible optimization. O3 may leads to larger code (due to inlining) and slower compilation. Common optimization level is-O2. -
-Werror: Transform any warning into error. This argument, once activated, may help you to clean your code from all warning before delivering it. -
-o {NAME}: Set output file to be{NAME}. -
-I {PATH}: Add{PATH}to the directory where compiler will look for header files. -
-l{LIB}: Link with external library{LIB}.
Compiling individual file
In the case where the source code is fully contained in an individual file, compiling and link can be performed in only one step.
$ g++ main.cpp -o pgm -g -Wall -Wextra
Compiling multiple files
Separate compilation
When the source code becomes large, it is recommended to split part of the program within multiple files.
Let us consider the case where a function F is defined in a file f1.cpp
#include <iostream>
void F()
{
std::cout << "function F" << std::endl;
}
Let us suppose that this function is called in the file main.cpp. Therefore, the executable must take into account both object files generated from f1.cpp and from main.cpp.
This can be done in three steps, called separate compilation.
-
Compile main.cpp into an object file main.o
-
Compile f1.cpp into an object file f1.o
-
Link main.o and f1.o into an executable file
$ g++ -c main.cpp -g -Wall -Wextra
$ g++ -c f1.cpp -g -Wall -Wextra
$ g++ main.o f1.o -o pgm
-
Note the
-cargument of the first two lines stoping the compilation at the object file. -
The first two lines (compilation of main.cpp and f1.cpp) can be performed in any order (in practice, often run in parallel by tools chain). Link must, however, be called only when main.o and f1.o exist.
-
Warning arguments can be only called during the actual compilation, they are not used for link.
-
If f1.cpp is modified, f1.cpp must be recompiled, and linker must be called, but there is no need to recompile main.cpp.
Header files
Example case
Let us consider the file main.cpp calling the function F defined in f1.cpp. Remember that before using a function, its signature has to be defined. Therefore main.cpp must have access to the signature of F before being called.
A common way of providing such signature is to use header files. Header files are separated files containing, notably, signature of functions that can be called from other files.
In our case, header file f1.hpp is the following
void F();
This file can then be used by main.cpp using the instruction #include "f1.hpp".
More precisely, #include {FILENAME} is a pre-processor instruction that will copy and past the content of the designated file.
The final code valid corresponding to main.cpp is the following
#include "f1.hpp"
int main()
{
F();
return 0;
}
-
Note that header files don’t have to be compiled: they are directly included (their content is copied) in .cpp files.
-
#include "{FILENAME}"looks"{FILENAME}"at local directoy in priority. -
#include <{FILENAME}>looks"{FILENAME}"at system directory in priority. -
Compiler option
-I {PATHNAME}adds directory where the compiler is looking for included header files.
Include guards
In the case of program with multiple files, function signature must be defined only once per compilation unit (can be seen as a file and all its inclusions). It is highly possible a given file is included many times when dealing with multiple headers.
In this case, we use the so called include guards ensuring that if a file has already been included in the compilation unit, it is not included a second time.
In practice, we can start each header file by the statement #pragma once recognized by the compiler.
ex.
#pragma once
void F();
General principles
Here is a reminder on general principles when dealing with multiple files.
-
Functions must be implemented in
.cppfiles. -
Functions that can be called from external file must have their signature defined in header files
.hpp. -
Start all header files by the instruction
#pragma onceto avoid multiple inclusions. -
Calling a function defined in an external file must be preceded in the beginning of the file by the preprocessor instruction
#include "{FILENAME}", where{FILENAME}is the header file containing the signature of the function.