Table of contents
1.
Introduction
2.
What is Object-Oriented Programming?
3.
Object-Oriented Programming in C++
4.
Encapsulation (Classes)
5.
Inheritance
6.
Polymorphism
7.
Virtual Table (vtbl) and Virtual Pointer (vptr)
7.1.
1. Virtual Table (vtbl)
7.2.
2. Virtual Pointer (vptr)
8.
Setting the vptr in the Constructor
9.
Inheriting the vtbl and Overriding the vptr in the Subclasses
9.1.
Virtual Call (Late Binding)
10.
Examples of Using Virtual Functions
10.1.
1. Shape Example
10.2.
2. Animal Example
11.
Frequently Asked Questions 
11.1.
What is the difference between a class and an object in C++?
11.2.
Can a derived class access private members of its base class?
11.3.
What is the purpose of virtual destructors in C++?
12.
Conclusion
Last Updated: Nov 29, 2024
Easy

Object Oriented Programming in C

Author Gaurav Gandhi
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Object-oriented programming (OOP) is a programming paradigm that organizes software design around objects, which are instances of classes that encapsulate data and behavior. In OOP, programs are structured as collections of interacting objects, each with its own properties (attributes) & methods (functions). This approach allows for code reusability, modularity, and abstraction, which makes it easier to develop, maintain, and extend complex software systems. C++ is a powerful language that supports OOP concepts like encapsulation, inheritance, and polymorphism. 

Object Oriented Programming in C

In this article, we will discuss the basics of OOP in C++, such as classes, inheritance, virtual functions, and more.

What is Object-Oriented Programming?

Object-oriented programming (OOP) is a programming paradigm that organizes software design around objects, which are instances of classes. In OOP, a class is a blueprint or template that defines the properties (data) and behaviors (methods) of a particular type of object. Objects are created from classes and interact with each other to perform tasks and solve problems.

The main principles of OOP are:
 

1. Encapsulation: Encapsulation is the mechanism of bundling data & functions that operate on that data within a single unit, i.e., a class. It hides the internal details of an object & provides a public interface for interacting with the object.
 

2. Inheritance: Inheritance allows the creation of new classes based on existing ones. The new class, known as the derived or child class, inherits the properties and methods of the existing class, called the base or parent class. Inheritance promotes code reuse and allows for the creation of specialized classes from more general ones.
 

3. Polymorphism: Polymorphism means "many forms" and refers to the ability of objects to take on multiple forms. In OOP, polymorphism allows objects of different classes to be treated as objects of a common parent class. This is achieved through function overloading, operator overloading, and virtual functions.

Object-Oriented Programming in C++

C++ is a powerful language that fully supports object-oriented programming concepts. It provides mechanisms for creating classes, objects, inheritance, polymorphism & more. Let's look at a simple example of a class in C++:

class Rectangle {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) {
        length = l;
        width = w;
    }

    double area() {
        return length * width;
    }
};


In this example, we define a `Rectangle` class with private data members `length` and `width`, a constructor that initializes these data members, and a public member function `area()` that calculates the rectangle's area.


To create an object of the `Rectangle` class & use its member function, we can do the following:

Rectangle rect(5.0, 3.0);
double rectArea = rect.area();


Here, we create a `Rectangle` object named `rect` with a length of 5.0 and a width of 3.0. We then call the `area()` function on `rect` and store the result in the `rectArea` variable.

C++ also supports other OOP features like constructor overloading, destructors, operator overloading, friend functions, and more. These features provide powerful tools for creating complex and flexible object-oriented designs.

Encapsulation (Classes)

Encapsulation is a fundamental concept in OOP that involves bundling data and functions that operate on that data within a single unit, i.e., a class. It is a mechanism for hiding the internal details of an object and providing a public interface for interacting with it.

In C++, encapsulation is achieved through the use of access specifiers: `private`, `protected`, and `public`. These access specifiers determine the accessibility of class members (data and functions) from outside the class.

- `private` members are accessible only within the class itself. They cannot be accessed from outside the class, even by objects of the same class.
 

- `protected` members are accessible within the class & its derived classes. They cannot be accessed from outside the class hierarchy.
 

- `public` members are accessible from anywhere, including outside the class.
 

For example: 

class BankAccount {
private:
    double balance;

public:
    BankAccount(double initial_balance) {
        balance = initial_balance;
    }


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

    void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
        }
    }

    double getBalance() {
        return balance;
    }
};


