Mastery Series · File 01 of 04
C++ Programming

Foundations
From Zero to Confident

Everything you need to go from absolute zero to writing real C++ programs. Environment setup, syntax, data types, operators, control flow, functions, arrays, pointers, and references — with full code at every step.

Ch 1 · Setup & First Program Ch 2 · Variables & Data Types Ch 3 · Operators & Expressions Ch 4 · Control Flow Ch 5 · Functions Ch 6 · Arrays & Strings Ch 7 · Pointers Ch 8 · References & Memory Ch 9 · Traps & Master Reference
Chapter 01
Setup & First Program — Your C++ Environment

1.1   What is C++ and Why Learn It Core

C++ is a compiled, statically-typed, general-purpose language created by Bjarne Stroustrup at Bell Labs in 1979 as "C with Classes." It gives you the raw performance of C combined with high-level abstractions like OOP, templates, and the STL. It remains the language of choice for systems programming, game engines, competitive programming, embedded systems, and high-frequency trading.

Mental Model: The Bridge

Think of C++ as a bridge. On one side, you have high-level human logic (classes, objects, complex algorithms). On the other, you have the raw hardware (registers, memory addresses, CPU cycles). C++ is unique because it lets you stand on both sides at once. You can write code that reads like a story, while simultaneously keeping precise control over every byte of memory.

Why C++ is fast

Compiles directly to machine code. No virtual machine, no garbage collector. You control memory allocation and deallocation explicitly.

Where C++ is used

Chrome, Firefox, VS Code, Unreal Engine, MySQL, Linux kernel drivers, NASA spacecraft software, every major game engine.

C vs C++

C++ is a superset of C. Valid C code is (nearly) valid C++. C++ adds OOP, templates, references, and the entire Standard Library.

C++ Standards

C++98 → C++03 → C++11 (big leap) → C++14 → C++17 → C++20 → C++23. We write modern C++ (C++17) throughout this series. Always compile with -std=c++17.

1.2   Setting Up Your Environment Core

You need two things: a compiler (turns your code into machine instructions) and an editor/IDE (where you write code). The compiler is the essential part.

