Lecture 1: Introduction, Compilation Review
1 Course Syllabus
Be sure to read the syllabus.
2 Academic Integrity
Be sure to review the academic integrity page in Office of Student Conduct's website.
3 Makefiles (a simple example)
Here is a simple C++ program:We did not use using namespace
std
because using namespaces may be harmful (especially in header
files). They may create conflicts, for example if we have both
std::vector
and foo::vector
then the two vector definitions will
clash when we import both namespaces. In general, avoiding including
namespaces unless you're absolutely sure is a good idea.
// main.cpp
#include <iostream>
int main() {
std::cout << "Hello CS 32!" << std::endl;
return 0;
}
3.1 Can compile this with g++ or clang++:
$ clang++ main.cpp
$ ls
a.out main.cpp
$ ./a.out
Hello CS 32!
Note that the executable is called a.out
. This is the default name of the executable.
3.2 Using make
command
$ make main
c++ main.cpp -o main
- This default behavior of
make
tries to compile the.cpp
with that name and output the executable to that name. - Works well for very simple programs that exist in one file.
3.3 Change the output of the executable with the -o
flag
$ clang++ -o main main.cpp
$ ls
functions.cpp functions.h main main.cpp
$ ./main
Hello CS 32!
Changes the executable name from a.out
to main
.
4 C++ Build Process
- Preprocessor: Text-based program that runs before the compilation
step. Looks for statements such as
#include
and modifies the source which is the input for compilation. - Compiler: A program that translates source code into "object code",
which is a lower-level representation optimized for executing
instructions on the specific platform. Lower level representations
are stored in an object file. The object file ends with
.o
on most compilers on Linux. The object file contains which symbols (variables and functions) an object provides, and which symbols (variables and functions) it needs. - Linker: Resolves dependencies and maps appropriate functions located in various object files. The output of the linker is an executable file for the specific platform.
5 Compiling multiple files example
// functions.h
// declaration of doubleInt
int doubleInt(int x);
// ------------------
// functions.cpp
// definition of doubleInt
int doubleInt(int x) {
return 2 * x;
}
// ------------------
// main.cpp
#include <iostream>
#include "functions.h"
int main() {
// uses doubleInt
std::cout << doubleInt(12) << std::endl;
return 0;
}
We can't just compile main.cpp
anymore, it uses a function defined elsewhere:
$ clang++ main.cpp
Undefined symbols for architecture x86_64:
"doubleInt(int)", referenced from:
_main in main-7c4a04.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
- We see a "linker" error.
- The linker doesn't know where to find the
doubleInt()
function definition. - We need to compile
functions.cpp
somain.cpp
knows where to find thedoubleInt()
function.
$ clang++ -o main main.cpp functions.cpp
$ ls
functions.cpp functions.h main main.cpp
$ ./main
24
- Makefiles are useful since writing commands with many files is tedious and error-prone.
- Makefiles are used to define compilation rules and dependencies so
we can simply type
make [some_target]
and not have to type everything out all the time. - Makefiles can also run the compiler for only necessary tasks, we will talk about this shortly.
6 General format of a Makefile
# this is a comment
[target]: [dependencies]
[commands]
In the structure above, each command must follow a TAB character (not
a series of spaces, make
explicitly requires tabs).
- Example:
main: functions.cpp main.cpp
g++ -o main main.cpp functions.cpp
$ make main
g++ -o main main.cpp functions.cpp
$ make main
make: `main' is up to date.
- In this case,
make
uses timestamps to determine if something needs to be recompiled. - If a recent change occurred, then it will recompile. If nothing
changes, then nothing is done since everything is
up to date
.
6.1 Note:
- The object (.o) files are not explicitly generated.
- You will need to use the
-c
(compile only) flag. - Having make use .o files as a dependency is useful for maintenance reasons.
- Only recompiles source files that have changed.
# Rules to build the object files
functions.o: functions.h functions.cpp
g++ -c -o functions.o functions.cpp
main.o: functions.h main.cpp
g++ -c -o main.o main.cpp
# This rule builds the main program from the object files
main: functions.o main.o
g++ -o main main.o functions.o
$ make main
c++ -c -o functions.o functions.cpp
c++ -c -o main.o main.cpp
g++ -o main main.o functions.o
$ ls
Makefile functions.h main main.o
functions.cpp functions.o main.cpp
6.2 Variables in Makefiles
# Makefile
#CXX=clang++
CXX=g++
DEPENDENCIES=functions.o main.o
functions.o: functions.h functions.cpp
${CXX} -c -o functions.o functions.cpp
main.o: functions.h main.cpp
${CXX} -c -o main.o main.cpp
main: ${DEPENDENCIES}
${CXX} -o main ${DEPENDENCIES}
6.3 make clean
- Useful when trying to remove the executable and all .o files so everything can be recompiled from scratch.
# Makefile
main: functions.o main.o
g++ -o main main.o functions.o
# this means that clean is not a real file, but a "phony" file to create a rule
.PHONY: clean
clean:
rm -f *.o main
- If we want to "start fresh" and recompile everything, it's nice to have a "clean" target to remove all object files and the executable
7 Compiling a specific version of C++
- We can specify the specific C++ version to use when compiling our programs with a special std flag.
main: ${DEPENDENCIES}
${CXX} -std=c++17 -o main ${DEPENDENCIES}
clean:
rm -f *.o main
For this class we will be using C++17, so you should have
-std=c++17
as a compiler argument. This works both for GCC and
Clang.
8 Using $@
and $^
- Obtaining the target and dependencies is a common pattern when writing Makefile rules.
- We can use
$@
and$^
to obtain the target and dependencies respectively.$@
- target$^
- dependencies
- Example:
main: ${DEPENDENCIES}
${CXX} -std=c++17 -o $@ $^
# Expands out to
# main: functions.o main.o
# g++ -std=c++17 -o main functions.o main.o
9 Using variables for compilation flags
Sometimes, we give many flags to the compiler, we can also put them
in a variable so that we don't need to repeat them and edit them
quickly. Usually, we call this variable CXXFLAGS
. For example, we
can pass -std=c++17
this way, so the following Makefile acts as
the one we built so far:
# use a variable to maintain the list of headers most modules depend on
COMMON_HEADERS=functions.h
# use a variable to switch the compiler
CXX=g++
# use a variable to maintain compiler flags
CXXFLAGS=-std=c++17
functions.o: functions.cpp $(COMMON_HEADERS)
$(CXX) $(CXXFLAGS) -c functions.cpp -o functions.o
main.o: main.cpp $(COMMON_HEADERS)
$(CXX) $(CXXFLAGS) -c main.cpp -o main.o
main: main.o functions.o
$(CXX) $^ -o $@ # same as g++ main.exe -o main.o functions.o
.PHONY: clean
clean:
rm -f *.o *.exe main
10 How does Make know what to build?
Make builds a graph of dependencies, and tries to re-build dependencies if they have changed. For our Makefile, Make builds the following graph:
Then, if we change functions.cpp
, like the following:
// New functions.cpp
int doubleInt(int x) {
return x + x;
}
Then, when we run make
, it will re-build only the dependencies
that use functions.cpp
:
% make main
g++ -c -o functions.o functions.cpp
g++ -o main functions.o main.o
We did not rebuild main.o
because its dependencies have not
changed. For a project like this, it may seem small but build
systems and incremental builds reduce time to re-build from hours to
minutes when you are working on large real-world projects.
Footnotes:
We did not use using namespace
std
because using namespaces may be harmful (especially in header
files). They may create conflicts, for example if we have both
std::vector
and foo::vector
then the two vector definitions will
clash when we import both namespaces. In general, avoiding including
namespaces unless you're absolutely sure is a good idea.