Lesson 2 of 25

Installing C++ & Setup

Which Compiler Should You Install?

C++ is a standard, not a program. Nobody "downloads C++" the way they download Python. What you download is a compiler — a tool written by some organisation that reads C++ source and produces machine code. There are three that matter, and although they all implement the same language, they differ in the flags they accept, the wording of their error messages, and the small corners where they are stricter or laxer than each other.

GCC (whose C++ front end is the command g++) is the default on Linux and the compiler most online judges and university labs use. Clang (clang++) is the default on macOS and is known for producing much friendlier error messages, which matters a great deal when you start using templates. MSVC (cl.exe) is Microsoft's compiler, installed as part of Visual Studio, and is what most Windows-native and game-industry projects use.

For learning, pick g++ if you can. Not because it is technically the best, but because the overwhelming majority of C++ tutorials, competitive-programming setups and placement-test environments assume it, so commands you read online will work as written. If you are on Windows and find the toolchain setup frustrating, installing WSL and using Linux's g++ inside it is a perfectly respectable answer.

Do not agonise over the choice. Code that compiles on only one of the three has usually relied on something non-standard by accident, and finding that out early is a feature rather than a nuisance.

  • g++ — GNU Compiler Collection; the default on Linux, available everywhere, assumed by most tutorials
  • clang++ — the default on macOS; the clearest error messages of the three
  • MSVC (cl.exe) — ships with Visual Studio; uses / for flags instead of -
  • An online compiler — fine for a first week, useless once your program spans more than one file
  • WSL on Windows — gives you a real Linux toolchain without leaving Windows

Installing It

The commands below cover the usual setups. Run the version check first — on many machines a compiler is already present and you can go straight to writing code. If the shell replies that the command is not found, install it and check again. That second check is how you confirm the installation actually put the tool on your PATH, which is the step that most often goes wrong.

On Windows the two sane routes are MSYS2, which gives you a genuine g++, or Visual Studio Community with the "Desktop development with C++" workload, which gives you MSVC plus a full debugger. Installing some ancient MinGW build from a search result is how people end up with a compiler stuck on C++11, wondering why half the features in this course do not exist.

One detail that surprises people on macOS: g++ exists there, but it is usually just another name for Clang. Running g++ --version and seeing the word "clang" in the output is normal and nothing is broken.

Example
# --- check what you already have ---
g++ --version
clang++ --version

# --- Ubuntu / Debian / WSL ---
sudo apt update
sudo apt install build-essential      # g++, make and the standard headers

# --- Fedora ---
sudo dnf install gcc-c++

# --- macOS (installs Apple's Clang) ---
xcode-select --install

# --- Windows, via MSYS2 (after installing MSYS2 itself) ---
pacman -S mingw-w64-ucrt-x86_64-gcc

# --- compile and run ---
g++ -std=c++17 -o app main.cpp
./app            # on Windows: app.exe
Notes
  • If g++ --version reports a version older than 7, several things this course teaches will not compile. GCC 7 was the first release with reasonable C++17 support; GCC 10 or newer is a comfortable baseline.

The Flags That Actually Matter

A bare g++ main.cpp works, and it is also the worst way to compile. It picks whatever language standard that compiler version happens to default to, stays silent about suspicious code, and produces an executable named a.out. Four flags fix all of that, and typing them becomes muscle memory within a week.

-std=c++17 pins the language version. Without it you are at the mercy of the compiler's default, which is why a program that builds on your laptop can fail on a friend's. -Wall -Wextra switch on the warning sets, and this is the single highest-value habit in the whole language. C++ lets a great deal of dubious-but-legal code through, and a warning is the compiler saying "this compiles, but I do not believe you meant it". Comparing a signed int against an unsigned size(), using a variable before assigning it, a function that can reach its end without returning — all of these are warnings, not errors.

-o name sets the output filename. -g embeds debug information so a debugger can show you source lines instead of raw addresses. -O2 turns on optimisation, which you want for a submission or a release build but not while debugging, because optimised code gets reordered and stepping through it becomes confusing.

The two sanitizer flags deserve special mention while you are learning. -fsanitize=address catches out-of-bounds access and use-after-free at run time, and -fsanitize=undefined catches several kinds of undefined behaviour. They make your program noticeably slower, which is exactly the right trade while you are still building instincts. Both are supported by g++ and clang++ on Linux and macOS.

