00:00

C++ Tutorial

Last updated: 14 min read

C++ is a compiled, general-purpose programming language that combines the speed and hardware access of C with powerful tools for building large programs: classes, templates, a rich standard library and automatic resource management. It powers game engines, web browsers, databases, trading systems, compilers, embedded devices and much of the software where performance really matters.

This tutorial teaches modern C++ (C++17, C++20 and C++23) from the ground up. You will learn how a C++ program is built, the core language, references and const, classes and RAII, inheritance and polymorphism, templates and concepts, the Standard Library containers and algorithms, smart pointers, move semantics, error handling and concurrency. Each section includes small programs you can compile and run.

1. What Is C++?

C++ is a multi-paradigm language. You can write simple procedural code as in C, design programs around classes and objects, write generic code that works with any type through templates, and use a functional style with lambdas and ranges, all in the same program.

Its guiding idea is zero-overhead abstraction: features you do not use cost nothing, and features you do use should be as efficient as code you could write by hand. A std::vector is as fast as a manually managed array, and a lambda passed to std::sort is typically inlined so there is no function-call overhead.

C++ is used wherever performance, low latency or control over hardware is essential:

  • Games and graphics: major game engines, 3D tools and graphics drivers.
  • Browsers and desktop software: web browser engines, office suites and creative tools.
  • Databases and infrastructure: database engines, search engines and high-performance servers.
  • Finance: low-latency trading platforms where microseconds matter.
  • Embedded and automotive: real-time control systems, robotics and in-vehicle software.
  • Machine learning: the fast core of popular ML frameworks, which Python code calls into.

2. A Short History of C++

Bjarne Stroustrup began creating C++ at Bell Labs in 1979. He wanted the efficiency of C together with the program-organisation features of the Simula language. The language was first called "C with Classes" and was renamed C++ in 1983; ++ is C's increment operator, so the name means "one step beyond C".

YearMilestone
1979Work on "C with Classes" began at Bell Labs.
1983The language was renamed C++.
1985The first commercial release and the first edition of The C++ Programming Language.
1998C++98, the first ISO standard, including the Standard Template Library (STL).
2011C++11, the start of "modern C++": auto, lambdas, move semantics, smart pointers, range-based for, nullptr and threads.
2014C++14 refined C++11 with generic lambdas and std::make_unique.
2017C++17 added structured bindings, std::optional, std::variant, std::string_view, std::filesystem and if constexpr.
2020C++20 brought concepts, ranges, coroutines, modules, the <=> operator and std::format.
2023C++23 added std::print, std::expected, "deducing this" and the standard library module import std;.

A new standard now appears every three years. The next one, C++26, is set to add static reflection, contracts and the std::execution framework for asynchronous code.

3. Key Features of C++

FeatureWhat it means for you
PerformanceCompiled to native machine code with no virtual machine or garbage collector, giving fast and predictable execution.
Object-oriented programmingClasses, encapsulation, inheritance and polymorphism for modelling complex systems.
Generic programmingTemplates and concepts let you write code once that works efficiently with many types.
RAIIResources such as memory, files and locks are released automatically when objects go out of scope.
Standard LibraryReady-made containers, algorithms, strings, I/O, threads, time and file-system utilities.
C compatibilityMost C code and libraries can be used directly from C++.
Low-level controlPointers, custom memory allocation and direct hardware access when you need them.
Strong static typingMany errors are caught at compile time, and constexpr can even run code during compilation.

4. Setting Up a C++ Compiler

The three major compilers are GCC (g++), Clang (clang++) and Microsoft Visual C++ (MSVC). All are free.

  • Linux: install GCC with your package manager, for example sudo apt install build-essential.
  • macOS: run xcode-select --install to get Clang.
  • Windows: install Visual Studio Community with the "Desktop development with C++" workload, or use GCC through MSYS2 or WSL.

Popular editors and IDEs include Visual Studio, CLion, Visual Studio Code with the C/C++ extension, and Qt Creator. Check your compiler with g++ --version or clang++ --version; for the examples here, a compiler with good C++20 support is recommended.

5. Your First C++ Program

