Welcome to your journey into C++! This module sets the foundation for everything that follows. By the end, you'll understand where C++ came from, why it matter…
Table of Contents
Module 1: Introduction to C++
Module 2: C++ Basics
Module 3: Control Flow
Module 4: Functions
Module 5: Memory Management
Module 6: Object-Oriented Programming
Module 7: The Standard Template Library (STL)
Module 8: Modern C++ Features
Module 9: Advanced Topics
Final Thoughts and Next Steps
Module 1: Introduction to C++
Welcome to your journey into C++! This module sets the foundation for everything that follows. By the end, you'll understand where C++ came from, why it matters, how your code actually turns into a running program, and how to write your very first C++ application.
1.1 A Brief History of C++
C++ is a general-purpose programming language created by Bjarne Stroustrup at Bell Labs, with its first release in 1985. Stroustrup wanted a language that combined the speed and low-level control of C with the organizational power of Simula (an early object-oriented language).
The name "C++" is a programmer's joke: in C, ++ is the increment operator, so "C++" implies "one better than C."
| Standard |
Year |
Key Additions |
| C++98 |
1998 |
First ISO standard; STL included |
| C++03 |
2003 |
Minor bug fixes |
| C++11 |
2011 |
auto, lambdas, smart pointers, move semantics |
| C++14 |
2014 |
Generic lambdas, make_unique |
| C++17 |
2017 |
Structured bindings, std::optional, filesystem |
| C++20 |
2020 |
Concepts, ranges, coroutines, modules |
Definition: A programming standard is a formal specification that defines what the language must do, ensuring code behaves consistently across compilers.
1.2 Why Learn C++?
C++ remains one of the most influential languages in computing. Learning it gives you:
- Performance: C++ compiles to highly optimized machine code.
- Control: You manage memory and hardware resources directly.
- Versatility: Used in game engines (Unreal), operating systems (Windows, parts of Linux), browsers (Chrome), databases (MySQL), and embedded systems.
- Foundation: Understanding C++ makes learning other languages (Rust, Java, C#) much easier.
Encouragement: C++ has a reputation for being difficult. That reputation is partly earned — but it is also one of the most rewarding languages to master. Take it one concept at a time.
1.3 The Compilation Process
C++ is a compiled language. This means your human-readable source code must be translated into machine code before it can run. This happens in four stages:
- Preprocessing: The preprocessor handles directives starting with
# (like #include and #define). It produces an expanded source file.
- Compilation: The compiler translates the preprocessed code into assembly, then into object code (
.o or .obj files).
- Linking: The linker combines your object files with library code (like the standard library) into a single executable.
- Loading: When you run the program, the loader places it into memory for execution.
Source (.cpp) → [Preprocessor] → Expanded Source
→ [Compiler] → Object Code (.o)
→ [Linker] → Executable (.exe / a.out)
→ [Loader] → Running Program
Common Pitfall: A linker error (e.g., "undefined reference") means the compiler understood your code but couldn't find a function's definition. A compiler error means your syntax or types were wrong.
1.4 Setting Up Your Environment
You need two things: a compiler and an editor/IDE (Integrated Development Environment).
| Tool |
Type |
Platform |
Notes |
| GCC / g++ |
Compiler |
Linux, macOS, Windows |
Free, standard |
| Clang |
Compiler |
All |
Fast, great error messages |
| MSVC |
Compiler |
Windows |
Bundled with Visual Studio |
| Visual Studio |
IDE |
Windows |
Full-featured, heavy |
| VS Code |
Editor |
All |
Lightweight, needs extensions |
| CLion |
IDE |
All |
Paid, JetBrains quality |
Recommended for beginners: VS Code + g++ (or MinGW on Windows).
Compiling from the Command Line
# Compile a single file into an executable named "program"
g++ -std=c++20 -Wall -o program main.cpp
# Run it
./program # Linux/macOS
program.exe # Windows
-std=c++20 selects the C++20 standard.
-Wall enables all common warnings — always use this.
1.5 Your First Program: "Hello, World!"
// main.cpp
#include <iostream> // Include the standard I/O stream library
int main() { // The entry point of every C++ program
std::cout << "Hello, World!" << std::endl; // Print to console
return 0; // Return 0 to the OS means "success"
}
Line-by-line explanation:
#include <iostream> — Pulls in the library that defines std::cout.
int main() — Every C++ program starts executing at main. int means it returns an integer.
std::cout — "Character output" — the standard output stream. std:: is the namespace prefix.
<< — The stream insertion operator.
std::endl — Inserts a newline and flushes the buffer.
return 0; — Signals successful termination.
Common Pitfall: Forgetting the semicolon (;) at the end of a statement is one of the most common beginner errors.
1.6 Module Summary
- C++ was created by Bjarne Stroustrup in 1985 as an extension of C.
- It is a compiled language with four build stages: preprocessing, compilation, linking, loading.
- You need a compiler (g++, Clang, MSVC) and an editor/IDE.
- Every C++ program begins execution at
main().
- Use
-Wall when compiling to catch mistakes early.
1.7 Practice Exercises
- Install a compiler and IDE on your machine.
- Write a program that prints your name, your university, and your favorite hobby on three separate lines.
- Intentionally introduce a syntax error (remove a semicolon) and observe the compiler message.
- Research: What is the difference between C and C++? List three concrete differences.
Module 2: C++ Basics
Now that you can compile and run code, let's explore the fundamental building blocks of the language.
2.1 Variables and Data Types
A variable is a named location in memory that stores a value. Every variable has a type, which determines:
- How much memory it uses.
- What operations are allowed.
- How the bits are interpreted.
Primitive (Fundamental) Types
| Type |
Typical Size |
Range / Notes |
bool |
1 byte |
true or false |
char |
1 byte |
Single character, e.g., 'A' |
int |
4 bytes |
~ -2.1 to 2.1 billion |
float |
4 bytes |
~7 decimal digits of precision |
double |
8 bytes |
~15 decimal digits of precision |
void |
— |
"No type"; used for functions returning nothing |
Declaring and Initializing
int age = 20; // Copy initialization
int score(95); // Direct initialization
int lives{3}; // Brace (uniform) initialization — preferred in modern C++
double pi = 3.14159;
char grade = 'A';
bool isPassing = true;
Definition: Initialization means giving a variable its first value. Assignment means overwriting an existing value.
Best Practice: Use brace initialization {}. It prevents narrowing conversions (e.g., assigning a double to an int loses data, and brace init will error).
int x = 3.9; // Compiles; x becomes 3 (silent data loss)
int y{3.9}; // Compiler ERROR — protects you
Type Modifiers
short, long, long long — change integer size.
unsigned — only non-negative values, doubling the positive range.
signed — the default.
unsigned int population = 8000000000u;
long long bigNumber = 9000000000000LL;
2.2 Constants and Literals
A constant is a value that cannot change after initialization.
const double PI = 3.14159; // Runtime constant
constexpr int MAX_STUDENTS = 100; // Compile-time constant (preferred)
// Literals
42 // int literal
3.14 // double literal
'c' // char literal
"Hello" // string literal (const char[])
true // bool literal
0x1A // hexadecimal literal
0b1010 // binary literal (C++14)
Best Practice: Prefer constexpr over const when the value is known at compile time. It allows the compiler to optimize better and catches errors earlier.
2.3 Scope and Lifetime
Scope determines where a variable can be seen in code. Lifetime determines how long it exists in memory.
| Scope |
Description |
| Local |
Inside a function or block {}. Destroyed when block ends. |
| Global |
Outside all functions. Exists for the whole program. |
| Namespace |
Inside a named namespace. |
| Class |
Member of a class. |
int globalVar = 10; // Global scope
void function() {
int localVar = 5; // Local scope — exists only inside function()
{
int blockVar = 1; // Block scope
} // blockVar destroyed here
} // localVar destroyed here
Common Pitfall: Shadowing — declaring a local variable with the same name as a global one. This causes confusion and bugs.
int x = 5;
int main() {
int x = 10; // Shadows the global x
// To access global: ::x
}
2.4 Namespaces
A namespace is a container that groups related names to avoid collisions.
namespace Math {
const double PI = 3.14159;
int add(int a, int b) { return a + b; }
}
int main() {
std::cout << Math::PI << std::endl; // Fully qualified
std::cout << Math::add(2, 3) << std::endl;
}
The using Directive
using namespace std; // Brings all of std into scope
cout << "Hello" << endl; // Now no std:: needed
Common Pitfall: using namespace std; in header files or large projects pollutes the global namespace and can cause name clashes. Avoid it in production code. Use it sparingly, mainly in small learning programs.
Modern C++ alias:
namespace fs = std::filesystem; // Namespace alias
2.5 Basic Input and Output
The <iostream> header provides:
std::cin — standard input (keyboard)
std::cout — standard output (console)
std::cerr — standard error (unbuffered)
std::clog — standard log (buffered)
#include <iostream>
#include <string>
int main() {
std::string name;
int age;
std::cout << "Enter your name: ";
std::getline(std::cin, name); // Read a whole line
std::cout << "Enter your age: ";
std::cin >> age; // Read an integer
std::cout << "Hello, " << name
<< "! You are " << age << " years old.\n";
return 0;
}
Explanation:
>> is the stream extraction operator.
std::getline reads until a newline — good for names with spaces.
\n is faster than std::endl (which also flushes the buffer).
Common Pitfall: Mixing cin >> and getline leaves a newline in the buffer, causing getline to read an empty string. Fix with std::cin.ignore().
2.6 Operators
C++ offers a rich set of operators.
| Category |
Operators |
| Arithmetic |
+ - * / % |
| Relational |
== != < > <= >= |
| Logical |
&& || ! |
| Assignment |
= += -= *= /= %= |
| Increment/Decrement |
++ -- |
| Bitwise |
& | ^ ~ << >> |
| Ternary |
? : |
int a = 10, b = 3;
std::cout << a / b; // 3 (integer division — truncates!)
std::cout << a % b; // 1 (remainder)
std::cout << (a > b ? "yes" : "no"); // ternary
Common Pitfall: Integer division truncates. 10 / 3 is 3, not 3.333. Cast to double if you need a decimal result.
double result = static_cast<double>(a) / b; // 3.3333
2.7 Module Summary
- Variables have types that determine size and behavior.
- Prefer brace initialization
{} and constexpr.
- Scope controls where a name is visible; lifetime controls how long it exists.
- Namespaces prevent naming conflicts.
cin/cout handle standard I/O.
- C++ has a full set of arithmetic, logical, and bitwise operators.
2.8 Practice Exercises
- Write a program that asks for two numbers and prints their sum, difference, product, and quotient (as a double).
- Create a
constexpr constant for the speed of light and print it.
- Write a program that reads a full name (with spaces) and prints it in uppercase.
- Predict the output:
int x = 7; int y = 2; cout << x / y << " " << x % y;
Module 3: Control Flow
Programs are more than straight-line instructions. Control flow determines the order in which statements execute.
3.1 Conditional Statements
if, else if, else
int score = 85;
if (score >= 90) {
std::cout << "Grade: A\n";
} else if (score >= 80) {
std::cout << "Grade: B\n";
} else if (score >= 70) {
std::cout << "Grade: C\n";
} else {
std::cout << "Grade: F\n";
}
if with Initializer (C++17)
if (int x = compute(); x > 0) {
std::cout << "Positive: " << x << "\n";
}
// x is scoped to the if statement
Common Pitfall: Using = instead of == in a condition.
if (x = 5) { ... } // BUG: assigns 5 to x, always true
if (x == 5) { ... } // CORRECT
Enable -Wall and the compiler will warn you.
3.2 Switch Statements
switch is a cleaner alternative to long if-else-if chains when comparing one variable against constants.
char grade = 'B';
switch (grade) {
case 'A':
std::cout << "Excellent!\n";
break; // Prevents "fall-through"
case 'B':
case 'C': // Multiple cases can share code
std::cout << "Good job.\n";
break;
case 'F':
std::cout << "Try again.\n";
break;
default: // Runs if no case matches
std::cout << "Invalid grade.\n";
}
Common Pitfall: Forgetting break causes fall-through — execution continues into the next case. This is sometimes intentional but is usually a bug.
3.3 Loops
for Loop
Best when the number of iterations is known.
for (int i = 0; i < 5; ++i) {
std::cout << "i = " << i << "\n";
}
Anatomy: for (initialization; condition; update)
while Loop
Best when the number of iterations is unknown.
int countdown = 5;
while (countdown > 0) {
std::cout << countdown << "\n";
--countdown;
}
do-while Loop
Like while, but the body executes at least once.
int input;
do {
std::cout << "Enter a positive number: ";
std::cin >> input;
} while (input <= 0);
Comparison Table
| Loop |
When to Use |
Guaranteed to Run? |
for |
Known count |
No (if condition false initially) |
while |
Unknown count |
No |
do-while |
Unknown count, must run once |
Yes |
Common Pitfall: Infinite loops. If the loop condition never becomes false, your program hangs. Always verify your update step.
for (int i = 0; i < 10; ++i) {
// If you accidentally write --i, infinite loop!
}
3.4 Break and Continue
break — exits the loop immediately.
continue — skips to the next iteration.
for (int i = 0; i < 10; ++i) {
if (i == 5) break; // Stop at 5
if (i % 2 == 0) continue; // Skip even numbers
std::cout << i << "\n"; // Prints 1, 3
}
Best Practice: Avoid break/continue in deeply nested loops — it hurts readability. Consider refactoring into a function.
3.5 Module Summary
if/else handles branching logic.
switch is best for multiple constant comparisons.
for, while, and do-while cover all loop needs.
break and continue fine-tune loop behavior.
3.6 Practice Exercises
- Write a program that prints all prime numbers between 1 and 100 using nested loops.
- Build a simple calculator using
switch for + - * /.
- Write a
do-while loop that keeps asking for a password until the correct one is entered.
- Print the first 20 Fibonacci numbers using a
for loop.
Module 4: Functions
Functions are reusable blocks of code that perform a specific task. They are the primary unit of organization in C++.
4.1 Function Basics
Declaration vs. Definition
- Declaration (prototype): Tells the compiler the function's name, return type, and parameters.
- Definition: Provides the actual body.
// Declaration (prototype)
int add(int a, int b);
// Definition
int add(int a, int b) {
return a + b;
}
int main() {
std::cout << add(3, 4); // 7
}
Anatomy:
return_type function_name(parameter_list) {
// body
return value;
}
return_type — What the function gives back (void if nothing).
parameter_list — Inputs the function accepts.
return — Sends a value back to the caller.
Common Pitfall: Forgetting return in a non-void function leads to undefined behavior. The compiler may warn but won't always error.
4.2 Function Overloading
You can define multiple functions with the same name but different parameter lists. The compiler picks the right one based on arguments.
int add(int a, int b) { return a + b; }
double add(double a, double b){ return a + b; }
int add(int a, int b, int c) { return a + b + c; }
int main() {
std::cout << add(1, 2); // Calls int version
std::cout << add(1.5, 2.5); // Calls double version
std::cout << add(1, 2, 3); // Calls three-arg version
}
Definition: Overloading means multiple functions with the same name but different signatures.
Note: You cannot overload on return type alone.
int getValue();
double getValue(); // ERROR — ambiguous
4.3 Default Arguments
Parameters can have default values, used when the caller omits them.
void greet(std::string name, std::string greeting = "Hello") {
std::cout << greeting << ", " << name << "!\n";
}
greet("Alice"); // Hello, Alice!
greet("Bob", "Good morning");// Good morning, Bob!
Rule: Default arguments must be trailing — all parameters after the first defaulted one must also have defaults.
4.4 Pass-by-Value vs. Pass-by-Reference
| Method |
Syntax |
Behavior |
Use Case |
| By value |
int x |
Copies the argument |
Small types, no side effects |
| By reference |
int& x |
Aliases the argument |
Large types, modify original |
| By const reference |
const int& x |
Aliases, read-only |
Large types, read-only |
| By pointer |
int* x |
Passes address |
Optional arguments, C interop |
void byValue(int x) { x = 100; } // Original unchanged
void byRef(int& x) { x = 100; } // Original modified
void byConstRef(const int& x) { /* read only */ }
int main() {
int a = 5;
byValue(a); // a is still 5
byRef(a); // a is now 100
}
Best Practice: For large objects (like std::string or std::vector), always pass by const& unless you need to modify or copy.
4.5 Inline Functions
An inline function is a hint to the compiler to insert the function's code directly at the call site, avoiding function call overhead.
inline int square(int x) {
return x * x;
}
Note: Modern compilers usually inline automatically when beneficial. Use inline mainly for functions defined in headers to avoid multiple definition linker errors.
4.6 Recursion
A recursive function calls itself. Every recursion needs:
- A base case (when to stop).
- A recursive case (the self-call that moves toward the base case).
// Factorial: n! = n * (n-1)!
int factorial(int n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}
int main() {
std::cout << factorial(5); // 120
}
Common Pitfall: Missing or unreachable base case → stack overflow (the call stack runs out of memory).
When to use recursion: Problems that are naturally tree- or divide-and-conquer-shaped (e.g., tree traversal, quicksort, Fibonacci).
When to avoid: When a simple loop works — recursion is often slower and uses more memory.
4.7 Module Summary
- Functions are declared (prototype) and defined (body).
- Overloading allows same-name functions with different parameters.
- Default arguments simplify common calls.
- Pass by value copies; pass by reference aliases.
- Inline hints the compiler to avoid call overhead.
- Recursion solves self-similar problems but needs a base case.
4.8 Practice Exercises
- Write an overloaded
max function for int, double, and std::string.
- Write a recursive function to compute the n-th Fibonacci number.
- Write a function
swap(int&, int&) that swaps two integers by reference.
- Write a function with default arguments for calculating the area of a rectangle (default width = 1).
Module 5: Memory Management
Memory is one of C++'s defining features — and one of its biggest responsibilities. This module explains how memory works and how to manage it safely.
5.1 The Stack vs. The Heap
C++ programs use two main memory regions:
| Feature |
Stack |
Heap |
| Allocation |
Automatic |
Manual (new) |
| Speed |
Very fast |
Slower |
| Size |
Small (few MB) |
Large (GBs) |
| Lifetime |
Until end of scope |
Until delete |
| Management |
Compiler |
Programmer (or smart pointers) |
| Fragmentation |
None |
Possible |
void example() {
int stackVar = 10; // On the stack
int* heapVar = new int(20); // On the heap
delete heapVar; // Must free manually
}
Common Pitfall: Forgetting delete → memory leak. The memory stays allocated until the program ends.
5.2 Pointers
A pointer is a variable that stores a memory address.
int value = 42;
int* ptr = &value; // & = "address of"
std::cout << ptr; // e.g., 0x7ffee...
std::cout << *ptr; // 42 — * = "dereference"
*ptr = 100; // Changes value to 100
Pointer Operations
| Operation |
Symbol |
Meaning |
| Address-of |
&x |
Get address of x |
| Dereference |
*p |
Get value at address p |
| Null |
nullptr |
Points to nothing |
| Pointer arithmetic |
p + 1 |
Advance by one element |
Common Pitfall: Dereferencing a null or uninitialized pointer → segmentation fault (crash).
int* p = nullptr;
*p = 5; // CRASH — undefined behavior
Best Practice: Always initialize pointers to nullptr and check before dereferencing.
5.3 References
A reference is an alias for another variable. It must be initialized and cannot be reseated.
int x = 10;
int& ref = x; // ref is another name for x
ref = 20; // x is now 20
Reference vs. Pointer:
| Aspect |
Reference |
Pointer |
| Nullable |
No |
Yes |
| Reseatable |
No |
Yes |
| Syntax |
int& r = x |
int* p = &x |
| Dereferenced |
Implicit |
Explicit * |
Rule of thumb: Use references when you can, pointers when you must.
5.4 Dynamic Allocation
new and delete manage heap memory.
// Single object
int* p = new int(5);
delete p;
// Array
int* arr = new int[10];
delete[] arr; // NOTE: delete[], not delete
Common Pitfall: Using delete on an array (or delete[] on a single object) → undefined behavior. Match them correctly.
Common Pitfall: Double-delete → crash.
int* p = new int(5);
delete p;
delete p; // CRASH
Best Practice: Avoid raw new/delete in modern C++. Prefer smart pointers (next section) or STL containers (Module 7).
5.5 Smart Pointers
Smart pointers are RAII wrappers (Resource Acquisition Is Initialization) that automatically free memory.
std::unique_ptr — Exclusive Ownership
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(42);
std::cout << *p; // 42
// Automatically deleted when p goes out of scope
- Cannot be copied (only moved).
- Overhead: essentially zero.
std::shared_ptr — Shared Ownership
std::shared_ptr<int> a = std::make_shared<int>(10);
std::shared_ptr<int> b = a; // Both point to same int
// Memory freed when the last shared_ptr is destroyed
- Uses reference counting.
- Slight overhead.
std::weak_ptr — Non-Owning Observer
std::weak_ptr<int> w = a; // Does not increase ref count
if (auto locked = w.lock()) {
std::cout << *locked;
}
- Prevents circular references between
shared_ptrs.
Comparison Table
| Smart Pointer |
Ownership |
Copyable |
Use Case |
unique_ptr |
Exclusive |
No (move only) |
Default choice |
shared_ptr |
Shared |
Yes |
Multiple owners |
weak_ptr |
None |
Yes |
Break cycles, observe |
Best Practice: Default to unique_ptr. Use shared_ptr only when ownership truly must be shared.
5.6 Module Summary
- Stack memory is automatic and fast; heap memory is manual and larger.
- Pointers store addresses; references alias variables.
new/delete manage raw heap memory but are error-prone.
- Smart pointers (
unique_ptr, shared_ptr, weak_ptr) automate cleanup.
- Modern C++ prefers smart pointers and containers over raw pointers.
5.7 Practice Exercises
- Write a function that allocates an array of 10 ints with
new, fills it, and returns it. Then rewrite it with std::unique_ptr<int[]>.
- Explain the difference between
int* p = &x; and int& r = x;.
- Demonstrate a memory leak with raw pointers, then fix it with
unique_ptr.
- Create two
shared_ptrs to the same object and print the reference count using .use_count().
Module 6: Object-Oriented Programming (Continued)
Object-Oriented Programming (OOP) organizes code around objects — self-contained units that bundle data (attributes) and behavior (methods). C++ is a multi-paradigm language, but OOP is one of its most powerful features.
6.1 Classes vs. Structs
A class is a blueprint for creating objects. A struct is nearly identical, with one key difference: default access level.
| Feature |
class |
struct |
| Default member access |
private |
public |
| Default inheritance |
private |
public |
| Typical use |
Complex types with invariants |
Simple data aggregates |
| Everything else |
Identical |
Identical |
// struct — members are public by default
struct Point {
double x;
double y;
};
// class — members are private by default
class BankAccount {
double balance; // private by default
public:
void deposit(double amount) { balance += amount; }
double getBalance() const { return balance; }
};
Definition: An object is an instance of a class. Point p; creates an object p of type Point.
Best Practice: Use struct for plain data (POD — Plain Old Data). Use class when you need encapsulation.
6.2 Encapsulation
Encapsulation means hiding internal details and exposing only a controlled interface. This is achieved via access specifiers:
| Specifier |
Accessible From |
public |
Anywhere |
protected |
Class and derived classes |
private |
Class only (and friends) |
class Temperature {
private:
double celsius_; // Convention: trailing underscore for members
public:
Temperature(double c) : celsius_(c) {}
double getCelsius() const { return celsius_; }
double getFahrenheit() const { return celsius_ * 9.0 / 5.0 + 32.0; }
void setCelsius(double c) {
if (c < -273.15) throw std::invalid_argument("Below absolute zero");
celsius_ = c;
}
};
Why encapsulate?
- Invariants — You can enforce rules (e.g., temperature can't be below absolute zero).
- Flexibility — You can change internals without breaking callers.
- Readability — Users see a clean interface, not implementation details.
Common Pitfall: Making everything public "to save time." This defeats the purpose of OOP and leads to fragile code.
6.3 Constructors and Destructors
Constructors
A constructor initializes an object when it's created. It has the same name as the class and no return type.
class Student {
std::string name_;
int id_;
public:
// Default constructor
Student() : name_("Unknown"), id_(0) {}
// Parameterized constructor
Student(std::string name, int id) : name_(std::move(name)), id_(id) {}
// Copy constructor
Student(const Student& other) : name_(other.name_), id_(other.id_) {}
// Move constructor (C++11)
Student(Student&& other) noexcept
: name_(std::move(other.name_)), id_(other.id_) {
other.id_ = -1;
}
};
Member Initializer List: The : name_(...) syntax initializes members before the constructor body runs. It's more efficient than assigning in the body.
// BAD — default-constructs then assigns
Student(std::string n) { name_ = n; }
// GOOD — directly constructs
Student(std::string n) : name_(n) {}
Destructors
A destructor cleans up when an object is destroyed. It's named ~ClassName().
class FileHandler {
FILE* file_;
public:
FileHandler(const char* path) { file_ = fopen(path, "r"); }
~FileHandler() {
if (file_) fclose(file_); // Automatically closes on destruction
}
};
RAII Principle: Resource Acquisition Is Initialization — acquire resources in the constructor, release them in the destructor. This is a cornerstone of safe C++.
Special Member Functions (Rule of Five)
If you define any of these, you should consider defining all five:
- Destructor —
~ClassName()
- Copy constructor —
ClassName(const ClassName&)
- Copy assignment —
ClassName& operator=(const ClassName&)
- Move constructor —
ClassName(ClassName&&)
- Move assignment —
ClassName& operator=(ClassName&&)
Rule of Zero: If your class doesn't manage a resource directly, define none of these. Let the compiler generate them. This is preferred in modern C++.
6.4 Inheritance
Inheritance lets a class (derived) acquire properties from another (base).
class Animal {
protected:
std::string name_;
public:
Animal(std::string name) : name_(std::move(name)) {}
void eat() { std::cout << name_ << " is eating.\n"; }
virtual void speak() { std::cout << "...\n"; }
virtual ~Animal() = default; // IMPORTANT: virtual destructor
};
class Dog : public Animal {
public:
Dog(std::string name) : Animal(std::move(name)) {}
void speak() override { std::cout << name_ << " says Woof!\n"; }
};
class Cat : public Animal {
public:
Cat(std::string name) : Animal(std::move(name)) {}
void speak() override { std::cout << name_ << " says Meow!\n"; }
};
Inheritance Types
| Type |
Syntax |
Meaning |
| Public |
class D : public B |
"is-a" (most common) |
| Protected |
class D : protected B |
Base's public → protected |
| Private |
class D : private B |
Base's public → private |
Best Practice: Use public inheritance almost always. It models "is-a" relationships.
Common Pitfall: Private inheritance is often confused with composition. Prefer composition (having a member object) when "has-a" is the relationship.
6.5 Polymorphism
Polymorphism means "many forms" — the same interface behaves differently for different types. C++ supports two kinds:
Compile-Time (Static) Polymorphism
- Function overloading
- Templates
Runtime (Dynamic) Polymorphism
- Virtual functions — the star of OOP.
void makeItSpeak(Animal& a) {
a.speak(); // Calls the correct speak() based on the actual type
}
int main() {
Dog d("Rex");
Cat c("Whiskers");
makeItSpeak(d); // Rex says Woof!
makeItSpeak(c); // Whiskers says Meow!
}
How it works: When a function is marked virtual, the compiler uses a vtable (virtual table) to look up the correct function at runtime.
virtual, override, and final
class Base {
public:
virtual void foo() { } // Can be overridden
virtual void bar() = 0; // Pure virtual — must be overridden
};
class Derived : public Base {
public:
void foo() override { } // override = compiler-checked
};
class Final : public Derived {
public:
void foo() final { } // final = cannot be overridden further
};
Best Practice: Always use override when overriding. It catches typos and signature mismatches.
Common Pitfall: Forgetting virtual in the base class → the derived version is hidden, not overridden.
class Base { public: void speak() { std::cout << "base"; } };
class Derived : public Base { public: void speak() { std::cout << "derived"; } };
Base* b = new Derived();
b->speak(); // Prints "base" — NOT polymorphic!
Virtual Destructors
If a class has virtual functions and will be deleted via a base pointer, its destructor must be virtual. Otherwise, the derived destructor won't run → resource leak.
class Base {
public:
virtual ~Base() = default; // Always virtual in polymorphic bases
};
6.6 Abstract Classes and Interfaces
An abstract class has at least one pure virtual function (= 0) and cannot be instantiated.
class Shape {
public:
virtual double area() const = 0; // Pure virtual
virtual double perimeter() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius_;
public:
Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
double perimeter() const override { return 2 * 3.14159 * radius_; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
double perimeter() const override { return 2 * (w_ + h_); }
};
Definition: An abstract class with only pure virtual functions and no data members is often called an interface.
When to use:
- Define a contract that many classes must fulfill.
- Enable polymorphism across unrelated types (e.g.,
Serializable, Drawable).
6.7 Operator Overloading
C++ lets you redefine operators for your own types.
class Vector2 {
public:
double x, y;
Vector2(double x, double y) : x(x), y(y) {}
// Addition
Vector2 operator+(const Vector2& other) const {
return Vector2(x + other.x, y + other.y);
}
// Compound assignment
Vector2& operator+=(const Vector2& other) {
x += other.x;
y += other.y;
return *this;
}
// Equality
bool operator==(const Vector2& other) const {
return x == other.x && y == other.y;
}
// Stream output (friend function)
friend std::ostream& operator<<(std::ostream& os, const Vector2& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
};
int main() {
Vector2 a(1, 2), b(3, 4);
Vector2 c = a + b; // (4, 6)
std::cout << c << "\n"; // (4, 6)
}
Rules:
- Cannot change operator precedence or arity.
- Cannot invent new operators.
- Some operators (
::, ., ?:, sizeof) cannot be overloaded.
Best Practice: Overload operators only when it's intuitive. + for vectors makes sense; + for Employee does not.
6.8 Module Summary
class vs struct differ only in default access.
- Encapsulation protects invariants and enables change.
- Constructors initialize; destructors clean up.
- The Rule of Zero/Five guides special member function definitions.
- Inheritance models "is-a"; composition models "has-a."
- Virtual functions enable runtime polymorphism.
- Abstract classes define interfaces.
- Operator overloading makes custom types behave like built-ins.
6.9 Practice Exercises
- Design a
BankAccount class with deposit, withdraw, and getBalance. Enforce that balance never goes negative.
- Create a hierarchy
Shape → Circle, Rectangle, Triangle. Store them in a std::vector<Shape*> and print each area polymorphically.
- Implement a
Fraction class with operator overloading for +, -, *, /, and <<.
- Explain why a base class with virtual functions needs a virtual destructor. Demonstrate with code.
- Implement the Rule of Five for a class that manages a dynamic array.
Module 7: The Standard Template Library (STL)
The Standard Template Library (STL) is a collection of generic containers, iterators, and algorithms. It's one of C++'s greatest strengths.
7.1 Introduction to the STL
The STL has three pillars:
- Containers — data structures (
vector, map, set, ...).
- Iterators — generalized pointers that traverse containers.
- Algorithms — functions that operate on ranges (
sort, find, count, ...).
All STL components are templates, so they work with any type.
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9};
std::sort(nums.begin(), nums.end());
for (int n : nums) std::cout << n << " "; // 1 2 5 8 9
}
7.2 Sequence Containers
Sequence containers store elements in a linear order.
| Container |
Backing Structure |
Strengths |
Weaknesses |
std::vector |
Dynamic array |
Fast random access, cache-friendly |
Slow inserts at front |
std::deque |
Double-ended queue |
Fast insert/remove at both ends |
Slightly slower access than vector |
std::list |
Doubly linked list |
Fast insert/remove anywhere |
No random access |
std::array |
Fixed-size array |
Zero overhead, stack-allocated |
Fixed size |
std::forward_list |
Singly linked list |
Memory-efficient |
Forward-only iteration |
std::vector — The Default Choice
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4); // Add to end
v.pop_back(); // Remove last
v[0] = 100; // Random access
std::cout << v.size(); // 3
v.reserve(100); // Preallocate capacity
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
Best Practice: Use vector unless you have a specific reason not to. It's the fastest general-purpose container in most cases due to CPU cache locality.
std::array — Fixed-Size Stack Array
#include <array>
std::array<int, 5> a = {1, 2, 3, 4, 5};
std::cout << a.size(); // 5
std::cout << a[2]; // 3
Prefer std::array over raw C arrays — it knows its size and supports STL algorithms.
std::list — Doubly Linked List
#include <list>
std::list<int> lst = {1, 2, 3};
lst.push_front(0); // O(1)
lst.push_back(4); // O(1)
auto it = lst.begin();
std::advance(it, 2);
lst.insert(it, 99); // Insert in middle: O(1) once positioned
When to use: Frequent insertions/removals in the middle, and you don't need random access.
7.3 Associative Containers
Associative containers store elements sorted by key.
| Container |
Keys Unique? |
Sorted? |
Backing Structure |
std::set |
Yes |
Yes |
Balanced BST (red-black) |
std::multiset |
No |
Yes |
Balanced BST |
std::map |
Yes |
Yes |
Balanced BST |
std::multimap |
No |
Yes |
Balanced BST |
std::unordered_set |
Yes |
No |
Hash table |
std::unordered_map |
Yes |
No |
Hash table |
std::map — Key-Value Pairs
#include <map>
#include <string>
std::map<std::string, int> ages;
ages["Alice"] = 25;
ages["Bob"] = 30;
ages["Charlie"] = 22;
for (const auto& [name, age] : ages) { // C++17 structured binding
std::cout << name << " is " << age << "\n";
}
// Lookup
if (ages.find("Alice") != ages.end()) {
std::cout << ages["Alice"]; // 25
}
Performance:
| Operation |
map |
unordered_map |
| Insert |
O(log n) |
O(1) average |
| Lookup |
O(log n) |
O(1) average |
| Ordered iteration |
Yes |
No |
Best Practice: Use unordered_map when you don't need sorted order — it's usually faster.
Common Pitfall: operator[] on a map inserts a default value if the key doesn't exist. Use .at() or .find() for pure lookup.
std::map<std::string, int> m;
m["nonexistent"]; // Inserts {"nonexistent", 0}!
m.at("nonexistent"); // Throws std::out_of_range
std::set — Unique Sorted Elements
#include <set>
std::set<int> s = {3, 1, 4, 1, 5, 9, 2, 6};
// s = {1, 2, 3, 4, 5, 6, 9} — duplicates removed, sorted
s.insert(7);
if (s.count(4)) std::cout << "4 is present\n";
7.4 Iterators
Iterators are like generalized pointers. They provide a uniform way to traverse any container.
| Iterator Category |
Supports |
Example Containers |
| Input |
Read forward, single-pass |
istream_iterator |
| Output |
Write forward, single-pass |
ostream_iterator |
| Forward |
Read/write forward, multi-pass |
forward_list |
| Bidirectional |
Forward + backward |
list, map, set |
| Random Access |
+ pointer arithmetic |
vector, deque, array |
Common Iterator Operations
std::vector<int> v = {10, 20, 30, 40};
auto it = v.begin(); // Points to first
auto end = v.end(); // Points *past* last
std::cout << *it; // 10
++it; // Advance
std::cout << *it; // 20
// Iterate manually
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
Best Practice: Prefer range-based for loops (for (int x : v)) when you don't need iterators explicitly.
Common Pitfall: Modifying a container while iterating invalidates iterators. Use erase carefully:
// Correct way to erase while iterating
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) it = v.erase(it);
else ++it;
}
7.5 Algorithms
The <algorithm> header provides over 100 generic algorithms.
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end()); // 1 2 3 5 8 9
auto it = std::find(v.begin(), v.end(), 8);
if (it != v.end()) std::cout << "Found at index " << (it - v.begin());
int count = std::count_if(v.begin(), v.end(),
[](int x) { return x > 3; });
int sum = std::accumulate(v.begin(), v.end(), 0);
std::reverse(v.begin(), v.end());
Commonly Used Algorithms
| Algorithm |
Purpose |
sort |
Sort a range |
find |
Find first matching value |
count / count_if |
Count matches |
min_element / max_element |
Find min/max |
accumulate |
Sum/reduce |
for_each |
Apply function to each element |
remove_if |
Remove matching elements |
unique |
Remove consecutive duplicates |
all_of / any_of / none_of |
Predicate tests |
Best Practice: Prefer STL algorithms over hand-written loops. They're tested, optimized, and more readable.
7.6 std::string
std::string is a specialized container for text.
#include <string>
std::string s = "Hello";
s += ", World!"; // Concatenation
std::cout << s.length(); // 13
std::cout << s.substr(0, 5); // "Hello"
std::cout << s.find("World"); // 7
if (s == "Hello, World!") { } // Comparison
if (s.empty()) { } // Empty check
// C++20: starts_with / ends_with
if (s.starts_with("Hello")) { }
Common Pitfall: Using + with a char* and a char can lead to pointer arithmetic.
std::string s = "abc" + 'd'; // ERROR — pointer arithmetic!
std::string s = std::string("abc") + 'd'; // OK
String conversion:
int n = std::stoi("42");
double d = std::stod("3.14");
std::string s = std::to_string(42);
7.7 Module Summary
- STL = containers + iterators + algorithms.
vector is the default sequence container.
map / unordered_map for key-value storage.
set / unordered_set for unique collections.
- Iterators provide uniform traversal.
<algorithm> offers generic, tested algorithms.
std::string is a full-featured text type.
7.8 Practice Exercises
- Read a list of integers from the user, store them in a
vector, sort them, and print the median.
- Build a word-frequency counter using
std::map<std::string, int>.
- Given a
vector<int>, use std::remove_if to remove all negatives, then erase them.
- Use
std::transform to square every element in a vector.
- Find the top 3 most frequent words in a paragraph.
Module 8: Modern C++ Features
Modern C++ (C++11 onward) drastically improves safety, expressiveness, and performance. This module covers the essentials.
8.1 The auto Keyword
auto lets the compiler deduce a variable's type from its initializer.
auto x = 42; // int
auto y = 3.14; // double
auto name = std::string("Alice");
auto v = std::vector<int>{1, 2, 3};
// Especially useful with iterators
std::map<std::string, int> m;
for (auto it = m.begin(); it != m.end(); ++it) { }
Best Practice:
- Use
auto when the type is obvious from context.
- Avoid
auto when it obscures meaning.
Common Pitfall: auto strips references and const unless you're explicit.
const int& ref = x;
auto a = ref; // a is int (copy!), not const int&
auto& b = ref; // b is const int& — correct
const auto& c = ref; // Explicit
8.2 Range-Based For Loops
A concise way to iterate over any container.
std::vector<int> v = {1, 2, 3, 4, 5};
// By value (copy)
for (int x : v) std::cout << x;
// By reference (modify)
for (int& x : v) x *= 2;
// By const reference (efficient, read-only) — BEST DEFAULT
for (const auto& x : v) std::cout << x;
Best Practice: Use const auto& by default. Use auto& when you need to modify. Use auto only for cheap-to-copy types like int.
8.3 Lambdas
A lambda is an anonymous inline function. Its syntax:
[capture](parameters) -> return_type { body }
// Simple lambda
auto add = [](int a, int b) { return a + b; };
std::cout << add(2, 3); // 5
// With capture
int threshold = 10;
auto isBig = [threshold](int x) { return x > threshold; };
// By reference capture
int counter = 0;
auto increment = [&counter]() { ++counter; };
// Capture all by value / reference
auto byVal = [=]() { /* ... */ };
auto byRef = [&]() { /* ... */ };
Lambdas with Algorithms
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end(),
[](int a, int b) { return a > b; }); // Descending
auto it = std::find_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
Common Pitfall: Capturing by reference a local variable that goes out of scope → dangling reference.
std::function<int()> makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // BUG: count is destroyed
}
// Fix: capture by value [count]() mutable { return ++count; }
Generic Lambdas (C++14)
auto print = [](const auto& x) { std::cout << x << "\n"; };
print(42);
print("hello");
print(3.14);
8.4 Move Semantics
Move semantics allows transferring resources (like dynamically allocated memory) from one object to another without copying.
The Problem: Expensive Copies
std::vector<int> makeBigVector() {
std::vector<int> v(1'000'000, 42);
return v; // Before C++11: deep copy! After: move or elision
}
Lvalues vs. Rvalues
- lvalue — has a name, persists beyond the expression.
int x = 5; → x is an lvalue.
- rvalue — a temporary, no name.
5, x + y, makeBigVector() are rvalues.
Move Constructor and std::move
class Buffer {
int* data_;
size_t size_;
public:
Buffer(size_t n) : data_(new int[n]), size_(n) {}
// Move constructor
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // Leave source in valid state
other.size_ = 0;
}
~Buffer() { delete[] data_; }
};
Buffer a(100);
Buffer b = std::move(a); // Moves — no copy
std::move doesn't actually move anything — it casts an lvalue to an rvalue, enabling the move constructor.
Best Practice:
- Mark move constructors/assignments
noexcept.
- Don't use
std::move on const objects — it silently copies instead.
8.5 Exception Handling
Exceptions handle errors that can't be resolved locally.
#include <stdexcept>
double divide(double a, double b) {
if (b == 0) throw std::invalid_argument("Division by zero");
return a / b;
}
int main() {
try {
std::cout << divide(10, 0);
} catch (const std::invalid_argument& e) {
std::cerr << "Error: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cerr << "Generic: " << e.what() << "\n";
} catch (...) {
std::cerr << "Unknown error\n";
}
}
Standard Exception Hierarchy
std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::out_of_range
│ └── std::domain_error
├── std::runtime_error
│ ├── std::range_error
│ └── std::overflow_error
└── std::bad_alloc
Best Practices
- Throw by value, catch by reference (usually
const&).
- Use exceptions for exceptional conditions, not normal control flow.
- Destructors should never throw.
- Use RAII to ensure resources are cleaned up during stack unwinding.
Common Pitfall: Catching by value causes object slicing — the derived parts get cut off.
catch (std::exception e) { } // BAD — slicing
catch (const std::exception& e) { } // GOOD
8.6 Module Summary
auto deduces types; use it wisely.
- Range-based for loops simplify iteration.
- Lambdas are inline anonymous functions with captures.
- Move semantics avoid expensive copies.
- Exceptions separate error handling from normal flow.
8.7 Practice Exercises
- Use
std::sort with a lambda to sort a vector of structs by a specific field.
- Write a lambda that captures a counter by reference and increments it each call.
- Implement a move constructor for a class managing a
std::string resource.
- Write a function that throws
std::out_of_range and demonstrate catching it.
- Explain why
std::move on a const object does nothing useful.
Module 9: Advanced Topics
These topics round out your C++ foundation and prepare you for real-world projects.
9.1 Templates
Templates enable generic programming — writing code that works with any type.
Function Templates
template <typename T>
T maxOf(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << maxOf(3, 7); // int
std::cout << maxOf(3.14, 2.71); // double
std::cout << maxOf(std::string("a"), std::string("b"));
}
Class Templates
template <typename T>
class Box {
T value_;
public:
Box(T v) : value_(v) {}
T get() const { return value_; }
void set(T v) { value_ = v; }
};
Box<int> b(42);
Box<std::string> s("hello");
Template Specialization
template <typename T>
struct Printer {
void print(const T& v) { std::cout << v; }
};
// Specialization for bool
template <>
struct Printer<bool> {
void print(bool v) { std::cout << (v ? "true" : "false"); }
};
Variadic Templates (C++11)
template <typename... Args>
void printAll(Args... args) {
((std::cout << args << " "), ...); // C++17 fold expression
}
printAll(1, "hello", 3.14, 'c');
Common Pitfall: Templates are compiled per instantiation — errors may only surface when you use a particular type. Big templates also increase compile time.
Concepts (C++20)
Concepts constrain template parameters, improving error messages.
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }
// add(1.5, 2.5); // ERROR: double doesn't satisfy std::integral
9.2 File I/O
C++ uses <fstream> for file input/output.
#include <fstream>
#include <string>
// Writing
{
std::ofstream out("data.txt");
if (!out) { std::cerr << "Cannot open file\n"; return 1; }
out << "Hello, file!\n";
out << 42 << "\n";
} // File automatically closed
// Reading line by line
{
std::ifstream in("data.txt");
std::string line;
while (std::getline(in, line)) {
std::cout << line << "\n";
}
}
// Reading tokens
{
std::ifstream in("numbers.txt");
int n;
while (in >> n) { std::cout << n << " "; }
}
File Modes
| Mode |
Meaning |
std::ios::in |
Read |
std::ios::out |
Write (truncates) |
std::ios::app |
Append |
std::ios::binary |
Binary mode |
std::ios::trunc |
Truncate on open |
std::ofstream out("log.txt", std::ios::app); // Append mode
Best Practice: Always check if (!stream) after opening. Use RAII — the stream closes automatically when it leaves scope.
9.3 Preprocessor Directives
The preprocessor runs before compilation.
#include
#include <iostream> // System header
#include "myheader.h" // Local header
#define — Macros
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
Common Pitfall: Macros are text substitution — no type checking.
#define SQUARE(x) x * x
int y = SQUARE(2 + 3); // Becomes 2 + 3 * 2 + 3 = 11 (WRONG)
// Fixed: #define SQUARE(x) ((x) * (x))
Best Practice: Prefer constexpr and inline functions over macros.
constexpr double PI = 3.14159;
constexpr int square(int x) { return x * x; }
Conditional Compilation
#ifdef DEBUG
std::cout << "Debug mode\n";
#endif
#ifndef HEADER_H
#define HEADER_H
// header contents
#endif
Include Guards vs. #pragma once
// Traditional include guard
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ...
#endif
// Modern (widely supported)
#pragma once
Best Practice: Use #pragma once — it's simpler and supported by all major compilers. Include guards still work everywhere.
9.4 Module Summary
- Templates enable generic code across types.
- Concepts (C++20) constrain template parameters.
<fstream> handles file I/O with RAII semantics.
- Preprocessor directives run before compilation.
- Prefer
constexpr and inline over macros.
9.5 Practice Exercises
- Write a template function
sum that works with int, double, and std::string (concatenation).
- Build a
Stack<T> class template with push, pop, and top.
- Read a text file, count the lines, words, and characters, and print the results.
- Write a header file with an include guard and use it from two
.cpp files.
- Convert a macro
MAX(a, b) into a constexpr function template.
Final Thoughts and Next Steps
Congratulations — you've completed a comprehensive tour of C++! You've gone from "Hello, World!" to templates, move semantics, and the STL. That's a substantial journey.
What You've Learned
| Module |
Core Skill |
| 1 |
Compilation, tooling, first program |
| 2 |
Types, variables, operators, I/O |
| 3 |
Branching and looping |