Understanding C++ as a Programming Language

C++ is a general purpose programming language that supports procedural, object oriented, and generic programming. It was created by Bjarne Stroustrup at Bell Labs starting in 1979 as an extension of the C language. Today, C++ is one of the most widely used languages in the world, powering everything from operating systems to game engines to financial trading systems.

C++ gives you direct control over hardware and memory while also providing high level abstractions like classes, templates, and the Standard Template Library (STL). This unique combination of low level power and high level expressiveness is what makes C++ special.

Why Should You Learn C++ in 2026?
  • Performance C++ compiles directly to machine code, making it one of the fastest programming languages available. When speed matters, C++ is the go to choice.
  • Systems Programming Operating systems like Windows, macOS, and Linux use C++ extensively. If you want to understand how software works at the deepest level, C++ is essential.
  • Game Development Major game engines like Unreal Engine are built with C++. Most AAA games use C++ for their performance critical code.
  • Competitive Programming C++ is the most popular language for competitive programming due to its speed and the powerful STL library.
  • Career Opportunities C++ developers are in high demand at companies like Google, Microsoft, Amazon, and in finance, embedded systems, and robotics.
  • Foundation for Other Languages Learning C++ gives you a deep understanding of memory, types, and computer architecture that transfers to every other language.
Where is C++ Used in the Real World?
IndustryExamplesWhy C++?
Operating SystemsWindows, macOS, LinuxDirect hardware access, zero overhead abstractions
Game EnginesUnreal Engine, Unity (runtime), GodotReal time performance, memory control
DatabasesMySQL, MongoDB, RedisHigh throughput, low latency data processing
Web BrowsersChrome (V8 engine), Firefox (SpiderMonkey)Fast JavaScript execution, rendering
Embedded SystemsArduino, automotive ECUs, IoT devicesRuns on tiny processors, no garbage collector
FinanceHigh frequency trading, risk enginesMicrosecond latency requirements
Machine LearningTensorFlow, PyTorch, ONNXGPU acceleration, numerical computation
CompilersGCC, Clang, MSVCSelf hosting language, template metaprogramming
Your First C++ Program

Here is the simplest C++ program you can write. This prints "Hello, World!" to the screen. Every C++ program starts from the main() function.

#include <iostream> // this lets us use cout for printing int main() { std::cout << "Hello, World!" << std::endl; return 0; // 0 means the program ran successfully }

Let us break down what each line does:

  • #include <iostream> tells the compiler to include the input/output library so we can print text
  • int main() is the entry point of every C++ program. The int means it returns a number
  • std::cout is the standard output stream. Think of it as a way to send text to the screen
  • << is the insertion operator. It sends data into the output stream
  • std::endl adds a new line and flushes the output buffer
  • return 0 tells the operating system the program finished without errors

Beginner Tip: You can also write using namespace std; at the top so you do not need to type std:: before everything. This is fine for learning, but in larger projects it is better to use the full std:: prefix to avoid naming conflicts.

How to Set Up C++ on Your Computer

Before you can write and run C++ code, you need a compiler. A compiler translates the C++ code you write into machine code that your computer can actually execute. Here is how to set it up on different operating systems.

Windows Setup

The easiest way to get started on Windows is to install MinGW (Minimalist GNU for Windows) which includes the g++ compiler.

// Step 1: Download and install MinGW from https://www.mingw-w64.org/ // Step 2: Add MinGW's bin folder to your system PATH // Step 3: Open Command Prompt and verify installation: g++ --version // Should show: g++ (MinGW-w64) 13.x.x or similar // Step 4: Compile and run your first program: g++ hello.cpp -o hello hello.exe
macOS Setup
// Install Xcode Command Line Tools (includes clang++ compiler) xcode-select --install // Verify installation clang++ --version // Compile and run clang++ hello.cpp -o hello ./hello
Linux Setup (Ubuntu/Debian)
// Install g++ compiler sudo apt update sudo apt install g++ // Verify installation g++ --version // Compile and run g++ hello.cpp -o hello ./hello
Online Compilers (No Installation Needed)

If you just want to start coding right away without installing anything, use one of these free online compilers:

  • Compiler Explorer (godbolt.org) great for seeing the assembly output alongside your C++ code
  • OnlineGDB full featured online IDE with debugging support
  • Replit collaborative online IDE that supports C++ with build system
  • Wandbox supports multiple C++ standard versions from C++11 to C++23
Recommended Compiler Flags for Beginners
// Compile with warnings enabled (catches common mistakes) g++ -Wall -Wextra -std=c++17 -o myprogram myprogram.cpp // What each flag does: // -Wall : enable all common warnings // -Wextra : enable extra warnings // -std=c++17 : use the C++17 standard // -o myprogram : name the output file "myprogram" // For debugging (adds debug symbols) g++ -g -Wall -Wextra -std=c++17 -o myprogram myprogram.cpp // For optimized release build g++ -O2 -Wall -std=c++17 -o myprogram myprogram.cpp

Editor Recommendation: VS Code with the C/C++ extension by Microsoft is the most popular free editor for C++ development. It provides syntax highlighting, IntelliSense code completion, and integrated debugging.

The Story Behind C++

C++ was created by Bjarne Stroustrup at Bell Labs in 1979, initially called "C with Classes". It was officially renamed C++ in 1983. The name itself is a programmer's joke. The ++ operator in C increments a value by one, so C++ literally means "C incremented by one" or "one step above C".

Why Was C++ Created?

Stroustrup was working on his PhD thesis and needed to simulate distributed computing systems. He liked the object oriented features of a language called Simula, but Simula was too slow for systems programming. Meanwhile, C was fast but had no way to organize complex code into objects and classes. So he combined the best of both worlds.

  • Simulate complex systems Stroustrup needed to model distributed computing for his PhD thesis at Cambridge University
  • C lacked abstraction C was powerful and fast but had no way to model real world entities cleanly using classes
  • Simula had the right ideas Object oriented concepts from Simula (classes, inheritance) were elegant but the language was too slow
  • The goal Combine the raw performance and hardware access of C with the organizational power of Simula's object oriented programming
Evolution Timeline
YearVersionKey Additions
1979C with ClassesClasses, basic inheritance, inline functions
1983C++Virtual functions, function overloading, references
1985Cfront 1.0First commercial release, "The C++ Programming Language" book published
1989C++ 2.0Multiple inheritance, abstract classes, static members
1998C++98First ISO standard, STL, templates, exceptions, namespaces
2003C++03Bug fixes and clarifications for C++98
2011C++11Move semantics, lambdas, auto, nullptr, smart pointers, threads
2014C++14Generic lambdas, binary literals, return type deduction
2017C++17std::optional, structured bindings, if constexpr, filesystem
2020C++20Concepts, ranges, coroutines, modules, spaceship operator
2023C++23std::expected, std::print, stacktrace, multidimensional subscript
2026C++26Contracts, reflection (proposed), sender/receiver for async
C++ vs C: Key Differences
FeatureCC++
Programming ParadigmProcedural onlyProcedural, OOP, Generic, Functional
Classes & ObjectsNot supportedFull support with inheritance and polymorphism
Memory Managementmalloc/freenew/delete plus smart pointers
Standard LibraryLimited (stdio, stdlib)Rich STL with containers, algorithms, iterators
Exception HandlingNot supported (uses error codes)try/catch/throw
Function OverloadingNot supportedFully supported
Templates/GenericsNot supportedPowerful template system
NamespacesNot supportedFull namespace support

C++ Fundamentals

This section covers the building blocks of every C++ program. Understanding these fundamentals well is critical because everything else in C++ builds on top of them.

Program Structure Explained

Every C++ program follows a basic structure. Here is a complete example that shows all the main parts:

#include <iostream> // header file for input/output #include <string> // header file for string type using namespace std; // so we can write cout instead of std::cout // This is a single line comment /* This is a multi line comment */ int main() { cout << "Hello, World!" << endl; return 0; }
Data Types

C++ is a statically typed language, meaning every variable must have a type declared at compile time. Here are the fundamental types:

