LearnAI ToolsCareerPractice BuildsPlayContact
C++ ProgrammingIntermediate~2.5 hours

Library Management System

Track books and members with different privileges using inheritance and polymorphism.

InheritancePolymorphismSTL

Overview

Every library has more than one kind of member, and every kind of member follows different rules: a student can usually borrow a handful of books at a time, while a faculty member might be allowed far more. Modeling that in C++ is a textbook use case for inheritance and polymorphism — a `Student` and a `Faculty` are both unmistakably a `Member`, sharing a name, an ID, and a list of borrowed books, but each supplies its own answer to "how many books can you borrow at once?" That is exactly what a pure virtual function is for.

By the end of this tutorial you will have a console library application with an abstract `Member` base class, two concrete derived classes (`Student` and `Faculty`), a `Book` class, and a `Library` class that manages both collections using `std::vector`. The most important moment in the whole project is a single line — `member->canBorrow()` — that behaves differently depending on whether `member` actually points to a `Student` or a `Faculty` object, decided automatically at runtime. That is polymorphism working exactly as intended.

What You'll Build
  • A `Book` class tracking title, author, ISBN, and issued/available status.
  • An abstract `Member` base class with pure virtual `getBorrowLimit()` and `getRole()` methods.
  • `Student` and `Faculty` classes that each override those methods with their own privileges.
  • A `Library` class holding `std::vector<Book>` and `std::vector<Member*>` collections.
  • Polymorphic `issueBook()`/`returnBook()` operations that respect each member's actual borrow limit.
  • A menu loop for adding books/members, issuing and returning books, and listing the catalog.

Prerequisites

  • Classes — `private`/`protected`/`public` sections and member functions.
  • Inheritance — deriving a class from a base class with `class Derived : public Base`.
  • Polymorphism — `virtual` and pure virtual (`= 0`) methods, and calling them through a base-class pointer.
  • The Standard Library basics — `std::string`, `std::vector`, and range-based `for` loops.
  • Dynamic memory basics — creating objects with `new` and understanding that whoever allocates them must eventually `delete` them.

Project Structure

The program lives in a single file, `library.cpp`. `Book` is a plain, non-polymorphic class — every book behaves the same way regardless of what it contains. `Member`, by contrast, is deliberately made abstract: it declares two pure virtual methods, `getBorrowLimit()` and `getRole()`, which means `Member` itself can never be instantiated directly — only `Student` and `Faculty`, which each supply real implementations, can. The `Library` class stores books by value in a `std::vector<Book>`, but stores members as `std::vector<Member*>`, because only pointers (or references) to a base class can actually hold either a `Student` or a `Faculty` object and dispatch to the correct overridden method at runtime.

Because `Library` holds raw `Member*` pointers created with `new`, it also takes on the responsibility of freeing them — its destructor loops over every stored pointer and calls `delete`. `Member` declares a `virtual` destructor for exactly this reason: without one, deleting a `Student` through a `Member*` would technically be undefined behavior, since the compiler would not know to also run `Student`'s part of the cleanup.

Step 1: Define the Book Class

A `Book` is simple enough to fully define in one step: an ISBN, title, author, and two fields tracking whether it is currently issued and to whom. `markIssued`/`markReturned` are the only ways those two fields ever change, keeping a book's issued status consistent no matter which part of the program touches it.

#include <iostream> // std::cin/std::cout for all console interaction
#include <string> // std::string for titles, authors, names, and ISBNs
#include <vector> // std::vector to hold the catalog of books and the list of members
class Book {
private:
std::string isbn; // Unique identifier for this book
std::string title; // Book title
std::string author; // Book author
bool issued; // Whether this book is currently checked out
int issuedToId; // Member id who currently holds this book; -1 means not issued
public:
Book(const std::string &isbn, const std::string &title, const std::string &author)
: isbn(isbn), title(title), author(author), issued(false), issuedToId(-1) {} // New books start available
std::string getIsbn() const { return isbn; }
std::string getTitle() const { return title; }
std::string getAuthor() const { return author; }
bool isIssued() const { return issued; }
int getIssuedToId() const { return issuedToId; }
void markIssued(int memberId) { // Called by the Library when a book is checked out
issued = true;
issuedToId = memberId;
}
void markReturned() { // Called by the Library when a book comes back
issued = false;
issuedToId = -1;
}
void display() const {
std::cout << isbn << " | " << title << " by " << author
<< (issued ? " [ISSUED]" : " [AVAILABLE]") << "\n";
}
};