hello.cpp
#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::cout << "What is your name? ";
    std::getline(std::cin, name);

    std::cout << "Hello, " << name << "! Welcome to C++.\n";
    return 0;
}
terminal
# Compile with the C++20 standard and useful warnings, then run
g++ -std=c++20 -Wall -Wextra -o hello hello.cpp
./hello
  • #include <iostream> brings in the input/output streams std::cout and std::cin.
  • std:: is the namespace of the Standard Library. Namespaces prevent name clashes between libraries.
  • << sends values to an output stream, and std::getline reads a whole line of input into a std::string.
  • main returns 0 to report success. Like C, a C++ program starts by running main.
C++23 tip: compilers with C++23 support offer std::print and std::println from <print>, for example std::println("Hello, {}!", name);. They are type-safe like streams but use a compact format string like Python's. If your compiler does not support them yet, std::format from C++20 works similarly with std::cout.

6. Variables, Types and auto

C++ shares C's fundamental types (bool, char, int, long long, double and so on) and adds Standard Library types such as std::string. Use brace initialisation, which refuses narrowing conversions that would lose data, and auto to let the compiler deduce a type from its initial value.

basics.cpp
#include <iostream>
#include <string>
#include <vector>
#include <map>

int main()
{
    int quantity{3};
    double unit_price{249.5};
    // int bad{9.99};             // compile error: narrowing double to int
    std::string product{"Notebook"};

    auto total = quantity * unit_price;          // deduced as double
    auto tags = std::vector<std::string>{"paper", "stationery"};

    std::cout << product << " x" << quantity << " = " << total << '\n';

    // Range-based for loop over a container
    for (const auto& tag : tags) {
        std::cout << "#" << tag << ' ';
    }
    std::cout << '\n';

    // Structured bindings (C++17) unpack pairs and structs
    std::map<std::string, int> stock{{"pen", 40}, {"eraser", 15}};
    for (const auto& [item, count] : stock) {
        std::cout << item << ": " << count << '\n';
    }
}

Use auto when the type is obvious from the right-hand side or long to write, such as iterator types. Write the type explicitly when it documents an important decision, such as a specific integer width.

7. References, const and constexpr

A reference is another name for an existing object. Unlike a pointer, it must be initialised, cannot be null and cannot be changed to refer to something else. References are the usual way to pass objects to functions without copying them.

references.cpp
#include <iostream>
#include <string>
#include <vector>

// Pass by const reference: no copy, and the function promises not to modify it
double average(const std::vector<int>& values)
{
    if (values.empty()) return 0.0;
    long sum = 0;
    for (int v : values) sum += v;
    return static_cast<double>(sum) / values.size();
}

// Pass by non-const reference: the function changes the caller's object
void add_bonus(std::vector<int>& values, int bonus)
{
    for (int& v : values) v += bonus;
}

// constexpr: can be evaluated at compile time
constexpr long long factorial(int n)
{
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main()
{
    std::vector<int> marks{70, 82, 91};
    add_bonus(marks, 5);
    std::cout << "average = " << average(marks) << '\n';

    constexpr auto f10 = factorial(10);        // computed by the compiler
    static_assert(f10 == 3628800);             // checked at compile time
    std::cout << "10! = " << f10 << '\n';

    const std::string site{"javabytechie"};
    // site += ".com";                         // compile error: site is const
}

A simple rule for parameters: pass cheap types such as int and double by value, pass larger objects you only read by const&, and use a non-const reference only when the function must modify the caller's object. Mark everything that should not change as const; it documents intent and lets the compiler catch mistakes.

8. Functions and Lambdas

C++ functions can be overloaded (several functions with the same name but different parameters), can have default arguments and can be defined inline as lambdas: unnamed functions you can store in variables or pass to algorithms.

functions.cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

// Overloading: the compiler picks the version that matches the arguments
double area(double radius)                { return 3.141592653589793 * radius * radius; }
double area(double width, double height)  { return width * height; }

// Default argument
std::string greet(const std::string& name, const std::string& greeting = "Hello")
{
    return greeting + ", " + name + "!";
}

int main()
{
    std::cout << area(2.0) << ' ' << area(3.0, 4.0) << '\n';
    std::cout << greet("Anil") << ' ' << greet("Anil", "Welcome") << '\n';

    std::vector<int> numbers{14, 3, 27, 8, 19, 6};
    int threshold = 10;

    // A lambda: [captures](parameters) { body }
    auto above = [threshold](int n) { return n > threshold; };
    auto count = std::count_if(numbers.begin(), numbers.end(), above);
    std::cout << count << " numbers are above " << threshold << '\n';

    // Sort in descending order with an inline comparison lambda
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; });
    for (int n : numbers) std::cout << n << ' ';
    std::cout << '\n';
}