In this example, the `BankAccount` class encapsulates the `balance` data member as a private member. The public member functions `deposit()`, `withdraw()`, & `getBalance()` provide an interface for interacting with the `balance` member. This encapsulation ensures that the `balance` can only be modified through the provided member functions, maintaining the integrity of the object's state.

Inheritance

Inheritance is a key concept in OOP that allows the creation of new classes based on existing classes. The existing class is called the base class or parent class, while the new class is called the derived class or child class. The derived class inherits the data members and member functions of the base class, allowing for code reuse and specialization.

In C++, inheritance is declared using the colon (`:`) syntax followed by an access specifier (`public`, `protected`, or `private`) & the name of the base class. 

For example:

class Animal {
protected:
    std::string name;
public:
    Animal(std::string n) {
        name = n;
    }

    void eat() {
        std::cout << name << " is eating." << std::endl;
    }
};

class Dog : public Animal {
public:
    Dog(std::string n) : Animal(n) {}


    void bark() {
        std::cout << name << " is barking." << std::endl;
    }
};


In this example, the `Animal` class is the base class, and the `Dog` class is the derived class. The `Dog` class inherits the `name` data member and the `eat()` function from the `Animal` class. It also defines its own `bark()` function.

The access specifier used in the inheritance declaration determines the accessibility of the base class members in the derived class:

- `public` inheritance: public members of the base class become public members of the derived class, & protected members of the base class become protected members of the derived class.

- `protected` inheritance: public & protected members of the base class become protected members of the derived class.

- `private` inheritance: public & protected members of the base class become private members of the derived class.

Note: Inheritance lets you arrange classes in a system where specific classes come from more general ones. This setup helps keep code tidy, makes it reusable, and allows it to be easily extended.

Polymorphism

Polymorphism is another fundamental concept in OOP that allows objects of different classes to be treated as objects of a common base class. It enables the use of a single interface to represent multiple related classes, providing flexibility & extensibility in software design.

C++ supports two types of polymorphism:

1. Compile-time polymorphism (static polymorphism): This includes function overloading & operator overloading, where multiple functions or operators with the same name can be defined with different parameters.
 

2. Run-time polymorphism (dynamic polymorphism): This is achieved through virtual functions, which allow derived classes to provide their own implementations of functions declared in the base class.


For example: 

 

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


class Rectangle : public Shape {
private:
    double length;
    double width;


public:
    Rectangle(double l, double w) : length(l), width(w) {}


    double area() override {
        return length * width;
    }
};


class Circle : public Shape {
private:
    double radius;


public:
    Circle(double r) : radius(r) {}


    double area() override {
        return 3.14159 * radius * radius;
    }
};


In this example, the `Shape` class is an abstract base class that declares a pure virtual function, `area()`. The `Rectangle` and `Circle` classes inherit from `Shape` and provide their own implementations of the `area()` function.

Polymorphism allows us to create pointers or references to the base class (`Shape`) & use them to call the appropriate derived class implementation of the virtual function at run-time. For example:

Shape* shape1 = new Rectangle(4.0, 5.0);
Shape* shape2 = new Circle(3.0);


std::cout << "Area of shape1: " << shape1->area() << std::endl;
std::cout << "Area of shape2: " << shape2->area() << std::endl;


This code will output the correct area for each shape object, eventually showing run-time polymorphism.

Virtual Table (vtbl) and Virtual Pointer (vptr)

In C++, virtual functions are implemented using a mechanism called the virtual table (vtbl) and virtual pointer (vptr). Let's understand how they work:

1. Virtual Table (vtbl)

   - The virtual table is a static array that contains pointers to the virtual functions of a class.
 

   - Each class with virtual functions has its own virtual table.
 

   - The virtual table is created by the compiler at compile-time and exists for the lifetime of the program.
 

   - The virtual table contains entries for each virtual function declared in the class, including inherited virtual functions.

2. Virtual Pointer (vptr)

   - The virtual pointer is a hidden member added by the compiler to every object of a class that has virtual functions.
 

   - It is a pointer to the virtual table of the class.
 

   - The vptr is initialized in the constructor of the class to point to the virtual table of the class.


For example: 

class Base {
public:
    virtual void func1() { std::cout << "Base::func1()" << std::endl; }
    virtual void func2() { std::cout << "Base::func2()" << std::endl; }
};

class Derived : public Base {
public:
    void func1() override { std::cout << "Derived::func1()" << std::endl; }
};


In this example, the `Base` class has two virtual functions, `func1()` and `func2()`. The `Derived` class overrides `func1()`.

