Exercises — C++ as superset of C — key additions
Before we start, one word we will lean on constantly:
Level 1 — Recognition
Goal: identify which C++ addition is on screen. No coding yet.
L1.1
For each line, name the bucket it belongs to (Classes/OOP · Templates · References+new/delete+overloading · Namespaces · STL+exceptions+stronger typing):
std::vector<int> v;template<typename T> T id(T x){ return x; }namespace gfx { void init(); }int& r = x;class Cat { public: void meow(); };
Recall Solution
std::vector→ STL (andstd::is a namespace too, but the object itself is STL).template<...>→ Templates.namespace gfx→ Namespaces.int& r(the&on a type, not on a variable) → References.classwithpublic:→ Classes/OOP. Score: 5 correct = you can read C++ vocabulary.
L1.2
One of these compiles in C but not in C++. Which, and which bucket explains the difference?
(a) int new = 5;
(b) int x = 10;
(c) struct P { int a; };Recall Solution
(a) fails in C++. new is a reserved keyword in C++ (it belongs to the new/delete bucket), so it cannot be used as a variable name. In C, new is just an ordinary identifier. (b) and (c) compile in both. This is exactly why C++ is a near-superset, not a true superset.
Level 2 — Application
Goal: write or fix small snippets that use one feature correctly.
L2.1
Rewrite this fragile C macro as a proper C++ template, then predict the value printed.
#define MAX(a,b) ((a)>(b)?(a):(b))
int i = 3;
int m = MAX(i++, 5); // what is m, and what is i afterward?Recall Solution
Predict the C bug first. The macro expands to ((i++)>(5)?(i++):(5)). The i++ on the left evaluates to 3 (then i becomes 4). Since 3 > 5 is false, the result is 5, so m = 5. But i was incremented once here (to 4). Had the comparison been true, i++ would run a second time — that is the double-evaluation bug.
So with this input: m == 5, i == 4.
The template fix:
template<typename T>
T maxv(T a, T b) { return a > b ? a : b; }
int i = 3;
int m = maxv(i++, 5); // arguments evaluated ONCE, at the callNow i++ is evaluated exactly once (a real function argument), giving a = 3, i = 4, and m = maxv(3,5) = 5. Same answer here, but no lurking double-evaluation for other inputs.
L2.2
This C code is rejected by a C++ compiler. Add the one change that fixes it.
int* p = malloc(4 * sizeof(int));Recall Solution
C++ forbids the implicit void* → int* conversion that malloc returns. Add an explicit cast:
int* p = (int*)malloc(4 * sizeof(int)); // or static_cast<int*>(...)Better still, use the C++ tool built for this: int* p = new int[4]; — see new and delete vs malloc and free.
Level 3 — Analysis
Goal: explain why a behaviour happens, not just what.
L3.1
In C, this is the only way to protect balance; in C++, the compiler protects it. Explain what the compiler is actually enforcing.
class Account {
int balance = 0; // private by default
public:
void deposit(int x) { balance += x; }
int get() const { return balance; }
};
Account a;
a.balance = -9999; // does this compile?Recall Solution
a.balance = -9999; does not compile. Because balance is private (the default for class), the only code allowed to touch it is code inside Account's own member functions. The compiler enforces the invariant "balance changes only through deposit/withdraw" at compile time — it is a static check, costing zero runtime. In C, struct Account exposes balance to everyone; the discipline lives only in the programmer's head. See Classes and Objects in C++.
L3.2
A reference "cannot be null and cannot be reseated." Explain what "reseated" means and why the pointer version below has a bug the reference version cannot have.
void incP(int* p) { p++; (*p)++; } // pointer
void incR(int& r) { r++; } // referenceRecall Solution
"Reseated" means: made to point at a different object after being set. A pointer p is a variable holding an address; p++ reseats it to the next address, so (*p)++ then modifies whatever is next in memory — almost certainly not what the caller wanted (undefined/garbage). A reference r is a permanent alias: r++ can only ever mean "add one to the aliased variable"; there is no syntax to make r refer to something else. So incR cannot commit the "wandered off" bug. See Pointers vs References.
Level 4 — Synthesis
Goal: combine two or more features to solve one problem.
L4.1
Combine templates + overloading + std::string (STL). Write a single describe that works for any type, plus one overload specialised for std::string. Predict both outputs.
#include <string>
#include <iostream>
template<typename T>
void describe(T v) { std::cout << "generic\n"; }
void describe(std::string v){ std::cout << "string!\n"; }
describe(42); // (1)
describe(std::string("hi")); // (2)Recall Solution
(1) describe(42) — 42 is an int. There is no non-template overload taking int, so the compiler instantiates the template with T = int → prints generic.
(2) describe(std::string("hi")) — an exact non-template overload for std::string exists. C++ prefers a non-template exact match over a template instantiation, so the overload wins → prints string!.
Outputs, in order: generic then string!. This shows overloading and templates coexisting: the compiler picks the most specific match. See Templates and Generic Programming and The STL — vector, map, string.
L4.2
Combine namespaces + the collision problem. Two libraries both define init. Complete the calls so each library's init runs exactly once.
namespace net { int init(){ return 1; } }
namespace gfx { int init(){ return 2; } }
// call net's init, then gfx's init, and sum the results
int total = /* ??? */ ;Recall Solution
Qualify each name with its namespace:
int total = net::init() + gfx::init(); // 1 + 2total == 3. Without namespaces (as in C) two global init functions would be a link-time collision — the program would refuse to build. The :: scope-resolution operator disambiguates. See Namespaces and the std namespace.
Level 5 — Mastery
Goal: reason at the edges — undefined behaviour, cost, and every case.
L5.1 (All-cases reasoning)
Classify each pairing as correct, undefined behaviour (UB), or won't compile:
int* p = new int(5); delete p;int* p = new int(5); free(p);int* p = (int*)malloc(4); delete p;int* p = new int[4]; delete p;(note: notdelete[])
Recall Solution
- Correct.
new↔deletematched.*p == 5was constructed, then reclaimed. - UB. Memory from
newfreed withfree— the destructor never runs and the allocator bookkeeping mismatches. Compiles, but behaviour is undefined. - UB.
mallocmemory reclaimed withdelete— same mismatch in reverse. - UB. Array
new[]must pair withdelete[], not scalardelete. Only the first element's cleanup is well-defined; the rest is UB. Only case 1 is safe. Rule: the shape of the allocation dictates the shape of the release. See new and delete vs malloc and free.
L5.2 (Cost reasoning — "don't pay for what you don't use")
A student claims: "Making deposit a member function makes it slower than a free C function." For a non-virtual method, is this true? What single change would introduce a real (tiny) runtime cost?
Recall Solution
False for non-virtual methods. A call like a.deposit(10) compiles to essentially the same machine code as a free function deposit(&a, 10) — the object address is passed as a hidden this pointer, exactly what you'd do by hand in C. Zero overhead.
The change that does add cost: mark it virtual. Then calls go through a vtable (a table of function pointers), costing one extra indirection per call — but you only pay it when you ask for runtime polymorphism. That is the "don't pay for what you don't use" principle made concrete. See Compilation Model — C vs C++.
L5.3 (Exception synthesis + edge case)
Predict the printed output and explain the control flow.
#include <iostream>
int f(int x){
if (x < 0) throw std::string("neg");
return x * 2;
}
int main(){
try {
std::cout << f(3) << "\n"; // (A)
std::cout << f(-1) << "\n"; // (B)
std::cout << f(9) << "\n"; // (C)
} catch (std::string& e) {
std::cout << "caught " << e << "\n";
}
}Recall Solution
Line (A): f(3) returns 6, prints 6.
Line (B): f(-1) throws. Control immediately abandons the try block — line (C) never runs — and jumps to the matching catch, which prints caught neg.
Line (C): skipped entirely.
Final output, in order:
6
caught neg
Key edge insight: a thrown exception unwinds past any remaining statements in the try. This is why exceptions "separate the error path from the happy path." See Exception Handling try-catch-throw.
Figures

L2.1 — why the macro double-evaluates while the template evaluates once.

L5.3 — the throw abandons the try block and jumps to catch (line C never runs).
Wrap-up recall
Recall One-line takeaways for each level
- L1: C++ adds reserved keywords → C code using them fails. Recognise the five buckets on sight.
- L2: templates fix macro double-evaluation;
mallocneeds a cast (or usenew). - L3:
privateis compile-time discipline; references cannot be reseated. - L4: overloading prefers exact non-template matches;
::resolves namespace collisions. - L5: match allocation shape to release shape; non-
virtual= zero cost;throwunwinds, it does not resume.
Connections
- 5.2.01 C++ as superset of C — key additions (Hinglish)
- Classes and Objects in C++
- Templates and Generic Programming
- Pointers vs References
- new and delete vs malloc and free
- Namespaces and the std namespace
- The STL — vector, map, string
- Exception Handling try-catch-throw
- Compilation Model — C vs C++