Library Management System in C++

Build a C++ OOP library system with inheritance, polymorphism, and file I/O.

Introduction

30 Second Summary

Every library needs a system to keep track of what it owns. Books, magazines, DVDs all share some traits (a title, a year, an ID) but differ in the details that matter most.

In this project, you will build a console-based Library Management System in C++ that manages books, magazines, and DVDs using a class hierarchy. You will practice encapsulation, inheritance, polymorphism, operator overloading, and file persistence.

What You'll Build

A fully interactive menu-driven console application that stores, displays, searches, and removes library items using object-oriented design.

By the end of this project, you'll have:

  • A class hierarchy with an abstract base class and three derived classes demonstrating inheritance and polymorphism.
  • An interactive menu that lets you add, display, search, and remove items from your library collection.
  • A file persistence layer that saves and loads your library data between sessions.
  • Secret Mission: Add a borrowing system with a Member class that tracks who borrowed what.

Are there any prerequisites?

You should have basic familiarity with C++ syntax (variables, loops, functions) and an IDE already installed on your machine. No prior experience with object-oriented programming is required.

Before We Start

Before you can build classes and inheritance hierarchies, you need a working C++ project that compiles cleanly.

In this step, you will create your project files and verify that your compiler supports C++17. This confirms your environment is ready for the OOP features you will use throughout the project.

In this step, get ready to:
  • Create a C++ project with a minimal program that compiles with C++17.
  • Set up the header files that will hold your class hierarchy.
  • Verify the full project compiles with no errors.
Create your project and verify C++17 compilation

Your compiler needs to support the C++17 standard for this project. You will confirm this by compiling a minimal program.

  • Open your IDE.
  • Create a new file called main.cpp in your project folder.
  • Add the following code to main.cpp:
#include <iostream>

int main() {
    std::cout << "Hello Library!\n";
    return 0;
}

What does this code do?

This is the smallest valid C++ program that produces visible output. #include <iostream> gives you access to std::cout for printing text to the console.

If this compiles and runs with the C++17 flag, your environment is ready for the rest of the project.

  • Compile and run the program to verify C++17 support.

macOS

  • Open your terminal and navigate to your project folder.
  • Compile and run by entering these commands:
g++ -std=c++17 main.cpp -o library
./library

You should see Hello Library! printed in your terminal. These same commands work on Linux as well.

Windows

  • If you use MinGW or g++ on Windows, open your terminal and navigate to your project folder.
  • Compile and run by entering these commands:
g++ -std=c++17 main.cpp -o library.exe
library.exe

You should see Hello Library! printed in your terminal.

Using Visual Studio?

If you use Visual Studio instead of the command line, set the C++ standard in your project settings.

  • Open Project Properties.
  • Select C/C++, then Language.
  • Set C++ Language Standard to ISO C++17.
  • Press Ctrl+F5 to build and run.

Not seeing Hello Library! in the output?

Make sure you saved main.cpp before compiling. Unsaved changes will not be picked up by the compiler.

If you see an error about -std=c++17 not being recognized, your compiler may be too old. Update to GCC 7+ or Clang 5+ for C++17 support.

Help me troubleshoot my C++17 compiler setup.

Create the project file structure

Your library system will use a separate header file for each class. This keeps your code organized and makes each class easy to find.

  • Create five new files in the same folder as main.cpp:
  • LibraryItem.h
  • Book.h
  • Magazine.h
  • DVD.h
  • Library.h
  • Add the following content to LibraryItem.h:
#pragma once
// LibraryItem class - to be implemented
  • Add this to Book.h:
#pragma once
// Book class - to be implemented
  • Add this to Magazine.h:
#pragma once
// Magazine class - to be implemented
  • Add this to DVD.h:
#pragma once
// DVD class - to be implemented
  • Add this to Library.h:
#pragma once
// Library class - to be implemented

What is #pragma once?

#pragma once is an include guard. It tells the compiler to only include this file once per compilation, even if multiple files reference it. Without it, you would get duplicate definition errors when your classes start including each other.

  • Save all five header files.
  • Confirm the files exist by checking your project folder in your IDE's file explorer. You should see six files total: main.cpp, LibraryItem.h, Book.h, Magazine.h, DVD.h, and Library.h.

Don't see all six files?

Make sure each file is in the same folder as main.cpp. If you accidentally created a file inside a subfolder, move it to the top level of your project directory.

Check that file extensions are correct. Header files must end in .h (not .hpp or .txt).

Help me troubleshoot my file structure.

Verify the full project compiles

Now that all files exist, confirm the project still compiles cleanly. The header files are empty placeholders, so they will not cause errors yet.

  • Recompile your project using the same command from earlier:
g++ -std=c++17 main.cpp -o library
  • Run the program again to confirm it still prints Hello Library! with no errors or warnings.

Why recompile now?

You have not included the header files in main.cpp yet, so the compiler does not know about them. This recompile confirms that nothing you created accidentally interferes with the build. You will include these headers in later steps.

✔️ Awesome, I've got everything!

Great. Double check you have saved all six files.

ⓧ I'd like to double check the full code

Here is what each file should contain at this point:

#include <iostream>

int main() {
    std::cout << "Hello Library!\n";
    return 0;
}
#pragma once
// LibraryItem class - to be implemented
#pragma once
// Book class - to be implemented
#pragma once
// Magazine class - to be implemented
#pragma once
// DVD class - to be implemented
#pragma once
// Library class - to be implemented

Your project structure is ready. Next up, you will build the base class that every library item inherits from.

Build the Base Class with Encapsulation

Every item in a library has something in common. Books, magazines, and DVDs all have a title, an ID, and a publication year.

In this step, you will define a base class that captures these shared properties. The key principle is encapsulation: keeping the data private and exposing it only through controlled public methods.

