Introduction to OOPS Concepts in C++

C++ is a powerful programming language that supports the features of object oriented programming. It is widely used in game development, embedded systems, high-performance computing, etc. On the other side, many popular C++ libraries and frameworks are developed using the idea of OOPS concepts.

In this blog, we will explore brief highlights of fundamental OOPS concepts in C++: Classes and Objects, Encapsulation, Abstraction, Inheritance and Polymorphism.

Class and Object

A class is a fundamental building block of OOPS in C++. It is a user-defined data type that encapsulates data and methods into a single unit and serves as a blueprint for creating objects. On the other side, an object is an instance of a class created with specific data. So each object contains data and methods that work on that data.

  • When a class is defined, no memory is allocated.
  • Memory is allocated when an object is created.

The critical question is: Why is the idea of class and object important in OOPS? Here is an analogy to understand: The world around us has a much more complex structure than just numbers or strings. There are cars, people, houses, and all kinds of things that are part of the real-world design. So if we want to develop real-life software applications, we need to group attributes, behaviour and relationship of real-world objects in a meaningful fashion. So, the idea of class and object helps us define such structures in real-life software.

Using classes and objects:

  • We can group different types of attributes to represent complex structures.
  • We can animate the real-world representation through interactions or behaviour.

In C++, we define a class using the class keyword followed by the name of the class.

class ClassName {
    // Member variables
    // Member functions
};

Example code to understand class and object

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

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;
}

Now, the critical question is: What is the meaning of "private" and "public" in the above code? Why do we use methods like getSpeed() and setSpeed() to access and modify data members? To know the answer, we need to understand the idea of encapsulation.

Encapsulation

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 hides the implementation details of the object from the outside world and only exposes a public interface to interact with the object. So it will help us to achieve abstraction because users of the object do not need to know the implementation details to use the object.

How do we achieve encapsulation in C++?

To achieve encapsulation, we use access modifiers: private, protected, and public. The private member can only be accessed within the class, while the public members can be accessed from anywhere. The protected member is similar to private, but its scope is only limited to the same class or derived classes.

  • One of the best practices is to declare class variables as private and provide public getter and setter methods to access and modify. This will ensure that data within an object is accessed and modified in a controlled and consistent way.
  • In addition to data, we can also declare some internal methods as private. These methods will be only accessed by methods defined inside the class.

Example code

class BankAccount {
private:
    double balance;
    int accountNumber;
    // Private method
    void printTransaction(double amount, const string& action) {
        cout << "Transaction: " << action << " $" << amount << endl;
    }

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

    void deposit(double amount) {
        balance = balance + amount;
        printTransaction(amount, "Deposit");
    }

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

    double getBalance() {
        return balance;
    }

    int getAccountNumber() {
        return accountNumber;
    }
};

int main() {
    // Create a BankAccount object
    BankAccount account(1000.0, 123456);
    // Perform operations on the account
    account.deposit(500.0);
    account.withdraw(200.0);
    // Display account information
    cout << "Account Number: " << account.getAccountNumber() << endl;
    cout << "Balance: $" << account.getBalance() << endl;

    return 0;
}
  • BankAccount class has encapsulated its data by defining balance and accountNumber private. This means: they cannot be accessed or modified directly by some external code.
  • BankAccount class 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 private data.
  • We have also added a private method printTransaction, which can only be accessed within the class. If we observe, we are calling this method inside the functions deposit() and withdraw() to display a message whenever a deposit or withdrawal occurs.
  • We have defined a constructor BankAccount(double b, int num). A constructor is a special member function of a class that is automatically called when an object of that class is created. We use it to initialize data members or perform any other necessary setup operations. We will discuss the idea of a C++ constructor in a separate blog.

Overall, encapsulation provides flexibility in code development because the implementation details of an object can be changed without affecting the code of any other objects that use it. 

Inheritance

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

Inheritance helps us create new classes based on existing classes. It is a process by which derived classes (subclasses or child classes) inherit attributes and methods of their base class (superclass or parent class).

  • 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.
  • In other words, by using inheritance, we can avoid code duplication for the common characteristics in each derived class. For this, we can simply define them once in the base class and reuse them in all derived classes.
  • We use inheritance to model relationships between different similar classes. This is an idea of "is-a" relationship, where a derived class "is-a" specialized type of the base class.

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;
}

In the above code, Shape is the parent class, while Rectangle and Square are child classes. These two classes inherit attributes and methods from the Shape class. Child classes also have their own methods: getArea() method in the Rectangle class and getArea(), setSide(), and getSide() methods in the Square class.

In the main function, we have created objects of the child classes and used inherited properties from the base class. 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.

Polymorphism

Polymorphism presents the common interface to perform a single action in different ways. There are two types of polymorphism in C++: Compile-time polymorphism (static binding) and Runtime polymorphism (dynamic binding).

Compile-time polymorphism

In C++, 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.

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 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. We can achieve it using inheritance and abstract class.

