C++ • GETTING STARTED AND TOOLING

Command-Line Compilation — Compile and run a C++ program from the command line (g++/clang++ and ./a.out)

Master the fundamental workflow of transforming C++ source code into executable binaries using terminal-based compilers.

Historical Context & Motivation

Before integrated development environments abstracted away the compilation process, every programmer interacted directly with the command line to transform human-readable source code into machine-executable binaries. The evolution of C++ compilers is deeply intertwined with the history of Unix, the GNU project, and the open-source movement. Understanding this lineage illuminates why g++ and clang++ remain the dominant tools for serious C++ development, even in an era of sophisticated IDEs. These tools form the backbone upon which virtually all graphical build environments are constructed, and proficiency with them is expected of any working software engineer.

1979
C with Classes
Bjarne Stroustrup begins developing C with Classes at Bell Labs, using a preprocessor called Cpre that translated the new constructs into plain C for compilation by the existing cc compiler.
1985
Cfront and The C++ Programming Language
Stroustrup releases the first edition of The C++ Programming Language. The Cfront compiler translates C++ to C, which is then compiled by the host system's C compiler — an early form of source-to-source compilation.
1987
GCC Adds C++ Support
The GNU Compiler Collection (GCC) begins incorporating C++ support. The g++ front-end becomes the de facto free and open-source C++ compiler on Unix-like systems, enabling widespread adoption outside of commercial toolchains.
2007
LLVM/Clang Project Emerges
Apple and the University of Illinois release Clang, a new C/C++/Objective-C front-end for the LLVM compiler infrastructure. Clang++ rapidly gains popularity for its faster compile times, superior diagnostics, and modular architecture.
2011–Present
Modern C++ and Toolchain Maturity
The C++11 standard and its successors (C++14, C++17, C++20, C++23) drive both g++ and clang++ to compete on standards conformance, optimization quality, and developer experience — making command-line compilation more powerful and accessible than ever.

The central question this lesson addresses is deceptively simple: how does a text file containing C++ source code become a running program on your machine? By working directly with the compiler from the command line, you gain transparent insight into each stage of the build process — knowledge that remains essential whether you eventually work with Makefiles, CMake, Bazel, or IDE-managed projects.

Core Principles & Definitions

Command-line compilation involves invoking a compiler driver program — such as g++ or clang++ — from a terminal shell. The compiler driver orchestrates a multi-phase pipeline that transforms source code (typically files ending in .cpp, .cc, or .cxx) into a native executable binary. Understanding the distinct phases of this pipeline is critical for diagnosing errors, optimizing build times, and managing multi-file projects.

1

Preprocessing

The preprocessor expands #include directives, evaluates #define macros, and processes conditional compilation directives (#ifdef, #endif). The output is a single translation unit — pure C++ with all headers inlined.
2

Compilation

The compiler front-end parses the translation unit, performs semantic analysis and type checking, then generates assembly code (or an intermediate representation like LLVM IR). This is where syntax errors and type mismatches are caught.
3

Assembly

The assembler translates architecture-specific assembly code into object code — machine instructions stored in .o (Unix) or .obj (Windows) files. Each object file contains relocatable binary code with unresolved external symbols.
4

Linking

The linker resolves all external symbol references, combines object files with library code, and produces the final executable binary. By default, g++ and clang++ name this output a.out on Unix-like systems.
✦ KEY TAKEAWAY
Think of the compilation pipeline like an automotive assembly line. The preprocessor gathers all the raw materials (headers and macros) into one place. The compiler stamps those materials into precisely shaped parts (assembly instructions). The assembler welds those parts into rigid subassemblies (object files). Finally, the linker bolts everything together into a finished vehicle (the executable). When something fails, knowing which station on the line produced the defect tells you exactly where to look.

The Compilation Pipeline — Visual Overview

The compilation pipeline from source file (main.cpp) through four stages — preprocessing, compilation, assembly, and linking — producing the executable a.out. Libraries are linked in the final stage. Running g++ main.cpp performs all four stages in a single invocation.