The capture list in square brackets controls which outside variables a lambda can use: [threshold] copies the value, [&total] captures by reference, [=] copies everything used and [&] references everything used. Prefer listing captures explicitly so it is clear what the lambda depends on.

9. Classes and Objects

A class bundles data (member variables) with the functions that operate on it (member functions), and controls access with public, private and protected. The struct keyword creates the same thing, except that members are public by default; by convention, struct is used for simple data groupings.

bank_account.cpp
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>

class BankAccount {
public:
    // Constructor with a member initialiser list
    BankAccount(std::string owner, double opening_balance)
        : owner_{std::move(owner)}, balance_{opening_balance}
    {
        if (opening_balance < 0) {
            throw std::invalid_argument("Opening balance cannot be negative");
        }
    }

    void deposit(double amount)
    {
        if (amount <= 0) throw std::invalid_argument("Deposit must be positive");
        balance_ += amount;
    }

    bool withdraw(double amount)
    {
        if (amount > balance_) return false;    // invariant: balance never negative
        balance_ -= amount;
        return true;
    }

    // const member functions promise not to change the object
    double balance() const { return balance_; }
    const std::string& owner() const { return owner_; }

private:
    std::string owner_;
    double balance_;
};

int main()
{
    BankAccount account{"Priya", 1000.0};
    account.deposit(250.0);

    if (!account.withdraw(5000.0)) {
        std::cout << "Insufficient funds\n";
    }
    std::cout << account.owner() << " has " << account.balance() << '\n';
}

Keeping data private and changing it only through member functions lets the class protect its invariants, here that the balance is never negative. Initialise members in the member initialiser list (after the colon) rather than by assignment in the constructor body; it is more efficient and required for const and reference members.

10. RAII and Object Lifetime

RAII (Resource Acquisition Is Initialisation) is the most important idea in C++. A resource, such as memory, a file, a network socket or a mutex lock, is acquired in an object's constructor and released in its destructor. Because C++ calls destructors automatically and deterministically when objects go out of scope, even when an exception is thrown, resources can never leak.

raii.cpp
#include <fstream>
#include <iostream>
#include <string>

class Timer {
public:
    explicit Timer(std::string label) : label_{std::move(label)}
    {
        std::cout << "[start] " << label_ << '\n';
    }
    ~Timer()                                   // destructor: runs automatically
    {
        std::cout << "[end]   " << label_ << '\n';
    }
private:
    std::string label_;
};

int main()
{
    {
        Timer t{"writing report"};
        std::ofstream out{"report.txt"};       // file opened by the constructor
        out << "Quarterly sales: 1,250 units\n";
    }   // out is closed, then t prints "[end]", in reverse order of creation

    std::ifstream in{"report.txt"};
    std::string line;
    while (std::getline(in, line)) {
        std::cout << "read: " << line << '\n';
    }
}   // in is closed automatically here

The Standard Library is built on RAII: std::vector and std::string free their memory, std::fstream closes files, std::unique_ptr deletes objects and std::lock_guard unlocks mutexes. In well-written modern C++, you almost never write delete or call a manual "close" function.

The rule of zero: if your class holds only members that already manage themselves (strings, containers, smart pointers), do not write a destructor, copy operations or move operations at all; the compiler-generated ones are correct. If you must manage a raw resource yourself, follow the rule of five and define or delete all five special members: destructor, copy constructor, copy assignment, move constructor and move assignment.

11. Inheritance and Polymorphism

