Getting Started with GCC

C99/C++ Compilation with the GNU Compiler Collection

This document describes the first steps in creating C99/C++ programs, using the GNU C++ compiler. Apart from directory and path specifications, these steps are applicable to MSYS2, Cygwin, Mingw-W64, Linux, Unix, Mac OS X — basically anywhere GCC is avail­able. We also provide some background regarding the development process and avail­able tools.
PREREQUISITES — You should already…
  • have some non-trivial programming experience with a statically typed language;
  • have an installation of a relatively recent GCC (at least C99/C++11 capable);
  • be familiar with command line basics in a POSIX shell, Command Prompt, or PowerShell.

Introduction to GCC

GCC (GNU Compiler Collection) is available on many operating systems. It is used for many pur­pos­es, including cross-compilation to other systems, which are often embedded. For Windows, GCC can be obtained via Cygwin, MSYS2 or a distribution of MinGW-w64 (get at Source­Forge or nuwen).

GCC provides several compilers, not all of which may be installed. At minimum, you need the C & C++ compilers and binutils, which is the name of a set of programs working with objects, or ex­e­cu­tab­les, and includes the GNU assembler. Having some, or all of the GNU coreutils available, could be con­ve­nient.

You must ensure that the GCC program executables are on your PATH, so they can be ex­e­cut­ed from any directory. You may use our custom packaging of MinGW-w64 & u­t­i­l­i­ties. Al­ter­na­tive­ly, you can use MSYS2 to provide you with the various POSIX shell & utilities, and its pacman to in­stall GCC and numerous other libraries, including boost and Qt. Pacman also allows you to keep everything cur­rent. Instead of using the Command Prompt Console, use ConEmu.

We will discuss some, but not all, of the command options you can use with GCC. For more in­for­ma­tion, see GNU's extensive documentation on Invoking GCC.

All example commands here assume that you are using GCC in either a POSIX shell, or the Win­dows® Com­mand Prompt.

The Development Process

Process, indeed. Although tools help simplify the process, it must be very clear in your mind that compiling C/C++ programs, require a series of tasks, which we explain below.


Regardless of how you control the process, or which tools you use, the pro­cess to suc­cess­ful­ly cre­ate an executable (including DLLs or shared libraries) involves the fol­low­ing steps or phases. The first two steps must be repeated for every .c or .cpp file.

C/C++ Simple Single File Compilation Process
C/C++ Simple Single File Compilation Process
  1. Preprocessing — This part of the compilation process only manipulates text, which pro­gram­mers control via special (non-C/C++) preprocessor directives. The pre­pro­ces­sor (cpp in GCC) out­puts a temporary text file which incorporates the ef­fects of the di­rec­tiv­es. The process will ter­mi­nate if any invalid preprocessor directives are en­count­er­ed.

  2. Code generation — This phase of the process only uses the temporary file created by the pre­pro­ces­sor, and from this file outputs machine code and information to a single as­sem­bler (.s) file. In GCC, the as­sem­bler (as) physically creates the object file (.o) from the as­sem­bler file. The code generation phase also consists of several well-de­fin­ed and text par­sing phases, but we are gen­e­ral­ly not concerned with those.

  3. Linking — After successful object file(s) creation, the link­er (ld in GCC) is called. By using eit­her com­mand line arguments, or embedded object file information, the linker is instructed to link the object file(s) with object files from the standard C and C++ libraries, and with the start­up code for your operating system. If all ex­ter­nal ref­e­ren­ces have been resolved, an ex­e­cu­tab­le is cre­at­ed, whose name is dependent on the argument to the GCC -o command line option. The preprocessor and compiler (and optionally, an assembler) phases execute for every source file in a multi-file program. Only at the linking phases does it all come together. The C/C++ compilers never have any idea of the “big picture”.

C/C++ Multi-File Compilation Process
C/C++ Multi-File Compilation Process

In embedded systems, the result from the linker may just be a binary image of the prog­ram, as it would appear in flash memory or ROM. The executable does not have to be des­tin­ed for the same ar­chi­tect­ure as the compilation suite used to produce it — then we call it cross-compilation (compile on one architecture, for another architecture).

C Preprocessor

Compared to many other languages, the C/C++ languages are deficient in some ways. To alleviate this to some degree, the C/C++ standards specify a preprocessor. In the GCC, this is called cpp (for C PreProcessor). C++ will create the __cplusplus preprocessor macro, but otherwise there is no dif­fer­ence in preprocessor behaviour when called on C++ programs.

