C++ • 2026-10-10

C++ Tutorial: Modern C++, STL Containers, References, and RAII

Explore modern C++ fundamentals: types, references, functions, STL containers and algorithms, lambdas, classes, RAII, smart pointers, templates, exceptions, and resource-safe programming.

Explore modern C++ fundamentals: types, references, functions, STL containers and algorithms, lambdas, classes, RAII, smart pointers, templates, exceptions, and resource-safe programming.

Developer workspace with code on a screen
Build your programming knowledge one practical example at a time.

What Is C++?

C++ is a compiled language used in games, desktop software, browsers, simulation, finance, embedded systems, and other performance-sensitive work. Modern C++ emphasizes standard-library tools, value semantics, and resource-management patterns that reduce manual memory handling.

What makes modern C++ different

  • Use the standard library (std::vector, std::string, algorithms) instead of hand-written low-level code.
  • Use RAII and smart pointers instead of manual new and delete.
  • Use **references and const** to avoid copies and communicate intent.
  • Use templates and lambdas for generic, reusable code.
  • Prefer the newest practical standard your compiler supports (C++17, C++20, or C++23).

Hello, world and compiling

#include <iostream>

int main() {
    std::cout << "Hello, C++!\n";
    return 0;
}

// Compile and run:
//   c++ -std=c++17 -Wall -Wextra -Wpedantic main.cpp -o main
//   ./main

Types, auto, and const

  • Fundamental types: int, double, bool, char, std::size_t.
  • auto lets the compiler deduce a type; use it when the type is obvious or very long.
  • Prefer const for values that should not change, and constexpr for compile-time constants.
  • Use brace initialization {} to avoid accidental narrowing conversions.
  • std::string and std::string_view replace most raw char* usage.

Types, auto, const, and brace initialization

#include <iostream>
#include <string>

int main() {
    const int year = 2026;
    constexpr double kPi = 3.14159;
    auto name = std::string{"Asha"};
    auto ratio = 3.0 / 4;          // double
    int count{5};                  // brace init

    // int bad{3.5};               // error: narrowing is rejected

    std::cout << name << " " << year << " " << ratio << " " << count << "\n";
    std::cout << "Pi ~ " << kPi << "\n";
}

Functions, References, and Pointers

  • Pass small values (like int) by value.
  • Pass large objects by const T& to avoid copying when the function only reads them.
  • Pass T& when the function must modify the caller's object.
  • A reference is an alias that must refer to an existing object and cannot be reseated.
  • A pointer can be null and can be reassigned; use it for optional or reseatable relationships.
  • Never return a reference or pointer to a local variable.

Pass by value, const reference, and reference

#include <iostream>
#include <string>
#include <vector>

int square(int x) { return x * x; }                       // by value

void print_all(const std::vector<std::string>& names) {   // read-only, no copy
    for (const auto& name : names) std::cout << name << "\n";
}

void add_exclaim(std::string& text) {                      // modifies caller's object
    text += "!";
}

int main() {
    std::string s = "Hello";
    add_exclaim(s);
    std::cout << s << " " << square(7) << "\n";
    print_all({"Asha", "Ravi"});
}

STL Containers

The standard library provides ready-made, well-tested containers. Choose based on ordering, lookup behavior, and cost of insertion.

  • **std::vector:** dynamic array; the default choice for sequences.
  • **std::array:** fixed-size array with known size at compile time.
  • **std::string:** dynamic text.
  • **std::map:** sorted key-value pairs with logarithmic lookup.
  • **std::unordered_map:** hash table with average constant-time lookup.
  • **std::set / std::unordered_set:** unique elements.
  • **std::deque, std::list:** specialized sequences when you need front insertion or stable iterators.

Using vector, map, and set

#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

int main() {
    std::vector<int> nums{5, 2, 9, 1};
    nums.push_back(7);
    for (int n : nums) std::cout << n << " ";
    std::cout << "\nsize = " << nums.size() << "\n";

    std::map<std::string, int> ages{{"Asha", 21}, {"Ravi", 19}};
    ages["Meera"] = 24;
    if (auto it = ages.find("Ravi"); it != ages.end()) {
        std::cout << it->first << " is " << it->second << "\n";
    }

    std::unordered_map<std::string, int> fast{{"a", 1}};
    std::set<int> unique{3, 1, 3, 2};          // {1, 2, 3}
    std::cout << unique.size() << "\n";
}