In this step, get ready to:
  • Create an abstract base class with private data and public getters.
  • Add a utility method that formats item data into a summary string.
  • Overload the stream output operator for convenient printing.
Define the abstract LibraryItem class

An abstract class in C++ is a class that contains at least one pure virtual function. You cannot create objects from it directly. Instead, it serves as a blueprint that derived classes must complete.

  • Open LibraryItem.h in your IDE.
  • Replace the entire file contents with the following code:
#pragma once
#include <string>
#include <iostream>

class LibraryItem {
private:
    // Core data every library item shares
    std::string title;
    std::string id;
    int year;
    bool available;

public:
    // Constructor initializes all fields, items start as available
    LibraryItem(const std::string& title, const std::string& id, int year)
        : title(title), id(id), year(year), available(true) {}

    // Virtual destructor for safe polymorphic deletion
    virtual ~LibraryItem() = default;

    // Pure virtual functions - derived classes MUST implement these
    virtual void display() const = 0;
    virtual std::string getType() const = 0;

    // Public getters - controlled access to private data
    std::string getTitle() const { return title; }
    std::string getId() const { return id; }
    int getYear() const { return year; }
    bool isAvailable() const { return available; }

    // State-changing methods
    void checkout() { available = false; }
    void checkin() { available = true; }
};

What does this code do?

This defines the skeleton of your class hierarchy. Here is what each section accomplishes:

  • The private section holds data that nothing outside the class can touch directly. This is encapsulation.
  • The constructor uses an initializer list (the : title(title), id(id)... syntax) to set all fields at creation time.
  • The virtual ~LibraryItem() = default line ensures that when you delete a derived object through a base pointer, the correct destructor runs.
  • The = 0 on display() and getType() makes them pure virtual. Every derived class must provide its own implementation.
  • The getters and checkout()/checkin() methods are the only way external code can read or modify the item's state.

Why a virtual destructor?

Per the C++ Core Guidelines (C.35), a base class destructor should be either public and virtual, or protected and non-virtual. Since we plan to store derived objects as base-class pointers, the destructor must be virtual to avoid memory leaks when those objects are destroyed.

  • Save LibraryItem.h.
  • You won't see a runtime change yet because the class is abstract and cannot be instantiated. Compile your project to confirm there are no syntax errors.

Seeing a compile error?

  • Check that #include <string> and #include <iostream> are both present at the top. Missing includes cause "undefined type" errors.
  • Make sure #pragma once is the very first line. If it is not, some compilers will throw duplicate definition warnings.
  • Confirm your IDE is still set to C++17. The initializer list syntax and = default require C++11 or later.

Still stuck? Help me debug a compile error in my LibraryItem.h file

Add the getSummary() method

Right now, if you wanted to print a quick one-line description of any item, you would need to call three separate getters and format them yourself. A getSummary() method bundles that formatting logic inside the class where it belongs.

  • In LibraryItem.h, find the closing brace of the checkin() method.
  • Add the following method directly below it, still inside the public: section:
    // Returns a formatted one-line summary of the item
    std::string getSummary() const {
        return "[" + id + "] " + title + " (" + std::to_string(year) + ")";
    }

What does this code do?

This method accesses the private fields id, title, and year directly because it lives inside the class. Code outside the class would have to call getters instead.

This is encapsulation in practice. The formatting logic is bundled with the data it operates on, and external code never needs to know how the summary is assembled.

  • Save LibraryItem.h and compile your project.
  • You won't see a change yet because the class is still abstract. A clean compile confirms the method signature and return type are correct.

Error mentioning std::to_string?

  • Make sure #include <string> is at the top of the file. The std::to_string function lives in that header.
  • Verify the method is placed inside the class braces (before the final };). Code placed outside the class cannot access private members.

Need more help? Help me fix a to_string error in my getSummary method

Overload operator<< for stream output

C++ lets you redefine how operators work for your own types. By overloading operator<<, you can write std::cout << item instead of calling item.display() every time.

  • In LibraryItem.h, find the closing brace of getSummary().
  • Add the following friend function directly below it, still inside the class definition:
    // Friend function enables std::cout << item syntax
    friend std::ostream& operator<<(std::ostream& os, const LibraryItem& item) {
        item.display();
        return os;
    }

What does this code do?

The friend keyword gives this function access to the class's private members. It takes a reference to an output stream (os) and a LibraryItem reference, then calls the virtual display() method.

This connects operator overloading with polymorphism. When a derived class overrides display(), the operator automatically uses the derived version. One operator definition handles all item types.

  • Save LibraryItem.h and compile your project.
  • Confirm you get a clean compile with no errors. The abstract class is complete.

Compile error around the friend function?

  • Check that #include <iostream> is at the top of the file. The std::ostream type is defined there.
  • Make sure the function is placed before the class closing };. A friend declaration must appear inside the class body.
  • Verify you have not accidentally added a second }; that closes the class early.

Still stuck? Help me debug my operator<< overload in LibraryItem.h

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete LibraryItem.h file. Compare it against yours to spot any differences.

#pragma once
#include <string>
#include <iostream>

class LibraryItem {
private:
    std::string title;
    std::string id;
    int year;
    bool available;

public:
    LibraryItem(const std::string& title, const std::string& id, int year)
        : title(title), id(id), year(year), available(true) {}

    virtual ~LibraryItem() = default;

    virtual void display() const = 0;
    virtual std::string getType() const = 0;

    std::string getTitle() const { return title; }
    std::string getId() const { return id; }
    int getYear() const { return year; }
    bool isAvailable() const { return available; }

    void checkout() { available = false; }
    void checkin() { available = true; }

    std::string getSummary() const {
        return "[" + id + "] " + title + " (" + std::to_string(year) + ")";
    }

