Exercises — 26 drills, three parts

← Module home · Attempt before opening solutions — they are separated below their parts. For ★★ items, write real code and compile with -Wall -Wextra; the compiler is the first reviewer.

Part A — First classes and encapsulation (E1–E9)

Solutions A

S1. class Counter { public: void increment() { count++; } void reset() { count = 0; } int getCount() const { return count; } private: int count = 0; }; — the = 0 default means even the implicit constructor leaves it valid; getCount is const because it only reads.

S2. void decrement() { if (count > 0) count--; } — the floor guard is a boundary rule (count = 0 is the edge), exactly the Records module’s guard chain minus the return-early shape; returning bool would also be fine if callers need to know.

S3. void set(double c) { if (c >= -273.15) celsius = c; } — the guard lives in the class because the class is the only path to the attribute: with celsius private, a bypassing main line is a compile error, so the invariant “celsius ≥ absolute zero” is structural, not hopeful. In main it would be one more rule every caller must remember.

S4. class Rectangle { public: Rectangle(double w, double h) { setWidth(w); setHeight(h); } void setWidth(double w) { if (w > 0) width = w; } void setHeight(double h) { if (h > 0) height = h; } double area() const { return width * height; } double perimeter() const { return 2 * (width + height); } private: double width = 1, height = 1; }; — the constructor delegates its guarding to the setters (call them in the body); negative width is impossible because the only two paths (constructor, setter) both pass the guard. Tests: Rectangle(-3, 4) → width stays 1; setWidth(0) → no change; area() on defaults → 1.

S5. int face = 1; must exist (the state), and roll() assigns it rand() % 6 + 1. No setter: the face is produced by the die’s behaviour, not chosen by callers — a setter would let outsiders “roll” a six at will, breaking the one thing a die means. (Seeding with srand stays in main — the class shouldn’t re-seed per object.)

S6. Interface: StudentRecord(const string& n, int roll), getName/ getRollNo const, setMark(int quiz, int m) guarding quiz ∈ {0,1,2} and m ∈ [0,100], average() const over entered marks (−1 sentinel = not entered; count them, don’t average sentinels). Tests: valid set → reflected; setMark(2, 101) → unchanged; setMark(3, 50) → unchanged; average() with no marks → 0.0 (division guarded); average with two marks → mean of the two, not divided by 3. The derived-data rule from the mini-project carries over: grade would likewise be a const method, never a stored attribute.

S7. Problems: (1) public attribute — any line anywhere can set radius = -2, so the invariant is unenforced and the debugging search space is the whole program; (2) area logic lives outside the class — behaviour that belongs to the object’s data was left in main, so every caller reimplements it (DRY violation). Fix: private radius, setRadius with > 0 guard (or constructor-time guard), area() const and circumference() const as members.

S8. clock_t started = 0; bool running = false; plus methods: start() (mutates; also guards re-start), stop() (mutates; refuses if not running), elapsedSeconds() const (reads), isRunning() const. The guard that matters: stop() before start() — refused via running flag, so started = 0 never masquerades as an elapsed time. Two const methods: the readers.

S9. bool set(const string& c) { bool hasDigit = false; if (c.length() < 8) return false; for (char ch : c) if (isdigit((unsigned char)ch)) hasDigit = true; if (!hasDigit) return false; value = c; return true; } and bool ok() const { return !value.empty(); }. No getter: the whole point of the class is that the cleartext password is not handed out — it is stored for verification, not for reading. Encapsulation hiding something on purpose is the concept with teeth; a get() would undo it. (This is also the honest answer to “getters for every attribute”: no.)

Part B — Constructors, lifetime, this (E10–E18)

Solutions B

S10. Counter() = default; plus Counter(int start) { if (start > 0) count = start; } — with the member default int count = 0; in place, Counter() = default; leaves count 0. Both constructors exist; Counter() is the default constructor (callable with no arguments); Counter(int) is a second overload. Testing: Counter c1; → 0; Counter c2(-5); → 0 (guard); Counter c3(7); → 7.

S11. Book(const string& t, const string& a, int y) : title(t), author(a), year(y) {} — every string parameter is const& to avoid copying each argument (the Functions module’s cost rule), while const promises not to modify; int would pass by value happily (cheap, and a copy is the point).

S12. Book(const string& t, const string& a, int y) : title(t), author(a), year(y) {} with members declared title, author, year — the list matches declaration order. Reordering the list doesn’t reorder the initialization (members initialize in declaration order); with -Wall g++ warns -Wreorder only when the mismatch is detectable. The honest lesson: the compiler’s warning is a service, not a correction — the order is declaration order regardless, so keep the list visually aligned with the declarations and the trap cannot exist.

S13. class Timer { public: Timer(const string& n) : name(n) { cout << "born " << name << "\n"; } ~Timer() { cout << "released " << name << "\n"; } private: string name; }; — with Timer outer("outer"); then { Timer t1("t1"), t2("t2"); ... } inside main, destruction is reverse of construction within each scope: t2, t1 at the inner closing brace; outer at main’s end. The prediction must be written before running — that’s the exercise.

S14. BankAccount() : BankAccount("Unnamed") {} BankAccount(const string& name) : BankAccount(name, 0) {} BankAccount(const string& name, long long opening) : owner(name), balance(opening > 0 ? opening : 0) {} — BankAccount("Sana", -99) routes through the real constructor’s guard → balance 0. Verification test: three constructions, three getBalance() reads (0, 0, expected opening where positive).

