Learn C++
RAII, smart pointers, templates, and the STL - zero-overhead abstraction over C.
Setup, Compiling, and the Basics
Install a compiler, compile your first program, and meet C++'s core syntax.
Setup, Compiling, and the Basics
C++ is a compiled, statically typed systems language created by Bjarne Stroustrup in the early 1980s as "C with Classes." It keeps C's machine-level model - direct memory access, no mandatory runtime, no garbage collector - while layering on classes, templates, and the Standard Template Library (STL). Its guiding principle is zero-overhead abstraction: you do not pay (in time or space) for features you do not use, and the features you do use are as efficient as hand-written code. This lesson gets a compiler working and walks through the syntax shared with every later lesson.
Installing a compiler
C++ has several mature, free compilers. On Linux you almost certainly want GCC (the g++ driver) or Clang; on macOS, Clang ships with the Xcode command-line tools; on Windows, MSVC (Visual Studio) or MSYS2's GCC.
$ sudo apt install g++ # Debian/Ubuntu
$ xcode-select --install # macOS (installs clang++)
$ g++ --version
g++ (GCC) 14.2.0
C++ evolves on a three-year ISO cycle: C++11, C++14, C++17, C++20, and C++23 are the milestones, with C++26 in progress as of 2026. Always tell the compiler which standard to target with -std; this track assumes C++17 or newer.
Your first program
Create hello.cpp:
#include <iostream>
int main() {
std::cout << "Hello, C++!" << '\n';
return 0;
}
Every program starts at int main(). #include <iostream> pulls in the I/O stream library; std::cout is the standard output stream and << is the (overloaded) stream-insertion operator. Returning 0 from main signals success - in fact you may omit the return in main and the compiler supplies return 0; for you.
Compile and run:
$ g++ -std=c++17 -Wall -Wextra -O2 hello.cpp -o hello
$ ./hello
Hello, C++!
Get in the habit of always passing -Wall -Wextra (turn on the common warnings) - C++ will happily compile code with latent bugs, and the warnings catch many of them.
Variables, types, and auto
C++ has the same fundamental types as C - int, double, char, bool, plus fixed-width types like int32_t, uint64_t from <cstdint>. Variables are declared with their type, but auto lets the compiler deduce it from the initializer:
int count = 42;
double ratio = 3.14;
bool ready = true;
auto name = std::string("Ada"); // deduced as std::string
const int LIMIT = 100; // const = cannot be reassigned
Prefer brace initialization (int x{5};) where you can: it is uniform across types and forbids narrowing conversions (int x{3.9}; is an error, catching accidental data loss).
Control flow and functions
Control flow is C-style: if/else, for, while, switch. Functions declare a return type, a name, and typed parameters:
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
for (int i = 0; i < 3; ++i) {
std::cout << "add(" << i << ", 10) = " << add(i, 10) << '\n';
}
int total = 0;
int xs[] = {1, 2, 3, 4};
for (int x : xs) { // range-based for loop (C++11)
total += x;
}
std::cout << "total = " << total << '\n';
}
The range-based for loop (for (int x : xs)) iterates any container or array without index bookkeeping and is the idiomatic way to loop in modern C++.
References vs. pointers
C++ keeps C's pointers but adds references - an alias for an existing object that can never be null and never rebound. Pass large objects by const reference to avoid copies:
void grow(int& n) { n += 1; } // reference: modifies caller's variable
void show(const std::string& s) { // const ref: no copy, read-only
std::cout << s << '\n';
}
int main() {
int v = 5;
grow(v); // v is now 6 - no pointer syntax needed
int* p = &v; // pointer still available when you need it
*p = 100; // dereference to write
}
Use references for "this function works on the caller's object," pointers when a thing may be absent (null) or you need pointer arithmetic. This split foreshadows the memory model in the next lesson.
Reference
- cppreference (the canonical reference): https://en.cppreference.com/
- LearnCpp tutorial: https://www.learncpp.com/
Next we tackle the idea at the heart of modern C++: how it manages memory.
The Memory Model: Stack, Heap, and RAII
Manual new/delete, why it is dangerous, and how RAII tames it.
The Memory Model: Stack, Heap, and RAII
C++ has manual, garbage-collector-free memory management - this is the single most important thing to understand about the language. You have direct, predictable control over where every object lives and exactly when it is destroyed. The danger of that power (leaks, double-frees, dangling pointers) is what RAII - Resource Acquisition Is Initialization - exists to eliminate. RAII is the distinctive C++ idea, and the rest of the language is built around it.
Where objects live: stack vs. heap
Every object lives in one of two places:
- Automatic storage (the stack). Local variables live here. They are created when execution reaches their declaration and destroyed automatically, in reverse order, when their enclosing scope ends. No allocation call, no free call.
- Dynamic storage (the heap). Objects you create with
newlive here and persist until you destroy them withdelete. The compiler does not track them.
void f() {
int x = 10; // stack: destroyed automatically at end of f()
int* p = new int(20); // heap: lives until you call delete p
// ...
delete p; // YOU must free it - or it leaks
} // x is gone here; the heap int was freed manually
new / delete and what goes wrong
new allocates and constructs; delete destructs and deallocates. Arrays use new[]/delete[] (mixing the two forms is undefined behavior):
int* one = new int(5);
int* many = new int[100]; // array of 100 ints
delete one; // single object
delete[] many; // array form - note the []
Because the compiler never frees heap memory for you, manual management invites three classic bugs:
int* p = new int(1);
// ... early return or exception here ...
delete p; // LEAK: never reached if we left early
int* q = new int(2);
delete q;
delete q; // DOUBLE-FREE: undefined behavior
int* r = new int(3);
delete r;
*r = 9; // USE-AFTER-FREE: r is now a dangling pointer
C-style malloc/free are also available (via <cstdlib>) but should be avoided in C++: they do not run constructors or destructors. The point of modern C++ is that you should almost never write new/delete by hand at all - because RAII does it for you.
RAII: tie resources to object lifetime
The insight of RAII is: acquire a resource in a constructor and release it in the destructor. Then the resource's lifetime is bound to an object's lifetime, and the language's automatic, deterministic destruction guarantees cleanup - even when an exception is thrown or you return early.
#include <cstdio>
class File {
std::FILE* fp;
public:
explicit File(const char* path) : fp(std::fopen(path, "r")) {} // acquire
~File() { if (fp) std::fclose(fp); } // release
std::FILE* get() const { return fp; }
};
void read_config() {
File f("config.txt"); // file opened here
// ... use f.get() ...
// anything can throw or return; the file STILL closes,
// because ~File() runs automatically when f goes out of scope.
} // <- fclose happens here, guaranteed
A destructor is named ~ClassName, takes no arguments, and runs automatically at scope exit (for stack objects) or on delete (for heap objects). This is the deterministic counterpart to garbage collection: cleanup happens at a precise, knowable moment, not "eventually."
Destruction is deterministic and ordered
Stack objects are destroyed in reverse order of construction, at the closing brace:
{
Logger a("first");
Logger b("second");
} // destructors run here: ~b() then ~a()
This ordering is what makes RAII compose: a std::lock_guard releases its mutex, a std::vector frees its buffer, a File closes its handle - all automatically, all in the right order, all exception-safe.
The Rule of Five (and Zero)
If a class manages a raw resource it must define how it is copied, moved, and destroyed. The five special members are the destructor, copy constructor, copy assignment, move constructor, and move assignment - the Rule of Five: if you write one, you usually need to consider all five.
The better goal is the Rule of Zero: do not manage raw resources yourself at all. Hold them in standard RAII types (std::vector, std::string, std::unique_ptr - the next lesson) that already implement the five correctly, and your class needs none of them. Modern C++ pushes you toward Rule of Zero everywhere.
Why this matters
Manual memory is what makes C++ fast and predictable - no GC pauses, no runtime overhead, total control over layout and lifetime. RAII is what makes it safe to use that power: by binding every resource to an object's deterministic lifetime, leaks and double-frees become structurally impossible in idiomatic code. Master RAII and you have mastered the soul of C++.
Reference
- RAII (cppreference): https://en.cppreference.com/w/cpp/language/raii
- Rule of three/five/zero: https://en.cppreference.com/w/cpp/language/rule_of_three
Next we build our own types with classes, then make them generic with templates.
Classes, Objects, and Polymorphism
Encapsulation, constructors, inheritance, and virtual functions.
Classes, Objects, and Polymorphism
C++ added object orientation to C, but it is optional and zero-overhead: a class with no virtual functions costs exactly what the equivalent C struct would. This lesson covers encapsulation with classes, construction and destruction, operator overloading, and runtime polymorphism through virtual functions.
Classes and encapsulation
A class bundles data (members) and behavior (methods) and controls access with public, private, and protected. Members are private by default; a struct is identical except members default to public.
#include <string>
#include <iostream>
class Account {
std::string owner; // private: hidden implementation detail
double balance = 0.0;
public:
Account(std::string name, double initial) // constructor
: owner(std::move(name)), balance(initial) {}
void deposit(double amount) { balance += amount; }
bool withdraw(double amount) {
if (amount > balance) return false;
balance -= amount;
return true;
}
double get_balance() const { return balance; } // const = does not modify
};
The : owner(...), balance(...) part is the member initializer list - it constructs members directly, which is more efficient than assigning to them inside the body. A method marked const (like get_balance) promises not to modify the object, so it can be called on const instances.
Constructors, destructors, and this
Constructors share the class name and have no return type; the destructor is ~ClassName(). Inside a method, this is a pointer to the current object:
class Counter {
int n = 0;
public:
Counter() { std::cout << "born\n"; } // default constructor
~Counter() { std::cout << "dies\n"; } // destructor (RAII!)
Counter& increment() { ++n; return *this; } // return *this to chain calls
int value() const { return n; }
};
Counter c;
c.increment().increment(); // chaining works because increment returns *this
Operator overloading
C++ lets you give operators meaning for your own types, so user-defined types feel built in:
struct Vec2 {
double x, y;
Vec2 operator+(const Vec2& o) const {
return Vec2{x + o.x, y + o.y};
}
bool operator==(const Vec2& o) const {
return x == o.x && y == o.y;
}
};
// free function for stream output
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << '(' << v.x << ", " << v.y << ')';
}
Vec2 a{1, 2}, b{3, 4};
std::cout << (a + b) << '\n'; // (4, 6)
This is exactly how std::string supports + and std::cout supports << - they are ordinary overloaded operators, not language magic.
Inheritance and virtual functions
A class can inherit from a base class, reusing and extending it. Runtime polymorphism comes from virtual functions: a call through a base pointer or reference dispatches to the actual derived type at run time, via a vtable.
#include <memory>
#include <vector>
class Shape {
public:
virtual double area() const = 0; // pure virtual -> abstract class
virtual ~Shape() = default; // virtual destructor: ESSENTIAL
};
class Circle : public Shape {
double r;
public:
explicit Circle(double r) : r(r) {}
double area() const override { return 3.14159265 * r * r; }
};
class Square : public Shape {
double s;
public:
explicit Square(double s) : s(s) {}
double area() const override { return s * s; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Square>(3.0));
for (const auto& sh : shapes) {
std::cout << sh->area() << '\n'; // virtual dispatch picks the right area()
}
}
Three things to internalize here:
= 0makes a function pure virtual, which makes the class abstract (you cannot instantiateShapedirectly) - C++'s version of an interface.overrideis not required but you should always write it: the compiler then verifies you really are overriding a base method, catching subtle signature mismatches.- A polymorphic base class must have a virtual destructor. Without it,
delete-ing a derived object through a base pointer is undefined behavior and leaks the derived part.
Static vs. dynamic dispatch, and the cost
A non-virtual call is resolved at compile time (static dispatch) and can be inlined - zero overhead. A virtual call costs one indirection through a vtable pointer. You pay for polymorphism only when you ask for it with virtual, which is the zero-overhead principle in action. Prefer composition and templates (next lesson) when you do not need runtime polymorphism.
Reference
- Classes (cppreference): https://en.cppreference.com/w/cpp/language/classes
- Virtual functions: https://en.cppreference.com/w/cpp/language/virtual
Next: templates and generic programming, C++'s compile-time polymorphism.
Templates and Generic Programming
Function and class templates, the engine behind the STL.
Templates and Generic Programming
Templates are C++'s mechanism for writing code that works for many types, resolved entirely at compile time. They are the foundation of the STL and the purest expression of zero-overhead abstraction: the compiler stamps out a specialized version for each type you use, so generic code is exactly as fast as hand-written type-specific code.
Function templates
Prefix a function with template <typename T> and use T as a stand-in type. The compiler instantiates a concrete version for each type you call it with:
template <typename T>
T max_of(T a, T b) {
return (a > b) ? a : b;
}
int main() {
auto i = max_of(3, 7); // T = int -> 7
auto d = max_of(2.5, 1.5); // T = double -> 2.5
auto s = max_of(std::string("apple"), std::string("pear")); // T = std::string
}
You usually do not write max_of<int>(...); the compiler deduces T from the arguments. Each distinct T generates its own machine code - this is monomorphization, and it means no runtime dispatch and full inlining.
Class templates
Types can be templated too. This is how every STL container works - std::vector<int>, std::vector<std::string>, and so on are instantiations of one class template:
template <typename T>
class Stack {
std::vector<T> data;
public:
void push(const T& value) { data.push_back(value); }
T pop() {
T top = data.back();
data.pop_back();
return top;
}
bool empty() const { return data.empty(); }
std::size_t size() const { return data.size(); }
};
int main() {
Stack<int> ints;
ints.push(1);
ints.push(2);
std::cout << ints.pop() << '\n'; // 2
Stack<std::string> words;
words.push("hello");
}
Since C++17, the compiler can often deduce class template arguments too (Class Template Argument Deduction), so std::pair p{1, 2.0}; works without spelling out <int, double>.
Multiple and non-type parameters
Templates can take several type parameters, and even compile-time values as parameters:
template <typename K, typename V>
struct Pair { K key; V value; };
template <typename T, std::size_t N> // N is a non-type (value) parameter
struct FixedArray {
T data[N];
constexpr std::size_t size() const { return N; }
};
FixedArray<double, 16> buffer; // a stack array of 16 doubles, size known at compile time
std::array<T, N> in the standard library is exactly this pattern.
Constraining templates: concepts (C++20)
Plain templates accept any type and fail with long error messages if the type lacks a required operation. Concepts (C++20) let you state the requirements up front, giving clear errors and self-documenting interfaces:
#include <concepts>
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T> // only accepts integral or floating-point types
T square(T x) { return x * x; }
square(5); // ok
square(2.5); // ok
// square("no"); // clear error: "no" does not satisfy Numeric
Before concepts, the same constraint required verbose SFINAE (std::enable_if) tricks; concepts make generic code dramatically more readable.
Variadic templates
A template can accept any number of arguments via a parameter pack (...). This powers utilities like std::make_unique and printf-style functions:
template <typename... Args>
void print_all(const Args&... args) {
((std::cout << args << ' '), ...); // C++17 fold expression
std::cout << '\n';
}
print_all(1, "two", 3.0, 'x'); // 1 two 3 x
The (... , ) is a fold expression that expands the pack, applying the operation to each element.
Templates vs. inheritance
Both achieve polymorphism, but at different times. Templates give compile-time (static) polymorphism: zero runtime cost, full inlining, but code bloat and longer compiles. Virtual functions give run-time (dynamic) polymorphism: one vtable indirection, but a single binary and the ability to choose behavior at run time. Reach for templates when the type set is known at compile time (the STL's choice), and virtual functions when you need heterogeneous objects behind one interface at run time.
Reference
- Templates (cppreference): https://en.cppreference.com/w/cpp/language/templates
- Constraints and concepts: https://en.cppreference.com/w/cpp/language/constraints
Next we put templates to work via the Standard Template Library: containers, iterators, and algorithms.
The STL: Containers, Iterators, and Algorithms
vector, map, and the iterator-driven algorithm library.
The STL: Containers, Iterators, and Algorithms
The Standard Template Library is C++'s crown jewel: a set of generic, container-independent algorithms that operate on containers through a common abstraction called iterators. Designed by Alexander Stepanov, it is the practical payoff of templates - reusable, type-safe, and as fast as raw loops. You will use it in nearly every program.
Containers
The STL containers are RAII types that own their storage and free it automatically (Rule of Zero in action). The ones you reach for most:
#include <vector>
#include <string>
#include <map>
#include <unordered_map>
#include <set>
std::vector<int> v = {1, 2, 3}; // dynamic array (your default container)
v.push_back(4);
std::string s = "hello"; // really a vector<char> with extras
std::map<std::string, int> ordered; // sorted (red-black tree), O(log n) lookup
std::unordered_map<std::string, int> fast; // hash table, average O(1) lookup
std::set<int> uniques = {3, 1, 2}; // sorted unique values
Rules of thumb: use std::vector by default (contiguous, cache-friendly, fast); std::unordered_map for fast key lookups; std::map/std::set when you need keys kept in sorted order. All grow automatically and release their memory in their destructors - no manual delete.
Element access and the entry pattern
#include <unordered_map>
#include <string>
std::unordered_map<std::string, int> counts;
std::string text = "the cat the dog the bird";
std::size_t pos = 0, next;
// operator[] inserts a default-constructed (0) value if the key is missing:
for (const auto& word : {"the", "cat", "the", "dog", "the"}) {
counts[word]++; // classic word counter
}
std::cout << counts["the"] << '\n'; // 3
if (auto it = counts.find("cat"); it != counts.end()) { // C++17 if-with-init
std::cout << "cat seen " << it->second << " times\n";
}
counts[key] inserts a default if absent (convenient, but it mutates); use .find() or .contains() (C++20) when you only want to check.
Iterators: the universal abstraction
An iterator is a generalization of a pointer: begin() points at the first element, end() points one past the last, ++it advances, *it dereferences. Every container exposes the same interface, which is what lets one algorithm work on all of them.
std::vector<int> v = {10, 20, 30};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << ' '; // 10 20 30
}
// the range-based for loop is just sugar over begin()/end():
for (int x : v) { std::cout << x << ' '; }
Algorithms
<algorithm> and <numeric> provide dozens of generic functions that take iterator ranges. Because they are decoupled from containers, the same std::sort sorts a vector, a deque, or a slice of an array:
#include <algorithm>
#include <numeric>
#include <vector>
std::vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end()); // 1 2 3 5 8 9
bool has8 = std::binary_search(v.begin(), v.end(), 8);
int sum = std::accumulate(v.begin(), v.end(), 0); // 28
auto it = std::find(v.begin(), v.end(), 5);
int evens = std::count_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
The [](int x){ ... } is a lambda - an anonymous function you pass to algorithms as the predicate or transformation. Lambdas can capture surrounding variables ([factor](int x){ return x * factor; }), making the STL expressive without naming a function for every step.
Transforming data
#include <algorithm>
#include <vector>
std::vector<int> nums = {1, 2, 3, 4, 5};
std::vector<int> squares(nums.size());
std::transform(nums.begin(), nums.end(), squares.begin(),
[](int x) { return x * x; }); // 1 4 9 16 25
// remove-erase idiom: drop all odd numbers
nums.erase(std::remove_if(nums.begin(), nums.end(),
[](int x) { return x % 2 != 0; }),
nums.end()); // nums is now {2, 4}
Ranges (C++20): a cleaner pipeline
C++20 ranges let you compose operations as a pipeline and pass containers directly, no begin()/end() pairs:
#include <ranges>
#include <vector>
std::vector<int> v = {1, 2, 3, 4, 5, 6};
auto evens_squared = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : evens_squared) std::cout << x << ' '; // 4 16 36
Views are lazy (they compute on demand, allocating nothing in between), echoing the iterator pipelines of other modern languages while keeping C++'s performance.
Reference
- Containers library: https://en.cppreference.com/w/cpp/container
- Algorithms library: https://en.cppreference.com/w/cpp/algorithm
Next: smart pointers and move semantics - how modern C++ manages ownership without manual delete.
Smart Pointers, Move Semantics, and Tooling
unique_ptr, shared_ptr, std::move, and the modern C++ toolchain.
Smart Pointers, Move Semantics, and Tooling
This final lesson covers how idiomatic modern C++ owns heap memory without ever writing delete - through smart pointers - and how move semantics let you transfer resources cheaply. These features, plus the build and analysis tooling, are what make large C++ codebases manageable and safe.
The problem smart pointers solve
Raw new/delete (Lesson 2) leak on early returns and crash on double-frees. Smart pointers are RAII wrappers around a heap pointer: they own the object and delete it automatically in their destructor. They live in <memory>.
unique_ptr: exclusive ownership
std::unique_ptr<T> owns its object exclusively and frees it when it goes out of scope. It is move-only - it cannot be copied, which enforces single ownership at compile time. Create one with std::make_unique:
#include <memory>
struct Widget {
Widget() { std::cout << "made\n"; }
~Widget() { std::cout << "freed\n"; } // runs automatically
void use() { std::cout << "using\n"; }
};
void demo() {
auto w = std::make_unique<Widget>(); // heap-allocated, owned by w
w->use();
// no delete needed - ~Widget() runs when w leaves scope,
// even if an exception is thrown above.
}
unique_ptr has zero overhead - it is exactly the size of a raw pointer and compiles to the same code, plus the guaranteed cleanup. It should be your default for owning heap objects.
shared_ptr: shared ownership
When several owners must keep an object alive until the last one is done, use std::shared_ptr<T>. It maintains a reference count; the object is destroyed when the count hits zero:
#include <memory>
auto a = std::make_shared<Widget>(); // ref count = 1
{
auto b = a; // ref count = 2 (shared)
} // b gone, count = 1
// count hits 0 and Widget is freed when a also goes away
shared_ptr is not free - it carries an atomic reference count and a control block - so prefer unique_ptr and reach for shared_ptr only when ownership is genuinely shared. For cycles (A holds B, B holds A) use std::weak_ptr to break the cycle, since two shared_ptrs pointing at each other would never reach count zero and would leak.
Move semantics
Copying a large object (a million-element vector, say) is expensive. Move semantics (C++11) let you transfer a resource - steal its internal buffer - instead of copying it, leaving the source in a valid empty state. This is what makes unique_ptr transferable and vector returns cheap.
#include <vector>
#include <utility>
std::vector<int> make_big() {
std::vector<int> v(1'000'000, 7);
return v; // moved out, not copied (also covered by RVO)
}
std::vector<int> a = make_big();
std::vector<int> b = std::move(a); // steal a's buffer; a is now empty
// using a's contents now is valid but a is empty
std::move does not move anything itself - it casts a value to an rvalue reference (T&&), signaling "you may pillage this object." The compiler then selects the move constructor/move assignment (two of the Rule of Five members), which typically just swaps pointers - O(1) instead of O(n). Returning a local by value, as in make_big, is automatically moved (or elided entirely by Return Value Optimization).
Putting ownership together
The modern C++ ownership model, in one example:
#include <memory>
#include <vector>
class Scene {
std::vector<std::unique_ptr<Shape>> objects; // Scene exclusively owns its shapes
public:
void add(std::unique_ptr<Shape> s) {
objects.push_back(std::move(s)); // transfer ownership into the vector
}
double total_area() const {
double sum = 0;
for (const auto& obj : objects) sum += obj->area();
return sum;
}
}; // when a Scene is destroyed, the vector frees every Shape automatically
No new, no delete, no leaks, no manual lifetime tracking - just RAII types owning each other in a clear hierarchy. This is the Rule of Zero realized.
Build tooling
Real projects do not call g++ by hand. CMake is the de facto build system; it generates native build files for your platform:
cmake_minimum_required(VERSION 3.20)
project(myapp CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(myapp src/main.cpp)
$ cmake -S . -B build
$ cmake --build build
$ ./build/myapp
Dependencies are managed by package managers like vcpkg or Conan, since C++ has no single standard registry the way some languages do.
Analysis and sanitizers
C++'s manual memory makes tooling essential, and the ecosystem is excellent:
# AddressSanitizer: catches use-after-free, buffer overflows, leaks at runtime
$ g++ -std=c++20 -fsanitize=address -g main.cpp -o app && ./app
# UndefinedBehaviorSanitizer
$ g++ -std=c++20 -fsanitize=undefined -g main.cpp -o app
# Valgrind: runtime memory error and leak detection
$ valgrind --leak-check=full ./app
# clang-tidy: static analysis and modernization lints
$ clang-tidy main.cpp -- -std=c++20
# clang-format: canonical formatting
$ clang-format -i src/*.cpp
Run your tests under AddressSanitizer - it catches the exact bugs (use-after-free, leaks, overflows) that manual memory management risks, usually pointing straight at the line. Combined with smart pointers and RAII, sanitizers make modern C++ far safer than its reputation.
Reference
- Smart pointers (cppreference): https://en.cppreference.com/w/cpp/memory
- Move semantics / std::move: https://en.cppreference.com/w/cpp/utility/move
- CMake documentation: https://cmake.org/documentation/
You now have the full arc: setup, the manual-yet-RAII memory model, classes and polymorphism, templates, the STL, and modern ownership with smart pointers and moves. The best next step is to build something small with CMake, hold every resource in a smart pointer or container, and run it under AddressSanitizer.