A class can inherit from a base class, reusing its interface and behaviour. Marking a member function virtual enables runtime polymorphism: calling it through a base-class pointer or reference runs the derived class's version.

shapes.cpp
#include <iostream>
#include <memory>
#include <numbers>
#include <string>
#include <vector>

class Shape {
public:
    virtual ~Shape() = default;                // virtual destructor for safe deletion
    virtual double area() const = 0;           // pure virtual: makes Shape abstract
    virtual std::string name() const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_{r} {}
    double area() const override { return std::numbers::pi * r_ * r_; }
    std::string name() const override { return "Circle"; }
private:
    double r_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : w_{w}, h_{h} {}
    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
private:
    double w_, h_;
};

int main()
{
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.5));
    shapes.push_back(std::make_unique<Rectangle>(2.0, 3.5));

    double total = 0;
    for (const auto& s : shapes) {             // each call dispatches to the real type
        std::cout << s->name() << ": " << s->area() << '\n';
        total += s->area();
    }
    std::cout << "Total area: " << total << '\n';
}   // unique_ptr deletes every shape automatically
How a virtual function call is dispatched A Shape pointer points to a Circle object. The object contains a hidden pointer to the Circle virtual table, whose entries point to Circle::area and Circle::name. Calling area through the pointer follows this table to run Circle::area. Shape* s Circle object hidden vptr r_ = 1.5 Circle vtable area name Circle::area Circle::name s->area() → follow vptr → look up "area" in the table → call Circle::area
Figure 1: Virtual calls go through a small table of function pointers, which is how the right override is chosen at run time.

Key rules for safe inheritance:

  • Give every polymorphic base class a virtual destructor, so deleting a derived object through a base pointer cleans up correctly.
  • Mark overriding functions with override. The compiler then reports an error if the signature does not actually match a base function.
  • Use final to prevent further overriding or inheritance where it does not make sense.
  • Prefer composition (a class containing another as a member) over inheritance when there is no true "is-a" relationship.

12. Templates and Concepts

Templates let you write a function or class once and use it with many types. The compiler generates a specialised version for each type you use, so generic code is as fast as hand-written code. The whole Standard Library, from std::vector<T> to std::sort, is built on templates.

C++20 concepts state what a template requires of its types. They make templates easier to read and give much clearer error messages when a type does not fit.

templates.cpp
#include <concepts>
#include <iostream>
#include <string>
#include <vector>

// A concept: any type that supports + and can be default-constructed
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
} && std::default_initializable<T>;

// A constrained function template
template <Addable T>
T sum(const std::vector<T>& items)
{
    T total{};
    for (const auto& item : items) total = total + item;
    return total;
}

// A class template
template <typename T>
class Box {
public:
    explicit Box(T value) : value_{std::move(value)} {}
    const T& get() const { return value_; }
private:
    T value_;
};

int main()
{
    std::cout << sum(std::vector<int>{1, 2, 3, 4}) << '\n';                 // 10
    std::cout << sum(std::vector<double>{0.5, 1.25}) << '\n';                // 1.75
    std::cout << sum(std::vector<std::string>{"mod", "ern"}) << '\n';       // modern

    Box intBox{42};                         // template argument deduced as int
    Box textBox{std::string{"C++20"}};
    std::cout << intBox.get() << ' ' << textBox.get() << '\n';
}

13. The Standard Library: Containers and Algorithms

The Standard Library is organised around three ideas that work together: containers store data, iterators point to elements, and algorithms process ranges of elements through iterators. Because algorithms only depend on iterators, one std::sort works with arrays, vectors and deques alike.

Containers, iterators and algorithms Containers such as vector, map and unordered_map provide iterators. Algorithms such as sort, find and transform work on any container through those iterators. Containers vector · deque · list map · set · unordered_map array · string Iterators begin() … end() Algorithms sort · find · count_if transform · accumulate remove_if · binary_search
Figure 2: Iterators connect any container to any compatible algorithm.
ContainerDescriptionUse it for
std::vectorDynamic array stored in one contiguous block.The default choice for a sequence of elements.
std::arrayFixed-size array with a container interface.Small collections whose size is known at compile time.
std::stringDynamic sequence of characters.Text.
std::dequeDouble-ended queue.Fast insertion and removal at both ends.
std::map / std::setSorted balanced trees; O(log n) operations.Keys kept in order, range queries.
std::unordered_map / std::unordered_setHash tables; average O(1) operations.Fast lookup by key when order does not matter.
stl.cpp
#include <algorithm>
#include <iostream>
#include <numeric>
#include <ranges>
#include <string>
#include <unordered_map>
#include <vector>