S15. double distanceFrom(const Point& o) const { return sqrt(pow(x - o.x, 2) + pow(y - o.y, 2)); } — needs <cmath>, and is const (it reads both points, mutates nothing — including other, which is itself const&). moveBy mutates, so it is not const, and its guards keep both coordinates in [0, 100]. Note the parameter is also const& even for a small object — consistency with the convention.

S16. The statement assigns the parameter to itself — the member is never touched, the setter silently does nothing. The compiler warns (“-Wself-assign-overloaded” on some compilers; at minimum the shadowing warning -Wshadow under -Wextra flags the parameter hiding the member). Cures: rename the parameter (void setOwner(const string& name) { owner = name; }) or write this->owner = owner;. Course-preferred: rename — the name is documentation, and this-> as a routine disambiguator suggests the naming was wrong.

S17. ChainedCounter& increment() { count++; return *this; } — the return type is a reference: returning Counter by value would return a copy, and the chained calls would increment the copy — each a separate temporary — leaving the original moved once at best. *this is the current object itself; the reference return keeps the chain on one object. (One sentence: value return = hand back a photo; reference return = hand back the object.)

S18. ~SessionLog() { delete[] entries; } — the matcher for new string[64]. If a SessionLog is passed by value, the parameter is a copy whose entries pointer holds the same address: both objects now believe they own one block; when both die, delete[] runs twice on it — undefined behaviour, typically a crash or heap corruption. The course’s rule follows directly: objects owning raw resources pass by reference (or const& when read-only). The full machinery for making copies safe (copy constructor/assignment, the rule of three) is named in Lesson 2’s scope note and left for the next course.

Part C — Design, composition, collections (E19–E26)

Solutions C

S19. Date(int y, int m, int d) : year(y), month(clamp(m,1,12)), day(clamp(d,1,31)) {} — where clamp is a small private static-style helper or inline ternaries: month = m < 1 ? 1 : (m > 12 ? 12 : m). toString is const (builds a string, mutates nothing): return to_string(year) + "-" + string(month < 10 ? "0" : "") + to_string(month) + "-" + .... The guarantee: every Date that exists has month 1–12 and day 1–31 — because those are the only two paths to existence, and both validate.

S20. Employee(const string& n, int y, int m, int d) : name(n), hired(y, m, d) {} — the member hired is constructed in the initializer list; there is no way to create an Employee with an unconstructed or invalid Date. const Date& getHireDate() const { return hired; } — returns const reference to avoid copying while promising no mutation. Printing through the getter only: cout << e.getHireDate().getYear(); — the wall holds even for nested objects.

S21. ostream& operator<<(ostream& out, const Employee& e) { out << e.getName() << " (hired " << e.getHireDate().toString() << ")"; return out; } — chaining works because the function returns the same stream it received; << is left-associative, so (cout << e1) yields the stream that then receives " | ", and so on. Uses only getters — no friend needed, no wall breached.

S22. for (const Student& s : roster) cout << s.getRollNo() << " " << s.getName() << " avg " << s.average() << "\n"; — const Student& because (1) by-value Student would copy every string member for every iteration (the cost rule at collection scale), and (2) const is required before calling const methods like average()… precisely: a plain Student& would compile too, but const& documents read-only intent and prevents accidental mutation. (A bare Student would also compile but pays the copy.)

S23. bool buy(int qty) { if (qty <= 0 || qty > stock) return false; stock -= qty; return true; } and bool restock(int qty) { if (qty <= 0) return false; stock += qty; return true; }. Boundary table for buy (stock = 5): qty = 1 → true, 4 (5−1); qty = 5 → true, 0 (the edge: exactly enough); qty = 6 → false, unchanged (the other side of the edge); qty = 0 → false; qty = −2 → false. Both sides of every boundary are exercised — the Debugging module’s family, applied to a method.

S24. Composition. The Cart owns its own vector<Product> — products added are copies whose lifetime is exactly the cart’s; destroy the cart and its contents go with it, automatically, because the vector’s destructor destroys its elements. (If instead products lived in an inventory that the cart merely referenced by index or pointer, that would be aggregation — the ownership decision is the whole answer, and it must be written down per the Lesson 3 field guide.)

S25. bool borrow(const string& title) { if ((int)titles.size() >= 3) return false; if (hasBook(title)) return false; titles.push_back(title); return true; } · bool giveBack(const string& title) { for (size_t i = 0; i < titles.size(); i++) if (titles[i] == title) { titles.erase(titles.begin() + i); return true; } return false; } — hasBook (a const loop over titles) and borrowedCount (titles.size()) as readers. Both invariant clauses (“≤ 3”, “not already borrowed”) are enforced inside borrow; giveBack refuses returns of non-held titles. Every guard is a sentence from step 3 of the design method, and each lives in the method that owns that rule.

S26. The class: private status (an enum class from the Records module — Available, Borrowed), borrow() guards status == Available, giveBack() guards status == Borrowed; both return bool; the status attribute has no setter. What the struct version permitted: any code could write book.status = Borrowed; (or set it twice, or “return” an Available book) — the transitions were a convention. The class makes the illegal transition a compile error rather than a data corruption discovered later — the strongest form of the defensive-programming rule, and the cleanest one-paragraph answer to “why OOP” this exercise can produce.

Where next

— Engr. Dr. Muhammad Nadeem Majeed, Professor, Department of Data Science (PUCIT), University of the Punjab, Lahore · About the instructor