Home

 › 

Articles

 › 

Understanding Pure Virtual Functions in C++, With Examples

C++ vs. JavaScript

Understanding Pure Virtual Functions in C++, With Examples

Key Points

  • Pure virtual functions encourage encapsulation by hiding the implementation in derived classes from the base class and external programs.
  • Pure virtual functions enable polymorphism by providing a common interface and allowing dynamic binding to select the desired implementation at runtime.
  • The main differences between virtual functions and pure virtual functions include the requirement for implementation in derived classes, the classification of the base class as abstract, and the ability to instantiate the base class containing virtual functions.

Virtual functions are a key concept in object-oriented programming (OOP) languages, such as Java and C++. At a high level, virtual functions help to enforce the principles of encapsulation and polymorphism, but there are distinct differences between virtual functions and pure virtual functions. In this article, we’re going to explore what pure virtual functions are and how they’re defined, as well as how they differ from non-pure virtual functions.

What Are Pure Virtual Functions in C++?

Pure virtual functions are functions that are defined in the base class but have no implementation details. The base class is known as an abstract class if it contains at least one pure virtual function. This is because the class can’t be instantiated by itself, so it serves as a guide. In this way, pure virtual functions are designed to be inherited by derived classes, which should provide their own specific implementations. Otherwise, the derived classes will also become abstract classes themselves.

Encapsulation is encouraged by pure virtual functions because the implementation in derived classes is hidden from the base class and any external programs accessing it. Likewise, we can access the details of the base class without knowing the details of the derived classes. Using pure virtual functions also enables polymorphism through two main mechanisms. The first is by providing a common interface, i.e., objects of derived classes can be accessed through the same base class reference. The second is through dynamic binding, where the desired implementation is selected at runtime based on the object type. This allows us to execute specific methods and treat different objects as if they’re of the same type.

What Are the Differences Between Virtual Functions and Pure Virtual Functions?

Although pure and non-pure virtual functions can achieve many of the same goals, they’re not the same thing. The main differences between the two are given in the table.

Pure Virtual FunctionNon-Pure Virtual Function
A pure virtual function is declared within the base class, with no implementation. This must be provided in derived classes.A standard virtual function can be overridden by derived classes but also has an implementation provided in the base class.
The pure virtual function must be redefined within any derived class.The derived classes may redefine the virtual function, but have the option not to.
The base class containing the pure virtual function is classified as an abstract class.The base class containing the virtual function isn’t abstract.
The abstract base class can’t be instantiated.The base class the virtual function belongs to can be instantiated.

Overall, we use pure virtual functions when we want to enforce a strict contract that derived classes must override the function, and when we desire an abstract base class.

How are Pure Virtual Functions Used?

Now that we’ve explained what makes a virtual function pure, it’s time to take a look at how they’re used in C++. To declare them, we add “= 0” to the definition of the function. This indicates that implementations must be provided in derived classes. We don’t have to use the “override” keyword in the derived class when working with either kind of virtual function. But generally, it’s good practice. This is because it lets the compiler know what our intention is. Therefore, we’ll be notified via a compilation error if there’s a mistake in the function signature.

To get a clearer idea of how these functions work in action, consider the following code:

#include <iostream>

class Employee {
public:
    virtual void displayInfo() const = 0;
};

class FullTimeEmployee : public Employee {
private:
    std::string name;
    double salary;

public:
    FullTimeEmployee(const std::string& n, double s) : name(n), salary(s) {}

    void displayInfo() const override {
        std::cout << "Name: " << name << ", Salary: $" << salary << std::endl;
    }
};

class ContractEmployee : public Employee {
private:
    std::string name;
    double hourlyRate;
    int hoursWorked;

public:
    ContractEmployee(const std::string& n, double hr, int hw) : name(n), hourlyRate(hr), hoursWorked(hw) {}

    void displayInfo() const override {
        std::cout << "Name: " << name << ", Hourly Rate: $" << hourlyRate << ", Hours Worked: " << hoursWorked << std::endl;
    }
};

int main() {
    FullTimeEmployee fullTimeEmployee("John Doe", 5000.0);
    ContractEmployee contractEmployee("Jane Smith", 25.0, 160);

    fullTimeEmployee.displayInfo();
    contractEmployee.displayInfo();

    return 0;
}

Explanation of Code

Let’s break this down. First, we have the base “Employee” class, containing the pure virtual function “displayInfo()”, as indicated by “= 0”. Therefore, we know the derived classes must provide their own implementation for this function.

We then declare two derived classes, “FullTimeEmployee” and “ContractEmployee”. These both inherit the displayInfo method but provide their own details for implementation. For the FullTimeEmployee class, the method is used to display the name of the employee as a string, and the salary as a data type known as a double (a number with decimal precision). On the other hand, the ContractEmployee class is used to display the information of contract employees. This includes their name, hourly rate, and number of hours worked (the latter is displayed as the integer type). Both classes contain a constructor declaration, which takes the relevant parameters for the employees as arguments. The use of the override keyword can be seen here.