TypeSizeRangeExample
int4 bytesAbout ±2 billionint age = 25;
long long8 bytesAbout ±9.2 quintillionlong long bigNum = 9000000000LL;
float4 bytes~7 decimal digits precisionfloat price = 9.99f;
double8 bytes~15 decimal digits precisiondouble pi = 3.14159265;
char1 byteSingle character or -128 to 127char grade = 'A';
bool1 bytetrue or falsebool isActive = true;
stringvariesAny textstring name = "Alice";
size_t8 bytes0 to about 18 quintillionsize_t count = vec.size();
Variable Declaration and Initialization

There are several ways to create variables in C++. Here are the most common approaches with simple examples:

// Method 1: Copy initialization (most familiar to beginners) int age = 25; double price = 19.99; string name = "Alice"; bool isStudent = true; // Method 2: Direct initialization int count(10); // Method 3: Uniform initialization with braces (C++11, recommended) int score{95}; // prevents narrowing conversions // int x{3.14}; // ERROR: would lose data, compiler catches this // Using auto to let the compiler figure out the type auto x = 42; // compiler knows this is an int auto y = 3.14; // compiler knows this is a double auto s = "hello"; // const char* (use string{"hello"} for std::string) // Constants: values that never change const int MAX_STUDENTS = 100; // cannot modify after this line constexpr double PI = 3.14159265; // computed at compile time (C++11)
Type Casting (Converting Between Types)
// Implicit conversion (automatic, but can lose data) int a = 10; double b = a; // OK: int to double, no data loss int c = 3.99; // WARNING: double to int, decimal part lost (c = 3) // Explicit casting (recommended C++ way) double pi = 3.14159; int rounded = static_cast<int>(pi); // rounded = 3 // Useful example: integer division fix int total = 7, count = 2; double average = static_cast<double>(total) / count; // 3.5, not 3
Control Flow: if, else, and switch
// Simple if/else int age = 20; if (age >= 18) { cout << "You are an adult" << endl; } else if (age >= 13) { cout << "You are a teenager" << endl; } else { cout << "You are a child" << endl; } // Ternary operator (shorthand for simple if/else) string status = (age >= 18) ? "adult" : "minor"; // Switch statement (use when comparing one variable to many values) int day = 3; switch (day) { case 1: cout << "Monday"; break; case 2: cout << "Tuesday"; break; case 3: cout << "Wednesday"; break; case 4: cout << "Thursday"; break; case 5: cout << "Friday"; break; default: cout << "Weekend"; }
Loops: for, while, and do while
// Basic for loop: print numbers 1 to 5 for (int i = 1; i <= 5; i++) { cout << i << " "; // Output: 1 2 3 4 5 } // Range based for loop (C++11): iterate over a collection vector<string> fruits = {"apple", "banana", "cherry"}; for (const string& fruit : fruits) { cout << fruit << endl; // prints each fruit on a new line } // While loop: runs as long as condition is true int count = 5; while (count > 0) { cout << count << " "; // Output: 5 4 3 2 1 count--; } // Do while loop: always runs at least once int input; do { cout << "Enter a positive number: "; cin >> input; } while (input <= 0); // keeps asking until positive // break and continue for (int i = 1; i <= 10; i++) { if (i == 3) continue; // skip 3 if (i == 8) break; // stop at 8 cout << i << " "; // Output: 1 2 4 5 6 7 }
Functions

Functions let you organize code into reusable blocks. Think of them as mini programs within your program. Each function has a name, takes some inputs (parameters), does something, and optionally returns a result.

// Simple function that adds two numbers int add(int a, int b) { return a + b; } // Function with default parameter void greet(string name, string greeting = "Hello") { cout << greeting << ", " << name << "!" << endl; } // greet("Alice"); // prints: Hello, Alice! // greet("Bob", "Good morning"); // prints: Good morning, Bob! // Function overloading: same name, different parameters double area(double radius) { return 3.14159 * radius * radius; // area of circle } double area(double length, double width) { return length * width; // area of rectangle } // Pass by reference: modify the original variable void doubleIt(int& num) { num *= 2; // changes the original variable, not a copy } int x = 5; doubleIt(x); // x is now 10 // Pass by const reference: read only, no copy (efficient for large objects) void printVector(const vector<int>& v) { for (int num : v) cout << num << " "; }
Arrays and Strings
// C style array (fixed size, avoid in modern C++) int scores[5] = {90, 85, 78, 92, 88}; cout << scores[0]; // 90 (first element) // std::array (C++11, fixed size, safer) #include <array> array<int, 5> arr = {1, 2, 3, 4, 5}; arr.at(2); // 3 (bounds checked, throws if out of range) arr.size(); // 5 // std::vector (dynamic size, most commonly used) #include <vector> vector<int> nums = {10, 20, 30}; nums.push_back(40); // add to end: {10, 20, 30, 40} nums.pop_back(); // remove last: {10, 20, 30} nums.size(); // 3 // std::string (much safer than char arrays) #include <string> string greeting = "Hello"; greeting += " World"; // "Hello World" greeting.length(); // 11 greeting.substr(0, 5); // "Hello" greeting.find("World"); // 6 (position where "World" starts) greeting.empty(); // false

Best Practice: In modern C++, prefer std::vector over C style arrays. Vectors automatically manage their memory, can grow and shrink, and provide bounds checking with .at().

Reading Input and Printing Output in C++

Input and output (I/O) is one of the first things you need to learn. C++ uses streams for I/O. Think of a stream as a flow of data. cout sends data out to the screen, and cin brings data in from the keyboard.

Basic Output with cout
#include <iostream> using namespace std; int main() { // Print text cout << "Hello!" << endl; // Print multiple things on one line string name = "Alice"; int age = 25; cout << name << " is " << age << " years old." << endl; // Output: Alice is 25 years old. // New line: endl vs "\n" cout << "Line 1" << endl; // flushes buffer (slower) cout << "Line 2\n"; // just new line (faster) return 0; }
Basic Input with cin
#include <iostream> #include <string> using namespace std; int main() { // Read a number int age; cout << "Enter your age: "; cin >> age; cout << "You are " << age << " years old.\n"; // Read a single word (cin stops at spaces) string firstName; cout << "Enter your first name: "; cin >> firstName; // Read a full line (including spaces) cin.ignore(); // clear the leftover newline from previous cin string fullName; cout << "Enter your full name: "; getline(cin, fullName); cout << "Hello, " << fullName << "!\n"; return 0; }
Reading Multiple Values
// Read two numbers on one line int x, y; cout << "Enter two numbers: "; cin >> x >> y; cout << "Sum = " << x + y << endl; // Read until end of input (useful in competitive programming) int num; while (cin >> num) { cout << "You entered: " << num << endl; } // Press Ctrl+D (Linux/Mac) or Ctrl+Z (Windows) to stop
File Input and Output
#include <fstream> // for file I/O #include <iostream> #include <string> using namespace std; int main() { // Writing to a file ofstream outFile("output.txt"); outFile << "Hello, File!" << endl; outFile << "Line 2" << endl; outFile.close(); // Reading from a file ifstream inFile("output.txt"); string line; while (getline(inFile, line)) { cout << line << endl; // prints each line from the file } inFile.close(); return 0; }
Formatting Output
#include <iomanip> // for setw, setprecision, fixed // Fixed decimal places double pi = 3.14159265358979; cout << fixed << setprecision(2) << pi << endl; // 3.14 cout << fixed << setprecision(4) << pi << endl; // 3.1416 // Column alignment (great for tables) cout << left << setw(15) << "Name" << setw(10) << "Score" << endl; cout << left << setw(15) << "Alice" << setw(10) << 95 << endl; cout << left << setw(15) << "Bob" << setw(10) << 87 << endl;
Fast I/O for Competitive Programming
// Add these two lines at the start of main() for faster I/O ios_base::sync_with_stdio(false); // unsync C and C++ I/O cin.tie(nullptr); // untie cin from cout // This makes cin/cout nearly as fast as scanf/printf // Important: do NOT mix cout with printf after this

Common Mistake: When using cin >> followed by getline(), the newline character from pressing Enter is left in the buffer. Always add cin.ignore(); between them to clear it.

Object Oriented Programming in C++