The C preprocessor only manipulates text. Its input is text, and its output is text. All it un­der­stands about C/C++ is the concept of a token (words, punctuation, literals, etc.). It has no idea of C/C++ syn­tax, semantics or scope (blocks) at all. It does not know what a “library” is, or the mean­ing of in­clude files. We emphasise this, because some people expect a higher degree of in­tel­li­gen­ce from the pre­pro­ces­sor than it actually possesses.

We can fake the lack of “symbolic constants”, “global variables”, “global functions” and “fast func­tions” with the preprocessor. Both C & C++ have inline functions, which some­what re­duce the need for the preprocessor in this regard, but do not completely replace it. An ad­di­tion­al, and very power­ful, use for the preprocessor, is for “conditional compilation”. This means that we can use it to po­ten­ti­al­ly compile different parts of the same code base, by simp­ly changing command line arguments during compilation.

C++ inline functions, together with template functions, can reduce reliance on the pre­pro­ces­sor. C++11 also adds “proper” symbolic constants (compile-time values), with the ad­di­tion of the new constexpr keyword, which eliminates the need for #define to represent immediate values. Nev­er­the­less, the preprocessor plays a vital role in creating portable pro­grams.

Object Code Generator

The output of the preprocessor is normally fed to the “actual” C/C++ compiler — the part that un­der­stan­ds C/C++ syntax, and that creates object files (indirectly, since there may be an assembler in­volved). According to the standard, there may only be one object file, given one source file, or com­pi­la­tion unit. If preprocessor directives are encountered at this point, the compilation will fail, since they are not part of the lan­guage syntax.

If an assembler is involved, as in the case of GCC, the C/C++ compiler produces a temporary as­sem­bler file (*.s in GCC), which is in turn fed to the assembler. This makes the assembler the ac­tu­al pro­gram that creates the object file, but the C/C++ compiler still controls what will even­tu­al­ly end up in the object file. GNU's assembler is called as, but is seldom called directly.

Whether or not there is an assembler, the object file will contain the following: data with global life­time, machine code, public symbols for C/C++ elements with external linkage, and external refer­en­ces. The external references must link up with public sym­bols in the same, or other, ob­ject files that constitute the complete program.


A linker can be used to create an executable from any number of object files, produced by any com­pi­ler, or mixture of compilers, provided the correct calling and naming con­ven­tions are used. You cannot, for ex­amp­le, expect an object file created by the Fortran com­pil­er, to be prac­ti­cal­ly linkable with an object file created by a C compiler, since the calling and naming conventions may not match. It is theoretically possible, but there are practical issues to be considered.

Linkers also create shared libraries (POSIX, *.so), or DLLs (Dynamic Linked Libraries — Windows®, *.dll). GNU's linker is called ld, but it is rarely called directly. Linkers can set required free mem­o­ry and stack space for programs.

The linker must link all external references in object files and libraries, to public symbols in the same, or other, object files and libraries. This is called: “re­solv­ing ex­ter­nal ref­e­ren­ces”. If public symbols are not found, for example when a library has been omit­ted, the link­er will report a mes­sage con­tain­ing the phrase “…unresolved external…”. Duplicate public sym­bols are not al­low­ed, but the C/C++ compiler can mark some duplicate symbols in such a way that the linker will only use one, instead of re­port­ing a “…duplicate symbol…” error. This is normally used for inline and/or template func­tions.

Development Tools

Here we discuss the development tools used in C/C++ compilation, from a GCC perspective.

Compilation Drivers

Using the preprocessor, code-generator and linker together, and in the correct sequence, is cru­ci­al for the creation of C/C++ programs. Fortunately, some tools make this easier; in par­ti­cu­lar, two tools: gcc (for C), and g++ (for C++), are “compilation drivers”, if you like. With options (switches), these tools can be controlled to call, or not call, the linker; to leave or de­lete tem­po­ra­ry or object files, etc. Some of these options are described below.


Several options can be passed to the preprocessor. The most common are:


The most common switches to control warnings are:

There are options to control code optimisation:


The compiler drivers will generally pass the correct standard libraries and their locations to the lin­ker.