When you type g++ main.cpp at the terminal, the compiler driver transparently invokes all four phases in sequence. You can halt the pipeline at any intermediate stage using specific flags: -E stops after preprocessing, -S stops after generating assembly, and -c stops after producing the object file. This fine-grained control is one of the key advantages of command-line compilation — it allows you to inspect intermediate artifacts, isolate the source of build failures, and integrate specific stages into automated build systems.

How It Works — Flags, Options, and the Driver Model

Both g++ and clang++ function as compiler drivers — programs that coordinate the execution of the preprocessor, compiler proper, assembler, and linker. The driver parses command-line flags, translates them into arguments for each sub-tool, and manages the temporary files created between stages. Although g++ and clang++ are separate implementations with different internal architectures, they share a largely compatible flag interface by design, which allows developers to switch between them with minimal friction.

Essential Compiler Flags

Common g++/clang++ flags for controlling compilation
FlagPurposeExample
-o <name>Name the output file (instead of a.out)g++ main.cpp -o main
-std=c++17Select the C++ language standardg++ -std=c++20 main.cpp
-Wall -WextraEnable comprehensive warning diagnosticsg++ -Wall -Wextra main.cpp
-gEmbed debugging symbols (for GDB/LLDB)g++ -g main.cpp -o main
-O0 / -O2 / -O3Set optimization level (0 = none, 3 = aggressive)g++ -O2 main.cpp -o main
-EStop after preprocessing; output to stdoutg++ -E main.cpp > main.ii
-SStop after compilation; emit assembly (.s file)g++ -S main.cpp
-cStop after assembly; produce object file (.o)g++ -c main.cpp

Multi-File Compilation

Real-world C++ projects span multiple translation units. A common pattern is to compile each source file into an object file independently, then link them together in a final step. For example, given main.cpp and utils.cpp, you could run: g++ -c main.cpp and g++ -c utils.cpp to produce main.o and utils.o, then g++ main.o utils.o -o myprogram to link them. This separate compilation model is critical for large codebases because only the modified files need to be recompiled — a principle that build systems like Make and CMake automate.

💡 g++ vs. clang++: Which to Use?
For learning purposes, both compilers are excellent choices and produce conformant executables. Clang++ is often preferred by beginners for its clearer, more expressive error messages (including source-range highlighting and fix-it hints). g++ is ubiquitous on Linux and sometimes has faster adoption of the newest C++ standard features. Many professional developers install both and switch between them to catch different classes of warnings.

Understanding Compiler Output — Errors, Warnings, and Diagnostics

One of the most practical skills in command-line compilation is reading and interpreting diagnostic messages. Both g++ and clang++ report issues using a consistent format: the file name, line number, column number, severity level (error or warning), and a description of the problem. Errors halt compilation because the compiler cannot produce valid code; warnings indicate suspicious constructs that may compile but likely indicate a bug or unintended behavior. Developing fluency in reading these messages dramatically reduces debugging time.

Compiler output is structured: the file name, line, and column pinpoint the exact location. The severity tells you whether compilation can continue. Compile-time errors (left) differ from linker errors (right) in both their causes and the stage at which they appear.

A critical distinction to internalize is the difference between compile-time errors and linker errors. Compile-time errors (syntax issues, type violations, undeclared identifiers) originate from the compiler and reference specific source locations. Linker errors — such as undefined reference to 'foo()' — appear after all files have compiled successfully and indicate that the linker could not find the definition of a declared symbol. Beginners frequently confuse the two, but recognizing which stage produced the error immediately narrows the search space for the fix.

Worked Example — From Source to Execution

Let us walk through the complete process of writing, compiling, and running a C++ program from the command line. We will use a small program that computes the factorial of a user-provided integer, demonstrating the full edit-compile-run cycle along with useful compiler flags.