    friend std::ostream& operator<<(std::ostream& os, const LibraryItem& item) {
        item.display();
        return os;
    }
};

Your base class is locked in. It defines the contract that every library item must follow. Next up, you will create three derived classes that fulfill that contract with their own unique behavior.

Create Derived Classes with Inheritance

Your base class LibraryItem defines the shared blueprint. Now you need actual, concrete types that a library holds: books, magazines, and DVDs.

Each of these types shares the base class's fields (title, ID, year) but adds its own data and displays itself differently. This is where inheritance shines. You define what's common once in the parent, and each child class extends it with what makes it unique.

In this step, get ready to:
  • Create a Book class that inherits from LibraryItem and adds an author field.
  • Create Magazine and DVD classes with their own unique fields and display behavior.
  • Verify that calling the same method on different types produces different output.
Create the Book class

A Book has everything a LibraryItem has, plus an author. The constructor needs to pass the shared fields up to the base class using an initializer list, then store the author separately.

  • Open Book.h in your IDE.
  • Replace the placeholder content with the following code:
#pragma once
#include "LibraryItem.h"

class Book : public LibraryItem {
private:
    std::string author;

public:
    Book(const std::string& title, const std::string& id, int year, const std::string& author)
        : LibraryItem(title, id, year), author(author) {}

    void display() const override {
        std::cout << "[Book] " << getTitle() << " by " << author
                  << " (" << getYear() << ") - ID: " << getId()
                  << (isAvailable() ? " [Available]" : " [Checked Out]") << "\n";
    }

    std::string getType() const override { return "Book"; }
    std::string getAuthor() const { return author; }
};

What does this code do?

class Book : public LibraryItem declares that Book publicly inherits from LibraryItem. This means Book gets all the public methods of its parent (getters, getSummary(), etc.) without rewriting them.

The initializer list : LibraryItem(title, id, year), author(author) passes the shared fields up to the base class constructor first, then stores the book-specific author field. The base class handles its own initialization. The child only worries about what's new.

The override keyword on display() tells the compiler this function is meant to replace the parent's version. If you accidentally misspell the function name, the compiler catches it immediately instead of silently creating a new function.

  • Save Book.h.
  • Compile your project to check for errors. You should see no compilation errors related to Book.h.

Seeing a compilation error?

  • Check that #include "LibraryItem.h" is at the top of Book.h. Without it, the compiler doesn't know what LibraryItem is.
  • Make sure the constructor's initializer list uses LibraryItem(title, id, year) with exactly three arguments matching the base class constructor.
  • Confirm both files are in the same folder. A relative include like "LibraryItem.h" only works if both headers are in the same directory.

Why isn't my Book class compiling?

Create the Magazine class

Magazines differ from books. Instead of an author, they have an issue number. The pattern is the same as Book: inherit from the base, add a unique field, and override display() to show magazine-specific information.

  • Open Magazine.h in your IDE.
  • Replace the placeholder content with the following code:
#pragma once
#include "LibraryItem.h"

class Magazine : public LibraryItem {
private:
    int issue;

public:
    Magazine(const std::string& title, const std::string& id, int year, int issue)
        : LibraryItem(title, id, year), issue(issue) {}

    void display() const override {
        std::cout << "[Magazine] " << getTitle() << " - Issue #" << issue
                  << " (" << getYear() << ") - ID: " << getId()
                  << (isAvailable() ? " [Available]" : " [Checked Out]") << "\n";
    }

    std::string getType() const override { return "Magazine"; }
    int getIssue() const { return issue; }
};

What's different here?

The private member is now int issue instead of a string. The constructor still delegates the shared fields to the base class, but stores an integer for the issue number.

The display() override prints a completely different format than Book. Each derived class decides how to present itself. This is the real power of overriding: same method name, different behavior.

  • Save Magazine.h.
  • Compile your project. You should see no new errors.

Getting a redefinition error?

  • Make sure #pragma once is at the very top of both Magazine.h and LibraryItem.h. This prevents the same header from being included twice.
  • Check that issue is declared as int, not std::string. The constructor parameter type must match.

Why am I getting a redefinition or type mismatch error in Magazine.h?

Create the DVD class

The last derived class represents DVDs. It stores a duration in minutes and displays that in its output. By now the inheritance pattern should feel familiar.

  • Open DVD.h in your IDE.
  • Replace the placeholder content with the following code:
#pragma once
#include "LibraryItem.h"

class DVD : public LibraryItem {
private:
    int duration;

public:
    DVD(const std::string& title, const std::string& id, int year, int duration)
        : LibraryItem(title, id, year), duration(duration) {}

    void display() const override {
        std::cout << "[DVD] " << getTitle() << " - " << duration << " min"
                  << " (" << getYear() << ") - ID: " << getId()
                  << (isAvailable() ? " [Available]" : " [Checked Out]") << "\n";
    }

    std::string getType() const override { return "DVD"; }
    int getDuration() const { return duration; }
};

What does this code do?

DVD follows the same inheritance pattern as Book and Magazine. The private member duration stores the runtime in minutes.

All three classes share the same base constructor call (LibraryItem(title, id, year)) but each adds its own unique data. This is inheritance in action: shared behavior lives in the parent, specialized behavior lives in the child.

  • Save DVD.h.
  • Compile your project. All three header files should compile without errors.

To verify polymorphism is working, you can temporarily include the derived class headers in main.cpp and instantiate each type. Try adding this test code after your existing cout line:

// Temporary test - include at the top of main.cpp
#include "Book.h"
#include "Magazine.h"
#include "DVD.h"

// Add inside main() after your Hello Library line
Book book("The Great Gatsby", "B001", 1925, "F. Scott Fitzgerald");
Magazine mag("National Geographic", "M001", 2024, 45);
DVD dvd("Inception", "D001", 2010, 148);

