Object-oriented programming in C

Object orientation is a feature that the C language does not have. But it is possible to organize a C to behave as an object-oriented code. Here is one easy way of doing it by establishing a code style.

Use a name pattern to identify the object-oriented style: Pascal-case for classes and snake-case for methods. Make a structure named after the class to store the internal state of the objects. Give private variables an underline at the beginning of their names. Identify functions that work as class methods by the class name, an underline, and the method name. The first parameter should be a pointer to the structure, named self. The self is an instance of the class. For protected methods, make static functions.

typedef struct
{
  int x, y, _private_variable;
} MyClass;

void
MyClass_public_method(MyClass *self);

void
MyClass_another_public_method(MyClass *self);
#include "example01.h"

static void
MyClass_protected_method(MyClass *self)
{
  // Do something.
}

void
MyClass_public_method(MyClass *self)
{
  // Do something.
}

void
MyClass_another_public_method(MyClass *self)
{
  // Do something.
}

One of the features of object-oriented programming is automated memory management. Languages like C++, C#, Ruby, and Python use constructors and destructors for memory management. Because C does not have such functionalities, constructors should be explicitly called every time an object is created and the destructor after using the structure.

Use a pointer to a base structure inside the derived for inheritance. To make a polymorphic call, use the base class instance. Manually doing memory management solves a common problem in C++ called the “deadly diamond.” The deadly diamond happens when a class inherits two others with the same base class. For example, D inherits from B and C, and B and C inherits from A. So, when using a value from A in D, the D can use A from B or C. C++ creates two different instances of A for D, causing the deadly diamond. Because the C language gives more memory management control, it is possible to fix this problem by making both B and C instances point to the same A instance.

struct A
{
  char *text;
};

void
A_constructor(struct A *self, const char *text);

void
A_destructor(struct A *self);

struct B
{
  struct A *a;
};

void
B_constructor(struct B *self);

void
B_destructor(struct B *self);

struct C
{
  struct A *a;
};

void
C_constructor(struct C *self);

void
C_destructor(struct C *self);

struct D
{
  struct B *b;
  struct C *c;
};

void
D_constructor(struct D *self);

void
D_destructor(struct D *self);
#include "oop.h"
#include <stdio.h>

int
main(void)
{
  struct D object;
  printf("\n\x1b[32m%s\x1b[0m\n", "Calling constructor D:");
  D_constructor(&object);
  printf("\n\x1b[32m%s\x1b[0m\n", "Calling destructor D:");
  D_destructor(&object);

  return 0;
}
#include "oop.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void
A_constructor(struct A *self, const char *text)
{
  printf("%s\n", "Constructor A");
  self->text = malloc(sizeof(char) * (strlen(text) + 1));
  strcpy(self->text, text);
}

void
A_destructor(struct A *self)
{
  printf("%s\n", "Destructor A");
  free(self->text);
}

void
B_constructor(struct B *self)
{
  printf("%s\n", "Constructor B");
  self->a = malloc(sizeof(struct A));
  A_constructor(self->a, "Object B");
}

void
B_destructor(struct B *self)
{
  printf("%s\n", "Destructor B");
  A_destructor(self->a);
  free(self->a);
}

void
C_constructor(struct C *self)
{
  printf("%s\n", "Constructor C");
  self->a = malloc(sizeof(struct A));
  A_constructor(self->a, "Object C");
}

void
C_destructor(struct C *self)
{
  printf("%s\n", "Destructor C");
  A_destructor(self->a);
  free(self->a);
}

void
D_constructor(struct D *self)
{
  printf("%s\n", "Constructor D");
  self->b = malloc(sizeof(struct B));
  B_constructor(self->b);
  self->c = malloc(sizeof(struct C));
  C_constructor(self->c);

  // Make both, B and C point to the same A.
  A_destructor(self->c->a);
  free(self->c->a);
  self->c->a = self->b->a;
}

void
D_destructor(struct D *self)
{
  // Create a new A for destructor C to function normally.
  printf("%s\n", "Destructor D");
  self->c->a = malloc(sizeof(struct A));
  A_constructor(self->c->a, "Object C");

  B_destructor(self->b);
  free(self->b);
  C_destructor(self->c);
  free(self->c);
}