Compile and Run a Factorial Program
1
Step 1 — Create the Source FileUsing any text editor (nano, vim, VS Code, etc.), create a file called factorial.cpp with the following contents: #include <iostream> int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); } int main() { int num; std::cout << "Enter a non-negative integer: "; std::cin >> num; std::cout << num << "! = " << factorial(num) << std::endl; return 0; }
File factorial.cpp saved in the working directory.
2
Step 2 — Compile with Warnings EnabledOpen your terminal, navigate to the directory containing factorial.cpp, and compile: g++ -std=c++17 -Wall -Wextra -o factorial factorial.cpp Here, -std=c++17 selects the C++17 standard, -Wall -Wextra enables comprehensive warnings, and -o factorial names the output executable factorial instead of the default a.out.
If no errors or warnings appear, the compilation succeeded silently — this is normal and expected behavior. Silence means success.
3
Step 3 — Verify the Executable ExistsList the directory contents to confirm the executable was created: ls -l factorial You should see a file with execute permissions (the x bits in the permission string). On macOS, you can also use file factorial to confirm it is a Mach-O executable; on Linux it will report an ELF binary.
-rwxr-xr-x 1 user user 16384 Jan 15 12:00 factorial
4
Step 4 — Run the ProgramExecute the compiled binary using the dot-slash prefix: ./factorial The ./ prefix is necessary because most Unix shells do not include the current directory in the PATH environment variable for security reasons. Without it, the shell will search system directories and likely report "command not found."
Enter a non-negative integer: 6 6! = 720
5
Step 5 — Inspect Intermediate Artifacts (Optional)To build intuition about the pipeline, try stopping at each stage: g++ -E factorial.cpp > factorial.ii # Preprocessed output g++ -S factorial.cpp # Assembly: factorial.s g++ -c factorial.cpp # Object file: factorial.o Open factorial.ii — you will see thousands of lines from the expanded <iostream> header. Open factorial.s to see the x86-64 assembly generated by the compiler. This exercise demystifies what happens "under the hood" and reinforces the four-stage model.
Three intermediate files: factorial.ii (~40,000 lines), factorial.s (~100 lines of assembly), and factorial.o (binary object file).

Comparing g++ and clang++ — Strengths and Tradeoffs

Both g++ (part of the GNU Compiler Collection) and clang++ (part of the LLVM project) are production-quality, standards-conforming C++ compilers. Choosing between them for a given task depends on your priorities — whether you need the deepest optimization on a specific platform, the best diagnostic experience, or compatibility with a particular toolchain ecosystem. In practice, many professional teams use both as a form of cross-validation.

Head-to-head comparison of the two major open-source C++ compilers
Criteriong++ (GCC)clang++ (LLVM)
Error MessagesGood; improving steadily in recent versionsExcellent; includes fix-it suggestions, source-range highlighting, and colored output by default
Compile SpeedModerate; can be slower on large projectsGenerally faster parsing and code generation
Optimization QualityMature and aggressive, especially at -O3; strong auto-vectorizationCompetitive; LLVM's modular pass pipeline excels at link-time optimization (LTO)
Platform AvailabilityDefault on most Linux distributions; available on macOS (via Homebrew), Windows (MinGW/MSYS2)Default on macOS (Xcode); available on Linux and Windows
Standard ConformanceOften first to implement new C++ features from recent standardsClose to feature parity; sometimes stricter in rejecting non-conforming code
SanitizersSupports ASan, TSan, UBSanOriginated ASan, TSan, MSan; generally more mature sanitizer support
LicenseGPL v3 with GCC Runtime Library ExceptionApache 2.0 with LLVM Exception (more permissive for toolchain embedding)
✦ KEY TAKEAWAY
Think of g++ and clang++ as two high-quality power drills from different manufacturers — both bore perfectly round holes and accept the same standard drill bits (C++ source files and flags). The ergonomics differ slightly: one might have a better LED light (diagnostics), the other a more powerful motor (optimization). A professional carpenter keeps both in the toolbox and reaches for whichever suits the task. Similarly, compiling your code with both compilers is an excellent habit because each may catch different subtle issues in your code.

