Quick Answer

Classes define custom data types with encapsulated data and methods. Use public, private, and protected access specifiers for proper data hiding and interface design.

Understanding the Issue

Classes in C++ are user-defined types that encapsulate data members and member functions, forming the cornerstone of object-oriented programming. They provide data abstraction, encapsulation, inheritance, and polymorphism capabilities. Proper class design involves choosing appropriate access specifiers (public, private, protected), implementing constructors and destructors for resource management, and following the Rule of Three/Five for classes that manage resources. Understanding class design principles is essential for writing maintainable, efficient C++ code.

The Problem

This code demonstrates the issue:

Cpp Error
// Problem 1: Poor encapsulation and no resource management
struct BadClass {
    int* data;  // Public raw pointer - unsafe
    int size;   // Public data - no validation
};

// Problem 2: Missing essential member functions
class IncompleteClass {
public:
    int* ptr;
    IncompleteClass() { ptr = new int(42); }
    // Missing destructor, copy constructor, assignment operator
};

The Solution

Here's the corrected code:

Cpp Fixed
// Solution 1: Well-designed class with proper encapsulation
#include <iostream>
#include <memory>
#include <vector>
#include <string>

class BankAccount {
private:
    std::string accountNumber;
    std::string ownerName;
    double balance;
    static int nextAccountId;  // Static member for unique IDs

public:
    // Default constructor
    BankAccount() : accountNumber(generateAccountNumber()), ownerName("Unknown"), balance(0.0) {
        std::cout << "Account created: " << accountNumber << std::endl;
    }
    
    // Parameterized constructor
    BankAccount(const std::string& owner, double initialBalance) 
        : accountNumber(generateAccountNumber()), ownerName(owner), balance(initialBalance) {
        if (initialBalance < 0) {
            throw std::invalid_argument("Initial balance cannot be negative");
        }
        std::cout << "Account created for " << owner << " with balance $" << balance << std::endl;
    }
    
    // Copy constructor
    BankAccount(const BankAccount& other) 
        : accountNumber(generateAccountNumber()), ownerName(other.ownerName), balance(other.balance) {
        std::cout << "Account copied from " << other.accountNumber << std::endl;
    }
    
    // Move constructor (C++11)
    BankAccount(BankAccount&& other) noexcept 
        : accountNumber(std::move(other.accountNumber)), 
          ownerName(std::move(other.ownerName)), 
          balance(other.balance) {
        other.balance = 0;  // Reset moved-from object
        std::cout << "Account moved" << std::endl;
    }
    
    // Copy assignment operator
    BankAccount& operator=(const BankAccount& other) {
        if (this != &other) {  // Self-assignment check
            ownerName = other.ownerName;
            balance = other.balance;
            // accountNumber remains unchanged
        }
        return *this;
    }
    
    // Move assignment operator (C++11)
    BankAccount& operator=(BankAccount&& other) noexcept {
        if (this != &other) {
            ownerName = std::move(other.ownerName);
            balance = other.balance;
            other.balance = 0;
        }
        return *this;
    }
    
    // Destructor
    ~BankAccount() {
        std::cout << "Account " << accountNumber << " destroyed" << std::endl;
    }
    
    // Public interface methods
    void deposit(double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("Deposit amount must be positive");
        }
        balance += amount;
        std::cout << "Deposited $" << amount << ". New balance: $" << balance << std::endl;
    }
    
    bool withdraw(double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("Withdrawal amount must be positive");
        }
        if (amount > balance) {
            std::cout << "Insufficient funds. Current balance: $" << balance << std::endl;
            return false;
        }
        balance -= amount;
        std::cout << "Withdrew $" << amount << ". New balance: $" << balance << std::endl;
        return true;
    }
    
    // Const member functions for read-only operations
    double getBalance() const { return balance; }
    const std::string& getOwnerName() const { return ownerName; }
    const std::string& getAccountNumber() const { return accountNumber; }
    
    // Static member function
    static int getTotalAccountsCreated() { return nextAccountId - 1; }
    
    // Friend function for output
    friend std::ostream& operator<<(std::ostream& os, const BankAccount& account);

private:
    // Private helper method
    static std::string generateAccountNumber() {
        return "ACC" + std::to_string(++nextAccountId);
    }
};

