Introduction to OOPS Concepts in C++

Object-Oriented Programming binds data and methods in the form of an object and selectively exposes data to other objects. It revolves around the idea of classes and objects, including their definition, instantiation, relationship, communication, and more. In contrast, procedural programming's building block is procedures or functions that perform operations on data.

  • The idea of class and object brings dynamics within code and, most importantly, makes it modular and reusable, unlike procedural programming.
  • OOPS is crucial for large-scale software development as it introduces many features such as Inheritance, Encapsulation, Abstraction, and Polymorphism. This makes it easier to manage complex codebases.

Object-oriented programming vs Procedural programming

Classes in C++

Why is the idea of class essential in OOP? Here is an analogy to understand: The world around us has much more structure than just numbers and strings. There are cars, people, houses, and all kinds of things that are part of the structure involved in real-world design.

So, if we want to solve real-world problems using software, we need to group values in a meaningful fashion to mimic the real world's structure with their attributes and behaviour. The idea of class helps us define such structures and build scalable and reusable software.

  • It allows us to group different types of attributes (data) to represent more complex structures.
  • It allows us to animate the real-world representation through interactions or behaviour (methods).
  • Classes bundle together attributes and methods working on those attributes as a single entity.

Objects in C++

An object is an instance of a class created with specific data, whereas a class is an abstract blueprint used to create more specific, concrete objects. Each object contains data and methods that work on that data.

  • When a class is defined, no memory is allocated, but memory is allocated when an object is created.
  • Objects can communicate with each other by sending messages, without knowing each other's details.

Example code

In the following example, we have defined a Car class with attributes speed and gear, and methods for setting and getting the speed and gear of a car. Inside the main method, we have created two objects of the Car class (car1 and car2) with their own unique speed and gear values.

// Template of the Car Class
class Car {
  private:
    // Attributes of the car
    int speed;
    int gear;
  
  public:
    // Methods working on the Car attributes
    void setSpeed(int s) {
        speed = s;
    }

    void setGear(int g) {
        gear = g;
    }
  
    int getSpeed() {
        return speed;
    }
 
    int getGear() {
        return gear;
    }
};

int main() {
    // Creating an object of the class Car
    Car car1;
    // Calling member methods of the Car Class
    car1.setSpeed(50);
    car1.setGear(3);
    cout << "Speed: " << car1.getSpeed() << endl;
    cout << "Gear: " << car1.getGear() << endl;

    // Creating another object of the Car class
    Car car2;
    // Calling member methods of the Car class
    car2.setSpeed(60);
    car2.setGear(4);
    cout << "Speed: " << car2.getSpeed() << endl;
    cout << "Gear: " << car2.getGear() << endl;

    return 0;
}

Encapsulation in C++

On large-scale software projects, it is crucial to take care of the accessibility and security of our data, and encapsulation helps us achieve this goal. Encapsulation is a technique in C++ to hide implementation details of the object from the outside world and only expose a public interface to interact with the object. For this, we use access modifiers: private, protected, and public.

  • To achieve encapsulation: We declare class variables as private and provide public getter and setter methods to access and modify variable values. So this will ensure that data within an object is accessed and modified in a controlled and consistent way, improving the reliability and security of software.
  • In addition to data, we can also declare some of the internal methods as private. These methods will be only accessed by methods defined inside the class.
  • Encapsulation provides flexibility in code development since the implementation details of an object can be changed without affecting the code of any other objects that use it.
  • Encapsulation help us to achieve abstraction between an object and its users. This eliminate the need for users to know the implementation details to use the object.

Example code

class BankAccount {
private:
    double balance;
    int accountNumber;

public:
    BankAccount(double b, int num) {
        balance = b;
        accountNumber = num;
    }

    void deposit(double amount) {
        balance = balance + amount;
    }

    void withdraw(double amount) {
        if (amount <= balance) {
            balance = balance - amount;
        } else {
            cout << "Insufficient funds." << endl;
        }
    }

    double getBalance() {
        return balance;
    }

    int getAccountNumber() {
        return accountNumber;
    }
};

In the above example, the BankAccount class has encapsulated its data by defining balance and accountNumber private. This means that they cannot be accessed or modified by external code, which provides a layer of protection for sensitive information.

The BankAccount class also provides a public interface for interacting with the class objects through public methods: depositwithdrawgetBalance, and getAccountNumber. These methods are the only ways for external code to access or modify the private variables.

Inheritance in C++

In large-scale software projects that involve millions of lines of code, it is often necessary to reuse existing code or extend functionalities. To accomplish this, we use the concept of inheritance.

Inheritance is a process by which derived classes (subclasses or child classes) inherit attributes and methods of their base class (superclass or parent class). This is also an idea where the base class serves as a template for creating other derived classes.

  • Each derived class can also implement its attributes and methods while still being able to reuse attributes or methods of the parent class. Even if the parent class is modified, all derived classes will inherit the new code. So inheritance enables code reusability and saves time.
  • It is essential for extensibility. After creating one superclass, you can use its features to create several subclasses that inherit some features from their parent class and have a few additional features.

