NEB 12 Computer Science study materials
NEB 12 Computer Science study resources

Returning a Book Through a Base-Class Pointer

NEB 12 Computer Science · Put your knowledge to work in context.

All case studies

Returning a Book Through a Base-Class Pointer

A Grade 12 team in Biratnagar is building a library loan program in C++. They define class Person { public: std::string name; Person(){} ~Person(){} };, then class Member : public Person { public: int borrowLimit; }; and class Teacher : public Member { public: std::string department; };. Books are class Book { public: std::string title; virtual void show() const; ~Book(){} }; and class EBook : public Book { public: char* pdfData; EBook(int kb){ pdfData = new char[kb*1024]; } ~EBook(){ delete[] pdfData; } };. The shelf is Book* shelf[30];, allocated with new Book(...) or new EBook(...) and freed with delete shelf[i] when a book is returned. A card-printing function is void printCard(Person p), and two loan records are sometimes overwritten with *shelf[i] = *shelf[j];. Symptoms: the card printer never shows borrowLimit or department; a long test loop of borrow-and-return slowly exhausts memory; and after an accidental double return, the program crashes on exit.

1. Explain why printCard(Person p) never displays borrowLimit or department, and why `*shelf[i] = *shelf[j];` is dangerous here. Give the corrected declarations.

Read the worked answer

Object slicing in the parameter. A by-value parameter is a new object of the declared type. printCard(Person p) constructs a Person on the stack and copy-constructs it from the argument. Only the Person subobject of the Teacher or Member is copied; borrowLimit and department are literally cut off - sliced - and the copy has no storage for them. This is a size problem, not a visibility problem: no cast or friendship can get the data back, because that data was never copied. The same happens with void store(Person p) in any container of Person values.

Slicing in assignment. *shelf[i] = *shelf[j] calls Book::operator= on two Book subobjects. If slot j holds an EBook, only title and author are copied; the pdfData pointer in slot i keeps pointing at slot i's own buffer, so the two records now claim the same text but different content. If instead slot i is a plain Book and j an EBook, the EBook's payload is silently dropped.

Corrected declarations:

void printCard(const Person& p);      // no copy, no slicing
virtual void printCard(std::ostream&) const;   // then override in Member, Teacher
Book& operator=(const Book&) = delete;         // forbid partial assignment
virtual Book* clone() const = 0;               // copy polymorphically when needed

The rule worth memorising: pass polymorphic objects by reference or by pointer, never by value, and never assign between objects through a base-class reference unless the base class is designed for it.

2. Trace what happens when `delete shelf[i]` is executed on an EBook that was allocated with `new EBook(...)` and stored as a Book*. Explain the memory growth in the test loop, and give the fix with the precise declaration.

Read the worked answer

shelf[i] has static type Book*. delete on a pointer to a class with a non-virtual destructor performs a non-virtual call: it invokes Book::~Book() only. Since EBook::~EBook() never runs, the delete[] pdfData; inside it never executes. The EBook object's own storage is released (the compiler knows the size of Book), but the heap buffer pdfData points to is orphaned - no pointer to it survives, so it can never be freed. That is a memory leak, and it is why the borrow-and-return loop climbs steadily: each cycle allocates a buffer at new EBook(...) and leaks it at delete. With a scanned PDF of, say, 20 MB per copy, forty returns exhaust the process.

Fix - one word in the base class:

class Book {
public:
    std::string title;
    virtual void show() const;
    virtual ~Book() = default;   // must be declared virtual in the BASE
};

With a virtual destructor, delete shelf[i] looks up the dynamic type and runs EBook::~EBook() first, freeing pdfData, then Book::~Book(). Two related traps to mention: declaring the derived destructor virtual changes nothing, because the call is resolved through the base; and the destructor must be virtual, not merely protected, if deletion through the base pointer is intended. A good exam addition: making ~EBook override the virtual base destructor guarantees at compile time that the signatures match.

3. The crash on exit was traced to the same book being returned twice, so `delete` ran twice on one address. Recommend a redesign of the shelf that makes this failure impossible, and justify each choice.

Read the worked answer

Root cause. Book* shelf[30] is 30 raw pointers that each claim ownership. Nothing prevents two slots from holding the same address, and nothing clears a slot after delete, so a second return deletes an already-freed pointer - undefined behaviour, typically detected as a double free by the allocator. Raw pointers express 'points at' but not 'owns', so the design cannot enforce its own rule.

Redesign:

#include <memory>
#include <vector>

std::vector<std::unique_ptr<Book>> shelf;

// borrow
shelf.push_back(std::make_unique<EBook>(20480));

// return
for (auto& b : shelf)
    if (b->title == returnedTitle) { b.reset(); break; }
  • std::unique_ptr<Book> encodes single ownership in the type system. It cannot be copied, so two slots cannot own one book - the double-return bug becomes a compile error.
  • reset() sets the slot to null after freeing, so a second return finds nothing to delete.
  • Destruction is automatic when the vector goes out of scope (RAII), so no path - including an exception thrown mid-loan - can leak.
  • std::vector removes the arbitrary limit of 30 and the manual bookkeeping of which slots are occupied.
  • std::make_unique<EBook> still needs virtual ~Book() from the previous task; smart pointers fix ownership and deletion-once, not polymorphic destruction.

If the library later needs books shared between a waiting list and a shelf, the correct move is std::shared_ptr<Book> plus std::weak_ptr for non-owning observers - but for 'one copy, one borrower', unique_ptr is the honest model.