The compiler generates the following virtual tables:

Base vtbl:
  &Base::func1()
  &Base::func2()


Derived vtbl:
  &Derived::func1()
  &Base::func2()


Each object of the `Base` and `Derived` classes will have a vptr that points to their respective virtual tables.

Note: When a virtual function is called through a pointer or reference to the base class, the program follows the vptr to the virtual table and calls the appropriate function based on the actual type of the object.

Setting the vptr in the Constructor

In C++, the virtual pointer (vptr) is initialized in the constructor of a class. The compiler automatically inserts code in the constructor to set the vptr to point to the virtual table of the class. This ensures that the vptr is properly initialized when an object is created.


For example: 

class Base {
public:
    Base() { std::cout << "Base constructor" << std::endl; }
    virtual void func() { std::cout << "Base::func()" << std::endl; }
};


class Derived : public Base {
public:
    Derived() { std::cout << "Derived constructor" << std::endl; }
    void func() override { std::cout << "Derived::func()" << std::endl; }
};


When an object of the `Derived` class is created, the following steps occur:
 

1. Memory is allocated for the `Derived` object.
 

2. The `Base` constructor is called first, as it is the base class constructor.
 

   - The `Base` constructor sets the vptr of the `Derived` object to point to the `Base` virtual table.
 

   - The `Base` constructor executes its body, printing "Base constructor".
 

3. The `Derived` constructor is called next.
 

   - The `Derived` constructor sets the vptr of the `Derived` object to point to the `Derived` virtual table.
 

   - The `Derived` constructor executes its body, printing "Derived constructor".
 

After the constructors have executed, the `Derived` object's vptr points to the `Derived` virtual table, which contains the overridden `func()` function.

Derived d;
d.func();  // Output: Derived::func()


In this example, when `d.func()` is called, the program follows the vptr to the `Derived` virtual table and invokes the `Derived::func()` function.

Note: Setting the vptr in the constructor ensures that virtual function calls resolve to the correct overridden functions based on the actual type of the object, even when accessed through pointers or references to the base class.

Inheriting the vtbl and Overriding the vptr in the Subclasses

In C++, when a class is derived from a base class that has virtual functions, the derived class inherits the virtual table (vtbl) from the base class. However, if the derived class overrides any of the virtual functions, the corresponding entries in the vtbl are updated to point to the overridden functions.

Let's consider an example to understand how the vtbl is inherited and the vptr is overridden in subclasses:

class Base {
public:
    virtual void func1() { std::cout << "Base::func1()" << std::endl; }
    virtual void func2() { std::cout << "Base::func2()" << std::endl; }
};

class Derived : public Base {
public:
    void func1() override { std::cout << "Derived::func1()" << std::endl; }
    virtual void func3() { std::cout << "Derived::func3()" << std::endl; }
};


In this example, the `Base` class has two virtual functions, `func1()` and `func2()`. The `Derived` class inherits from `Base` and overrides `func1()`. It also introduces a new virtual function `func3()`.

The vtbl and vptr for these classes will be as:

1. `Base` vtbl:

   - `&Base::func1()`
   - `&Base::func2()`


2. `Derived` vtbl:

   - `&Derived::func1()`
   - `&Base::func2()`
   - `&Derived::func3()`


When an object of the `Derived` class is created, the following steps occur:

1. The `Base` constructor is called, setting the vptr to point to the `Base` vtbl.
 

2. The `Derived` constructor is called, overriding the vptr to point to the `Derived` vtbl.


The `Derived` vtbl contains the overridden `func1()` function from the `Derived` class, the inherited `func2()` function from the `Base` class, and the new `func3()` function introduced in the `Derived` class.

When a virtual function is called on a `Derived` object, the program follows the vptr to the `Derived` vtbl and invokes the appropriate function based on the vtbl entries.

Derived d;
d.func1();  // Output: Derived::func1()
d.func2();  // Output: Base::func2()
d.func3();  // Output: Derived::func3()


Note: Inheriting the vtbl and overriding the vptr in subclasses lets dynamic dispatch happen, where the correct function is called based on the actual type of the object, even when accessed through pointers or references to the base class.

Virtual Call (Late Binding)

In C++, when a virtual function is called through a pointer or reference to the base class but the actual object is of a derived class type, it is known as a virtual call or late binding. The term "late binding" refers to the fact that the decision of which function to call is made at runtime based on the actual type of the object rather than at compile time.

For example: 

class Base {
public:
    virtual void func() { std::cout << "Base::func()" << std::endl; }
};

