C++ Tutorial
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".
| Year | Milestone |
|---|---|
| 1979 | Work on "C with Classes" began at Bell Labs. |
| 1983 | The language was renamed C++. |
| 1985 | The first commercial release and the first edition of The C++ Programming Language. |
| 1998 | C++98, the first ISO standard, including the Standard Template Library (STL). |
| 2011 | C++11, the start of "modern C++": auto, lambdas, move semantics, smart pointers, range-based for, nullptr and threads. |
| 2014 | C++14 refined C++11 with generic lambdas and std::make_unique. |
| 2017 | C++17 added structured bindings, std::optional, std::variant, std::string_view, std::filesystem and if constexpr. |
| 2020 | C++20 brought concepts, ranges, coroutines, modules, the <=> operator and std::format. |
| 2023 | C++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++
| Feature | What it means for you |
|---|---|
| Performance | Compiled to native machine code with no virtual machine or garbage collector, giving fast and predictable execution. |
| Object-oriented programming | Classes, encapsulation, inheritance and polymorphism for modelling complex systems. |
| Generic programming | Templates and concepts let you write code once that works efficiently with many types. |
| RAII | Resources such as memory, files and locks are released automatically when objects go out of scope. |
| Standard Library | Ready-made containers, algorithms, strings, I/O, threads, time and file-system utilities. |
| C compatibility | Most C code and libraries can be used directly from C++. |
| Low-level control | Pointers, custom memory allocation and direct hardware access when you need them. |
| Strong static typing | Many 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 --installto 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
#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;
}
# 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 streamsstd::coutandstd::cin.std::is the namespace of the Standard Library. Namespaces prevent name clashes between libraries.<<sends values to an output stream, andstd::getlinereads a whole line of input into astd::string.mainreturns0to report success. Like C, a C++ program starts by runningmain.
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.
#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.
#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.
#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.
#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.
#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.
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.
#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
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
finalto 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.
#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.
| Container | Description | Use it for |
|---|---|---|
std::vector | Dynamic array stored in one contiguous block. | The default choice for a sequence of elements. |
std::array | Fixed-size array with a container interface. | Small collections whose size is known at compile time. |
std::string | Dynamic sequence of characters. | Text. |
std::deque | Double-ended queue. | Fast insertion and removal at both ends. |
std::map / std::set | Sorted balanced trees; O(log n) operations. | Keys kept in order, range queries. |
std::unordered_map / std::unordered_set | Hash tables; average O(1) operations. | Fast lookup by key when order does not matter. |
#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 pointer | Ownership | Use 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. |
#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.
#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.
#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.
#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:
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()
# 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
| Aspect | C++ | C | Java |
|---|---|---|---|
| Paradigms | Procedural, object-oriented, generic, functional | Procedural | Object-oriented with functional features |
| Execution | Native machine code | Native machine code | Bytecode on the JVM with JIT compilation |
| Memory | RAII and smart pointers; manual when needed | Manual malloc/free | Garbage collected |
| Generics | Templates generate specialised code at compile time | None (macros or void*) | Generics with type erasure |
| Inheritance | Multiple inheritance supported | None | Single class inheritance, multiple interfaces |
| Operator overloading | Yes | No | No |
| Destructors | Deterministic, at end of scope | None | No destructors; try-with-resources for cleanup |
| Portability | Recompile per platform | Recompile per platform | Same 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/W4on MSVC). - Use RAII for every resource, and let containers and smart pointers manage memory; avoid naked
newanddelete. - Prefer
std::vectorandstd::stringover raw arrays and C strings. - Be
const-correct and pass large objects byconst&. - Prefer Standard Library algorithms to hand-written loops.
- Mark overrides with
overrideand 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
| Mistake | What goes wrong | Fix |
|---|---|---|
Using new without a matching delete | Memory leaks, especially when exceptions are thrown. | Use std::make_unique or a container. |
| Missing virtual destructor in a base class | Deleting through a base pointer skips derived cleanup. | Add virtual ~Base() = default;. |
| Returning a reference to a local variable | The reference dangles once the function returns. | Return by value. |
| Keeping iterators or references while a vector grows | Reallocation invalidates them. | Re-acquire them after inserting, or reserve capacity first. |
Using an object after std::move | Its contents are unspecified. | Assign a new value before reusing it. |
| Passing large objects by value unintentionally | Unnecessary, expensive copies. | Use const& for read-only parameters. |
using namespace std; in header files | Name 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
constlet 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::optionalandstd::expectedfor 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++.
Frequently Asked Questions (FAQ)
Q1. Is C++ still worth learning?
Yes. C++ remains one of the most widely used languages for games, browsers, databases, finance, embedded systems, robotics and the performance-critical core of machine-learning frameworks. Employers in these fields actively look for C++ skills.
Learning C++ also deepens your understanding of memory, performance and object lifetimes, knowledge that makes you a stronger developer in Java, Python or any other language.
Q2. Should I learn C before C++?
It is not necessary. You can start directly with modern C++ and learn good habits from the beginning, such as using std::string, std::vector and smart pointers instead of C-style strings, arrays and manual memory management.
Knowing C helps when you work with C libraries or low-level code, but learning C first can lead to writing "C with classes" rather than idiomatic C++.
Q3. Is C++ harder to learn than Java or Python?
C++ is usually considered harder because it gives you more control and therefore more responsibility: you decide between values, references and pointers, you must understand object lifetimes, and mistakes can lead to undefined behaviour instead of a clear runtime error.
Modern C++ makes this much easier than it used to be. Using containers, smart pointers, RAII, compiler warnings and sanitizers removes most of the traditional pitfalls.
Q4. Which C++ standard should I use?
For new projects, choose C++20 if your compilers support it well, as current GCC, Clang and MSVC versions do. It offers concepts, ranges and many improvements. C++17 is a safe minimum with excellent support everywhere. C++23 features such as std::print and std::expected are worth using where your toolchain supports them.
Set the standard explicitly, for example with -std=c++20 or set(CMAKE_CXX_STANDARD 20) in CMake.
Q5. What is the difference between a pointer and a reference?
- A reference is an alias for an existing object. It must be initialised, cannot be null and cannot be re-bound to another object.
- A pointer is a variable holding an address. It can be null, can be changed to point elsewhere, and supports pointer arithmetic.
Use references for function parameters and return values that always refer to a valid object; use pointers (preferably smart pointers for ownership) when "no object" is a valid state or the target can change.
Q6. What is RAII?
RAII stands for Resource Acquisition Is Initialisation. A resource (memory, a file, a lock, a network connection) is acquired in an object's constructor and released in its destructor. Because destructors run automatically when objects go out of scope, even when exceptions are thrown, the resource is always released.
Standard types such as std::vector, std::unique_ptr, std::fstream and std::lock_guard all work this way.
Q7. Does C++ have garbage collection?
No. C++ does not have a garbage collector. Instead, it uses deterministic destruction: objects are destroyed at a known point, at the end of their scope or when their owner is destroyed. RAII, containers and smart pointers handle memory automatically without the pauses or unpredictable timing of garbage collection.
A std::shared_ptr uses reference counting, which frees an object as soon as its last owner is gone.
Q8. When should I use unique_ptr versus shared_ptr?
Use std::unique_ptr by default. It represents single, clear ownership, has no runtime overhead compared with a raw pointer, and ownership can be transferred with std::move.
Use std::shared_ptr only when several parts of the program genuinely share an object and none of them alone decides when it should be destroyed. It costs an extra allocation for the control block and atomic reference-count updates. Use std::weak_ptr to observe a shared object without keeping it alive.
Q9. What does std::move actually do?
std::move does not move anything by itself. It is a cast that marks an object as an rvalue, meaning "I no longer need this value". That allows the compiler to choose a move constructor or move assignment operator, which can transfer the object's internal resources instead of copying them.
After being moved from, an object is still valid but its value is unspecified, so give it a new value before using it again.
Q10. What is the difference between struct and class in C++?
Technically there is only one difference: members and base classes are public by default in a struct and private by default in a class. Both can have constructors, member functions, inheritance and templates.
By convention, use struct for simple aggregates of public data, and class for types with private data and invariants maintained by member functions.
Q11. What is a virtual function, and why is a virtual destructor needed?
A virtual function can be overridden in a derived class, and calls made through a base-class pointer or reference run the derived version at run time. This is runtime polymorphism.
If you delete a derived object through a base-class pointer and the base destructor is not virtual, only the base part is destroyed, which is undefined behaviour and usually leaks resources. Any class meant to be used polymorphically should therefore declare virtual ~Base() = default;.
Q12. What are templates used for?
Templates let you write functions and classes that work with any type that meets their requirements. The compiler generates a specialised version for each type used, so there is no runtime cost. Containers such as std::vector<T>, algorithms such as std::sort and smart pointers are all templates.
In C++20, concepts describe a template's requirements, making templates easier to read and giving clearer error messages.
Q13. What are C++20 concepts?
A concept is a named compile-time requirement on template arguments, such as std::integral or std::sortable. You can write template <std::integral T> or define your own with a requires expression.
If a type does not satisfy the concept, the compiler reports a short, readable message instead of pages of errors from deep inside the template.
Q14. When should I use std::vector instead of other containers?
Use std::vector as your default container. Its elements are stored contiguously, which makes iteration very cache-friendly and fast, and it often outperforms linked lists even for insertions in the middle of small and medium collections.
Choose std::unordered_map for fast lookup by key, std::map when keys must stay sorted, std::deque for frequent insertion at both ends, and std::array for fixed-size collections.
Q15. Should I use exceptions or error codes?
Use exceptions for unexpected failures that cannot be handled by the immediate caller, such as running out of resources or a corrupted file. They keep normal code clean and work with RAII to clean up automatically.
For expected outcomes, such as "not found" or invalid user input, return std::optional or C++23's std::expected. Some fields, such as games and embedded systems, disable exceptions entirely and rely on return values.
Q16. What is undefined behaviour in C++?
Undefined behaviour is any operation for which the standard sets no rules, such as reading outside an array, dereferencing a null or dangling pointer, signed integer overflow or a data race between threads. The program might seem to work, crash, or produce wrong results that change with compiler versions and optimisation settings.
Avoid it with safe library types and detect it with AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer.
Q17. What does 'using namespace std;' do, and is it bad?
It makes every name from the std namespace available without the std:: prefix. That is convenient in small examples, but in larger code it can cause name clashes, for example between your own count or distance and the standard ones.
Never put it in a header file, since it would affect every file that includes the header. In source files, prefer writing std:: or importing specific names, such as using std::string;.
Q18. What is the difference between C++ and C#?
Despite the similar names, they are very different. C++ compiles to native machine code, uses deterministic destruction and RAII, and gives low-level control over memory. C# runs on the .NET runtime, uses garbage collection and is designed for productivity in business, desktop, web and game scripting work, for example with Unity.
Choose C++ for maximum performance and hardware control; choose C# for rapid application development within the .NET ecosystem.
Q19. What build tools and libraries should I learn with C++?
The essentials are:
- CMake, the most common build system generator.
- A package manager such as vcpkg or Conan for third-party libraries.
- A testing framework such as GoogleTest or Catch2.
- A debugger (gdb, LLDB or the Visual Studio debugger), sanitizers, and clang-tidy and clang-format for analysis and formatting.
Popular libraries include Boost, fmt, spdlog, nlohmann/json and Qt for desktop applications.
Q20. What are good projects to practise C++?
Choose projects that use classes, containers, files and smart pointers together:
- A library, inventory or student-management system that saves data to a file.
- A text-based adventure game or a chess move validator using class hierarchies.
- Your own simplified
vectororunique_ptrto understand RAII and move semantics. - A multithreaded file search or log analyser.
- A small 2D game with a library such as SFML or raylib.