Step 2: Define an Abstract Member Base Class

Every member — regardless of type — has an ID, a name, and a list of currently borrowed titles, so those live in `Member` as `protected` (not `private`) fields, meaning `Student` and `Faculty` can read and use them directly instead of going through accessor methods. `getBorrowLimit()` and `getRole()` are declared `virtual ... = 0`, which makes them pure virtual: `Member` provides no implementation for either, and as a direct consequence, `Member` becomes an abstract class that C++ will refuse to instantiate on its own — you can only ever create a `Student` or a `Faculty`.

class Member {
protected:
int id; // protected, not private, so Student/Faculty can read it directly
std::string name; // Member's display name
std::vector<std::string> borrowedBooks; // Titles this member currently has checked out
public:
Member(int id, const std::string &name) : id(id), name(name) {}
virtual ~Member() {} // Virtual destructor: required because Library deletes Members through base pointers
int getId() const { return id; }
std::string getName() const { return name; }
virtual int getBorrowLimit() const = 0; // Pure virtual: every concrete member type must supply its own limit
virtual std::string getRole() const = 0; // Pure virtual: identifies the concrete type for display purposes
bool canBorrow() const; // Defined in Step 3
void borrowBook(const std::string &title); // Defined in Step 3
void returnBook(const std::string &title); // Defined in Step 3
virtual void displayInfo() const; // Defined in Step 3; virtual so it always shows the real type's data
};

A pure virtual function does not mean "does nothing" — it means "no default exists, and every concrete subclass must provide one." That is different from an ordinary `virtual` method, which does have a body in the base class that a subclass may optionally override.

Step 3: Implement Member's Shared Behavior

`canBorrow()` is where polymorphism does real work: it calls `getBorrowLimit()` on `this`, and because that method is virtual, the call resolves at runtime to whichever concrete class the object actually is — three books borrowed is under the limit for a `Faculty` member but over the limit for a `Student`, and `canBorrow()` does not need to know or care which one it is talking to.

bool Member::canBorrow() const {
return (int)borrowedBooks.size() < getBorrowLimit(); // Virtual call: dispatches to the real derived-class limit
}
void Member::borrowBook(const std::string &title) {
borrowedBooks.push_back(title); // Record that this member now holds this title
}
void Member::returnBook(const std::string &title) {
for (size_t i = 0; i < borrowedBooks.size(); i++) {
if (borrowedBooks[i] == title) {
borrowedBooks.erase(borrowedBooks.begin() + i); // Remove exactly the returned title
break; // No need to keep scanning after removing it
}
}
}
void Member::displayInfo() const {
std::cout << "ID: " << id << " | Name: " << name << " | Role: " << getRole()
<< " | Borrow Limit: " << getBorrowLimit()
<< " | Currently Borrowed: " << borrowedBooks.size() << "\n";
}

`displayInfo()` itself is never overridden by `Student` or `Faculty` in this project — it does not need to be, since it already produces the correct output for either type by calling the virtual `getRole()` and `getBorrowLimit()` internally. This is a subtle but important point: a derived class benefits from polymorphism the moment it overrides even one small virtual method that a larger, un-overridden method depends on.

Step 4: Derive Student and Faculty

