The C++ Interview Answers
Every solution, the code, and why it works. Written plain.
These are the problems people ask in C++ interviews. None of them were invented by the person asking. They come from textbooks. They get taught in school, then passed around, then asked in rooms.
Knowing them does not prove you are smart. It proves you have seen them. That is the only thing an interview puzzle measures, so here they are, laid out, so you can see them.
Every entry starts from the beginning. Nothing is assumed. The code compiles. The explanation says which line runs and what value it produces. Terms are the ones programmers actually use, so you hear the same words in the room that you read here; any term the page does not explain where it appears carries a small number linking to the glossary.
Groundwork
What a node is
A node is a struct that holds a value and holds the addresses of other nodes.
That is all it is. In C++ the address of something is held in a pointer. A pointer is a variable whose value is a memory address, the way a street address names a house without being the house.
struct Node {
int value;
Node* left;
Node* right;
};
That node holds one number and two addresses. Either address can be nullptr, which is the value a pointer has when it points at nothing.
What a tree is
A tree is a set of nodes connected so that one node is at the top and every other node is pointed at by exactly one node above it.
- The node at the top is the root.
- A node pointed at by another node is its child. The one pointing is the parent.
- A node whose child pointers are all
nullptris a leaf.
The picture is drawn upside down from a real tree. The root is at the top and the leaves are at the bottom. That is just the convention.
Two rules hold for every tree, and both matter for the code:
- There are no cycles. Follow child pointers down and you can never arrive back at a node you already left.
- Every node is the root of a smaller tree. Pick any node, ignore everything above it, and what remains satisfies the definition of a tree.
What actually makes recursion work on a tree
It sounds too simple to be true, so it is worth separating into three parts that do three different jobs.
- The base case is what makes it finish. Nothing else does. A call that matches the base case returns a value without calling anything, and that is the only way the chain of calls ever ends.
- Rule 1 above is what guarantees you reach the base case. Because there are no cycles, following a child pointer always moves you closer to a leaf. You cannot loop back and keep going forever. So the base case is not just written in the code, it is reachable from anywhere in the tree. Graphs give you no such guarantee, which is exactly why graph code needs a visited set and tree code does not.
- Matching types are what let you call the same function on a child. The function takes a
Node*. A child pointer is also aNode*. Same type, so the same function accepts it, and nothing in the function's body distinguishes a pointer to the root from a pointer to any node below it.
Rule 2 above — every node is the root of a smaller tree — is sometimes given as the reason recursion works. It is not doing much. For a leaf it is true only in an empty way: a leaf is the root of a one-node tree, which tells you nothing, and leaves are exactly where the work stops. The three items in this list are the ones carrying the load.
About the raw pointers here
Every tree and list in part one uses plain Node* pointers. Part two of this page says to use unique_ptr and never write delete by hand. Those look like they contradict each other. They do not, and the reason comes up in interviews.
A pointer can have one of two jobs.
- Owning. This pointer is the one that must be passed to
delete. A raw pointer doing this job is a problem, because nothing callsdeletefor you. - Observing. This pointer is only read. Some other pointer owns the object and will be the one deleted.
In every function below, the Node* parameter is observing. The function reads the tree, returns a value, and calls delete on nothing. A raw pointer is the correct type for that. A parameter of type unique_ptr<Node> taken by value would take ownership and delete the tree when the function returned, which is not what you want. A parameter of type const unique_ptr<Node>& would compile, but you would call .get() on it in the first line to get a Node* anyway.
The struct is the part that is genuinely wrong:
struct Node { // left and right own their children
int value; // and no destructor ever deletes them
Node* left;
Node* right;
};
Those two pointers own the child nodes, there is no destructor, so delete is never called on them and the memory is never freed. In an interview it does not matter: the program is forty lines, it exits in a millisecond, and the operating system reclaims everything. In code you ship it matters. Here is the owning version:
struct Node {
int value;
std::unique_ptr<Node> left;
std::unique_ptr<Node> right;
};
Now each node owns its children. Destroying the root runs its destructor, which destroys both unique_ptr members, which destroys their nodes, on down the tree. No delete is written anywhere.
The walking functions do not change. They still take Node*, because they are still only reading. You call them with root.get(), which returns the raw address held inside the unique_ptr without transferring ownership.
The code in part one uses raw pointers on purpose, because that is the shape an interviewer will hand you. LeetCode's own function signatures1 are raw TreeNode* and the habit travels with the problems. If you want to say something about it in the room, say this: "I'll treat these as non-owning observer pointers — I'm assuming the caller owns the tree and deletes it." It takes six seconds and it says you think about lifetime before you think about syntax.
What recursion is
Recursion means a function that calls itself.
That sounds like a mistake, or a loop with no exit. It is not. It works because each call passes itself a smaller argument than the one it received. Keep shrinking the argument and eventually a call gets one small enough that it can compute a return value without calling anything. That call returns. Then the call that was waiting on it computes its own return value and returns. That continues back up the chain to the first call.
Every recursive function has exactly two parts, and these are the names interviewers use for them.
- The base case. An argument small enough that the function returns a value without making another call.
- The recursive case. Call yourself with a smaller argument, then combine the returned values into this call's return value.
Leave out the base case and every call makes another call. The call stack2 grows with each one until it is exhausted, and the process crashes. That is the one way to get recursion wrong.
A small example
int sumTo(int n) {
if (n == 0) return 0; // base case
return n + sumTo(n - 1); // recursive case
}
Call sumTo(3) and this is the order things happen:
sumTo(3) runs return n + sumTo(n - 1); and stops to call sumTo(2)
sumTo(2) runs the same line and stops to call sumTo(1)
sumTo(1) runs the same line and stops to call sumTo(0)
sumTo(0) matches the base case and returns 0
sumTo(1) now has 0, computes 1 + 0, returns 1
sumTo(2) now has 1, computes 2 + 1, returns 3
sumTo(3) now has 3, computes 3 + 3, returns 6
Look at the shape. All four calls start before any addition happens. The additions all happen on the way back, in reverse order. Nothing is added until sumTo(0) returns.
While sumTo(0) is running, three other calls exist and are each stopped partway through the same line of code, waiting on a return value. That is what the indenting in the trace shows.
The part that stops people in the room
You do not have to trace it like that to write it. Tracing is how you check it afterward. Writing it is a different and shorter process:
- State what value the function returns for a given argument.
- Write the base case.
- For everything else, write the call as though the function already works correctly on the smaller argument, and combine the returned values.
Step three feels like cheating. It is not. You can rely on the function you are in the middle of writing, because the version that runs is the finished one. People freeze on recursion because they try to hold every pending call in their head at once. Nobody can do that, and you do not need to.
The shapes
1. Evaluate an expression tree
What they ask you
You get a tree. Each node holds one of two things: a math operator like + or *, or a number. A node holding a number has no children. A node holding an operator has exactly two children. Walk the tree and return the value the whole expression works out to.
Here is a small one:
*
/ \
+ 3
/ \
1 2
That tree means (1 + 2) * 3, and the value is 9.
The code
struct Node {
bool isOperator; // true = holds +, -, *, / false = holds a number
char op; // only read when isOperator is true
int value; // only read when isOperator is false
Node* left;
Node* right;
};
int evaluate(Node* n) {
// Base case: a number node's value is the answer for that node.
if (!n->isOperator) {
return n->value;
}
// Recursive case: call evaluate on each child, then combine the two ints.
int a = evaluate(n->left);
int b = evaluate(n->right);
switch (n->op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
}
return 0; // not reached if the tree is well formed
}
Why it works
evaluate returns an int. One int, for whatever node it was given. It never builds an expression and never writes a parenthesis.
Every value node is a base case. A value node has no children, so it is a leaf, and it returns its number without making any call.
That holds in both directions here. Every leaf is a value node too, because an operator node always has exactly two children and so can never be a leaf. In this tree, "value" and "leaf" name the same set of nodes.*
* That holds because every operator in this problem takes two numbers. If somebody added an operator that takes one number, like a negative sign or a square root, that node would have a single child, and a leaf would no longer have to be a value.
An operator node does not read anything below it. It calls evaluate on its left child and stores the returned int in a. It calls evaluate on its right child and stores the returned int in b. It applies the one operator it holds to a and b and returns that. Three lines.
The parentheses you were trying to reconstruct are already determined by the shape of the tree. In the picture above, the + node is deeper than the * node. Depth decides order: a node's two child calls both run to completion before that node's arithmetic runs. So the + finishes before the * starts, which is exactly what the parentheses in (1 + 2) * 3 specify. You do not add the parentheses. Whoever built the tree already did.
What it looks like running
Two lines in the function make the calls:
int a = evaluate(n->left);
int b = evaluate(n->right);
When the function reaches the first line, it calls evaluate with the address of its left child. That call runs all the way to a return before the next line of the caller runs. The returned int is stored in a. Then the same for b.
evaluate(*) runs int a = evaluate(n->left); and stops there
evaluate(+) runs int a = evaluate(n->left); and stops there
evaluate(1) matches the base case, returns 1
evaluate(+) now has a = 1
evaluate(+) runs int b = evaluate(n->right); and stops there
evaluate(2) matches the base case, returns 2
evaluate(+) now has a = 1 and b = 2, computes 1 + 2, returns 3
evaluate(*) now has a = 3
evaluate(*) runs int b = evaluate(n->right); and stops there
evaluate(3) matches the base case, returns 3
evaluate(*) now has a = 3 and b = 3, computes 3 * 3, returns 9
Any line indented further to the right is a call that an outer call is stopped and waiting on. At the deepest point two calls exist that are each stopped partway through a line while a third one runs.
Where this shows up in real work
This is what a compiler does with your source code, what a spreadsheet does with a typed formula, and what a database does with a SQL query. Any program that has to read an expression a human wrote and compute its value builds a tree like this one and walks it this way.
You would almost never build one of these by hand in application code, which is why it can feel like a made-up puzzle. It is not made up. It is the inside of tools you use every day.
2. Walk a tree in order
What they ask you
Visit every node in a tree and print the values. Then they name a specific order they want.
The code
void preOrder(Node* n) { // this node, then left, then right
if (n == nullptr) return;
std::cout << n->value << " ";
preOrder(n->left);
preOrder(n->right);
}
void inOrder(Node* n) { // left, then this node, then right
if (n == nullptr) return;
inOrder(n->left);
std::cout << n->value << " ";
inOrder(n->right);
}
void postOrder(Node* n) { // left, then right, then this node
if (n == nullptr) return;
postOrder(n->left);
postOrder(n->right);
std::cout << n->value << " ";
}
Why it works
All three functions are the same function. The only difference is which line the std::cout sits on relative to the two calls. Move that one line and the output order changes. That is the whole mechanism.
All three return void. They print to std::cout and return no value.
- Pre-order prints a node's value before either call runs, so every parent prints before its children. Use it to copy a tree: you have to construct the parent node before you can store child addresses in its
leftandright. - In-order prints a node's value between the two calls. On a binary search tree, where every value in the left subtree is smaller and every value in the right subtree is larger, this prints the values in sorted order, smallest first. That is the reason in-order gets asked about.
- Post-order prints a node's value after both calls return, so every child prints before its parent. Use it to destroy a tree: you must
deletethe children before youdeletethe parent, because the parent holds the only pointers to them.
Why the base case tests for null
The base case is if (n == nullptr) return; rather than a test for a leaf. That is deliberate.
If you test for a leaf instead, you still need a null test for a node that has one child and one null pointer, so you end up writing two tests. One null test at the top covers both that case and a null root passed in for an empty tree.
The follow-up question
They often ask whether you can do it without recursion. Yes: declare your own std::stack<Node*>, push the root, then loop — pop a node, print it, push its children. The recursive version was storing pending nodes on the call stack. The loop version stores them in a stack you declared. Same information, different place.
3. Walk a tree level by level
What they ask you
Print the tree one level at a time. The root, then every node one step below it, then every node two steps below. Left to right across each level.
The name for this order is breadth-first, and the name for the order recursion produces is depth-first. Interviewers use both terms, often as BFS and DFS.
The code
#include <queue>
void levelOrder(Node* root) {
if (root == nullptr) return;
std::queue<Node*> pending;
pending.push(root);
while (!pending.empty()) {
Node* n = pending.front();
pending.pop();
std::cout << n->value << " ";
if (n->left) pending.push(n->left);
if (n->right) pending.push(n->right);
}
}
Why it works
A std::queue is first in, first out. push adds to the back, front returns the item at the front, and pop removes it.
Each pass of the loop does four things: read the front node, pop it, print its value, and push that node's children onto the back.
Because children are always pushed to the back, they sit behind every node already in the queue. Every node on level one was pushed before any node on level two, since level two's nodes were only pushed while level one's were being popped. So the pop order is level order.
Nothing in the code records which level it is on. The first-in-first-out ordering of the queue produces level order on its own.
Why this one is not recursive
Recursion produces depth-first order: in preOrder, the call on n->left runs to completion, including everything beneath it, before the call on n->right begins. Level order needs the opposite, so this one uses a loop and a queue.
Change std::queue to std::stack, and front() to top(), and the same loop produces depth-first order instead. That is worth knowing, because it is a common follow-up.
If they want each level printed separately
while (!pending.empty()) {
int levelSize = (int)pending.size(); // read before the inner loop runs
for (int i = 0; i < levelSize; i++) {
Node* n = pending.front();
pending.pop();
std::cout << n->value << " ";
if (n->left) pending.push(n->left);
if (n->right) pending.push(n->right);
}
std::cout << "\n"; // level finished
}
You read pending.size() into levelSize before the inner loop starts. At that moment the queue holds exactly the nodes of the current level and nothing else. The inner loop pops that many. Anything pushed during the inner loop is on the next level and is not popped until the next pass of the outer loop.
4. Walk a graph
What they ask you
Same as walking a tree, except the pointers can lead anywhere, including back to a node you already visited.
What a graph is
A graph is a set of nodes and the pointers between them, with no constraint on shape. A node can hold twenty pointers or none. Two nodes can each hold a pointer to the other. Follow pointers and you can arrive back where you started.
A tree is a graph that satisfies two extra constraints: no cycles, and exactly one parent per node. Remove those constraints and tree code breaks, because a cycle makes it recurse without end.
The code
#include <unordered_set>
#include <vector>
struct GraphNode {
int value;
std::vector<GraphNode*> neighbors; // any number of pointers
};
void visit(GraphNode* n, std::unordered_set<GraphNode*>& visited) {
if (n == nullptr) return;
if (visited.count(n)) return; // this node was visited already
visited.insert(n);
std::cout << n->value << " ";
for (GraphNode* next : n->neighbors) {
visit(next, visited);
}
}
Why it works
One line does the new work: if (visited.count(n)) return;
visited is a std::unordered_set of pointers. visited.count(n) returns 1 if that pointer is in the set and 0 if it is not. So the first line of real work in the function is a test: if this node's address is already in the set, return immediately without printing it and without looping over its neighbors.
Without that test, a cycle in the graph makes visit call itself along the cycle forever. The call stack grows with every call until it is exhausted and the process crashes with a stack overflow. With the test, each node is printed exactly once no matter how the pointers are arranged.
The sentence to say in the room
"A tree has no cycles by definition, so a tree walk needs no visited set. A graph can have cycles, so a graph walk needs one."
Saying that shows you know why the code changed, not just that it changed.
The ampersand in the parameter is load-bearing
The parameter is written std::unordered_set<GraphNode*>& visited. The & makes it a reference, so every call reads and writes the one set that the first caller declared.
Leave the & off and the parameter is taken by value. Each call gets its own copy of the set and inserts only into that copy, so a later call's count() never finds what an earlier call inserted. The cycle protection stops working.
This is a real mistake and it is silent. The code compiles without a warning and then hangs or crashes at run time.
5. Two pointers
What they ask you
Two common versions. Reverse an array in place3. Or: given a sorted array, find two values in it that add up to a target.
The code, reversing
void reverse(std::vector<int>& v) {
int left = 0;
int right = (int)v.size() - 1;
while (left < right) {
std::swap(v[left], v[right]);
left++;
right--;
}
}
The code, finding a pair that adds up
// v must already be sorted, smallest to largest.
bool hasPair(const std::vector<int>& v, int target) {
int left = 0;
int right = (int)v.size() - 1;
while (left < right) {
int sum = v[left] + v[right];
if (sum == target) return true;
if (sum < target) left++; // sum too small, move to a larger v[left]
else right--; // sum too large, move to a smaller v[right]
}
return false;
}
Why it works
"Two pointers" means two index variables instead of one, moved toward each other. The name says pointers but the code uses indexes, and interviewers say it both ways.
Reversing is direct. Swap the first and last elements, move both indexes inward one step, swap again, stop when the indexes meet. Each element is swapped once and no second array is allocated.
The pair problem is the one to understand, because the reason it is correct depends on the array being sorted.
Start with the smallest value and the largest value and add them.
- If the sum is below the target, the only way to raise it is a larger value on the left, so
left++. You can discard the oldv[left]permanently: it was just added to the largest value remaining and the sum was still too small, so no remaining value paired with it can reach the target either. - If the sum is above the target, the same reasoning in reverse.
right--, and the discardedv[right]never needs checking again.
Each pass of the loop permanently eliminates one value from consideration, so the loop runs at most n times and never revisits anything.
What they want you to say about speed
Checking every pair means roughly n times n comparisons. Two pointers touches each element at most once, so it is n comparisons. On a thousand elements that is a million versus a thousand. Written as big O, that is O(n²) versus O(n), and big O means how the work grows as n grows.
6. Sliding window
What they ask you
Find the length of the longest substring with no repeated characters. A substring is a run of characters that sit next to each other in the original string. Or: find the largest sum of any five consecutive numbers in an array.
The code
#include <string>
#include <unordered_set>
int longestSubstringNoRepeats(const std::string& s) {
std::unordered_set<char> inWindow;
int best = 0;
int left = 0;
for (int right = 0; right < (int)s.size(); right++) {
// s[right] is already in the window.
// Erase from the left end until it is not.
while (inWindow.count(s[right])) {
inWindow.erase(s[left]);
left++;
}
inWindow.insert(s[right]);
int length = right - left + 1;
if (length > best) best = length;
}
return best;
}
Why it works
The window is the substring from index left through index right. inWindow holds exactly those characters.
right advances one index per pass of the for loop. If s[right] is already in the set, the inner while erases s[left] and advances left, repeating until the duplicate has been erased. Then s[right] is inserted and the window length right - left + 1 is compared against best.
The reason this is fast: neither index ever decreases. right advances exactly n times across the whole run, and left advances at most n times in total. The while inside the for looks like it should cost n times n, but it cannot, because every pass of the inner loop advances left and left only has n positions to advance through. Total work is proportional to n.
How to recognize one of these
The words to watch for are "in a row," "consecutive," "substring," and "contiguous." If the problem is about a run of neighboring elements rather than any scattered subset, it is a window problem.
7. Counting with a map
What they ask you
Find the first character that appears twice. Check whether two words use the same letters. Find which value appears most often.
The code
#include <unordered_map>
// Do these two words use the same letters the same number of times?
bool sameLetters(const std::string& a, const std::string& b) {
if (a.size() != b.size()) return false;
std::unordered_map<char, int> count;
for (char c : a) count[c]++; // add one per letter of a
for (char c : b) count[c]--; // subtract one per letter of b
for (auto& entry : count) {
if (entry.second != 0) return false;
}
return true;
}
// Which value appears most often?
int mostCommon(const std::vector<int>& v) {
std::unordered_map<int, int> count;
for (int n : v) count[n]++;
int best = 0;
int bestCount = 0;
for (auto& entry : count) {
if (entry.second > bestCount) {
best = entry.first;
bestCount = entry.second;
}
}
return best;
}
Why it works
An unordered_map stores key and value pairs. count['a'] refers to the int stored under the key 'a'.
It is fast because it does not search. It runs the key through a hash function, which turns the key into a number, reduces that number to an index into an internal array of buckets, and then looks only in that one bucket. The number of buckets grows with the number of entries, so the work per lookup stays about the same whether the map holds ten entries or ten million. That hashing step is where the name hash map comes from.
The C++ detail they are checking for
Look at count[c]++ on a key the map has never held.
operator[] on a missing key does not fail and does not return garbage. It inserts the key with the value zero, and returns a reference to that stored zero. The reference is the part that matters: ++ applied to it increments the int inside the map, not a copy. That is why you never have to test whether a key exists before counting it.
The same behavior causes a bug in the other direction. count[c] inserts the key even when you only meant to read. To look up a key without inserting it, call count.find(c), which returns an iterator6, or count.count(c), which returns 0 or 1. Interviewers ask about this because it is a real bug that is easy to write and hard to see.
unordered_map or map?
std::map keeps its keys in sorted order and its lookups are slower. std::unordered_map keeps them in no particular order and its lookups are faster. Use unordered_map unless you need the keys in order. Saying that answers the follow-up.
8. Binary search
What they ask you
Find a value in a sorted array. Return its index, or report that it is not present.
The code
// Returns the index, or -1 if the value is not in the array.
int find(const std::vector<int>& v, int target) {
int low = 0;
int high = (int)v.size() - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (v[mid] == target) return mid;
if (v[mid] < target) low = mid + 1; // target is above mid
else high = mid - 1; // target is below mid
}
return -1;
}
Why it works
Compare v[mid] with the target. Because the array is sorted, that one comparison eliminates half the remaining range.
If v[mid] is below the target, every element from low through mid is also below the target, so low = mid + 1 discards all of them. If v[mid] is above the target, high = mid - 1 discards the other half. Either way one comparison halves the range, and the loop repeats on what is left.
A thousand elements takes about ten comparisons. A million takes about twenty. Each doubling of the array costs one more comparison. That is why this gets asked.
The three places people get it wrong
This is eight lines and most people write a bug in it the first time. The bugs are always in the same three spots.
low <= high, notlow < high. When one element remains,lowandhighhold the same index. With<the loop condition is false and the loop exits without ever comparing that element, so the function returns -1 for a value that is present.mid + 1andmid - 1, notmid. You already comparedv[mid]and it did not match, so exclude that index from the next range. Writinglow = midcan leavelow,highandmidunchanged from one pass to the next, and the loop never terminates.low + (high - low) / 2, not(low + high) / 2. Both compute the same index almost always. But on a very large array,low + highcan exceed what anintcan represent. Signed overflow5 is undefined behavior4, and in practice the value wraps to a negative number, which is then used as an array index. The first form subtracts before adding, so no intermediate value exceedshigh. This exact bug was in Java's standard library for nine years. Write the safe form and interviewers notice.
What to say about real code
In real code you would call std::binary_search or std::lower_bound and not write this. Say that first. Then write it, because they want to see that you can.
9. Reverse a linked list
What a linked list is
A chain of nodes. Each node holds a value and the address of the next node. The last node's next is nullptr. You hold the address of the first node, and that is your only way in.
struct ListNode {
int value;
ListNode* next;
};
A list is a tree where every node has one child instead of two.
What they ask you
Reverse the chain. The last node becomes the first. Do it by changing the next pointers in the existing nodes, not by building a second list.
The code
ListNode* reverse(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next; // copy the next address before overwriting it
curr->next = prev; // point this node at the previous one
prev = curr; // advance both pointers
curr = next;
}
return prev; // prev holds the address of the old last node
}
Those three names — prev, curr, next — are what everyone calls them, including interviewers. Use them.
Why it works
The loop walks the chain once and reverses each node's next pointer as it goes.
Three pointers are needed instead of two because of the order of two assignments. The line curr->next = prev; overwrites curr->next, and before that line runs, curr->next holds the only copy of the next node's address anywhere in the program. Once it is overwritten, there is no way to reach the rest of the chain. So the line above it, ListNode* next = curr->next;, copies that address into a local variable first.
Three nodes going through it. Start: 1 → 2 → 3 → nullptr.
prev=null curr=1 next=2, 1->next = null
prev=1 curr=2 next=3, 2->next = 1
prev=2 curr=3 next=null, 3->next = 2
prev=3 curr=null loop condition is false, return 3
Result: 3 → 2 → 1 → nullptr
When the loop exits, curr holds nullptr, because it was assigned the last node's next, which is null. prev holds the address of the last node processed, which was the old end of the list and is now the first node. That is why the function returns prev.
It does not return head. head still holds the address of the old first node, which is now the last one, and its next is nullptr.
The question tests two things: whether you copy the next address before overwriting it, and whether you return the right end.
10. Matching brackets
What they ask you
Given a string of brackets like {[()]}, report whether they are balanced. Every opening bracket needs a matching closing bracket, in the right order. ([)] is unbalanced even though the counts match.
The code
#include <stack>
bool balanced(const std::string& s) {
std::stack<char> open;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
open.push(c);
}
else if (c == ')' || c == ']' || c == '}') {
if (open.empty()) return false; // closing bracket, nothing open
char last = open.top();
open.pop();
if (c == ')' && last != '(') return false;
if (c == ']' && last != '[') return false;
if (c == '}' && last != '{') return false;
}
}
return open.empty(); // anything left was never closed
}
Why it works
A std::stack is last in, first out. push adds to the top, top returns the top item, and pop removes it.
That ordering matches how brackets nest. The most recently opened bracket is the one that must close first. So each opening bracket is pushed, and each closing bracket is compared against open.top(), which holds the most recent unclosed opening bracket.
There are three ways a string can be unbalanced, and the code tests all three:
- The loop reads a closing bracket while the stack is empty. Example:
)on its own. Caught byif (open.empty()) return false; - A closing bracket does not match
open.top(). Example:([)]— when the loop reaches),top()is[. Caught by the three comparison lines. - The loop ends with items still on the stack. Example:
([— neither was ever closed. Caught byreturn open.empty();
Omit any one of those tests and the function returns wrong answers on real input. Interviewers pick this problem because it is small enough to finish in the time available and has exactly three edge cases, so it shows whether you enumerated them.
Where this shows up in real work
Every compiler does this, and so does every editor that highlights matching brackets. It is also the first half of writing a parser: check that the brackets match, then build the tree from problem 1.
11. Storing answers
What they ask you
They give you a recursive function that is slow and ask you to make it fast. The usual one is Fibonacci, where each number is the sum of the two before it. Or climbing stairs: you can go up one or two steps at a time, so how many different ways are there to climb n steps?
The slow version
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
That is correct and unusably slow. fib(50) will not finish while you watch.
Here is why. Count the calls for fib(5):
fib(5) calls fib(4) and fib(3)
fib(4) calls fib(3) and fib(2)
fib(3) calls fib(2) and fib(1) <-- fib(3) is computed twice
fib(2) calls fib(1) and fib(0) <-- fib(2) is computed three times
Nothing is stored between calls, so the same arguments are recomputed from scratch every time they come up. Each increase of n roughly doubles the number of calls.
The fix
#include <unordered_map>
int fib(int n, std::unordered_map<int, int>& memo) {
if (n <= 1) return n;
if (memo.count(n)) return memo[n]; // this argument was computed already
int answer = fib(n - 1, memo) + fib(n - 2, memo);
memo[n] = answer; // store it before returning
return answer;
}
Why it works
Two lines were added. memo.count(n) returns 1 if the map already holds a value for this argument, and in that case the function returns memo[n] without making any calls. The other line, memo[n] = answer;, stores the computed value under this argument before returning it.
Each value of n is now computed once. Every later call with the same n returns the stored value. fib(50) returns immediately.
The name for this technique is memoization, which is why the map is named memo. It only works when the same argument always produces the same result, which is true for arithmetic and false for anything that reads a clock, a file, or random input.
The other approach: compute upward
int fib(int n) {
if (n <= 1) return n;
int twoBack = 0;
int oneBack = 1;
for (int i = 2; i <= n; i++) {
int current = oneBack + twoBack;
twoBack = oneBack;
oneBack = current;
}
return oneBack;
}
No recursion and no map. Instead of starting at n and recursing downward, the loop starts at 2 and works upward, keeping only the two values the next step needs.
This is what dynamic programming means. The term covers one idea: a recursive solution that recomputes the same arguments, fixed either by storing results in a map or by computing upward from the base cases in a loop.
Climbing stairs is the same problem
int climbStairs(int n) {
if (n <= 2) return n;
int twoBack = 1; // one way to climb 1 step
int oneBack = 2; // two ways to climb 2 steps
for (int i = 3; i <= n; i++) {
int current = oneBack + twoBack;
twoBack = oneBack;
oneBack = current;
}
return oneBack;
}
To arrive at step n, the last move was either one step from n-1 or two steps from n-2. So the number of ways to reach n is the number of ways to reach n-1 plus the number of ways to reach n-2, which is the Fibonacci recurrence7. Same loop, different starting values.
That is worth knowing in general. Many of these problems are the same few recurrences with different wording.
The language questions
12. friend
First, what private means
Members of a class are marked public or private. The compiler performs an access check on every use of a member. Public members pass the check from anywhere. Private members pass only when the use appears inside that same class.
class Wallet {
public:
int dollars() const { return cents / 100; } // callable from anywhere
private:
int cents; // only Wallet's own code may read or assign it
};
Private exists so you can change how the class stores its data without breaking code that uses the class. If no code outside Wallet can name cents, you can rename it, split it, or change it to a long tomorrow and nothing outside has to change.
What friend does
A friend declaration names one class or one function, and code in that class or function passes the access check on this class's private members.
class Wallet {
public:
int dollars() const { return cents / 100; }
friend class Auditor; // Auditor's code may use Wallet's private members
friend void printWallet(const Wallet&); // so may this one function
private:
int cents;
};
Now code inside Auditor and inside printWallet can read and assign cents. Every other class still fails the access check.
The four facts they are checking for
- The declaration goes inside the class whose private members are being opened up, not inside the class receiving access. The class holding the data decides. You cannot declare yourself a friend of somebody else's class.
- It applies in one direction. Declaring
Auditora friend ofWalletletsAuditor's code useWallet's private members. It does not letWallet's code useAuditor's. - It is not inherited. If
Auditoris a friend ofWallet, a class derived fromAuditorgets no access. The declaration namedAuditor. - It is not transitive. A friend declaration names one class or function; that class's own friends get nothing.
Why it exists
It is a named exception to the access check, so being suspicious of it is reasonable. Two cases make it the right tool.
Stream output. To use your class with std::cout, you write operator<<. That operator cannot be a member function of your class, because the left operand is the stream and a member function's left operand is always the object. So it has to be a free function declared outside the class. But it needs to read your private members to print them. A friend declaration is how it passes the access check.
class Wallet {
public:
friend std::ostream& operator<<(std::ostream& out, const Wallet& w) {
return out << w.cents << " cents"; // w.cents is private; this function is a friend
}
private:
int cents;
};
Two classes implementing one thing. Sometimes a job is split across two classes that have to use each other's internals — a container and the iterator that walks it, for example. Marking the members public to let the two classes use them would let every other class in the program use them too. A friend declaration lets only the named class pass the check.
What to say in the room
"A friend declaration lets one named class or function use the private members of the class that declares it. You write it inside the class granting access. It applies in one direction, it is not inherited, and it is not transitive. The usual reason you need it is operator<<, which has to be a free function but still needs to read your data."
It has nothing to do with polymorphism, inheritance, or virtual functions. Those are the next two entries.
13. virtual and override
The setup
A class can be derived from another class. The derived class has all the members of the base class plus whatever it adds. The original is the base class; the new one is the derived class.
class Shape {
public:
double area() const { return 0; }
};
class Circle : public Shape { // Circle has everything Shape has, plus radius
public:
Circle(double r) : radius(r) {}
double area() const { return 3.14159 * radius * radius; }
private:
double radius;
};
The problem
Circle c(2.0);
Shape* s = &c; // legal: a Circle is a Shape
std::cout << s->area(); // prints 0, not 12.57
The object at that address is a Circle. The call returns 0.
Here is the mechanism. s is declared Shape*. Without virtual, the compiler resolves s->area() to Shape::area using the declared type of the pointer, and it does that while compiling. The generated instruction is a direct call to Shape::area, which returns 0. What the object actually is at run time never enters into it, because the decision was already made and compiled in.
The fix
class Shape {
public:
virtual double area() const { return 0; }
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
private:
double radius;
};
Now s->area() returns 12.57.
Why virtual changes it
Marking a function virtual makes the compiler add a hidden pointer to each object of that class. That pointer holds the address of a table of function addresses, one slot per virtual function. The table is per class, and a Circle's table holds the address of Circle::area in the area slot. The table is usually called the vtable.
The call is then compiled differently. Instead of a direct call to a known address, it reads the hidden pointer out of the object, reads the area slot out of the table, and calls whatever address is in that slot. The address comes out of the object, so an object that is a Circle produces Circle::area.
That costs one pointer per object and two extra memory reads per call. Small, but not zero, which is why C++ makes you ask for it instead of applying it everywhere. Some other languages make every method virtual by default. C++ does not.
What override is for
When a derived function has the same signature as a virtual function in the base class, it overrides it, meaning the derived version goes in the vtable slot. That is the standard word for it and interviewers use it.
The override keyword changes nothing when the code is correct. It exists to make the compiler reject the code when it is not.
class Shape {
public:
virtual double area() const { return 0; }
};
class Circle : public Shape {
public:
double area() override { ... } // compile error: no const
};
That derived function is missing const. Without the override keyword, this compiles. A member function differing in const has a different signature, so C++ treats it as a new unrelated function that happens to share a name. It does not override anything. The vtable slot still holds Shape::area, calls through a base pointer still return 0, and nothing warns you.
With override, the compiler verifies that this function really does override a virtual function in the base class — matching name, parameter list, constness, and return type. Any mismatch is a compile error naming the problem. Put override on every function meant to override a base one. It costs nothing and it catches a bug that is hard to find by reading.
Pure virtual
class Shape {
public:
virtual double area() const = 0; // declared, not defined
virtual ~Shape() = default;
};
The = 0 means the function is declared with no definition in this class. Two consequences follow. Every derived class must define it, or that derived class is abstract too. And the compiler rejects any attempt to create a Shape object, which is usually what you want, since a shape with no particular shape has no area to return. A class with at least one pure virtual function is called abstract.
14. The virtual destructor
What a destructor is
A destructor is a member function that runs when an object is destroyed. It is where you call delete on memory the object allocated, fclose on a file it opened, or unlock on a mutex8 it locked. It is named with a tilde and the class name: ~Wallet().
The bug
class Base {
public:
~Base() { std::cout << "Base\n"; } // not virtual
};
class Derived : public Base {
public:
Derived() { data = new int[1000]; }
~Derived() { delete[] data; std::cout << "Derived\n"; }
private:
int* data;
};
Base* p = new Derived();
delete p;
That prints Base and nothing else. ~Derived() does not run, so the delete[] data; inside it does not run, so the thousand ints are never freed.
Why
Same mechanism as the previous entry. delete p compiles to a call to the destructor chosen from the declared type of p, which is Base*. Since ~Base() is not virtual, there is no vtable lookup — the call to ~Base() is compiled in directly, and the run-time type of the object is never consulted.
The fix
class Base {
public:
virtual ~Base() { std::cout << "Base\n"; }
};
Now delete p reads the destructor's address out of the object's vtable, the same way any virtual call does. The object is a Derived, so the address is ~Derived(). It runs, delete[] data runs, and then ~Base() runs. Both lines print and nothing leaks.
Destructors always run from the derived class to the base, in that order, and the compiler generates the call to the base destructor for you. You never call it yourself.
The rule
If a class has any virtual function, give it a virtual destructor.
The reasoning in one line: having virtual functions means you expect the class to be used through a base pointer, and a class used through a base pointer will eventually be deleted through one.
The failure is silent. Nothing crashes and no warning is issued. Every allocation the derived class made is simply never freed, so the process's memory use climbs with each one until allocation fails. That is why this gets asked in interviews: it compiles clean and ships.
What to say in the room
"If you delete an object through a base-class pointer and the base destructor is not virtual, the call is resolved from the pointer's declared type, so the derived destructor never runs and anything it allocated is never freed. Any class with virtual functions needs a virtual destructor."
15. const
const makes the compiler reject code that would modify something. It appears in four places and the thing being protected is different in each one.
1. A variable that cannot be assigned
const int maxRetries = 3;
maxRetries = 4; // compile error
2. A parameter the function cannot modify
void print(const std::string& name);
Two things are happening in that declaration. The & binds the parameter to the caller's object, so no copy of the string is made. The const makes any assignment through that parameter a compile error, so the caller's object cannot be changed.
Without the const, the caller has no way to know whether the function modifies their string. With it, the compiler enforces that it does not.
This is the most common use of const in real code. Any parameter bigger than a machine word9 that the function only reads is written this way.
3. A member function that cannot modify the object
class Wallet {
public:
int dollars() const { return cents / 100; } // cannot assign to any member
void deposit(int c) { cents += c; } // can
private:
int cents;
};
The const after the parameter list makes any assignment to a member inside that function a compile error.
Here is why you cannot skip it. On a const Wallet, or through a const Wallet& parameter, the compiler rejects calls to non-const member functions. So leaving const off dollars() breaks this:
void report(const Wallet& w) {
std::cout << w.dollars(); // compile error if dollars() is not const
}
One missing const deep in a class forces callers to drop const from their own parameters to compile, and then their callers, outward through the program. The term for getting this right is const-correctness, and it means marking every member function const that does not assign to a member, from the beginning, because adding it later means changing everything that touches the class.
4. On a pointer, where the position changes the meaning
const int* a; // cannot assign through a
int* const b; // cannot assign to b itself
const int* const c; // neither
Read it right to left from the variable name. b is a const pointer to int: *b = 5; compiles, b = &x; does not. a is a pointer to const int: a = &x; compiles, *a = 5; does not.
The first form is the one that comes up. It is the pointer equivalent of a const reference parameter.
What to say in the room
"On a parameter it means assignment through that parameter is a compile error, so the caller's object is safe. After a member function it means assigning to a member inside that function is a compile error, and that matters because the compiler rejects non-const member calls on a const object. On a pointer, which side of the star it is on decides whether the pointer or the pointed-to value is protected."
16. References and pointers
What each one is
A pointer is a variable whose value is a memory address. It occupies memory of its own and can be assigned a different address. It can hold nullptr. You read the pointed-to object with *p or p->member.
A reference is a second name for an existing object. It is bound at initialization and cannot be rebound. It cannot be null. You use it with the same syntax as the original object.
int x = 5;
int* p = &x; // p holds x's address
*p = 10; // x is now 10
int& r = x; // r names x
r = 20; // x is now 20
int y = 99;
p = &y; // p now holds y's address
r = y; // assigns y's value into x. r still names x.
That last line is the one to look at. r = y looks like it should rebind the reference. It does not. r names x, so the line assigns 99 to x. There is no syntax for rebinding a reference.
The differences worth saying out loud
- A reference cannot be null. A pointer can, so a function taking a pointer has to compare it against
nullptrbefore dereferencing it. A function taking a reference does not. - A reference must be initialized where it is declared.
int& r;is a compile error. A pointer can be declared uninitialized, and dereferencing one is undefined behavior. - A reference cannot be rebound. A pointer can be assigned a new address.
- Pointer arithmetic exists.
p + 1is the address of the next element in an array. References have no equivalent.
Which to use
Use a reference when the object is known to exist and you want to read or modify it without copying. That covers most cases.
Use a pointer when "no object" is a state you need to represent, or when you need to refer to different objects over time.
In both cases, add const if the function only reads.
The follow-up question
They often ask whether a reference is a pointer underneath. Usually yes, the compiler stores an address. The standard does not require it, and when every use of the reference is visible and inlined11, the compiler can keep the value in a register10 and never produce an address at all.
The difference that matters is not what the machine does. It is what the compiler accepts. A reference cannot be null and cannot be rebound, so code written with references has fewer ways to be wrong.
17. RAII
The problem it solves
Memory from new must be passed to delete. If it is not, the process holds that memory until it exits. Do that in a loop and memory use climbs until the allocator13 cannot satisfy a request.
Remembering is harder than it sounds:
void process() {
int* data = new int[1000];
if (somethingWrong()) {
return; // delete[] never runs
}
mightThrowAnException(); // delete[] never runs
delete[] data;
}
Two paths out of that function skip the delete[]. Every early return and every exception12 is another exit you have to write cleanup for. Six returns means six deletes, and the one you forget is the leak.
What RAII is
The letters stand for Resource Acquisition Is Initialization. The idea:
Put the resource in an object. Call new (or open, or lock) in the constructor. Call the matching delete (or close, or unlock) in the destructor.
Then you write no cleanup code, because the language guarantees that when an object goes out of scope its destructor runs. That holds on a normal return, on an early return, and during stack unwinding — the process by which an exception propagates, destroying every in-scope object in reverse order of construction as it goes. There is no path out of a function that skips a destructor.
class Buffer {
public:
Buffer(int n) { data = new int[n]; } // acquire
~Buffer() { delete[] data; } // release
int* get() { return data; }
private:
int* data;
};
void process() {
Buffer b(1000);
if (somethingWrong()) return; // ~Buffer runs, delete[] runs
mightThrowAnException(); // ~Buffer runs, delete[] runs
} // ~Buffer runs, delete[] runs
Three exits, no cleanup code written, nothing leaked on any of them.
Why it matters beyond memory
The same pattern covers every resource that has to be released. Open a file in the constructor, fclose it in the destructor. Lock a mutex in the constructor, unlock it in the destructor. The standard library already does this: std::lock_guard unlocks in its destructor, and std::fstream closes in its destructor.
In C you write cleanup by hand on every exit path. In C++ you put the resource in an object and the compiler generates the cleanup for you.
What to say in the room
"Tie the resource's lifetime to an object's lifetime. Acquire in the constructor, release in the destructor. The language guarantees the destructor runs on every exit path, including stack unwinding during an exception, so you never write a delete."
18. unique_ptr and shared_ptr
What they are
RAII applied to one pointer. Each is a small object that holds a raw pointer and calls delete on it in its destructor, so you stop writing delete. They are in <memory>.
unique_ptr: exactly one owner
#include <memory>
void process() {
std::unique_ptr<int[]> data = std::make_unique<int[]>(1000);
if (somethingWrong()) return; // ~unique_ptr runs, calls delete[]
mightThrowAnException(); // ~unique_ptr runs, calls delete[]
} // ~unique_ptr runs, calls delete[]
One unique_ptr holds the owning pointer. When it goes out of scope its destructor runs and calls delete. There is never a second owner, because the compiler rejects copying it.
std::unique_ptr<int> a = std::make_unique<int>(5);
std::unique_ptr<int> b = a; // compile error: copy constructor is deleted
std::unique_ptr<int> c = std::move(a); // compiles: ownership transferred
// a now holds nullptr. c holds the address.
Rejecting the copy is the point. Two unique_ptrs holding the same address would mean two destructors each calling delete on it. The second delete on an already-freed address is undefined behavior, which usually corrupts the allocator and crashes somewhere unrelated. The compiler makes that impossible to write by accident.
It costs nothing. A unique_ptr is the same size as a raw pointer and the generated machine code is the same. There is no reason to call new and delete by hand instead.
shared_ptr: several owners
std::shared_ptr<Thing> a = std::make_shared<Thing>();
std::shared_ptr<Thing> b = a; // compiles. reference count is now 2.
// The Thing is deleted when the count reaches 0.
A shared_ptr holds a pointer to the object and a pointer to a reference count stored alongside it. Copying one increments the count. Destroying one decrements it. Whichever destructor decrements the count to zero is the one that calls delete.
That counting is not free. A shared_ptr is twice the size of a raw pointer, and because several threads14 may hold copies, the count is incremented and decremented atomically15, which costs more than a plain increment.
Which one to use
Use unique_ptr by default. Use shared_ptr only when you cannot determine at compile time which owner outlives the other.
That is the answer interviewers want, and the reason is not performance. unique_ptr states in the code that there is one owner and shows you which variable it is. With shared_ptr, which destructor frees the object depends on run-time order, so you cannot tell by reading.
Most code reaching for shared_ptr wants a unique_ptr for the owner, and plain references or raw pointers for the code that uses the object but does not control when it is destroyed.
The trap they may ask about
Two objects each holding a shared_ptr to the other are never deleted. A's count cannot reach zero while B holds a copy, and B's cannot while A holds one, so neither destructor ever runs and the memory is never freed even though you used smart pointers. The fix is std::weak_ptr, which holds the address without incrementing the count.
19. Copy, move, and std::move
The problem
std::vector<int> makeBigList() {
std::vector<int> v;
for (int i = 0; i < 1000000; i++) v.push_back(i);
return v;
}
std::vector<int> mine = makeBigList();
A million ints were allocated inside that function. Before C++11, this allocated a second million-element block for mine, copied every element into it, and then freed the first block. A million copies and two allocations for no reason.
What a move is
A vector object is small: a pointer to a heap16-allocated block, plus a size and a capacity17. The million ints are in the block, not in the vector object.
So the move constructor does this: copy the pointer, the size, and the capacity from the old vector into the new one, then set the old vector's pointer to nullptr and its size to zero. The old vector's destructor sees a null pointer and frees nothing, so the block is not freed.
Three assignments instead of a million copies, and the block is never reallocated. The ints are never touched. Only which vector object holds the address of the block changes.
What std::move actually does
This is the part people get wrong, and interviewers know it.
std::move does not move anything. It copies no bytes and generates no instructions. It does not write to the object or set a flag on it. Nothing exists at run time that records that std::move was called.
It is a cast. It casts its argument to an rvalue reference18, which changes which overload the compiler selects for the next operation. Overload resolution happens at compile time, so this is purely a compile-time effect. The actual copying of the pointer and nulling of the original happens inside the move constructor, which somebody wrote. std::move only determines that the move constructor is the one called instead of the copy constructor.
std::vector<int> a = makeBigList();
std::vector<int> b = a; // copy constructor: allocates, copies a million ints
std::vector<int> c = std::move(a); // move constructor: copies pointer, size, capacity
// a's pointer is now null, a's size is 0
The state of the object you moved from
After a move, the source object still exists and its destructor will still run. You must not read its value. The standard describes it as "valid but unspecified," which means: it is safe to destroy and safe to assign a new value into, and anything else is a bug.
So do not move from an object and then read it.
What this means in practice
Less than you would expect. The compiler already moves return values without being asked. Every standard container has a move constructor. You can write C++ for a long time without typing std::move.
It earns its place when you write a class that owns a resource, and when you pass something large into a container and will not use the original again:
std::vector<std::string> names;
std::string s = "a long string whose characters live on the heap";
names.push_back(s); // copy constructor: allocates and copies the characters
names.push_back(std::move(s)); // move constructor: copies the pointer. s is empty after.
What to say in the room
"A move copies the pointer, size and capacity into the new object and nulls the original's pointer so its destructor frees nothing. std::move does not move anything — it is a compile-time cast to an rvalue reference, which changes overload resolution so the move constructor is selected instead of the copy constructor. After a move, the source object is still alive and still gets destroyed, but reading its value is a bug."
20. Rule of three, five, zero
The five functions
Every class can have these, and the compiler generates any you do not write:
- Destructor —
~Thing() - Copy constructor —
Thing(const Thing&) - Copy assignment —
Thing& operator=(const Thing&) - Move constructor —
Thing(Thing&&) - Move assignment —
Thing& operator=(Thing&&)
Rule of three
If you wrote any one of the destructor, copy constructor, or copy assignment, you almost certainly need all three.
The bug it prevents:
class Buffer {
public:
Buffer(int n) { data = new int[n]; }
~Buffer() { delete[] data; } // written by hand
// copy constructor generated by the compiler
private:
int* data;
};
void bad() {
Buffer a(100);
Buffer b = a; // the generated copy constructor copies the pointer value
} // both destructors run, both call delete[] on the same address
The compiler's copy constructor copies each member individually. data is a pointer, so it copies the address. Now a.data and b.data hold the same address, and both destructors call delete[] on it. The second call frees already-freed memory, which is undefined behavior and usually crashes somewhere far from this function.
A hand-written destructor is the signal. It means the class holds a resource, and once it does, copying it member by member is wrong.
Rule of five
C++11 added the move constructor and move assignment, so the reasoning extends: if you wrote any of the five, consider all five. If your class owns a resource, you are the only one who knows how it should be copied, moved, and released.
Rule of zero, which is the one to follow
Write none of the five. Build the class out of members that already release their own resources.
class Buffer {
public:
Buffer(int n) : data(n) {}
private:
std::vector<int> data;
};
No destructor, no copy constructor, no move constructor, and no delete anywhere in the class. All five generated functions are correct, because each one just calls the corresponding function on data, and vector's versions are right:
vector's constructor allocates the block.vector's destructor frees it.vector's copy constructor allocates a new block and copies the elements into it, so twoBuffers never share one block.vector's move constructor copies the pointer and nulls the source.
What to say in the room
"Rule of three: if you wrote a destructor, copy constructor, or copy assignment, you need all three, because a hand-written destructor means the class owns a resource and the compiler's member-by-member copy will produce two objects that both delete the same address. Rule of five adds the two move operations. Rule of zero is better — use vector, string and unique_ptr as members, write none of the five, and the generated ones are correct."
Say rule of zero last and say you prefer it.
21. static, three jobs
static means three unrelated things depending on where it appears. That is the question.
1. Inside a function: one variable for all calls
void counter() {
static int timesCalled = 0; // initialized once, on the first call
timesCalled++;
std::cout << timesCalled << "\n";
}
counter(); // 1
counter(); // 2
counter(); // 3
A normal local variable is created when control enters its scope and destroyed when control leaves. A static local is created and initialized the first time control reaches its declaration, and then lives until the program exits. Every later call reads and writes the same variable.
The initializer runs exactly once no matter how many times the function is called. Since C++11 that initialization is thread-safe: if two threads reach it at the same time, one initializes and the other waits.
2. Inside a class: one variable for all objects
class Widget {
public:
Widget() { count++; }
~Widget() { count--; }
static int howMany() { return count; } // no object required to call it
private:
static int count; // one variable, shared by every Widget
};
int Widget::count = 0; // definition, required in exactly one source file
std::cout << Widget::howMany(); // called on the class
A normal member variable exists once per object. A static member variable exists once for the whole program, and it exists whether or not any Widget has been created.
A static member function is called without an object. There is no this pointer inside it, so it cannot read non-static members — only static ones.
Note the line outside the class. A static member declared in the class header is a declaration, not a definition, so it needs a definition in exactly one source file. Leave it out and the link fails with an undefined symbol19 for Widget::count. In C++17 you can write static inline int count = 0; inside the class and skip the separate definition.
3. At file scope: the name is not visible to other files
// helper.cpp
static int secret = 42; // not visible outside this file
static void helperThing() { ... } // not visible outside this file
At file scope, static gives the name internal linkage: the linker will not match it against a same-named symbol in another translation unit20, and no other source file can refer to it.
This is how you keep two files from accidentally sharing a variable because both happened to name it buffer. Modern C++ prefers an anonymous namespace21 for the same purpose, but file-scope static is everywhere in existing code.
What to say in the room
"Three meanings. In a function, a variable initialized once that persists across calls. In a class, one variable shared by all objects, and a function callable without an object and with no this. At file scope, internal linkage — the name is not visible outside the translation unit."
22. How to pass an argument
The three ways
void byValue(std::string s); // parameter is a copy
void byReference(std::string& s); // parameter names the caller's object
void byConstReference(const std::string& s); // same, and cannot be assigned through
By value
The caller's object is copied into the parameter. Assignments to the parameter do not affect the caller's object. For an int, a double, or a pointer, the copy is a few bytes and costs nothing.
For a string or a vector, the copy constructor runs: it allocates a block and copies every element, on every call. If the function only reads the parameter, that work is wasted.
By reference
No copy. The parameter is bound to the caller's object, so assignments through it change the caller's object. Use this when modifying the caller's object is the purpose of the function.
void addItem(std::vector<int>& list, int item) {
list.push_back(item); // the caller's vector grows
}
By const reference
No copy, and any assignment through the parameter is a compile error. This is the default for anything larger than a machine word that the function only reads.
void print(const std::string& name) {
std::cout << name;
}
The rule
- Small and cheap to copy —
int,double,char, a pointer, a small struct — pass by value. A reference would be the same size or larger and would add a memory read to get at the value. - Large, and the function only reads it — pass by const reference.
- The function must modify the caller's object — pass by reference.
- The function will store its own copy regardless — pass by value and
std::moveit into place. This is the exception to rule 2 and it comes up in constructors.
class Person {
public:
Person(std::string n) : name(std::move(n)) {} // a copy is being stored either way
private:
std::string name;
};
What to say in the room
"Small types by value. Large types by const reference if I am reading, by plain reference if I need to modify the caller's object. If the function stores its own copy regardless, take it by value and move it into place."
Reference
Glossary
These are terms the page uses without stopping to explain them, because stopping would break the explanation they appear in. They are the standard words, so you will hear them in the room.
- 1. signature
- A function's name together with the types of its parameters, and for a member function whether it is
const. This is what the compiler uses to tell two functions with the same name apart. - 2. call stack
- The region of memory where each running function call keeps its parameters, its local variables, and the address to return to when it finishes. One layer per call, stacked. Run out of room and the process crashes with a stack overflow.
- 3. in place
- Changing data where it already sits, without allocating a second array or string to hold the result.
- 4. undefined behavior
- Code the C++ standard gives no rules for, so the compiler is allowed to produce anything at all. In practice the program may work today, work differently tomorrow, or crash somewhere unrelated to the mistake. This is why undefined behavior is worse than a bug with a defined wrong answer.
- 5. signed overflow
- A calculation on a signed number whose true result is too large for the type to hold. For
intthat limit is usually 2,147,483,647. - 6. iterator
- An object that refers to one element of a container and can be stepped to the next one. Standard library functions that search a container return an iterator, and return the container's
end()iterator when they find nothing. - 7. recurrence
- A rule that defines each value of a sequence in terms of earlier values of the same sequence. Fibonacci's is: each value is the sum of the two before it.
- 8. mutex
- A lock that allows only one thread at a time to run the code it guards. Short for mutual exclusion.
- 9. machine word
- The amount of data the processor handles as one unit. On most machines today that is 8 bytes, which is also the size of a pointer.
- 10. register
- A storage slot inside the processor itself. Reading one is faster than reading memory, and the compiler decides which values get to live in them.
- 11. inlined
- A function call the compiler replaced with a copy of the function's body, so no call happens when the program runs.
- 12. exception
- An error raised by one function with
throwand handled by a function further up the call stack withcatch. Every function in between exits immediately, running the destructors of its local objects on the way out. - 13. allocator
- The part of the runtime that hands out memory for
newand takes it back ondelete, and keeps track of which blocks are in use. - 14. thread
- One flow of execution inside a program. A program can have several running at the same time on different processor cores, all reading and writing the same memory.
- 15. atomically
- An operation that finishes as a whole, so no other thread can observe it half done or interleave its own operation in the middle. Without this, two threads each decrementing a count can both read the same old value and the count loses one of the decrements.
- 16. heap
- The pool of memory that
newallocates from. A heap allocation lasts until something frees it, unlike a local variable, which ends when its function returns. - 17. capacity
- How many elements a vector's current block can hold before the vector has to allocate a bigger one. Size is how many it actually holds. Capacity is always at least the size.
- 18. rvalue reference
- A reference written
T&&that binds only to a temporary, or to something explicitly handed over withstd::move. It is how the compiler tells "this object is finished with, take its insides" apart from "this object is still in use, make a copy." That distinction is what selects the move constructor over the copy constructor. - 19. symbol
- A name in a compiled file that the linker has to match up with the one place the thing is actually defined. "Undefined symbol" means the linker found uses of a name but no definition.
- 20. translation unit
- One source file plus every header it includes, compiled as a single piece. The compiler sees one translation unit at a time; the linker joins them.
- 21. anonymous namespace
- A namespace with no name, written
namespace { ... }. Everything inside is visible only within that translation unit, the same effect as file-scopestatic, and it is the modern way to write it.