Algorithms, Iterators, and Lambdas

  • <algorithm> provides std::sort, std::find_if, std::count_if, std::transform, and more.
  • Iterators connect containers to algorithms.
  • Lambdas are inline anonymous functions: [captures](params) { body }.
  • Prefer [&] or explicit captures carefully; capturing by reference to something that dies first is a bug.
  • C++20 ranges provide a more readable pipeline style when available.

Sorting and searching with lambdas

#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people{{"Asha", 21}, {"Ravi", 19}, {"Meera", 24}};

    std::sort(people.begin(), people.end(),
              [](const Person& a, const Person& b) { return a.age < b.age; });

    auto adult = std::find_if(people.begin(), people.end(),
                              [](const Person& p) { return p.age >= 21; });
    if (adult != people.end()) std::cout << adult->name << "\n";

    int total = std::accumulate(people.begin(), people.end(), 0,
                                [](int sum, const Person& p) { return sum + p.age; });
    std::cout << "Average age: " << static_cast<double>(total) / people.size() << "\n";
}

Classes and Object-Oriented Design

  • A class bundles data (members) with behavior (member functions).
  • Use private data and a small public interface.
  • Constructors establish a valid object; use member initializer lists.
  • Mark functions that do not modify the object const.
  • Rule of zero: let standard types manage resources so you need no custom destructor, copy, or move.
  • Use virtual functions and override for runtime polymorphism; give polymorphic base classes a virtual destructor.

A class with const member functions and an interface

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class BankAccount {
public:
    explicit BankAccount(std::string owner, double balance = 0.0)
        : owner_{std::move(owner)}, balance_{balance} {}

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

    double balance() const { return balance_; }
    const std::string& owner() const { return owner_; }

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

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_{r} {}
    double area() const override { return 3.14159 * r_ * r_; }
private:
    double r_;
};

int main() {
    BankAccount acct{"Asha", 100};
    acct.deposit(50);
    std::cout << acct.owner() << ": " << acct.balance() << "\n";

    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(2.0));
    for (const auto& s : shapes) std::cout << s->area() << "\n";
}

RAII and Smart Pointers

RAII (Resource Acquisition Is Initialization) ties a resource's lifetime to an object's lifetime. When the object goes out of scope, its destructor releases the resource automatically, even if an exception is thrown.

  • Standard containers, std::string, std::fstream, and std::lock_guard are all RAII types.
  • **std::unique_ptr:** single ownership; cheap and the default smart pointer.
  • **std::shared_ptr:** shared ownership with reference counting; use only when ownership is truly shared.
  • **std::weak_ptr:** non-owning observer that breaks reference cycles.
  • Create them with std::make_unique and std::make_shared.
  • Avoid owning raw pointers and manual new/delete unless there is a specific reason.

unique_ptr, shared_ptr, and a custom RAII guard

#include <iostream>
#include <memory>

struct Resource {
    Resource()  { std::cout << "acquired\n"; }
    ~Resource() { std::cout << "released\n"; }
    void use() const { std::cout << "using\n"; }
};

class ScopeTimerLog {
public:
    explicit ScopeTimerLog(const char* label) : label_{label} { std::cout << "start " << label_ << "\n"; }
    ~ScopeTimerLog() { std::cout << "end " << label_ << "\n"; }
private:
    const char* label_;
};

int main() {
    ScopeTimerLog log{"main"};

    auto unique = std::make_unique<Resource>();   // sole owner
    unique->use();

    auto shared = std::make_shared<Resource>();
    auto another = shared;                          // reference count = 2
    std::cout << shared.use_count() << "\n";

    std::unique_ptr<Resource> moved = std::move(unique);  // ownership transfer
}   // everything is released automatically here

Templates and Generic Code

  • Templates let one function or class work with many types.
  • The compiler generates a version for each type used.
  • C++20 concepts document and enforce requirements on template parameters.
  • Standard containers and algorithms are all templates.

A function template and a class template

#include <iostream>
#include <string>
#include <vector>

template <typename T>
T maximum(const T& a, const T& b) {
    return (a < b) ? b : a;
}