All switches have defaults. Given a simple program, like your typical C99 or C++ “Hello, world!” program in a main.c or main.cpp file, running gcc main.c, or g++ main.cpp will pro­duce an executable called a.exe on Win­dows, or a.out on POSIX systems. It will au­to­ma­ti­cal­ly con­trol the various phases of com­pi­la­tion, and delete all temporary files ge­ne­rat­ed. As a minimum, you should add a -oexecutable› switch, to name the executable file, instead of using the default name.

Build Utilities

For non-trivial programs, you want to use a project build utility. IDEs either use the same tools you can use on the command line, or proprietary build tools — but they all provide a way to au­to­mate the build process efficiently. We focus on command line tools here.

GNU Make

Regardless of which of the many build utilities you may use, you should know at least some fea­tur­es of GNU Make — it is almost considered mandatory. The simplicity of C/C++ com­pil­ers, which allow you to compile only one file at a time, is useful, as it quickly becomes te­di­ous to recompile all files in a big project every time one file changes.

The strategy is to control the compilation process, so that it leaves the object files, instead of de­le­ting them. Furthermore, if we compile one source file at a time, it means we only have to re­com­pile any changed files. Then we follow with linking all the objects files into an executable. If, in addition, we can formalise the process, this can be automated.

This is where Makefile comes in: it is the default name make will look for when invoked. It can contain dependencies and recipes using GNU Make's syntax. If correctly specified, this means that make will only compile modified files when invoked, and run the linking recipe, if necessary.

Other Build Tools

A useful tool is CMake, which creates make files for various Make utilities, in­cl­u­d­ing Micro­soft's MSBuild. It supports numerous environments, and should definitely be part of your toolset. An­ot­her emerging tool is Ninja (Android, for example, is built with it). It is very fast and uses a simpler syntax than Make, although it is less powerful for complicated builds. Also, CMake can create Ninja project files.

Supplementary Tools

In practice, compilers, linkers and program maintenance utilities are not enough for real-world ap­p­li­ca­tion development. Luckily, many free top-shelf tools are available, in addition to the com­mer­ci­al tools.

Code Formatting

Consistent formatting of code across a project sounds like a trivial topic, but it builds con­fi­den­ce in the code. It also helps the brain's pattern matching mechanisms — it is easier to re­cog­nise lan­gu­age con­structs if they look familiar, and you do not have to parse the text for every different va­ri­a­tion in for­mat­ting.

This is often called “pretty printing”, or “code beautification”, suggesting the aesthetic ap­peal of con­sis­tent­ly for­mat­ted code. The key is consistency. The problem is that, even when con­sis­tent­ly for­mat­ted, free-format languages like C/C++ allow everybody to have a dif­fer­ent ideal regarding the act­ual for­matting (whitespace, curly brace position, etc.).

There are many available tools that can be used for formatting. One of these is Artistic Style, which has been around for many years, and supports many languages and styles. It is also very configurable, so you should be able to find a set of options that satisfy your, or your team's, re­qui­re­ments.

Static Code Analysis

Static analysis tools can help to find questionable code or program errors beyond syntax errors, which is outside the scope of C/C++ compilers. One of the oldest is the Unix Lint utility, but the only freeware version for Windows is Splint, which is heavily out of date.

The best freeware alternative to Lint is the portable Cppcheck. This has grown, from a single exe­cu­t­able to a set of files that must be installed (although a portable-app from a third party, exists for Win­dows). It can also be integrated in some IDEs. A list of checks is available. Cppcheck is ac­tive and con­stant­ly updated, and highly recommended.

Code Documentation

Although it is not many programmers' favourite activity, correct and cur­rent code doc­u­men­ta­tion is crucial during the lifetime of a project. This is so important that many pro­gram­ming languages (e.g., Java, Python, C#, Go, Rust) have formalised the concept of stan­dard­is­ed comments and do­cu­men­ta­tion ge­ne­ra­tion tools.

For C/C++, we recommend the venerable Doxygen. Doxygen can utilise tools like dot from the Graph­Viz suite to automatically generate call graphs, include graphs, and inheritance diagrams. It can gen­e­rate CHM files, if the HTML Help Workshop tool is installed (it only requires the hhc.exe command line program, but there is no way to obtain it other than to install the whole Workshop program). Doxygen can be integrated in the build process, and some IDEs have plug­ins that sup­port Doxygen.