Object Oriented Programming (OOP) is a way of organizing code around objects that combine data and behavior. Instead of having separate functions and variables floating around, you group related data and the functions that operate on that data into classes. This makes code easier to understand, maintain, and reuse.

The four pillars of OOP are: Encapsulation (bundling data and methods together), Inheritance (creating new classes from existing ones), Polymorphism (one interface, many implementations), and Abstraction (hiding complex details behind simple interfaces).

Classes and Objects: The Basics

A class is a blueprint or template. An object is an actual instance created from that blueprint. Think of a class as the design plan for a house, and an object as an actual house built from that plan.

// Define a class (the blueprint) class Student { private: // only accessible inside this class string name; int age; double gpa; public: // accessible from anywhere // Constructor: called automatically when creating an object Student(string n, int a, double g) : name(n), age(a), gpa(g) {} // initializer list // Getter methods (read private data) string getName() const { return name; } int getAge() const { return age; } double getGpa() const { return gpa; } // Method: a function that belongs to the class void introduce() const { cout << "Hi, I'm " << name << ", age " << age << ", GPA: " << gpa << endl; } // Setter with validation void setGpa(double newGpa) { if (newGpa >= 0.0 && newGpa <= 4.0) { gpa = newGpa; } else { cout << "Invalid GPA!" << endl; } } }; // Create objects (actual instances) Student alice("Alice", 20, 3.8); Student bob("Bob", 22, 3.5); alice.introduce(); // Hi, I'm Alice, age 20, GPA: 3.8 bob.setGpa(3.9); // Updates Bob's GPA
Constructors and Destructors
class BankAccount { private: string owner; double balance; public: // Default constructor (no arguments) BankAccount() : owner("Unknown"), balance(0.0) {} // Parameterized constructor BankAccount(string o, double b) : owner(o), balance(b) {} // Copy constructor BankAccount(const BankAccount& other) : owner(other.owner), balance(other.balance) { cout << "Account copied!" << endl; } // Destructor: called automatically when object goes out of scope ~BankAccount() { cout << owner << "'s account closed." << endl; } void deposit(double amount) { if (amount > 0) balance += amount; } void withdraw(double amount) { if (amount > 0 && amount <= balance) balance -= amount; else cout << "Cannot withdraw " << amount << endl; } double getBalance() const { return balance; } }; // Usage { BankAccount acc("Alice", 1000); acc.deposit(500); // balance: 1500 acc.withdraw(200); // balance: 1300 } // destructor called here: "Alice's account closed."
Inheritance: Building on Existing Classes

Inheritance lets you create a new class based on an existing one. The new class (child/derived) gets all the features of the parent (base) class and can add its own. This avoids duplicating code.

// Base class (parent) class Animal { protected: // accessible in this class and child classes string name; int age; public: Animal(string n, int a) : name(n), age(a) {} virtual void speak() const { cout << name << " makes a sound" << endl; } void info() const { cout << name << ", age " << age << endl; } virtual ~Animal() = default; // virtual destructor is important }; // Derived class (child) class Dog : public Animal { string breed; public: Dog(string n, int a, string b) : Animal(n, a), breed(b) {} void speak() const override { // override parent's method cout << name << " says: Woof!" << endl; } void fetch() const { // new method only in Dog cout << name << " fetches the ball!" << endl; } }; class Cat : public Animal { public: Cat(string n, int a) : Animal(n, a) {} void speak() const override { cout << name << " says: Meow!" << endl; } }; // Polymorphism: use base class pointer to call derived class methods Dog rex("Rex", 3, "Golden Retriever"); Cat whiskers("Whiskers", 5); Animal* pets[] = {&rex, &whiskers}; for (Animal* pet : pets) { pet->speak(); // Rex says: Woof! then Whiskers says: Meow! }
Access Modifiers Explained
Access LevelSame ClassChild ClassOutside Code
publicYesYesYes
protectedYesYesNo
privateYesNoNo
Abstract Classes and Interfaces

An abstract class has at least one pure virtual function (declared with = 0). You cannot create objects from an abstract class directly. It serves as a contract that child classes must follow.

// Abstract class (cannot be instantiated) class Shape { public: virtual double area() const = 0; // pure virtual: MUST be implemented virtual double perimeter() const = 0; // pure virtual void describe() const { // regular method (shared by all) cout << "Area: " << area() << ", Perimeter: " << perimeter() << endl; } virtual ~Shape() = default; }; class Rectangle : public Shape { double width, height; public: Rectangle(double w, double h) : width(w), height(h) {} double area() const override { return width * height; } double perimeter() const override { return 2 * (width + height); } }; 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; } }; // Using polymorphism Rectangle r(5, 3); Circle c(4); r.describe(); // Area: 15, Perimeter: 16 c.describe(); // Area: 50.2655, Perimeter: 25.1327
Operator Overloading

C++ lets you define custom behavior for operators (+, -, ==, <<, etc.) when used with your own classes. This makes your code more intuitive and readable.

class Vector2D { public: double x, y; Vector2D(double x = 0, double y = 0) : x(x), y(y) {} // Add two vectors Vector2D operator+(const Vector2D& other) const { return Vector2D(x + other.x, y + other.y); } // Compare two vectors bool operator==(const Vector2D& other) const { return x == other.x && y == other.y; } // Print a vector with cout friend ostream& operator<<(ostream& os, const Vector2D& v) { return os << "(" << v.x << ", " << v.y << ")"; } }; Vector2D a(1, 2), b(3, 4); Vector2D c = a + b; // uses our custom + operator cout << c << endl; // (4, 6) — uses our custom << operator cout << (a == b) << endl; // 0 (false)
The Rule of Five (and Rule of Zero)

If your class manages a resource (raw pointer, file handle, etc.), you should define all five special member functions. But in modern C++, the best approach is the Rule of Zero: use smart pointers and STL containers so you do not need to write any of them.

// Rule of Five: define all five if you manage raw resources class Buffer { int* data; size_t size; public: Buffer(size_t n) : data(new int[n]), size(n) {} // constructor ~Buffer() { delete[] data; } // 1. destructor Buffer(const Buffer& o) // 2. copy constructor : data(new int[o.size]), size(o.size) { copy(o.data, o.data + size, data); } Buffer& operator=(const Buffer& o) { // 3. copy assignment if (this != &o) { delete[] data; size = o.size; data = new int[size]; copy(o.data, o.data + size, data); } return *this; } Buffer(Buffer&& o) noexcept // 4. move constructor : data(o.data), size(o.size) { o.data = nullptr; o.size = 0; } Buffer& operator=(Buffer&& o) noexcept { // 5. move assignment if (this != &o) { delete[] data; data = o.data; size = o.size; o.data = nullptr; o.size = 0; } return *this; } }; // Rule of Zero: use vector instead and you need NONE of the above! class BetterBuffer { vector<int> data; // vector handles all memory automatically public: BetterBuffer(size_t n) : data(n) {} // No destructor, copy/move constructors, or assignments needed! };

Modern C++ Tip: Always aim for the Rule of Zero. Use std::vector, std::string, and std::unique_ptr instead of raw pointers and arrays. The compiler will generate correct copy/move/destructor automatically.

How Memory Works in C++

Unlike languages like Java or Python, C++ does not have a garbage collector. You are responsible for managing memory yourself. This gives you incredible control and performance, but it also means you need to understand how memory allocation works to avoid bugs like memory leaks and dangling pointers.

Stack vs Heap Memory

Your program uses two main areas of memory: the stack and the heap. Understanding the difference is fundamental.

FeatureStackHeap
What is it?Fast, automatic memory for local variablesLarge, manually managed memory pool
AllocationAutomatic when you declare a variableManual using new keyword
DeallocationAutomatic when variable goes out of scopeManual using delete keyword
SpeedVery fast (just moves a pointer)Slower (OS must find free space)
SizeSmall (usually 1 to 8 MB)Large (limited by system RAM)
LifetimeTied to the scope (curly braces)Lives until you explicitly free it
Common ErrorStack overflow (too deep recursion)Memory leak (forgot to free)
// Stack allocation (automatic, preferred when possible) void stackExample() { int x = 42; // lives on the stack string name = "Alice"; // string object on stack (data may be on heap) vector<int> v = {1, 2, 3}; // vector object on stack (elements on heap) } // x, name, v all automatically cleaned up here // Heap allocation (manual, use when you need dynamic lifetime) void heapExample() { int* p = new int(42); // allocate one int on the heap cout << *p << endl; // prints 42 delete p; // free the memory p = nullptr; // good practice: set to null after delete int* arr = new int[5]; // allocate array of 5 ints on heap arr[0] = 10; arr[1] = 20; delete[] arr; // IMPORTANT: use delete[] for arrays }
Common Memory Bugs and How to Avoid Them
// Bug 1: Memory Leak (forgot to free memory) void leak() { int* p = new int(42); // oops, function returns without delete p // the memory is lost forever until program exits } // Bug 2: Dangling Pointer (using memory after freeing it) int* p = new int(42); delete p; // *p = 10; // CRASH or random behavior — p points to freed memory // Bug 3: Double Free (freeing memory twice) int* q = new int(42); delete q; // delete q; // CRASH — cannot free same memory twice // Bug 4: Mismatched new/delete int* arr = new int[10]; // delete arr; // WRONG — must use delete[] for arrays delete[] arr; // CORRECT
RAII: The C++ Way to Manage Resources

RAII stands for Resource Acquisition Is Initialization. The idea is simple: tie the lifetime of a resource (memory, file, lock) to the lifetime of an object. When the object is created, it acquires the resource. When the object is destroyed (goes out of scope), it releases the resource. This is the most important C++ idiom.

// RAII example: file handle that automatically closes class FileHandle { FILE* file; public: FileHandle(const char* path, const char* mode) : file(fopen(path, mode)) { if (!file) throw runtime_error("Failed to open file"); } ~FileHandle() { if (file) fclose(file); // automatic cleanup } // Prevent copying (one owner only) FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; FILE* get() { return file; } }; // Usage: file is guaranteed to close, even if an exception occurs { FileHandle f("data.txt", "r"); // read from f.get()... } // file automatically closed here

Key Takeaway: In modern C++, you should almost never write new and delete directly. Use smart pointers (unique_ptr, shared_ptr) for dynamic memory and STL containers (vector, string) for collections. They follow RAII automatically.

Understanding Pointers and References in C++

Pointers and references are what give C++ its power and flexibility. A pointer is a variable that stores the memory address of another variable. A reference is an alias (another name) for an existing variable. They are both ways to indirectly access data, but they have different rules.

Pointers: Step by Step
// Step 1: Create a normal variable int age = 25; // Step 2: Create a pointer that stores the address of age int* ptr = &age; // & gets the address of age // Step 3: Use the pointer cout << age; // 25 (the value) cout << &age; // 0x7fff5e3a (the address in memory) cout << ptr; // 0x7fff5e3a (same address, stored in ptr) cout << *ptr; // 25 (dereference: get the value at the address) // Step 4: Modify the value through the pointer *ptr = 30; // changes age to 30! cout << age; // 30 // Null pointer (points to nothing) int* empty = nullptr; if (empty == nullptr) { cout << "Pointer is null, safe to check!" << endl; }
Pointer Arithmetic
int numbers[] = {10, 20, 30, 40, 50}; int* p = numbers; // points to first element cout << *p; // 10 cout << *(p + 1); // 20 (next int, 4 bytes forward) cout << *(p + 2); // 30 p++; // move pointer to next element cout << *p; // 20 // Pointer arithmetic is type-aware: // int* moves by 4 bytes per step // char* moves by 1 byte per step // double* moves by 8 bytes per step
Const Pointers (What Can and Cannot Change)
int x = 10, y = 20; // 1. Pointer to const: cannot change the VALUE through the pointer const int* p1 = &x; // *p1 = 20; // ERROR: cannot modify value p1 = &y; // OK: can change what it points to // 2. Const pointer: cannot change the ADDRESS int* const p2 = &x; *p2 = 20; // OK: can modify value // p2 = &y; // ERROR: cannot change address // 3. Const pointer to const: cannot change ANYTHING const int* const p3 = &x; // *p3 = 20; // ERROR // p3 = &y; // ERROR // Easy way to remember: read right to left // const int* → pointer to a const int // int* const → const pointer to an int // const int* const → const pointer to a const int
References: Simpler and Safer
int original = 42; int& ref = original; // ref is now another name for original ref = 100; // original is now 100 cout << original; // 100 cout << ref; // 100 (same variable) // References vs Pointers: // ✓ References must be initialized when created // ✓ References cannot be reassigned to refer to something else // ✓ References cannot be null (always valid) // ✓ No special syntax to use (* or &), just use the name // Common use: function parameters void swap(int& a, int& b) { int temp = a; a = b; b = temp; } int x = 5, y = 10; swap(x, y); // x=10, y=5 — originals are swapped!
When to Use Pointers vs References
SituationUseWhy
Function parameters (read only)const T&No copy, cannot accidentally modify
Function parameters (modify original)T&Cleaner syntax than pointers
Optional parameter (might be null)T*References cannot be null
Dynamic memory / ownershipSmart pointersRAII, automatic cleanup
PolymorphismT* or T&Base class pointer/reference to derived object
Data structures (linked list, tree)T* or smart ptrNeed reassignable, nullable connections
Move Semantics: Transferring Ownership

C++11 introduced rvalue references (T&&) and move semantics. Instead of copying expensive objects, you can move them, transferring their internal resources in constant time.

// Without move semantics: expensive copy vector<string> v1 = {"hello", "world", "foo", "bar"}; vector<string> v2 = v1; // COPY: duplicates all data // With move semantics: fast transfer vector<string> v3 = std::move(v1); // MOVE: steals v1's data // v1 is now empty, v3 has the data // This is O(1), no matter how big the vector is! // std::move does not actually move anything // It just casts to an rvalue reference, signaling "you can steal from me"

Smart Pointers in C++ (C++11 and Later)

Smart pointers are one of the most important features of modern C++. They are RAII wrappers around raw pointers that automatically manage memory. When a smart pointer goes out of scope, it automatically frees the memory it owns. No manual delete needed. They are defined in the <memory> header.

unique_ptr: One Owner, Automatic Cleanup

unique_ptr is the most commonly used smart pointer. It owns a resource exclusively. When it goes out of scope, the resource is freed. It cannot be copied, only moved.

#include <memory> #include <iostream> using namespace std; // Creating a unique_ptr unique_ptr<int> p = make_unique<int>(42); // preferred way (C++14) cout << *p << endl; // 42 // Cannot copy (one owner only) // unique_ptr<int> p2 = p; // ERROR: cannot copy // Can move (transfer ownership) unique_ptr<int> p2 = move(p); // p2 now owns it, p is null // Using with custom classes class Player { public: string name; Player(string n) : name(n) { cout << name << " created\n"; } ~Player() { cout << name << " destroyed\n"; } }; { auto player = make_unique<Player>("Alice"); cout << player->name << endl; // Alice } // "Alice destroyed" printed automatically // With arrays auto arr = make_unique<int[]>(5); arr[0] = 10; arr[1] = 20;
shared_ptr: Multiple Owners

shared_ptr uses reference counting to allow multiple owners. The resource is freed when the last shared_ptr pointing to it is destroyed.

// Multiple shared_ptrs can point to the same object shared_ptr<int> sp1 = make_shared<int>(42); shared_ptr<int> sp2 = sp1; // both own the same int, ref count = 2 shared_ptr<int> sp3 = sp1; // ref count = 3 cout << sp1.use_count(); // 3 cout << *sp1; // 42 sp2.reset(); // sp2 releases, ref count = 2 sp3.reset(); // ref count = 1 // when sp1 goes out of scope, ref count = 0 and memory is freed
weak_ptr: Observing Without Owning

weak_ptr holds a reference to an object managed by shared_ptr without affecting the reference count. It is mainly used to break circular references that would otherwise cause memory leaks.

// weak_ptr does not keep the object alive shared_ptr<int> sp = make_shared<int>(100); weak_ptr<int> wp = sp; // does NOT increase ref count cout << wp.use_count(); // 1 (only sp counts) cout << wp.expired(); // false (sp still alive) // Must "lock" before using (converts to shared_ptr) if (auto locked = wp.lock()) { cout << *locked << endl; // 100 } else { cout << "Object no longer exists" << endl; } sp.reset(); // object destroyed cout << wp.expired(); // true (object is gone) // Circular reference example: use weak_ptr to break the cycle struct Node { shared_ptr<Node> next; // creates a cycle = memory leak! }; // Fix: change to weak_ptr<Node> next;
Which Smart Pointer Should You Use?
Smart PointerOwnershipCan Copy?OverheadWhen to Use
unique_ptrExclusive (one owner)No (move only)Zero (same as raw pointer)Default choice for everything
shared_ptrShared (many owners)YesReference count + control blockMultiple owners needed (rare)
weak_ptrNone (observer)YesSame as shared_ptrBreak cycles, caches, observers

Best Practice: Start with unique_ptr for everything. Only use shared_ptr when you genuinely need shared ownership. Always use make_unique and make_shared instead of new because they are exception safe and more efficient.

The Standard Template Library (STL)

The STL is one of C++'s greatest strengths. It provides ready to use containers (data structures), algorithms (sorting, searching), and iterators (for traversing containers). Using STL means you do not have to implement common data structures from scratch.

vector: Dynamic Array (Most Used Container)
#include <vector> #include <iostream> using namespace std; // Create and initialize vector<int> nums = {10, 20, 30}; vector<string> names(3, "empty"); // {"empty", "empty", "empty"} vector<int> zeros(5); // {0, 0, 0, 0, 0} // Add and remove elements nums.push_back(40); // add to end: {10, 20, 30, 40} nums.emplace_back(50); // same but constructs in place (faster for objects) nums.pop_back(); // remove last: {10, 20, 30, 40} // Access elements nums[0]; // 10 (no bounds checking) nums.at(0); // 10 (throws exception if out of bounds) nums.front(); // 10 (first element) nums.back(); // 40 (last element) // Size and capacity nums.size(); // 4 (number of elements) nums.empty(); // false nums.clear(); // remove all elements // Loop through vector for (int n : nums) cout << n << " "; // range based for for (int i = 0; i < nums.size(); i++) // index based cout << nums[i] << " ";
map and unordered_map: Key Value Storage
#include <map> #include <unordered_map> // map: sorted by key, O(log n) operations, uses red-black tree map<string, int> ages; ages["Alice"] = 25; ages["Bob"] = 30; ages.insert({"Charlie", 28}); // Check if key exists if (ages.count("Alice")) { cout << "Alice is " << ages["Alice"] << endl; } // Loop through map (sorted by key) for (const auto& [name, age] : ages) { // C++17 structured binding cout << name << ": " << age << endl; } // unordered_map: unsorted, O(1) average operations, uses hash table unordered_map<string, int> scores; scores["math"] = 95; scores["english"] = 87; // Faster for lookups, but no guaranteed order
set and unordered_set: Unique Collections
#include <set> #include <unordered_set> // set: sorted unique elements, O(log n) set<int> uniqueNums = {5, 3, 1, 3, 5}; // stores {1, 3, 5} (sorted, no duplicates) uniqueNums.insert(2); // {1, 2, 3, 5} uniqueNums.count(3); // 1 (exists) uniqueNums.count(4); // 0 (not found) uniqueNums.erase(3); // {1, 2, 5} // unordered_set: O(1) average, uses hash table unordered_set<string> words = {"cat", "dog", "bird"};
stack, queue, and priority_queue
#include <stack> #include <queue> // Stack: Last In, First Out (LIFO) stack<int> st; st.push(1); st.push(2); st.push(3); cout << st.top(); // 3 (last pushed) st.pop(); // removes 3 // Queue: First In, First Out (FIFO) queue<int> q; q.push(1); q.push(2); q.push(3); cout << q.front(); // 1 (first pushed) q.pop(); // removes 1 // Priority Queue: highest value comes first (max heap) priority_queue<int> pq; pq.push(3); pq.push(1); pq.push(4); pq.push(1); cout << pq.top(); // 4 (largest) // Min heap (smallest first) priority_queue<int, vector<int>, greater<int>> minHeap; minHeap.push(3); minHeap.push(1); minHeap.push(4); cout << minHeap.top(); // 1 (smallest)
STL Algorithms: Sorting, Searching, and More
#include <algorithm> #include <numeric> vector<int> v = {5, 2, 8, 1, 9, 3}; // Sorting sort(v.begin(), v.end()); // {1, 2, 3, 5, 8, 9} sort(v.begin(), v.end(), greater<int>()); // {9, 8, 5, 3, 2, 1} // Custom sort (by absolute value) sort(v.begin(), v.end(), [](int a, int b) { return abs(a) < abs(b); }); // Searching (vector must be sorted for binary_search) sort(v.begin(), v.end()); bool found = binary_search(v.begin(), v.end(), 5); // true // Find (works on unsorted too, returns iterator) auto it = find(v.begin(), v.end(), 8); if (it != v.end()) cout << "Found: " << *it; // Count occurrences int c = count(v.begin(), v.end(), 3); // Min and Max auto minIt = min_element(v.begin(), v.end()); auto maxIt = max_element(v.begin(), v.end()); cout << "Min: " << *minIt << ", Max: " << *maxIt; // Sum all elements int total = accumulate(v.begin(), v.end(), 0); // Reverse reverse(v.begin(), v.end()); // Remove duplicates (must be sorted first) sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end()); // Transform: double every element transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 2; });
Container Complexity Quick Reference
ContainerAccessInsertDeleteSearchBest For
vectorO(1)O(1) end, O(n) middleO(n)O(n)General purpose, random access
dequeO(1)O(1) both endsO(n)O(n)Need fast front/back insert
listO(n)O(1) anywhereO(1)O(n)Frequent insert/delete in middle
set/mapO(log n)O(log n)O(log n)O(log n)Sorted data, moderate speed
unordered_set/mapO(1) avgO(1) avgO(1) avgO(1) avgFast lookups, no order needed

Templates: Write Code That Works with Any Type

Templates let you write a function or class once and have it work with any data type. Instead of writing separate functions for int, double, string, etc., you write one template and the compiler generates the specific versions for you. This is called generic programming.

Function Templates
// Without templates: need separate functions for each type int maxInt(int a, int b) { return a > b ? a : b; } double maxDouble(double a, double b) { return a > b ? a : b; } // With templates: one function works for ALL types template<typename T> T maxVal(T a, T b) { return a > b ? a : b; } // The compiler creates the right version automatically cout << maxVal(3, 7); // uses int version cout << maxVal(3.14, 2.71); // uses double version cout << maxVal("abc"s, "xyz"s); // uses string version // Template with two different types template<typename T, typename U> auto add(T a, U b) { return a + b; // compiler figures out the return type } cout << add(5, 3.14); // int + double = double (8.14)
Class Templates
// A generic stack that works with any type template<typename T> class SimpleStack { vector<T> data; public: void push(const T& value) { data.push_back(value); } void pop() { data.pop_back(); } T& top() { return data.back(); } bool empty() const { return data.empty(); } size_t size() const { return data.size(); } }; // Use with different types SimpleStack<int> intStack; intStack.push(1); intStack.push(2); cout << intStack.top(); // 2 SimpleStack<string> strStack; strStack.push("hello"); strStack.push("world"); cout << strStack.top(); // "world"
Template Specialization

Sometimes you need a template to behave differently for a specific type. Template specialization lets you provide a custom implementation for one particular type.

// General template template<typename T> bool isZero(T value) { return value == T{}; // works for int, char, etc. } // Specialization for double (floating point needs epsilon comparison) template<> bool isZero<double>(double value) { return abs(value) < 1e-9; // within epsilon of zero } isZero(0); // uses general template: true isZero(0.0000001); // uses double specialization: true
Variadic Templates (C++11): Any Number of Arguments
// A print function that accepts any number of any types template<typename... Args> void print(Args... args) { (cout << ... << args) << endl; // fold expression (C++17) } print(1, " + ", 2, " = ", 3); // prints: 1 + 2 = 3 print("Hello", " ", "World"); // prints: Hello World // Sum any number of values template<typename... Args> auto sum(Args... args) { return (args + ...); // fold expression } cout << sum(1, 2, 3, 4); // 10

Handling Errors Gracefully with Exceptions

Exceptions provide a structured way to handle errors in C++. Instead of checking error codes after every function call, you can throw an exception when something goes wrong and catch it at a higher level where you can actually deal with the error.

Try, Catch, and Throw
#include <stdexcept> // Function that might fail double divide(double a, double b) { if (b == 0) { throw runtime_error("Cannot divide by zero!"); } return a / b; } // Calling code handles the error try { double result = divide(10, 0); cout << result << endl; } catch (const runtime_error& e) { cout << "Error: " << e.what() << endl; // Output: Error: Cannot divide by zero! } catch (const exception& e) { cout << "General error: " << e.what() << endl; } catch (...) { cout << "Unknown error occurred" << endl; }
Standard Exception Types
ExceptionWhen to UseExample
runtime_errorErrors detectable only at runtimeFile not found, network timeout
logic_errorProgramming logic mistakesInvalid argument, out of range
invalid_argumentBad function argumentNegative value where positive expected
out_of_rangeIndex out of boundsAccessing vector beyond its size
overflow_errorArithmetic overflowNumber too large to represent
bad_allocMemory allocation failednew fails to allocate
Creating Custom Exceptions
class InsufficientFundsError : public runtime_error { double amount; double balance; public: InsufficientFundsError(double amt, double bal) : runtime_error("Insufficient funds"), amount(amt), balance(bal) {} double getAmount() const { return amount; } double getBalance() const { return balance; } }; // Usage void withdraw(double amount, double& balance) { if (amount > balance) { throw InsufficientFundsError(amount, balance); } balance -= amount; } try { double bal = 100; withdraw(500, bal); } catch (const InsufficientFundsError& e) { cout << e.what() << endl; cout << "Tried: $" << e.getAmount() << ", Have: $" << e.getBalance() << endl; }
noexcept: Promise Not to Throw
// Mark functions that will never throw int add(int a, int b) noexcept { return a + b; // simple math, cannot fail } // Move constructors should be noexcept for vector optimization class MyClass { public: MyClass(MyClass&& other) noexcept { // transfer resources } };

Important: Never throw exceptions from destructors. If a destructor throws while another exception is being processed (during stack unwinding), the program calls std::terminate() and crashes.

Lambda Expressions: Inline Functions (C++11)

A lambda is an anonymous function that you can define right where you need it. Instead of writing a separate named function, you write it inline. Lambdas are especially useful with STL algorithms like sort, find_if, and for_each.

Lambda Syntax
// Basic lambda syntax: // [capture](parameters) -> return_type { body } // Simplest lambda auto greet = []() { cout << "Hello from a lambda!" << endl; }; greet(); // call it // Lambda with parameters auto add = [](int a, int b) { return a + b; }; cout << add(3, 4); // 7 // Lambda with explicit return type auto divide = [](int a, int b) -> double { return static_cast<double>(a) / b; }; cout << divide(7, 2); // 3.5
Captures: Using Variables from Outside
int multiplier = 3; string prefix = "Result: "; // Capture by value (copies the variable) auto multiply = [multiplier](int x) { return x * multiplier; }; cout << multiply(5); // 15 // Capture by reference (uses the original variable) int total = 0; auto addTo = [&total](int x) { total += x; // modifies the original total }; addTo(5); addTo(3); cout << total; // 8 // Capture everything by value auto f1 = [=]() { cout << multiplier << prefix; }; // Capture everything by reference auto f2 = [&]() { total += multiplier; }; // Mix: total by reference, multiplier by value auto f3 = [&total, multiplier](int x) { total += x * multiplier; };
Practical Lambda Examples
vector<int> numbers = {5, 2, 8, 1, 9, 3}; // Sort in descending order sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; // {9, 8, 5, 3, 2, 1} }); // Find first element greater than 4 auto it = find_if(numbers.begin(), numbers.end(), [](int x) { return x > 4; }); cout << *it; // 9 // Count even numbers int evens = count_if(numbers.begin(), numbers.end(), [](int x) { return x % 2 == 0; }); // Apply function to each element for_each(numbers.begin(), numbers.end(), [](int x) { cout << x << " "; }); // Remove elements that match condition (erase-remove idiom) numbers.erase( remove_if(numbers.begin(), numbers.end(), [](int x) { return x < 3; }), numbers.end() );
Generic Lambdas (C++14)
// auto parameters make lambdas work with any type auto print = [](const auto& value) { cout << value << endl; }; print(42); // works with int print(3.14); // works with double print("hello"); // works with string // Generic comparator auto descending = [](const auto& a, const auto& b) { return a > b; }; vector<int> nums = {3, 1, 4}; sort(nums.begin(), nums.end(), descending);

Modern C++ Begins: C++11 and C++14

C++11 was a revolutionary update that transformed C++ into a modern language. It introduced features that make code shorter, safer, and more expressive. C++14 polished these features further. Together, they are the foundation of modern C++ programming.

auto Type Deduction
// Let the compiler figure out the type auto x = 42; // int auto pi = 3.14; // double auto name = string("Alice"); // string auto nums = vector<int>{1, 2, 3}; // vector<int> // Useful with complex types map<string, vector<int>> data; auto it = data.begin(); // much cleaner than map<string, vector<int>>::iterator
Range Based For Loops
vector<string> colors = {"red", "green", "blue"}; // Read only for (const auto& color : colors) { cout << color << endl; } // Modify elements vector<int> nums = {1, 2, 3}; for (auto& n : nums) { n *= 2; // doubles each element in place }
nullptr (Replacing NULL)
// Old way (ambiguous: is it int 0 or pointer?) int* p1 = NULL; // works but NULL is actually integer 0 // New way (type safe) int* p2 = nullptr; // clearly a null pointer, not an integer // Why it matters: void process(int x) { cout << "int version"; } void process(int* ptr) { cout << "pointer version"; } // process(NULL); // ambiguous! which overload? process(nullptr); // clearly calls the pointer version
Initializer Lists and Uniform Initialization
// Brace initialization works everywhere int x{5}; vector<int> v{1, 2, 3}; map<string, int> m{{"a", 1}, {"b", 2}}; // Prevents narrowing conversions (catches bugs at compile time) // int y{3.14}; // ERROR: double to int would lose data int z = 3.14; // silently truncates to 3 (old style)
enum class (Scoped Enumerations)
// Old enum (pollutes the namespace, implicitly converts to int) enum OldColor { RED, GREEN, BLUE }; int x = RED; // accidentally used as int, no warning // New enum class (scoped, type safe) enum class Color { Red, Green, Blue }; enum class Size { Small, Medium, Large }; Color c = Color::Red; Size s = Size::Large; // int y = Color::Red; // ERROR: cannot implicitly convert int y = static_cast<int>(Color::Red); // explicit conversion: 0
delegating and inheriting Constructors
class Connection { string host; int port; bool secure; public: // Main constructor Connection(string h, int p, bool s) : host(h), port(p), secure(s) {} // Delegating constructor: calls another constructor Connection(string h, int p) : Connection(h, p, false) {} Connection(string h) : Connection(h, 80, false) {} Connection() : Connection("localhost") {} };
static_assert: Compile Time Checks
// Verify assumptions at compile time, not runtime static_assert(sizeof(int) == 4, "int must be 4 bytes"); static_assert(sizeof(void*) == 8, "must be 64 bit system"); // Useful in templates template<typename T> T safeDivide(T a, T b) { static_assert(is_arithmetic_v<T>, "T must be a number type"); return a / b; }
C++14 Additions
// Generic lambdas (auto parameters) auto multiply = [](auto a, auto b) { return a * b; }; multiply(3, 4); // 12 multiply(2.5, 4.0); // 10.0 // Return type deduction for functions auto square(int x) { return x * x; } // return type deduced as int // Binary literals int mask = 0b11001010; // binary literal // Digit separators for readability int million = 1'000'000; double pi = 3.141'592'653; int hex = 0xFF'FF'FF'FF; // make_unique (was missing in C++11) auto p = make_unique<int>(42);

C++17: Making C++ Simpler and More Expressive

C++17 brought many quality of life improvements that make everyday C++ code cleaner. Structured bindings, optional values, compile time branching, and filesystem support are just a few highlights.

Structured Bindings: Unpack Any Aggregate
// Unpack a pair pair<string, int> p = {"Alice", 25}; auto [name, age] = p; // name = "Alice", age = 25 // Unpack map entries (no more .first and .second!) map<string, int> scores = {{"Alice", 95}, {"Bob", 87}}; for (const auto& [student, score] : scores) { cout << student << ": " << score << endl; } // Unpack a struct struct Point { double x, y; }; Point origin{0.0, 0.0}; auto [x, y] = origin; // x = 0.0, y = 0.0 // Unpack an array int arr[] = {1, 2, 3}; auto [a, b, c] = arr; // a=1, b=2, c=3
std::optional: Maybe a Value, Maybe Not
#include <optional> // A function that might not find what you are looking for optional<string> findUser(int id) { if (id == 1) return "Alice"; if (id == 2) return "Bob"; return nullopt; // nothing found } auto user = findUser(1); if (user.has_value()) { cout << "Found: " << *user << endl; // "Found: Alice" } cout << findUser(99).value_or("Guest"); // "Guest"
std::variant: Type Safe Union
#include <variant> // Can hold exactly one of these types at a time variant<int, double, string> data; data = 42; // holds int data = 3.14; // now holds double data = "hello"; // now holds string // Check what it currently holds if (holds_alternative<string>(data)) { cout << get<string>(data); // "hello" } // Visit pattern (handle all types) visit([](auto&& val) { cout << val << endl; }, data);
if constexpr: Compile Time Branching
// Different code generated for different types template<typename T> string typeInfo(T value) { if constexpr (is_integral_v<T>) { return "Integer: " + to_string(value); } else if constexpr (is_floating_point_v<T>) { return "Float: " + to_string(value); } else { return "Other type"; } } cout << typeInfo(42); // "Integer: 42" cout << typeInfo(3.14); // "Float: 3.140000"
std::string_view: Read Strings Without Copying
#include <string_view> // string_view is just a pointer + length, no copy void greet(string_view name) { cout << "Hello, " << name << "!" << endl; } greet("Alice"); // from string literal, no copy string s = "Bob"; greet(s); // from std::string, no copy // Use string_view for read only string parameters // Use const string& when you need to store a copy later
std::filesystem: File and Directory Operations
#include <filesystem> namespace fs = filesystem; // Check if file/directory exists if (fs::exists("myfile.txt")) { cout << "File size: " << fs::file_size("myfile.txt") << endl; } // Path manipulation fs::path p = "/home/user/data.txt"; cout << p.filename(); // "data.txt" cout << p.extension(); // ".txt" cout << p.parent_path(); // "/home/user" // Create directories fs::create_directories("output/logs/2026"); // Copy and rename files fs::copy("source.txt", "backup.txt"); fs::rename("old.txt", "new.txt"); // List all files in a directory for (const auto& entry : fs::directory_iterator(".")) { cout << entry.path().filename() << endl; }
C++17 Feature Summary
FeatureWhat It DoesExample Use Case
Structured BindingsUnpack pairs, tuples, structsLooping over map entries cleanly
std::optionalValue that might not existFunctions that may fail to find something
std::variantType safe unionConfig values that can be int, string, or bool
if constexprCompile time if/elseTemplate code that behaves differently per type
string_viewNon owning string referenceRead only string function parameters
std::filesystemFile/directory operationsListing files, checking existence, copying
Fold ExpressionsOperate on variadic argsSum all template arguments
CTADDeduce template argsWrite pair(1, "hi") instead of pair<int,string>

C++20: The Next Big Leap

C++20 is considered the biggest update since C++11. It introduces concepts for constraining templates, ranges for composable data processing, coroutines for async programming, and modules for faster compilation.

Concepts: Constrain Your Templates

Before C++20, if you passed the wrong type to a template, you got pages of incomprehensible error messages. Concepts let you specify what types a template accepts, giving you clear error messages.

#include <concepts> // Define a concept: T must support + operator and be constructible from int template<typename T> concept Addable = requires(T a, T b) { { a + b } -> same_as<T>; }; // Use the concept to constrain the template template<Addable T> T add(T a, T b) { return a + b; } add(3, 4); // OK: int is Addable add(1.5, 2.5); // OK: double is Addable // add(mutex{}, mutex{}); // ERROR: clear message "mutex does not satisfy Addable" // Built in concepts template<integral T> // T must be an integer type T doubleIt(T x) { return x * 2; } template<floating_point T> // T must be float/double T half(T x) { return x / 2.0; } // Shorthand with auto void printSortable(sortable auto&& container) { sort(container.begin(), container.end()); }
Ranges: Composable Data Processing
#include <ranges> vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // Chain operations with the pipe operator auto result = nums | views::filter([](int n) { return n % 2 == 0; }) // keep evens | views::transform([](int n) { return n * n; }) // square them | views::take(3); // first 3 for (int n : result) cout << n << " "; // 4 16 36 // Ranges are lazy: nothing computed until you iterate // This means they are very efficient for large datasets // Generate a range of numbers for (int n : views::iota(1, 6)) { cout << n << " "; // 1 2 3 4 5 }
The Spaceship Operator (Three Way Comparison)
#include <compare> struct Point { int x, y; auto operator<=>(const Point&) const = default; // This single line gives you: ==, !=, <, >, <=, >= for FREE! }; Point a{1, 2}, b{3, 4}; cout << (a < b); // true cout << (a == b); // false cout << (a != b); // true
std::span: A View into Contiguous Data
#include <span> // span is like string_view but for arrays/vectors void printAll(span<const int> data) { for (int n : data) cout << n << " "; cout << endl; } vector<int> v = {1, 2, 3, 4, 5}; int arr[] = {10, 20, 30}; printAll(v); // works with vector printAll(arr); // works with C array printAll({v.data(), 3}); // works with first 3 elements
std::format: Python Style String Formatting
#include <format> string name = "Alice"; int age = 25; double gpa = 3.85; // Format strings (similar to Python f-strings) string msg = format("{} is {} years old with GPA {:.2f}", name, age, gpa); // "Alice is 25 years old with GPA 3.85" // Alignment and padding format("{:<10}", "left"); // "left " format("{:>10}", "right"); // " right" format("{:^10}", "center"); // " center " // C++23 adds std::print for direct output // print("Hello, {}!\n", name); // no need for cout
C++20 Feature Quick Reference
FeatureWhat It DoesCompiler Support
ConceptsConstrain templates with requirementsGCC 10+, Clang 10+, MSVC 19.26+
RangesComposable lazy data pipelinesGCC 10+, Clang 15+, MSVC 19.29+
CoroutinesSuspendable/resumable functionsGCC 10+, Clang 14+, MSVC 19.28+
ModulesReplace headers for faster compilationPartial support in all major compilers
Spaceship <=>Auto generate comparison operatorsGCC 10+, Clang 10+, MSVC 19.26+
std::spanNon owning view into contiguous dataGCC 10+, Clang 10+, MSVC 19.26+
std::formatType safe string formattingGCC 13+, Clang 14+, MSVC 19.29+
Calendar/TimezoneDate and time handlingGCC 11+, Clang 14+, MSVC 19.29+

Multithreading in C++ (C++11 and Later)

Modern C++ provides built in support for multithreading. You can run code in parallel across multiple CPU cores to speed up your programs. The key components are threads (units of execution), mutexes (for protecting shared data), and futures (for getting results from async operations).

Creating Threads
#include <thread> #include <iostream> using namespace std; // Function to run in a thread void sayHello(string name, int times) { for (int i = 0; i < times; i++) { cout << "Hello from " << name << "!\n"; } } int main() { // Create threads thread t1(sayHello, "Thread 1", 3); thread t2(sayHello, "Thread 2", 3); // Wait for threads to finish t1.join(); // blocks until t1 finishes t2.join(); // blocks until t2 finishes // Lambda thread thread t3([]() { cout << "Hello from lambda thread!\n"; }); t3.join(); // How many cores does this machine have? cout << "CPU cores: " << thread::hardware_concurrency() << endl; return 0; }
Mutex: Protecting Shared Data

When multiple threads access the same variable, you get a race condition. A mutex (mutual exclusion) ensures only one thread can access the shared data at a time.

#include <mutex> mutex mtx; // the lock int counter = 0; // shared data void increment(int times) { for (int i = 0; i < times; i++) { lock_guard<mutex> lock(mtx); // locks on creation, unlocks on destruction counter++; // safe: only one thread at a time } } // Run from two threads thread t1(increment, 100000); thread t2(increment, 100000); t1.join(); t2.join(); cout << counter; // always 200000 (without mutex, it could be less!)
async and future: Easy Parallel Results
#include <future> // Run a function asynchronously and get the result later int expensiveCalculation(int x) { this_thread::sleep_for(chrono::seconds(2)); // simulate work return x * x; } // Launch async tasks future<int> result1 = async(launch::async, expensiveCalculation, 10); future<int> result2 = async(launch::async, expensiveCalculation, 20); // Both run in parallel! Total time: ~2 seconds, not 4 cout << result1.get(); // blocks until done, returns 100 cout << result2.get(); // returns 400
Atomic: Lock Free Thread Safety
#include <atomic> // atomic operations are thread safe without a mutex atomic<int> counter{0}; void increment(int times) { for (int i = 0; i < times; i++) { counter++; // atomic increment, no mutex needed! } } // Other atomic operations counter.store(42); // write int val = counter.load(); // read counter.fetch_add(5); // add 5, returns old value
Condition Variables: Thread Communication
#include <condition_variable> mutex mtx; condition_variable cv; bool dataReady = false; int sharedData = 0; // Producer thread: prepares data then signals thread producer([&]() { sharedData = 42; { lock_guard<mutex> lock(mtx); dataReady = true; } cv.notify_one(); // wake up the consumer }); // Consumer thread: waits for data thread consumer([&]() { unique_lock<mutex> lock(mtx); cv.wait(lock, [&] { return dataReady; }); // wait until ready cout << "Got: " << sharedData << endl; // 42 }); producer.join(); consumer.join();

Interview Tip: Know the difference between mutex (exclusive, one thread at a time), shared_mutex (many readers OR one writer), and atomic (lock free, simple types only). Use atomic for simple counters, mutex for complex operations, and shared_mutex for read heavy workloads.

Writing Clean, Safe, and Modern C++ Code

Following best practices will help you write C++ code that is easier to read, maintain, and debug. These guidelines are drawn from the C++ Core Guidelines maintained by Bjarne Stroustrup and Herb Sutter.

Memory and Resource Management
  • Never use raw new/delete Use make_unique and make_shared instead. Let RAII handle cleanup for you.
  • Prefer stack allocation Only use dynamic allocation when you actually need dynamic lifetime or polymorphism.
  • Use containers over raw arrays std::vector, std::array, and std::string are almost always better than C style arrays.
  • Follow the Rule of Zero Design classes so they do not need custom destructors, copy constructors, or assignment operators.
Function Design
// Pass small types by value void process(int x); // Pass large types by const reference (read only, no copy) void display(const vector<int>& data); // Pass strings as string_view for read only void greet(string_view name); // Use reference when you need to modify the argument void populate(vector<int>& out); // Use smart pointers for ownership transfer void takeOwnership(unique_ptr<Widget> w); // caller gives up ownership void shareOwnership(shared_ptr<Widget> w); // caller shares ownership // Return by value (compiler optimizes copies away via RVO) vector<int> generateData() { vector<int> result = {1, 2, 3}; return result; // no copy thanks to Return Value Optimization }
Safety and Correctness
  • Use const everywhere possible Mark variables, parameters, and member functions as const when they should not change. This catches bugs at compile time.
  • Prefer enum class over plain enum Scoped enums prevent name collisions and implicit conversions.
  • Initialize all variables Uninitialized variables contain garbage data. Always initialize on declaration.
  • Use .at() for bounds checking vec.at(i) throws an exception if i is out of range. vec[i] silently causes undefined behavior.
  • Mark move constructors noexcept This lets std::vector use moves instead of copies during reallocation.
  • Compile with warnings Always use -Wall -Wextra flags. Treat warnings as errors with -Werror in production code.
Modern C++ Idioms
// Use auto for complex types, explicit types for simple ones auto it = myMap.find("key"); // good: complex iterator type int count = 0; // good: simple, clear type // Use structured bindings for readability for (const auto& [key, value] : myMap) { ... } // Prefer algorithms over raw loops // Instead of this: for (int i = 0; i < v.size(); i++) { if (v[i] > 10) count++; } // Write this: int count = count_if(v.begin(), v.end(), [](int x) { return x > 10; }); // Use emplace_back instead of push_back for objects vector<pair<string, int>> v; v.emplace_back("Alice", 25); // constructs in place, no temporary // Use std::optional instead of sentinel values optional<int> find(const vector<int>& v, int target); // clear: might not find // int find(...); // bad: what does -1 mean? what about 0?

Golden Rule: Write code that is easy to read first, optimize later only if profiling shows it is necessary. Premature optimization is the root of all evil. The compiler is very good at optimizing clear, idiomatic C++ code.

Common C++ Interview Questions Answered

What is the difference between C and C++?

C is a procedural programming language focused on functions and step by step execution. C++ extends C with object oriented programming (classes, inheritance, polymorphism), generic programming (templates), exception handling, the STL, and modern features like smart pointers, lambdas, and move semantics. C++ is backward compatible with most C code, meaning valid C code usually compiles in a C++ compiler.

What is the difference between a pointer and a reference?

A pointer is a variable that stores a memory address. It can be null, reassigned to point to different objects, and requires dereferencing with * to access the value. A reference is an alias for an existing variable. It must be initialized when created, cannot be null, cannot be reassigned, and uses the same syntax as a normal variable. Use references when you can, pointers when you must.

What is polymorphism in C++?

Polymorphism means "many forms." In C++, there are two types. Compile time polymorphism is achieved through function overloading and templates. Runtime polymorphism is achieved through virtual functions and inheritance. When you call a virtual function through a base class pointer, the correct derived class version is called based on the actual object type.

What are the differences between stack and heap memory?

Stack memory is fast, automatically managed, and limited in size (usually 1 to 8 MB). Variables on the stack are destroyed when they go out of scope. Heap memory is slower to allocate, can be very large (limited by RAM), and must be managed manually using new/delete or smart pointers. Use stack for local variables and heap for objects that need to outlive their creating scope.

When should I use unique_ptr vs shared_ptr?

Use unique_ptr by default. It has zero overhead compared to a raw pointer and clearly shows exclusive ownership. Only switch to shared_ptr when multiple parts of your code genuinely need to share ownership of the same object, which is actually rare. Never use shared_ptr just because you are not sure about ownership; instead, clarify your design.

What is RAII and why is it important?

RAII (Resource Acquisition Is Initialization) is the most important C++ idiom. The idea is that resource lifetime (memory, files, locks) is tied to object lifetime. You acquire a resource in the constructor and release it in the destructor. Since C++ guarantees destructors are called when objects go out of scope, resources are always cleaned up, even if an exception occurs. Smart pointers, lock_guard, and fstream all use RAII.

What is the Rule of Five?

If your class needs a custom destructor, copy constructor, or copy assignment operator, it probably needs all five special member functions: destructor, copy constructor, copy assignment, move constructor, and move assignment. However, the best practice is the Rule of Zero: use smart pointers and STL containers so you do not need any of them.

How do I choose which STL container to use?

Use vector as your default. It is the fastest for sequential access and has good cache performance. Use unordered_map/unordered_set when you need fast lookups by key. Use map/set when you need sorted order. Use deque when you frequently add/remove from both ends. Use list only when you need constant time insert/erase at arbitrary positions with stable iterators.

What C++ standard should I use for interviews?

Target C++17 for most interviews. It is widely supported and includes the most useful modern features (structured bindings, optional, variant, string_view, filesystem). Knowing C++11 features (auto, lambdas, smart pointers, move semantics) is essential. C++20 features (concepts, ranges) are a bonus that shows you are up to date.