The Compilation Pipeline [ Source Code (.cpp) ] ──▶ [ Preprocessor ] ──▶ [ Compiler ] ──▶ [ Assembler ] ──▶ [ Linker ] ──▶ [ Binary (.exe/out) ] │ │ │ │ │ Human text Header pasting Code to ASM ASM to Machine Joining Raw CPU (hello.cpp) (#include) (hello.s) (hello.o) Libraries instructions

Installing a Compiler

Windows

Install MinGW-w64 (GCC for Windows) or use Visual Studio Community (has MSVC compiler built in). Recommended: VS Code + MinGW.

macOS

Run xcode-select --install in Terminal. This installs Clang. Alternatively install GCC via Homebrew: brew install gcc.

Linux

GCC is usually pre-installed. If not: sudo apt install g++ on Ubuntu/Debian. Verify with g++ --version.

Compiling from the Terminal

Once GCC/G++ is installed, the compile-run cycle is:

# Compile a single file g++ hello.cpp -o hello # Compile with C++17 standard (always do this) g++ -std=c++17 hello.cpp -o hello # Compile with warnings enabled (strongly recommended) g++ -std=c++17 -Wall -Wextra hello.cpp -o hello # Run the output ./hello # Linux/Mac hello.exe # Windows
Use -Wall Always

The -Wall -Wextra flags enable all compiler warnings. Treat every warning as an error. Warnings are the compiler catching bugs you haven't noticed yet. Senior programmers never ignore warnings.

1.3   Your First C++ Program — Anatomy Core

Every C++ program has the same fundamental skeleton. Learn this structure cold — you will type it thousands of times.

Mental Model: The Envelope and the Letter

Think of writing a C++ program like sending a formal letter. The #include statements at the top are like checking your reference books before you write. The int main() { ... } block is the letter itself—the exact instructions you want the computer to follow. When the computer executes your program, it doesn't just read randomly; it always looks for the "Dear Computer" line, which in C++ is exactly int main().

// hello.cpp — The anatomy of every C++ program #include <iostream> // 1. Preprocessor: include the I/O library using namespace std; // 2. Use the std namespace (discussed in ch5) int main() { // 3. main() — program execution starts here cout << "Hello, World!" << endl; // 4. Output to console return 0; // 5. Signal success to the OS (0 = success) }
Anatomy of main()

int main() is the entry point of every C++ program. It returns an int0 means success, any non-zero value signals an error to the operating system. The OS uses this return value; always return 0 on success.

Breaking Down Each Line

ElementWhat it isRequired?
#include <iostream>Preprocessor directive. Pastes the iostream header into your file before compilation.Yes for I/O
using namespace std;Allows writing cout instead of std::cout. Convenient but has trade-offs (see ch5).Optional
int main()The mandatory entry point. Every program must have exactly one.Always
cout << ... << endl;cout = character output stream. << = insertion operator. endl = newline + flush.For output
return 0;Return success code to OS. In main() only, this can be omitted (compiler adds it), but always include it explicitly.Best practice
endl vs "\n" — Performance Trap

endl outputs a newline AND flushes the output buffer. "\n" only outputs a newline. In loops or heavy output code, using endl can be 10-100x slower. Always prefer "\n" unless you explicitly need a flush. This is a classic competitive programming trap.

Write a program that prints your name and age on separate lines.

#include <iostream> using namespace std; int main() { cout << "Name: Arjun\n"; cout << "Age: 20\n"; // Or combine with multiple << operators: cout << "Name: " << "Arjun" << "\n" << "Age: " << 20 << "\n"; return 0; }

1.4   Basic Input with cin Core

cin (character input) reads from the keyboard. The extraction operator >> pulls data from the input stream into a variable.

Mental Model: The Conveyor Belt

Imagine your keyboard is connected to a conveyor belt called the "input stream". Every time you type a character and press Enter, those characters drop onto the belt. The command cin >> age; is like a robot arm that grabs characters off the conveyor belt, interprets them as a number, and drops that number into the age variable box. The arrows >> point from the conveyor (cin) right into your variable.

#include <iostream> using namespace std; int main() { int age; double height; cout << "Enter your age: "; cin >> age; // Read one integer cout << "Enter your height (m): "; cin >> height; // Read one double cout << "Age: " << age << ", Height: " << height << "\n"; return 0; } // Chain reads: cin >> a >> b reads two values separated by whitespace // int a, b; cin >> a >> b;
cin Leaves the Newline in the Buffer

When you cin >> someInt, the newline character from pressing Enter stays in the buffer. If you then call getline(), it reads that leftover newline as an empty string. Fix: call cin.ignore() between them, or use cin.ignore(numeric_limits<streamsize>::max(), '\n').

1.5   Chapter Traps & Key Points

As a beginner, you will encounter these points repeatedly. Understanding the "why" behind these rules will save you hours of debugging.

1. The Silent Failure of Defaults

Many beginners compile with just g++ file.cpp. This is a trap. By default, the compiler uses an older C++ standard and hides many useful warnings. Always include -std=c++17 and -Wall. Think of -Wall (Warnings: All) as a code reviewer sitting next to you; it points out logical flaws before they become runtime crashes.

Trap: endl vs "\n" Performance

You will see endl used everywhere in beginner tutorials. Ignore them. In C++, endl does two things: it adds a newline AND it forces the program to pause and "flush" everything to the screen. Flushing is slow. In a loop printing 100,000 lines, endl can make your program 100x slower. Always use "\n" unless you are writing a debugger or have a specific reason to flush.

2. Case Sensitivity and Syntax

C++ is completely case-sensitive. Main is not main, and Cout is not cout. To the compiler, these are entirely different names. Similarly, the semicolon ; is the "period" at the end of a C++ sentence. Forgetting it is like leaving a sentence hanging; the compiler won't know where one thought ends and the next begins.

Pro Tip: Return 0

In Unix-like systems (and internally in Windows), a program communicates its success status to the operating system using an integer. Zero is the universal signal for "All good, I finished successfully." Any other number is a "cry for help" or an error code. Always return 0 unless your program actually failed.

↑ back to top
Chapter 02
Variables & Data Types — Memory, Values & Storage

2.1   Variables — Named Memory Locations Core

A variable is a named location in memory that holds a value. In C++, every variable has a type (what kind of data it stores), a name (how you refer to it), and a value (what's currently stored there).

Mental Model: The Memory Locker

Imagine a long hallway of lockers. Each locker has an address (a number painted on the door). A variable is like puting a name-tag on one of those lockers. Instead of remembering "Address 0x7ffd5a," you just remember "score." The type determines the size of the locker — an int locker is smaller than a long long locker.

Variable Declaration vs Initialization

Declaration creates the variable: int age; — memory is allocated but the value is garbage (whatever was in that memory).
Initialization sets the first value: int age = 20; — always prefer this.
Assignment changes the value later: age = 21;

// Three ways to initialize in modern C++ int a = 10; // Copy initialization (C-style, fine) int b(10); // Direct initialization int c{10}; // Uniform (brace) initialization — PREFERRED in modern C++ // Brace init prevents narrowing conversions: int x = 3.7; // Compiles: silently truncates to 3 (BUG!) int y{3.7}; // COMPILE ERROR — brace init catches this. Safer!
Always Use Brace Initialization

Prefer int x{10}; over int x = 10; in modern C++. Brace initialization is the most uniform syntax, works for all types including containers, and prevents silent narrowing conversions. It's a hallmark of clean modern C++.

Naming Rules

  • Can contain letters, digits, underscores. Must start with a letter or underscore.
  • Case-sensitive: myVar, MyVar, and MYVAR are three different variables.
  • Cannot be a C++ keyword: int, class, return, etc.
  • Convention: use camelCase for variables, UPPER_SNAKE for constants, PascalCase for types.

2.2   Fundamental Data Types Most Tested

C++ has several built-in (fundamental) types. These map directly to how the CPU and memory work. Knowing their sizes and ranges is critical for avoiding overflow bugs.

Motivation: Why So Many Types?

In languages like Python or JavaScript, a "number" is just a number. So why does C++ have int, short, long long, float, and double? Performance and Memory. If you need to store the age of a person (0-120), giving it 8 bytes of memory (a long long) is incredibly wasteful if you have an array of a billion people. C++ forces you to choose the right box size for your data so the CPU can pack it efficiently and process it instantly.

TypeSizeRange / NotesTypical Use
bool1 bytetrue (1) or false (0)Flags, conditions
char1 byte-128 to 127 (signed) or 0–255 (unsigned)Single characters, ASCII
int4 bytes-2,147,483,648 to 2,147,483,647 (~±2.1 billion)General integers
long long8 bytes-9.2×10¹⁸ to 9.2×10¹⁸Large numbers, CP problems
float4 bytes~7 decimal digits precisionGraphics (not finance)
double8 bytes~15 decimal digits precisionScientific, financial calc
unsigned int4 bytes0 to 4,294,967,295Sizes, indices (non-negative)
Trap: Integer Overflow

If you have an int and you keep adding to it, eventually it hits the limit (around 2.1 billion). When it goes one step further, it doesn't stop — it "wraps around" to a large negative number. This is like an odometer in a car hitting 999,999 and rolling back to 000,000. In competitive programming, this is the #1 cause of "Wrong Answer." Always use long long if your result could exceed 1 billion.

// Size check at compile time #include <iostream> using namespace std; int main() { cout << "int: " << sizeof(int) << " bytes\n"; cout << "long long: " << sizeof(long long) << " bytes\n"; cout << "double: " << sizeof(double) << " bytes\n"; cout << "char: " << sizeof(char) << " bytes\n"; return 0; } // sizeof() returns the size in bytes. It's evaluated at compile time.

2.3   auto, const, and Type Aliases Core

auto — Let the Compiler Deduce the Type

Motivation: Don't Repeat Yourself

If you write std::vector<std::string>::iterator it = myVec.begin();, you're explicitly telling the compiler what type you are making. But the compiler already knows myVec.begin() returns that exact type! The auto keyword was introduced to save you from writing redundant, massive type names.

The auto keyword tells the compiler to deduce the variable's type from its initializer. It's not "dynamic typing" — the type is still fixed at compile time. It just saves you from writing long type names.

auto x = 42; // x is int auto pi = 3.14159; // pi is double auto name = "Alice"; // name is const char* (NOT std::string) auto flag = true; // flag is bool // Critically useful with iterators (covered in STL chapter): // auto it = myVector.begin(); -- saves writing std::vector<int>::iterator

const — Values That Never Change

const int MAX_SIZE = 1000; // Cannot be modified after initialization const double PI = 3.14159265358979; // constexpr — evaluated at COMPILE TIME (faster, use for true constants) constexpr int ARRAY_SIZE = 100; constexpr double GRAVITY = 9.81; // Rule: anything that should never change should be const or constexpr // This prevents accidental modification and enables compiler optimizations
const vs constexpr

const means "don't change this at runtime." constexpr means "evaluate this at compile time." Use constexpr for true compile-time constants (array sizes, math constants). Use const for runtime values that shouldn't change (like a function parameter you promise not to modify).

typedef and using — Type Aliases

// Old way (typedef) — still common in legacy code typedef long long ll; typedef unsigned int uint; // Modern way (using) — preferred in C++11+ using ll = long long; using ull = unsigned long long; using pii = std::pair<int,int>; // In competitive programming you'll often see: using ll = long long; #define int long long // Hacky but common in CP — replaces ALL int with long long

2.4   Scope, Lifetime & Storage Duration Important

Scope is where in your code a variable can be accessed. Lifetime is how long it exists in memory. These are different concepts and confusing them causes bugs.

Mental Model: One-Way MIRRORS and Lifespans

Think of your code's curly braces { ... } as a room with one-way mirrors. If you are inside a room, you can see out into the larger house (the global scope) or the outer room (the parent block). But people in the outer room cannot see in. When you exit a room (reach the closing brace }), everything that was created inside that room is instantly deleted. That is exactly how scope (who can see what) and lifetime (when things are deleted) operate in C++.

int globalVar = 100; // Global scope: accessible anywhere, lives for program lifetime int main() { int localVar = 5; // Local scope: only visible inside main() { // Anonymous block — creates a new scope int blockVar = 10; cout << localVar; // OK: can see outer scope cout << blockVar; // OK: in scope } // blockVar is DESTROYED here // cout << blockVar; ERROR: blockVar out of scope static int count = 0; // Static local: persists between function calls count++; cout << count; // Increments every time this code runs return 0; }
Storage Classes

auto (default local) — lives on the stack, destroyed when scope ends.
static — lives in static memory for the entire program duration; for local statics, retains value between calls.
extern — declares that a variable is defined in another translation unit.
register — hint to store in CPU register (ignored by modern compilers).

Global Variables — Use Sparingly

Global variables are accessible and modifiable from anywhere. This makes code hard to reason about, test, and debug. In professional code, avoid globals. In competitive programming, globals are acceptable for performance (avoid stack overflow with large arrays) — but understand the trade-off.

2.5   Chapter Traps & Key Points

Variables are the building blocks of your program, but they come with hidden traps that even experienced programmers occasionally fall into.

1. The "Garbage" Value Trap

In C++, if you declare a variable like int x; and don't give it a value, it doesn't start at zero. It starts with whatever "garbage" was left in that memory location by the last program that used it. Always initialize your variables at the moment of creation using the brace syntax: int x{0};. Modern C++ makes this easy and safe.

Trap: Floating Point Precision

Computers cannot represent numbers like 0.1 perfectly in binary. If you add 0.1 ten times, you might get 0.999999999 instead of 1.0. Never use == to compare two floating-point numbers. Instead, check if the difference between them is very small: if (abs(a - b) < 1e-9).

2. Char is Actually a Number

A char like 'A' is stored in memory as the number 65 (its ASCII code). This is a feature, not a bug! It means you can do math with characters. For example, '5' - '0' gives you the integer 5. Use this to your advantage in problems involving digits and letters.

Pro Tip: LL Literals

When using long long, get into the habit of adding LL to your numbers, e.g., long long large = 2000000000LL;. Without the LL, the compiler might treat the number as a regular int first, potentially causing an overflow before the value is even assigned to the long long variable.

↑ back to top
Chapter 03
Operators & Expressions — Math, Logic & Bit Manipulation

3.1   Arithmetic Operators Core

Computers are, at their heart, massive calculators. Arithmetic operators are the basic instructions you give to the CPU to manipulate data. Whether you're calculating a player's health or the trajectory of a spacecraft, these are the tools you'll use.

Mental Model: The Assembly Line

Imagine an assembly line where data items (operands) move through machines (operators). An + machine takes two numbers and produces their sum. A / machine takes two numbers and produces their quotient. The output of one machine can become the input for the next, forming an expression.

int a = 17, b = 5; cout << a + b << "\n"; // 22 — addition cout << a - b << "\n"; // 12 — subtraction cout << a * b << "\n"; // 85 — multiplication cout << a / b << "\n"; // 3 — INTEGER division (truncates!) cout << a % b << "\n"; // 2 — modulo (remainder) // For true division, cast to double first: cout << (double)a / b << "\n"; // 3.4 cout << static_cast<double>(a) / b << "\n"; // 3.4 (preferred modern cast)
Integer Division Silently Truncates

7 / 2 gives 3, not 3.5. The decimal is silently dropped. This is correct for integer division, but if you expect a floating-point result, you must cast: (double)7 / 2 gives 3.5. Forgetting this causes wrong answers in geometry and percentage problems.

Compound Assignment Operators

a += 5; // a = a + 5 a -= 3; // a = a - 3 a *= 2; // a = a * 2 a /= 4; // a = a / 4 a %= 3; // a = a % 3 // Increment / Decrement a++; // Post-increment: use a, THEN increment ++a; // Pre-increment: increment FIRST, then use a--; // Post-decrement --a; // Pre-decrement // The difference matters in expressions: int x = 5; int y = x++; // y = 5, x = 6 (y gets old value) int z = ++x; // z = 7, x = 7 (z gets new value)

3.2   Comparison & Logical Operators Core

Comparison operators return bool (true or false). They are the foundation of all conditions and loops.

Mental Model: The Bouncer at the Door

Think of comparison operators like a bouncer at a club checking IDs. The bouncer only returns one of two answers: "Yes" (true) or "No" (false). Similarly, logical operators (&&, ||) are like multiple bouncers talking to each other. "Is this person over 18 AND do they have a ticket?" Both must agree for the final answer to be True.

// Comparison operators — all return bool int a = 5, b = 10; a == b // false — equal to a != b // true — not equal a < b // true — less than a > b // false — greater than a <= b // true — less than or equal a >= b // false — greater than or equal // Logical operators (a < b) && (a > 0) // AND: true if BOTH sides are true (a < b) || (a > 100) // OR: true if EITHER side is true !(a == b) // NOT: flips true/false
Short-Circuit Evaluation

In A && B, if A is false, B is never evaluated. In A || B, if A is true, B is never evaluated. This is called short-circuit evaluation. It's a performance feature AND a safety feature — you can write ptr != nullptr && ptr->value > 0 safely: if ptr is null, the second part is never reached.

= vs == — The Most Classic C++ Bug

if (x = 5) assigns 5 to x and then evaluates as true (since 5 is non-zero). It doesn't compare x to 5. if (x == 5) compares. To protect against this: some programmers write if (5 == x) — the compiler will error if you accidentally write if (5 = x) since you can't assign to a literal. Compiler warnings (-Wall) will also catch this.

3.3   Bitwise Operators — Power Tools CP Essential

Bitwise operators work directly on the binary representation of integers. They are extremely fast (single CPU instruction) and are used heavily in competitive programming, systems programming, and optimization.

Motivation: The Surgeon's Scalpel

Normal operators (+, *) treat a number like 5 as a single, indivisible concept. Bitwise operators treat that 5 as what it actually is in memory: a row of switches (00000101). Using bitwise operators is like taking a scalpel and flipping individual genetic switches inside the number. It's the most direct, lowest-level way to talk to the CPU.

OperatorNameExample (a=5=0101, b=3=0011)Result
a & bAND0101 & 00110001 = 1
a | bOR0101 | 00110111 = 7
a ^ bXOR0101 ^ 00110110 = 6
~aNOT (flip all bits)~0101...11111010 = -6
a << 1Left shift0101 << 11010 = 10 (×2)
a >> 1Right shift0101 >> 10010 = 2 (÷2)
// Bitwise tricks every CP programmer must know // 1. Check if number is even or odd if (n & 1) cout << "odd"; // Faster than n % 2 == 1 if (!(n & 1)) cout << "even"; // 2. Multiply/divide by powers of 2 n << 3; // n * 8 (multiply by 2^3) n >> 2; // n / 4 (divide by 2^2, integer division) // 3. Swap two integers without temp variable a ^= b; b ^= a; a ^= b; // XOR swap trick // 4. Check if bit i is set bool isSet = (n >> i) & 1; // 5. Set bit i n |= (1 << i); // 6. Clear bit i n &= ~(1 << i); // 7. Toggle bit i n ^= (1 << i); // 8. Check if power of 2 (very common in CP) bool isPow2 = n && !(n & (n-1));

Operator Precedence (Most Important)

Precedence (High→Low)Operators
1 (highest)++ -- (postfix), (), [], ., ->
2++ -- (prefix), !, ~, unary -, * (deref), & (addr)
3* / %
4+ -
5<< >>
6< <= > >=
7== !=
8–10& then ^ then |
11&&
12||
13 (lowest)?: (ternary), = += etc.
When in Doubt, Parenthesize

Never rely on remembering precedence for complex expressions. Use parentheses to make intent explicit: (a & b) == 0 not a & b == 0 (the latter checks a & (b == 0) which is wrong!). Readability beats cleverness every time.

3.4   Chapter Traps & Key Points

Expressions are the logic of your program, but C++ has specific rules about how those expressions are evaluated. If you don't know the rules, your math will be wrong.

1. The Integer Division Trap

In C++, 7 / 2 is 3, not 3.5. When you divide two integers, the result must be an integer, so the decimal part is simply cut off. This is a common source of bugs in geometry and physics code. To get a decimal result, at least one of the numbers must be a double: 7.0 / 2 or static_cast<double>(7) / 2.

Trap: Assignment vs. Comparison

The single = is for assignment (setting a value). The double == is for comparison (checking equality). Writing if (x = 5) will set x to 5 and then evaluate as "true" because 5 is not zero. This is one of the most famous bugs in C history. Always use == for checks.

2. Short-Circuit Logic

In an && (AND) expression, if the first part is false, C++ doesn't even look at the second part. This is a safety feature! You can write if (ptr != nullptr && *ptr > 10) without crashing, because if ptr is null, the second part (which would cause a crash) is never executed.

Pro Tip: Parentheses are Free

Don't try to memorize the entire operator precedence table. It's complex and error-prone. Instead, use parentheses to make your intent explicit: (a & b) == 0 is much clearer than a & b == 0. Clear code is better than "clever" code.

↑ back to top
Chapter 04
Control Flow — Decisions, Loops & Branching

4.1   if / else if / else Core

Programs rarely run in a straight line. Control flow structures allow your code to make decisions and repeat actions based on data.

Mental Model: The Fork in the Road

Think of an if statement as a fork in a road. At the fork, there is a sign (the condition). If the sign is true, you take the left path. If it's false, you take the right path (the else). else if is like having multiple signs at the same fork, directing you to different paths based on which sign matches your situation.

// Basic if-else int score = 75; if (score >= 90) { cout << "Grade: A\n"; } else if (score >= 80) { cout << "Grade: B\n"; } else if (score >= 70) { cout << "Grade: C\n"; } else { cout << "Grade: F\n"; } // Ternary operator — compact if-else for single expressions int max = (a > b) ? a : b; // max = a if a>b, else b string sign = (n >= 0) ? "non-negative" : "negative"; // C++17: if with initializer — scopes the init variable to the if block if (int n = getValue(); n > 0) { cout << "Positive: " << n << "\n"; } // n is not accessible here
Always Use Braces — The Dangling Else Trap

Never write brace-less if/else for multi-statement blocks: if (x) doA(); doB();doB() always runs regardless of x. Always use { } even for single statements. The famous Apple SSL bug (goto fail) happened because of missing braces.

4.2   switch Statement Core

Use switch when comparing one value against multiple constant integer or char values. It compiles to a jump table — faster than a long if-else chain for many cases.

Motivation: The Train Switchyard

Imagine a long chain of if-else if-else as searching for a matching door in a long hallway: you have to check door 1, then door 2, then door 3, all the way down. A switch statement is like a train switchyard. The compiler looks at the incoming value and instantly flips the tracks to send execution directly to the correct case, without checking the others. It's cleaner to read and faster to execute when you have many exact-match options.

int day = 3; switch (day) { case 1: cout << "Monday\n"; break; // MUST break or execution falls through! case 2: cout << "Tuesday\n"; break; case 3: case 4: case 5: cout << "Weekday\n"; // Cases 3, 4, 5 all fall through to here break; default: cout << "Weekend\n"; break; }
Forgetting break — Fallthrough Bug

Without break, execution continues into the next case. This is called fallthrough. It's rarely intentional. Always add break or the C++17 attribute [[fallthrough]]; (to document intentional fallthrough). Missing break is one of the most common C++ bugs.

4.3   Loops — for, while, do-while Most Used

Computers excel at doing the same thing millions of times without getting tired. Loops are how you instruct the CPU to repeat a block of code.

Mental Model: The Running Track

A loop is like a lap around a running track. You start at a certain point (initialization), you check if you have more laps to go (condition), and every time you finish a lap, you record it (update/increment). You keep running until the condition is no longer met. A break is like jumping over the fence to stop early, and a continue is like skipping the rest of the current lap and starting the next one immediately.

for Loop — When You Know the Count

// Classic for loop — 3 parts: init; condition; update for (int i = 0; i < 5; i++) { cout << i << " "; // Prints: 0 1 2 3 4 } // Count down for (int i = 10; i >= 0; i--) cout << i << " "; // Multiple variables for (int i = 0, j = 10; i < j; i++, j--) { cout << i << " " << j << "\n"; } // Range-based for loop (C++11) — iterate over collections int arr[] = {1, 2, 3, 4, 5}; for (int x : arr) { cout << x << " "; // Prints each element } // Use auto& to avoid copying large objects for (auto& element : collection) { // & = reference, avoids copy element *= 2; // Can modify element }

while Loop — When Count is Unknown

// while: checks condition BEFORE each iteration int n; cin >> n; while (n > 0) { cout << n % 10; // Print last digit n /= 10; // Remove last digit } // do-while: checks condition AFTER — guarantees at least one execution int input; do { cout << "Enter positive number: "; cin >> input; } while (input <= 0); // Repeat until valid input

break, continue, goto

for (int i = 0; i < 10; i++) { if (i == 5) break; // Exit loop entirely when i==5 if (i % 2 == 0) continue; // Skip even numbers, go to next iteration cout << i << " "; // Prints: 1 3 } // Breaking out of nested loops — one pattern using goto (rare but valid): for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (found(i, j)) goto done; } } done: cout << "Exited nested loop\n"; // Cleaner alternative: use a flag variable or factor into a function

Print all prime numbers from 1 to 100.

for (int n = 2; n <= 100; n++) { bool isPrime = true; for (int d = 2; d * d <= n; d++) { // Only check up to sqrt(n) if (n % d == 0) { isPrime = false; break; } } if (isPrime) cout << n << " "; } // Output: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

Key insight: checking divisors only up to √n reduces checks from O(n) to O(√n) per number.

4.4   Chapter Traps & Key Points

Control flow is where logically complex bugs live. Most errors here come from misunderstanding exactly when a condition is checked or when a loop ends.

1. The Off-By-One Error

This is the most common loop bug. Should the loop go i < n or i <= n? If you're counting 10 items starting from 0, you go up to 9 (i < 10). If you go up to 10, you've tried to access an 11th item. Always trace the first and last iteration of your loops mentally to be sure.

Trap: The Dangling Else

If you have nested if statements without braces, an else will always attach to the nearest if. This can lead to logic that looks right but executes wrong. Rule: Always use braces { }, even for single-line statements. It makes your code future-proof and readable.

2. Switch Fallthrough

In a switch, if you forget a break, the program doesn't stop. It "falls through" into the next case. This is almost always a bug. C++17 added the [[fallthrough]] attribute to let you tell the compiler "I meant to do this," but 99% of the time, you just need a break.

Pro Tip: Loop Early Exit

If you're searching for something in a loop, use break as soon as you find it. There's no reason to keep running a loop 10,000 more times if you already have the answer. This is a simple but powerful performance optimization.

↑ back to top
Chapter 05
Functions — Reusable Logic, Recursion & Scope

5.1   Function Anatomy & Declarations Core

In programming, as in life, we don't do everything in one giant step. We break complex tasks into smaller, reusable pieces. Functions are the primary tool for this decomposition.

Mental Model: The Recipe / Contract

Think of a function as a recipe. It has ingredients (parameters), a process (the body), and a result (the return value). When you call a function, you are making a contract: "I will give you these specific inputs, and you promise to perform this action and give me back this result." Once the recipe is written, you can use it as many times as you want without knowing how it works inside.

Function Syntax

returnType functionName(paramType paramName, ...) { body; return value; }
A function takes zero or more inputs (parameters), executes some logic, and optionally returns one value. Functions with return type void return nothing.

// Function declaration (prototype) — tells compiler the function exists int add(int a, int b); // Declaration (forward declaration) // Function definition — the actual implementation int add(int a, int b) { return a + b; } // void function — no return value void greet(string name) { cout << "Hello, " << name << "!\n"; } // Function with default parameters double power(double base, int exp = 2) { // exp defaults to 2 double result = 1.0; for (int i = 0; i < exp; i++) result *= base; return result; } // power(3.0) = 9.0 (uses default exp=2) // power(3.0, 3) = 27.0 int main() { cout << add(3, 4) << "\n"; // 7 greet("Alice"); // Hello, Alice! return 0; }

5.2   Pass by Value, Reference & Pointer Critical

This is one of the most important concepts in C++. How you pass a variable to a function determines whether the function can modify the original.

Mental Model: The Photocopy vs. The Original Document

Pass by Value is like handing someone a photocopy of your document. They can scribble all over their copy, but your original stays perfect. (Safe, but making copies of books is slow).
Pass by Reference is like sliding the original document across the table. They can read it, but if they cross out a word and write a new one, your original document is permanently changed. (Fast, but requires trust).

// 1. PASS BY VALUE — function gets a COPY. Original unchanged. void doubleVal(int x) { x *= 2; // modifies the copy, NOT the original } int a = 5; doubleVal(a); cout << a; // Still 5! // 2. PASS BY REFERENCE — function gets the actual variable void doubleRef(int& x) { // & makes it a reference parameter x *= 2; // modifies the ORIGINAL } int b = 5; doubleRef(b); cout << b; // 10! Original modified. // 3. PASS BY CONST REFERENCE — efficient read-only access (best for large objects) void printName(const std::string& name) { // No copy, but can't modify cout << name; } // 4. PASS BY POINTER — like reference but can be null void doublePtr(int* x) { if (x != nullptr) *x *= 2; } int c = 5; doublePtr(&c); // Pass address of c cout << c; // 10
When to Use Each Passing Mode

Pass by value — small types (int, double, char, bool). Cheap to copy.
Pass by const& — large objects (string, vector, struct) that you only READ. Zero copy cost.
Pass by & — when you need to MODIFY the caller's variable.
Pass by pointer — when the argument might be nullptr, or for C-style APIs.

Write a swap function that actually swaps two integers.

void swap(int& a, int& b) { // Must pass by reference! int temp = a; a = b; b = temp; } int x = 10, y = 20; swap(x, y); cout << x << " " << y; // 20 10 — swapped! // Note: std::swap() in <algorithm> already does this.

5.3   Function Overloading & Inline Functions Core

Function overloading lets you have multiple functions with the same name but different parameter types or counts. The compiler picks the right version based on the arguments.

Motivation: One Concept, One Name

In the old C language, you couldn't reuse names. You had to write add_int(), add_float(), add_double(), and remember which to call. C++ fixed this. "Adding" is a single human concept, so C++ lets you just use the name add(). The compiler looks at the "shape" of the arguments you pass (e.g., two ints vs two floats) to figure out which exact function you meant.

// Overloaded functions — same name, different signatures 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; } cout << add(1, 2); // Calls int version: 3 cout << add(1.5, 2.5); // Calls double version: 4.0 cout << add(1, 2, 3); // Calls 3-arg version: 6 // inline — hint to compiler to paste function body at call site (no function call overhead) inline int square(int x) { return x * x; } // Modern compilers inline automatically when appropriate. The keyword is mostly a suggestion.

5.4   Recursion — Functions That Call Themselves CP Critical

Recursion is often considered a "threshold" topic—once you understand it, your perspective on problem-solving changes forever. A recursive function is simply a function that calls itself to solve a smaller version of the same problem.

Mental Model: The Stack of Plates

Imagine you're washing a stack of plates. You can't wash the bottom plate until you've washed all the ones above it. When a function calls itself, it puts the current task on pause and starts a "new copy" of the task on top of the old one. We call this the Call Stack. You keep adding plates to the stack until you reach the Base Case (the last plate). Then, you start finishing them one by one, working your way back down.

A recursive function calls itself to solve smaller versions of the same problem. Every recursion must have a base case (stopping condition) and a recursive case that moves toward the base case.

Anatomy of Recursion

1. Base case: The simplest input where the answer is known directly. Return immediately.
2. Recursive case: Break the problem into a smaller version of itself and recurse.
3. Progress: Each recursive call must get closer to the base case or you get infinite recursion (stack overflow).

// Classic: Factorial — n! = n × (n-1)! long long factorial(int n) { if (n <= 1) return 1; // Base case return n * factorial(n - 1); // Recursive case }
Call Stack Trace: factorial(4) Step 1 (Call): factorial(4) sees 4 > 1. Computes 4 * factorial(3). PAUSE. Step 2 (Call): factorial(3) sees 3 > 1. Computes 3 * factorial(2). PAUSE. Step 3 (Call): factorial(2) sees 2 > 1. Computes 2 * factorial(1). PAUSE. Step 4 (Base): factorial(1) hits the base case. Returns 1 immediately. Now the stack "unwinds", resolving the paused calculations from top down: Step 3 (Return): factorial(2) resumes: 2 * 1. Returns 2. Step 2 (Return): factorial(3) resumes: 3 * 2. Returns 6. Step 1 (Return): factorial(4) resumes: 4 * 6. Returns 24. Result: 24
// Fibonacci — fib(n) = fib(n-1) + fib(n-2) int fib(int n) { if (n <= 1) return n; // Base: fib(0)=0, fib(1)=1 return fib(n-1) + fib(n-2); // SLOW! O(2^n) — see memoization in File 04 } // GCD using Euclid's algorithm — clean recursive solution int gcd(int a, int b) { if (b == 0) return a; return gcd(b, a % b); } // gcd(48, 18) = gcd(18, 12) = gcd(12, 6) = gcd(6, 0) = 6 // Binary search — recursive version int bsearch(int arr[], int lo, int hi, int target) { if (lo > hi) return -1; // Base: not found int mid = lo + (hi - lo) / 2; // Avoid overflow vs (lo+hi)/2 if (arr[mid] == target) return mid; if (arr[mid] < target) return bsearch(arr, mid+1, hi, target); return bsearch(arr, lo, mid-1, target); }
Stack Overflow from Deep Recursion

Each recursive call uses stack memory. If recursion goes too deep (~10,000–100,000 levels on most systems), you get a stack overflow and the program crashes. For deep recursion, convert to an iterative solution with an explicit stack, or use tail-call optimization. Never use naive recursive Fibonacci for large n.

5.5   Namespaces & using namespace std Core

Mental Model: Last Names (Surnames)

Imagine working in an office with three people named "John." To avoid confusion, you call them "John Smith," "John Doe," and "John Clark." The last name isolates them. In C++, libraries are written by thousands of different programmers. If two libraries both create a function called sort(), your program won't compile because it doesn't know which one to use. Namespaces are like last names for code. The standard library's last name is std. So the official name of the output stream is std::cout.

// std is the namespace of the C++ standard library // Everything in the STL lives in std:: std::cout << "Hello\n"; // Fully qualified name std::string s = "world"; std::vector<int> v; // using namespace std; — imports all std names into current scope using namespace std; cout << "Hello\n"; // Now works without std:: // Better: selective using — import only what you need using std::cout; using std::string; // Define your own namespace namespace MyApp { int version = 1; void init() { cout << "Initializing v" << version << "\n"; } } MyApp::init(); // Access with ::
using namespace std in Headers — Never Do This

In a .cpp file, using namespace std; is fine (personal choice). But never put it in a header file (.h/.hpp). Headers are included in many .cpp files, so you'd force that namespace import on every file that includes yours — causing naming conflicts that are nearly impossible to debug.

5.6   Chapter Traps & Key Points

Functions are where you define the architecture of your program. Mistakes here often lead to "silent bugs" where the code runs but gives the wrong answer.

1. The "Pass-by-Value" Misconception

By default, C++ copies everything you send to a function. If you pass a 1GB file to a function, C++ will try to make a 1GB copy of it. This is slow! Rule: Pass small things (int, bool) by value, and large things (string, vector) by const reference. If you need the function to modify the original variable, pass it by reference (int&).

Trap: Returning Local References

Never return a reference to a variable created inside a function. Once the function ends, that variable is destroyed. Returning a reference to it is like giving someone the address of a house that has been demolished. It's called a Dangling Reference and it will crash your program unpredictably.

2. Recursion without an Exit

Every recursive function must have a Base Case. Without it, the function will call itself forever—or until it runs out of "stack space." When the stack fills up, your program crashes with a "Stack Overflow." This is the programming equivalent of an infinite loop, but it's more dangerous because it consumes memory until the crash.

Pro Tip: Inline for Speed

For very small functions that are called millions of times inside loops, use the inline keyword. This tells the compiler to copy the function's code directly into the location where it's called, skipping the tiny "cost" of a function call. Modern compilers are smart and do this automatically, but inline is still a useful hint.

↑ back to top
Chapter 06
Arrays & Strings — Collections, C-Strings & std::string

6.1   Arrays — Fixed-Size Collections Core

An array is a contiguous block of memory holding a fixed number of elements of the same type. Arrays in C++ are zero-indexed — the first element is at index 0.

Mental Model: The Apartment Building

Think of an array as an apartment building. The whole building is built at once (fixed size) and all apartments are identical (same type). If you know the address of the building, and you know you want apartment #4, you can walk directly there instantly because they are packed perfectly next to each other in memory. In C++, we start counting apartments from 0 instead of 1.

// Declaration and initialization int arr[5]; // Uninitialized — contains garbage! int arr2[5] = {1, 2, 3, 4, 5}; // Fully initialized int arr3[5] = {1, 2}; // Partially: {1, 2, 0, 0, 0} — rest zeroed int arr4[5] = {}; // All zeros int arr5[] = {10, 20, 30}; // Size deduced: 3 elements // Access and modification arr2[0] = 10; // Set first element cout << arr2[4]; // Access last element (index 4 for size 5) // Iterate int n = 5; for (int i = 0; i < n; i++) cout << arr2[i] << " "; // Size of array int size = sizeof(arr2) / sizeof(arr2[0]); // = 20 / 4 = 5
Array Out of Bounds — Undefined Behavior

Accessing arr[-1] or arr[5] on a size-5 array is undefined behavior — the program might crash, corrupt memory, or silently produce wrong results. C++ does NOT check array bounds at runtime (for performance). Always validate indices. Use std::vector with .at(i) if you want bounds checking.

2D Arrays

// 2D array: rows × columns int matrix[3][4]; // 3 rows, 4 columns — uninitialized int grid[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; // Access: row then column cout << grid[1][2]; // row 1, col 2 = 6 // Traverse 2D array for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { cout << grid[i][j] << " "; } cout << "\n"; }
Prefer std::vector Over Raw Arrays

In modern C++, prefer std::vector<int> over raw arrays. Vectors are dynamic (can resize), know their own size, can be returned from functions, and work with all STL algorithms. Use raw arrays only when you need stack allocation for performance, or are interfacing with C code.

6.2   std::string — The Right Way to Handle Text Core

Motivation: Safety and Convenience

Historically in C, text was handled as raw character arrays. If you tried to add "World" to "Hello", you had to manually allocate new memory, ensure there was enough space, and copy the bytes. std::string is a modern "smart" class that handles all that memory management for you. It grows automatically when you add to it, cleans up after itself, and provides tools to search and slice.

std::string (from <string>) is a dynamic, safe, feature-rich string class. Always prefer it over C-style char[] strings.

#include <string> using namespace std; // Construction string s1 = "Hello"; string s2("World"); string s3(5, 'x'); // "xxxxx" — 5 copies of 'x' string s4 = s1; // Copy // Common operations s1 + " " + s2 // Concatenation: "Hello World" s1 += "!"; // Append: s1 = "Hello!" s1.size() // Length: 6 (also .length()) s1.empty() // Is it empty? false s1[0] // Character access: 'H' s1.at(0) // Same but bounds-checked (throws exception) s1.substr(1, 3) // Substring from index 1, length 3: "ell" s1.find("ll") // Find position of "ll": returns 2 s1.find("xyz") // Not found: returns string::npos s1.replace(0, 1, "J") // Replace 1 char at index 0: "Jello!" // Comparison (lexicographic) s1 == s2 // false s1 < s2 // true (H < W in ASCII) // Convert between string and number string num = to_string(42); // int → string int n = stoi("42"); // string → int double d = stod("3.14"); // string → double // Reading full line (including spaces) string line; getline(cin, line); // Reads until newline

String Iteration and Character Operations

#include <cctype> // For character functions string s = "Hello World 123"; // Iterate over characters for (char c : s) cout << c; // Print each char for (auto& c : s) c = tolower(c); // Convert to lowercase in-place // Character classification isalpha('A') // true — is letter? isdigit('5') // true — is digit? isalnum('a') // true — is letter or digit? isspace(' ') // true — is whitespace? isupper('A') // true — is uppercase? islower('a') // true — is lowercase? toupper('a') // 'A' — convert to uppercase tolower('A') // 'a' — convert to lowercase

Count vowels and reverse a string.

string s = "Hello World"; // Count vowels int vowels = 0; string v = "aeiouAEIOU"; for (char c : s) { if (v.find(c) != string::npos) vowels++; } cout << "Vowels: " << vowels << "\n"; // 3 // Reverse using STL (include <algorithm>) reverse(s.begin(), s.end()); cout << s << "\n"; // "dlroW olleH" // Manual reverse int lo = 0, hi = s.size() - 1; while (lo < hi) { swap(s[lo++], s[hi--]); }

6.3   C-Style Strings (char arrays) Know It, Avoid It

Motivation: The Legacy of C

Why do we still learn C-style strings if std::string is better? Because C++ is built on top of C. Every time you write "Hello" in quotes, the compiler actually creates a C-style string, not a std::string. Furthermore, almost all operating system APIs (Windows, Linux) and older libraries expect string data in this raw format.

C-style strings are null-terminated char arrays: char str[] = "hello" stores {'h','e','l','l','o','\0'}. The null terminator '\0' marks the end. You'll encounter these in legacy code and C APIs.

#include <cstring> char s1[] = "Hello"; // Size 6: 5 chars + '\0' char s2[20] = "World"; // Fixed buffer of 20 char s3[20]; const char* s4 = "literal"; // Pointer to string literal (immutable!) strlen(s1) // Length: 5 (not counting '\0') strcpy(s3, s1) // Copy s1 into s3 — DANGER: no bounds check! strncpy(s3, s1, 19) // Safer: copy at most 19 chars strcat(s2, s1) // Concatenate — DANGER: buffer overflow! strcmp(s1, s2) // Compare: 0 if equal, <0 if s1<s2, >0 if s1>s2
Buffer Overflow — Security Vulnerability

C-string functions like strcpy, strcat, gets don't check buffer sizes. Writing past the end of a char array corrupts memory. This is the basis of buffer overflow attacks. In modern C++, use std::string. If you must use C-strings, use bounded versions: strncpy, strncat, fgets.

6.4   Chapter Traps & Key Points

TrapWrongCorrect
Array out of boundsarr[5] on a size-5 arrayValid indices: 0 to size-1. Use vector.at() for bounds check.
String comparisonchar* s1 == char* s2 (compares pointers!)Use strcmp() for C-strings, == for std::string
string::nposif (s.find("x") == -1) — wrong typeif (s.find("x") == string::npos) — correct
getline after cincin >> n; getline(cin, line); — reads emptycin >> n; cin.ignore(); getline(cin, line);
Modifying string literalchar* s = "hello"; s[0] = 'H'; — crash!char s[] = "hello"; (array copy) or std::string
↑ back to top
Chapter 07
Pointers — The Heart of C++

7.1   What is a Pointer? Core

A pointer is a variable that stores the memory address of another variable. Every variable in your program occupies memory at a specific address. A pointer holds that address as its value.

Mental Model: The House vs. The Address

Imagine a variable is a house. The house contains data (like furniture). Every house has a unique address (like "123 Main St"). A pointer is a piece of paper with that address written on it. It isn't the house itself, but it tells you exactly where the house is. If you have the address, you can go to the house and change the furniture (the data) even if you're standing on the other side of town.

Visualizing Variable x and Pointer p [ Variable x ] [ Pointer p ] [ Pointer pp ] Value: 10 ◀──── Value: 0x100 ◀──── Value: 0x200 addr: 0x100 addr: 0x200 addr: 0x300
// Pointer syntax int x = 42; int* p = &x; // p is a pointer to int; & gets the address of x cout << x; // 42 — the value of x cout << &x; // 0x... — the address of x cout << p; // 0x... — same address (that's what p stores) cout << *p; // 42 — dereference: go to address p, read value

Trace: Step-by-Step Dereferencing (*p = 100)

  • 1 Look up p: The CPU finds the variable p in memory.
  • 2 Read address: It reads the address stored inside p (e.g., 0x7ffe...).
  • 3 Go to address: It jumps to that address and writes the value 100 into that location.
  • // Pointer to pointer int** pp = &p; // pp stores the address of p (double pointer) cout << **pp; // 100 — double dereference

    7.2   Why Does This Exist? Motivation

    You might wonder: "Why not just use the variable name?" Pointers exist for three critical reasons:

    1. Dynamic Memory

    Pointers are the only way to manage memory that you "request" at runtime (using new). This allows for flexible data structures like linked lists.

    2. Efficiency

    Instead of copying a 1GB object into a function (slow), you can just pass its 8-byte address. This is near-instant.

    3. Shared Access

    Pointers allow multiple parts of your program to work on the same data without making multiple copies.

    The Two Key Operators

    & (address-of): applied to a variable, gives its memory address. &x = "the address where x lives."
    * (dereference): applied to a pointer, gives the value at that address. *p = "go to address p, read the value."

    7.3   Null Pointer & Pointer Safety Critical

    Motivation: The Blank Piece of Paper

    If a pointer is a piece of paper with an address written on it, what happens when you create the pointer but don't give it an address yet? In C++, it contains "garbage" — an old, random address from a previous program. If you try to go there (dereference), your program crashes. nullptr is essentially writing "NO ADDRESS YET" explicitly on that piece of paper so you can test it safely.

    // nullptr — the safe null pointer (C++11) int* p = nullptr; // p points to nothing — safe initial value // ALWAYS check before dereferencing if (p != nullptr) { cout << *p; // Safe } // or equivalently: if (p) cout << *p; // nullptr is falsy // Dangling pointer — points to freed/out-of-scope memory int* dangling; { int x = 10; dangling = &x; // x is valid here } // x is destroyed here // *dangling now is UNDEFINED BEHAVIOR — x no longer exists! // After freeing heap memory, set pointer to nullptr delete p; p = nullptr; // Prevents use-after-free
    Dereferencing Null/Dangling Pointer = Crash

    Dereferencing a null pointer or a dangling pointer causes undefined behavior — usually a segmentation fault crash. This is one of the most common C++ bugs. Rule: never dereference a pointer without verifying it's non-null and still valid. Use smart pointers (File 03) to automate this.

    7.4   Pointer Arithmetic & Arrays Core

    Mental Model: Walking Down the Street

    If you stand in front of House #100 on a street and take "one step" to the next house, you don't end up at House #101. If the houses are 50 feet wide, you end up at House #150. Pointer arithmetic works the exact same way. If you add 1 to an int*, the compiler actually adds `4` (the size of an int) to the memory address. It automatically scales your "steps" to the size of the objects you are jumping over.

    Arrays and pointers are intimately connected. An array name decays to a pointer to its first element.

    int arr[] = {10, 20, 30, 40, 50}; int* p = arr; // p points to arr[0] (array decays to pointer) cout << *p; // 10 — arr[0] cout << *(p+1); // 20 — arr[1] (pointer arithmetic: +1 moves by sizeof(int) bytes) cout << *(p+2); // 30 — arr[2] p++; // Move pointer to next element cout << *p; // 20 // p[i] is EXACTLY the same as *(p+i) p[2] == *(p + 2) // Always true — these are identical // Pointer difference = number of elements between int* start = arr; int* end = arr + 5; cout << end - start; // 5 — not 20 bytes, but 5 elements // Iterate using pointers (same as using indices) for (int* p = arr; p < arr + 5; p++) { cout << *p << " "; }

    7.5   Dynamic Memory — new & delete Critical

    Local vs Heap Pointers

    A pointer to a local variable (e.g. int* p = &x;) points to something on the stack. The memory is managed automatically. When the function ends, x disappears. The pointer still has the address, but pointing to it is now an error.
    A pointer to a heap-allocated variable (e.g. int* p = new int;) points to memory you manually requested. This memory NEVER disappears automatically. It will live forever—even after the function ends—until you explicitly destroy it with delete p;.

    Stack memory is fast but limited and automatically managed. Heap memory (dynamic allocation) lets you allocate large amounts and control lifetime explicitly using new and delete.

    // Allocate a single value on the heap int* p = new int; // Allocate uninitialized int on heap int* q = new int(42); // Allocate and initialize to 42 int* r = new int{42}; // Same (brace init, preferred) *p = 10; cout << *p; // 10 delete p; // MUST free heap memory when done p = nullptr; // Best practice: null the pointer after delete // Allocate an array on the heap int n = 100; int* arr = new int[n]; // Dynamic array of n ints arr[0] = 1; // Use like a regular array delete[] arr; // NOTE: delete[] for arrays, not delete arr = nullptr; // 2D dynamic array int rows = 3, cols = 4; int** matrix = new int*[rows]; for (int i = 0; i < rows; i++) matrix[i] = new int[cols]; // ... use matrix[i][j] ... // Free in reverse order: for (int i = 0; i < rows; i++) delete[] matrix[i]; delete[] matrix;
    Memory Leak — Heap Memory Never Freed

    If you new something and never delete it, that memory is never returned to the OS — called a memory leak. In long-running programs, leaks accumulate and eventually crash the program. Rule: every new must have a matching delete. In modern C++, avoid raw new/delete entirely — use std::unique_ptr and std::vector (see File 03).

    7.6   Chapter Traps & Key Points

    Pointers are where the most dangerous C++ bugs live. A mistake here won't just give you the wrong math—it will crash your computer or create a security hole.

    1. The Dangling Pointer

    A dangling pointer is a pointer that stores the address of a variable that no longer exists. For example, if you have a pointer to a local variable in a function, and that function ends, the pointer still points to that memory—but that memory is now being used for something else. Using a dangling pointer leads to Undefined Behavior: your program might work today and crash tomorrow.

    Trap: Dereferencing nullptr

    A nullptr is a pointer that points to "nothing." Attempting to read or write to *ptr when ptr is null is like trying to enter a house that doesn't exist. It will cause an immediate "Segmentation Fault" or "Access Violation." Always check: if (ptr != nullptr) before using a pointer.

    2. Memory Leaks

    When you allocate memory using new, it stays allocated until you manually call delete. If you lose the pointer (the address) without deleting the memory, that memory is "leaked"—it stays used but you can't access it. Always null your pointers after deletion to prevent "Double Free" bugs.

    Pro Tip: Modern Smart Pointers

    In modern C++, we almost never use new and delete directly. Instead, we use Smart Pointers (unique_ptr, shared_ptr). They work just like regular pointers but "clean up after themselves" automatically. We'll cover these in File 04.

    ↑ back to top
    Chapter 08
    References & Memory — Stack, Heap & Aliases

    8.1   References — Aliases for Variables Core

    A reference is an alias — an alternative name for an existing variable. Unlike a pointer, a reference cannot be null, cannot be reseated (made to refer to something else after initialization), and doesn't need dereferencing syntax.

    Mental Model: The Nickname

    Think of a reference as a nickname. If your name is Robert but everyone calls you "Bob," you are still the same person. Anything that happens to Bob happens to Robert. In memory, Robert and Bob are the same physical location. This makes references safer and cleaner than pointers for simple task like passing data to functions.

    int x = 10; int& ref = x; // ref is an alias for x — they're THE SAME VARIABLE cout << ref; // 10 — reads x through ref ref = 20; // Changes x! ref and x are the same cout << x; // 20 // References MUST be initialized at declaration int& r; // COMPILE ERROR: reference must be initialized // References cannot be reseated int a = 1, b = 2; int& r2 = a; r2 = b; // This ASSIGNS b's value (2) to a, does NOT make r2 refer to b // const reference — can bind to temporaries and literals const int& cr = 42; // OK: const ref extends the lifetime of the temporary const int& cr2 = x + 1; // OK: binds to result of expression

    Pointer vs Reference — When to Use Which

    Use Reference when

    The alias must always be valid (never null). You don't need to change what it refers to. Cleaner syntax with no * or &. Function parameters that modify the caller's variable.

    Use Pointer when

    The value might be null (optional parameter). You need to point to different things over time. Dynamic memory allocation. Arrays of variable size. Interfacing with C APIs.

    8.2   The Memory Model — Stack vs Heap Critical Concept

    Understanding where your variables live in memory is essential for writing correct, performant C++ programs. There are two main memory regions a typical C++ program uses.

    Mental Model: The Desk vs. The Warehouse

    Think of the Stack like your literal study desk. It's very fast, everything is right in front of you, and things get stacked precisely in order. But if you try to put a car on it, the desk will break (Stack Overflow). Think of the Heap like a giant warehouse. You can put massive things there, but you have to specifically request a spot, get a receipt (a pointer) to know where you put it, and explicitly throw the item away when you're done.

    C++ Memory Layout High Address ┌─────────────────────────────────┐ │ STACK │ ← Local variables, function call frames │ (grows downward ↓) │ Fast. Fixed size (~1-8 MB). Auto-managed. ├─────────────────────────────────┤ │ (free space) │ ├─────────────────────────────────┤ │ HEAP │ ← new/delete (dynamic allocation) │ (grows upward ↑) │ Slow. Large. Manual management. ├─────────────────────────────────┤ │ Static / Global │ ← Global vars, static locals ├─────────────────────────────────┤ │ Code / Text Segment │ ← Your compiled instructions └─────────────────────────────────┘ Low Address
    PropertyStackHeap
    Allocation speedVery fast (just move stack pointer)Slower (OS call, fragmentation)
    SizeLimited (~1–8 MB typically)Limited by RAM
    LifetimeAutomatic (freed at scope exit)Manual (new/delete) or smart pointers
    SafetyNo leaksLeaks if not deleted
    Use forLocal vars, function paramsLarge data, variable-size, needs to outlive function
    Stack Overflow from Large Arrays

    Declaring int arr[1000000]; as a local variable (on the stack) uses 4 MB and often causes a stack overflow at runtime. For large arrays, either declare them globally (in static memory) or use new int[1000000] / std::vector<int>(1000000) to put them on the heap.

    8.3   Structures — Custom Data Types Core

    Motivation: Bundling Related Data

    Imagine writing a game. A player has an X coordinate, a Y coordinate, a health score, and a name. You could have four separate variables passed around into every function: movePlayer(string name, int x, int y, int health). But this is messy and error-prone. A struct lets you stitch these variables together into a single, cohesive unit called a "Player". It allows you to think and code in terms of whole entities rather than loose parts.

    A struct groups related variables of different types into a single named type. It's the foundation of OOP (classes are just structs with private members and methods).

    struct Student { string name; int age; double gpa; }; // Note the semicolon after the closing brace! // Create and use a struct Student s1; s1.name = "Alice"; s1.age = 20; s1.gpa = 3.8; Student s2 = {"Bob", 21, 3.5}; // Aggregate initialization Student s3 {"Carol", 22, 3.9}; // Modern brace init // Access members with . (dot) operator cout << s2.name << " " << s2.gpa << "\n"; // Pointer to struct: use -> operator Student* ptr = &s1; cout << ptr->name; // ptr->name == (*ptr).name // Pass struct to function void printStudent(const Student& s) { // const& to avoid copy cout << s.name << " GPA: " << s.gpa << "\n"; } // struct with methods (this is essentially a class) struct Point { double x, y; double distanceTo(const Point& other) const { double dx = x - other.x, dy = y - other.y; return sqrt(dx*dx + dy*dy); } };

    8.4   Chapter Traps & Key Points

    References are powerful because they are virtually "free" and safe, but they have strict rules about their lifetime.

    1. The "Return Local Reference" Disaster

    As we saw in Chapter 5, never return a reference to a variable created inside the function. That variable dies the moment the function returns. Your reference now points to dead memory. This is the #1 cause of "Garbage Output" in C++.

    Trap: Reference vs Copy in Loops

    When you write for (auto x : myLargeVector), C++ makes a copy of every element. If the elements are large structs, your loop will be 10x slower. Always use for (const auto& x : myLargeVector) to access them by reference instead. Safe, fast, and professional.

    2. Reference Initialization

    A reference MUST be initialized when it is created. You can't have an "empty" reference like you can have a null pointer. Once a reference is tied to a variable, it is tied to that variable for life. You cannot "reseat" it later.

    ↑ back to top
    Chapter 09
    Traps & Master Reference — The Complete Foundations Cheatsheet

    9.1   Essential Header Reference Cheatsheet

    HeaderWhat It ProvidesKey Contents
    <iostream>I/O streamscout, cin, cerr, endl
    <string>std::string classstring, to_string, stoi, stod
    <cmath>Math functionssqrt, pow, abs, floor, ceil, sin, cos, log
    <algorithm>STL algorithmssort, reverse, min, max, find, binary_search
    <vector>Dynamic arrayvector<T>, push_back, size, at, begin, end
    <climits>Integer limitsINT_MAX, INT_MIN, LLONG_MAX, LLONG_MIN
    <cstring>C-string functionsstrlen, strcpy, strcmp, memset, memcpy
    <cctype>Character functionsisalpha, isdigit, toupper, tolower

    9.2   The "Foundational 15": Professional Debugging Guide Must Know

    As a senior developer, I see these 15 bugs more than any others. Mastering these is the difference between a "beginner" and someone who can write production-ready code.

    Group A: The "Logic and Syntax" Bugs

    • 1. Uninitialized Variables: C++ doesn't zero out your memory. Using int x; results in random "garbage" data. Always use int x{0};.
    • 2. The `=` vs `==` Slip: if(x = 5) sets x to 5 and is always true. if(x == 5) is the check. Enable -Wall to let the compiler catch this for you.
    • 3. Integer Overflow: If you add 1 to a max-value int, it wraps to a negative number. Always use long long in competitive programming when numbers exceed 10⁹.
    • 4. Integer Division: (double)7 / 2 is 3.5, but 7 / 2 is 3. The compiler follows the type of the inputs, not your expectations.
    • 5. Switch Fallthrough: Forgetting a break in a switch means every case below it runs too. It's a "silent logic" bug.

    Group B: The "Memory and Pointer" Bugs

    • 6. Array Out-of-Bounds: Accessing arr[10] on an array of size 10. This corrupts adjacent memory or causes a Segfault. C++ doesn't stop you—it's your responsibility.
    • 7. Null Dereference: Using *ptr when ptr is nullptr. This is an immediate, hard crash. Always check if (ptr).
    • 8. Memory Leaks: Every new needs a delete. If you lose the pointer before deleting, that RAM is "gone" until the program ends.
    • 9. `delete` vs `delete[]`: Use the square-bracket version for anything allocated with new T[n]. Using the wrong one causes partial memory corruption.
    • 10. Dangling References: Returning a reference to a temporary variable inside a function. It's like giving someone a key to a house that was just demolished.

    Group C: The "Modern and Performance" Bugs

    • 11. `endl` in Tight Loops: Using endl flushes the I/O buffer to the hardware. It's 100x slower than "\n". This is the #1 reason C++ solutions fail Time Limits in CP.
    • 12. Unnecessary Copies: Passing a std::string or std::vector to a function by value. This makes a deep copy of the whole object. Pass by const& instead.
    • 13. `getline` after `cin >>`: The >> operator leaves a newline in the buffer. getline will read that newline and return an empty string. Use cin.ignore() between them.
    • 14. Negative Modulo: In C++, -7 % 3 is -1. If you need a mathematically positive result (for indices), use ((n % m) + m) % m.
    • 15. Naive Recursion: Using simple recursion for problems like Fibonacci. Without memoization, it grows exponentially (2ⁿ) and crashes your program for even small inputs.

    9.3   Fast I/O Template for Competitive Programming CP Essential

    // Paste this at the top of every CP solution #include <bits/stdc++.h> // GCC-only: includes EVERYTHING using namespace std; using ll = long long; using pii = pair<int,int>; using vi = vector<int>; #define pb push_back #define all(x) (x).begin(),(x).end() #define sz(x) (int)(x).size() int main() { // Fast I/O — makes cin/cout as fast as printf/scanf ios_base::sync_with_stdio(false); cin.tie(NULL); // Your solution here return 0; }
    Why ios_base::sync_with_stdio(false)?

    By default, cin/cout synchronize with C's printf/scanf (so you can mix them). This sync is slow. Calling sync_with_stdio(false) removes that sync, making cin/cout nearly as fast as scanf/printf. cin.tie(NULL) stops cout from being flushed before every cin read. Together, they make I/O roughly 5-10x faster — critical for large inputs.

    9.4   Foundations Quick-Reference Card Reference

    Integer Limits

    int: ±2.1×10⁹
    long long: ±9.2×10¹⁸
    INT_MAX = 2147483647
    LLONG_MAX = 9.2×10¹⁸

    Type Sizes (typical)

    char = 1 byte
    int = 4 bytes
    long long = 8 bytes
    double = 8 bytes
    pointer = 8 bytes (64-bit)

    Useful Math

    abs(x) — absolute value
    sqrt(x) — square root
    pow(x,n) — x^n
    min(a,b), max(a,b)
    __gcd(a,b) — GCD (GCC)

    String Quick Ops

    s.size() — length
    s.substr(i,n) — substring
    s.find(t) — find t in s
    stoi(s), to_string(n)
    sort(s.begin(),s.end())

    Pointer Quick Ref

    int* p = &x — pointer to x
    *p — dereference (get value)
    p->member — member via ptr
    nullptr — null pointer
    delete p; p=nullptr;

    Loop Patterns

    Forward: for(int i=0;i<n;i++)
    Reverse: for(int i=n-1;i>=0;i--)
    Range: for(auto& x : vec)
    Until: while(condition)
    At-least-once: do{}while(cond)

    ↑ back to top