Running Doxygen simply means setting up a Doxyfile (default name) configuration file, and run­ning the single command line program: doxygen, in the same directory. It can even gen­e­rate a default Doxyfile for you, with comments for each and every setting. The hardest part is setting up the nu­mer­ous options according to your preferences — after that, you simp­ly copy your Doxyfile to new projects, and change project-specific settings in the Doxyfile. For those who prefer it, Doxygen does have a GUI.

Doxygen only processes special comments. It has a rich keyword set to formally document files, func­tions, parameters, return values, classes etc. The comment text can use Mark­down markup for for­mat­ting, bullet lists, links, images, headings, etc. A pro­ject-spe­ci­fic ex­ter­nal “main page” can be con­fig­ur­ed to serve as the landing page for your HTML for­mat­ted do­cu­men­ta­tion. The CSS and other fea­t­ur­es can be customised. There is little negative to say about Doxygen.

Library API Lookup

On MacOS, almost every serious developer uses, or at least knows of, Dash. Dash has a freeware component, as well as the option to purchase a licence for extra features. It supplies several official “docsets” which can be down­load­ed for offline use, and several third party docsets are also avail­able.

For those who are not using MacOS, a recent freeware tool called Zeal is available on Linux and Windows. It uses the same docset compilations as Dash, which for C/C++ comes from — as good as, or arguably better than, You can also down­load a portable .zip version — no installation required.

We strongly recommend Zeal. Firstly, it is much faster than online lookup. Secondly, the doc­u­men­ta­tion is regularly updated. Thirdly, it uses formal and correct terminology, consistent with the C/C++ standards, and not colloquialisms and slang as is common on forums. It is worth the effort to learn how to read formal C/C++ doc­u­men­ta­tion, and at the same time learn to be more consistent with your use of C/C++ terminology.


Source-level debugging can be performed with GNU Debugger (gdb), as long as the source files of the executable have been compiled with the -g switch. Depending on your version of gdb, and the op­tions you use, you might need to have Python on your path.

GDB can do everything you expect from a debugger, except you have to enter com­mands, instead of click­ing on buttons. Once you run gdb, it displays a prompt, waiting for your de­bug­ging com­mands, just like a shell. It executes those commands when you press ‹Enter›. You can set and list break­points, watch variables, dump memory, single-step into func­tions, single-step over func­tion calls, exit the current function, and numerous other possibilities.

Generalised Steps for Simple Projects

In the learning process, we do not require many files or libraries. As a result, the process to create a new project for testing or exercises is quite simple. The steps outlined here assume you have your command line shell open, and have set the current working directory to the location where all pro­jects are stored.

The top-level ‘work’ directory, can be any directory where you practice your development. On our course, it could be: C:\Course\Work, or C:\Course\rxmingw64\work, or any directory of your choice. The example command lines here assume that the directory already exist.

If using the Command Prompt on Windows:

$> Set Work Directory (Cmd Prompt)
cd /d C:\Course\rxmingw64\work

If using the Cygwin or MSYS2 environments with Bash:

$> Set Work Directory (Cygwin/MSYS)
cd /c/Course/rxmingw64/work

The prompt part of the command line, unless customised, will show the current directory. It can be any directory, but we generally avoid spaces in the whole path.

In a POSIX shell, like bash under Unix, MacOS, Linux or MSYS2, you will generally work in your home directory ($HOME). In this case, assuming you have a work directory inside your home di­rec­to­ry, simp­ly execute (the first command will fail if the dirctory already exists, so it is safe to run):

$> Set Work Directory (POSIX)
mkdir ~/work
cd ~/work

The ~ (tilde) is a shortcut for $HOME in many POSIX shells.

Choose a Project Name

The first step is to decide on a project name. This is also often requested by IDEs. Ideally, the pro­ject name should not contain spaces or other illegal file system characters. For your pur­pos­es, we'll re­pre­sent the chosen name with: ‹project› — replace it everywhere below with your pro­ject name.

C programmers, in particular, generally avoid capital letters in project, file and directory names. C++ programmers are more likely to use capital letters for file and directory names. Generally, this is be­cause, organisationally, each source file will contain a single major class, whose name will con­ven­tion­ly contain capital letters. Files containing these classes are normally named after the class.

Create a Project Directory

It is common practice to create a directory for the project using the project name. Once the di­rec­to­ry is created, we should make it the current working directory. The following com­mands will ac­com­pl­ish that in any shell:

$> Create ‹Project› Directory
mkdir ‹project›
cd ‹project›