// Initialize static member
int BankAccount::nextAccountId = 1000;

// Friend function implementation
std::ostream& operator<<(std::ostream& os, const BankAccount& account) {
    os << "Account[" << account.accountNumber << "]: " 
       << account.ownerName << " - $" << account.balance;
    return os;
}

// Solution 2: Advanced class design patterns
// Abstract base class with virtual functions
class Shape {
protected:
    std::string name;
    
public:
    Shape(const std::string& shapeName) : name(shapeName) {}
    
    // Pure virtual function makes this an abstract class
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    
    // Virtual destructor for proper polymorphic destruction
    virtual ~Shape() = default;
    
    // Non-virtual interface (NVI) pattern
    void printInfo() const {
        std::cout << name << " - Area: " << area() 
                  << ", Perimeter: " << perimeter() << std::endl;
    }
    
    const std::string& getName() const { return name; }
};

// Concrete derived class
class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : Shape("Rectangle"), width(w), height(h) {
        if (w <= 0 || h <= 0) {
            throw std::invalid_argument("Width and height must be positive");
        }
    }
    
    double area() const override {
        return width * height;
    }
    
    double perimeter() const override {
        return 2 * (width + height);
    }
    
    // Additional methods specific to Rectangle
    double getWidth() const { return width; }
    double getHeight() const { return height; }
    
    void setDimensions(double w, double h) {
        if (w <= 0 || h <= 0) {
            throw std::invalid_argument("Width and height must be positive");
        }
        width = w;
        height = h;
    }
};

class Circle : public Shape {
private:
    double radius;
    static constexpr double PI = 3.14159265359;
    
public:
    Circle(double r) : Shape("Circle"), radius(r) {
        if (r <= 0) {
            throw std::invalid_argument("Radius must be positive");
        }
    }
    
    double area() const override {
        return PI * radius * radius;
    }
    
    double perimeter() const override {
        return 2 * PI * radius;
    }
    
    double getRadius() const { return radius; }
    void setRadius(double r) {
        if (r <= 0) {
            throw std::invalid_argument("Radius must be positive");
        }
        radius = r;
    }
};

// Template class for generic containers
template<typename T>
class DynamicArray {
private:
    std::unique_ptr<T[]> data;
    size_t size;
    size_t capacity;
    
    void resize() {
        size_t newCapacity = capacity == 0 ? 1 : capacity * 2;
        auto newData = std::make_unique<T[]>(newCapacity);
        
        for (size_t i = 0; i < size; ++i) {
            newData[i] = std::move(data[i]);
        }
        
        data = std::move(newData);
        capacity = newCapacity;
    }
    
public:
    // Constructor
    DynamicArray() : data(nullptr), size(0), capacity(0) {}
    
    explicit DynamicArray(size_t initialCapacity) 
        : data(std::make_unique<T[]>(initialCapacity)), size(0), capacity(initialCapacity) {}
    
    // Copy constructor
    DynamicArray(const DynamicArray& other) 
        : data(std::make_unique<T[]>(other.capacity)), size(other.size), capacity(other.capacity) {
        for (size_t i = 0; i < size; ++i) {
            data[i] = other.data[i];
        }
    }
    
    // Move constructor
    DynamicArray(DynamicArray&& other) noexcept 
        : data(std::move(other.data)), size(other.size), capacity(other.capacity) {
        other.size = 0;
        other.capacity = 0;
    }
    
    // Copy assignment
    DynamicArray& operator=(const DynamicArray& other) {
        if (this != &other) {
            auto newData = std::make_unique<T[]>(other.capacity);
            for (size_t i = 0; i < other.size; ++i) {
                newData[i] = other.data[i];
            }
            data = std::move(newData);
            size = other.size;
            capacity = other.capacity;
        }
        return *this;
    }
    
