As a follow up to my previous
post where I demonstrated the 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)
Lines 5-11:
Animal class, with an attribute
description and methods
move,
make_sound,
destroy and
init.
Lines 13-38: Function definitions for the above Animal methods.
Lines 40-55: The object creation function. Takes in an
Animal prototype, assigns methods and allocates heap memory for the object.
Line 47: The memory space allocated depends on the parameter size passed to the function. This is useful if the object is a sub-class of
Animal with a bigger size. More on this later.
Line 48: Copy the contents of the prototype into the allocated space.
Line 57: Macro definition to simplify object creation syntax.
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
};
Lines 1-4:
Human class with
Animal attributes and methods (
proto), and an extra attribute
species.
Lines 6-25: Function definitions for
Human methods
move,
sound and
init to override the base class definitions in
Animal.
Lines 23-27:
Animal prototype for
Human object, with overriding methods assigned. This will be passed to the object creation function.
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 the each 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".