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

  1. 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.
  2. 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.
  3. 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 so main.cpp knows where to find the doubleInt() 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:

make-dag.png

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:

1

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.

Author: Mehmet Emre

Created:

The material for this class is based on Prof. Richert Wang's material for CS 32