Example code

class Shape {
  public:
    int width;
    int height;
    
    void setWidth(int w) { 
        width = w;
    }
    void setHeight(int h) { 
        height = h;
    }
    int getWidth() { 
        return width;
    }
    int getHeight() { 
        return height;
    }
};

class Rectangle: public Shape {
  public:
    int getArea() { 
        return width * height;
    }
};

class Square: public Shape {
  public:
    void setSide(int s) { 
        width = height = s; 
    }
    
    int getSide() { 
        return width;
    }
    
    int getArea() { 
        return width * width;
    }
};

int main() {
    Rectangle rect;
    rect.setWidth(5);
    rect.setHeight(7);
    cout << "Width: " << rect.getWidth() << endl;
    cout << "Height: " << rect.getHeight() << endl;
    cout << "Area: " << rect.getArea() << endl;

    Square sq;
    sq.setSide(5);
    cout << "Side: " << sq.getSide() << endl;
    cout << "Area: " << sq.getArea() << endl;

    return 0;
}

The Shape class serves as the parent class, while Rectangle and Square are child classes that inherit attributes and methods from the Shape class. Child classes also have their own methods, such as the getArea method in the Rectangle class and the getAreasetSide, and getSide methods in the Square class.

In the main function, we have created objects of the child classes and utilized inherited properties from the base class and unique properties defined in the child classes. The Rectangle class inherits the width and height properties from Shape and calculates its area using them. The Square class also inherits these properties but also has its own setSide and getSide methods to set and retrieve its side length.

Abstraction in C++

Abstraction is a process of providing only the essential or relevant information to the user, so the user only needs to know what the code does, not how it does it. For example, to drive a car, one only needs to know the driving process and not the mechanics of the car engine.

  • Using abstraction, if implementation details of an object or method change in the future, the user's access to it remains the same.
  • Abstraction makes modifications easier as the interface to access the function remains the same. For example, if the type and working of the car engine change, it does not affect the driving process.

Advantages of abstraction:

  • It helps the user avoid writing low-level code.
  • It helps prevent code duplication and increases reusability.
  • It helps change the internal implementation of a class independently without affecting the user.
  • It helps increase the security of an application or program as only essential details are provided to the user.

If we observe, both encapsulation and abstraction work together in the process of designing extensible, modular, and reusable code. But the critical question is: How is encapsulation different from abstraction? The idea is simple: Encapsulation is the process of hiding internal details, and abstraction is the process of exposing relevant details.

Ways to achieve abstraction in C++

  • Via Abstract Classes: An abstract class is a class that cannot be instantiated but can be inherited from. It can contain both concrete and pure virtual functions. Pure virtual functions are functions that have no implementation in the abstract class and must be implemented in derived classes. This allows the user to work with the object without knowing the specific implementation details of the pure virtual functions.
  • Via Interfaces: An interface is similar to an abstract class, but it only contains pure virtual functions. This means that it does not have any implementation details, and the user must use a concrete class that implements the interface to access the functionality.
  • Via Encapsulation: Encapsulation is the process of hiding the implementation details of an object or function and only exposing its interface or functionality to the user. This is achieved by making the data members of a class private and providing public member functions to access and modify the data.
  • Via Function Templates: Function templates allow you to write a single function that can work with different data types. The implementation details of the function are hidden from the user, who only needs to know the function's interface.

Example code

class Shape {
  public:
    virtual double area() = 0;  // pure virtual function
};

class Rectangle: public Shape {
  private:
    double width, height;
  public:
    Rectangle(double w, double h) {
        width = w;
        height = h;
    }
    double area() {
        return width * height;
    }
};

class Circle: public Shape {
  private:
    double radius;
  public:
    Circle(double r) {
        radius = r;
    }
    double area() {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Shape *shape1 = new Rectangle(10, 5);
    Shape *shape2 = new Circle(2);
    cout << "Area of rectangle: " << shape1->area() << endl;
    cout << "Area of circle: " << shape2->area() << endl;
    return 0;
}

In this example, Shape class is an abstract class and Rectangle and Circle classes are concrete classes that inherit from Shape class. The area() function is a pure virtual function in Shape class, which means it needs to be implemented in derived classes. Using the object of the shape class, user can use the area() function without knowing how it's implemented.

Polymorphism in C++

Polymorphism is the ability of objects of different classes to be treated as if they are objects of a common parent class. This allows the same method name to be used for different purposes in different classes, with the behaviour of the method determined by the class of the object on which it is called.

In simple words, Polymorphism in OOP presents the common interface to perform a single action in different ways and presents the common interface to access similar structures.