class Derived : public Base {
public:
    void func() override { std::cout << "Derived::func()" << std::endl; }
};

void callFunc(Base* ptr) {
    ptr->func();  // Virtual call
}


In this example, the `Base` class has a virtual function `func()`, and the `Derived` class overrides it. The `callFunc()` function takes a pointer to the `Base` class as an argument.

Now, let's see how virtual calls work:

Base* ptr1 = new Base();
Base* ptr2 = new Derived();

callFunc(ptr1);  // Output: Base::func()
callFunc(ptr2);  // Output: Derived::func()


When `callFunc(ptr1)` is called, `ptr1` points to an object of the `Base` class. The virtual call `ptr->func()` inside the `callFunc()` function resolves to `Base::func()` because the actual object is of type `Base`.

On the other hand, when `callFunc(ptr2)` is called, `ptr2` points to an object of the `Derived` class. Even though `ptr2` is of type `Base*`, the virtual call `ptr->func()` inside the `callFunc()` function resolves to `Derived::func()` because the actual object is of type `Derived`.

The virtual call mechanism relies on the virtual table (vtbl) and virtual pointer (vptr) to determine the correct function to call at runtime. When a virtual function is called through a pointer or reference, the program follows the vptr to the vtbl of the actual object and invokes the corresponding function from the vtbl.

Note: Virtual calls allow for polymorphic behavior, where the same function call can result in different actions depending on the actual type of the object. This is a powerful feature of object-oriented programming that enables runtime polymorphism and extensibility.

Examples of Using Virtual Functions

Virtual functions are commonly used in C++ to achieve runtime polymorphism and to create extensible and modular code. Let’s discuss few of a examples that shows the use of virtual functions:

1. Shape Example

class Shape {
public:
    virtual double area() = 0;
    virtual void display() { std::cout << "Shape" << std::endl; }
};


class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() override { return 3.14159 * radius * radius; }
    void display() override { std::cout << "Circle" << std::endl; }
};


class Rectangle : public Shape {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() override { return length * width; }
    void display() override { std::cout << "Rectangle" << std::endl; }
};


void printArea(Shape* shape) {
    std::cout << "Area: " << shape->area() << std::endl;
    shape->display();
}


int main() {
    Shape* shapes[2];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);


    for (int i = 0; i < 2; i++) {
        printArea(shapes[i]);
    }


    return 0;
}
You can also try this code with Online C Compiler
Run Code


Output

Area: 78.5397
Circle
Area: 24
Rectangle

 

In this example, the `Shape` class is an abstract base class with a pure virtual function `area()` and a virtual function `display()`. The `Circle` and `Rectangle` classes inherit from `Shape` and provide their own implementations of `area()` and `display()`. The `printArea()` function takes a pointer to the `Shape` class and calls the virtual functions `area()` and `display()`, which are resolved based on the actual object type at runtime.

2. Animal Example

class Animal {
public:
    virtual void makeSound() { std::cout << "Animal sound" << std::endl; }
};


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


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


int main() {
    Animal* animals[2];
    animals[0] = new Dog();
    animals[1] = new Cat();


    for (int i = 0; i < 2; i++) {
        animals[i]->makeSound();
    }


    return 0;
}
You can also try this code with Online C Compiler
Run Code


Output

Dog barks
Cat meows


In this example, the `Animal` class has a virtual function `makeSound()`. The `Dog` and `Cat` classes inherit from `Animal` and override the `makeSound()` function with their specific implementations. The `main()` function creates an array of `Animal` pointers and calls the `makeSound()` function on each object, which is resolved based on the actual object type at runtime.

Frequently Asked Questions 

What is the difference between a class and an object in C++?

A class is a blueprint or template that defines the properties and behavior of objects, while an object is an instance of a class created at runtime.

Can a derived class access private members of its base class?

No, a derived class cannot directly access the private members of its base class. However, it can access them indirectly through public or protected member functions of the base class.

What is the purpose of virtual destructors in C++?

Virtual destructors ensure that the proper destructor is called for objects of derived classes when they are deleted through a pointer or reference to the base class, preventing memory leaks.

Conclusion

In this article, we discussed the basics of object-oriented programming in C++, like classes, encapsulation, inheritance, and polymorphism. We looked into the inner workings of virtual functions, the virtual table (vtbl), and the virtual pointer (vptr). With the help of examples, we showed how these concepts are applied in C++ to create modular, extensible, and efficient code. 

You can also check out our other blogs on Code360.

Live masterclass