Writing Object-Oriented Code In C: Abstraction and Encapsulation

In this post, we will discuss about ways for using the C programming language to write Object-Oriented code, a feat that would seem impossible at first glance because C was never intended to be used for OOP when it was first designed.

You should be asking, wouldn't we be better off by using C++ or other Object Oriented language? That is a very valid question and I completely agree with you, so why bother? Well, first of all, it is a very interesting topic and we will be getting deeper into the inner workings of C and Object Oriented Programming. This is a great way of improving your knowledge of OOP and your programming skills as a whole. This is without saying that, in some cases, this could come in handy when developing on constrained environments that only support C, like some basic microcontrollers, or operating system programming (For example, Linux Kernel Drivers are written in C).

Let's start by listing the main OOP principles:

  1. Encapsulation
  2. Abstraction
  3. Inheritance
  4. Polymorphism

We will only be covering Abstraction and Encapsulation in this post.

Abstraction

In it's most basic form, OOP is a programming paradigm that organizes code in "objects", which group data and code together. Objects are often instances of classes, which describe their intended structure and define their type.

For example, let's say that we have a cat called Luke, Luke is a Cat, so we could say that Luke is an instance of the class Cat.

For example, in C++ we would define the class Cat as follows:

Cat.h (C++)

#include <iostream>
class Cat {
public:
    // These are the properties of the cat
    char name[23];
    unsigned int age;
    
    // This is the constructor
    Cat();
    // These are the methods of the cat
    void walk(int speed);
    void jump(int height);
    void meow(int volume);
    void purr(int intensity);    
};

Then we would implement the methods in cat.cpp as follows:

Cat.cpp

#include "Cat.h"

Cat::Cat()
{
    // Set default properties
    /* 
     Note the "this" keyword, 
     it points to the current object to which the method was called.
     (We could have omitted "this->" as it is optional in C++)
    */
    this->name = "unnammed kitten";
    this->age = 0;
}

void Cat::walk(int speed)
{
    // Implementation goes here
}
...

Then we would instantiate a cat in our main code:

main.cpp

#include <iostream>
#include "cat.h"

Cat *luke;

int main()
{
    luke = new Cat();
    luke->name = "Luke";
    luke->age = 1;
    luke->purr(9000);
    delete luke;
    return 0;
}

Now, How would we do this in C? In C we don't have classes, however, we have structs, that are a way of grouping variables together.

So, let's start by defining a Cat struct in C:

#include <stdio.h>
#include <stdint.h>

typedef struct Cat {
    char name[23];
    unsigned int age;
} Cat;

So now we have the Cat properties grouped together in C, however, we don't have an easy way of grouping functions into a struct. The solution that I'll suggest now is to define some conventions on function naming:

  1. Let's prepend every method with the name of the class and a _, in this case, that would be Cat_<method name>
  2. To replicate the "this" keyword functionality, let's make every method receive a pointer of an instance of the struct as the first argument. We will choose "self" as our keyword so we don't conflict with C++ keywords, this way we are making our code interoperable with C++ code.
  3. Every class will have a constructor method called <class name>_new and a destructor called <class name>_delete.
  4. We will always create an instance by calling the constructor method.
  5. We will always use pointers to reference instances. dynamic memory management will be handled by the constructor and destructor functions.

So, cat.h would end up like:

cat.h (C)

#include <stdio.h>
#include <stdint.h>

typedef struct Cat {
    char name[23];
    unsigned int age;
} Cat;

// Constructor
Cat *Cat_new();

// Destructor
void Cat_delete(Cat *self);

// Methods
void Cat_walk(Cat *self, int speed);
void Cat_jump(Cat *self, int height);
void Cat_meow(Cat *self, int volume);
void Cat_purr(Cat *self, int intensity);

Then we would implement the methods in cat.c as follows:

cat.c

#include "cat.h"

Cat *Cat_new()
{
    Cat *self = malloc(sizeof(Cat));
    self->name = "unnammed kitten";
    self->age = 0;
    return self;
}

void Cat_delete(Cat *self)
{
    free(self);
}
...

Then we would instantiate a cat in our main code:

main.c

#include <stdio.h>
#include "cat.h"

Cat *luke;

int main()
{
    luke = Cat_new();
    luke->name = "Luke";
    luke->age = 1;
    Cat_purr(luke, 9000);
    Cat_delete(luke); // :'(
    return 0;
}

Not too bad, eh? However, we haven't covered two important encapsulation topics:

  1. Private methods and properties
  2. Static methods and properties

Encapsulation

Private and Static methods and properties

Private and Static methods are easy; for a Private method we would just declare and implement it in the .c file and not the .h, so it will only be accessible from within the .c file. For Static methods, those would be methods that don't receive the "self" pointer to the struct of our Class.

For example, let's define a private "run" method that is called when the speed passed to the "walk" function is greater than 5. Also, let's define a static method that returns the number of legs of a cat:

cat.h (C)

#include <stdio.h>
#include <stdint.h>

typedef struct Cat {
    char name[23];
    unsigned int age;
} Cat;

// Constructor
Cat *Cat_new();

// Destructor
void Cat_delete(Cat *self);

// Methods
void Cat_walk(Cat *self, int speed);
void Cat_jump(Cat *self, int height);
void Cat_meow(Cat *self, int volume);
void Cat_purr(Cat *self, int intensity);

// Static method, note that it doesn't receive self.
int Cat_nlegs();

cat.c

#include "cat.h"

Cat *Cat_new()
{
    Cat *self = malloc(sizeof(Cat));
    self->name = "unnammed kitten";
    self->age = 0;
    return self;
}

void Cat_delete(Cat *self)
{
    free(self);
}

// Static method
int Cat_nlegs()
{
    return 4;
}

// Private method
void Cat_run(Cat *self)
{
    ...
}

void Cat_walk(Cat *self, int speed)
{
    // Calling a private method
    if(speed > 5) Cat_run(self);
    else ...
}

...

Now, private and static properties are harder to implement in a way that "looks and feels" similar to C++, for example, static properties could be defined as global variables in the .h of the class, and we could apply the same convention for naming as the functions, so we could have an int Cat_nears = 2; defined in the .h and that would be "shared" between all the "instances", however accessing it wouldn't be like in C++, where we could for example do this:

C++

// Accessing a normal property
printf("%s\n", luke->name);
// Accessing a static property
printf("%d\n", luke->nears);

Instead we would have to do:

C

// Accessing a normal property
printf("%s\n", luke->name);
// Accessing a static property
printf("%d\n", Cat_nears);

So it wouldn't be as straightforward.

Private properties are harder, How would we hide a property, part of the struct, to the outside world?

The first thing that comes into mind is adopting a convention, like prepending an underscore to all "private" properties, however, this is not 100% hidden because one could still access the property ignoring the convention. There could be other, more involved ways of doing this, but I'll keep this as an exercise to the reader ;).