Connection to Build Systems and Professional Workflows

Direct command-line compilation is the foundation upon which all higher-level build systems are constructed. Once a project grows beyond a handful of files, manually typing compiler invocations becomes tedious and error-prone. Tools like Make, CMake, Bazel, and Meson automate the process by tracking file dependencies, only recompiling what has changed, and generating the exact g++ or clang++ commands for you. Understanding command-line compilation ensures you can debug build system configurations, write custom build rules, and comprehend what your IDE is doing behind the scenes when you press the "Build" button.

Progression from direct CLI compilation to professional build systems
ApproachComplexityTypical Use Case
Direct CLI (this lesson)1–5 filesLearning, quick prototypes, single-file scripts, competitive programming
Makefile5–50 filesSmall to medium projects; dependency tracking with manual rule authoring
CMakeAny sizeIndustry standard meta-build system; generates Makefiles, Ninja files, or IDE project files
Bazel / MesonLarge-scaleMonorepo-scale builds with reproducibility, caching, and distributed compilation

As you advance through systems programming, operating systems, and compiler design courses, you will encounter situations where understanding the raw compilation commands is not merely helpful but essential — for instance, when cross-compiling for embedded targets, integrating third-party C libraries via -l and -I flags, or debugging obscure linker errors that a high-level build system has abstracted away. The command line is always the escape hatch.

🚀 Looking Ahead
After mastering single-file compilation, natural next steps include learning Makefiles for dependency-based builds, CMake for cross-platform project generation, and compiler sanitizers (-fsanitize=address,undefined) for catching memory errors and undefined behavior at runtime.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain the difference between a compile-time error and a linker error. If you see undefined reference to 'compute()', at which stage of the pipeline did the error occur, and what is the most likely cause?
PROBLEM 2 — BASIC
Write the exact terminal command to compile a file called hello.cpp using g++ with the C++17 standard, all warnings enabled (-Wall -Wextra), and name the output executable hello. Then write the command to run it.
PROBLEM 3 — INTERMEDIATE
You have three source files: main.cpp, math_utils.cpp, and io_utils.cpp. Write the commands to perform separate compilation — compile each file to an object file individually, then link them into an executable called app. Why is this preferable to a single compilation command for large projects?
PROBLEM 4 — APPLIED
A colleague reports that their program compiles successfully with g++ -Wall main.cpp matrix.cpp -o solver but crashes at runtime with a segmentation fault. Describe two command-line compilation strategies you would use to diagnose the crash without modifying the source code, and explain what information each provides.
PROBLEM 5 — CRITICAL THINKING
Consider a project where foo.cpp includes bar.h, and bar.h defines an inline function inline int bar() { return 42; }. Meanwhile, baz.cpp also includes bar.h. When you link foo.o and baz.o together, will the linker report a "multiple definition" error for bar()? Why or why not? What would change if the function were defined as int bar() { return 42; } (without the inline keyword)?

Summary

Command-line compilation with g++ or clang++ is the foundational skill upon which all C++ development workflows are built. The four-stage pipeline — preprocessing (macro expansion and header inclusion), compilation (parsing, semantic analysis, code generation), assembly (translation to machine code), and linking (symbol resolution and executable generation) — transforms source code into a runnable binary, typically named a.out by default.

Key flags like -o (naming output), -std=c++17 (standard selection), -Wall -Wextra (warnings), -g (debug symbols), and -O2 (optimization) give you precise control over every aspect of the build. The separate compilation model (compiling to .o files with -c then linking) is the foundation of efficient incremental builds. Mastering these fundamentals prepares you for Makefiles, CMake, and professional-scale build systems.

Varsity Tutors • C++ • Command-Line Compilation — Compile and run a C++ program from the command line (g++/clang++ and ./a.out)