Simulating object oriented programming (OOP) in C
Most of the languages support OOP out of the box, but many people are not aware that OOP and even functional programming can be done also in C (even though it is not very practical, unless we use some macros to hack our way through and make it more convenient by reducing the boilerplate code).
In this tutorial I will provide some insight on how to simulate an object in C language in order to write in OOP style. Before let us review again the definition of a class and object in OOP.
Contents
What is a class in OOP ?
A class is a blueprint that describes the contents of the objects that belong to it. It describes the data fields (also known as instance variables), and defines the operations (functions or methods).
Depending on the programming language, the data fields and operations can have different access modifiers, usually private, protected and public. The role of the access modifiers is to provide a way for the programmer to enforce encapsulation of the data (hiding implementation details from the user).
What is an object in OOP ?
An object in its simplest terms is an instance of a class, a “living” instance of the blueprint known as class. It contains its own set of instance data fields, and shares the methods with all the other instances of the same class.
Simulating objects in C
From the definitions above, we realize that we need to find a way to encapsulate related data fields and methods to define a class and then instantiate objects of this particular class.
In C we can group related data fields by using structures, so this part is already covered. What is left is to find a way to provide methods to our structures. This can be done by using function pointers.
Let us see an example. I will create a class named Faker, which will be responsible for generating dummy data that are going to be used for testing.
The header file faker.h looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// faker.h #ifndef faker_H_ #define faker_H_ typedef struct Faker Faker; //forward declaration to allow self referencing in the struct declaration struct Faker { int minAge; char* (*address) (); char* (*email) (); int age(Faker* self) }; Faker* newFaker(int minAge); #endif |
The function newFaker will serve as the constructor, it will be responsible for initializing an instance of the Faker class.
The header serves a very important role: provides a public interface to the other parts of the system. This means that the implementation is up to us, and we can enforce the encapsulation and hide the way its functionality is implemented (especially the private variables necessary to provide the desired functionality).
The faker implementation file faker.c looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include <stdlib.h> #include <string.h> #include "faker.h" char* pickRandomFromArray(const char* array[], int size) { int randomIndex = rand() % size; return (char*) array[randomIndex]; } const char* addressesPool[] = { "Forest Run Way 72", "Vera Parkway 70", "3 East Sulphur Springs St. Murfreesboro, TN 37128" }; const int NR_ADDRESSES = (sizeof (addressesPool) / sizeof (const char *)); const char* emailsPool[] = { "asommomepu-9364@yopmail.com", "iArmstrong@Lazzy.com", "ut@Gabspot.biz" }; const int NR_EMAILS = (sizeof (emailsPool) / sizeof (const char *)); char* address() { return pickRandomFromArray(addressesPool, NR_ADDRESSES); } char* email() { return pickRandomFromArray(emailsPool, NR_EMAILS); } int age(Faker* self) { return rand() % 120 + self->minAge + 1; } Faker* newFaker() { Faker* self = (Faker*)malloc(sizeof(Faker)); self->address = &address; self->email = &email; self->age = &age; return self; } |
An important point to notice here is how we bind the instance to its methods. We are passing Faker* self to the age method, and then we can access the minAge property.
Another important point to take away is the way we are hiding the implementation details. The variables addressesPool and emailsPool will not be accessible by the clients of the faker.h interface. So we provide the desired functionality, while we hide the implementation details. We can as well compile the faker library to a shared runtime library so the source code will not be visible.
The client of the faker, can look like:
1 2 3 4 5 6 7 8 9 10 11 12 |
// main.c #include <stdio.h> #include "faker.h" int main() { Faker* faker = newFaker(18); printf("Random age %d\n", faker->age(faker)); printf("Random address: %s\n", faker->address()); printf("Random email: %s\n", faker->email()); } |
I usually use Faker to seed random users and populate a database, or create random users for testing certain functionalities of my software.
This was just a short introduction to OOP in C.
If you have any questions, feel free to comment below.