  • It is crucial for building modular and extensible applications.
  • Instead of complex conditional statements describing the different courses of action, we can create interchangeable objects that we select based on our needs. So one benefit of polymorphism is: Code working with the different classes does not need to know which class it uses.

There are two types of polymorphism in C++: Compile-time polymorphism (also known as static polymorphism) and Runtime polymorphism (also known as dynamic polymorphism).

Compile-time polymorphism

We can achieve compile-time polymorphism using method overloading and operator overloading.

In method overloading, multiple methods can have the same name but different parameter lists. The appropriate method call is determined at compile time based on the number and types of arguments passed to it. For example, the parameters list of a function myfun(int a, int b) is (int, float) which is different from the function myfun(float a, int b) parameter list (float, int).

In the following example, we have a class Shape that has two methods with the same name draw, but with different parameters. The first method takes a single string parameter color, and the second method takes two parameters: color and width. When we call the first method, it will print "Drawing a shape with color red" and when we call the second method, it will print "Drawing a shape with color blue and width 10".

class Shape {
  public:
    void draw(string color) {
        cout << "Drawing a shape with color " << color << endl;
    }
    void draw(string color, int width) {
        cout << "Drawing a shape with color " << color << " and width " << width << endl;
    }
};

int main() {
    Shape s;
    s.draw("red");
    s.draw("blue", 10);
    return 0;
}

Similarly, in operator overloading, operators such as +, -, *, and / can be overloaded to work with user-defined data types. For understanding operator overloading, you can explore this blog: Operator overloading in C++

Runtime polymorphism

Runtime polymorphism allows objects of different classes to be used interchangeably. We can achieve it through inheritance and abstract class. Here we define base class as an abstract class and derived classes inherit from it. These derived classes can have their own unique methods and properties, but they can still be treated as objects of the base class.

In other words: In runtime polymorphism, a base class pointer can point to an object of a derived class, and the appropriate function to call is determined at runtime based on the type of object being pointed to. This allows for the dynamic dispatch of functions and for different behaviour to be exhibited by objects of the same class hierarchy.

For example, suppose we have a game where different types of characters can be controlled by the player. Here each character class has its own unique abilities but they all share some common properties.

class Character {
  public:
    int health;
    int attack;
    int defense;
    virtual void attackEnemy() = 0;
    virtual void takeDamage(int damage) = 0;
};

class Warrior: public Character {
  public:
    void attackEnemy() {
        // warrior attack code
    }
    void takeDamage(int damage) {
        // warrior take damage code
    }
};

class Mage: public Character {
  public:
    void attackEnemy() {
        // mage attack code
    }
    void takeDamage(int damage) {
        // mage take damage code
    }
};

class Thief: public Character {
  public:
    void attackEnemy() {
        // thief attack code
    }
    void takeDamage(int damage) {
        // thief take damage code
    }
};

//Inside the main method
Character *playerCharacter = new Warrior();
playerCharacter->attackEnemy();

Character *enemyCharacter = new Thief();
enemyCharacter->takeDamage(10);

Instead of writing separate code to handle each character class, we can create a base class called Character and other classes (Warrior, Mage and Thief) inherit from it. This way, all the characters can be treated as objects of the Character class and we can write code that works with any character without having to know the specific class it belongs to.

This allows for more flexibility and maintainability in the code. For example, if we want to add a new character class or change the behaviour of an existing class, we do not have to change the code that works with characters, because it is not dependent on the specific class.

Advantages of Object Oriented Programming

  • Makes code easy to maintain and modify: In OOPS, each object has its own data and behavior, and the interactions between objects are defined through methods. This modular approach makes it easier to identify and make changes to specific parts of the code without affecting the entire system.
  • Helps us write reusable code: OOP promotes code reuse through the use of classes and objects. Classes can be used as templates for creating new objects, and objects can be used in multiple parts of the code. This reduces the amount of duplicate code and makes it easier to add new features to the system.
  • Provide data security and control data accessibility: OOP allows for the encapsulation of data through the use of private and protected access modifiers. This means that the data can only be accessed and modified through the class's methods, providing a level of security and control over the data.
  • Provide a mechanism to support high code cohesion: Code cohesion refers to the degree to which the elements of a module belong together. In OOP, classes and objects provide a natural way to group related data and behavior together, resulting in high code cohesion.
  • Provide a mechanism to minimize code coupling or dependency: Code coupling refers to the degree to which one module depends on another. OOP promotes the use of encapsulation, inheritance, and polymorphism to minimize code coupling and dependency between modules.
  • Improve code readability and ease in documentation: OOP makes the code more readable and easier to understand by breaking it down into smaller, more manageable pieces. Additionally, the use of class and object diagrams, and commenting can make the code more self-documenting.
  • Simplify large-scale software development: OOP allows for the development of large-scale software systems by breaking them down into smaller, more manageable pieces. This makes it easier to identify and correct bugs, and to add new features to the system.

Enjoy learning, Enjoy OOPS!

Share Feedback

Coding Interview

Machine Learning

System Design

EnjoyAlgorithms Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.

Explore More Content

Follow us on

©2023 Code Algorithms Pvt. Ltd.

All rights reserved.