Lastly, the “main()” function is declared, which shows the starting point for the program. Objects of each derived class are created, and the displayInfo function is called. The class-appropriate implementations are executed, and we can see the employee information displayed in the output in the image.

example of pure virtual function in C++.
Pure virtual functions are intended to be overridden by derived classes.

Using Virtual Destructors and Base Class Pointers

We didn’t use a base class pointer in the previous example, but directly created objects of the derived classes instead. If you anticipate the need to allocate memory dynamically or create objects but maintain uniform treatment, base class pointers can be useful. Similarly, if memory and resources are allocated and you need a specific cleanup method, virtual destructors help delete objects correctly. Continuing with the same example, here is some modified code to show the use of base class pointers and virtual destructors.

#include <iostream>

class Employee {
public:
    virtual ~Employee() {}

    virtual void displayInfo() const = 0;
};

class FullTimeEmployee : public Employee {
private:
    std::string name;
    double salary;

public:
    FullTimeEmployee(const std::string& n, double s) : name(n), salary(s) {}

    void displayInfo() const override {
        std::cout << "Name: " << name << ", Salary: $" << salary << std::endl;
    }

    ~FullTimeEmployee() {
        std::cout << "FullTimeEmployee destructor." << std::endl;
    }
};

class ContractEmployee : public Employee {
private:
    std::string name;
    double hourlyRate;
    int hoursWorked;

public:
    ContractEmployee(const std::string& n, double hr, int hw) : name(n), hourlyRate(hr), hoursWorked(hw) {}

    void displayInfo() const override {
        std::cout << "Name: " << name << ", Hourly Rate: $" << hourlyRate << ", Hours Worked: " << hoursWorked << std::endl;
    }

    ~ContractEmployee() {
        std::cout << "ContractEmployee destructor." << std::endl;
    }
};

int main() {
    Employee* empPtr1 = new FullTimeEmployee("John Doe", 5000.0);
    Employee* empPtr2 = new ContractEmployee("Jane Smith", 25.0, 160);

    empPtr1->displayInfo();
    empPtr2->displayInfo();

    delete empPtr1;
    delete empPtr2;

    return 0;
}

Explanation of Code

We can see the first difference is the inclusion of the virtual destructor, “~Employee”, in the base class. In addition, each derived class has its own destructor, namely “~FullTimeEmployee” and “~ContractEmployee”. The last change is in the definition of the “main()” function. We use the base class pointers, “Employee* empPtr1” and “Employee* empPtr2” to create class objects, and call the “displayInfo()” function through these pointers. These objects are then deleted by using the “delete” keyword. The order of this deletion is shown in the output.

example of virtual destructor and base class pointers in C++.
Virtual destructors and base class pointers aren’t mandatory, but are often used with pure virtual functions.

Wrapping Up

In conclusion, pure virtual functions in C++ are useful in creating abstract base classes, and enforcing that derived classes provide their own specific implementations for the function. While both non-pure and pure virtual functions help to enable encapsulation and polymorphism, the class containing non-pure virtual functions can be instantiated. As such, overriding the function in the derived class is optional. By letting objects of different classes be treated consistently (through dynamic binding), we can work with a common interface and write more flexible, extendable, and reusable code. While it isn’t necessary to use base class pointers or virtual destructors with pure virtual functions, this is advised in situations where resources are allocated dynamically and proper deletion of class objects is desired.

Frequently Asked Questions

What is a pure virtual function?

A pure virtual function is one that’s declared in a base class using “= 0”, but has no implementation. The base class is known as an abstract class, and implementation must be provided for this function in the derived classes.

What are pure virtual functions used for?

Pure virtual functions allow us to treat different object types as if they’re of the same type using appropriate implementations, i.e., runtime polymorphism.

Can classes have both virtual functions and pure virtual functions?

Yes, classes can have both kinds of functions, but the pure virtual functions must be overridden in a derived class.

How do pure virtual functions enable encapsulation and polymorphism?

Pure virtual functions allow polymorphism by providing a common interface that must be implemented by derived classes, and ensuring derived class objects are treated consistently through dynamic binding. Because these functions allow us to define derived classes with their own implementation, these details are hidden from external programs. Similarly, clients using the base class don’t have access to the derived class details unless required, so the data is encapsulated.

What is dynamic binding?

This is a mechanism by which the program can select the suitable implementation of a function at runtime based on object type. When the function is called through a base class pointer or reference, the appropriate implementation method from the derived class is used. Through dynamic binding, you can maintain a common interface, and make your code more reusable and flexible.

What are virtual destructors?

Virtual destructors are defined in the base class and used to delete derived class objects through a base class pointer. This ensures proper deletion of class objects, particularly when resources have been allocated dynamically.

What are base class pointers?

Base class pointers are declared within the base class but retain the memory address of derived class objects, especially when we need to create objects and allocate memory dynamically. These pointers allow us to treat objects uniformly and use the appropriate implementation for each object.

To top