Introduction to Encapsulation
Encapsulation is one of the four fundamental pillars of Object-Oriented Programming. It refers to the bundling of data (attributes) and methods (functions) that operate on that data within a single unit called a class, while restricting direct access to some of the object's components.
Encapsulation
Encapsulation is the technique of hiding the internal state and implementation details of an object from the outside world, exposing only what is necessary through a public interface. This protects data integrity and promotes modular design.
Key Principle: Keep data private, provide controlled access through public methods.
// Without encapsulation - data is exposed and vulnerable
class BankAccountBad {
public:
double balance; // Anyone can modify directly!
string accountNumber;
};
int main() {
BankAccountBad account;
account.balance = 1000;
account.balance = -500; // Invalid! But nothing stops this
account.balance = 9999999; // Fraud! No validation
return 0;
}
Without encapsulation, any code can directly access and modify the balance field.
This can lead to invalid states (negative balance), security vulnerabilities (unauthorized
changes), and bugs that are hard to track down.
// With encapsulation - data is protected
class BankAccountGood {
private:
double balance; // Hidden from outside
string accountNumber;
public:
BankAccountGood(string accNum, double initial) {
accountNumber = accNum;
if (initial >= 0) {
balance = initial;
} else {
balance = 0;
}
}
double getBalance() {
return balance;
}
bool deposit(double amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
};
With encapsulation, the balance is private - it can only be modified through
controlled methods like deposit() and withdraw(). These methods
validate input, preventing invalid operations and maintaining data integrity.
int main() {
BankAccountGood account("ACC123", 1000);
cout << "Balance: $" << account.getBalance() << endl; // 1000
account.deposit(500);
cout << "After deposit: $" << account.getBalance() << endl; // 1500
account.withdraw(200);
cout << "After withdrawal: $" << account.getBalance() << endl; // 1300
// Invalid operations are prevented
account.deposit(-100); // Returns false, balance unchanged
account.withdraw(5000); // Returns false, insufficient funds
// Cannot access directly:
// account.balance = -500; // ERROR! balance is private
return 0;
}
Benefits of Encapsulation
- Protects data from unauthorized access
- Enables input validation
- Hides implementation details
- Makes code easier to maintain and modify
- Reduces coupling between classes
Problems Without Encapsulation
- Data can be corrupted or set to invalid values
- No way to enforce business rules
- Changes to internal structure break external code
- Harder to debug and test
- Security vulnerabilities
Practice Questions: Introduction to Encapsulation
Task: Create a Counter class with a private count variable. Provide methods increment(), decrement(), getCount(), and reset(). Ensure count never goes below 0.
Show Solution
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
Counter() : count(0) {}
void increment() {
count++;
}
void decrement() {
if (count > 0) {
count--;
}
}
int getCount() {
return count;
}
void reset() {
count = 0;
}
};
int main() {
Counter c;
c.increment();
c.increment();
c.increment();
cout << "Count: " << c.getCount() << endl; // 3
c.decrement();
cout << "Count: " << c.getCount() << endl; // 2
c.decrement();
c.decrement();
c.decrement(); // Cannot go below 0
cout << "Count: " << c.getCount() << endl; // 0
return 0;
}
Task: Create a Temperature class that stores temperature in Celsius (private). Provide setCelsius(), getCelsius(), getFahrenheit(), and getKelvin(). Validate that temperature is above absolute zero (-273.15C).
Show Solution
#include <iostream>
using namespace std;
class Temperature {
private:
double celsius;
const double ABSOLUTE_ZERO = -273.15;
public:
Temperature(double c = 0) {
setCelsius(c);
}
bool setCelsius(double c) {
if (c >= ABSOLUTE_ZERO) {
celsius = c;
return true;
}
return false;
}
double getCelsius() {
return celsius;
}
double getFahrenheit() {
return (celsius * 9.0 / 5.0) + 32;
}
double getKelvin() {
return celsius + 273.15;
}
};
int main() {
Temperature temp(25);
cout << "Celsius: " << temp.getCelsius() << "C" << endl;
cout << "Fahrenheit: " << temp.getFahrenheit() << "F" << endl;
cout << "Kelvin: " << temp.getKelvin() << "K" << endl;
temp.setCelsius(-300); // Invalid, below absolute zero
cout << "After invalid set: " << temp.getCelsius() << "C" << endl; // Still 25
return 0;
}
Task: Create a Password class that stores a password privately. Provide setPassword() with validation (min 8 chars, must contain digit), verify(string) to check if a given password matches, and getStrength() returning "weak", "medium", or "strong".
Show Solution
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
class Password {
private:
string password;
bool hasDigit(const string& s) {
for (char c : s) {
if (isdigit(c)) return true;
}
return false;
}
bool hasUpper(const string& s) {
for (char c : s) {
if (isupper(c)) return true;
}
return false;
}
bool hasSpecial(const string& s) {
string special = "!@#$%^&*()";
for (char c : s) {
if (special.find(c) != string::npos) return true;
}
return false;
}
public:
Password() : password("") {}
bool setPassword(const string& pwd) {
if (pwd.length() < 8) {
cout << "Password must be at least 8 characters" << endl;
return false;
}
if (!hasDigit(pwd)) {
cout << "Password must contain at least one digit" << endl;
return false;
}
password = pwd;
return true;
}
bool verify(const string& input) {
return password == input;
}
string getStrength() {
int score = 0;
if (password.length() >= 8) score++;
if (password.length() >= 12) score++;
if (hasDigit(password)) score++;
if (hasUpper(password)) score++;
if (hasSpecial(password)) score++;
if (score <= 2) return "weak";
if (score <= 4) return "medium";
return "strong";
}
};
int main() {
Password pwd;
pwd.setPassword("short"); // Fails: too short
pwd.setPassword("longenough"); // Fails: no digit
pwd.setPassword("MyPass123"); // Success
cout << "Strength: " << pwd.getStrength() << endl;
cout << "Verify 'wrong': " << (pwd.verify("wrong") ? "Yes" : "No") << endl;
cout << "Verify 'MyPass123': " << (pwd.verify("MyPass123") ? "Yes" : "No") << endl;
pwd.setPassword("MyStr0ng!Pass");
cout << "New Strength: " << pwd.getStrength() << endl;
return 0;
}
Access Specifiers
Access specifiers control the visibility of class members (attributes and methods) from outside
the class. C++ provides three access specifiers: public, private, and
protected. Understanding these is essential for proper encapsulation.
Access Specifiers
Access specifiers determine which parts of your code can access class members. They enforce encapsulation by controlling what is exposed to the outside world and what remains hidden within the class.
| Specifier | Same Class | Derived Class | Outside Class |
|---|---|---|---|
public |
Yes | Yes | Yes |
protected |
Yes | Yes | No |
private |
Yes | No | No |
class Employee {
private:
// Only accessible within this class
double salary;
string ssn; // Social Security Number - very sensitive!
protected:
// Accessible within this class and derived classes
string department;
int employeeId;
public:
// Accessible from anywhere
string name;
string email;
Employee(string n, double s) : name(n), salary(s) {}
// Public method to access private data safely
double getSalary() {
return salary;
}
};
In this example, sensitive data like salary and ssn are private -
only the class itself can access them. The department and employeeId
are protected, allowing derived classes to access them. Public members like name
are accessible from anywhere.
class Manager : public Employee {
public:
Manager(string n, double s) : Employee(n, s) {}
void displayInfo() {
cout << "Name: " << name << endl; // OK - public
cout << "Dept: " << department << endl; // OK - protected
// cout << salary; // ERROR! salary is private in Employee
}
};
int main() {
Employee emp("John", 50000);
cout << emp.name << endl; // OK - public
cout << emp.email << endl; // OK - public
// cout << emp.department; // ERROR! protected
// cout << emp.salary; // ERROR! private
cout << emp.getSalary() << endl; // OK - using public method
return 0;
}
class, members are private by
default. In a struct, members are public by default. This is the
only difference between class and struct in C++.
// class vs struct default access
class MyClass {
int x; // private by default
};
struct MyStruct {
int x; // public by default
};
int main() {
MyClass c;
// c.x = 10; // ERROR! x is private
MyStruct s;
s.x = 10; // OK! x is public
return 0;
}
Practice Questions: Access Specifiers
Task: Create a Person class with private ssn, protected age, and public name. Add appropriate getter methods for private members.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string ssn;
protected:
int age;
public:
string name;
Person(string n, int a, string s)
: name(n), age(a), ssn(s) {}
string getLastFourSSN() {
if (ssn.length() >= 4) {
return "***-**-" + ssn.substr(ssn.length() - 4);
}
return "Invalid SSN";
}
int getAge() {
return age;
}
};
int main() {
Person person("Alice", 30, "123-45-6789");
cout << "Name: " << person.name << endl; // Direct access
cout << "Age: " << person.getAge() << endl; // Through getter
cout << "SSN: " << person.getLastFourSSN() << endl; // Masked
return 0;
}
Task: Create a Vehicle base class with private vin, protected speed, and public brand. Create a Car derived class that accesses and modifies the protected speed.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Vehicle {
private:
string vin; // Vehicle Identification Number
protected:
int speed;
public:
string brand;
Vehicle(string b, string v) : brand(b), vin(v), speed(0) {}
string getMaskedVIN() {
return "****" + vin.substr(vin.length() - 4);
}
};
class Car : public Vehicle {
public:
Car(string b, string v) : Vehicle(b, v) {}
void accelerate(int amount) {
speed += amount; // Can access protected member
if (speed > 200) speed = 200; // Max speed
}
void brake(int amount) {
speed -= amount;
if (speed < 0) speed = 0;
}
int getSpeed() {
return speed;
}
void displayInfo() {
cout << "Brand: " << brand << endl;
cout << "Speed: " << speed << " km/h" << endl;
cout << "VIN: " << getMaskedVIN() << endl;
// cout << vin; // ERROR! vin is private
}
};
int main() {
Car car("Toyota", "ABC123XYZ456789");
car.accelerate(60);
car.displayInfo();
car.accelerate(100);
car.brake(30);
cout << "Current speed: " << car.getSpeed() << " km/h" << endl;
return 0;
}
Task: Create a Logger base class with private log storage, protected formatting methods, and public logging interface. Create FileLogger and ConsoleLogger derived classes that use different formatting.
Show Solution
#include <iostream>
#include <string>
#include <vector>
#include <ctime>
using namespace std;
class Logger {
private:
vector<string> logs;
int maxLogs = 100;
void addToHistory(const string& entry) {
if (logs.size() >= maxLogs) {
logs.erase(logs.begin());
}
logs.push_back(entry);
}
protected:
string getTimestamp() {
time_t now = time(0);
char* dt = ctime(&now);
string timestamp(dt);
timestamp.pop_back();
return timestamp;
}
virtual string formatMessage(const string& level, const string& msg) {
return "[" + level + "] " + msg;
}
public:
virtual void log(const string& level, const string& message) {
string formatted = formatMessage(level, message);
addToHistory(formatted);
}
void showHistory() {
cout << "=== Log History ===" << endl;
for (const string& entry : logs) {
cout << entry << endl;
}
}
virtual ~Logger() {}
};
class ConsoleLogger : public Logger {
protected:
string formatMessage(const string& level, const string& msg) override {
return getTimestamp() + " [" + level + "] " + msg;
}
public:
void log(const string& level, const string& message) override {
string formatted = formatMessage(level, message);
cout << formatted << endl;
Logger::log(level, message);
}
};
class FileLogger : public Logger {
private:
string filename;
protected:
string formatMessage(const string& level, const string& msg) override {
return "[" + filename + "] " + getTimestamp() + " " + level + ": " + msg;
}
public:
FileLogger(const string& file) : filename(file) {}
void log(const string& level, const string& message) override {
string formatted = formatMessage(level, message);
cout << "(Writing to " << filename << ") " << formatted << endl;
Logger::log(level, message);
}
};
int main() {
ConsoleLogger console;
FileLogger file("app.log");
console.log("INFO", "Application started");
console.log("WARNING", "Low memory");
file.log("ERROR", "Connection failed");
file.log("INFO", "Retrying...");
cout << endl;
console.showHistory();
return 0;
}
Getters and Setters
Getters and setters (also called accessors and mutators) are public methods that provide controlled access to private data members. Getters retrieve values, while setters modify them. This pattern allows you to add validation, logging, or computed values without exposing internal details.
Getters and Setters
Getters (accessors) return the value of a private member. Setters (mutators) modify the value of a private member, typically with validation. Together, they provide a controlled interface to private data.
Naming Convention: getPropertyName() and setPropertyName(value)
class Product {
private:
string name;
double price;
int quantity;
public:
// Getters
string getName() const {
return name;
}
double getPrice() const {
return price;
}
int getQuantity() const {
return quantity;
}
// Setters with validation
void setName(const string& n) {
if (!n.empty()) {
name = n;
}
}
void setPrice(double p) {
if (p >= 0) {
price = p;
}
}
void setQuantity(int q) {
if (q >= 0) {
quantity = q;
}
}
// Computed property (no direct member)
double getTotalValue() const {
return price * quantity;
}
};
Notice the const keyword after getter methods. This indicates that the method does
not modify the object's state, making it callable on const objects and improving code safety.
// Advanced: Returning references for efficiency
class LargeData {
private:
vector<int> data;
public:
// Return const reference to avoid copying large data
const vector<int>& getData() const {
return data;
}
// Setter that moves data for efficiency
void setData(vector<int>&& newData) {
data = move(newData);
}
// Alternative: copy setter
void setData(const vector<int>& newData) {
data = newData;
}
};
For large objects, returning by const reference avoids expensive copies. The getter returns a read-only reference, maintaining encapsulation while improving performance. Move semantics can be used in setters for efficiency when the caller no longer needs the data.
// Read-only properties (getter only, no setter)
class Circle {
private:
double radius;
public:
Circle(double r) : radius(r > 0 ? r : 1) {}
double getRadius() const { return radius; }
// Computed read-only properties
double getDiameter() const { return 2 * radius; }
double getCircumference() const { return 2 * 3.14159 * radius; }
double getArea() const { return 3.14159 * radius * radius; }
// Only way to change radius (with validation)
void setRadius(double r) {
if (r > 0) {
radius = r;
}
}
};
int main() {
Circle c(5);
cout << "Radius: " << c.getRadius() << endl;
cout << "Diameter: " << c.getDiameter() << endl;
cout << "Circumference: " << c.getCircumference() << endl;
cout << "Area: " << c.getArea() << endl;
return 0;
}
Practice Questions: Getters and Setters
Task: Create a Rectangle class with private width and height. Add getters, setters (dimensions must be positive), and computed getters for getArea() and getPerimeter().
Show Solution
#include <iostream>
using namespace std;
class Rectangle {
private:
double width;
double height;
public:
Rectangle(double w = 1, double h = 1) {
setWidth(w);
setHeight(h);
}
// Getters
double getWidth() const { return width; }
double getHeight() const { return height; }
// Setters with validation
void setWidth(double w) {
width = (w > 0) ? w : 1;
}
void setHeight(double h) {
height = (h > 0) ? h : 1;
}
// Computed properties
double getArea() const {
return width * height;
}
double getPerimeter() const {
return 2 * (width + height);
}
};
int main() {
Rectangle rect(5, 3);
cout << "Width: " << rect.getWidth() << endl;
cout << "Height: " << rect.getHeight() << endl;
cout << "Area: " << rect.getArea() << endl;
cout << "Perimeter: " << rect.getPerimeter() << endl;
rect.setWidth(-10); // Invalid, uses default
cout << "After invalid width: " << rect.getWidth() << endl;
return 0;
}
Task: Create a Student class with name and a vector of grades. Provide addGrade() (0-100), getGrades(), getAverage(), and getLetterGrade() (A/B/C/D/F based on average).
Show Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Student {
private:
string name;
vector<int> grades;
public:
Student(const string& n) : name(n) {}
string getName() const { return name; }
void setName(const string& n) {
if (!n.empty()) {
name = n;
}
}
bool addGrade(int grade) {
if (grade >= 0 && grade <= 100) {
grades.push_back(grade);
return true;
}
return false;
}
const vector<int>& getGrades() const {
return grades;
}
double getAverage() const {
if (grades.empty()) return 0;
double sum = 0;
for (int g : grades) {
sum += g;
}
return sum / grades.size();
}
char getLetterGrade() const {
double avg = getAverage();
if (avg >= 90) return 'A';
if (avg >= 80) return 'B';
if (avg >= 70) return 'C';
if (avg >= 60) return 'D';
return 'F';
}
};
int main() {
Student student("Alice");
student.addGrade(95);
student.addGrade(87);
student.addGrade(92);
student.addGrade(78);
student.addGrade(150); // Invalid, ignored
cout << "Student: " << student.getName() << endl;
cout << "Grades: ";
for (int g : student.getGrades()) {
cout << g << " ";
}
cout << endl;
cout << "Average: " << student.getAverage() << endl;
cout << "Letter Grade: " << student.getLetterGrade() << endl;
return 0;
}
Task: Create a DateRange class with start and end dates. Setters must ensure start is before end. Add getDuration() (days), contains(date), and overlaps(DateRange) methods.
Show Solution
#include <iostream>
using namespace std;
struct Date {
int year, month, day;
bool operator<(const Date& other) const {
if (year != other.year) return year < other.year;
if (month != other.month) return month < other.month;
return day < other.day;
}
bool operator<=(const Date& other) const {
return !(other < *this);
}
bool operator==(const Date& other) const {
return year == other.year && month == other.month && day == other.day;
}
int toDays() const {
return year * 365 + month * 30 + day;
}
};
class DateRange {
private:
Date startDate;
Date endDate;
public:
DateRange(Date start, Date end) {
if (start <= end) {
startDate = start;
endDate = end;
} else {
startDate = end;
endDate = start;
}
}
Date getStart() const { return startDate; }
Date getEnd() const { return endDate; }
bool setStart(Date start) {
if (start <= endDate) {
startDate = start;
return true;
}
return false;
}
bool setEnd(Date end) {
if (startDate <= end) {
endDate = end;
return true;
}
return false;
}
int getDuration() const {
return endDate.toDays() - startDate.toDays();
}
bool contains(Date date) const {
return startDate <= date && date <= endDate;
}
bool overlaps(const DateRange& other) const {
return !(endDate < other.startDate || other.endDate < startDate);
}
};
int main() {
DateRange vacation({2024, 7, 1}, {2024, 7, 15});
DateRange conference({2024, 7, 10}, {2024, 7, 12});
cout << "Vacation duration: " << vacation.getDuration() << " days" << endl;
Date checkDate = {2024, 7, 8};
cout << "July 8 in vacation? " << (vacation.contains(checkDate) ? "Yes" : "No") << endl;
cout << "Vacation overlaps conference? " << (vacation.overlaps(conference) ? "Yes" : "No") << endl;
return 0;
}
Data Validation
One of the key benefits of encapsulation is the ability to validate data before it enters your object. This ensures that objects always maintain a valid state, preventing bugs and making your code more robust. Validation can happen in constructors, setters, or dedicated methods.
Class Invariants
A class invariant is a condition that must always be true for any instance of a class. Encapsulation allows you to enforce invariants by validating all modifications to the object's state.
Example: A BankAccount invariant might be "balance must never be negative."
class Email {
private:
string address;
bool isValidEmail(const string& email) {
// Simple validation: must contain @ and .
size_t atPos = email.find('@');
size_t dotPos = email.rfind('.');
return atPos != string::npos &&
dotPos != string::npos &&
atPos < dotPos &&
atPos > 0 &&
dotPos < email.length() - 1;
}
public:
Email(const string& email) {
if (!setEmail(email)) {
address = "invalid@example.com";
}
}
bool setEmail(const string& email) {
if (isValidEmail(email)) {
address = email;
return true;
}
return false;
}
string getEmail() const {
return address;
}
string getDomain() const {
size_t atPos = address.find('@');
return address.substr(atPos + 1);
}
};
The Email class validates email format both in the constructor and setter. The
isValidEmail() helper method is private since it is an implementation detail.
Invalid emails are rejected, ensuring the object always contains a valid email address.
class Age {
private:
int value;
static const int MIN_AGE = 0;
static const int MAX_AGE = 150;
public:
Age(int age = 0) {
setValue(age);
}
bool setValue(int age) {
if (age >= MIN_AGE && age <= MAX_AGE) {
value = age;
return true;
}
return false;
}
int getValue() const { return value; }
// Business logic methods
bool isAdult() const { return value >= 18; }
bool isSenior() const { return value >= 65; }
bool canVote() const { return value >= 18; }
bool canDrink() const { return value >= 21; }
};
int main() {
Age age(25);
cout << "Age: " << age.getValue() << endl;
cout << "Is adult: " << (age.isAdult() ? "Yes" : "No") << endl;
cout << "Can vote: " << (age.canVote() ? "Yes" : "No") << endl;
age.setValue(200); // Invalid, rejected
cout << "After invalid set: " << age.getValue() << endl; // Still 25
return 0;
}
Using encapsulation, we can add business logic methods like isAdult() and
canVote() that derive information from the stored value. The validation ensures
age is always within realistic bounds.
// Throwing exceptions for invalid data
class PositiveNumber {
private:
double value;
public:
PositiveNumber(double v) {
setValue(v);
}
void setValue(double v) {
if (v <= 0) {
throw invalid_argument("Value must be positive");
}
value = v;
}
double getValue() const { return value; }
};
int main() {
try {
PositiveNumber num(10);
cout << "Value: " << num.getValue() << endl;
num.setValue(-5); // Throws exception
} catch (const invalid_argument& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Practice Questions: Data Validation
Task: Create a Percentage class that only accepts values 0-100. Provide setValue(), getValue(), and getDecimal() (returns value/100).
Show Solution
#include <iostream>
using namespace std;
class Percentage {
private:
double value;
public:
Percentage(double v = 0) {
setValue(v);
}
bool setValue(double v) {
if (v >= 0 && v <= 100) {
value = v;
return true;
}
return false;
}
double getValue() const {
return value;
}
double getDecimal() const {
return value / 100.0;
}
string getDisplay() const {
return to_string((int)value) + "%";
}
};
int main() {
Percentage score(85);
cout << "Score: " << score.getValue() << endl;
cout << "Decimal: " << score.getDecimal() << endl;
cout << "Display: " << score.getDisplay() << endl;
score.setValue(150); // Invalid
cout << "After invalid set: " << score.getValue() << endl; // Still 85
return 0;
}
Task: Create a PhoneNumber class that validates US phone numbers (10 digits). Store internally as digits only. Provide getFormatted() returning "(XXX) XXX-XXXX" format.
Show Solution
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
class PhoneNumber {
private:
string digits;
string extractDigits(const string& input) {
string result;
for (char c : input) {
if (isdigit(c)) {
result += c;
}
}
return result;
}
public:
PhoneNumber(const string& phone = "") {
setNumber(phone);
}
bool setNumber(const string& phone) {
string extracted = extractDigits(phone);
if (extracted.length() == 10) {
digits = extracted;
return true;
}
return false;
}
string getRaw() const {
return digits;
}
string getFormatted() const {
if (digits.length() != 10) return "Invalid";
return "(" + digits.substr(0, 3) + ") " +
digits.substr(3, 3) + "-" +
digits.substr(6, 4);
}
string getAreaCode() const {
return digits.substr(0, 3);
}
bool isValid() const {
return digits.length() == 10;
}
};
int main() {
PhoneNumber phone1("(555) 123-4567");
PhoneNumber phone2("5551234567");
PhoneNumber phone3("123"); // Invalid
cout << "Phone 1: " << phone1.getFormatted() << endl;
cout << "Phone 2: " << phone2.getFormatted() << endl;
cout << "Phone 3 valid: " << (phone3.isValid() ? "Yes" : "No") << endl;
cout << "Area code: " << phone1.getAreaCode() << endl;
return 0;
}
Task: Create a CreditCard class that validates card numbers using the Luhn algorithm. Provide isValid(), getType() (Visa/MasterCard/Amex based on prefix), and getMasked() showing only last 4 digits.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class CreditCard {
private:
string number;
string extractDigits(const string& input) {
string result;
for (char c : input) {
if (isdigit(c)) result += c;
}
return result;
}
bool luhnCheck(const string& num) {
int sum = 0;
bool alternate = false;
for (int i = num.length() - 1; i >= 0; i--) {
int digit = num[i] - '0';
if (alternate) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
alternate = !alternate;
}
return (sum % 10 == 0);
}
public:
CreditCard(const string& cardNum) {
number = extractDigits(cardNum);
}
bool isValid() const {
if (number.length() < 13 || number.length() > 19) {
return false;
}
// Note: luhnCheck needs non-const, so we copy
string numCopy = number;
CreditCard* self = const_cast<CreditCard*>(this);
return self->luhnCheck(numCopy);
}
string getType() const {
if (number.length() == 0) return "Unknown";
if (number[0] == '4') return "Visa";
if (number.substr(0, 2) == "51" ||
number.substr(0, 2) == "52" ||
number.substr(0, 2) == "53" ||
number.substr(0, 2) == "54" ||
number.substr(0, 2) == "55") return "MasterCard";
if (number.substr(0, 2) == "34" ||
number.substr(0, 2) == "37") return "American Express";
if (number.substr(0, 4) == "6011") return "Discover";
return "Unknown";
}
string getMasked() const {
if (number.length() < 4) return "****";
return string(number.length() - 4, '*') + number.substr(number.length() - 4);
}
};
int main() {
CreditCard visa("4111111111111111"); // Test Visa number
CreditCard amex("378282246310005"); // Test Amex number
cout << "Card: " << visa.getMasked() << endl;
cout << "Type: " << visa.getType() << endl;
cout << "Valid: " << (visa.isValid() ? "Yes" : "No") << endl;
cout << endl;
cout << "Card: " << amex.getMasked() << endl;
cout << "Type: " << amex.getType() << endl;
cout << "Valid: " << (amex.isValid() ? "Yes" : "No") << endl;
return 0;
}
Best Practices
Following best practices for encapsulation leads to more maintainable, secure, and flexible code. These guidelines help you design classes that are easy to use, hard to misuse, and adaptable to changing requirements.
Do
- Make data members private by default
- Use const for getters that do not modify state
- Validate all input in setters and constructors
- Return by const reference for large objects
- Document invariants and validation rules
- Use protected only when inheritance requires it
Do Not
- Expose internal data structures directly
- Create getters/setters for every field blindly
- Return non-const references to private data
- Skip validation "because it is just internal"
- Make everything public "for convenience"
- Use friend classes excessively
// BAD: Exposing internal implementation
class BadList {
public:
vector<int> items; // Anyone can modify directly!
};
// GOOD: Hiding internal implementation
class GoodList {
private:
vector<int> items;
public:
void add(int item) { items.push_back(item); }
void remove(int index) {
if (index >= 0 && index < items.size()) {
items.erase(items.begin() + index);
}
}
int get(int index) const {
if (index >= 0 && index < items.size()) {
return items[index];
}
throw out_of_range("Index out of bounds");
}
int size() const { return items.size(); }
};
The GoodList class hides the vector implementation. If you later decide to use a
different data structure (like a linked list), code using the class does not need to change.
This is the power of encapsulation - implementation flexibility.
// Immutable class example - all members const after construction
class ImmutablePoint {
private:
const double x;
const double y;
public:
ImmutablePoint(double xVal, double yVal) : x(xVal), y(yVal) {}
double getX() const { return x; }
double getY() const { return y; }
// Instead of modifying, return new instances
ImmutablePoint translate(double dx, double dy) const {
return ImmutablePoint(x + dx, y + dy);
}
ImmutablePoint scale(double factor) const {
return ImmutablePoint(x * factor, y * factor);
}
};
int main() {
ImmutablePoint p1(3, 4);
ImmutablePoint p2 = p1.translate(1, 1); // New point, p1 unchanged
cout << "P1: (" << p1.getX() << ", " << p1.getY() << ")" << endl;
cout << "P2: (" << p2.getX() << ", " << p2.getY() << ")" << endl;
return 0;
}
Immutable objects are inherently thread-safe and easier to reason about. Instead of setters, provide methods that return new instances with modified values. This pattern is common in functional programming and increasingly popular in C++.
// Builder pattern for complex object construction
class Pizza {
private:
string size;
string crust;
vector<string> toppings;
Pizza() {} // Private constructor
public:
class Builder {
private:
Pizza pizza;
public:
Builder& setSize(const string& s) {
pizza.size = s;
return *this;
}
Builder& setCrust(const string& c) {
pizza.crust = c;
return *this;
}
Builder& addTopping(const string& t) {
pizza.toppings.push_back(t);
return *this;
}
Pizza build() {
if (pizza.size.empty()) pizza.size = "medium";
if (pizza.crust.empty()) pizza.crust = "regular";
return pizza;
}
};
void display() const {
cout << size << " " << crust << " pizza with: ";
for (const string& t : toppings) {
cout << t << " ";
}
cout << endl;
}
};
int main() {
Pizza pizza = Pizza::Builder()
.setSize("large")
.setCrust("thin")
.addTopping("pepperoni")
.addTopping("mushrooms")
.addTopping("olives")
.build();
pizza.display();
return 0;
}
Practice Questions: Best Practices
Task: Given this poorly designed class, refactor it with proper encapsulation:
class User {
public:
string username;
string password;
int loginAttempts;
};
Show Solution
#include <iostream>
#include <string>
using namespace std;
class User {
private:
string username;
string passwordHash; // Store hash, not plain text
int loginAttempts;
bool locked;
static const int MAX_ATTEMPTS = 3;
string hashPassword(const string& pwd) {
// Simple hash simulation
size_t hash = 0;
for (char c : pwd) {
hash = hash * 31 + c;
}
return to_string(hash);
}
public:
User(const string& user, const string& pwd)
: username(user), loginAttempts(0), locked(false) {
passwordHash = hashPassword(pwd);
}
string getUsername() const { return username; }
bool isLocked() const { return locked; }
bool authenticate(const string& pwd) {
if (locked) {
cout << "Account is locked" << endl;
return false;
}
if (hashPassword(pwd) == passwordHash) {
loginAttempts = 0;
return true;
}
loginAttempts++;
if (loginAttempts >= MAX_ATTEMPTS) {
locked = true;
cout << "Account locked after " << MAX_ATTEMPTS << " failed attempts" << endl;
}
return false;
}
void unlock() {
locked = false;
loginAttempts = 0;
}
};
int main() {
User user("john_doe", "secret123");
cout << "Attempt 1: " << (user.authenticate("wrong") ? "Success" : "Failed") << endl;
cout << "Attempt 2: " << (user.authenticate("wrong") ? "Success" : "Failed") << endl;
cout << "Attempt 3: " << (user.authenticate("wrong") ? "Success" : "Failed") << endl;
cout << "Attempt 4: " << (user.authenticate("secret123") ? "Success" : "Failed") << endl;
user.unlock();
cout << "After unlock: " << (user.authenticate("secret123") ? "Success" : "Failed") << endl;
return 0;
}
Task: Create an immutable Money class with currency and amount. All operations (add, subtract, convert) should return new Money instances instead of modifying the original.
Show Solution
#include <iostream>
#include <string>
#include <map>
using namespace std;
class Money {
private:
const double amount;
const string currency;
static map<string, double> exchangeRates;
public:
Money(double amt, const string& curr)
: amount(amt >= 0 ? amt : 0), currency(curr) {}
double getAmount() const { return amount; }
string getCurrency() const { return currency; }
Money add(const Money& other) const {
if (currency != other.currency) {
Money converted = other.convert(currency);
return Money(amount + converted.amount, currency);
}
return Money(amount + other.amount, currency);
}
Money subtract(const Money& other) const {
if (currency != other.currency) {
Money converted = other.convert(currency);
return Money(amount - converted.amount, currency);
}
double result = amount - other.amount;
return Money(result >= 0 ? result : 0, currency);
}
Money convert(const string& targetCurrency) const {
if (currency == targetCurrency) return *this;
double rate = exchangeRates[currency + "_" + targetCurrency];
return Money(amount * rate, targetCurrency);
}
string toString() const {
return currency + " " + to_string(amount);
}
static void setExchangeRate(const string& from, const string& to, double rate) {
exchangeRates[from + "_" + to] = rate;
exchangeRates[to + "_" + from] = 1.0 / rate;
}
};
map<string, double> Money::exchangeRates;
int main() {
Money::setExchangeRate("USD", "EUR", 0.85);
Money wallet(100, "USD");
Money expense(20, "USD");
Money euroExpense(10, "EUR");
Money remaining = wallet.subtract(expense);
cout << "After expense: " << remaining.toString() << endl;
Money converted = wallet.convert("EUR");
cout << "Converted: " << converted.toString() << endl;
Money total = wallet.add(euroExpense);
cout << "Total in USD: " << total.toString() << endl;
// Original unchanged
cout << "Original wallet: " << wallet.toString() << endl;
return 0;
}
Task: Create a ServerConfig class using the Builder pattern. Config should include host, port, timeout, maxConnections, and SSL settings. Provide sensible defaults and validation.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class ServerConfig {
private:
string host;
int port;
int timeout;
int maxConnections;
bool sslEnabled;
string sslCertPath;
ServerConfig() {} // Private constructor
public:
// Getters only - immutable after construction
string getHost() const { return host; }
int getPort() const { return port; }
int getTimeout() const { return timeout; }
int getMaxConnections() const { return maxConnections; }
bool isSslEnabled() const { return sslEnabled; }
string getSslCertPath() const { return sslCertPath; }
void display() const {
cout << "Server Configuration:" << endl;
cout << " Host: " << host << endl;
cout << " Port: " << port << endl;
cout << " Timeout: " << timeout << "ms" << endl;
cout << " Max Connections: " << maxConnections << endl;
cout << " SSL: " << (sslEnabled ? "Enabled" : "Disabled") << endl;
if (sslEnabled) {
cout << " SSL Cert: " << sslCertPath << endl;
}
}
class Builder {
private:
ServerConfig config;
public:
Builder() {
// Defaults
config.host = "localhost";
config.port = 8080;
config.timeout = 30000;
config.maxConnections = 100;
config.sslEnabled = false;
config.sslCertPath = "";
}
Builder& host(const string& h) {
config.host = h;
return *this;
}
Builder& port(int p) {
if (p > 0 && p <= 65535) {
config.port = p;
}
return *this;
}
Builder& timeout(int t) {
if (t > 0) {
config.timeout = t;
}
return *this;
}
Builder& maxConnections(int max) {
if (max > 0) {
config.maxConnections = max;
}
return *this;
}
Builder& enableSsl(const string& certPath) {
config.sslEnabled = true;
config.sslCertPath = certPath;
return *this;
}
Builder& disableSsl() {
config.sslEnabled = false;
config.sslCertPath = "";
return *this;
}
ServerConfig build() {
// Validation
if (config.sslEnabled && config.sslCertPath.empty()) {
throw runtime_error("SSL enabled but no certificate path provided");
}
return config;
}
};
};
int main() {
// Development config
ServerConfig devConfig = ServerConfig::Builder()
.host("127.0.0.1")
.port(3000)
.timeout(60000)
.build();
cout << "=== Development ===" << endl;
devConfig.display();
// Production config
ServerConfig prodConfig = ServerConfig::Builder()
.host("api.example.com")
.port(443)
.maxConnections(1000)
.timeout(15000)
.enableSsl("/etc/ssl/server.crt")
.build();
cout << "\n=== Production ===" << endl;
prodConfig.display();
return 0;
}
Task: Create a ShoppingCart class with private item storage. Provide methods to add/remove items, get total, apply discount (percentage), and get item count. Ensure quantities cannot be negative.
Show Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct CartItem {
string name;
double price;
int quantity;
};
class ShoppingCart {
private:
vector<CartItem> items;
double discountPercent;
int findItem(const string& name) {
for (int i = 0; i < items.size(); i++) {
if (items[i].name == name) return i;
}
return -1;
}
public:
ShoppingCart() : discountPercent(0) {}
void addItem(const string& name, double price, int qty = 1) {
if (price < 0 || qty <= 0) return;
int idx = findItem(name);
if (idx >= 0) {
items[idx].quantity += qty;
} else {
items.push_back({name, price, qty});
}
}
bool removeItem(const string& name, int qty = 1) {
int idx = findItem(name);
if (idx < 0) return false;
items[idx].quantity -= qty;
if (items[idx].quantity <= 0) {
items.erase(items.begin() + idx);
}
return true;
}
void applyDiscount(double percent) {
if (percent >= 0 && percent <= 100) {
discountPercent = percent;
}
}
double getSubtotal() const {
double total = 0;
for (const auto& item : items) {
total += item.price * item.quantity;
}
return total;
}
double getTotal() const {
double subtotal = getSubtotal();
return subtotal * (1 - discountPercent / 100);
}
int getItemCount() const {
int count = 0;
for (const auto& item : items) {
count += item.quantity;
}
return count;
}
void display() const {
cout << "Shopping Cart:" << endl;
for (const auto& item : items) {
cout << " " << item.name << " x" << item.quantity
<< " @ $" << item.price << endl;
}
cout << "Subtotal: $" << getSubtotal() << endl;
if (discountPercent > 0) {
cout << "Discount: " << discountPercent << "%" << endl;
}
cout << "Total: $" << getTotal() << endl;
}
};
int main() {
ShoppingCart cart;
cart.addItem("Laptop", 999.99);
cart.addItem("Mouse", 29.99, 2);
cart.addItem("Keyboard", 79.99);
cart.display();
cout << "\nApplying 10% discount..." << endl;
cart.applyDiscount(10);
cart.display();
cout << "\nRemoving one mouse..." << endl;
cart.removeItem("Mouse");
cout << "Item count: " << cart.getItemCount() << endl;
return 0;
}
Abstract Classes
Abstraction is another fundamental pillar of OOP that focuses on hiding complex implementation details while exposing only the essential features. Abstract classes provide a way to define interfaces and enforce contracts that derived classes must fulfill.
Abstraction
Abstraction is the concept of hiding complex implementation details and showing only the necessary features of an object. It reduces complexity by providing a simplified view of what an object does, not how it does it.
Key Principle: Define WHAT should be done, let derived classes define HOW.
Creating an Abstract Class
An abstract class contains at least one pure virtual function (declared with = 0). It cannot be instantiated directly:
// Abstract class - cannot be instantiated directly
class Shape {
public:
// Pure virtual function - makes the class abstract
virtual double getArea() const = 0;
virtual double getPerimeter() const = 0;
virtual void draw() const = 0;
// Regular virtual function with default implementation
virtual void describe() const {
cout << "This is a shape" << endl;
}
// Virtual destructor is essential for abstract classes
virtual ~Shape() {}
};
Implementing an Abstract Class
Concrete classes must implement all pure virtual functions to be instantiable:
// Concrete class implementing the abstract class
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() const override {
return 3.14159 * radius * radius;
}
double getPerimeter() const override {
return 2 * 3.14159 * radius;
}
void draw() const override {
cout << "Drawing a circle with radius " << radius << endl;
}
};
The Shape class is abstract because it contains pure virtual functions (declared with
= 0). You cannot create instances of Shape directly - you must create
instances of concrete derived classes like Circle that implement all pure virtual functions.
Another Concrete Implementation
Multiple classes can inherit from the same abstract class, each providing its own implementation:
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() const override {
return width * height;
}
double getPerimeter() const override {
return 2 * (width + height);
}
void draw() const override {
cout << "Drawing a rectangle " << width << "x" << height << endl;
}
};
Using Abstract Classes with Polymorphism
Abstract classes enable polymorphism - you can use base class pointers to work with different derived types:
int main() {
// Shape s; // ERROR! Cannot instantiate abstract class
Circle circle(5);
Rectangle rect(4, 6);
// Polymorphism with abstract base class
Shape* shapes[] = {&circle, &rect};
for (Shape* shape : shapes) {
shape->draw();
cout << "Area: " << shape->getArea() << endl;
cout << "Perimeter: " << shape->getPerimeter() << endl;
cout << endl;
}
return 0;
}
When to Use Abstract Classes
- When you want to provide a common interface for related classes
- When you have shared code that derived classes can inherit
- When you want to enforce that certain methods must be implemented
- When you need protected members accessible to derived classes
Benefits of Abstract Classes
- Enforces a contract that derived classes must follow
- Enables polymorphism through base class pointers
- Provides code reuse through shared implementations
- Makes code more maintainable and extensible
Abstract Class with Protected Members
Abstract classes can have data members and implemented methods that derived classes inherit and use:
// Abstract class with mix of pure virtual and implemented methods
class Database {
protected:
string connectionString;
bool isConnected;
public:
Database(const string& connStr) : connectionString(connStr), isConnected(false) {}
// Pure virtual - each database type connects differently
virtual bool connect() = 0;
virtual bool disconnect() = 0;
virtual bool executeQuery(const string& query) = 0;
// Implemented method - common to all databases
bool isConnectionActive() const {
return isConnected;
}
void setConnectionString(const string& connStr) {
if (!isConnected) {
connectionString = connStr;
}
}
virtual ~Database() {
if (isConnected) {
disconnect();
}
}
};
Concrete Database Implementation
The MySQLDatabase class inherits the protected members and implements all pure virtual functions:
class MySQLDatabase : public Database {
public:
MySQLDatabase(const string& connStr) : Database(connStr) {}
bool connect() override {
cout << "Connecting to MySQL: " << connectionString << endl;
isConnected = true;
return true;
}
bool disconnect() override {
cout << "Disconnecting from MySQL" << endl;
isConnected = false;
return true;
}
bool executeQuery(const string& query) override {
if (!isConnected) return false;
cout << "MySQL executing: " << query << endl;
return true;
}
};
Practice Questions: Abstract Classes
Task: Create an abstract Animal class with pure virtual methods makeSound() and move(). Create Dog and Bird classes that implement these methods.
Show Solution
#include <iostream>
using namespace std;
class Animal {
protected:
string name;
public:
Animal(const string& n) : name(n) {}
virtual void makeSound() const = 0;
virtual void move() const = 0;
string getName() const { return name; }
virtual ~Animal() {}
};
class Dog : public Animal {
public:
Dog(const string& n) : Animal(n) {}
void makeSound() const override {
cout << name << " says: Woof! Woof!" << endl;
}
void move() const override {
cout << name << " runs on four legs" << endl;
}
};
class Bird : public Animal {
public:
Bird(const string& n) : Animal(n) {}
void makeSound() const override {
cout << name << " says: Tweet! Tweet!" << endl;
}
void move() const override {
cout << name << " flies through the air" << endl;
}
};
int main() {
Dog dog("Buddy");
Bird bird("Tweety");
Animal* animals[] = {&dog, &bird};
for (Animal* animal : animals) {
cout << "--- " << animal->getName() << " ---" << endl;
animal->makeSound();
animal->move();
cout << endl;
}
return 0;
}
Task: Create an abstract PaymentProcessor class with processPayment(amount) and refund(amount). Create CreditCardProcessor and PayPalProcessor implementations.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class PaymentProcessor {
protected:
double balance;
string processorName;
public:
PaymentProcessor(const string& name) : processorName(name), balance(0) {}
virtual bool processPayment(double amount) = 0;
virtual bool refund(double amount) = 0;
double getBalance() const { return balance; }
string getName() const { return processorName; }
virtual ~PaymentProcessor() {}
};
class CreditCardProcessor : public PaymentProcessor {
private:
string cardNumber;
public:
CreditCardProcessor(const string& card)
: PaymentProcessor("Credit Card"), cardNumber(card) {}
bool processPayment(double amount) override {
if (amount <= 0) return false;
cout << "Processing $" << amount << " via Credit Card ending in "
<< cardNumber.substr(cardNumber.length() - 4) << endl;
balance += amount;
return true;
}
bool refund(double amount) override {
if (amount <= 0 || amount > balance) return false;
cout << "Refunding $" << amount << " to Credit Card" << endl;
balance -= amount;
return true;
}
};
class PayPalProcessor : public PaymentProcessor {
private:
string email;
public:
PayPalProcessor(const string& e)
: PaymentProcessor("PayPal"), email(e) {}
bool processPayment(double amount) override {
if (amount <= 0) return false;
cout << "Processing $" << amount << " via PayPal (" << email << ")" << endl;
balance += amount;
return true;
}
bool refund(double amount) override {
if (amount <= 0 || amount > balance) return false;
cout << "Refunding $" << amount << " to PayPal account" << endl;
balance -= amount;
return true;
}
};
int main() {
CreditCardProcessor cc("4111111111111234");
PayPalProcessor pp("user@email.com");
cc.processPayment(100);
pp.processPayment(50);
cout << "\nBalances:" << endl;
cout << cc.getName() << ": $" << cc.getBalance() << endl;
cout << pp.getName() << ": $" << pp.getBalance() << endl;
cc.refund(25);
cout << "\nAfter refund: $" << cc.getBalance() << endl;
return 0;
}
Task: Create an abstract Serializable class with toJSON() and toXML() methods. Create Person and Product classes that implement serialization.
Show Solution
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
class Serializable {
public:
virtual string toJSON() const = 0;
virtual string toXML() const = 0;
virtual string getType() const = 0;
void printAll() const {
cout << "=== " << getType() << " ===" << endl;
cout << "JSON: " << toJSON() << endl;
cout << "XML: " << toXML() << endl;
}
virtual ~Serializable() {}
};
class Person : public Serializable {
private:
string name;
int age;
string email;
public:
Person(const string& n, int a, const string& e)
: name(n), age(a), email(e) {}
string getType() const override { return "Person"; }
string toJSON() const override {
ostringstream oss;
oss << "{\"name\":\"" << name << "\",\"age\":" << age
<< ",\"email\":\"" << email << "\"}";
return oss.str();
}
string toXML() const override {
ostringstream oss;
oss << "<Person><name>" << name << "</name><age>" << age
<< "</age><email>" << email << "</email></Person>";
return oss.str();
}
};
class Product : public Serializable {
private:
string name;
double price;
int stock;
public:
Product(const string& n, double p, int s)
: name(n), price(p), stock(s) {}
string getType() const override { return "Product"; }
string toJSON() const override {
ostringstream oss;
oss << "{\"name\":\"" << name << "\",\"price\":" << price
<< ",\"stock\":" << stock << "}";
return oss.str();
}
string toXML() const override {
ostringstream oss;
oss << "<Product><name>" << name << "</name><price>" << price
<< "</price><stock>" << stock << "</stock></Product>";
return oss.str();
}
};
int main() {
Person person("John Doe", 30, "john@email.com");
Product product("Laptop", 999.99, 50);
Serializable* items[] = {&person, &product};
for (Serializable* item : items) {
item->printAll();
cout << endl;
}
return 0;
}
Interface Design
An interface in C++ is typically implemented as an abstract class with only pure virtual functions and no data members. Interfaces define a contract that classes must follow, enabling loose coupling and making code more flexible and testable.
Interface (Pure Abstract Class)
An interface is a class with only pure virtual functions and no implementation. It defines a contract specifying what methods a class must implement without dictating how they should be implemented.
Convention: Interface names often start with "I" (e.g., IDrawable, IComparable)
Defining Interfaces
An interface contains only pure virtual functions (declared with = 0) and a virtual destructor. No data members or implementation:
// Interface - pure abstract class with no data members
class IDrawable {
public:
virtual void draw() const = 0;
virtual void resize(double factor) = 0;
virtual ~IDrawable() {}
};
class IPrintable {
public:
virtual void print() const = 0;
virtual string toString() const = 0;
virtual ~IPrintable() {}
};
Implementing Multiple Interfaces
A class can implement multiple interfaces, providing a safe form of multiple inheritance. Each interface method must be overridden:
// A class can implement multiple interfaces
class Document : public IDrawable, public IPrintable {
private:
string content;
double scale;
public:
Document(const string& c) : content(c), scale(1.0) {}
// IDrawable implementation
void draw() const override {
cout << "Drawing document at scale " << scale << endl;
}
void resize(double factor) override {
scale *= factor;
}
// IPrintable implementation
void print() const override {
cout << "Printing: " << content << endl;
}
string toString() const override {
return "Document: " + content;
}
};
Unlike some languages (Java, C#) that have a dedicated interface keyword, C++ uses
abstract classes to achieve the same goal. A class can inherit from multiple interfaces, providing
a form of multiple inheritance that is generally considered safe.
Dependency Injection with Interfaces
Interfaces enable dependency injection, where a class depends on an abstraction rather than a concrete implementation. First, define the interface:
// Dependency Injection using interfaces
class ILogger {
public:
virtual void log(const string& message) = 0;
virtual void error(const string& message) = 0;
virtual ~ILogger() {}
};
Create different implementations of the same interface:
class ConsoleLogger : public ILogger {
public:
void log(const string& message) override {
cout << "[LOG] " << message << endl;
}
void error(const string& message) override {
cerr << "[ERROR] " << message << endl;
}
};
class FileLogger : public ILogger {
private:
string filename;
public:
FileLogger(const string& file) : filename(file) {}
void log(const string& message) override {
cout << "(Writing to " << filename << ") [LOG] " << message << endl;
}
void error(const string& message) override {
cout << "(Writing to " << filename << ") [ERROR] " << message << endl;
}
};
The Application class depends on the ILogger interface, not any specific implementation. This makes it easy to swap loggers:
// Class depends on interface, not concrete implementation
class Application {
private:
ILogger* logger; // Dependency injected via interface
public:
Application(ILogger* log) : logger(log) {}
void run() {
logger->log("Application started");
// Do work...
logger->log("Application finished");
}
};
int main() {
ConsoleLogger consoleLog;
FileLogger fileLog("app.log");
// Easy to switch implementations
Application app1(&consoleLog);
Application app2(&fileLog);
app1.run();
cout << endl;
app2.run();
return 0;
}
Small, Focused Interfaces
Instead of one large interface, create small interfaces each with a single responsibility:
// Good: Small, focused interfaces
class IReadable {
public:
virtual string read() = 0;
virtual ~IReadable() {}
};
class IWritable {
public:
virtual void write(const string& data) = 0;
virtual ~IWritable() {}
};
class ICloseable {
public:
virtual void close() = 0;
virtual ~ICloseable() {}
};
A full-featured File class implements all three interfaces:
// File implements all three interfaces
class File : public IReadable, public IWritable, public ICloseable {
private:
string filename;
string content;
bool isOpen;
public:
File(const string& name) : filename(name), isOpen(true) {}
string read() override {
return isOpen ? content : "";
}
void write(const string& data) override {
if (isOpen) content += data;
}
void close() override {
isOpen = false;
cout << "File " << filename << " closed" << endl;
}
};
A ReadOnlyFile only needs to implement IReadable, avoiding unnecessary methods:
// ReadOnlyFile only implements IReadable
class ReadOnlyFile : public IReadable {
private:
string content;
public:
ReadOnlyFile(const string& data) : content(data) {}
string read() override {
return content;
}
};
Practice Questions: Interface Design
Task: Create an IComparable interface with compareTo() method. Implement it in a Student class that compares by grade.
Show Solution
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class IComparable {
public:
// Returns: negative if this < other, 0 if equal, positive if this > other
virtual int compareTo(const IComparable& other) const = 0;
virtual ~IComparable() {}
};
class Student : public IComparable {
private:
string name;
double grade;
public:
Student(const string& n, double g) : name(n), grade(g) {}
int compareTo(const IComparable& other) const override {
const Student& otherStudent = dynamic_cast<const Student&>(other);
if (grade < otherStudent.grade) return -1;
if (grade > otherStudent.grade) return 1;
return 0;
}
string getName() const { return name; }
double getGrade() const { return grade; }
};
int main() {
Student s1("Alice", 85);
Student s2("Bob", 92);
Student s3("Charlie", 85);
cout << s1.getName() << " vs " << s2.getName() << ": " << s1.compareTo(s2) << endl;
cout << s2.getName() << " vs " << s1.getName() << ": " << s2.compareTo(s1) << endl;
cout << s1.getName() << " vs " << s3.getName() << ": " << s1.compareTo(s3) << endl;
return 0;
}
Task: Create an INotifier interface. Implement EmailNotifier, SMSNotifier, and PushNotifier. Create a NotificationService that can use any notifier.
Show Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class INotifier {
public:
virtual bool send(const string& recipient, const string& message) = 0;
virtual string getType() const = 0;
virtual ~INotifier() {}
};
class EmailNotifier : public INotifier {
public:
bool send(const string& recipient, const string& message) override {
cout << "Email to " << recipient << ": " << message << endl;
return true;
}
string getType() const override { return "Email"; }
};
class SMSNotifier : public INotifier {
public:
bool send(const string& recipient, const string& message) override {
cout << "SMS to " << recipient << ": " << message << endl;
return true;
}
string getType() const override { return "SMS"; }
};
class PushNotifier : public INotifier {
public:
bool send(const string& recipient, const string& message) override {
cout << "Push to " << recipient << ": " << message << endl;
return true;
}
string getType() const override { return "Push"; }
};
class NotificationService {
private:
vector<INotifier*> notifiers;
public:
void addNotifier(INotifier* notifier) {
notifiers.push_back(notifier);
}
void notifyAll(const string& recipient, const string& message) {
cout << "Sending notifications..." << endl;
for (INotifier* notifier : notifiers) {
notifier->send(recipient, message);
}
}
};
int main() {
EmailNotifier email;
SMSNotifier sms;
PushNotifier push;
NotificationService service;
service.addNotifier(&email);
service.addNotifier(&sms);
service.addNotifier(&push);
service.notifyAll("user123", "Your order has shipped!");
return 0;
}
Task: Create an IPlugin interface with initialize(), execute(), and shutdown(). Create a PluginManager that loads, runs, and manages multiple plugins.
Show Solution
#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;
class IPlugin {
public:
virtual string getName() const = 0;
virtual string getVersion() const = 0;
virtual bool initialize() = 0;
virtual void execute() = 0;
virtual void shutdown() = 0;
virtual ~IPlugin() {}
};
class LoggingPlugin : public IPlugin {
public:
string getName() const override { return "LoggingPlugin"; }
string getVersion() const override { return "1.0.0"; }
bool initialize() override {
cout << "LoggingPlugin: Initializing..." << endl;
return true;
}
void execute() override {
cout << "LoggingPlugin: Logging application events" << endl;
}
void shutdown() override {
cout << "LoggingPlugin: Shutting down, flushing logs" << endl;
}
};
class SecurityPlugin : public IPlugin {
public:
string getName() const override { return "SecurityPlugin"; }
string getVersion() const override { return "2.1.0"; }
bool initialize() override {
cout << "SecurityPlugin: Loading security rules..." << endl;
return true;
}
void execute() override {
cout << "SecurityPlugin: Scanning for threats" << endl;
}
void shutdown() override {
cout << "SecurityPlugin: Saving security state" << endl;
}
};
class PluginManager {
private:
map<string, IPlugin*> plugins;
vector<string> loadOrder;
public:
bool registerPlugin(IPlugin* plugin) {
string name = plugin->getName();
if (plugins.find(name) != plugins.end()) {
return false;
}
plugins[name] = plugin;
loadOrder.push_back(name);
return true;
}
void initializeAll() {
cout << "=== Initializing Plugins ===" << endl;
for (const string& name : loadOrder) {
cout << "Loading " << name << " v" << plugins[name]->getVersion() << endl;
plugins[name]->initialize();
}
}
void executeAll() {
cout << "\n=== Executing Plugins ===" << endl;
for (const string& name : loadOrder) {
plugins[name]->execute();
}
}
void shutdownAll() {
cout << "\n=== Shutting Down Plugins ===" << endl;
for (auto it = loadOrder.rbegin(); it != loadOrder.rend(); ++it) {
plugins[*it]->shutdown();
}
}
void listPlugins() {
cout << "\nRegistered Plugins:" << endl;
for (const auto& pair : plugins) {
cout << " - " << pair.first << " v" << pair.second->getVersion() << endl;
}
}
};
int main() {
LoggingPlugin logging;
SecurityPlugin security;
PluginManager manager;
manager.registerPlugin(&logging);
manager.registerPlugin(&security);
manager.listPlugins();
manager.initializeAll();
manager.executeAll();
manager.shutdownAll();
return 0;
}
Key Takeaways
Data Hiding
Keep data members private to protect them from unauthorized access and maintain control over modifications
Access Specifiers
Use public for interface, private for implementation, and protected for inheritance needs
Controlled Access
Getters and setters provide controlled access with validation, logging, and computed properties
Class Invariants
Use validation to ensure objects always remain in a valid state throughout their lifetime
Abstract Classes
Use pure virtual functions to define contracts that derived classes must implement
Interface Design
Design clean interfaces to enable loose coupling, dependency injection, and testable code
Knowledge Check
Test your understanding of C++ Encapsulation & Abstraction:
What is the main purpose of encapsulation in OOP?
Which access specifier makes members accessible only within the same class?
What is a class invariant?
What happens when you return a non-const reference to a private member from a getter?
What makes a class abstract in C++?
What is the main difference between an abstract class and an interface in C++?