book.display();
mag.display();
dvd.display();

What should I see?

Each call to display() produces different output despite the same method name. The book shows its author, the magazine shows its issue number, and the DVD shows its duration. This is what polymorphism looks like in practice.

  • Once you've confirmed the output, remove the test code from main.cpp so it returns to just printing Hello Library!. You'll build the real interactive menu later.

Seeing undefined reference errors when compiling?

  • Make sure all your header files (Book.h, Magazine.h, DVD.h, LibraryItem.h) are in the same folder as main.cpp.
  • If you're compiling from the terminal, make sure you include the -std=c++17 flag. Without it, some modern C++ features may not be recognized.
  • Verify that each derived class has both display() and getType() implemented. Missing either one means the class is still abstract and cannot be instantiated.

Help me debug compilation errors with my derived classes.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three header files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file at this point:

#pragma once
#include "LibraryItem.h"

class Book : public LibraryItem {
private:
    std::string author;

public:
    Book(const std::string& title, const std::string& id, int year, const std::string& author)
        : LibraryItem(title, id, year), author(author) {}

    void display() const override {
        std::cout << "[Book] " << getTitle() << " by " << author
                  << " (" << getYear() << ") - ID: " << getId()
                  << (isAvailable() ? " [Available]" : " [Checked Out]") << "\n";
    }

    std::string getType() const override { return "Book"; }
    std::string getAuthor() const { return author; }
};
#pragma once
#include "LibraryItem.h"

class Magazine : public LibraryItem {
private:
    int issue;

public:
    Magazine(const std::string& title, const std::string& id, int year, int issue)
        : LibraryItem(title, id, year), issue(issue) {}

    void display() const override {
        std::cout << "[Magazine] " << getTitle() << " - Issue #" << issue
                  << " (" << getYear() << ") - ID: " << getId()
                  << (isAvailable() ? " [Available]" : " [Checked Out]") << "\n";
    }

    std::string getType() const override { return "Magazine"; }
    int getIssue() const { return issue; }
};
#pragma once
#include "LibraryItem.h"

class DVD : public LibraryItem {
private:
    int duration;

public:
    DVD(const std::string& title, const std::string& id, int year, int duration)
        : LibraryItem(title, id, year), duration(duration) {}

    void display() const override {
        std::cout << "[DVD] " << getTitle() << " - " << duration << " min"
                  << " (" << getYear() << ") - ID: " << getId()
                  << (isAvailable() ? " [Available]" : " [Checked Out]") << "\n";
    }

    std::string getType() const override { return "DVD"; }
    int getDuration() const { return duration; }
};

Why use the override keyword?

Without override, if you accidentally write void Display() const (capital D), the compiler happily creates a brand new function instead of replacing the parent's. Your program compiles but behaves incorrectly at runtime.

The override keyword turns this silent bug into a compile-time error. The C++ Core Guidelines recommend specifying exactly one of virtual, override, or final on every virtual function.

You now have three concrete classes that inherit from a single base. Each one stores its own unique data and displays itself differently. Next, you'll put these classes to work inside a Library manager that stores them all in one collection and calls display() through base-class pointers. That's where polymorphism really comes alive.

Implement Polymorphism with the Library Manager

Your three derived classes can each display themselves differently. But right now, they exist as isolated objects with no way to manage them as a unified collection.

In this step, you will build a Library class that stores Books, Magazines, and DVDs together in a single vector of base-class pointers. When you call display() through those pointers, each item will automatically use its own override. This is runtime polymorphism in action.

In this step, get ready to:
  • Create a Library class that stores items polymorphically using smart pointers.
  • Add methods to display and search the collection through base-class pointers.
  • Implement item removal and verify polymorphic behavior.
Create the Library class with smart pointer storage

The key to polymorphism is storing derived objects through base-class pointers. A std::vector<std::unique_ptr<LibraryItem>> lets you hold a mixed collection of Books, Magazines, and DVDs in one container.

  • Open Library.h in your IDE and replace the placeholder content with this code:
#pragma once
#include "LibraryItem.h"
#include "Book.h"
#include "Magazine.h"
#include "DVD.h"
#include <vector>
#include <memory>
#include <fstream>
#include <sstream>
#include <algorithm>

class Library {
private:
    // Stores all library items as base-class pointers for polymorphism
    std::vector<std::unique_ptr<LibraryItem>> items;

public:
    // Creates a Book and adds it to the collection
    void addBook(const std::string& title, const std::string& id, int year, const std::string& author) {
        items.push_back(std::make_unique<Book>(title, id, year, author));
    }

    // Creates a Magazine and adds it to the collection
    void addMagazine(const std::string& title, const std::string& id, int year, int issue) {
        items.push_back(std::make_unique<Magazine>(title, id, year, issue));
    }

    // Creates a DVD and adds it to the collection
    void addDVD(const std::string& title, const std::string& id, int year, int duration) {
        items.push_back(std::make_unique<DVD>(title, id, year, duration));
    }
};

What does this code do?

The items vector holds std::unique_ptr<LibraryItem> smart pointers. Each pointer owns a derived object (Book, Magazine, or DVD) and automatically deletes it when removed from the vector.

Each add method uses std::make_unique to construct the derived object on the heap and push the owning pointer into the vector. The vector's element type is the base class, but the actual objects are derived types.

Why smart pointers instead of raw pointers?

Raw new/delete forces you to manually track ownership and risks memory leaks. std::unique_ptr follows the RAII pattern: the pointer owns the object and automatically frees memory when it goes out of scope.

This is the modern C++ approach recommended by the C++ Core Guidelines.

  • Save Library.h.

You won't see a change yet. The next chunk adds the methods that iterate this vector polymorphically.

Add display and search methods