This is simply organisational convenience. The C/C++ compiler and other tools do not care in what di­rec­to­ry you put your source files. Even for simple programs, this is just a good convention, not a re­qui­re­ment imposed by any tool.

Create Source Files

Any editor can be used, but generally we prefer a programmer's editor like Vim, gVim, or Note­pad++ (Win­dows only). Linux and Mac OS X also have many free and capable programmer's ed­i­tors. We usually rename Notepad++.exe to npp.exe in our packaging of MinGW-w64 + utilities on Windows, which is what we demonstrate here:

$> Create Source File
npp ‹project›.cpp

You can replace npp with the appropriate editor name, as long as it is on your PATH. For ex­ample, if you're using Vim/gVim, you can use vim, or gvim. Otherwise, just open your editor, create a new file with the proper extension, and save it in the ‹project› directory.

We suggest you have a look at Visual Studio Code, which is not well-named, but is a very capable and modern programmer's editor… though has nothing to do with Visual Studio itself. It runs on MacOS, Windows and Linux, and on Windows (at least), you can set it up as a relocatable (portable) program, if you do not want to install it. If its executable (Code.exe) is on your PATH, you can invoke it simply with: code.

NOTEMain File Name

It is acceptable to name the source file that contains main() as main.cpp/.c, in­stead of ‹project›.cpp/.c. This is, in fact, a very common convention, especially when we write bigger programs containing more than one source file.

‹project›.cpp — Example Minimal C++ Program
‹project›.c — Example Mininal C Program

After saving the source file, it can be compiled. It is not necessary to close the editor. You can have the editor open in one shell, if you're using Vim, and compile in another instance of the shell.

NoteOptional Return Statement in Main

In either C99 or C++11, a return statement in main() is not required — but this is not a generalisation; it only applies to the main() function, and will cause the compiler to automatically add: return 0; at the end.

Compile the Code

The following command will drive several compilation phases. It will delete all temporary files and object files, regardless of whether an executable was successfully created or not.

$> C++ Debug Compile
g++ -Wall -Wextra -pedantic -std=c++11 -O0 -g -o ‹project› ‹project›.cpp
$> C99 Debug Compile
gcc -Wall -Wextra -pedantic -std=c99 -O0 -g -o ‹project› ‹project›.c

The g++/gcc programs drive the whole process as described above, just like an IDE drives the exact same process, when you click on the ‟Build” or equivalent button for your IDE. Also, the above com­mand line adds support for the GNU debugger (gdb), with the -g switch, which means this com­mand line is for a debug build. For a release build, you can use:

$> C++ Release Compile
g++ -Wall -Wextra -pedantic -std=c++11 -O2 -DNDEBUG -o ‹project› ‹project›.cpp
$> C99 Release Compile
gcc -Wall -Wextra -pedantic -std=c99 -O2 -DNDEBUG -o ‹project› ‹project›.c

To further reduce the size of a release build executable, you can run strip on it, which will remove all symbols from the executable:

$> Strip Symbols (Optional)
# On Windows, even Cygwin:
strip -s ‹project›.exe
# On Linux:
strip -s ‹project›
# On MacOS:
strip -xX ‹project›

In order to debug a debug build executable with the GNU Debugger, you can run the following command. Just omit the .exe where not applicable.

$> Debug with GDB
gdb ‹project›.exe

The actual debugging process is described in another document.

Execute the Programs

If you are used to a GUI operating system environment, you might be inclined to double-click on eve­ry­thing to get some action from the operating system. Yes: double-clicking the resulting exe­cu­ta­ble from the above process might execute the program. But since it is a com­mand-line pro­gram, a con­sole or terminal will be created for it to run in, which will be closed when the pro­gram ter­mi­nat­es. You will not see any output, unless the program pauses for input.

The best way to execute a command-line program, is to run it from an existing shell. Windows Com­mand Prompt automatically looks in the current directory, so to execute the above program, you simply have to type:

$> Execute Program (Cmd Prompt)

In POSIX shells, and PowerShell, all of which do not automatically look in the cur­rent di­rec­to­ry, you must execute the pro­gram as follows (where the period character means “current directory”).

$> Execute Program (POSIX & PowerShell)
# PowerShell:

For any operating system, if the directory containing the executable is listed in the PATH en­vi­ron­ment variable, simply stating the name will be sufficient. Alternatively, the full path name must precede the executable name.