    // Move assignment
    DynamicArray& operator=(DynamicArray&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
            size = other.size;
            capacity = other.capacity;
            other.size = 0;
            other.capacity = 0;
        }
        return *this;
    }
    
    // Element access
    T& operator[](size_t index) {
        if (index >= size) throw std::out_of_range("Index out of range");
        return data[index];
    }
    
    const T& operator[](size_t index) const {
        if (index >= size) throw std::out_of_range("Index out of range");
        return data[index];
    }
    
    // Add element
    void push_back(const T& value) {
        if (size >= capacity) {
            resize();
        }
        data[size++] = value;
    }
    
    void push_back(T&& value) {
        if (size >= capacity) {
            resize();
        }
        data[size++] = std::move(value);
    }
    
    // Remove last element
    void pop_back() {
        if (size > 0) {
            --size;
        }
    }
    
    // Utility methods
    size_t getSize() const { return size; }
    size_t getCapacity() const { return capacity; }
    bool empty() const { return size == 0; }
    
    // Iterator support (basic)
    T* begin() { return data.get(); }
    T* end() { return data.get() + size; }
    const T* begin() const { return data.get(); }
    const T* end() const { return data.get() + size; }
};

// Singleton pattern implementation
class Logger {
private:
    static std::unique_ptr<Logger> instance;
    static std::mutex instanceMutex;
    std::string logFile;
    
    // Private constructor
    Logger(const std::string& filename) : logFile(filename) {}
    
public:
    // Delete copy constructor and assignment operator
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
    
    static Logger& getInstance() {
        std::lock_guard<std::mutex> lock(instanceMutex);
        if (!instance) {
            instance = std::unique_ptr<Logger>(new Logger("app.log"));
        }
        return *instance;
    }
    
    void log(const std::string& message) {
        std::cout << "[LOG] " << message << std::endl;
        // In real implementation, write to file
    }
};

// Initialize static members
std::unique_ptr<Logger> Logger::instance = nullptr;
std::mutex Logger::instanceMutex;

// RAII (Resource Acquisition Is Initialization) example
class FileHandler {
private:
    std::FILE* file;
    std::string filename;
    
public:
    explicit FileHandler(const std::string& fname, const char* mode = "r") 
        : filename(fname) {
        file = std::fopen(fname.c_str(), mode);
        if (!file) {
            throw std::runtime_error("Failed to open file: " + fname);
        }
        std::cout << "File opened: " << filename << std::endl;
    }
    
    ~FileHandler() {
        if (file) {
            std::fclose(file);
            std::cout << "File closed: " << filename << std::endl;
        }
    }
    
    // Delete copy operations (file handles shouldn't be copied)
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
    
    // Allow move operations
    FileHandler(FileHandler&& other) noexcept 
        : file(other.file), filename(std::move(other.filename)) {
        other.file = nullptr;
    }
    
    FileHandler& operator=(FileHandler&& other) noexcept {
        if (this != &other) {
            if (file) {
                std::fclose(file);
            }
            file = other.file;
            filename = std::move(other.filename);
            other.file = nullptr;
        }
        return *this;
    }
    
    std::FILE* get() const { return file; }
    bool isOpen() const { return file != nullptr; }
};

// Usage examples
void demonstrateClasses() {
    try {
        // Basic class usage
        BankAccount account1("John Doe", 1000.0);
        account1.deposit(500.0);
        account1.withdraw(200.0);
        std::cout << account1 << std::endl;
        
        // Polymorphism
        std::vector<std::unique_ptr<Shape>> shapes;
        shapes.push_back(std::make_unique<Rectangle>(5.0, 3.0));
        shapes.push_back(std::make_unique<Circle>(2.5));
        
        for (const auto& shape : shapes) {
            shape->printInfo();
        }
        
        // Template class usage
        DynamicArray<int> numbers;
        for (int i = 0; i < 10; ++i) {
            numbers.push_back(i * i);
        }
        
        for (size_t i = 0; i < numbers.getSize(); ++i) {
            std::cout << numbers[i] << " ";
        }
        std::cout << std::endl;
        
        // Singleton usage
        Logger& logger = Logger::getInstance();
        logger.log("Application started");
        
        // RAII usage
        {
            FileHandler handler("test.txt", "w");
            if (handler.isOpen()) {
                std::fprintf(handler.get(), "Hello, World!\n");
            }
        } // File automatically closed here
        
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

Key Takeaways

Design classes with proper encapsulation using private data members and public interfaces. Implement the Rule of Three/Five for resource management. Use constructors for initialization and destructors for cleanup. Apply RAII principles for automatic resource management. Leverage inheritance and virtual functions for polymorphism when appropriate.