Now you will add the methods that demonstrate polymorphism. When you call display() through a base-class pointer, C++ resolves the call at runtime to the correct derived override.

  • In Library.h, find the closing }; of the class. Add the following two methods just above it (after the addDVD method):
    // Prints every item in the collection using polymorphic display()
    void displayAll() const {
        if (items.empty()) {
            std::cout << "The library is empty.\n";
            return;
        }
        std::cout << "\n=== Library Collection ===\n";
        for (const auto& item : items) {
            item->display();
        }
        std::cout << "==========================\n";
    }

    // Finds items whose title contains the search query
    void searchByTitle(const std::string& query) const {
        bool found = false;
        for (const auto& item : items) {
            if (item->getTitle().find(query) != std::string::npos) {
                item->display();
                found = true;
            }
        }
        if (!found) {
            std::cout << "No items found matching \"" << query << "\".\n";
        }
    }

What does this code do?

  • displayAll() iterates the vector and calls item->display() on each pointer. Because display() is virtual, C++ calls the correct override (Book's, Magazine's, or DVD's) based on the actual object type at runtime.
  • searchByTitle() uses std::string::find for substring matching. If the query appears anywhere in the title, the item is displayed.
  • Save Library.h.

You won't see a change yet. The next chunk adds the removal method, then you will test everything together.

Add item removal and test polymorphism

The last method lets you remove items by ID. It uses std::find_if with a lambda to locate the matching item, then erases it from the vector.

  • In Library.h, add this method below searchByTitle (still above the closing };):
    // Finds an item by ID and removes it from the collection
    void removeItem(const std::string& id) {
        auto it = std::find_if(items.begin(), items.end(),
            [&id](const std::unique_ptr<LibraryItem>& item) {
                return item->getId() == id;
            });
        if (it != items.end()) {
            std::cout << "Removed: " << (*it)->getTitle() << "\n";
            items.erase(it);
        } else {
            std::cout << "Item with ID \"" << id << "\" not found.\n";
        }
    }

What does this code do?

  • std::find_if takes a lambda that checks each item's ID against the target. It returns an iterator to the first match.
  • The lambda captures id by reference with [&id] so it can compare against the parameter.
  • If found, the item is erased from the vector. The unique_ptr automatically frees the object's memory when it is removed.
  • Save Library.h.

✔️ Awesome, I've got everything!

Great. Make sure you have saved Library.h before continuing.

ⓧ I'd like to double check the full code

#pragma once
#include "LibraryItem.h"
#include "Book.h"
#include "Magazine.h"
#include "DVD.h"
#include <vector>
#include <memory>
#include <fstream>
#include <sstream>
#include <algorithm>

class Library {
private:
    std::vector<std::unique_ptr<LibraryItem>> items;

public:
    void addBook(const std::string& title, const std::string& id, int year, const std::string& author) {
        items.push_back(std::make_unique<Book>(title, id, year, author));
    }

    void addMagazine(const std::string& title, const std::string& id, int year, int issue) {
        items.push_back(std::make_unique<Magazine>(title, id, year, issue));
    }

    void addDVD(const std::string& title, const std::string& id, int year, int duration) {
        items.push_back(std::make_unique<DVD>(title, id, year, duration));
    }

    void displayAll() const {
        if (items.empty()) {
            std::cout << "The library is empty.\n";
            return;
        }
        std::cout << "\n=== Library Collection ===\n";
        for (const auto& item : items) {
            item->display();
        }
        std::cout << "==========================\n";
    }

    void searchByTitle(const std::string& query) const {
        bool found = false;
        for (const auto& item : items) {
            if (item->getTitle().find(query) != std::string::npos) {
                item->display();
                found = true;
            }
        }
        if (!found) {
            std::cout << "No items found matching \"" << query << "\".\n";
        }
    }

    void removeItem(const std::string& id) {
        auto it = std::find_if(items.begin(), items.end(),
            [&id](const std::unique_ptr<LibraryItem>& item) {
                return item->getId() == id;
            });
        if (it != items.end()) {
            std::cout << "Removed: " << (*it)->getTitle() << "\n";
            items.erase(it);
        } else {
            std::cout << "Item with ID \"" << id << "\" not found.\n";
        }
    }
};

Now update main.cpp to use the new Library class and see polymorphism in action.

  • Replace the entire contents of main.cpp with this code:
#include "Library.h"
#include <iostream>

int main() {
    Library library;

    // Add one of each item type to demonstrate polymorphism
    library.addBook("The Pragmatic Programmer", "B001", 1999, "Andrew Hunt");
    library.addMagazine("National Geographic", "M001", 2023, 42);
    library.addDVD("Inception", "D001", 2010, 148);

    // Each item displays differently despite being stored as base-class pointers
    library.displayAll();

    return 0;
}

What does this code do?

This creates a single Library object, adds one Book, one Magazine, and one DVD, then calls displayAll(). All three items are stored in the same vector as LibraryItem pointers.

