C++ · lesson 2 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7Setting Up C++ Development Environment
Install GCC or Clang, configure CMake, set up VS Code, and compile your first C++ program with modern standards.
By SkillByExample · Updated
Installing a C++ Compiler
Before you can write and run C++, you need a compiler that translates your source code into machine code. The two dominant choices are GCC (GNU Compiler Collection) and Clang (LLVM-based). Both fully support C++17 and C++20, produce fast binaries, and are completely free. Clang tends to produce friendlier error messages, making it particularly good for learning. GCC often edges ahead on raw optimization. In practice, professionals use both.
Linux (Ubuntu/Debian)
sudo apt update
sudo apt install build-essential # installs g++, gcc, make
sudo apt install clang # optional: install Clang too
g++ --version # verify GCC
clang++ --version
build-essential gives you g++, gcc, make, and standard headers. It is the standard starting point on Debian-based distros.
macOS
Apple ships Clang via Xcode Command Line Tools. This is the fastest path to a working compiler on macOS — no full Xcode install required.
# Install Xcode Command Line Tools (includes Apple Clang)
xcode-select --install
# Verify
clang++ --version
# Optionally install GCC via Homebrew
brew install gcc
g++-14 --version # note: Homebrew installs as g++-<version>
Windows (MSYS2 / MinGW-w64)
The cleanest way to get GCC on Windows is through MSYS2. It provides a full Linux-like shell environment and package manager, so you can follow the same workflow as on Linux.
# In MSYS2 UCRT64 terminal
pacman -S mingw-w64-ucrt-x86_64-gcc
pacman -S mingw-w64-ucrt-x86_64-clang # optional
g++ --version
Add C:\msys64\ucrt64\bin to your Windows PATH so you can run g++ from any terminal.
Alternative on Windows: Install Visual Studio Build Tools to get MSVC (cl.exe), which is Microsoft’s own compiler. VS Code works with all three (GCC, Clang, MSVC).
Compiling from the Command Line
Understanding the compiler flags directly is essential before relying on build systems. When something goes wrong in a build, you need to know what command the build system is actually running. These flags are the vocabulary of C++ compilation.
# Basic compile — outputs executable named 'main'
g++ main.cpp -o main
# With modern standard and warnings — use this while learning
g++ -std=c++17 -Wall -Wextra -o main main.cpp
# With optimizations (release build)
g++ -std=c++17 -O2 -DNDEBUG -o main main.cpp
# With debug info (debug build) — required for debuggers like gdb/lldb
g++ -std=c++17 -g -O0 -o main main.cpp
# C++20
g++ -std=c++20 -Wall -Wextra -o main main.cpp
# Using Clang (same flags)
clang++ -std=c++17 -Wall -Wextra -o main main.cpp
Key flags:
-std=c++17/-std=c++20— enable the specified standard (GCC 11 and later default to C++17 with GNU extensions; older compilers default to C++14 or earlier)-Wall— enable common warnings-Wextra— enable additional warnings beyond-Wall-O2— optimize for speed (release builds)-g— include debug symbols for debuggers like gdb/lldb-o <name>— set output file name
A simple test program to verify your setup:
// verify.cpp
#include <iostream>
#include <format> // C++20 — if this compiles, you have C++20 support
#include <vector>
#include <algorithm> // std::ranges::sort lives here
#include <ranges>
int main() {
std::vector<int> v = {5, 3, 1, 4, 2};
// C++20 ranges sort — cleaner than std::sort(v.begin(), v.end())
std::ranges::sort(v);
for (int x : v) {
std::cout << std::format("{} ", x);
}
std::cout << "\n";
return 0;
}
g++ -std=c++20 -o verify verify.cpp && ./verify
# 1 2 3 4 5
Setting Up VS Code
VS Code is the most popular editor for C++ development on all platforms. It is free, fast, and has excellent C++ tooling through extensions. The right extension combination gives you IntelliSense, debugging, and integrated CMake builds all in one window.
Extensions to install:
- C/C++ by Microsoft (
ms-vscode.cpptools) — IntelliSense, debugging, code navigation - clangd (
llvm-vs-code-extensions.vscode-clangd) — faster, more accurate IntelliSense based on Clang’s toolchain (recommended over the default IntelliSense for large projects) - CMake Tools (
ms-vscode.cmake-tools) — CMake integration with build/run buttons
If you use both cpptools and clangd, disable cpptools IntelliSense to avoid conflicts: add "C_Cpp.intelliSenseEngine": "disabled" to your VS Code settings.
A minimal .vscode/c_cpp_properties.json for GCC on Linux:
{
"configurations": [
{
"name": "Linux GCC",
"compilerPath": "/usr/bin/g++",
"cppStandard": "c++17",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
Setting Up CMake
CMake is the de-facto build system for C++ projects. It solves a real problem: you want your project to build on Linux with Makefiles, on macOS with Xcode, and on Windows with Visual Studio — all from the same source. CMake generates the native build files for each platform from a single CMakeLists.txt description.
Install CMake:
# Ubuntu/Debian
sudo apt install cmake
# macOS
brew install cmake
# Windows (MSYS2)
pacman -S mingw-w64-ucrt-x86_64-cmake
A minimal CMakeLists.txt for a single-target project:
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)
# Require C++17 — fail the build if the compiler doesn't support it
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # use -std=c++17, not -std=gnu++17
# Define the executable and its source files
add_executable(myapp
src/main.cpp
src/utils.cpp
)
# Enable warnings for this target
target_compile_options(myapp PRIVATE -Wall -Wextra)
Build and run:
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .
./myapp
For a debug build:
cmake .. -DCMAKE_BUILD_TYPE=Debug
cmake --build .
CMake separates source and build directories — the build/ folder contains all generated files and compiled objects. Your source tree stays clean and the build directory can be deleted and regenerated at any time.
Recommended Project Layout
A clean, consistent directory layout makes projects easier to navigate and scales naturally as the codebase grows. This layout separates source, headers, and tests, and keeps generated build artifacts out of the source tree.
myproject/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ └── engine/
│ ├── engine.hpp
│ └── engine.cpp
├── tests/
│ └── engine_test.cpp
└── build/ # generated, not committed to git
With this structure, your CMakeLists.txt grows naturally as you add source files, and VS Code’s CMake Tools extension can auto-detect the project and provide build buttons in the status bar.
Practice
- Which standard am I on? Every compiler defines the macro
__cplusplusto match the standard it is compiling. Print it with no-stdflag, then with-std=c++17,-std=c++20and-std=c++23.
Solution
#include <iostream>
int main() {
std::cout << "__cplusplus = " << __cplusplus << "\n";
}
$ g++ --version | head -n 1
g++ (GCC) 14.4.0
$ g++ standard.cpp -o standard && ./standard
__cplusplus = 201703
$ g++ -std=c++17 standard.cpp -o standard && ./standard
__cplusplus = 201703
$ g++ -std=c++20 standard.cpp -o standard && ./standard
__cplusplus = 202002
$ g++ -std=c++23 standard.cpp -o standard && ./standard
__cplusplus = 202302
The value is the year and month the standard was finalised. GCC 14 with no flag compiles C++17 (strictly
gnu++17, which adds GNU extensions), so verify.cpp will not build without -std=c++20. Pass the flag
explicitly instead of relying on a default that changes between compiler versions.
- Prove
verify.cppneeds C++20. Compileverify.cppwith-std=c++17and look only at theerror:lines. Then build it with-std=c++20.
Solution
$ g++ -std=c++17 -o verify verify.cpp 2>&1 | grep error
verify.cpp:12:10: error: 'std::ranges' has not been declared
verify.cpp:15:27: error: 'format' is not a member of 'std'
$ g++ -std=c++20 -o verify verify.cpp && ./verify
1 2 3 4 5
Under C++17, #include <format> and #include <ranges> still succeed, but the headers declare nothing, so
the errors only appear at the first use. Note also that std::ranges::sort is declared in <algorithm>,
not <ranges>. Leave that include out and GCC 14 can reject the program even with -std=c++20, depending on which
other headers happen to pull it in.
-
Let
-Wextrafind the mismatch. This loop compiles silently with no flags. Build it with-Wall -Wextra, read the warning, and fix it.#include <iostream> #include <vector> int main() { std::vector<int> v = {5, 3, 1, 4, 2}; int total = 0; for (int i = 0; i < v.size(); i++) total += v[i]; std::cout << "total = " << total << "\n"; }
Solution
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {5, 3, 1, 4, 2};
int total = 0;
for (std::size_t i = 0; i < v.size(); i++)
total += v[i];
std::cout << "total = " << total << "\n";
}
$ g++ -std=c++17 -o loop loop.cpp && ./loop
total = 15
$ g++ -std=c++17 -Wall -Wextra -o loop loop.cpp
loop.cpp: In function 'int main()':
loop.cpp:7:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
7 | for (int i = 0; i < v.size(); i++)
| ~~^~~~~~~~~~
$ g++ -std=c++17 -Wall -Wextra -o loop loop_fixed.cpp && ./loop
total = 15
v.size() returns an unsigned size_type, so comparing it with a signed int converts the int, and a
negative value would turn into a huge positive one. Declaring the index as std::size_t removes the
mismatch. A range-based for (int x : v) avoids the index entirely.
-
Debug build vs release build. This program checks its own assumption with
assert. Build it as a debug build (-g -O0) and as a release build (-O2 -DNDEBUG), and compare the runs.#include <cassert> #include <iostream> #include <vector> int main() { std::vector<int> v = {5, 3, 1}; assert(v.size() == 4 && "expected four elements"); std::cout << "passed the check, size = " << v.size() << "\n"; }
Solution
$ g++ -std=c++17 -g -O0 -o check check.cpp && ./check
check: check.cpp:7: int main(): Assertion `v.size() == 4 && "expected four elements"' failed.
bash: line 1: 54 Aborted (core dumped) ./check
$ g++ -std=c++17 -O2 -DNDEBUG -o check check.cpp && ./check
passed the check, size = 3
-DNDEBUG defines the NDEBUG macro, and <cassert> turns every assert into nothing when that macro is
set. The release build therefore skips the check and carries on with the wrong size. Asserts guard against
programmer mistakes during development. They are not a way to validate input in production.
- Build a two-file project with CMake. Create
src/main.cppandsrc/utils.cpp(plus autils.hppheader) for theCMakeLists.txtin this lesson, then configure, build and run them. Afterwards,touch src/main.cppand build again.
Solution
// src/utils.hpp
#pragma once
#include <string>
std::string shout(const std::string& s);
// src/utils.cpp
#include "utils.hpp"
#include <cctype>
std::string shout(const std::string& s) {
std::string out = s;
for (char& c : out)
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return out + "!";
}
// src/main.cpp
#include <iostream>
#include "utils.hpp"
int main() {
std::cout << shout("hello from cmake") << "\n";
}
$ mkdir build && cd build
$ cmake .. -DCMAKE_BUILD_TYPE=Release
-- The CXX compiler identification is GNU 14.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/local/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (1.4s)
-- Generating done (0.2s)
-- Build files have been written to: /src/build
$ cmake --build .
[ 33%] Building CXX object CMakeFiles/myapp.dir/src/main.cpp.o
[ 66%] Building CXX object CMakeFiles/myapp.dir/src/utils.cpp.o
[100%] Linking CXX executable myapp
[100%] Built target myapp
$ ./myapp
HELLO FROM CMAKE!
$ touch ../src/main.cpp
$ cmake --build .
[ 33%] Building CXX object CMakeFiles/myapp.dir/src/main.cpp.o
[ 66%] Linking CXX executable myapp
[100%] Built target myapp
The configure step runs once and writes Makefiles into build/. After that, cmake --build . rebuilds
only what changed: after the touch, it recompiled main.cpp and relinked, but reused utils.cpp.o. The
source tree stays clean, and deleting build/ resets everything.