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, macOS — basically anywhere GCC is available. We also provide some background regarding the development process and available 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 purposes, 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 SourceForge or nuwen).

GCC provides compilers for several languages, but not all of which may be installed, or available. At minimum, you need the C & C++ compilers and binutils, which is the name of a set of programs working with objects, or executables, and includes the GNU assembler (as). Having some, or all of the GNU coreutils available, could be convenient, as well as the GNU debugger (gdb).

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

GCC on Linux

You Linux distribution's package manager will have all necessary development packages. The problem is finding the correct package name, since they are not necessarily the same across all distributions.

On Debian-based distributions like Ubuntu (and Ubuntu in WSL), the meta-package is called build-essential. This will install GCC compilers (C & C++), linker, binary utilies (‘binutils’), and other common development tools, including make.

You may also want to install gdb (GNU debugger), git, cmake, and ninja (works well with CMake (we include library documentation man pages):

shInstall GCC and Other Tools on Debian-Derived Linux Distributions
> sudo apt update
> sudo apt install build-essential cmake ninja gdb git manpages-dev -y

And that's that. You can test by running: ‘gcc --version’ and/or ‘g++ --version’ in your shell.

GCC on Windows

Several distributions/builds of MinGW-w64 are available. These do not require installation… simply extract, set the PATH environment variable, and run gcc or g++:

That is not the end of the possibilities though…

You must ensure that the GCC program executables are on your PATH, so they can be executed from any directory.

Instead of using the Command Prompt Console, use Windows Terminal (or download from GitHub). It comes standard with Windows 11, and can be configured to be the default host for all command-line programs (even on later builds of Windows 10).

All example commands here assume that you are using GCC in either a POSIX shell, or PowerShell (either the legacy ‘Windows PowerShell’, or the cross-platform modern ‘PowerShell 7’).

Minimal MinGW-w64 Installation

There are many ways to install a MinGW-w64 distribution. Here is one method with the fewest steps, and which does not require Administrator privileges on your PC. We will…

Furthermore, to test the installation and establish an environment and repeatable process for writing C programs (projects), we will…

We will be using the built-in Command Prompt shell for all commands, just in case you are unfamiliar with Powershell. You should use PowerShell (PowerShell 7 specifically), and we shall show equivalent commands.

Prerequisites

An up to date Windows 10/11 installation, and the ability to find and run applications from the Windows Start menu is required, particularly because later builds of Windows provide the cURL program. In addition, the ability to type command lines verbatim, would help.

Open Command Prompt

Click on the Windows Start Menu, and start typing: Command. You should then see ‘Command Prompt’ being selected for you:

Command Prompt in Start Menu

Click on Command Prompt. This will run cmd.exe. You now have a command line shell.

Download & Install WinLibs's MinGW-w64 Distribution

Type each of the following command lines, pressing the enter/return key after each line.

Command PromptDownload and Run Nuwen MinGW-w64 Installer
> cd %USERPROFILE%\Downloads
> set "URL=https://github.com/brechtsanders/winlibs_mingw/"
> set "URL=%URL%releases/download/14.2.0posix-18.1.8-12.0.0"
> set "URL=%URL%-ucrt-r1/winlibs-x86_64-posix-seh-gcc-14.2.0"
> set "URL=%URL%-mingw-w64ucrt-12.0.0-r1.zip"
> curl -LO %URL%

Here is the equivalent series of commands if you are using PowerShell (but using the URL for the 7-Zip version; change if necessary):

PowerShellDownload and Run Nuwen MinGW-w64 Installer
> cd $Env:USERPROFILE\Downloads
> $URL = "https://github.com/brechtsanders/winlibs_mingw"
> $URL += "/releases/download/14.2.0posix-18.1.8-12.0.0-"
> $URL += "ucrt-r1/winlibs-x86_64-posix-seh-gcc-14.2.0-"
> $URL += "mingw-w64ucrt-12.0.0-r1.7z"
> curl -LO $URL

Now you must extract the compressed archive file you downloaded into some directory, for example, called mingw64. If you happened to extract it with right-click in a file explorer, the directory will be mingw64 (name inside the archive). Be sure not to end up with something like mingw64\mingw64!

Choose any directory, but for this walk-through and simplicity, we suggest ‘C:\’. A subdirectory called mingw64 will be created in your chosen destination, e.g.: ‘C:\mingw64\’. Wait for the extraction to complete.

WARNINGSpaces in Directory Names

For any development purposes, and as a destination location for extracting Nuwen MinGW-W64, do not choose directories with spaces or unusual characters in the name. Only use alphabetic characters, and maybe underscores and decimal digits.

Optionally, you can now delete the archive file using the command line: del filename, or simply use Windows Explorer to delete it. Use ri filename in PowerShell.

You cannot continue with these steps until you have…

Please verify that the above steps were successful, that you understand what happened; and you know the location of the mingw directory where you extracted MinGW-w64.

Set PATH and Test MinGW-w64

Once the extraction is finished, we need to set the PATH, so that the gcc and make programs can be found without specifying a full path name. They are in C:\mingw64\bin (for example), assuming that you chose ‘C:\’ as the destination directory for extraction.

In the open Command Prompt window, enter the following commands. These will set C:\mingw64\bin; first in the PATH, and check if it was successful.

Command Prompt — Set PATH and Check for gcc.exe
> set PATH=C:\mingw64\bin;%PATH%
> where gcc

The equivalent commands for PowerShell:

PowerShell — Set PATH and Check for gcc.exe
> $Env:PATH = "C:\MinGW\bin;$Env:PATH"
> (Get-Command gcc).Source

The output of the last command should be (either one):

C:\mingw64\bin\gcc.exe

If not, you will have to check for typing mistakes, or omitting a step. In case gcc.exe can be found, you must either remember to set the PATH the same way every time you open a new Command Prompt. Or you can persist is in the Registry.

Persistent PATH Modification

To persistently set the PATH, you will have to edit the Windows Registry indirectly via a Control Panel dialog (applet). Select the Start Menu and type: edit environment. You should see the following, which you can click on:

Edit Environment Variables

Or, from the command-line (Command Prompt or PowerShell, run the following:

rundll32 sysdm.cpl,EditEnvironmentVariables

In the Environment Variables dialog that opens, select the ‘Path’ entry in the ‘User variables…’ table, so that it is highlighted. Then click on the Edit… button.

In the resulting Edit environment variable dialog, click on the New button. This will allow you to type: ‘C:\mingw64\binbelow the other paths that may be present.

NB! Do not edit Path in the System variables table!.

This should suffice, but to avoid potential issues, click the Move Up button repeatedly, until C:\mingw64\bin is at the top. Click OK, followed by OK on the first dialog, to complete the process. This does not affect currently open programs.

C:\mingw64\bin At Top of Path List

Now, every time you open a new Command Prompt, or even a new PowerShell window, the PATH will be set correctly, and the GCC executables can be run by name.

NOTEOlder Version of Windows & Control Panel

The editing of environment variables, in particular the PATH/Path environment variable, may not appear as shown in the image above. Instead, you will be given a text box containing the value of the PATH/Path environment variable. In this case, you must put ‘C:\mingw64\bin;in front of the existing value (note the semicolon). Then you can click OK to finish.

Set PATH Permanently From a Command Line

OPTIONAL/ADVANCED — One can modify the PATH environment variable persistently in the Windows Registry from the command line, in either Command Prompt with setx.exe, or in PowerShell via the Registry:: ‘drive’.

Command PromptChange PATH in Registry
> setx PATH "C:\mingw64\bin;%PATH%"

Alternatively, you can use PowerShell, which is a bit more verbose since we are not using shortcut aliases here:

PowerShellChange PATH in Registry
> $location = "Registry::HKCU\Environment"
> $curpath = (Get-ItemProperty -Path $location -Name "PATH").Path
> Set-ItemProperty -Path $location -Name "PATH" –Value "C:\mingw64\bin;$curpath"

Since the modification will be in the Registry, it will only affect new instances of programs. Open a new Command Prompt or PowerShell session, to see the effect.

Also note that we are modifying the user PATH environment variable. It will have no effect on other users of your PC.

Development Work Environment

But first, you have to decide where you will put your C programs. Each program should be in its own directory. If you write many programs, you will thus have many directories. It would be convenient to put these directories under some parent directory. This represents your ‘workspace’, or work directory.

Here we choose to make a ‘work’ directory under your ‘home’ directory (%USERPROFILE%), but you can choose any name. We will refer to this directory simply as: ‘your work-dir’.

Command Prompt — Create a Development ‘Work’ Directory
> cd %USERPROFILE%
> mkdir work
> cd %USERPROFILE%\work

The equivalent commands for PowerShell:

PowerShell — Create a Development ‘Work’ Directory
> cd $Env:USERPROFILE
> mkdir work
> cd $Env:USERPROFILE\work

In both cases, the last command could have been abbreviated to cd work, but would only work if your current directory is the parent directory of work. The commands presented, will work regardless of your current working directory.

Project Directory

Every programming session should start with the last line. This means we always assume from now on, that your work-dir is the current working directory. Every new program or project, should have a name, e.g.: ‘helloworld’ which is your project directory for our first example:

Command Prompt/PowerShell — Create Project Directory and Make Current
> cd work-dir
> mkdir helloworld
> cd helloworld

This will make %USERPROFILE%\work\helloworld\ ($Env:USERPROFILE\work\helloworld), the current directory, where we can now continue to create C source files for your project (helloworld, in this case).

Edit Project Source Code

To write programs, you need an editor. Tastes vary, so we hesitate to offer advice. The simplest option is Notepad2, or Notepad++, while VSCode requires some configuration, but is a better long-term investment. However, the standard Windows Notepad will do for now… choose one command-line below:

Command Prompt — Edit Project Source Code File (C or C++)
> notepad helloworld.c
> notepad helloworld.cpp

If you have installed VSCode, the executable is called code.exe, which should be on your PATH, so you could run: code . in your project directory (VSCode treats directories as projects). You could also just run: code helloworld.cpp to edit the file.

The last command will bring up Windows Notepad, where you must now enter a C, or C++ program. Remember to save the file, before you continue to compile it. Here is the C program:

helloworld.cExample ‘Hello World’ C Program
/*!@file  helloworld.c
*  @brief Example ‘Hello World’ C Program
*/
#include <stdbool.h>       //←good convention. for `bool`.
#include <stdlib.h>        //←good convention. for `EXIT_SUCCESS`.
#include <stdio.h>         //←necessary for `printf`, etc.

int main (void) {
   printf("Hello, World!\n");
   return EXIT_SUCCESS;
   }

And here is the equivalent C++ program:

helloworld.cppExample ‘Hello World’ in C++ Program
/*!@file  helloworld.cpp
*  @brief Example ‘Hello World’ C++ Program
*/
#include <iostream>

int main () {
   std::cout << "Hello, World!\n";
   return EXIT_SUCCESS;
   }

Once your program is saved, switch back to the Command Prompt window.

Compile and Run ‘Hello World’ C Program

The first command will create a debug compilation (-g2), suitable for development and learning. Replace ‘-g2’ with ‘-O2 -DNDEBUG’ to perform a release compilation suitable for production.

Command Prompt — Compile & Run the ‘Hello World’ Project
> gcc -Wall -Wextra -std=c99 -g2 -o helloworld helloworld.c
> .\helloworld

If the compilation was successful (no syntax errors, i.e. typing errors), the last command will run the helloworld.exe executable created by the compiler. It should output:

Hello, World!

Compile and Run ‘Hello World’ C++ Program

The first command will create a debug compilation (-g2), suitable for development and learning. Replace ‘-g2’ with ‘-O2 -DNDEBUG’ to perform a release compilation suitable for production.

Command Prompt — Compile & Run the ‘Hello World’ C++ Project
> g++ -Wall -Wextra -std=c++17 -g2 -o helloworld helloworld.cpp
> .\helloworld

If the compilation was successful (no syntax errors, i.e. typing errors), the last command will run the helloworld.exe executable created by the compiler. It should output:

Hello, World!

From this point, you have your very own GCC compiler, together with some utilities like GNU mingw32-make. To verify that mingw32-make.exe is available, your can run the following:

Command Prompt — Check Availability of GNU Make
> where mingw32-make
> mingw32-make --version
C:\mingw64\bin\make.exe

GNU Make 4.2.1
Built for Windows32
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Now you are all set to write real C programs on Windows. GCC is not a toy compiler.

WinLibs MinGW-w64 for Windows Installation Summary

Here is a summary of the steps described above (hopefully unambiguously).

ADVICEMSYS2's Alternative MinGW-w64 GCC Distribution

For production use, WinLib's MinGW-w64 is fine, but the MSYS2 project provides:

  • A minimal POSIX environment (bash shell and terminal).
  • The pacman package manager to install and update packages.
  • Many tools and libraries managed with pacman.

MSYS2 is recommended, and the closest you will get to an ‘official’ MinGW-w64 distribution, since the maintainers of MSYS2 are actively involved with the MinGW-w64 project. It will also make it easier to build projects that require typical UNIX build tools.

Even Visual Studio Code documents using MinGW-w64 with VSCode using MSYS2 as an example, including the UCRT (recommended) version.

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.

Overview

Regardless of how you control the process, or which tools you use, the process to successfully create an executable (including DLLs or shared libraries) involves the following steps or phases. The first two steps must be repeated for every .c or .cpp file.

C/C++ Simple Single File Compilation Process
  1. Preprocessing — This part of the compilation process only manipulates text, which programmers control via special (non-C/C++) preprocessor directives. The preprocessor (cpp in GCC) outputs a temporary text file which incorporates the effects of the directives. The process will terminate if any invalid preprocessor directives are encountered.

  2. Code generation — This phase of the process only uses the temporary file created by the preprocessor, and from this file outputs machine code and information to a single assembler (.s) file. In GCC, the assembler (as) physically creates the object file (.o) from the assembler file. The code generation phase also consists of several well-defined and text parsing phases, but we are generally not concerned with those.

  3. Linking — After successful object file(s) creation, the linker (ld in GCC) is called. By using either command 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 startup code for your operating system. If all external references have been resolved, an executable is created, 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

In embedded systems, the result from the linker may just be a binary image of the program, as it would appear in flash memory or ROM. The executable does not have to be destined for the same architecture 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 difference 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 understands about C/C++ is the concept of a token (words, punctuation, literals, etc.). It has no idea of C/C++ syntax, semantics or scope (blocks) at all. It does not know what a “library” is, or the meaning of include files. We emphasise this, because some people expect a higher degree of intelligence from the preprocessor than it actually possesses.

We can ‘fake’ the lack of symbolic constants, global variables, global functions and fast functions with the preprocessor. Both C & C++ have inline functions, which somewhat reduce the need for the preprocessor in this regard, but do not completely replace it. An additional, and very powerful, use for the preprocessor, is for “conditional compilation”. This means that we can use it to potentially compile different parts of the same code base, by simply changing command line arguments during compilation.

C++ inline functions, together with template functions, can reduce reliance on the preprocessor. C++11 also adds “proper” symbolic constants (compile-time values), with the addition of the new constexpr keyword, which eliminates the need for #define to represent immediate values. Nevertheless, the preprocessor plays a vital role in creating portable programs.

Object Code Generator

The output of the preprocessor is normally fed to the “actual” C/C++ compiler — the part that understands C/C++ syntax, and that creates object files (indirectly, since there may be an assembler involved). According to the standard, there may only be one object file, given one source file, or compilation unit. If preprocessor directives are encountered at this point, the compilation will fail, since they are not part of the language syntax.

If an assembler is involved, as in the case of GCC, the C/C++ compiler produces a temporary assembler file (*.s in GCC), which in turn is fed to the assembler. The assembler then creates the object file, but the C/C++ compiler still controls what will eventually 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 lifetime, machine code, public symbols for C/C++ elements with external linkage, and external references. The external references must link up with public symbols in the same, or other, object files that constitute the complete program.

Linker

A linker can be used to create an executable from any number of object files, produced by any compiler, or mixture of compilers, provided the correct calling and naming conventions are used. You cannot, for example, expect an object file created by the Fortran compiler, to be practically 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 memory 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: “resolving external references”. If public symbols are not found, for example when a library has been omitted, the linker will report a message containing the phrase “…unresolved external…”. Duplicate public symbols are not allowed, but the C/C++ compiler can mark some duplicate symbols in such a way that the linker will only use one, instead of reporting a “…duplicate symbol…” error. This is normally used for inline and/or template functions.

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 crucial for the creation of C/C++ programs. Fortunately, some tools make this easier; in particular, 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 delete temporary or object files, etc. Some of these options are described below.

PREPROCESSOR OPTIONS

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

ASSEMBLER OPTIONS
CODE GENERATION OPTIONS

The most common switches to control warnings are:

There are options to control code optimisation:

Additional warning options:

LINKER OPTIONS

The compiler drivers will generally pass the correct standard libraries and their locations to the linker.

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 produce an executable called a.exe on Windows, or a.out on POSIX systems. It will automatically control the various phases of compilation, and delete all temporary files generated. As a minimum, you should add a -oexecutable› switch, to name the executable file, instead of using the default name.

One can always use the strip program to remove symbols from release executables. This will greatly reduce their size. Or one can simply add the -s option for release builds.

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 automate 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 features of GNU Make — it is almost considered mandatory. The simplicity of C/C++ compilers, which allow you to compile only one file at a time, is useful, as it quickly becomes tedious 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 deleting them. Furthermore, if we compile one source file at a time, it means we only have to recompile 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, including Microsoft's MSBuild. It supports numerous environments, and should definitely be part of your toolset.

Another 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. Ninja is seldom used directly.

Supplementary Tools

In practice, compilers, linkers and program maintenance utilities are not enough for real-world application development. Luckily, many free top-shelf tools are available, in addition to the commercial tools.

Code Formatting

Consistent formatting of code across a project sounds like a trivial topic, but it builds confidence in the code. It also helps the brain's pattern matching mechanisms — it is easier to recognise language constructs if they look familiar, and you do not have to parse the text for every different variation in formatting.

This is often called “pretty printing”, or “code beautification”, suggesting the aesthetic appeal of consistently formatted code. The key is consistency. The problem is that, even when consistently formatted, free-format languages like C/C++ allow everybody to have a different ideal regarding the actual formatting (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, requirements.

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 executable to a set of files that must be installed (although a portable-app from a third party, exists for Windows). It can also be integrated in some IDEs. A list of checks is available. Cppcheck is active and constantly updated, and highly recommended.

Code Documentation

Although it is not many programmers' favourite activity, correct and current code documentation is crucial during the lifetime of a project. This is so important that many programming languages (e.g., Java, Python, C#, Go, Rust) have formalised the concept of standardised comments and documentation generation tools.

For C/C++, we recommend the venerable Doxygen. Doxygen can utilise tools like dot from the GraphViz suite to automatically generate call graphs, include graphs, and inheritance diagrams. It can generate 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 plugins that support Doxygen.

Running Doxygen simply means setting up a Doxyfile (default name) configuration file, and running the single command line program: doxygen, in the same directory. It can even generate a default Doxyfile for you, with comments for each and every setting. The hardest part is setting up the numerous options according to your preferences — after that, you simply 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, functions, parameters, return values, classes etc. The comment text can use Markdown markup for formatting, bullet lists, links, images, headings, etc. A project-specific external “main page” can be configured to serve as the landing page for your HTML formatted documentation. The CSS and other features 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 downloaded for offline use, and several third party docsets are also available.

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 CppReference.com — as good as, or arguably better than, cplusplus.com. You can also download a portable .zip version — no installation required.

We strongly recommend Zeal. Firstly, it is much faster than online lookup. Secondly, the documentation 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++ documentation, and at the same time learn to be more consistent with your use of C/C++ terminology.

Debugging

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 options 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 commands, instead of clicking on buttons. Once you run gdb, it displays a prompt, waiting for your debugging commands, just like a shell. It executes those commands when you press ‹Enter›. You can set and list breakpoints, watch variables, dump memory, single-step into functions, single-step over function 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 projects 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\Work

In case your are using PowerShell:

$> Set Work Directory (PowerShell)
> cd C:\Course\Work

If using the Cygwin or MSYS2 environments with Bash:

$> Set Work Directory (Cygwin/MSYS)
$ cd /c/Course/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 directory, simply execute (the first command will fail if the directory 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 project name should not contain spaces or other illegal file system characters. For your purposes, we'll represent the chosen name with: ‹project› — replace it everywhere below with your project 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 because, organisationally, each source file will contain a single major class, whose name will conventionly 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 directory is created, we should make it the current working directory. The following commands will accomplish 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 directory you put your source files. Even for simple programs, this is just a good convention, not a requirement imposed by any tool.

Create Source Files

Any editor can be used, but generally we prefer a programmer's editor like Vim, gVim, or Notepad++ (Windows only). Linux and Mac OS X also have many free and capable programmer's editors. 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 example, if you're using Vim/gVim, you can use vim, or gvim; use code for VSCode. 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, instead 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
/*!@file  ‹project›.cpp
*  @brief ‹Project› C++ Program
*/
#include <iostream>
#include <string>

int main () {

   using std::cin;  using std::cout;  using std::string;

   cout << "Your name?: ";
   string name;
   if (!getline(cin, name)) {
      std::cerr << "Invalid input. Terminating.\n";
      return EXIT_FAILURE;
      }
   if (name.length() == 0)
      name = "World";
   cout << "Hello, " << name << ". And bye..." << std::endl;

   return EXIT_SUCCESS;
   }
‹project›.c — Example Mininal C Program
/*!@file  ‹project›.c
*  @brief Minimal C Program
*/
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main (void) {

   printf("Your name?: ");
   char name[128] = { 0 };
   if (!fgets(name, sizeof name - 1, stdin)) {
      fprintf(stderr, "Invalid input. Terminating.\n");
      return EXIT_FAILURE;
      }
   int l = strlen(name);
   if (l <= 1)
      strcpy(name, "World");
   else if ((unsigned)l < sizeof name - 1)
      name[l - 1] = 0;
   printf("Hello, %s. And bye...\n", name);

   return EXIT_SUCCESS;
   }

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 and later, 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.

Some developers use the convention where the source file containing the main() function, is called main.c or main.cpp. This is not a bad convention, so you are welcome to follow this convention. Just replace project.c or project.cpp with main.c.cpp.

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 command line adds support for the GNU debugger (gdb), with the -g switch, which means this command 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 -s -o ‹project› ‹project›.cpp
$> C99 Release Compile
$> gcc -Wall -Wextra -pedantic -std=c99 -O2 -DNDEBUG -s -o ‹project› ‹project›.c

The -s option is optional. It will prevent the addition of symbols to the executable (useful only for debugging). One can alternatively use the strip command to reduce the size of a release build executable by removing those symbols:

$> 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 everything to get some action from the operating system. Double-clicking the resulting executable from the above process might execute the program. But since it is a command-line program, a console or terminal will be created for it to run in, which will be closed when the program terminates. Output will disappear, unless the program pauses for input.

The best way to execute a command-line program, is to run it from an existing shell. The Windows Command Prompt automatically looks in the current directory, so to execute the above program, you simply have to type (appending .exe is optional):

$> Execute Program (Cmd Prompt)
> ‹project›

In POSIX shells, and PowerShell, all of which do not automatically look in the current directory, execute the program as follows (where the period character means “current directory”).

$> Execute Program (POSIX & PowerShell)
 # POSIX:
$ ./project
 # PowerShell:
> .\project

For any operating system, if the directory containing the executable is listed in the PATH environment 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 individual compilation phases for projects with multiple source files, for better compilation efficiency (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++ orgcc drivers, but the -save-temps command-line option will instruct them to not delete, amongst other files, the temporary file produced by the preprocessor. 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 *.c/*.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 assembler 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 preprocessed output will be written to standard output. Calling the preprocessor (cpp) directly, depends on whether 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.

Note that we often combine -E with -P, which will output the results to standard output. This can be used as a convenience when debugging the results of specific macros, for example.

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

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

Makefiles

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 Windows).

MakefileMinimal Program Makefile
# Simple & Portable 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: Note that MinGW-w64's make is called `mingw32-make`.
#
# NOTE: The name of the executable is taken from the project directory.
#
# NOTE: For C programs, change occurrences of `$(CXX)` to `$(CC)`, and
#      `$(CXXFLAGS)` to `$(CFLAGS)`. Every `.cpp` should become `.c`.
#
PROG := $(notdir $(CURDIR:%/=%))
SRC1 := $(wildcard *.c)
OBJS := $(SRC1:%.c=%.o)
SRC2 := $(wildcard *.cpp)
OBJS += $(SRC2:%.cpp=%.o)
SRCS := $(SRC1) $(SRC2)

# compiler, linker and preprocessor options
#
CC = gcc
CXX = g++
CFLAGS += -Wall -Wextra -pedantic -std=c99
CXXFLAGS += -Wall -Wextra -pedantic -std=c++11
CPPFLAGS +=
LDFLAGS += -lm -s

# set release or debug build flags
#
ifdef RELEASE
   CFLAGS += -O2 -DNDEBUG
   CXXFLAGS += -O2 -DNDEBUG
   LDFLAGS += -s
else
   CFLAGS += -O0 -g2
   CXXFLAGS += -O0 -g2
endif

ifeq ($(OS),Windows_NT)
   PROG  := $(PROG).exe
   SHELL := powershell.exe
   .SHELLFLAGS := -NoProfile -NoLogo -Command
   FILES := $(PROG),*.o
   RMCMD := Remove-Item $(FILES) -ea SilentlyContinue; $$true > $$null
   CPCMD := Copy-Item $(PROG) $(PREFIX)/bin/
   ECHO  := Write-Output
else
   RMCMD := rm $(PROG) $(OBJS) > /dev/null 2>&1 || true
   CPCMD := cp $(PROG) $(PREFIX)/bin/
   ECHO  := echo
endif

# Generic rules to create object files from a `.cpp` or `.c` file.
#
%.o: %.cpp
	@$(ECHO) "COMPILE $^ -> $@"
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $<
%.o: %.c
	@$(ECHO) "COMPILE $^ -> $@"
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

# Use the compiler driver to link objects into an executable. For
# C programs, change `$(CXX)` to `$(CC)`.
#
$(PROG): $(OBJS)
	@$(ECHO) "LINK $^ -> '$@'"
	$(CXX) -o $@ $(OBJS) $(LDFLAGS)

# 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).
#
clean:
	$(RMCMD)

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 remove 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
clean:
	-@cmd /c del $(PROG) $(OBJS) *.ii *.s *.bc 2>NUL:

This should not be necessary in the above Makefile example, since we (simplistically) test if we are running on Windows, by checking the contents of the OS environment variable.

operating system detection in a makefile example
ifeq ($(OS),Windows_NT)
	THEOS := Windows
else
	THEOS := $(shell uname -s)
endif

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

conditional directives based on operating system
ifeq ($(THEOS),Windows)
	-@cmd /c del $(OBJS) $(PROG) 2>NUL:
else
	-@rm  $(OBJS) $(PROG) 2>/dev/null || true
endif

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 experimenting with persisting temporary preprocessed and assembler files.

Key Terminology

It is important to become familiar with the key terminology relating to C/C++ and the compilation process. For example, we must not confuse the terms declaration and definition when discussing variables 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 preprocessor. This is a single file, or compilation 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 preprocessor's run has completed.

declaration

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. Declarations 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 linked with a corresponding public symbol for the linking process to succeed.

definition

A syntax, different from declaration syntax, that creates an identifier (variable or function). 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 identifier is allocated in the object file, and constitutes either data space (variables), or machine code space (functions).

lifetime

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. Variables with global lifetime are initialised at compile time.

linkage

An identifier can have external linkage as part of its storage class specification, e.g. extern (implicitly 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 this, is the need to repeat declarations and types, which is where header files come in. The advantage is that, as long as object files are available 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 determined 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 binding syntactical 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 object files; and the machine code instructions emitted to work with values. This absolves the programmer 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 (external / internal), lifetime (local / global) and scope (file / block). Storage class does not include dynamic memory, or what some programmers refer to as “dynamic lifetime”: dynamic memory is not a syntax feature of the language.


2024-09-28: Prefer WinLib's MinGW-w64, new Makefile & minimal C program. [brx]
2023-10-23: Add note about strip and the -s option. [brx]
2021-11-22: Update Nuwen's MinGW version number. [brx]
2021-03-01: Fix output of where make. [brx]
2020-06-21: Add Nuwen-MinGW installation steps. Other minor edits. [brx]
2020-02-26: Add MinGW-w64 distributions links & CPPFLAGS in Makefile. [brx]
2019-02-25: Updated conventions for projects. Fixed command lines. [brx].
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]