When displayAll() calls item->display(), C++ resolves the virtual call to each object's actual type. You will see three different output formats from one loop.

  • Save main.cpp.
  • Compile and run the program with the -std=c++17 flag (or use your IDE's build button if it is already set to C++17).
g++ -std=c++17 main.cpp -o library && ./library

You should see output like this:

=== Library Collection ===
[Book] The Pragmatic Programmer by Andrew Hunt (1999) - ID: B001 [Available]
[Magazine] National Geographic - Issue #42 (2023) - ID: M001 [Available]
[DVD] Inception - 148 min (2010) - ID: D001 [Available]
==========================

Each item formats its own output differently. That is runtime polymorphism: one loop, one method call, three different behaviors.

Seeing a compilation error?

  • If you see errors about missing types or undeclared identifiers, check that Library.h includes all four headers: LibraryItem.h, Book.h, Magazine.h, and DVD.h.
  • If you see errors about make_unique not being found, confirm you have #include <memory> at the top of Library.h and are compiling with -std=c++17.
  • If you see errors about find_if, make sure you have #include <algorithm> in Library.h.

My Library class won't compile. Can you help me debug it?

✔️ Awesome, I've got everything!

Great. Make sure you have saved both Library.h and main.cpp.

ⓧ I'd like to double check the full code

#include "Library.h"
#include <iostream>

int main() {
    Library library;
    library.addBook("The Pragmatic Programmer", "B001", 1999, "Andrew Hunt");
    library.addMagazine("National Geographic", "M001", 2023, 42);
    library.addDVD("Inception", "D001", 2010, 148);
    library.displayAll();
    return 0;
}

Your Library manager stores a mixed collection of items and dispatches the correct behavior for each one at runtime. Next up, you will add file persistence so your library data survives between program runs.

Add File Persistence and the Interactive Menu

Your library system can add, display, search, and remove items. But the moment your program exits, everything disappears.

In this step, you will add file persistence so your library data survives between sessions. You will also build a proper interactive menu that lets users drive the application from the console.

In this step, get ready to:
  • Write a saveToFile() method that serializes items to a pipe-delimited text file.
  • Write a loadFromFile() method that reconstructs the correct derived objects from the file.
  • Build an interactive menu loop in main.cpp that ties everything together.
Add the saveToFile method

Right now, if you close the program, all added items are gone. To persist data, you need to write each item to a file in a format you can parse back later.

  • In Library.h, find the closing brace of the removeItem() method.
  • Add the following saveToFile() method below it:
    void saveToFile(const std::string& filename) const {
        // Open the file for writing
        std::ofstream file(filename);
        if (!file.is_open()) {
            std::cout << "Error: Could not open file for saving.\n";
            return;
        }
        // Write each item as: type|title|id|year|extra
        for (const auto& item : items) {
            file << item->getType() << "|" << item->getTitle() << "|"
                 << item->getId() << "|" << item->getYear() << "|";
            // Use dynamic_cast to access derived-class-specific data
            if (item->getType() == "Book") {
                file << dynamic_cast<Book*>(item.get())->getAuthor();
            } else if (item->getType() == "Magazine") {
                file << dynamic_cast<Magazine*>(item.get())->getIssue();
            } else if (item->getType() == "DVD") {
                file << dynamic_cast<DVD*>(item.get())->getDuration();
            }
            file << "\n";
        }
        file.close();
        std::cout << "Library saved to " << filename << "\n";
    }

What does this code do?

The method opens a file using std::ofstream and writes one line per item. Each line follows the format type|title|id|year|extra where the pipe character | separates fields.

The dynamic_cast lets you access derived-class-specific getters (like getAuthor()) through a base-class pointer. This is RTTI (Run-Time Type Information) in action.

Why a text file instead of binary?

Text-based serialization is human-readable and easy to debug. You can open library_data.txt in any editor and see exactly what was saved. Binary formats are faster but opaque.

  • Save Library.h.

Seeing a compile error about dynamic_cast?

Make sure you included the header files for all derived classes (Book.h, Magazine.h, DVD.h) at the top of Library.h. These should already be there from the previous step.

Also confirm your class has a virtual destructor. Without one, dynamic_cast will fail.

Still stuck? Help me fix a dynamic_cast compile error in my saveToFile method

Add the loadFromFile method

Saving is only half the story. You also need to read the file back and reconstruct the correct derived objects from each line.

  • In Library.h, add the following loadFromFile() method below saveToFile():
    void loadFromFile(const std::string& filename) {
        // Open the file for reading
        std::ifstream file(filename);
        if (!file.is_open()) {
            return; // No file yet, that's okay
        }
        items.clear();
        std::string line;
        // Read each line and reconstruct the correct derived object
        while (std::getline(file, line)) {
            std::istringstream ss(line);
            std::string type, title, id, yearStr, extra;
            std::getline(ss, type, '|');
            std::getline(ss, title, '|');
            std::getline(ss, id, '|');
            std::getline(ss, yearStr, '|');
            std::getline(ss, extra, '|');
            int year = std::stoi(yearStr);
            // Reconstruct the correct derived type
            if (type == "Book") {
                addBook(title, id, year, extra);
            } else if (type == "Magazine") {
                addMagazine(title, id, year, std::stoi(extra));
            } else if (type == "DVD") {
                addDVD(title, id, year, std::stoi(extra));
            }
        }
        file.close();
    }

What does this code do?

The method opens the file with std::ifstream and reads it line by line. If the file doesn't exist yet (first run), it silently returns.

Each line is fed into a std::istringstream so you can use std::getline with the '|' delimiter to split the fields. The type string determines which add method to call, reusing the same methods you already wrote.

  • Save Library.h.
  • Verify the file still compiles with no errors.

Undeclared identifier error on istringstream?

Check that #include <sstream> is at the top of Library.h. This header provides std::istringstream. It should already be there from the previous step.

Need help? Help me fix an undeclared identifier error for istringstream in my loadFromFile method

✔️ Awesome, I've got everything!

Great. Double check you have saved Library.h before moving on.

ⓧ I'd like to double check the full code

#pragma once
#include "LibraryItem.h"
#include "Book.h"
#include "Magazine.h"
#include "DVD.h"
#include <vector>
#include <memory>
#include <fstream>
#include <sstream>
#include <algorithm>

class Library {
private:
    std::vector<std::unique_ptr<LibraryItem>> items;

public:
    void addBook(const std::string& title, const std::string& id, int year, const std::string& author) {
        items.push_back(std::make_unique<Book>(title, id, year, author));
    }

    void addMagazine(const std::string& title, const std::string& id, int year, int issue) {
        items.push_back(std::make_unique<Magazine>(title, id, year, issue));
    }

    void addDVD(const std::string& title, const std::string& id, int year, int duration) {
        items.push_back(std::make_unique<DVD>(title, id, year, duration));
    }

    void displayAll() const {
        if (items.empty()) {
            std::cout << "The library is empty.\n";
            return;
        }
        std::cout << "\n=== Library Collection ===\n";
        for (const auto& item : items) {
            item->display();
        }
        std::cout << "==========================\n";
    }

    void searchByTitle(const std::string& query) const {
        bool found = false;
        for (const auto& item : items) {
            if (item->getTitle().find(query) != std::string::npos) {
                item->display();
                found = true;
            }
        }
        if (!found) {
            std::cout << "No items found matching \"" << query << "\".\n";
        }
    }

    void removeItem(const std::string& id) {
        auto it = std::find_if(items.begin(), items.end(),
            [&id](const std::unique_ptr<LibraryItem>& item) {
                return item->getId() == id;
            });
        if (it != items.end()) {
            std::cout << "Removed: " << (*it)->getTitle() << "\n";
            items.erase(it);
        } else {
            std::cout << "Item with ID \"" << id << "\" not found.\n";
        }
    }

    void saveToFile(const std::string& filename) const {
        std::ofstream file(filename);
        if (!file.is_open()) {
            std::cout << "Error: Could not open file for saving.\n";
            return;
        }
        for (const auto& item : items) {
            file << item->getType() << "|" << item->getTitle() << "|"
                 << item->getId() << "|" << item->getYear() << "|";
            if (item->getType() == "Book") {
                file << dynamic_cast<Book*>(item.get())->getAuthor();
            } else if (item->getType() == "Magazine") {
                file << dynamic_cast<Magazine*>(item.get())->getIssue();
            } else if (item->getType() == "DVD") {
                file << dynamic_cast<DVD*>(item.get())->getDuration();
            }
            file << "\n";
        }
        file.close();
        std::cout << "Library saved to " << filename << "\n";
    }

    void loadFromFile(const std::string& filename) {
        std::ifstream file(filename);
        if (!file.is_open()) {
            return;
        }
        items.clear();
        std::string line;
        while (std::getline(file, line)) {
            std::istringstream ss(line);
            std::string type, title, id, yearStr, extra;
            std::getline(ss, type, '|');
            std::getline(ss, title, '|');
            std::getline(ss, id, '|');
            std::getline(ss, yearStr, '|');
            std::getline(ss, extra, '|');
            int year = std::stoi(yearStr);
            if (type == "Book") {
                addBook(title, id, year, extra);
            } else if (type == "Magazine") {
                addMagazine(title, id, year, std::stoi(extra));
            } else if (type == "DVD") {
                addDVD(title, id, year, std::stoi(extra));
            }
        }
        file.close();
    }
};
Build the interactive menu in main.cpp

Your library now has all the backend logic it needs. The last piece is a user-facing menu that ties everything together into a usable application.

  • Open main.cpp and replace its entire contents with the following code:
#include "Library.h"
#include <iostream>
#include <string>
#include <limits>

int main() {
    Library library;
    const std::string filename = "library_data.txt";

    // Load any previously saved data
    library.loadFromFile(filename);
    std::cout << "Welcome to the Library Management System!\n";

    int choice = 0;
    while (choice != 7) {
        // Display the menu options
        std::cout << "\n--- Menu ---\n";
        std::cout << "1. Add Book\n";
        std::cout << "2. Add Magazine\n";
        std::cout << "3. Add DVD\n";
        std::cout << "4. Display All Items\n";
        std::cout << "5. Search by Title\n";
        std::cout << "6. Remove Item\n";
        std::cout << "7. Save and Exit\n";
        std::cout << "Choice: ";
        std::cin >> choice;
        // Clear the input buffer after reading an integer
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        switch (choice) {
            case 1: {
                std::string title, id, author;
                int year;
                std::cout << "Title: ";
                std::getline(std::cin, title);
                std::cout << "ID: ";
                std::getline(std::cin, id);
                std::cout << "Year: ";
                std::cin >> year;
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                std::cout << "Author: ";
                std::getline(std::cin, author);
                library.addBook(title, id, year, author);
                std::cout << "Book added!\n";
                break;
            }
            case 2: {
                std::string title, id;
                int year, issue;
                std::cout << "Title: ";
                std::getline(std::cin, title);
                std::cout << "ID: ";
                std::getline(std::cin, id);
                std::cout << "Year: ";
                std::cin >> year;
                std::cout << "Issue #: ";
                std::cin >> issue;
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                library.addMagazine(title, id, year, issue);
                std::cout << "Magazine added!\n";
                break;
            }
            case 3: {
                std::string title, id;
                int year, duration;
                std::cout << "Title: ";
                std::getline(std::cin, title);
                std::cout << "ID: ";
                std::getline(std::cin, id);
                std::cout << "Year: ";
                std::cin >> year;
                std::cout << "Duration (min): ";
                std::cin >> duration;
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                library.addDVD(title, id, year, duration);
                std::cout << "DVD added!\n";
                break;
            }
            case 4:
                library.displayAll();
                break;
            case 5: {
                std::string query;
                std::cout << "Search title: ";
                std::getline(std::cin, query);
                library.searchByTitle(query);
                break;
            }
            case 6: {
                std::string id;
                std::cout << "Enter item ID to remove: ";
                std::getline(std::cin, id);
                library.removeItem(id);
                break;
            }
            case 7:
                library.saveToFile(filename);
                std::cout << "Goodbye!\n";
                break;
            default:
                std::cout << "Invalid choice. Try again.\n";
        }
    }

    return 0;
}

What does this code do?

At startup, loadFromFile() restores any previously saved items. If no file exists yet, it silently skips.

The while (choice != 7) loop keeps showing the menu until the user picks option 7. Each case in the switch statement handles one menu action.

The std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n') line clears leftover characters from the input buffer after reading an integer. Without it, the next std::getline would read an empty string.

  • Save main.cpp.
  • Compile and run the program.
  • You should see the welcome message and a numbered menu. Try adding a book by entering 1 and filling in the prompts.
  • Enter 4 to display all items and confirm your new book appears.
  • Enter 7 to save and exit. You should see a confirmation message.
  • Run the program again. Your previously added items should still be there.

Items disappearing between runs?

Make sure you exit with option 7 (Save and Exit), not by closing the terminal window. Only option 7 calls saveToFile().

Also check that library_data.txt is being created in the same directory where you run the program. Look for it in your project folder after exiting.

Still having trouble? Help me debug why my library items aren't persisting between program runs

✔️ Awesome, I've got everything!

Great. Double check you have saved both Library.h and main.cpp.

ⓧ I'd like to double check the full code

Here is the complete main.cpp:

#include "Library.h"
#include <iostream>
#include <string>
#include <limits>

int main() {
    Library library;
    const std::string filename = "library_data.txt";

    library.loadFromFile(filename);
    std::cout << "Welcome to the Library Management System!\n";

    int choice = 0;
    while (choice != 7) {
        std::cout << "\n--- Menu ---\n";
        std::cout << "1. Add Book\n";
        std::cout << "2. Add Magazine\n";
        std::cout << "3. Add DVD\n";
        std::cout << "4. Display All Items\n";
        std::cout << "5. Search by Title\n";
        std::cout << "6. Remove Item\n";
        std::cout << "7. Save and Exit\n";
        std::cout << "Choice: ";
        std::cin >> choice;
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

        switch (choice) {
            case 1: {
                std::string title, id, author;
                int year;
                std::cout << "Title: ";
                std::getline(std::cin, title);
                std::cout << "ID: ";
                std::getline(std::cin, id);
                std::cout << "Year: ";
                std::cin >> year;
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                std::cout << "Author: ";
                std::getline(std::cin, author);
                library.addBook(title, id, year, author);
                std::cout << "Book added!\n";
                break;
            }
            case 2: {
                std::string title, id;
                int year, issue;
                std::cout << "Title: ";
                std::getline(std::cin, title);
                std::cout << "ID: ";
                std::getline(std::cin, id);
                std::cout << "Year: ";
                std::cin >> year;
                std::cout << "Issue #: ";
                std::cin >> issue;
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                library.addMagazine(title, id, year, issue);
                std::cout << "Magazine added!\n";
                break;
            }
            case 3: {
                std::string title, id;
                int year, duration;
                std::cout << "Title: ";
                std::getline(std::cin, title);
                std::cout << "ID: ";
                std::getline(std::cin, id);
                std::cout << "Year: ";
                std::cin >> year;
                std::cout << "Duration (min): ";
                std::cin >> duration;
                std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                library.addDVD(title, id, year, duration);
                std::cout << "DVD added!\n";
                break;
            }
            case 4:
                library.displayAll();
                break;
            case 5: {
                std::string query;
                std::cout << "Search title: ";
                std::getline(std::cin, query);
                library.searchByTitle(query);
                break;
            }
            case 6: {
                std::string id;
                std::cout << "Enter item ID to remove: ";
                std::getline(std::cin, id);
                library.removeItem(id);
                break;
            }
            case 7:
                library.saveToFile(filename);
                std::cout << "Goodbye!\n";
                break;
            default:
                std::cout << "Invalid choice. Try again.\n";
        }
    }

    return 0;
}

Secret mission

Add a Borrowing System

Your library can store items, but real libraries also lend them out. Can you add a Member class that tracks who borrowed what, with checkout and return functionality?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files to continue experimenting, or delete them entirely.

Resources you used:

  • Project source files: main.cpp, LibraryItem.h, Book.h, Magazine.h, DVD.h, Library.h, Member.h.
  • Generated data files: library_data.txt and members.txt (created when you run and save).
  • Compiled binary (the executable produced by your compiler).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your Library Management System or extending the Secret Mission.

  • Your project files stay in your IDE and you can recompile any time.
  • The library_data.txt and members.txt files persist your library data between sessions.

Pause - I'll come back to this later

Since this project is local with no running processes, there is nothing to pause. Your files stay on disk until you delete them.

  • Close the project in your IDE to free up memory.
  • Your source files and data files remain where they are. Reopen the project folder in your IDE when you want to continue.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your source code, compiled binary, and saved library data.

  • Close the project in your IDE.
  • Delete the project folder using your terminal. On macOS/Linux, run the command below. On Windows, use the second command.

macOS/Linux:

rm -rf ~/Desktop/library-management

Windows:

rmdir /s /q %USERPROFILE%\Desktop\library-management

Check your folder name

If you named your project folder something different when you created it in Step 1, replace library-management with whatever you named your folder.

Nice Work!

Nice Work!

Nice work! You just built a fully interactive Library Management System using core C++ OOP principles.

You've learned how to:

  • Design a class hierarchy with an abstract base class (LibraryItem) and three derived classes (Book, Magazine, DVD), using encapsulation, inheritance, and runtime polymorphism through pure virtual functions and the override keyword.
  • Store heterogeneous objects in a single std::vector using std::unique_ptr for memory-safe polymorphic dispatch, and overload operator<< for custom stream output.
  • Implement file persistence with std::ofstream and std::ifstream to save and reload library data across sessions, wrapped in a menu-driven console application.
  • Secret Mission: Added composition with a Member class and a borrowing system that links members to items.

Ready to quiz yourself?