struct Order {
    std::string customer;
    double amount;
};

int main()
{
    std::vector<Order> orders{
        {"Asha", 1200.0}, {"Ben", 350.0}, {"Asha", 640.0},
        {"Chen", 2100.0}, {"Ben", 90.0}
    };

    // Total revenue with std::accumulate
    double revenue = std::accumulate(orders.begin(), orders.end(), 0.0,
        [](double sum, const Order& o) { return sum + o.amount; });
    std::cout << "Revenue: " << revenue << '\n';

    // Group totals per customer with a hash map
    std::unordered_map<std::string, double> per_customer;
    for (const auto& o : orders) per_customer[o.customer] += o.amount;
    for (const auto& [name, total] : per_customer) {
        std::cout << name << " spent " << total << '\n';
    }

    // Sort by amount, largest first (C++20 ranges algorithm with a projection)
    std::ranges::sort(orders, std::greater{}, &Order::amount);
    std::cout << "Biggest order: " << orders.front().customer << '\n';

    // Ranges views: lazily filter and transform without temporary vectors
    auto large = orders
               | std::views::filter([](const Order& o) { return o.amount >= 500; })
               | std::views::transform(&Order::customer);
    std::cout << "Large orders from:";
    for (const auto& name : large) std::cout << ' ' << name;
    std::cout << '\n';
}

Before writing a loop, check whether an algorithm already does the job. Named algorithms such as count_if, find_if and any_of make code shorter, clearer and less error-prone than hand-written loops.

14. Smart Pointers and Memory

Objects in C++ can live on the stack (automatic storage, destroyed at the end of the scope) or on the heap (dynamic storage, created when you need it). Prefer stack objects and containers. When you do need heap objects, for example for polymorphism or shared ownership, use smart pointers from <memory> instead of raw new and delete.

Smart pointerOwnershipUse it for
std::unique_ptr<T>Exactly one owner; can be moved but not copied. No overhead compared with a raw pointer.The default choice for heap objects.
std::shared_ptr<T>Shared by several owners; a reference count deletes the object when the last owner goes away.Objects with genuinely shared lifetimes.
std::weak_ptr<T>Non-owning observer of a shared_ptr object.Caches and breaking reference cycles.
smart_pointers.cpp
#include <iostream>
#include <memory>
#include <string>

struct Document {
    std::string title;
    explicit Document(std::string t) : title{std::move(t)} { std::cout << "open  " << title << '\n'; }
    ~Document() { std::cout << "close " << title << '\n'; }
};

std::unique_ptr<Document> create(const std::string& title)
{
    return std::make_unique<Document>(title);   // ownership moves to the caller
}

int main()
{
    auto draft = create("draft.txt");
    auto moved = std::move(draft);               // unique_ptr: move, never copy
    std::cout << "draft is " << (draft ? "set" : "empty") << '\n';

    auto shared = std::make_shared<Document>("report.pdf");
    {
        auto another_owner = shared;             // reference count becomes 2
        std::cout << "owners: " << shared.use_count() << '\n';
    }                                            // back to 1
    std::cout << "owners: " << shared.use_count() << '\n';
}   // both documents are closed automatically

Create smart pointers with std::make_unique and std::make_shared. Raw pointers and references are still fine for non-owning access, such as passing an object to a function that only uses it during the call.

15. Move Semantics

Copying a large object, such as a vector of a million elements, is expensive. C++11 introduced move semantics: when the source object is about to disappear, or you explicitly say you no longer need it, its internal resources can be transferred instead of copied. Moving a vector just hands over its internal pointer, which takes constant time no matter how many elements it holds.

move.cpp
#include <iostream>
#include <string>
#include <utility>
#include <vector>

