-
Notifications
You must be signed in to change notification settings - Fork 0
CPlusPlus Course
You are a teacher who only has 4 hours with me and will never see me again. Your only goal is to make me functional with C++ (Latest Spec) before time runs out. Don't give me theory without use. Dont give me a list. Tell me: what to learn first, what to ignore completely, and what is the one exercise that, if i do it once, puts me ahead of 70% of people who have been studying this for months.
Alright. Clock's running. Sit down.
Here's the thing almost nobody tells you: C++ is two languages wearing one coat. There's the old one - new, delete, raw pointers, manual memory, printf - and there's the modern one you actually want. Most people who've "studied for months" are stuck because their tutorials taught the old coat first. We're going to ignore it almost entirely, and that alone is your head start.
The single idea everything hangs on: a variable is a value that owns its stuff and cleans up after itself when it goes out of scope. Not a pointer to a thing somewhere. The thing itself. When a std::string or std::vector dies at the end of its { } block, it frees its own memory. You don't free it. You never free it. That automatic cleanup is called RAII and it is the whole personality of the language. Burn that in now and half the "hard" stuff evaporates.
So here's your first hour. You learn exactly five things and you write real code with them immediately: std::string and std::vector as your default for text and lists (forget arrays, forget C strings), auto so you stop writing type names you don't need, the range-based for loop (for (auto& x : v)), passing things to functions by const& when you're reading and by & when you're changing, and returning by plain value (yes, just return the vector — the compiler makes it free, trust me). Write a program that reads names into a vector<string> and prints them back sorted. Make it compile. That's hour one.
What do you ignore completely, starting now and possibly forever? Raw new and delete. Manual memory management. Pointer arithmetic. Inheritance and virtual and the whole "animal → dog → poodle" OOP cathedral — it's wildly over-taught and you'll need it rarely. Writing your own templates (using them is fine, writing them can wait). The preprocessor beyond #include. printf/scanf. Move semantics mechanics — the && rvalue-reference rabbit hole — because you get all the benefit of moves for free without understanding the gears yet. Anyone who tells you to start with these is teaching you the 1990s.
Hours two and three are where you pull ahead, so pay attention, because this is the exercise.
Type this out yourself — don't copy-paste, type it — and run it:
#include <iostream>
#include <string>
#include <vector>
struct Tracer {
std::string name;
Tracer(std::string n) : name(std::move(n)) { std::cout << "construct " << name << '\n'; }
~Tracer() { std::cout << "destroy " << name << '\n'; }
Tracer(const Tracer& o) : name(o.name) { std::cout << "copy " << name << '\n'; }
Tracer(Tracer&& o) noexcept : name(std::move(o.name)) { std::cout << "move " << name << '\n'; }
};
int main() {
std::vector<Tracer> v;
v.push_back(Tracer{"A"});
v.push_back(Tracer{"B"});
std::cout << "--- copying A ---\n";
Tracer c = v[0];
std::cout << "--- end of main ---\n";
}Now stare at the output. This is the whole game. You will see things construct, you'll see moves happen when you didn't ask for them, and — the moment that breaks people open — when you push "B", the vector runs out of room, grows, and silently moves "A" into the new space. You'll watch it happen in the log. Then at the end you'll see everything destroy in reverse order of birth, automatically, with you never writing a single delete.
That's it. That's the exercise. Most people who've studied for six months have read the words "constructor," "destructor," "copy," and "move" — but they have never watched object lifetime happen in front of them. Once you have, you understand why C++ is fast (it moves instead of copying), why it's safe when you let it be (destructors fire on their own), and why every "I have a memory leak" question on the internet is someone who reached for new when they should have reached for a vector. You'll have a physical intuition that reading can't give you. Run it three times, change things — add a c = v[1] and watch — until the output stops surprising you.
Hour four is the one escape hatch and the survival kit. The escape hatch: there's exactly one situation where you genuinely need the heap, which is when something must outlive its scope or be polymorphic, and for that you use std::unique_ptr<T> via std::make_unique<T>(...) — never raw new. It's a value that owns a heap thing and deletes it for you when it dies. Same RAII rule, just pointing at the heap. Learn unique_ptr and you can ignore delete for the rest of your life. The survival kit: learn to compile with warnings cranked up (-Wall -Wextra on g++/clang, treat them as the truth), learn that "undefined behavior" means the program is allowed to do anything including look like it works, and learn to read the first compiler error only — the rest are usually noise cascading from it.
Do that, and in four hours you can write correct, modern, leak-free C++ that does useful work, and you'll understand why it's correct — which is the part the months-long crowd is still missing.
Go type the Tracer. That's your first move. What does the output look like when you run it?
Can we work on Hour One, as someone just starting
Good. Let's get you writing code in the next two minutes, because the thing that kills most beginners isn't C++ — it's spending three hours fighting to install a compiler. We skip all of that.
Open a browser tab and go to an online C++ runner. OnlineGDB (onlinegdb.com) or Programiz both work and let you type input; pick one. Make sure the language is set to C++. That's your whole setup. We can do a real local toolchain later in life; today it would just eat your clock.
Now type this in — actually type it, your fingers need to learn the shapes — and hit Run:
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names = {"Zara", "Adam", "Mia"};
for (const auto& name : names) {
std::cout << name << '\n';
}
}You should see the three names printed. Now let me tell you what's actually worth understanding here, and what to wave away.
The three #include lines mean "I want to use these tools" — input/output, text, and lists. int main() { ... } is just "the program starts here." Don't think about either of those again today; they're ceremony, the box the real code lives in.
The line that matters is std::vector<std::string> names. Read it right to left in plain English: a vector is a list, and <std::string> says it's a list of text. So names is a list of words, and the { } fills it with three. This is your default container for "I have several of something." Whenever you think "a bunch of things," you reach for vector.
Then the loop: for (const auto& name : names) means "go through names, one at a time, calling each one name." That little phrase const auto& is the single most-used pattern in modern C++, so let me unpack it: auto means "figure out the type for me" (it's a string, but you don't have to say so), the & means "look at the real one, don't make a copy" (faster, and it's a habit you want), and const means "I'm only reading it, not changing it." So the whole thing reads as: let me look at each name, one by one, without copying and without touching it. Ninety percent of your loops will look exactly like this.
That's it. That's the spine of Hour One. Everything else is variations.
Now make one change yourself before I give you anything more. Add a fourth name to the list, and then sort it. To sort, you need one new include and one new line. Add #include <algorithm> up top with the others, and put this line before the loop:
std::sort(names.begin(), names.end());names.begin(), names.end() is just the awkward C++ way of saying "the whole list" — don't worry about why yet, that's a real answer for another day. Run it, and the names should come out alphabetical.
Do that now — add a name, add the sort, run it. Tell me what comes out, and whether anything threw you. Once it's sorted, the next move is making the program ask you for the names instead of having them baked in, and that's where it starts to feel like a real tool.
< I'm happy with that, lets move on
Now we make it a real tool — the program asks you for the names instead of having them baked in. This is the jump from "code that runs" to "code that does something for someone." Here's the whole thing:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
int main() {
std::vector<std::string> names; // empty list to start
std::string input;
while (std::cin >> input) {
if (input == "done") break;
names.push_back(input);
}
std::sort(names.begin(), names.end());
for (const auto& name : names) {
std::cout << name << '\n';
}
}Three new pieces, and they're all vocabulary you'll use constantly.
std::vector<std::string> names; with no { } — that's an empty list. You start with nothing and fill it up.
while (std::cin >> input) is the read loop. std::cin >> input grabs one word that the user types and drops it into input. The while keeps doing that over and over. So this loop means "keep reading words." Inside, if (input == "done") break; is your off-switch — when the user types the word done, break jumps out of the loop. (Note == with two equals signs for "is equal to" — one equals sign means something else and is a classic beginner trap.)
names.push_back(input) is how you add one thing to the end of a vector. Read it as "push this onto the back of the list." This is the partner to everything you've done — vector holds things, push_back puts them in, the for loop reads them out. That trio is most of practical C++.
To run it: on OnlineGDB or Programiz there's an input box (or a panel that pops up when it runs). Type some names, one per line, then type done on its own line. You'll get them back sorted.
Run it, mess with it — give it ten names, give it zero names then done, see what happens.
When that feels solid, the last move in Hour One is functions — pulling a chunk of work out into its own named box you can reuse. That's where you'll meet "pass it the list to read" versus "pass it the list to change," which is the bit I flagged at the very start. Want to go straight there, or run this one first and tell me how it behaved?
I'm happy with that, lets progress
Last piece of Hour One, and it's the one that turns a pile of code into something you can actually maintain: functions. A function is just a named box of work you can run whenever you want. The payoff is that your main stops being a wall of instructions and starts reading like a summary of what the program does.
Here's your exact program from a minute ago, reorganized into functions. Same behavior, much clearer shape:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
std::vector<std::string> read_names() {
std::vector<std::string> names;
std::string input;
while (std::cin >> input) {
if (input == "done") break;
names.push_back(input);
}
return names;
}
void sort_names(std::vector<std::string>& names) {
std::sort(names.begin(), names.end());
}
void print_names(const std::vector<std::string>& names) {
for (const auto& name : names) {
std::cout << name << '\n';
}
}
int main() {
std::vector<std::string> names = read_names();
sort_names(names);
print_names(names);
}Look at main now. It reads like English: read the names, sort the names, print the names. That's the whole point of functions — you hide the how inside a box and give it a name that says what. When a program gets big, this is the difference between sane and unreadable.
Now the shape of a function: return-type name(stuff it needs) { body }. Read each of the three:
std::vector<std::string> read_names() — the first part says it hands back a list of strings, and return names; is how it hands it back. And here's that promise from the very start of this lesson paying off: returning the whole vector is free. The compiler doesn't make a slow copy. So never be afraid to return big things from functions — just return them.
void sort_names(...) — void means "hands back nothing." It just does a job and that's it.
Now the important bit, the thing I flagged at the start: look at the & symbols. sort_names takes std::vector<std::string>& names with an &, which means it gets the real list, so when it sorts, the sorting sticks in main. print_names takes const std::vector<std::string>& names — the real list too, but const means look-only, because printing shouldn't change anything.
Here's the rule, and it's the whole rule: if your function only reads the thing, pass it const&. If it needs to change the caller's thing, pass it &. If it's a tiny value like a single number, just pass it plain. That's it — looking, changing, or small.
And here's the trap that catches everyone, so I'll show it to you on purpose: if you'd written sort_names(std::vector<std::string> names) with no &, the function would get a private copy, sort the copy, throw it away, and your real list would come back unsorted — with zero error messages. Silent and baffling. The & is what makes the change real. Remember that one and you've sidestepped a bug that eats beginners for hours.
That closes Hour One. You can now take input, store it, transform it, print it, and organize it into functions — and you understand the difference between handing a function a copy versus the real thing. That's genuinely functional.
Prove it to yourself: write one more function called count_names that takes the list const& (you're only counting, not changing) and returns an int — the number of names. Inside, names.size() gives you the count. Then call it in main and print the result. Write it, run it, and show me what you came up with.
Hows my code?
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
void show_start() {
std::cout << "Please enter names below, then type 'done' when finished." << std::endl;
}
std::vector<std::string> read_names() {
std::vector<std::string> names;
std::string input;
while (std::cin >> input) {
if (input == "done") break;
names.push_back(input);
}
return names;
}
void sort_names(std::vector<std::string>& names) {
std::sort(names.begin(), names.end());
}
void print_names(const std::vector<std::string>& names) {
for (const auto& name : names) {
std::cout << name << '\n';
}
}
int count_names(const std::vector<std::string>& names) {
return names.size();
}
int main() {
show_start();
std::vector<std::string> names = read_names();
sort_names(names);
std::cout << "---------Alphabetical Names---------" << std::endl;
print_names(names);
std::cout << "-----------Amount of Names----------" << std::endl;
std::cout << count_names(names) << std::endl;
}Genuinely good — and I mean that specifically, not as a pat on the head. Two things tell me you actually got it rather than just copied.
First, count_names is exactly right: const& because you're only counting, returns an int, one clean line. That's the brief, nailed.
Second, and this is the part that matters — I never told you to write show_start. You looked at the program, decided it needed a prompt so the user isn't staring at a blank box wondering what to do, and you made your own void function for it. That's not following instructions, that's using the tool. You also added the header lines for readability. That instinct — "this should be its own named box, and the output should be pleasant to read" — is the thing most people don't develop for months. You did it in an hour.
Now, two small modern-habit notes. Neither is a bug; your code compiles and works correctly. These are just polish.
You used std::endl in the new lines but '\n' in print_names. They both end a line, but std::endl does something extra: it forces the output to flush every single time, which is slightly slower for no benefit here. The modern habit is to default to '\n' and only reach for std::endl when you genuinely need to force output out immediately (rare). So you can swap those std::endls for '\n' and be more idiomatic. Tiny thing, good habit.
The other one is honest but truly file-under-"someday": names.size() doesn't actually hand back an int — it hands back an unsigned counting type called std::size_t. Returning it as int quietly converts it, which is completely fine for a list of names and exactly what I asked you to do. I'm only flagging it so that when you eventually see a "conversion" or "signed/unsigned" warning from a compiler, you know what it's grumbling about. Ignore it today.
So: clean, correct, well-organized, and showing real instinct. That's Hour One done. You can take input, store it in a vector, transform it, print it, count it, and split work into functions with the right passing style. That is a functional baseline — more than a lot of people have after weeks.
Hour Two is where it gets interesting: right now a "name" is just one piece of text, but real things have several pieces — a person has a name and an age and an email. You'll learn struct, which lets you bundle related data into your own type, so instead of a list of strings you get a list of people. It's the step from storing data to modeling the world.
Want to roll into it?
Yes lets go
Here's the leap. Right now your list holds plain words. We're going to invent a new type - a Person - that bundles a name and an age together, and then make a list of those. Here's your program, evolved:
#include <iostream>
#include <string>
#include <vector>
struct Person {
std::string name;
int age;
};
void show_start() {
std::cout << "Enter a name then an age for each person. Type 'done' to finish.\n";
}
std::vector<Person> read_people() {
std::vector<Person> people;
std::string input;
while (std::cin >> input) {
if (input == "done") break;
Person p;
p.name = input;
std::cin >> p.age;
people.push_back(p);
}
return people;
}
void print_people(const std::vector<Person>& people) {
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ")\n";
}
}
int main() {
show_start();
std::vector<Person> people = read_people();
print_people(people);
}Run it like: type Alice 30 Bob 20 done, You'll get Alice (30) and Bob (20).
Here's what's new, and it's a big idea in a small package.
struct Person { ... }; defines a brand-new type that you invented. Before this, C++ handed you string and int; now you've built Person out of them. Inside the braces are its fields (also called members) - a name and an age. A Person is one bundle carrying both, the way a real person has both a name and an age that travel together.
One trap, and it bites everyone exactly once: there's a semicolon after the closing brace of a struct - };. Miss it and you'll get a wall of baffling errors. Burn that in now.
To use it: Person p; makes one. Then p.name and p.age - the dot reaches inside the bundle to one field. Read p.age out loud as "p's age." That dot is your tool for getting at the pieces.
Now look at the quiet magic: std::vector<Person>. Everything you already know about vectors just works - push_back, the range - for, const& in functions, all of it. The vector doesn't care whether it's holding words or whole people. That's he whole reason this scales: you learned the container once, and now it holds anything you invent.
And notice the reading: the first word becomes the name, then std::cin >> p.age grabs the next token and turns the typed digits into an actual number, because p.age is an int, >> adapts to whatever you're reading into.
So the real shift here: you're no longer just storing data, you're modeling a thing. A person isn't a name OR an age anymore - it's a Person, on unit, that you can store, pass to functions, and return, exactly like any built-in type.
Prove it: add a third field to Person - make it a std::string, like a city or a job (keep it one word for now, since >> stops at spaces). Update read_people to read it in, and print_people to show it. Touching all three spots will show you how a struct ties the data together across the whole program.
Build it, run it.
(From Me)
#include <iostream>
#include <string>
#include <vector>
struct Person {
std::string name;
int age;
std::string city;
};
void show_start() {
std::cout << "Enter a name then an age for each person. Type 'done' to finish.\n";
}
std::vector<Person> read_people() {
std::vector<Person> people;
std::string input;
while (std::cin >> input) {
if (input == "done") break;
Person p;
p.name = input;
std::cin >> p.age;
std::cin >> p.city;
people.push_back(p);
}
return people;
}
void print_people(const std::vector<Person>& people) {
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ") " << p.city << "\n";
}
}
int main() {
show_start();
std::vector<Person> people = read_people();
print_people(people);
}Correct, and correct for the right reason — you updated all three places: the struct definition, the reading, and the printing. That's the exact thing structs teach you: the data is tied together, so when it changes, you touch every spot that handles it. You did that without being walked through each one. Good.
One real catch, the kind a teammate would flag in review: your show_start message still says "Enter a name then an age" — it never mentions the city you now ask for. The program reads three things but tells the user about two. Always keep the thing you say in sync with the thing you do; a user following that prompt would type Alice 30 done and the program would sit there waiting for a city. Tiny fix, real habit.
Now let's give you back something you lost. Remember how std::sort alphabetized your names with one line? Try sorting people the same way and it won't even compile — because C++ knows how to compare two words, but it has no idea whether you want people sorted by name, by age, or by city. So you have to tell it. Add #include <algorithm> back at the top, and add this function:
void sort_by_age(std::vector<Person>& people) {
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
}Then call sort_by_age(people); in main, right before you print.
That third argument to sort is the new thing, and it's a big one in modern C++: a lambda. A lambda is just a little unnamed function you write right where you need it, inline. Read this one as: "given two people a and b, should a come first? Yes, if a.age is less than b.age." So smaller ages go first — youngest to oldest. sort calls your little function over and over to figure out the order.
The [] at the front is the lambda's signature mark — every lambda starts with it. The empty brackets mean "I don't need anything from the surrounding code to do my job," which is true here; you'll see things appear inside those brackets later, but empty is exactly right for now. After that it's just like any function: parameters in ( ), body in { }, a return.
This is one of the most-used patterns in real C++ — "do this operation, and here's a tiny inline rule for how." You'll see lambdas everywhere. Run it, confirm your people come out youngest-first. Then change one thing: write the lambda so it sorts alphabetically by name instead of by age. You've got everything you need — it's a one-word-and-one-field change inside the lambda. Show me what you land on.
Heres my code
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
struct Person {
std::string name;
int age;
std::string city;
};
void show_start() {
std::cout << "Enter a name then an age for each person. Type 'done' to finish.\n";
}
std::vector<Person> read_people() {
std::vector<Person> people;
std::string input;
while (std::cin >> input) {
if (input == "done") break;
Person p;
p.name = input;
std::cin >> p.age;
std::cin >> p.city;
people.push_back(p);
}
return people;
}
void sort_by_name(std::vector<Person>& people) {
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.name < b.name;
});
}
void print_people(const std::vector<Person>& people) {
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ") " << p.city << "\n";
}
}
int main() {
show_start();
std::vector<Person> people = read_people();
sort_by_name(people);
print_people(people);
}The lambda landed — a.name < b.name, function renamed to match what it does, called in the right spot. You took a brand-new piece of syntax and bent it to a different field on the first try. That's the concept sticking, not luck.
One loose thread from last round: show_start still says "Enter a name then an age" with no mention of city. Not a code error — your program runs fine — but it's the kind of thing that quietly confuses a real user, so close it out when you get a sec. I'm flagging it twice because "keep the message in sync with the behavior" is a habit worth more than any syntax.
That's Hour Two done. You can invent your own types, store them, sort them by whatever rule you choose, and organize the whole thing into clean functions. Genuinely solid.
Now — the main event. This is the exercise I promised you at the very start, the one that puts you ahead of people who've studied for months. Set the people program aside; this is a separate little tool, and it's not something you'll ship. It's a microscope. We're going to make the invisible visible: every time C++ creates, copies, moves, or destroys an object, we'll make it shout so you can watch the machinery you've only read about.
Type this exactly into a fresh file and run it:
#include <iostream>
#include <string>
#include <vector>
#include <utility>
struct Tracer {
std::string name;
Tracer(std::string n) : name(std::move(n)) { std::cout << "construct " << name << '\n'; }
~Tracer() { std::cout << "destroy " << name << '\n'; }
Tracer(const Tracer& o) : name(o.name) { std::cout << "copy " << name << '\n'; }
Tracer(Tracer&& o) noexcept : name(std::move(o.name)) { std::cout << "move " << name << '\n'; }
};
int main() {
std::vector<Tracer> v;
v.push_back(Tracer{"A"});
v.push_back(Tracer{"B"});
std::cout << "--- copying A ---\n";
Tracer c = v[0];
std::cout << "--- end of main ---\n";
}Now, important: do not try to understand the code inside those four functions. That &&, that std::move, that noexcept — that's exactly the move-semantics machinery I told you to skip on day one, and I meant it. You are not writing these functions in real life; the compiler writes them for you. Today you're just installing four sensors, and each sensor does nothing but print a label. Ignore the insides completely. Your entire job is to read the output.
Those four functions are the four things that can happen to any object in its life. Here's how to read the labels:
construct X means a brand-new X was built from nothing. copy X means X got duplicated into a separate, independent second X. move X means X's contents were handed over to a new X — fast, and it leaves the original hollowed out. destroy X means an X reached the end of its life and cleaned itself up.
Before you hit Run, do one thing: glance at main and try to guess how many lines will print and in what order. You'll almost certainly be wrong, and that's the point — the gap between your guess and reality is exactly the thing nobody internalizes from reading.
Run it. Then paste me the exact output, every line, in order. We're going to read it together line by line — and one or two of those lines are going to surprise you.
from me
construct A
move A
destroy
construct B
move B
move A
destroy
destroy
--- copying A ---
copy A
--- end of main ---
destroy A
destroy A
destroy B
Beautiful. You just produced the thing 70% of people never see. Let's read it together, because every single line is the language telling you a secret. I'll walk it in three acts.
Act one — building the vector (your first eight lines):
construct A ← Tracer{"A"} is born as a temporary
move A ← its guts get moved into the vector's storage
destroy ← the temporary dies... but its name is BLANK
construct B ← Tracer{"B"} is born as a temporary
move B ← B's guts moved into new, bigger storage
move A ← wait — A moved too, and you never touched A!
destroy ← blank again
destroy ← blank again
Two things here are the whole lesson. First, those destroy lines with nothing after them. Why is the name blank? Because a move doesn't duplicate — it steals. When the temporary's contents got moved into the vector, the string "A" was yanked out of the temporary, leaving it hollow. So when that emptied-out husk dies a moment later, it announces its name... and there's nothing left to announce. That blank line is you watching the difference between copy and move with your own eyes: a move leaves a corpse behind. That's the entire reason moves are fast — nothing gets duplicated, the guts just change owner.
Second, look at move A on the second push_back. You added B. You never mentioned A. So why did A move? Because the vector had room for exactly one item, and you asked it to hold two. So it grabbed a bigger chunk of memory and relocated everything it already had into the new chunk — including A. That's the line nobody predicts. It's also why experienced people warn that growing a vector can shuffle its contents around in memory: you just saw the shuffle. (And the reason A got moved rather than copied during that shuffle is that little noexcept I told you to leave alone — it's a promise that moving can't fail, which lets the vector trust moves while resizing. That was its job.)
Act two — the copy:
--- copying A ---
copy A ← a real, full duplicate
Tracer c = v[0]; made a copy, not a move. Why a copy this time? Because v[0] is a named thing that still lives in your vector — C++ won't gut something you might use again. The temporaries earlier were throwaways nobody would touch, so C++ moved them. Here, you'd be heartbroken if v[0] got hollowed out, so C++ copies. That's the genius you're watching: the language automatically moves what's disposable and copies what's precious, and you wrote zero lines to make that happen. This is also the & lesson from Hour One made visible — remember "no & means a silent copy"? There it is, no longer silent, costing you a real copy.
Act three — the end:
--- end of main ---
destroy A ← that's c, the copy
destroy A ← that's the A living in the vector
destroy B ← that's the B living in the vector
Notice the order. c was the last thing built, and it dies first. Objects clean themselves up in the reverse order they were created — last in, first out, like stacking plates and taking them off the top. And notice these three names are all present, not blank: these are the live, full objects, dying naturally at the end of their scope. You never wrote a single delete, yet every object — the temporaries, the copy, the two in the vector — was cleaned up, automatically, exactly once. That is RAII. That is the whole personality of the language, and you just watched it run.
Sit with this for a second, because here's what you now own that months-long students usually don't: a mental movie. When you write C++ from now on, you'll picture objects being born, occasionally moved when they're temporary, copied when they're kept, and torn down in reverse at the end of their block. That movie is the difference between fighting the language and flowing with it. Memory leaks, "why is this slow," "why did my pointer break after push_back" — you now have the intuition to see all of it coming.
That's the exercise. You did it. Hour Three, the important one, is in the bank.
Hour Four is the short, practical finish: the one time you actually need the heap (and the std::unique_ptr that handles it for you so RAII still does the cleanup), plus a tiny survival kit for when the compiler yells at you. Want to land the plane?
Lets Go!
Let's land it. Two things in this last hour: the one time you touch the heap, and how to survive the compiler. Then you're functional and on your own.
First, a truth that'll save you from a hundred bad tutorials: you've been using the heap safely this entire time and didn't have to think about it. Every std::vector and every std::string you wrote stores its contents on the heap internally — and cleans it up for you, automatically, via the RAII you watched in Hour Three. That's the point of them. So the honest answer to "when do I manually allocate heap memory?" is: far less often than tutorials pretend. Default to plain values, vector, and string, and you'll almost never need anything else.
But occasionally you genuinely need a single object whose life you control directly. When that day comes, you do not reach for new and delete — those belong to the old language we've ignored all along. You reach for this. Run it (note the new #include <memory>):
#include <iostream>
#include <string>
#include <memory>
#include <utility>
struct Tracer {
std::string name;
Tracer(std::string n) : name(std::move(n)) { std::cout << "construct " << name << '\n'; }
~Tracer() { std::cout << "destroy " << name << '\n'; }
};
int main() {
std::cout << "--- making one on the heap ---\n";
std::unique_ptr<Tracer> p = std::make_unique<Tracer>("Heap");
std::cout << "name is: " << p->name << '\n';
std::cout << "--- end of main ---\n";
}You'll see construct Heap, then the name, then — with no delete anywhere — destroy Heap fires on its own at the end. That's the whole lesson: std::make_unique<Tracer>(...) builds a Tracer on the heap and hands you a unique_ptr that owns it. The unique_ptr itself is an ordinary stack value following the exact RAII rule from Hour Three, so when it dies at the end of the block, it deletes the heap object it was guarding. Heap allocation, stack-like automatic cleanup. (One quirk you'll notice: you reach inside it with -> instead of ., because it's a pointer. And it's called unique because it's the sole owner — you can move it to a new owner but never copy it, which is precisely what stops two owners from both deleting the same thing and crashing.) So your rule for life: forget new and delete exist; if you must own a heap thing, make_unique does it and RAII cleans it.
Now the survival kit, because you will fight the compiler and I want you to win.
Turn warnings on. Online, look for a flags or settings box and add -Wall -Wextra; locally those go on your g++/clang command. Warnings are the compiler quietly telling you about real bugs before they bite — treat every one as something to fix, not ignore.
When you get errors, read the first one only. C++ errors cascade: a single missing ; can spew twenty angry messages. Don't read the wall. Fix the topmost error, recompile, and watch most of the rest evaporate. (And when that first error is a baffling pile of <> from deep inside a template, it's almost always something simple at your call site — usually a type mismatch.) The classic beginner one you've already been warned about: a missing semicolon after a struct's closing brace.
And the one genuinely scary idea, stated plainly so it never ambushes you: undefined behavior. If you do something illegal — read past the end of a vector, use an object after it's destroyed, use a variable you never gave a value — C++ does not promise to crash and tell you. It promises nothing. The program might work today, crash tomorrow, or quietly corrupt data and look fine the whole time. So "it ran on my machine" is never proof your code is correct. This is exactly why warnings and the RAII discipline you've built matter: they keep you out of that territory before it ruins an afternoon.
That's the plane on the ground. Here's where you actually are now, because I won't get to tell you later: you can read input, model the world with your own types, store and sort and transform collections, organize code into clean functions with the right copy-or-reference choice, and — the rare part — you can see how objects live and die, which means you understand why the language is fast and safe instead of just memorizing rules. That genuinely is functional, and the lifetime intuition puts you ahead of where a lot of people are after months.
The road from here, so you're not stranded: next pick up std::map (for key-to-value lookups, like a name to a person), std::optional (for "maybe there's a value, maybe not" without crashing), and a real wander through — std::sort was your first taste, there are dozens more that replace loops you'd otherwise write by hand. When you want a real reference, learncpp.com is the best free beginner course there is, and cppreference.com is where you look things up once you know what you're hunting for. Build a small thing you actually want — a to-do tracker, a tiny expense log — using the people program as your skeleton. Building beats reading, every time.
You did the work in four hours. Go write something. Good luck.