Introduction
Inheritance and polymorphism are the most powerful features of Object Oriented Programming Languages. With inheritance and polymorphism, we can achieve code reuse.
There are many tricky ways for implementing polymorphism in C.
The aim of this article is to demonstrate a simple and easy technique of applying inheritance and polymorphism in C. By creating a VTable (virtual Table) and providing proper access between base and derived objects, we can achieve inheritance and polymorphism in C. The concept of VTable can be implemented by maintaining a table of pointers to functions. For providing access between base and derived objects, we have to maintain the references of derived object in the base class and the reference of the base object in the derived class.
Description
Before proceeding to the implementation of inheritance and polymorphism in C, we should know the class representation in C.
Class representation in C
Consider a class “Person
” in C++.
class Person
{
private:
char* pFirstName;
char* pLastName;
public:
Person(const char* pFirstName, const char* pLastName); ~Person();
void displayInfo();
void writeToFile(const char* pFileName);
};
For representing the above class in C, we can use structures, and functions which operate on that structure as member functions.
typedef struct _Person
{
char* pFirstName;
char* pLastName;
}Person;
new_Person(const char* const pFirstName, const char* const pLastName); delete_Person(Person* const pPersonObj);
void Person_DisplayInfo(Person* const pPersonObj);
void Person_WriteToFile(Person* const pPersonObj, const char* const pFileName);
Here, the functions defined for the structure Person
are not encapsulated. For implementing encapsulation, which is binding between data and functions, pointers to functions are used. We need to create a table of function pointers. The constructor new_Person()
will set the values of the function pointers to point to the appropriate functions. This table of function pointers will then act as an interface for accessing the functions through the object.
Let us redefine the C implementation of the class Person
.
typedef struct _Person Person;
typedef void (*fptrDisplayInfo)(Person*);
typedef void (*fptrWriteToFile)( Person*, const char*);
typedef void (*fptrDelete)( Person *) ;
typedef struct _Person
{
char* pFName;
char* pLName;
fptrDisplayInfo Display;
fptrWriteToFile WriteToFile;
fptrDelete Delete;
}Person;
person* new_Person(const char* const pFirstName,
const char* const pLastName); void delete_Person(Person* const pPersonObj);
void Person_DisplayInfo(Person* const pPersonObj);
void Person_WriteToFile(Person* const pPersonObj, const char* pFileName);
The new_Person()
function acts as a constructor. The function returns the newly created instance of the structure. It initializes the interface of the function pointers to access other member functions. One thing to note here is, we are defining only those function pointers which are available for public access. We have not given access to private functions in the interface. Let us take a look at the new_Person()
function or the constructor of our Person
class in C.
person* new_Person(const char* const pFirstName, const char* const pLastName)
{
Person* pObj = NULL;
pObj = (Person*)malloc(sizeof(Person));
if (pObj == NULL)
{
return NULL;
}
pObj->pFirstName = malloc(sizeof(char)*(strlen(pFirstName)+1));
if (pObj->pFirstName == NULL)
{
return NULL;
}
strcpy(pObj->pFirstName, pFirstName);
pObj->pLastName = malloc(sizeof(char)*(strlen(pLastName)+1));
if (pObj->pLastName == NULL)
{
return NULL;
}
strcpy(pObj->pLastName, pLastName);
pObj->Delete = delete_Person;
pObj->Display = Person_DisplayInfo;
pObj->WriteToFile = Person_WriteToFile;
return pObj;
}
After the object creation, we can access its data members and functions.
Person* pPersonObj = new_Person("Anjali", "Jaiswal");
pPersonObj->Display(pPersonObj);
pPersonObj->WriteToFile(pPersonObj, "persondata.txt");
pPersonObj->Delete(pPersonObj);
pPersonObj = NULL;
Note: Unlike in C++, in C, we cannot access data members directly in the functions. In C++, data members can be directly accessed implicitly through the “this
” pointer. As we do not have the “this
” pointer in C, we pass the object explicitly to the member function. For accessing the data members of a class in C, we need to pass the calling object as an argument to the function. In the above example, we are passing the calling object as a first argument to the function. This way, the function can access the data members of the object.
Representation of the class in C
The Person
class representation - check the initialization of the interface to point to the member functions:
Simple example of inheritance and polymorphism
Inheritance - class Employee
derived from class Person
:
In the above example, class Employee
inherits the properties of class Person
. As DisplayInfo()
and WriteToFile()
functions are virtual
, we can access the same functions for the Employee
object from the Person
instance. For this, we need to initiate the Person
instance with the Employee
class. This is possible because of polymorphism. In the case of polymorphism, to resolve the function call, C++ makes use of the VTable, which is nothing but a table of pointers to functions.
The interface of pointers to functions that we are maintaining in the structure works similar to the VTable.
Person PersonObj("Anjali", "Jaiswal");
Employee EmployeeObj("Gauri", "Jaiswal", "HR", "TCS", 40000);
Person* ptrPersonObj = NULL;
ptrPersonObj = &PersonObj;
ptrPersonObj ->Display();
ptrPersonObj ->WriteToFile("persondata.txt");
ptrPersonObj = &EmployeeObj;
ptrPersonObj ->Display();
ptrPersonObj ->WriteToFile("employeedata.txt");
In C, inheritance can be achieved by maintaining a reference to the base class object in the derived class object. With the help of the base class' instance, we can access the base data members and functions. However, in order to achieve polymorphism, the base class object should be able to access the derived class object’s data. For this, the base class should have access rights to the derived class’ data members.
For implementing the virtual function, the signature of the derived class’ function has to be similar to the base class’ function pointers. That means the derived class function will take the instance of the base class as a parameter. We are maintaining the reference to the derived class in the base class. During function implementation, we can access the actual derived class data from the reference of the derived class.
Equivalent representation in C structures
Inheritance in C - Person
and Employee
structure in C:
As shown in the diagram, we have declared a pointer in the base class structure holding the derived class object, and a pointer in the derived class structure holding the base class object.
In the base class object, the function pointers point to the virtual functions of its own class. In the derived class object’s construction, we need to make the interface of the base class to point to the member function of the derived class. This gives us the flexibility of calling the derived class function through the base class object (polymorphism). For more details, check the construction on the Person
and Employee
objects.
When we talk about polymorphism in C++, there is a problem of object destruction. For the proper cleanup of objects, it uses virtual destructors. In C, this can be done by making the delete function pointer of the base class to point to the derived class destructor function. The derived class' destructor cleans up the derived class data as well as the base class data and object. Note: Check the sample source code for details about the implementation of the virtual destructor and the virtual functions.
Creating the Person object
typedef struct _Person Person;
typedef void (*fptrDisplayInfo)(Person*);
typedef void (*fptrWriteToFile)(Person*, const char*);
typedef void (*fptrDelete)(Person*) ;
typedef struct _person
{
void* pDerivedObj;
char* pFirstName;
char* pLastName;
fptrDisplayInfo Display;
fptrWriteToFile WriteToFile;
fptrDelete Delete;
}person;
Person* new_Person(const char* const pFristName,
const char* const pLastName); void delete_Person(Person* const pPersonObj);
void Person_DisplayInfo(Person* const pPersonObj);
void Person_WriteToFile(Person* const pPersonObj, const char* const pFileName);
Person* new_Person(const char* const pFirstName, const char* const pLastName)
{
Person* pObj = NULL;
pObj = (Person*)malloc(sizeof(Person));
if (pObj == NULL)
{
return NULL;
}
pObj->pDerivedObj = pObj;
pObj->pFirstName = malloc(sizeof(char)*(strlen(pFirstName)+1));
if (pObj->pFirstName == NULL)
{
return NULL;
}
strcpy(pObj->pFirstName, pFirstName);
pObj->pLastName = malloc(sizeof(char)*(strlen(pLastName)+1));
if (pObj->pLastName == NULL)
{
return NULL;
}
strcpy(pObj->pLastName, pLastName);
pObj->Delete = delete_Person;
pObj->Display = Person_DisplayInfo;
pObj->WriteToFile = Person_WriteToFile;
return pObj;
}
Struture of the Person object
Creating the Employee object
#include "Person.h"
typedef struct _Employee Employee;
typedef struct _Employee
{
Person* pBaseObj;
char* pDepartment;
char* pCompany;
int nSalary;
}Employee;
Person* new_Employee(const char* const pFirstName, const char* const pLastName,
const char* const pDepartment, const char* const pCompany,
int nSalary); void delete_Employee(Person* const pPersonObj);
void Employee_DisplayInfo(Person* const pPersonObj);
void Employee_WriteToFile(Person* const pPersonObj, const char* const pFileName);
Person* new_Employee(const char* const pFirstName, const char* const pLastName,
const char* const pDepartment,
const char* const pCompany, int nSalary)
{
Employee* pEmpObj;
Person* pObj = new_Person(pFirstName, pLastName);
pEmpObj = malloc(sizeof(Employee));
if (pEmpObj == NULL)
{
pObj->Delete(pObj);
return NULL;
}
pObj->pDerivedObj = pEmpObj;
pEmpObj->pDepartment = malloc(sizeof(char)*(strlen(pDepartment)+1));
if(pEmpObj->pDepartment == NULL)
{
return NULL;
}
strcpy(pEmpObj->pDepartment, pDepartment);
pEmpObj->pCompany = malloc(sizeof(char)*(strlen(pCompany)+1));
if(pEmpObj->pCompany== NULL)
{
return NULL;
}
strcpy(pEmpObj->pCompany, pCompany);
pEmpObj->nSalary = nSalary;
pObj->Delete = delete_Employee;
pObj->Display = Employee_DisplayInfo;
pObj->WriteToFile = Employee_WriteToFile;
return pObj;
}
Struture of the Employee object
Note: Changed the pointing position of the interface (VTable) from base class functions to derived class functions. Now we can access the derived class functions from the base class (polymorphism). Let us now look at how we can use polymorphism.
Person* PersonObj = new_Person("Anjali", "Jaiswal");
Person* EmployeeObj = new_Employee("Gauri", "Jaiswal","HR", "TCS", 40000);
PersonObj->Display(PersonObj);
PersonObj->WriteToFile(PersonObj,"persondata.txt");
PersonObj->Delete(PersonObj);
EmployeeObj->Display(EmployeeObj);
EmployeeObj->WriteToFile(EmployeeObj, "employeedata.txt");
EmployeeObj->Delete(EmployeeObj);
Conclusion
Involving the above described simple addition of code can grant the procedural C language a flavor of polymorphism and inheritance. We simply use function pointers for creating a VTable and maintain cross references of objects for accessibility among base and derived objects. With these simple steps, we can implement Inheritance and Polymorphism in C.