std::vector<int> make_readings()
{
    std::vector<int> data(1'000'000, 7);
    return data;                       // moved (or elided entirely), never deep-copied
}

int main()
{
    auto readings = make_readings();

    std::vector<std::string> names;
    std::string name = "a fairly long customer name that would be costly to copy";
    names.push_back(std::move(name));  // transfer the characters into the vector
    // name is now in a valid but unspecified state: assign to it before reusing it

    std::vector<int> archive = std::move(readings);
    std::cout << "archive: " << archive.size() << ", readings: " << readings.size() << '\n';
}

An expression that refers to a temporary or an object marked with std::move is an rvalue, and it selects the move constructor (T(T&&)) instead of the copy constructor. Standard Library types all support moving, and returning local objects by value is efficient thanks to moves and copy elision, so you can write natural code that returns objects without worrying about copies.

16. Error Handling

C++ offers several ways to report errors, each suited to different situations:

  • Exceptions (throw, try, catch) for unexpected failures that the immediate caller usually cannot handle, such as a missing configuration file.
  • std::optional<T> (C++17) when a value may simply be absent, such as a search with no result.
  • std::expected<T, E> (C++23) when a function returns either a value or an error that the caller is expected to handle.
errors.cpp
#include <charconv>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

// optional: "maybe a value"
std::optional<int> parse_int(std::string_view text)
{
    int value{};
    auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
    if (ec != std::errc{} || ptr != text.data() + text.size()) return std::nullopt;
    return value;
}

// exceptions: report a broken precondition
double safe_divide(double a, double b)
{
    if (b == 0.0) throw std::domain_error("division by zero");
    return a / b;
}

int main()
{
    for (std::string_view input : {"42", "4x2"}) {
        if (auto n = parse_int(input)) {
            std::cout << input << " -> " << *n << '\n';
        } else {
            std::cout << input << " is not a number\n";
        }
    }

    try {
        std::cout << safe_divide(10, 4) << '\n';
        std::cout << safe_divide(1, 0) << '\n';
    } catch (const std::exception& e) {          // catch by const reference
        std::cerr << "Error: " << e.what() << '\n';
    }
}

Exceptions work hand in hand with RAII: when one is thrown, destructors of all objects in the scopes being left run automatically, so files close and memory is freed on the error path too. Mark functions that will never throw as noexcept, especially move constructors, which lets containers such as std::vector use faster moves when they grow.

17. Concurrency

Since C++11 the language has a memory model and standard threading tools: std::thread and C++20's std::jthread (which joins automatically), std::mutex and lock guards, condition variables, std::atomic, and std::async with futures for running tasks and collecting their results.

concurrency.cpp
#include <future>
#include <iostream>
#include <mutex>
#include <numeric>
#include <thread>
#include <vector>

int main()
{
    // 1) Shared counter protected by a mutex
    int hits = 0;
    std::mutex m;
    {
        std::vector<std::jthread> workers;
        for (int t = 0; t < 4; ++t) {
            workers.emplace_back([&] {
                for (int i = 0; i < 10'000; ++i) {
                    std::lock_guard lock{m};          // unlocked automatically (RAII)
                    ++hits;
                }
            });
        }
    }   // jthreads join here
    std::cout << "hits = " << hits << '\n';        // always 40000

    // 2) Run two halves of a sum in parallel with std::async
    std::vector<long long> values(2'000'000);
    std::iota(values.begin(), values.end(), 1);
    auto mid = values.begin() + values.size() / 2;

    auto first_half = std::async(std::launch::async, [&] {
        return std::accumulate(values.begin(), mid, 0LL);
    });
    long long second_half = std::accumulate(mid, values.end(), 0LL);
    std::cout << "sum = " << first_half.get() + second_half << '\n';
}

Two threads accessing the same data at the same time, where at least one writes, without synchronisation is a data race and causes undefined behaviour. Protect shared data with a mutex or use std::atomic, and test multithreaded code with ThreadSanitizer (-fsanitize=thread).

18. Organising and Building Projects

Larger programs are split into header files (.h or .hpp) with declarations and source files (.cpp) with definitions, just as in C. C++20 modules (export module and import) are a newer, faster alternative to headers, and support for them in compilers and build tools keeps improving.