Separate Compilation Phases

Although it may not be useful for a single file program, it is recommended that you break down the in­di­vi­dual compilation phases for projects with multiple source files, for better compilation ef­fi­cien­cy (only compile what has changed).

The preprocessing phase is handled in GCC with the cpp (C pre-processor) program. It is easier to call it indirectly in the normal way with the g++ driver, but we can add the -save-temps com­mand-line option to instruct it not to delete, amongst other files, the temporary file pro­duc­ed by the pre­pro­ces­sor. Not all switches are repeated here:

$> C++ Save All Temporaries
g++ -Wall -Wextra -std=c++11 -save-temps -o ‹project› ‹project›.cpp
$> C99 Save All Temporaries
gcc -Wall -Wextra -std=c99 -save-temps -o ‹project› ‹project›.c

You can add -masm=intel and/or -fverbose-asm to control the assembly output. The temporary files will be:

Preprocess and compile one or more *.cpp files, save the objects, and do not call the linker:

$> C++ Prevent Linking
g++ -Wall -Wextra -std=c++11 -c ‹project›.cpp
$> C99 Prevent Linking
gcc -Wall -Wextra -std=c99 -c ‹project›.c

For any number of source files, only produce the resulting assembler files as *.s files. Adding the as­sem­bler switches: -fverbose-asm and -masm=intel, are optional.

$> C++ Produce Only Assembler Files
g++ -Wall -Wextra -std=c++11 -S -fverbose-asm -masm=intel ‹project›.cpp
$> C99 Produce Only Assembler Files
gcc -Wall -Wextra -std=c99 -S -fverbose-asm -masm=intel ‹project›.c

Only call the preprocessor on one file. If the -o switch and its argument are not given, the pre­pro­ces­sed output will be written to standard output. Calling the preprocessor (cpp) directly, depends on wheth­er it can find the include files. This way, it is guaranteed to work.

$> C++ Produce Only Preprocessed Files
g++ -Wall -Wextra -std=c++11 -E -o ‹project›.ii ‹project›.cpp
$> C99 Produce Only Preprocessed Files
gcc -Wall -Wextra -std=c99 -E -o ‹project›.ii ‹project›.c

Only call the linker, as long as only *.o files are passed as arguments.

$> C99/C++ Perform Linking Only
gcc/g++ -o ‹project› ‹project›.o

For most of the above examples, you can add the -pedantic switch for more diagnostics output.


The following Makefile is set up for the C++ example program we presented above, but you should be able to adapt it for the C99 minimal program. It is generalised enough, however, that you can use it as a template for new projects, where you have this Makefile in the same directory as any number of *.cpp (or *.c) files that make up the program. Just set the PROG value appropriately (remember to add .exe after the executable name, if you use this on Win­dows).

MakefileMinimal Program Makefile
# GNU Make Makefile for Minimal Program Example
# Call make with: `make all`           for a “debug compile”, or
# call make with: `make all RELEASE=1` for a “release compile”.
# Note: If using on Windows with GNU's `make`, or `mingw32-make`, add
#       `.exe` to the value for the `PROG` macro below.
# Note: For C programs, change occurrences of `$(CXX)` to `$(CC)`, and
#      `$(CXXFLAGS)` to `$(CFLAGS)`. Every `.cpp` should become `.c`.
PROG = minimal
SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)

# compiler and linker switches
CC = gcc
CXX = g++
CFLAGS = -Wall -Wextra -pedantic -std=c99
CXXFLAGS = -Wall -Wextra -pedantic -std=c++11

# set release or debug build flags
   CFLAGS += -O0 -g
   CXXFLAGS += -O0 -g

# generic rule to create an object file from a `.cpp` file. for C
# programs, change `%.cpp`, to `%.c`.
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c -o $@ $<

