C++ intermediate: from working code to idiomatic C++
You can compile and run a basic program. Now make it C++. This part turns "it works" into "it's the right way to do it" — the standard containers, references, classes, automatic resource management, and files.
If Part 1 was "make it run," Part 2 is "make it right." C++ gives you enormous control,
and with it the responsibility to manage memory and resources yourself — but modern
C++ (C++11 and later) hands you tools that do most of that management for you.
The theme of this whole part is borrowed straight from the
C++ Core Guidelines: prefer the standard library, prefer automatic resource management, and say what you
mean.1 Every feature below links to
cppreference.com2 or the Core Guidelines1
so you can read the authoritative version. Before starting, you should be comfortable with
Part 1 — variables, types, cin/cout, if,
loops, and functions.3
Type each example, watch what happens, and don't skip the project at the end — that's where it clicks.
01Better random numbers with <random>
Part 1 used std::rand() for the guessing game. It works, but it's low quality
and now discouraged. The modern replacement is the <random> header:
pick an engine (the source of randomness) and a distribution (the shape
you want), and combine them.4
#include <iostream>
#include <random>
int main() {
std::mt19937 eng{std::random_device{}()}; // seeded engine
std::uniform_int_distribution d{1, 20}; // inclusive range 1..20
for (int i = 0; i < 5; i = i + 1)
std::cout << d(eng) << " ";
return 0;
}
std::mt19937 is a well-tested engine; std::random_device seeds
it from the system; the distribution maps the engine's raw output into the range you asked
for.4 The Core Guidelines point you here
explicitly — avoid std::rand() in new code.1
02const and references
Two ideas separate beginner C++ from idiomatic C++: const (promise not to change something) and references (an alias for an existing variable, not a copy).
A reference is written with & in a type. Pass a big object by reference
to avoid copying it — and by const reference when the function only
reads it:5
void shout(const std::string& text) { // read-only alias, no copy
std::cout << text << "!" << std::endl;
}
int n = 5;
int& r = n; // r is another name for n
r = 10; // n is now 10
const int c = 42; // c can never change
const is a contract the compiler enforces. The Core Guidelines make it a
default: declare everything const unless you have a reason to mutate —
it documents intent and prevents whole classes of bugs.6
The const-reference parameter is the standard way to pass strings and other
large objects.5
03std::string and string_view
You met std::string in Part 1. It's a growable sequence of characters with
helpful methods.7
std::string s = "hello";
s += ", world"; // append
std::cout << s.size() << " " << s.substr(0, 5); // 11 hello
C++17 added std::string_view: a lightweight, non-owning "view" of a string's
characters — perfect for function parameters that only need to read a string,
because it never copies and works on substrings, literals, or
std::string alike.8 Prefer
std::string_view for read-only parameters when you don't need to mutate or
store the text.1
void log(std::string_view msg) { // no copy, accepts any string-like source
std::cout << "[log] " << msg << std::endl;
}
04The standard containers
The single biggest upgrade from Part 1 is the
standard library containers. You no longer manage raw arrays by hand. The
workhorse is std::vector — a growable array.9
#include <vector>
std::vector<int> nums = {3, 1, 4};
nums.push_back(5); // append
std::cout << nums.size() << " " << nums[0]; // 4 3
A few more you'll reach for constantly:
-
std::array<T, N>— a fixed-size array whose size is known at compile time; safer and clearer than a raw C array.10 -
std::map<K, V>— an ordered key→value tree (lookups are O(log n)).11 -
std::unordered_map<K, V>— a hash table; average O(1) lookup when you don't need order.12
std::vector as your default
sequence; reach for std::array only when the size is truly fixed and known at
compile time, and prefer unordered_map over map unless you need
ordered iteration.1
05Range-based for and auto
Looping over a container element-by-element is so common C++ gives you a shorter loop: the range-based for. It reads "for each element x in container c."13
for (int x : nums)
std::cout << x << " "; // copies each element
for (const int& x : nums) // read-only, no copy
std::cout << x << " ";
And auto lets the compiler deduce the type — handy with iterators and
long template names:14
for (const auto& x : nums) // x's type is deduced as const int&
std::cout << x << " ";
06Functions: overloading, defaults, inline
C++ lets you define several functions with the same name if their parameters differ — overloading. The compiler picks the best match.15 You can also give parameters default arguments so callers may omit them.16
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; } // overload
void greet(std::string_view who, bool loud = false) {
std::cout << "Hi, " << who << (loud ? "!!" : ".") << std::endl;
}
greet("Ada"); // Hi, Ada.
greet("Ada", true); // Hi, Ada!!
Small, hot functions can be marked inline as a hint to the compiler to paste
the body at the call site — though modern compilers decide this themselves; treat
inline mainly as the way to define a function in a header without link
errors.17
07enum class and struct
A plain enum leaks its names into the surrounding scope and converts
implicitly to integers — a source of bugs. The fix is enum class (a
"scoped enumeration"): its values are namespaced and don't convert to
int silently.18 The Core
Guidelines strongly prefer it.1
enum class Color { Red, Green, Blue };
Color c = Color::Green; // must qualify the name
// int x = c; // error: no implicit conversion
A struct groups related data into one named type — the lightweight
cousin of a class (the only difference is that struct members are
public by default).19
struct Point { double x, y; };
Point p{1.0, 2.0};
std::cout << p.x << std::endl;
08Classes and objects
A class bundles data and the functions that act on it. The functions are
member functions (methods); public is the interface,
private is hidden state.20 A
constructor builds each object; this refers to the current
instance.
class Counter {
public:
Counter() : count_{0} {} // constructor (member init list)
void increment() { count_ = count_ + 1; }
int value() const { return count_; } // const: doesn't modify the object
private:
int count_;
};
Counter c;
c.increment();
std::cout << c.value() << std::endl; // 1
Note the member initialiser list (: count_{0}) — the
preferred way to set members, and the const on value() promising
it won't change the object. Both are Core Guidelines recommendations.1
09RAII and smart pointers
This is the heart of modern C++. RAII — "Resource Acquisition Is Initialization" — means a resource (memory, file, lock) is tied to an object's lifetime: acquired in the constructor, released in the destructor. When the object goes out of scope, the resource is cleaned up automatically — even if an exception is thrown.21
For heap memory, never call new and delete by
hand. Use a smart pointer: std::unique_ptr owns one object
exclusively; std::shared_ptr shares ownership among several pointers.2223
#include <memory>
std::unique_ptr<Counter> p = std::make_unique<Counter>();
p->increment(); // use -> to call through a pointer
// no delete needed: p frees the Counter when it leaves scope
The Core Guidelines are blunt here: "don't use explicit new,
delete, or owning raw pointers"1
— use std::make_unique / std::make_shared. This single
habit eliminates the vast majority of C++ memory bugs.
10File input and output
Programs that only talk to the screen are limited. C++ reads and writes files with
<fstream>: std::ofstream to write,
std::ifstream to read.2425 Because streams are RAII objects, the
file closes itself when they go out of scope.
#include <fstream>
#include <string>
{ // write
std::ofstream out{"notes.txt"};
out << "first line\n";
} // file closed automatically here
{ // read line by line
std::ifstream in{"notes.txt"};
std::string line;
while (std::getline(in, line))
std::cout << line << std::endl;
}
11Project: a tiny contact book
Let's combine containers, classes, files, and strings into something real: a contact book
that loads names from a file, stores them in a vector, and saves changes
back.
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
int main() {
const std::string file = "contacts.txt";
std::vector<std::string> contacts;
std::string line;
{ // load existing contacts
std::ifstream in{file};
while (std::getline(in, line))
if (!line.empty()) contacts.push_back(line);
}
// add a new one from the user
std::cout << "Add a contact name: ";
std::getline(std::cin, line);
contacts.push_back(line);
{ // save everything back
std::ofstream out{file};
for (const auto& c : contacts) out << c << "\n";
}
std::cout << "Saved " << contacts.size() << " contacts.\n";
return 0;
}
Every piece is from this part: a vector of strings, const where
we don't mutate, std::getline for input, RAII file streams that close
themselves, and range-based for with const auto&. That's a
real, persistent program — and you understand all of it.
12Where to go next
You've gone from "it compiles" to genuinely idiomatic C++: the standard containers,
references and const, enum class, classes with constructors,
RAII and smart pointers, and file I/O. These are the daily tools of working C++ code.
-
Lean on the standard library. Before writing a loop or a raw array,
check whether a container or algorithm already does it —
std::vector,std::string, and the<algorithm>header cover most needs.1 -
Make resources automatic. RAII + smart pointers means you almost never
write
delete. This is the single biggest reliability win in modern C++.21 - Keep a reference open. cppreference.com for specifics; the C++ Core Guidelines for "what should I do?"21
-
Practise. Rewrite Part 1's guessing game using
<random>and aclassto hold the secret number.