Most C++ projects are built with CMake, which generates build files for Make, Ninja, Visual Studio and Xcode from one description:

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(inventory LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(inventory
    src/main.cpp
    src/product.cpp
)
target_include_directories(inventory PRIVATE include)

if(NOT MSVC)
    target_compile_options(inventory PRIVATE -Wall -Wextra -Wpedantic)
endif()
terminal
# Configure into a separate build folder, then compile
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/inventory

To use third-party libraries, package managers such as vcpkg and Conan download and build them and integrate with CMake. For testing, frameworks such as GoogleTest and Catch2 are widely used.

19. C++ vs. C vs. Java

AspectC++CJava
ParadigmsProcedural, object-oriented, generic, functionalProceduralObject-oriented with functional features
ExecutionNative machine codeNative machine codeBytecode on the JVM with JIT compilation
MemoryRAII and smart pointers; manual when neededManual malloc/freeGarbage collected
GenericsTemplates generate specialised code at compile timeNone (macros or void*)Generics with type erasure
InheritanceMultiple inheritance supportedNoneSingle class inheritance, multiple interfaces
Operator overloadingYesNoNo
DestructorsDeterministic, at end of scopeNoneNo destructors; try-with-resources for cleanup
PortabilityRecompile per platformRecompile per platformSame bytecode runs on any JVM

If you already know Java, C++'s syntax will feel familiar, but its object model is different: objects are values by default (assignment copies), lifetimes are deterministic, and you choose explicitly between values, references and pointers. Revisit the Java tutorial or the C tutorial to compare the three languages side by side.

20. Best Practices and Common Mistakes

Best practices

  • Write modern C++: target C++17 or later and enable compiler warnings (-Wall -Wextra, or /W4 on MSVC).
  • Use RAII for every resource, and let containers and smart pointers manage memory; avoid naked new and delete.
  • Prefer std::vector and std::string over raw arrays and C strings.
  • Be const-correct and pass large objects by const&.
  • Prefer Standard Library algorithms to hand-written loops.
  • Mark overrides with override and give polymorphic bases a virtual destructor.
  • Follow the rule of zero; only write special member functions when you manage a raw resource.
  • Test with sanitizers (-fsanitize=address,undefined) and use static analysis tools such as clang-tidy.

Common mistakes to avoid

MistakeWhat goes wrongFix
Using new without a matching deleteMemory leaks, especially when exceptions are thrown.Use std::make_unique or a container.
Missing virtual destructor in a base classDeleting through a base pointer skips derived cleanup.Add virtual ~Base() = default;.
Returning a reference to a local variableThe reference dangles once the function returns.Return by value.
Keeping iterators or references while a vector growsReallocation invalidates them.Re-acquire them after inserting, or reserve capacity first.
Using an object after std::moveIts contents are unspecified.Assign a new value before reusing it.
Passing large objects by value unintentionallyUnnecessary, expensive copies.Use const& for read-only parameters.
using namespace std; in header filesName clashes in every file that includes the header.Write std:: explicitly in headers.

21. Summary

  • C++ is a fast, compiled, multi-paradigm language built on the principle of zero-overhead abstraction.
  • Modern C++ (C++11 onwards) uses auto, lambdas, range-based loops, smart pointers and move semantics to write code that is both safe and fast.
  • References and const let you pass objects efficiently and state your intentions clearly.
  • Classes protect invariants; RAII ties every resource to an object's lifetime so nothing leaks.
  • Virtual functions provide runtime polymorphism; templates and concepts provide compile-time generic code.
  • The Standard Library connects containers and algorithms through iterators, with ranges making pipelines easy to write.
  • Use exceptions, std::optional and std::expected for errors, and mutexes or atomics to avoid data races.
  • Build projects with CMake, manage dependencies with vcpkg or Conan, and test with sanitizers.

A good next step is a small project that uses most of these ideas, such as an inventory manager that stores products in a std::vector, saves them to a file, sorts them with ranges and models product types with a class hierarchy. Then open the FAQ tab for answers to the questions learners most often ask about C++.