Home C++ Abstraction in C++ tutorial with code examples

Abstraction in C++ tutorial with code examples

Abstraction in C++ is an object-oriented programming principle that focuses on hiding the complexity of the system by exposing only essential information.

By providing only relevant details and hiding unnecessary complexity, abstraction helps to create a simplified interface for interacting with complex functionalities.

Key Concepts of Abstraction

  • Abstract Classes: A class that cannot be instantiated directly. It often serves as a base class for other derived classes.
  • Pure Virtual Functions: Functions with no implementation in the base class. They must be overridden in derived classes.
  • Interfaces: Classes that provide only abstract functions, allowing derived classes to define specific implementations.

Benefits of Abstraction

  1. Simplicity: Reduces complexity for the user by hiding internal details.
  2. Modularity: Separates code into different layers, making maintenance and updates easier.
  3. Code Reusability: Promotes the reuse of base classes with common functionalities in multiple derived classes.

Let’s explore abstraction in C++ with examples.

1. Basic Abstraction Using Member Functions

Using member functions to hide the internal workings of a class is the simplest form of abstraction.

#include <iostream>
using namespace std;

class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }

    int subtract(int a, int b) {
        return a - b;
    }
};

int main() {
    Calculator calc;
    cout << "Addition: " << calc.add(10, 5) << endl;
    cout << "Subtraction: " << calc.subtract(10, 5) << endl;

    return 0;
}

Explanation:

  • The Calculator class provides public methods add and subtract without exposing the logic or formula directly to the user.
  • Users of the class only need to call these methods without understanding the underlying implementation.

Output:

Addition: 15
Subtraction: 5

2. Abstract Classes with Pure Virtual Functions

An abstract class contains at least one pure virtual function and cannot be instantiated. Pure virtual functions are functions without implementations in the base class, forcing derived classes to implement them.

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0; // Pure virtual function

    void setColor(const string& color) {
        cout << "Shape color set to " << color << endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing Circle" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Drawing Rectangle" << endl;
    }
};

int main() {
    Circle circle;
    Rectangle rectangle;

    circle.draw();
    rectangle.draw();

    circle.setColor("Red"); // Accesses a non-pure function from base class

    return 0;
}

Explanation:

  • Shape is an abstract class with a pure virtual function draw.
  • Circle and Rectangle classes inherit from Shape and provide specific implementations of draw.
  • The abstract class Shape allows each derived class to have its specific drawing logic, providing an abstract interface to the user.

Output:

Drawing Circle
Drawing Rectangle
Shape color set to Red

3. Abstraction with Interfaces

An interface is created by defining a class with only pure virtual functions. Derived classes implement the interface by providing definitions for these functions.

#include <iostream>
using namespace std;

class Printable {
public:
    virtual void print() const = 0; // Pure virtual function
};

class Document : public Printable {
public:
    void print() const override {
        cout << "Printing Document" << endl;
    }
};

class Image : public Printable {
public:
    void print() const override {
        cout << "Printing Image" << endl;
    }
};

int main() {
    Document doc;
    Image img;

    doc.print();
    img.print();

    return 0;
}

Explanation:

  • Printable is an interface with a pure virtual function print.
  • Both Document and Image classes implement the print method in their way.
  • The interface provides a common abstraction for any “printable” object without specifying how each type should be printed.

Output:

Printing Document
Printing Image

4. Real-World Abstraction Example: Payment System

A common real-world abstraction example is a payment system with different payment methods, all implementing a common interface.

#include <iostream>
using namespace std;

class PaymentMethod {
public:
    virtual void pay(double amount) = 0; // Pure virtual function
};

class CreditCard : public PaymentMethod {
public:
    void pay(double amount) override {
        cout << "Paid $" << amount << " with Credit Card." << endl;
    }
};

class PayPal : public PaymentMethod {
public:
    void pay(double amount) override {
        cout << "Paid $" << amount << " with PayPal." << endl;
    }
};

int main() {
    PaymentMethod* payment1 = new CreditCard();
    PaymentMethod* payment2 = new PayPal();

    payment1->pay(100.0);
    payment2->pay(200.0);

    delete payment1;
    delete payment2;

    return 0;
}

Explanation:

  • PaymentMethod is an abstract class with a pure virtual function pay.
  • CreditCard and PayPal are derived classes that implement the pay method differently.
  • The main function uses pointers to the PaymentMethod type, making it possible to select any payment method without knowing its implementation details.

Output:

Paid $100 with Credit Card.
Paid $200 with PayPal.

5. Abstraction with a Factory Pattern

The factory pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects created.

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() = 0;
};

