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.
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.
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.
Compiles directly to machine code. No virtual machine, no garbage collector. You control memory allocation and deallocation explicitly.
Chrome, Firefox, VS Code, Unreal Engine, MySQL, Linux kernel drivers, NASA spacecraft software, every major game engine.
C++ is a superset of C. Valid C code is (nearly) valid C++. C++ adds OOP, templates, references, and the entire Standard Library.
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.
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.
Install MinGW-w64 (GCC for Windows) or use Visual Studio Community (has MSVC compiler built in). Recommended: VS Code + MinGW.
Run xcode-select --install in Terminal. This installs Clang. Alternatively install GCC via Homebrew: brew install gcc.
GCC is usually pre-installed. If not: sudo apt install g++ on Ubuntu/Debian. Verify with g++ --version.
Once GCC/G++ is installed, the compile-run cycle is:
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.
Every C++ program has the same fundamental skeleton. Learn this structure cold — you will type it thousands of times.
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().
int main() is the entry point of every C++ program. It returns an int — 0 means success, any non-zero value signals an error to the operating system. The OS uses this return value; always return 0 on success.
| Element | What it is | Required? |
|---|---|---|
#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 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.
cin (character input) reads from the keyboard. The extraction operator >> pulls data from the input stream into a variable.
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.
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').
As a beginner, you will encounter these points repeatedly. Understanding the "why" behind these rules will save you hours of debugging.
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.
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.
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.
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.
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).
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.
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;
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++.
myVar, MyVar, and MYVAR are three different variables.int, class, return, etc.camelCase for variables, UPPER_SNAKE for constants, PascalCase for types.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.
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.
| Type | Size | Range / Notes | Typical Use |
|---|---|---|---|
bool | 1 byte | true (1) or false (0) | Flags, conditions |
char | 1 byte | -128 to 127 (signed) or 0–255 (unsigned) | Single characters, ASCII |
int | 4 bytes | -2,147,483,648 to 2,147,483,647 (~±2.1 billion) | General integers |
long long | 8 bytes | -9.2×10¹⁸ to 9.2×10¹⁸ | Large numbers, CP problems |
float | 4 bytes | ~7 decimal digits precision | Graphics (not finance) |
double | 8 bytes | ~15 decimal digits precision | Scientific, financial calc |
unsigned int | 4 bytes | 0 to 4,294,967,295 | Sizes, indices (non-negative) |
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.
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.
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).
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.
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++.
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 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.
Variables are the building blocks of your program, but they come with hidden traps that even experienced programmers occasionally fall into.
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.
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).
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.
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.
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.
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.
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.
Comparison operators return bool (true or false). They are the foundation of all conditions and loops.
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.
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.
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.
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.
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.
| Operator | Name | Example (a=5=0101, b=3=0011) | Result |
|---|---|---|---|
a & b | AND | 0101 & 0011 | 0001 = 1 |
a | b | OR | 0101 | 0011 | 0111 = 7 |
a ^ b | XOR | 0101 ^ 0011 | 0110 = 6 |
~a | NOT (flip all bits) | ~0101 | ...11111010 = -6 |
a << 1 | Left shift | 0101 << 1 | 1010 = 10 (×2) |
a >> 1 | Right shift | 0101 >> 1 | 0010 = 2 (÷2) |
| 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. |
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.
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.
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.
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.
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.
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.
Programs rarely run in a straight line. Control flow structures allow your code to make decisions and repeat actions based on data.
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.
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.
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.
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.
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.
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.
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.
Print all prime numbers from 1 to 100.
Key insight: checking divisors only up to √n reduces checks from O(n) to O(√n) per number.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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&).
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.
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.
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.
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.
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.
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.
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.
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.
Count vowels and reverse a string.
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.
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.
| Trap | Wrong | Correct |
|---|---|---|
| Array out of bounds | arr[5] on a size-5 array | Valid indices: 0 to size-1. Use vector.at() for bounds check. |
| String comparison | char* s1 == char* s2 (compares pointers!) | Use strcmp() for C-strings, == for std::string |
| string::npos | if (s.find("x") == -1) — wrong type | if (s.find("x") == string::npos) — correct |
| getline after cin | cin >> n; getline(cin, line); — reads empty | cin >> n; cin.ignore(); getline(cin, line); |
| Modifying string literal | char* s = "hello"; s[0] = 'H'; — crash! | char s[] = "hello"; (array copy) or std::string |
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.
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.
p in memory.p (e.g., 0x7ffe...).100 into that location.You might wonder: "Why not just use the variable name?" Pointers exist for three critical reasons:
Pointers are the only way to manage memory that you "request" at runtime (using new). This allows for flexible data structures like linked lists.
Instead of copying a 1GB object into a function (slow), you can just pass its 8-byte address. This is near-instant.
Pointers allow multiple parts of your program to work on the same data without making multiple copies.
& (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."
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| Property | Stack | Heap |
|---|---|---|
| Allocation speed | Very fast (just move stack pointer) | Slower (OS call, fragmentation) |
| Size | Limited (~1–8 MB typically) | Limited by RAM |
| Lifetime | Automatic (freed at scope exit) | Manual (new/delete) or smart pointers |
| Safety | No leaks | Leaks if not deleted |
| Use for | Local vars, function params | Large data, variable-size, needs to outlive function |
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.
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).
References are powerful because they are virtually "free" and safe, but they have strict rules about their lifetime.
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++.
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.
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.
| Header | What It Provides | Key Contents |
|---|---|---|
<iostream> | I/O streams | cout, cin, cerr, endl |
<string> | std::string class | string, to_string, stoi, stod |
<cmath> | Math functions | sqrt, pow, abs, floor, ceil, sin, cos, log |
<algorithm> | STL algorithms | sort, reverse, min, max, find, binary_search |
<vector> | Dynamic array | vector<T>, push_back, size, at, begin, end |
<climits> | Integer limits | INT_MAX, INT_MIN, LLONG_MAX, LLONG_MIN |
<cstring> | C-string functions | strlen, strcpy, strcmp, memset, memcpy |
<cctype> | Character functions | isalpha, isdigit, toupper, tolower |
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.
int x; results in random "garbage" data. Always use int x{0};.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.int, it wraps to a negative number. Always use long long in competitive programming when numbers exceed 10⁹.(double)7 / 2 is 3.5, but 7 / 2 is 3. The compiler follows the type of the inputs, not your expectations.break in a switch means every case below it runs too. It's a "silent logic" bug.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.*ptr when ptr is nullptr. This is an immediate, hard crash. Always check if (ptr).new needs a delete. If you lose the pointer before deleting, that RAM is "gone" until the program ends.new T[n]. Using the wrong one causes partial memory corruption.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.std::string or std::vector to a function by value. This makes a deep copy of the whole object. Pass by const& instead.>> operator leaves a newline in the buffer. getline will read that newline and return an empty string. Use cin.ignore() between them.-7 % 3 is -1. If you need a mathematically positive result (for indices), use ((n % m) + m) % m.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.
int: ±2.1×10⁹
long long: ±9.2×10¹⁸
INT_MAX = 2147483647
LLONG_MAX = 9.2×10¹⁸
char = 1 byte
int = 4 bytes
long long = 8 bytes
double = 8 bytes
pointer = 8 bytes (64-bit)
abs(x) — absolute value
sqrt(x) — square root
pow(x,n) — x^n
min(a,b), max(a,b)
__gcd(a,b) — GCD (GCC)
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())
int* p = &x — pointer to x
*p — dereference (get value)
p->member — member via ptr
nullptr — null pointer
delete p; p=nullptr;
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)