With `Member` doing all the shared work, each derived class is remarkably small: a constructor that forwards to `Member`'s constructor, and two one-line overrides supplying that type's actual borrow limit and role label. The `override` keyword is not strictly required by the compiler, but it is good practice — it tells the compiler "I intend to override a virtual method from the base class," and the compiler will produce an error if the signature does not actually match one, catching typos immediately instead of silently creating an unrelated new method.

class Student : public Member { // "public Member" means a Student IS-A Member everywhere a Member is expected
public:
Student(int id, const std::string &name) : Member(id, name) {} // Forwards straight to the base constructor
int getBorrowLimit() const override { return 3; } // Students may have at most 3 books out at once
std::string getRole() const override { return "Student"; }
};
class Faculty : public Member {
public:
Faculty(int id, const std::string &name) : Member(id, name) {}
int getBorrowLimit() const override { return 10; } // Faculty get a much higher borrowing privilege
std::string getRole() const override { return "Faculty"; }
};

Step 5: Build the Library Class

`Library` owns both collections: books by value, since a book never needs to "become" a different kind of book, and members by base-class pointer, since a `Member*` can point at either a `Student` or a `Faculty` interchangeably. The destructor is what makes owning raw pointers safe here — every `Member` added via `addMember` was allocated with `new` somewhere else in the program, and `Library` promises to `delete` each one exactly once when it is itself destroyed.

class Library {
private:
std::vector<Book> books; // Every book in the catalog
std::vector<Member*> members; // Base-class pointers so Students and Faculty can be stored together
public:
~Library(); // Defined in this step; frees every dynamically allocated Member
void addBook(const std::string &isbn, const std::string &title, const std::string &author);
void addMember(Member *m);
Book* findBook(const std::string &isbn);
Member* findMember(int id);
void listBooks() const;
void listMembers() const;
void issueBook(const std::string &isbn, int memberId); // Defined in Step 6
void returnBook(const std::string &isbn); // Defined in Step 6
};
Library::~Library() {
for (Member *m : members) {
delete m; // Every Member was created with "new" in main(), so the Library must free it here
}
}
void Library::addBook(const std::string &isbn, const std::string &title, const std::string &author) {
books.push_back(Book(isbn, title, author)); // Copy-construct a new Book into the catalog
}
void Library::addMember(Member *m) {
members.push_back(m); // Library takes ownership of m; its destructor will delete it later
}
Book* Library::findBook(const std::string &isbn) {
for (Book &b : books) {
if (b.getIsbn() == isbn) {
return &b;
}
}
return nullptr;
}
Member* Library::findMember(int id) {
for (Member *m : members) {
if (m->getId() == id) {
return m;
}
}
return nullptr;
}
void Library::listBooks() const {
std::cout << "\n--- Library Catalog ---\n";
for (const Book &b : books) {
b.display();
}
}
void Library::listMembers() const {
std::cout << "\n--- Library Members ---\n";
for (Member *m : members) {
m->displayInfo(); // Polymorphic call: runs Student's or Faculty's actual data through the base pointer
}
}

Step 6: Issue and Return Books Polymorphically

`issueBook` is where every piece built so far comes together: it looks up the book and member, rejects the request if the book is already out or the member does not exist, and then calls `member->canBorrow()` — the same polymorphic check from Step 3 — to enforce whichever borrow limit actually applies to that member.

void Library::issueBook(const std::string &isbn, int memberId) {
Book *book = findBook(isbn);
Member *member = findMember(memberId);
if (book == nullptr) {
std::cout << "Book not found.\n";
return;
}
if (member == nullptr) {
std::cout << "Member not found.\n";
return;
}
if (book->isIssued()) {
std::cout << "That book is already issued.\n";
return;
}
if (!member->canBorrow()) { // canBorrow() uses the member's real (derived) borrow limit via virtual dispatch
std::cout << member->getName() << " has reached their borrow limit of " << member->getBorrowLimit() << ".\n";
return;
}
book->markIssued(memberId);
member->borrowBook(book->getTitle());
std::cout << "Issued \"" << book->getTitle() << "\" to " << member->getName() << ".\n";
}
void Library::returnBook(const std::string &isbn) {
Book *book = findBook(isbn);
if (book == nullptr || !book->isIssued()) {
std::cout << "That book is not currently issued.\n";
return;
}
Member *member = findMember(book->getIssuedToId()); // Look up whoever currently holds the book
if (member != nullptr) {
member->returnBook(book->getTitle()); // Remove the title from that member's borrowed list
}
book->markReturned();
std::cout << "\"" << book->getTitle() << "\" has been returned.\n";
}

