icon

KE Programmer

Object-Oriented Programming in C
Published on Dec 09, 2012 20:55 by KE Programmer

As a follow up to my previous post where I demonstrated how to create pointers to functions in C, in this post we'll look at how to implement simple object-oriented programming in C.

The basic idea in object-oriented programming is encapsulating data and operations on the data into a single structure. Since we can create pointers to functions, we can therefore create structures that contain both data variables and functions that manipulate these data variables. We shall also see a basic form of inheritance.

In the following example, we'll create a base Animal class, and from it derive a Human class and Duck class. Take the terms class, method and attribute as used below with a pinch of salt. Object-oriented programming is implemented in different ways in the major programming languages in use.

We first create the Animal class:

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

typedef struct {
  char *description;
  void (*move)(void *self, int steps);
  void (*make_sound)(void *self);
  void (*destroy)(void *self);
  int (*init)(void *self, const char *description);
} Animal;

void Animal_move(void *self, int steps)
{
  printf("Moves %d steps\n", steps);
}

void Animal_sound(void *self)
{
  printf("Makes a sound\n");
}

void Animal_destroy(void *self)
{
  Animal *this = self;
  if (this) {
    if (this->description) free(this->description);
    free(this); 
  }
}

int Animal_init(void *self, const char *description)
{
  Animal *this = self;
  this->description = strdup(description);
  return 1;
}

void *Animal_new(Animal proto, size_t size, const char *description)
{                                                                                                                 
  if (!proto.move) proto.move = Animal_move;
  if (!proto.make_sound) proto.make_sound = Animal_sound;
  if (!proto.destroy) proto.destroy = Animal_destroy;
  if (!proto.init) proto.init = Animal_init;

  Animal *animal = calloc(1, size);
  *animal = proto; 

  int initval = animal->init(animal, description);
  if (initval)
    return animal;
  else
    return NULL;
}

#define NEW(N, d) Animal_new(N##Proto, sizeof(N), d)

We then look at the implementation of the Human class:

typedef struct {
  Animal proto;
  char* species;
} Human;

void Human_move(void *self, int steps)
{
  printf("walkes %d strides\n", steps);
}

void Human_sound(void *self)
{
  printf("Talks\n");
}

int Human_init(void *self, const char *species)
{
  Human *h = self;
  h->species = strdup(species);
  return 1;
}

Animal HumanProto = {
  .init = Human_init,
  .make_sound = Human_sound,
  .move = Human_move 
};

The same implementation for the Duck class:

typedef struct {
  Animal proto;
  char *species;
} Duck;

void Duck_move(void *self, int steps)
{
  printf("wobbles %d steps\n", steps);
}

void Duck_sound(void *self)
{
  printf("Quacks");
}

int Duck_init(void *self, const char *species)
{
  Duck *d = self;
  d->species = strdup(species);
  return 1;
}

Animal DuckProto = {
  .move = Duck_move,
  .make_sound = Duck_sound,
  .init = Duck_init 
};

And finally, the main function, which creates an object for each of the three classes:

int main()
{
  Animal AnimalProto = {};
  Animal *animal = NEW(Animal, "An animal");
  Human *human = NEW(Human, "Homo sapien");
  Duck *duck = NEW(Duck, "Anas rubripes");
  printf("%s ", animal->description);
  animal->move(animal, 5);
  animal->make_sound(animal);
  printf("%s ", human->species);
  human->proto.move(human, 5);
  human->proto.make_sound(human);
  printf("%s ", duck->species);
  duck->proto.move(duck, 5);
  duck->proto.make_sound(duck);
  return 0;
}

We shall walk through creation of the Human object to get the gist of the program.

We first create an Animal prototype HumanProto for the Human object:

Animal HumanProto = {
  .init = Human_init,
  .make_sound = Human_sound,
  .move = Human_move 
};

The prototype is initialized with the Human init, sound and move functions.

The invocation of the NEW macro

NEW(Human, "Homo sapien");

is transformed to:

Animal_new(HumanProto, sizeof(Human), "Homo sapien");

The Animal_new function is called with HumanProto which carries with it the overriding functions. However, the size passed to it is that of type Human which is bigger than that of HumanProto (type Animal).

When execution gets to the statement

Animal *animal = calloc(1, size);

in the Animal_new function, the calloc function returns a pointer to a memory space of the size of type Human.

*animal = proto;

copies the contents of the prototype of HumanProto to the allocated memory space. Since the size of allocated space is larger than the size of the prototype, a space that exactly fits the attribute species (a pointer) of the Human class remains.

The statement:

animal->init(animal, "Homo sapien");

then initializes the Human object's species attribute with the string "Homo sapien".

[Previously published here]