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
The Problem
This code demonstrates the issue:
// 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:
// 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.