class Dog : public Animal {
public:
    void sound() override {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {
        cout << "Cat meows" << endl;
    }
};

class AnimalFactory {
public:
    static Animal* createAnimal(const string& type) {
        if (type == "dog") return new Dog();
        if (type == "cat") return new Cat();
        return nullptr;
    }
};

int main() {
    Animal* animal1 = AnimalFactory::createAnimal("dog");
    Animal* animal2 = AnimalFactory::createAnimal("cat");

    if (animal1) animal1->sound();
    if (animal2) animal2->sound();

    delete animal1;
    delete animal2;

    return 0;
}

Explanation:

  • AnimalFactory encapsulates the creation logic, allowing abstraction for object creation based on type.
  • The createAnimal method in the AnimalFactory class hides the complexity of object creation, allowing users to get the appropriate Animal object without knowing the exact type.

Output:

Dog barks
Cat meows

6. Abstraction in a Banking System

In a banking system, different types of accounts (like SavingsAccount and CheckingAccount) can inherit from a common BankAccount class, each with its specific behavior.

#include <iostream>
using namespace std;

class BankAccount {
public:
    virtual void calculateInterest() = 0; // Pure virtual function
    virtual void displayAccountType() const = 0;
};

class SavingsAccount : public BankAccount {
public:
    void calculateInterest() override {
        cout << "Interest calculated for Savings Account." << endl;
    }

    void displayAccountType() const override {
        cout << "Savings Account" << endl;
    }
};

class CheckingAccount : public BankAccount {
public:
    void calculateInterest() override {
        cout << "Interest calculated for Checking Account." << endl;
    }

    void displayAccountType() const override {
        cout << "Checking Account" << endl;
    }
};

int main() {
    BankAccount* account1 = new SavingsAccount();
    BankAccount* account2 = new CheckingAccount();

    account1->displayAccountType();
    account1->calculateInterest();

    account2->displayAccountType();
    account2->calculateInterest();

    delete account1;
    delete account2;

    return 0;
}

Explanation:

  • BankAccount is an abstract class that defines an interface for calculating interest and displaying account type.
  • SavingsAccount and CheckingAccount are derived classes that implement specific interest calculation methods.
  • The main function interacts with the abstract base class BankAccount, without needing to know the specific details of SavingsAccount or CheckingAccount.

Output:

Savings Account
Interest calculated for Savings Account.
Checking Account
Interest calculated for Checking Account.

Summary Table of Abstraction Concepts

Abstraction Concept Description Example
Basic Abstraction Hides internal logic using member functions Calculator with add, subtract
Abstract Classes Defines a base class with at least one pure virtual function Shape, BankAccount
Pure Virtual Functions Functions with no implementation, must be overridden by derived class draw in Shape
Interface Class with only pure virtual functions, serving as a contract Printable
Factory Pattern Encapsulates object creation based on type AnimalFactory
Banking System Example Different account types with common calculateInterest method SavingsAccount, CheckingAccount

Complete Example

This example uses both interfaces and a factory pattern to abstract the creation and usage of different devices, each with specific functions.

#include <iostream>
using namespace std;

// Device Interface
class Device {
public:
    virtual void turnOn() = 0;
    virtual void turnOff() = 0;
};

// Concrete classes implementing Device interface
class Television : public Device {
public:
    void turnOn() override {
        cout << "Television turned on." << endl;
    }

    void turnOff() override {
        cout << "Television turned off." << endl;
    }
};

class Radio : public Device {
public:
    void turnOn() override {
        cout << "Radio turned on." << endl;
    }

    void turnOff() override {
        cout << "Radio turned off." << endl;
    }
};

// Device Factory
class DeviceFactory {
public:
    static Device* createDevice(const string& deviceType) {
        if (deviceType == "television") return new Television();
        if (deviceType == "radio") return new Radio();
        return nullptr;
    }
};

int main() {
    Device* tv = DeviceFactory::createDevice("television");
    Device* radio = DeviceFactory::createDevice("radio");

    if (tv) {
        tv->turnOn();
        tv->turnOff();
    }

    if (radio) {
        radio->turnOn();
        radio->turnOff();
    }

    delete tv;
    delete radio;

    return 0;
}

Output:

Television turned on.
Television turned off.
Radio turned on.
Radio turned off.

This example demonstrates abstraction in C++ by using an interface Device, which is implemented by Television and Radio. A factory class DeviceFactory provides an abstracted way to create specific device objects without directly instantiating the classes.

Key Takeaways

  • Abstraction in C++ allows you to hide complexity by defining interfaces and abstract classes.
  • Using abstract classes, pure virtual functions, and design patterns like the factory pattern promotes simplicity and code reusability.
  • By focusing only on the essentials, abstraction helps you interact with objects at a high level, without needing to understand the implementation details.

 

You may also like