# use the compiler driver to link objects into an executable. for C
# change `$(CXX)` to: `$(CC)`, and `$(CXXFLAGS)` to `$(CFLAGS)`.
$(PROG): $(OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $(OBJS) $(LDFLAGS)
	strip $@

# generic (not real file) targets.
.PHONY: all clean

# also clean preprocessed and assembler files, just in case. checks
# if running on Windows (only OS that has `ALLUSERSPROFILE` as standard).
	-@cmd /c del $(PROG) $(OBJS) *.ii *.s *.bc 2>NUL:
	-@rm $(PROG) $(OBJS) *.ii *.s *.bc 2>/dev/null || true

all : $(PROG)

If you do not use Cygwin or MSYS2, you may not have make available, but you should have at least mingw32-make. You can use that, instead of make, on the command line. Before every recipe line, GNU Make expects a hard tab character; not a sequence of spaces. If you copy and paste the above Makefile, it must contain hard tab characters, and these should not be replaced by spaces in your editor.

The ⋯2>/dev/null || true part in the recipe will only work when running in a POSIX shell, and also Cygwin or MSYS2 on Windows. If you are using the Command Prompt instead, you will have to re­move that. In that case you might also not have rm available, so then you should replace the whole recipe with: cmd /c del… command:

clean recipe for Command Prompt users

This should not be necessary in the above Makefile example, since we (simplistically) test if we are running on Windows, by checking the existence of the ALLUSERSPROFILE environment variable. A more robust option, might be to check the OS environment variable:

operating system detection in a makefile example

From this point on, the rest of the Makefile code will test THEOS:

conditional directives based on operating system

On MacOS, uname -s will return Darwin, and on Linux: Linux, so you could use those values when you want to differentiate between the latter two operating systems.

In both examples, you can remove the *.ii, *.s and *.bc parts, if you have not been ex­pe­ri­men­ting with per­sis­ting temporary preprocessed and assembler files.

Key Terminology

It is important to become familiar with the key terminology relating to C/C++ and the com­pi­la­tion process. For example, we must not confuse the terms declaration and definition when dis­cus­sing var­iables and functions, as these have different effects regarding entries in the object file.

compilation unit

The formal term for the temporary file produced by the proprocessor. This is a single file, or com­pi­la­tion unit. The .c or .cpp file in your editor is not a compilation unit. It will, however, become part of the compilation unit, once the proprocessor's run has completed.


A syntax that only makes an identifier known to the compiler, with sufficient detail to allow code to be generated, with the missing parts to be supplied by the linker. De­cla­ra­tions with external linkage create external references in object files. They occupy no further space in an object file. All external references, in all object files, must be link­ed with a corresponding public symbol for the linking process to succeed.


A syntax, different from declaration syntax, that creates an identifier (variable or func­tion). In the process, it also makes the identifier known to the compiler. The names of identifiers with external linkage are written to the public symbol table of object files. The space for the id­en­ti­fi­er is al­lo­cat­ed in the object file, and constitutes either data space (variables), or machine code space (func­tions).


In C/C++, a variable can have local or global lifetime. Only variables with global lifetime are stored in object files, and consequently will form part of the executable file. Va­ri­ables with global lifetime are initialised at compile time.


An identifier can have external linkage as part of its storage class specification, e.g. extern (im­pli­cit­ly or explicitly), which means its name is entered as a public symbol in the object file. When it does not have external linkage, it is often denoted as having internal linkage.

separate compilation

This term refers to the behaviour of C/C++ compilers, where they can only compile one file (compilation unit) at a time. A consequence of this, is the need to re­peat de­cla­ra­tions and types, which is where header files come in. The advantage is that, as long as object files are avail­able for files in a given project, we only have to recompile changed source files, and then link the object files again to obtain a new executable.

static binding

Calls to functions (which involves calling and address), and the addresses of variables are de­ter­min­ed at compile time — either by the compiler, or the linker. This is what static binding refers to. Apart from the simplistic C++ RTTI support, C/C++ offers no dynamic bind­ing syn­tac­ti­cal features.

statically typed

C/C++ are statically typed languages, which means all types must be known at compile time. The concept of types is a compile-time abstraction, that determines storage size in memory or in ob­ject files; and the machine code instructions emitted to work with values. This absolves the pro­gram­mer from having to deal with it (a tedious and error-prone process in assembly language).

storage class

A collective term for the following three individual features: linkage (ex­ter­nal / in­ter­nal), life­time (local / global) and scope (file / block). Storage class does not include dy­na­mic mem­o­ry, or what some programmers refer to as “dynamic lifetime”: dynamic memory is not a syn­tax feature of the language.

2018-11-12: Added note about return statements in main(). [brx].
2018-07-23: Add compilation process images. Make more C99-friendly. [brx].
2018-05-14: Add -c switch to linker option list[brx].
2017-11-18: Update to new admonitions. [brx]
2017-09-21: Created. Edited. [brx;jjc]