Step 7: Build the Menu Loop

`main` seeds the library with a couple of starting books and members, then loops over a menu that lets the user add more of either, issue and return books, and list the catalog and member roster. Adding a member is the one place the user's choice directly decides which concrete class gets constructed — `new Student(...)` or `new Faculty(...)` — while everywhere else in the program that object is handled uniformly through its `Member*`.

int main() {
Library library;
// Seed the catalog and member list with a few starting records
library.addBook("ISBN001", "The C++ Programming Language", "Bjarne Stroustrup");
library.addBook("ISBN002", "Clean Code", "Robert C. Martin");
library.addMember(new Student(1, "Aditi Sharma")); // Library now owns this pointer
library.addMember(new Faculty(2, "Dr. Rakesh Gupta")); // Same ownership rule applies here
int choice;
do {
std::cout << "\n===== LIBRARY MANAGEMENT SYSTEM =====\n";
std::cout << "1. Add Book\n";
std::cout << "2. Add Member\n";
std::cout << "3. Issue Book\n";
std::cout << "4. Return Book\n";
std::cout << "5. List Books\n";
std::cout << "6. List Members\n";
std::cout << "7. Exit\n";
std::cout << "Enter your choice: ";
std::cin >> choice;
if (choice == 1) {
std::string isbn, title, author;
std::cout << "Enter ISBN: ";
std::cin >> isbn;
std::cin.ignore(); // Discard the leftover newline before reading full-line fields
std::cout << "Enter title: ";
std::getline(std::cin, title);
std::cout << "Enter author: ";
std::getline(std::cin, author);
library.addBook(isbn, title, author);
std::cout << "Book added.\n";
} else if (choice == 2) {
int id;
std::string name, type;
std::cout << "Enter member ID: ";
std::cin >> id;
std::cin.ignore();
std::cout << "Enter member name: ";
std::getline(std::cin, name);
std::cout << "Enter member type (student/faculty): ";
std::getline(std::cin, type);
if (type == "faculty") {
library.addMember(new Faculty(id, name)); // Polymorphism: stored as a Member* either way
} else {
library.addMember(new Student(id, name));
}
std::cout << "Member added.\n";
} else if (choice == 3) {
std::string isbn;
int memberId;
std::cout << "Enter ISBN to issue: ";
std::cin >> isbn;
std::cout << "Enter member ID: ";
std::cin >> memberId;
library.issueBook(isbn, memberId);
} else if (choice == 4) {
std::string isbn;
std::cout << "Enter ISBN to return: ";
std::cin >> isbn;
library.returnBook(isbn);
} else if (choice == 5) {
library.listBooks();
} else if (choice == 6) {
library.listMembers();
} else if (choice == 7) {
std::cout << "Goodbye!\n";
} else {
std::cout << "Invalid choice, try again.\n";
}
} while (choice != 7); // library's destructor runs here, deleting every Member* it owns
return 0;
}

Complete Code

Here is the full program, ready to compile with `g++ library.cpp -o library`.