Example
# a good everyday build
g++ -std=c++17 -Wall -Wextra -g -o app main.cpp

# while learning: add run-time checks that catch memory bugs
g++ -std=c++17 -Wall -Wextra -g -fsanitize=address,undefined -o app main.cpp

# a release / submission build
g++ -std=c++17 -O2 -o app main.cpp

# C++20 instead
g++ -std=c++20 -Wall -Wextra -o app main.cpp

# the MSVC equivalents use a slash
# cl /std:c++17 /W4 /EHsc main.cpp
Notes
  • -Werror turns every warning into a hard error. It is excellent discipline for a team project and mildly infuriating for a beginner, so add it once your code already compiles warning-free rather than on day one.

Declaration Before Use

C++ reads your file strictly from top to bottom. A name must already be known to the compiler at the point where you use it. If main calls greet and greet is written below main, the compiler reaches the call, has never seen that name, and stops.

The fix is a declaration: a line stating the function's name, return type and parameter types, ending in a semicolon instead of a body. That is all the compiler needs in order to check that your call is correct. The definition — the version with the actual body — can come later in the same file, or in a completely different file that gets linked in afterwards.

This split is not a quirk of old compilers; it is the mechanism that makes large C++ projects buildable. It is also why the standard library works the way it does. #include <iostream> does not hand you the code behind std::cout; it hands you declarations. The actual machine code lives in the standard library and is attached by the linker at the end.

Getting this wrong produces two very different messages, and reading which one you got saves a lot of guessing. Forget the declaration and the compiler says the name was not declared in this scope. Write the declaration but never write the definition anywhere, and the compiler is perfectly happy while the linker reports undefined reference. The first means "I have never heard of this"; the second means "I believed you, and then could not find it".

Example
#include <iostream>
#include <string>

// Declarations — enough for the compiler to check calls
void greet(const std::string& name);
double average(int a, int b);

int main() {
    greet("Ananya");
    std::cout << average(7, 10) << '\n';   // 8.5
    return 0;                              // 0 means success
}

// Definitions — the bodies, written afterwards
void greet(const std::string& name) {
    std::cout << "Hello, " << name << "!\n";
}

double average(int a, int b) {
    return (a + b) / 2.0;   // 2.0, not 2 — see the operators lesson
}

Splitting a Program Across Files

Once a program passes a few hundred lines, keeping it in one file stops being pleasant. The C++ convention is to split each logical piece in two: a header file (.h or .hpp) holding declarations, and a source file (.cpp) holding the definitions. Anyone who wants to use your code includes the header; the build compiles the .cpp once and links it in.

Every header needs protection against being included twice. Suppose main.cpp includes both math_utils.h and report.h, and report.h also includes math_utils.h. The preprocessor pastes text blindly, so those declarations arrive twice and the compiler complains about redefinition. Writing #pragma once as the first line of every header prevents this. It is not in the C++ standard, but every compiler you are realistically going to meet supports it; the strictly portable alternative is an include guard built from #ifndef.

Note the quotes-versus-angle-brackets rule while you are here. Use #include "my_header.h" for your own files and #include <iostream> for standard-library and system headers. Angle brackets tell the preprocessor to search only the system include paths, which is both faster and clearer about intent.

Compiling several files is simply a matter of naming them all on the command line. Once a project grows past a handful, you graduate to a build tool such as Make or CMake so that only the files you actually changed get recompiled — but the command underneath is exactly the one shown below.

Example
// ---------- math_utils.h ----------
#pragma once

int square(int n);
double average(int a, int b);

// ---------- math_utils.cpp ----------
#include "math_utils.h"

int square(int n) { return n * n; }
double average(int a, int b) { return (a + b) / 2.0; }

// ---------- main.cpp ----------
#include <iostream>
#include "math_utils.h"

int main() {
    std::cout << square(7) << '\n';        // 49
    std::cout << average(7, 10) << '\n';   // 8.5
}

// Build both translation units together:
//   g++ -std=c++17 -Wall main.cpp math_utils.cpp -o app
Notes
  • A very common first mistake: running g++ main.cpp alone when square lives in math_utils.cpp. It compiles fine and then fails at link time with undefined reference to 'square(int)'. The header promised the function exists; you simply never gave the linker the file that contains it.
Ask AI