What is an abstract class? In C++, an abstract class contains at least one pure virtual function and it cannot be instantiated directly. We use it to provide a common blueprint for its derived classes by specifying a set of virtual functions that must be implemented by the derived classes.

What is a pure virtual function? In C++, a pure virtual function is declared by appending = 0 to the function declaration in a base class but it has no implementation. It must be overridden by derived classes.

Now come to the main idea of runtime polymorphism: We define the abstract base class (which contains pure virtual functions) and derived classes inherit from it. Derived class provide the concrete implementation of the virtual functions defined in the base class.

Now we can assign any object of a derived class with the base class pointer. When we call a virtual function using a base class pointer, the appropriate implementation of the virtual function is determined at runtime based on the type of object being pointed to. So this idea will enable different derived classes to have their own implementations of the same function, which can be selected dynamically based on the type of object being referred to.

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 an abstract base class called Character and other classes of specific characters (Warrior, Mage and Thief) inherit from it. This way, all the characters can be treated as objects of the Character class. On the other side, by using an abstract base class, we enforce any derived class representing a character to provide concrete implementations for the pure virtual functions. 

So in this way, different types of characters can have their own unique behaviours and they can be treated uniformly through the abstract base class interface. If we want to add a new character class or change the existing behaviour in a class, we do not need to change the code that works with characters, because it is not dependent on the specific class.

This is a good example of abstraction using the idea of polymorphism and inheritance.

Abstraction

Abstraction is a process of providing only the essential details 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 engine. We can easily replace the existing engine with a new engine, without affecting the driving process.

If we observe, both encapsulation and abstraction work together. 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++

  1. Using Abstract Classes: We use an abstract class as a base class, which can contain both concrete and pure virtual functions. Now we provide an implementation of pure virtual function in all derived classes. This will help us to work with the reference of abstract base class without knowing the implementation details of the pure virtual functions in subclasses.
  2. Using Interfaces: An interface is similar to an abstract class, but it only contains pure virtual functions. This means: the interface does not have any implementation details, and the user must use a concrete class that implements the interface to access the functionality.
  3. Using Encapsulation: As we have seen above, encapsulation is the process of hiding the implementation details of an object. In other words, it helps us to achieve abstraction by exposing relevant functionality to the user. For this, we make the data members and some methods of a class private and provide public methods to access and modify the data.
  4. Using Header Files: We can hide the implementation of the function in header files. In this way, we can use the same function in our program without knowing its implementation details.

Example code

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

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 Recta ngle(10, 5);
    Shape *shape2 = new Circle(2);
    cout << "Area of rectangle: " << shape1->area() << endl;
    cout << "Area of circle: " << shape2->area() << endl;
    return 0;
}

Shape class: It is an abstract base class that defines the common interface for all shapes. It declares a pure virtual function area(), so any derived class must provide an implementation for calculating the area.

Creating derived classes: Rectangle and Circle classes are derived from the Shape class and they provide their own implementation of the area() method.

Using polymorphism: Inside the main, we used polymorphism to create two shape objects: shape1 (object of Rectangle class) and shape2 (object of Circle class). Since Shape is an abstract class, it cannot be instantiated directly. So, we have created pointers to the base class Shape and assigned derived class objects to them.

This will help us call specific implementations of the area() using the Shape class pointer, without being concerned about implementation details in derived classes. This is an idea of abstraction: How the areas are calculated is abstracted away from the client code.

Advantages of Object Oriented Programming

  1. Makes code easy to maintain and modify.
  2. Helps us write reusable code.
  3. Provide data security and control data accessibility.
  4. Provide a mechanism to support high code cohesion.
  5. Provide a mechanism to minimize code coupling or dependency.
  6. Improve code readability and ease in documentation.
  7. Simplify large-scale software development.

Critical questions to think and explore!

  1. What is the purpose of constructors and destructors in a class?
  2. Difference between default constructor, parameterized constructor, and copy constructor.
  3. Type of inheritance in C++
  4. Difference between encapsulation and abstraction.
  5. What is the "diamond problem" in multiple inheritance and how can it be resolved in C++?
  6. In C++, how abstract class is it different from an interface?
  7. Concept of function overloading and function overriding?
  8. Difference between composition and inheritance.
  9. What is object slicing and how can it be prevented?
  10. Difference between composition and aggregation in C++.
  11. Concept of function templates and class templates in C++.
  12. Difference between early binding and late binding.
  13. What is the "this" pointer in C++ and how is it used?
  14. What is the purpose of the "const" keyword in member function declarations?
  15. Explain the concept of friend functions and friend classes in C++.
  16. Difference between a shallow copy and a deep copy?
  17. What is a static member variable and how does it differ from a non-static member variable?
  18. What is a static member function and when would you use it?
  19. Concept of type casting in C++.
  20. What are lambda expressions in C++? How are they used?

In the coming future, we will write separate detailed blogs on several concepts mentioned in this blog. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, oops!

Share Your Insights

☆ 16-week live DSA course
☆ 16-week live ML course
☆ 10-week live DSA course

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

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