#include <iostream> // std::cin/std::cout for all console interaction
#include <string> // std::string for titles, authors, names, and ISBNs
#include <vector> // std::vector to hold the catalog of books and the list of members
class Book {
private:
std::string isbn;
std::string title;
std::string author;
bool issued;
int issuedToId;
public:
Book(const std::string &isbn, const std::string &title, const std::string &author)
: isbn(isbn), title(title), author(author), issued(false), issuedToId(-1) {}
std::string getIsbn() const { return isbn; }
std::string getTitle() const { return title; }
std::string getAuthor() const { return author; }
bool isIssued() const { return issued; }
int getIssuedToId() const { return issuedToId; }
void markIssued(int memberId) {
issued = true;
issuedToId = memberId;
}
void markReturned() {
issued = false;
issuedToId = -1;
}
void display() const {
std::cout << isbn << " | " << title << " by " << author
<< (issued ? " [ISSUED]" : " [AVAILABLE]") << "\n";
}
};
class Member {
protected:
int id;
std::string name;
std::vector<std::string> borrowedBooks;
public:
Member(int id, const std::string &name) : id(id), name(name) {}
virtual ~Member() {}
int getId() const { return id; }
std::string getName() const { return name; }
virtual int getBorrowLimit() const = 0;
virtual std::string getRole() const = 0;
bool canBorrow() const;
void borrowBook(const std::string &title);
void returnBook(const std::string &title);
virtual void displayInfo() const;
};
bool Member::canBorrow() const {
return (int)borrowedBooks.size() < getBorrowLimit();
}
void Member::borrowBook(const std::string &title) {
borrowedBooks.push_back(title);
}
void Member::returnBook(const std::string &title) {
for (size_t i = 0; i < borrowedBooks.size(); i++) {
if (borrowedBooks[i] == title) {
borrowedBooks.erase(borrowedBooks.begin() + i);
break;
}
}
}
void Member::displayInfo() const {
std::cout << "ID: " << id << " | Name: " << name << " | Role: " << getRole()
<< " | Borrow Limit: " << getBorrowLimit()
<< " | Currently Borrowed: " << borrowedBooks.size() << "\n";
}
class Student : public Member {
public:
Student(int id, const std::string &name) : Member(id, name) {}
int getBorrowLimit() const override { return 3; }
std::string getRole() const override { return "Student"; }
};
class Faculty : public Member {
public:
Faculty(int id, const std::string &name) : Member(id, name) {}
int getBorrowLimit() const override { return 10; }
std::string getRole() const override { return "Faculty"; }
};
class Library {
private:
std::vector<Book> books;
std::vector<Member*> members;
public:
~Library();
void addBook(const std::string &isbn, const std::string &title, const std::string &author);
void addMember(Member *m);
Book* findBook(const std::string &isbn);
Member* findMember(int id);
void listBooks() const;
void listMembers() const;
void issueBook(const std::string &isbn, int memberId);
void returnBook(const std::string &isbn);
};
Library::~Library() {
for (Member *m : members) {
delete m;
}
}
void Library::addBook(const std::string &isbn, const std::string &title, const std::string &author) {
books.push_back(Book(isbn, title, author));
}
void Library::addMember(Member *m) {
members.push_back(m);
}
Book* Library::findBook(const std::string &isbn) {
for (Book &b : books) {
if (b.getIsbn() == isbn) {
return &b;
}
}
return nullptr;
}
Member* Library::findMember(int id) {
for (Member *m : members) {
if (m->getId() == id) {
return m;
}
}
return nullptr;
}
void Library::listBooks() const {
std::cout << "\n--- Library Catalog ---\n";
for (const Book &b : books) {
b.display();
}
}
void Library::listMembers() const {
std::cout << "\n--- Library Members ---\n";
for (Member *m : members) {
m->displayInfo();
}
}
void Library::issueBook(const std::string &isbn, int memberId) {
Book *book = findBook(isbn);
Member *member = findMember(memberId);
if (book == nullptr) {
std::cout << "Book not found.\n";
return;
}
if (member == nullptr) {
std::cout << "Member not found.\n";
return;
}
if (book->isIssued()) {
std::cout << "That book is already issued.\n";
return;
}
if (!member->canBorrow()) {
std::cout << member->getName() << " has reached their borrow limit of " << member->getBorrowLimit() << ".\n";
return;
}
book->markIssued(memberId);
member->borrowBook(book->getTitle());
std::cout << "Issued \"" << book->getTitle() << "\" to " << member->getName() << ".\n";
}
void Library::returnBook(const std::string &isbn) {
Book *book = findBook(isbn);
if (book == nullptr || !book->isIssued()) {
std::cout << "That book is not currently issued.\n";
return;
}
Member *member = findMember(book->getIssuedToId());
if (member != nullptr) {
member->returnBook(book->getTitle());
}
book->markReturned();
std::cout << "\"" << book->getTitle() << "\" has been returned.\n";
}
int main() {
Library library;
library.addBook("ISBN001", "The C++ Programming Language", "Bjarne Stroustrup");
library.addBook("ISBN002", "Clean Code", "Robert C. Martin");
library.addMember(new Student(1, "Aditi Sharma"));
library.addMember(new Faculty(2, "Dr. Rakesh Gupta"));
int choice;
do {
std::cout << "\n===== LIBRARY MANAGEMENT SYSTEM =====\n";
std::cout << "1. Add Book\n";
std::cout << "2. Add Member\n";
std::cout << "3. Issue Book\n";
std::cout << "4. Return Book\n";
std::cout << "5. List Books\n";
std::cout << "6. List Members\n";
std::cout << "7. Exit\n";
std::cout << "Enter your choice: ";
std::cin >> choice;
if (choice == 1) {
std::string isbn, title, author;
std::cout << "Enter ISBN: ";
std::cin >> isbn;
std::cin.ignore();
std::cout << "Enter title: ";
std::getline(std::cin, title);
std::cout << "Enter author: ";
std::getline(std::cin, author);
library.addBook(isbn, title, author);
std::cout << "Book added.\n";
} else if (choice == 2) {
int id;
std::string name, type;
std::cout << "Enter member ID: ";
std::cin >> id;
std::cin.ignore();
std::cout << "Enter member name: ";
std::getline(std::cin, name);
std::cout << "Enter member type (student/faculty): ";
std::getline(std::cin, type);
if (type == "faculty") {
library.addMember(new Faculty(id, name));
} else {
library.addMember(new Student(id, name));
}
std::cout << "Member added.\n";
} else if (choice == 3) {
std::string isbn;
int memberId;
std::cout << "Enter ISBN to issue: ";
std::cin >> isbn;
std::cout << "Enter member ID: ";
std::cin >> memberId;
library.issueBook(isbn, memberId);
} else if (choice == 4) {
std::string isbn;
std::cout << "Enter ISBN to return: ";
std::cin >> isbn;
library.returnBook(isbn);
} else if (choice == 5) {
library.listBooks();
} else if (choice == 6) {
library.listMembers();
} else if (choice == 7) {
std::cout << "Goodbye!\n";
} else {
std::cout << "Invalid choice, try again.\n";
}
} while (choice != 7);
return 0;
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a `Librarian` derived class with an even higher (or unlimited) borrow limit and the ability to add books directly.
  • Track a due date per borrowed book and calculate a fine in a new virtual `calculateFine(int daysLate) const` method, overridden differently per role (e.g. Faculty pay no fine).
  • Replace the raw `new`/`delete` member ownership with `std::vector<std::unique_ptr<Member>>`, which frees each Member automatically and removes the need for a custom destructor.
  • Add file persistence for the book catalog using the same comma-separated `std::ofstream`/`std::ifstream` pattern from the Bank Management System project.
  • Let `Library::listMembers()` accept a role filter (e.g. only Students) using `dynamic_cast<Student*>` to check each member's concrete type.

Summary

You built a library system where the borrowing rules genuinely differ by member type, enforced through a pure virtual `getBorrowLimit()` rather than an `if`/`else` chain checking a "type" flag. That difference matters: adding a third member type later means writing one new small class, not hunting down every place in the program that branches on member type. Real polymorphism, and the abstract base class that makes it possible, is one of the most valuable tools C++ gives you for keeping growing programs manageable.