template <typename T>
class Stack {
public:
    void push(T value) { items_.push_back(std::move(value)); }
    T pop() {
        T top = std::move(items_.back());
        items_.pop_back();
        return top;
    }
    bool empty() const { return items_.empty(); }
private:
    std::vector<T> items_;
};

int main() {
    std::cout << maximum(3, 9) << " " << maximum(std::string{"a"}, std::string{"b"}) << "\n";

    Stack<int> s;
    s.push(1);
    s.push(2);
    std::cout << s.pop() << "\n";   // 2
}

Exceptions and Error Handling

  • Throw exceptions for failures the current function cannot handle.
  • Catch by const&, and catch the most specific type first.
  • RAII ensures resources are released while the stack unwinds.
  • Use std::optional for "maybe a value" and std::expected (C++23) or error codes where exceptions are not suitable.
  • Do not throw from destructors.

Throwing, catching, and std::optional

#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>

int parse_positive(const std::string& text) {
    int value = std::stoi(text);               // may throw
    if (value <= 0) throw std::invalid_argument("value must be positive");
    return value;
}

std::optional<int> try_parse(const std::string& text) {
    try {
        return parse_positive(text);
    } catch (const std::exception&) {
        return std::nullopt;
    }
}

int main() {
    try {
        std::cout << parse_positive("42") << "\n";
        std::cout << parse_positive("-5") << "\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "Invalid: " << e.what() << "\n";
    }

    if (auto v = try_parse("abc")) std::cout << *v << "\n";
    else std::cout << "not a number\n";
}

Move Semantics in Brief

  • Copying duplicates a resource; moving transfers it cheaply, leaving the source in a valid but unspecified state.
  • std::move casts to an rvalue, allowing a move; it does not move anything by itself.
  • Return local objects by value; the compiler optimizes or moves them automatically.
  • Standard types are already movable, so the rule of zero usually gets you move support for free.

Moving instead of copying

#include <iostream>
#include <string>
#include <utility>
#include <vector>

std::vector<std::string> make_names() {
    std::vector<std::string> names{"Asha", "Ravi", "Meera"};
    return names;                    // moved/elided, not copied
}

int main() {
    auto names = make_names();

    std::string big(1000, 'x');
    std::vector<std::string> store;
    store.push_back(std::move(big)); // transfers the buffer
    std::cout << store[0].size() << "\n";
}

Best Practices

  • Compile with -Wall -Wextra -Wpedantic and treat warnings seriously.
  • Prefer std::vector and std::string over raw arrays and char*.
  • Avoid owning raw pointers; use unique_ptr by default.
  • Use const and references to express intent and avoid copies.
  • Follow the rule of zero; if you define one special member function, think about all of them.
  • Prefer algorithms and range-based for loops over index loops.
  • Use sanitizers (-fsanitize=address,undefined) and static analysis tools.
  • Follow the C++ Core Guidelines.

Complete Example: Counting Values with a Map

Compile with a modern compiler using C++17 or later, for example: c++ -std=c++17 -Wall -Wextra main.cpp -o main.

Count values with a map

#include <iostream>
#include <map>
#include <string>
#include <vector>

int main() {
    const std::vector<std::string> languages{
        "C++", "Python", "C++", "JavaScript", "Python", "C++"
    };

    std::map<std::string, int> counts;

    for (const auto& language : languages) {
        ++counts[language];
    }

    for (const auto& [language, count] : counts) {
        std::cout << language << ": " << count << '\n';
    }

    return 0;
}

Frequently Asked Questions

What does STL mean?

STL commonly refers to the Standard Template Library concepts and facilities, including containers, iterators, and algorithms within the C++ standard library.

Should I use raw new and delete?

Prefer standard containers and smart pointers for ordinary ownership. Manual allocation is rarely the simplest choice in modern C++.

Is C++ the same as C?

No. C++ shares some syntax with C but has its own type system, abstractions, standard library, and language features.

What is RAII?

RAII ties the lifetime of a resource such as memory, a file, or a lock to the lifetime of an object, so cleanup happens automatically when the object goes out of scope.

When should I use unique_ptr versus shared_ptr?

Use unique_ptr by default for single ownership. Use shared_ptr only when several owners genuinely share responsibility for an object's lifetime.

Which C++ standard should I learn?

Learn with C++17 or later, ideally C++20, since modern features such as structured bindings, optional, and concepts make code safer and clearer.

← Back to All Articles